Events
Log events at different levels (info, debug, warn, error) from your Node.js backend.
The SDK provides four logging methods corresponding to the standard log levels.
Log Levels
Owl.info("Order placed", { order_id: "abc-123" });
Owl.debug("Cache miss for user lookup", { cache_key: "user:42" });
Owl.warn("Retry attempt on payment gateway", { attempt: "2" });
Owl.error("Failed to send notification email", { email: "[email protected]" });Reporting an Error Value
Owl.error is overloaded — pass a string message (logger-style) or pass an Error value (exception-style). When given an Error, the SDK extracts the type name, full stack, Error.cause chain, AggregateError children, and Node code/syscall/path fields into reserved _error_* attributes. The server's issue tracker uses _error_type as a fingerprint discriminator so different error classes with the same wording stay on separate issues.
try {
await processOrder(req.body);
} catch (err) {
Owl.error(err); // auto-derives message
Owl.error(err, "while processing order"); // caller-supplied context
Owl.error(err, "while processing order", { // + extra attributes
order_id: req.body.id,
});
}The Error overload accepts unknown, so throw "boom" / throw 42 patterns are handled too — they surface as _error_type=string / _error_type=number. Auto-attached attributes:
| Attribute | When set |
|---|---|
_error_type | Always. error.name for Errors, typeof error otherwise. |
_error_stack | When error.stack is present. Truncated at 16000 chars. |
_error_code / _error_errno / _error_syscall / _error_path | When the error has the Node ErrnoException shape (filesystem / network errors). |
_error_cause_<n>_type, _error_cause_<n>_message | When error.cause is set. Walked up to 5 levels. Cycle-safe. |
_error_aggregate_count, _error_aggregate_first_* | When the value is an AggregateError. |
Auto-Capture of Unhandled Errors
By default the SDK installs process.on('uncaughtException') and process.on('unhandledRejection') listeners that capture and forward unhandled errors as Owl.error events tagged with _unhandled = "uncaught_exception" | "unhandled_rejection". Crash semantics are preserved: a crashing app stays crashing, your own listeners still run, and any failure inside the SDK's listener is swallowed so it never compounds the original crash.
To opt out:
Owl.configure({
endpoint: "...",
apiKey: "owl_client_...",
captureUnhandled: false,
});Each method accepts:
| Parameter | Type | Description |
|---|---|---|
message | string | What happened -- a human-readable description. Truncated to 2000 characters. |
attrs | Record<string, unknown> | Optional custom attributes. Values are converted to strings and truncated to 200 characters. |
options | { attachments?, sessionId? } | Optional. attachments attaches files to the event. sessionId overrides the SDK's default session ID for just this event -- useful for linking backend events to a client's session. See Identity › withSession. |
Country Is Skipped For Backend Apps
The Node SDK is for backend services, where the request origin is your own server (a hosting datacenter), not an end user. Events ingested under an app with platform: "backend" always have country_code: null — capturing the Cloudflare-derived country would just reflect where your server runs, not where your users are. If you want per-user geography on backend events, attach it as a custom attribute or set it via the user-properties API. See Events › Country.
Automatic Source Module
The SDK captures the calling file and line number automatically by inspecting the call stack. This value is sent as source_module on each event, so you can trace an event back to the exact line of code that emitted it.
server.mjs:47
routes/orders.ts:123No configuration is required -- this works out of the box.
Custom Attributes
Attributes are key-value pairs attached to an event. Pass any object as the second argument -- values are stringified automatically:
Owl.info("Request completed", {
method: "POST",
path: "/api/orders",
status: 201,
duration_ms: 42,
});Values longer than 200 characters are truncated. See Events for more on how attributes are stored and queried.
File Attachments
The third options argument of every log method accepts an attachments array. Each entry is a file from disk (path) or in-memory bytes (buffer) that will be uploaded alongside the event and viewable from the dashboard, CLI, or MCP:
try {
await PdfParser.parse(inputPath);
} catch (err) {
Owl.error("pdf parse failed", { error: String(err) }, {
attachments: [
{ path: inputPath, name: "input.pdf" }, // from disk
{ buffer: diagnosticsJson, name: "debug.json", contentType: "application/json" }, // in memory
],
});
}Each attachment supports these fields:
| Field | Type | Description |
|---|---|---|
path | string | Absolute path to a file on disk. Mutually exclusive with buffer. |
buffer | Buffer | Uint8Array | In-memory bytes. Mutually exclusive with path. |
name | string | Filename shown in the dashboard. Required for buffer; defaults to the basename of path. |
contentType | string | Overrides the inferred content type (inferred from file extension otherwise). |
Uploads run on a serial queue and are awaited by Owl.flush() and Owl.shutdown(). Use attachments sparingly -- each project has a default 5 GB storage quota with a 250 MB per-user bucket. See Attachments for details on quotas and viewing attached files.
User-Scoped Events
Use Owl.withUser() to create a scoped instance that tags every event with a user ID:
const owl = Owl.withUser("user_42");
owl.info("Profile updated");
owl.warn("Rate limit approaching", { requests: "95" });This is particularly useful in request handlers where you know the authenticated user. See the Identity guide for the full pattern.
Per-Request Logging Pattern
In a typical HTTP server, create a scoped instance at the start of each request:
app.use((req, res, next) => {
const userId = req.auth?.userId;
req.owl = userId ? Owl.withUser(userId) : Owl;
next();
});
app.post("/api/orders", (req, res) => {
req.owl.info("Creating order", { items: String(req.body.items.length) });
// ...
req.owl.info("Order created", { order_id: order.id });
res.json(order);
});Event Lifecycle
Events are not sent immediately. They are buffered in memory and flushed in batches:
- Calling
Owl.info()(or any log method) creates an event and adds it to the buffer - When the buffer reaches
flushThreshold(default 20) or the flush timer fires (default every 5 seconds), the batch is sent to the server - If the request fails, it retries with exponential backoff up to 5 times
- Payloads over 512 bytes are gzip-compressed automatically
For serverless functions where the process may terminate before the timer fires, use Owl.wrapHandler() to flush after each invocation.
