Owlmetry
SDKsNode.js SDK

Identity

Manage anonymous and known user identities in the Node.js SDK.

The Node.js SDK provides Owl.withUser() to associate events with a specific user. This is the primary mechanism for per-user analytics, funnel tracking, and session tracing.

withUser

Owl.withUser(userId) returns a ScopedOwl instance. Every event emitted through this instance includes the given user_id:

const owl = Owl.withUser("user_42");
owl.info("Profile updated");
owl.warn("Rate limit approaching");
owl.step("onboarding-complete");

The returned ScopedOwl has the same API as Owl -- info(), debug(), warn(), error(), step(), startOperation(), recordMetric(), and setUserProperties(). It can be further narrowed with .withSession(sessionId) for session scoping.

Middleware Pattern

In HTTP servers, create a scoped instance per request based on the authenticated user. This is the recommended approach for associating backend events with users.

Express

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

// Extend the Request type
declare global {
  namespace Express {
    interface Request {
      owl: typeof Owl | ScopedOwl;
    }
  }
}

const app = express();

app.use((req, res, next) => {
  req.owl = req.auth?.userId
    ? Owl.withUser(req.auth.userId)
    : Owl;
  next();
});

app.get("/api/profile", (req, res) => {
  req.owl.info("Viewed profile");
  res.json(req.user);
});

app.post("/api/settings", (req, res) => {
  req.owl.info("Updated settings", { theme: req.body.theme });
  res.json({ ok: true });
});

Fastify

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

const app = Fastify();

app.decorateRequest("owl", null);

app.addHook("onRequest", async (request) => {
  request.owl = request.user?.id
    ? Owl.withUser(request.user.id)
    : Owl;
});

app.get("/api/orders", async (request, reply) => {
  request.owl.info("Listing orders");
  const orders = await getOrders(request.user.id);
  return orders;
});

withSession

By default, a Node SDK process generates one session ID in Owl.configure() and stamps every event with it. That works for a single long-running worker, but it isn't useful for a web server that handles many clients — all backend events end up in one giant session that doesn't line up with any client session.

Owl.withSession(sessionId) returns a ScopedOwl that overrides the session ID for every event it emits:

const owl = Owl.withSession("client-session-abc123");
owl.info("Processed request");

This is most commonly paired with withUser to scope a full request handler:

const owl = Owl.withSession(clientSessionId).withUser(userId);
owl.info("Order created", { orderId });

The scopes chain in either order — Owl.withUser(uid).withSession(sid) produces the same result.

sessionId should be a UUID string (the Swift SDK's Owl.sessionId already is). Non-UUID values are silently ignored and the scope falls back to the SDK's default session ID — so a malformed or missing header from a client will never crash the request handler. Enable debug: true on Owl.configure() to see warnings when invalid values are received.

Cross-SDK Session Correlation

The typical use case is linking backend events to the session of the client that triggered them. Swift clients expose their current session ID via Owl.sessionId; have the client send it as a request header, then scope the handler on the server:

Swift client:

var request = URLRequest(url: apiURL)
if let sessionId = Owl.sessionId {
    request.setValue(sessionId, forHTTPHeaderField: "X-Owl-Session-Id")
}

Node server (Fastify):

app.addHook("onRequest", async (request) => {
  const clientSessionId = request.headers["x-owl-session-id"] as string | undefined;
  const base = request.user?.id ? Owl.withUser(request.user.id) : Owl;
  request.owl = clientSessionId ? base.withSession(clientSessionId) : base;
});

Every event emitted through request.owl — plus any operations or funnel steps started from it — now shares the client's session ID, so the dashboard's session view shows both sides of the transaction together.

Per-call override

For callers that can't scope (e.g. a utility that's handed a sessionId but not a ScopedOwl), info/debug/warn/error accept a sessionId in their options:

Owl.info("Webhook received", { provider: "stripe" }, { sessionId: clientSessionId });

When both a scope and a per-call options.sessionId are present, the per-call value wins.

Anonymous vs Known Users

When no user ID is set (i.e., you use Owl directly instead of Owl.withUser()), events are emitted without a user_id field. They are still grouped by session_id, but cannot be attributed to a specific user.

For backend services that handle both authenticated and unauthenticated requests, the middleware pattern above handles this naturally -- unauthenticated requests fall through to the base Owl instance.

User Scoping with Operations

Operations started from a scoped instance carry the user ID across all lifecycle events (start, complete, fail, cancel):

const owl = Owl.withUser(userId);
const op = owl.startOperation("data-export");

try {
  const result = await exportUserData(userId);
  op.complete({ rows: String(result.rowCount) });
} catch (err) {
  op.fail(err.message);
}

Both the start and complete (or fail) events will have user_id set.

User Scoping with Funnels

Funnel analytics correlate steps by user ID. Always use a scoped instance when tracking funnel steps:

const owl = Owl.withUser(userId);
owl.step("signup-started");
// ... later
owl.step("email-verified");
owl.step("first-project-created");

Without a user ID, the server cannot determine which user completed which steps. See the Funnels guide for more backend funnel patterns.

Ready to get started?

Connect your agent via MCP or CLI and start tracking.