Sessions
Master telemetry sessions — actor/scope binding, scoped execution with AsyncLocalStorage, session lifecycle, and real-world patterns for HTTP request handlers and background jobs.
Sessions
Sessions are the context correlation engine of telemetry. They bind actor, scope, and attributes to events — automatically propagating context across async boundaries using Node.js AsyncLocalStorage.
Why Sessions?
Without sessions, you must manually pass user, tenant, and request context to every emit() call. With sessions, you set context once and all subsequent events inherit it:
// ❌ Without sessions — repetitive context passing
telemetry.emit("step.one", {
actor: { type: "user", id: "usr_123" },
scope: { type: "organization", id: "org_456" },
attributes: { "ctx.request.id": "req_abc" },
});
telemetry.emit("step.two", {
actor: { type: "user", id: "usr_123" }, // Repeated!
scope: { type: "organization", id: "org_456" }, // Repeated!
attributes: { "ctx.request.id": "req_abc" }, // Repeated!
});
// ✅ With sessions — set once, inherited everywhere
await telemetry.session()
.actor("user", "usr_123")
.scope("organization", "org_456")
.attributes({ "ctx.request.id": "req_abc" })
.run(async () => {
telemetry.emit("step.one", {}); // Context auto-inherited
telemetry.emit("step.two", {}); // Context auto-inherited
});Session Methods
| Method | Description |
|---|---|
.actor(type, id?, tags?) | Set the actor (who is acting) |
.scope(type, id, tags?) | Set the scope (what context) |
.attributes(attrs) | Merge session-level attributes |
.id(sessionId) | Set a custom session ID |
.emit(name, input?) | Emit an event with session context |
.run(fn) | Execute function with session as active context |
.end() | End the session (no more modifications) |
.getState() | Get a copy of current session state |
Session Mode A: Manual Handle
Create, configure, emit, and end manually. Best for short-lived, explicit sessions.
async function handleWebhook(payload: { orgId: string; userId: string }) {
const session = telemetry.session()
.actor("system", "webhook", { source: "stripe" })
.scope("organization", payload.orgId)
.attributes({ "ctx.webhook.type": "payment_intent" });
session.emit("webhook.received", {
attributes: { "ctx.webhook.id": payload.userId },
});
try {
await processWebhook(payload);
session.emit("webhook.processed", {
attributes: { "ctx.webhook.status": "success" },
});
} catch (error) {
session.emit("webhook.failed", {
level: "error",
error: {
name: error instanceof Error ? error.name : "Error",
message: error instanceof Error ? error.message : String(error),
},
});
}
await session.end();
}Session Mode B: Scoped Execution
Wrap logic in session.run() for automatic context propagation. All telemetry.emit() calls inside the callback inherit the session.
async function handleRequest(req: Request, res: Response) {
await telemetry.session()
.actor("user", req.headers["x-user-id"] as string)
.scope("organization", req.headers["x-org-id"] as string)
.attributes({
"ctx.request.method": req.method,
"ctx.request.path": req.url,
})
.run(async () => {
// All emit calls here inherit user + org context
telemetry.emit("request.started");
const result = await processRequest(req);
telemetry.emit("request.completed", {
attributes: { "ctx.request.status": res.statusCode },
});
return result;
});
}How it works: session.run() enters an AsyncLocalStorage context. The telemetry manager checks for an active session on every emit() call. If found, session fields (actor, scope, attributes) are merged into the event envelope.
Session Mode C: Nested Sessions
Sessions can be nested — inner sessions override outer session fields:
await telemetry.session()
.actor("system", "batch-job")
.scope("organization", "org_global")
.run(async () => {
// All events here have: actor=system, scope=org_global
telemetry.emit("batch.started");
for (const org of organizations) {
// Inner session overrides the scope
await telemetry.session()
.scope("organization", org.id)
.run(async () => {
// Events here: actor=system (inherited), scope=org_X (overridden)
telemetry.emit("batch.org_processed", {
attributes: { "ctx.org.id": org.id },
});
});
}
// Back to outer context: actor=system, scope=org_global
telemetry.emit("batch.completed");
});Session State
Get a copy of the current session state for inspection:
const session = telemetry.session()
.actor("user", "usr_123")
.scope("organization", "org_456");
const state = session.getState();
console.log(state.sessionId); // "sess_abc123..."
console.log(state.actor); // { type: "user", id: "usr_123" }
console.log(state.scope); // { type: "organization", id: "org_456" }
console.log(state.startedAt); // "2024-01-15T10:30:00.000Z"Session Lifecycle
telemetry.session() → Create session
.actor(...) → Configure actor
.scope(...) → Configure scope
.attributes(...) → Add shared attributes
.run(async () => { → Enter scoped context
telemetry.emit(...) → Events inherit session
telemetry.emit(...) → Events inherit session
}) → Exit scoped context
session.end() → End session (or auto-ended after run())After End
Once ended, the session rejects further modifications:
const session = telemetry.session().actor("user", "usr_123");
await session.end();
// ❌ Throws TELEMETRY_SESSION_ENDED
session.actor("user", "another_user");Static Method: getActive()
Check if there's an active session from anywhere in your code:
import { IgniterTelemetrySession } from "@igniter-js/telemetry";
const activeSession = IgniterTelemetrySession.getActive();
if (activeSession) {
console.log("Active session:", activeSession.sessionId);
console.log("Actor:", activeSession.actor);
console.log("Scope:", activeSession.scope);
}Real-World Example: Next.js Middleware
Wrap every API route in a telemetry session:
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { telemetry } from "@/lib/telemetry";
export async function middleware(request: NextRequest) {
const userId = request.headers.get("x-user-id") ?? "anonymous";
const orgId = request.headers.get("x-org-id") ?? "unknown";
return telemetry.session()
.actor("user", userId)
.scope("organization", orgId)
.attributes({
"ctx.request.method": request.method,
"ctx.request.path": request.nextUrl.pathname,
"ctx.request.ip": request.ip ?? "unknown",
})
.run(async () => {
telemetry.emit("request.started", {
attributes: { "ctx.request.path": request.nextUrl.pathname },
});
const response = NextResponse.next();
telemetry.emit("request.completed", {
attributes: { "ctx.request.status": response.status },
});
return response;
});
}
export const config = {
matcher: "/api/:path*",
};Real-World Example: Express Middleware
import express from "express";
import { telemetry } from "./telemetry";
const app = express();
app.use((req, res, next) => {
telemetry.session()
.actor("user", req.headers["x-user-id"] as string ?? "anonymous")
.scope("organization", req.headers["x-org-id"] as string ?? "unknown")
.attributes({
"ctx.request.method": req.method,
"ctx.request.path": req.path,
"ctx.request.ip": req.ip,
})
.run(async () => {
telemetry.emit("request.started", {
attributes: { "ctx.request.path": req.path },
});
res.on("finish", () => {
telemetry.emit("request.completed", {
attributes: { "ctx.request.status": res.statusCode },
});
});
next();
});
});Next Steps
- Emitting Events — Attributes, levels, errors, and source metadata
- Adapters — Route events to different destinations
- Framework Integration — Next.js, Express, and Fastify patterns
Sampling & Redaction
Control telemetry volume with per-level sampling rates and glob patterns. Protect PII with key denylists, SHA-256 hashing, and string truncation — all applied before events leave the application.
Troubleshooting
Diagnose and fix common telemetry issues — error codes, transport failures, session problems, event validation errors, and performance concerns.