Owlmetry
SDKsNode.js SDK

Serverless

Use wrapHandler() to guarantee event flush in Lambda, Vercel Serverless Functions, Firebase Cloud Functions, and other serverless environments.

In serverless environments, your function may terminate before the SDK's background flush timer fires. Owl.wrapHandler() solves this by flushing all buffered events after every invocation.

The SDK targets the Node.js runtime (AWS Lambda, Vercel Serverless Functions on Node, Firebase Cloud Functions v2, etc.). It is not compatible with Edge runtimes such as Vercel Edge Functions or Cloudflare Workers -- it depends on Node built-ins like node:crypto, node:zlib, and node:fs.

The Problem

The SDK buffers events and flushes them on a timer (default every 5 seconds) or when the buffer reaches a threshold (default 20 events). In a traditional server, this works well because the process is long-lived. In serverless:

  • The function may return and freeze before the timer fires
  • Buffered events are lost when the execution context is recycled
  • The beforeExit handler may not fire reliably in all runtimes

wrapHandler

Owl.wrapHandler() wraps an async function so that Owl.flush() is called automatically after every invocation, regardless of whether it succeeded or threw:

import { Owl } from "@owlmetry/node";

Owl.configure({
  endpoint: "https://ingest.owlmetry.com",
  apiKey: "owl_client_...",
  serviceName: "order-lambda",
});

export const handler = Owl.wrapHandler(async (event, context) => {
  Owl.info("Processing order", { order_id: event.orderId });

  const op = Owl.startOperation("process-order");
  try {
    const result = await processOrder(event.orderId);
    op.complete();
    return { statusCode: 200, body: JSON.stringify(result) };
  } catch (err) {
    op.fail(err.message);
    return { statusCode: 500, body: "Internal error" };
  }
});

The wrapper preserves the return value and re-throws any exceptions after flushing. It works with any async function signature -- Lambda handlers, Vercel serverless functions, or custom wrappers.

Owl.flush() also awaits any pending file attachments, so wrapHandler() delivers attached files before the runtime freezes.

Firebase Cloud Functions

Firebase's onCall (and similar wrappers) expect a strongly-typed callback. Because wrapHandler uses generic rest parameters, TypeScript sometimes cannot infer the handler's parameter type -- annotate it explicitly:

import { onCall, type CallableRequest } from "firebase-functions/v2/https";
import { Owl } from "@owlmetry/node";

export const myFunction = onCall(
  Owl.wrapHandler(async (request: CallableRequest) => {
    const { data, auth } = request;
    Owl.info("Function invoked", { uid: auth?.uid });
    // ...
    return { ok: true };
  })
);

beforeExit Handler

When you call Owl.configure(), the SDK registers a process.on("beforeExit") handler that flushes any remaining buffered events. This is a safety net for graceful process shutdown but should not be relied on in serverless -- use wrapHandler() instead.

The handler is registered once and does not prevent the process from exiting (the flush timer uses unref() to avoid keeping the event loop alive).

Graceful Shutdown

For long-running servers, call Owl.shutdown() to flush events and stop the background timer:

process.on("SIGTERM", async () => {
  console.log("Shutting down...");
  await Owl.shutdown();
  server.close();
  process.exit(0);
});

Owl.shutdown() emits a sdk:session_ended event, flushes all buffered events, and clears internal state.

Express Middleware

In Express, you can use wrapHandler() per route or build middleware that flushes after every request:

import express from "express";
import { Owl } from "@owlmetry/node";

Owl.configure({
  endpoint: "https://ingest.owlmetry.com",
  apiKey: "owl_client_...",
  serviceName: "express-api",
});

const app = express();

// Flush after every request
app.use((req, res, next) => {
  res.on("finish", () => {
    Owl.flush().catch(() => {});
  });
  next();
});

app.post("/api/orders", async (req, res) => {
  Owl.info("Order received");
  // ...
  res.json({ ok: true });
});

Fastify Hooks

In Fastify, use the onResponse hook to flush after each request:

import Fastify from "fastify";
import { Owl } from "@owlmetry/node";

Owl.configure({
  endpoint: "https://ingest.owlmetry.com",
  apiKey: "owl_client_...",
  serviceName: "fastify-api",
});

const app = Fastify();

app.addHook("onResponse", async () => {
  await Owl.flush();
});

app.addHook("onClose", async () => {
  await Owl.shutdown();
});

Complete Serverless Example

An AWS Lambda function with user scoping, operation tracking, and automatic flush:

import { Owl } from "@owlmetry/node";

Owl.configure({
  endpoint: "https://ingest.owlmetry.com",
  apiKey: "owl_client_...",
  serviceName: "checkout-lambda",
  appVersion: "2.1.0",
});

export const handler = Owl.wrapHandler(async (event) => {
  const body = JSON.parse(event.body);
  const owl = Owl.withUser(body.userId);

  const op = owl.startOperation("checkout", {
    item_count: String(body.items.length),
  });

  try {
    const charge = await processPayment(body);
    owl.step("payment-completed");
    op.complete({ charge_id: charge.id });

    return { statusCode: 200, body: JSON.stringify({ ok: true }) };
  } catch (err) {
    op.fail(err.message);
    return { statusCode: 500, body: JSON.stringify({ error: "Payment failed" }) };
  }
});

Every event -- the operation start, funnel step, and operation complete/fail -- is flushed before the Lambda returns.

Ready to get started?

Connect your agent via MCP or CLI and start tracking.