@traceon/monitor
v0.1.0
Published
Official Node.js / JavaScript SDK for TraceOn
Downloads
95
Maintainers
Readme
@traceon/monitor
Official Node.js / JavaScript SDK for TraceOn.
Zero runtime dependencies. Batches events, retries with backoff, and reports crashes that would otherwise vanish.
Installing
The SDK is not published to the public npm registry yet, so
npm install @traceon/monitor will not resolve. Until it is, install it
directly from a checkout - npm pack produces exactly the tarball that would
be published, so what you install is what you would get from the registry:
# in the traceon checkout
npm run build --workspace @traceon/monitor
cd packages/sdk-node && npm pack # -> traceon-monitor-0.1.0.tgz
# in your application
npm install /path/to/traceon-monitor-0.1.0.tgzOr, for a workspace that lives alongside the checkout:
npm install file:../traceon/packages/sdk-nodeOnce the package is published, this becomes:
npm install @traceon/monitorGetting started
import * as Monitor from '@traceon/monitor';
Monitor.init({
dsn: process.env.TRACEON_DSN!,
environment: process.env.NODE_ENV ?? 'production',
release: process.env.APP_VERSION,
});Call init() before the rest of your application starts, so unhandled
exceptions and rejections are captured from the first tick.
With no DSN the SDK becomes a no-op. That is deliberate: you can call
init() unconditionally and simply not set the variable in development or in
tests, without your application code branching on whether monitoring is on.
Capturing
// An exception, with its stack trace and any `cause` chain.
Monitor.captureException(error);
// A log-style message.
Monitor.captureMessage('Payment queue backing up', 'warning');
// Anything thrown, not just Errors. Strings, objects and numbers all produce
// a usable issue rather than being dropped.
Monitor.captureException('database unreachable');Both return the event id, or undefined when the SDK is disabled or the event
was sampled out.
Context
Everything set here is attached to subsequent events.
Monitor.setUser({ id: '42', email: '[email protected]' });
Monitor.setUser(null); // clear on logout
Monitor.setTag('region', 'eu-west-1'); // indexed, filterable in Discover
Monitor.setTags({ tier: 'pro', shard: 3 });
Monitor.setExtra('cartId', cart.id); // arbitrary, not indexed
Monitor.setContext('deployment', { cluster: 'prod-a', pod: 'checkout-7d9f' });
Monitor.setTransaction('POST /checkout'); // names the unit of work
Monitor.setLevel('warning');Breadcrumbs
What the application did before it broke. Buffered in memory and attached to the next event - never a request of their own, so recording them liberally costs nothing.
Monitor.addBreadcrumb({ type: 'http', category: 'fetch', message: 'GET /api/cart 200' });
Monitor.addBreadcrumb({ type: 'query', category: 'db', message: 'SELECT * FROM carts' });
Monitor.addBreadcrumb({ type: 'user', category: 'ui.click', message: 'Clicked Pay now' });The buffer is a ring of maxBreadcrumbs (default 100); the oldest fall off.
Per-request context
captureExceptionWithScope applies context to one event only, which is what
makes concurrent request handling safe:
app.use((req, res, next) => {
res.on('finish', () => {});
next();
});
try {
await handle(req);
} catch (error) {
Monitor.captureExceptionWithScope(error, (scope) => {
scope.setUser({ id: req.user?.id });
scope.setTransaction(`${req.method} ${req.route?.path}`);
scope.setTag('request_id', req.id);
});
throw error;
}Automatic capture
init() installs handlers for uncaughtException and unhandledRejection.
For an uncaught exception the process is already in an undefined state, so the
SDK flushes with a short deadline and then re-raises Node's own behaviour
(print the stack, exit 1) rather than swallowing the crash and leaving a zombie
process running. Set exitOnUncaught: false if you have your own supervisor.
Wrapping handlers
export const handler = Monitor.wrap(async (event) => {
return process(event);
});Reports any throw or rejection and rethrows it, so behaviour is unchanged.
Shutting down
Events are batched, so a process that exits immediately can lose the last batch:
await Monitor.flush(5_000); // wait for delivery, keep reporting
await Monitor.close(5_000); // flush and shut downBoth return false if the deadline passed with events still queued.
Options
| Option | Default | Notes |
| --- | --- | --- |
| dsn | - | Empty disables the SDK. |
| environment | production | |
| release | - | Enables release tracking and regression detection. |
| sampleRate | 1 | 0..1, applied per event before queueing. |
| maxBreadcrumbs | 100 | |
| maxBatchSize | 30 | Events per outbound request. |
| flushIntervalMs | 2000 | How long a partial batch waits. |
| maxQueueSize | 500 | Cap while the server is unreachable. Oldest are dropped. |
| maxRetries | 4 | Attempts per batch, including the first. |
| timeoutMs | 10000 | Per request. |
| captureUnhandled | true | Install the global handlers. |
| exitOnUncaught | true | Exit after reporting an uncaught exception. |
| beforeSend | - | Last chance to scrub or drop an event. |
| tags | {} | Applied to every event. |
| inAppInclude / inAppExclude | [] | Override which stack frames count as your code. |
| debug | false | SDK diagnostics on stderr. |
Scrubbing
beforeSend runs on every event. Return null to drop it.
Monitor.init({
dsn: process.env.TRACEON_DSN!,
beforeSend: (event) => {
if (event.exception?.values[0]?.type === 'AbortError') return null;
if (event.request?.headers) delete event.request.headers.authorization;
return event;
},
});The server also redacts obviously sensitive header and cookie names when displaying an event, but scrubbing at the source is what keeps a secret from leaving your process at all.
In-app frames
Frames under node_modules, node:internal and similar are marked as library
code: dimmed in the UI and excluded from grouping. In a monorepo where your own
packages are symlinked into node_modules, tell the SDK:
Monitor.init({
dsn: process.env.TRACEON_DSN!,
inAppInclude: ['/node_modules/@acme/'],
});How delivery works
Events accumulate in memory and are sent as one envelope, either when the batch
fills or after flushIntervalMs. A burst of 500 errors becomes a handful of
requests. Payloads over 1 KiB are gzipped.
Failures retry with exponential backoff and jitter. A 4xx other than 429 is not
retried - the payload is wrong and retrying it forever would be a bug. A 429 is
honoured for the duration of its Retry-After.
The flush timer is unref'd, so the SDK never keeps a process alive on its own.
Multiple clients
The module-level helpers operate on one implicit client, which is what almost every application wants. To report into two projects from one process:
import { Client } from '@traceon/monitor';
const billing = new Client({ dsn: process.env.BILLING_DSN! });
billing.captureException(error);
await billing.close();Demo
TRACEON_DSN="http://<key>@localhost:8080/<projectId>" \
node example/demo.mjsSends a handled exception with a real stack trace, a message, an error with a cause chain, a scoped capture and an unhandled rejection, then flushes.
