Owlmetry
SDKsNode.js SDK

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:

AttributeWhen set
_error_typeAlways. error.name for Errors, typeof error otherwise.
_error_stackWhen error.stack is present. Truncated at 16000 chars.
_error_code / _error_errno / _error_syscall / _error_pathWhen the error has the Node ErrnoException shape (filesystem / network errors).
_error_cause_<n>_type, _error_cause_<n>_messageWhen 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:

ParameterTypeDescription
messagestringWhat happened -- a human-readable description. Truncated to 2000 characters.
attrsRecord<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:123

No 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:

FieldTypeDescription
pathstringAbsolute path to a file on disk. Mutually exclusive with buffer.
bufferBuffer | Uint8ArrayIn-memory bytes. Mutually exclusive with path.
namestringFilename shown in the dashboard. Required for buffer; defaults to the basename of path.
contentTypestringOverrides 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:

  1. Calling Owl.info() (or any log method) creates an event and adds it to the buffer
  2. When the buffer reaches flushThreshold (default 20) or the flush timer fires (default every 5 seconds), the batch is sent to the server
  3. If the request fails, it retries with exponential backoff up to 5 times
  4. 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.

Ready to get started?

Connect your agent via MCP or CLI and start tracking.