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.
