Node.js SDK
Install @unhandled/node in any Node.js app, start it with --import or init(), and look up every option, export and default.
@unhandled/node sends traces, logs, metrics, exceptions and analytics events from any Node.js app to Unhandled over OTLP/HTTP. It builds the OpenTelemetry Node SDK with the auto-instrumentations, so the HTTP server, outgoing requests, databases, Redis and loggers produce spans and logs without code changes. Use it directly for plain node:http servers, workers and scripts. The Express, Fastify, Hono, NestJS, Next.js and AdonisJS packages build on it and add routes, users and error hooks. This page is the reference for the options and functions they all share.
Requirements
- Node.js 24 or later.
- A service in Unhandled and its DSN. See Services.
Install
npm install @unhandled/nodeStart the SDK
The SDK has to start before the modules it instruments are imported. There are two ways to do that.
With --import
Set the DSN and load the register module ahead of your entry file:
UNHANDLED_DSN=https://[email protected]/0b4f6c2e-8a1d-4c3e-9f7a-1234567890ab \
node --import @unhandled/node/register server.js@unhandled/node/register registers the ES module loader hook, then calls init() with options from the environment only. This path works for ES modules and CommonJS alike. Use it unless you need options that have no environment variable.
With init()
Call init() before you import anything you want traced:
import { init } from '@unhandled/node'
init({ dsn: process.env.UNHANDLED_DSN, serviceName: 'checkout', samplingRatio: 0.5 })init() without the loader hook patches only CommonJS modules required after it runs. In an ES module app, put the call in its own file and start it through the hook from @unhandled/node/hook:
// instrument.ts
import { startWithEsmHook } from '@unhandled/node/hook'
await startWithEsmHook(async () => {
const { init } = await import('@unhandled/node')
init({ serviceName: 'checkout', http: { captureBodies: { enabled: true } } })
})node --import ./instrument.ts server.jsstartWithEsmHook(load) registers the hook, runs load and waits until the hook knows every module it has to patch. Import @unhandled/node dynamically inside load. A static import would load the SDK's own dependencies before the hook and leave them unpatched.
init() returns the running UnhandledClient, or null when nothing would be exported and debug is off. A second call returns the client that is already running and ignores the new options.
Verify the setup
- Start the app with
UNHANDLED_DSNset andNODE_ENVnot set totest. - Send a few requests to it.
- Open Traces in Unhandled and pick the environment you send as. Each request appears as a SERVER span within about 5 seconds, the default batch delay.
If nothing shows up, set debug: true to print every span to the console, then work through troubleshooting.
DSN and environment variables
@unhandled/node/register reads only environment variables. init() reads them as fallbacks when the matching option is not set.
| Variable | Option it fills | Effect |
|---|---|---|
UNHANDLED_DSN | dsn | Endpoint, API key and service ID. See DSN. |
UNHANDLED_ENV | environment | production, staging or development. |
APP_ENV | environment | Used when UNHANDLED_ENV is not set and it is one of the three names. |
NODE_ENV | environment, enabled | Used as the environment when it is one of the three names and neither UNHANDLED_ENV nor a valid APP_ENV is set. test turns the SDK off. |
OTEL_SERVICE_NAME | serviceName | Local service name. |
APP_NAME | serviceName | Fallback after OTEL_SERVICE_NAME. |
UNHANDLED_VERSION, APP_VERSION | serviceVersion | Pins the release. See Service name and version. |
UNHANDLED_SOURCE_ROOT | sourceContext.root | Directory whose source files get source lines. See Which files are read. |
UNHANDLED_SANITIZE_URLS | http.sanitizeUrls | false or 0 turns URL sanitizing off. Other values leave it on. |
With a valid DSN, every export carries the x-api-key, x-unhandled-service-id and x-unhandled-env headers. An invalid DSN turns export off, extra destinations included. Node then emits a process warning with code UNHANDLED_INVALID_DSN that names the problem without printing the DSN. The app keeps running.
Options
Every option is optional. The framework packages accept the same object.
Connection
| Option | Default | Effect |
|---|---|---|
dsn | UNHANDLED_DSN | Endpoint, API key and service ID in one string. |
environment | UNHANDLED_ENV, then APP_ENV or NODE_ENV when it is a valid name, else development | Any other value throws UnhandledConfigError from init(). |
enabled | true, or false when NODE_ENV=test | false turns the SDK off. true runs it in tests. |
dev | false | Ignores the DSN. Data goes only to endpoint, if set. |
endpoint | none | Base URL of an OTLP receiver. Replaces the DSN host. In dev mode it is the only target. |
apiKey | none | Legacy connection, used with endpoint when there is no DSN. Sends x-api-key and x-unhandled-env. |
destinations | [] | Extra OTLP endpoints that get the same data. See Extra destinations. |
Export
| Option | Default | Effect |
|---|---|---|
compression | true | gzip on every signal. false or 'none' sends uncompressed. |
batch.maxExportBatchSize | 512 | Spans or log records per export request. |
batch.scheduledDelayMillis | 5000 | Delay between exports. |
batch.exportTimeoutMillis | 30000 | Timeout of one export request. |
batch.maxQueueSize | 2048 | Records held in memory before new ones are dropped. |
shutdownTimeoutMillis | 5000 | Longest flush() and shutdown() wait for the ingest before they give up. |
hostMetrics | true | Host CPU, memory and network metrics. false or { enabled: false } turns them off. |
debug | false | Also prints every span to the console. Starts the SDK even when nothing is exported. |
Resource
| Option | Default | Effect |
|---|---|---|
serviceName | detected | Local service.name. With a DSN, Unhandled files data under the DSN's service whatever this says. |
serviceVersion | detected | The release. Placeholders such as 0.0.1 are skipped. |
resourceAttributes | {} | Extra resource attributes. They win over the ones the SDK sets. |
Sampling
| Option | Default | Effect |
|---|---|---|
samplingRatio | 1 | Head sampling ratio from 0 to 1, parent based. A value outside that range throws. Below 1, kept spans carry unhandled.sampling.ratio so counts scale back up. |
sampler | none | Your own OpenTelemetry Sampler. Wins over samplingRatio. |
sampling | none | Per-job callback built with Sample. See Job sampling. Forces samplingRatio to 1. |
HTTP
| Option | Default | Effect |
|---|---|---|
ignoreOptionsRequests | true | Drops CORS preflight (OPTIONS) server spans. |
http.ignoredUrls | [] | Paths that never get a server span, added to the built-in list. |
http.sanitizeUrls | on | Rewrites secrets out of URLs on SERVER and CLIENT spans. |
http.captureBodies | off | Records request and response bodies and headers, with redaction. |
HTTP capture and redaction covers all three.
Instrumentations and integrations
| Option | Default | Effect |
|---|---|---|
instrumentations | see below | Config per auto-instrumentation package name, { enabled: false }, or an Instrumentation instance. |
integrations | [] | Unhandled integration packages. Each name may appear once. A duplicate throws. |
spanProcessors | [] | Extra span processors, run before export. |
logRecordProcessors | [] | Extra log record processors. |
metricReaders | [] | Extra metric readers. |
Exceptions
| Option | Default | Effect |
|---|---|---|
ignoreExceptions | [] | Error classes that framework hooks and process handlers never report. Matched with instanceof. |
ignoreStatuses | [] | Values of the error's status property that are never reported. |
ignoreCodes | [] | Values of the error's code property that are never reported. |
processHandlers | true | Reports uncaught exceptions and unhandled rejections. See Process handlers. |
sourceContext.root | entry script's project | Only source files inside this directory get source lines. See Which files are read. |
sourceContext.includeNodeModules | false | Also read source lines from files under node_modules. |
The ignore rules apply to errors that a framework hook or a process handler reports. captureException() always sends.
Turning export off
The SDK exports nothing, and does not start unless debug is on, when:
| Situation | Why |
|---|---|
enabled: false | Explicitly off. |
NODE_ENV=test and enabled not set | Tests stay quiet by default. |
No DSN, an empty UNHANDLED_DSN=, and no apiKey with endpoint | Nothing to send to. |
dev: true without endpoint | Dev mode never uses the DSN. |
Invalid DSN or invalid apiKey | The process warning says why. |
Extra destinations still receive data in the no-DSN and dev cases.
Extra destinations
import { destinations, init } from '@unhandled/node'
init({
serviceName: 'checkout',
destinations: [
destinations.otlp({
name: 'collector',
url: 'https://otel-collector.internal:4318',
headers: { authorization: 'Bearer <collector-token>' },
}),
],
})destinations.otlp() takes name, url, optional headers and optional enabled (default true). The SDK appends /v1/traces, /v1/logs and /v1/metrics to url, and uses the same compression and batch settings as for Unhandled.
What is recorded
With no code beyond starting the SDK:
- Incoming HTTP requests as SERVER spans, and outgoing calls as CLIENT spans.
- Database queries, Redis commands, loggers and every other package the OpenTelemetry Node auto-instrumentations cover.
- ClickHouse calls made with
@clickhouse/client. See ClickHouse. - Host CPU, memory and network metrics, and Node runtime metrics.
- Uncaught exceptions and unhandled rejections.
Six auto-instrumentations start turned off because they are noisy or duplicate higher-level spans: @opentelemetry/instrumentation-net, -dns, -socket.io, -pg, -mysql and -mysql2. The OpenTelemetry package also keeps -fs and -fastify off by default. Turn one on, or another one off, by its package name:
import { init } from '@unhandled/node'
init({
instrumentations: {
'@opentelemetry/instrumentation-pg': { enabled: true },
'@opentelemetry/instrumentation-ioredis': { enabled: false },
},
})ClickHouse
@clickhouse/client talks to ClickHouse over HTTP, so the OpenTelemetry instrumentations would only record a POST to port 8123. The SDK's own @unhandled/instrumentation-clickhouse records each query, insert, command and exec call on @clickhouse/client 1.0 or later in 1.x as one CLIENT span instead, and drops the HTTP span underneath it:
| Attribute | Value |
|---|---|
db.system.name | clickhouse |
db.operation.name | The statement's first keyword, such as SELECT, INSERT or ALTER. |
db.collection.name | The table after FROM, INTO or TABLE, when there is one. |
db.query.text | The statement with literals replaced by ?, comments removed and whitespace collapsed, cut at 1,000 characters. Quoted strings, $$…$$ and $tag$…$tag$ strings, numbers and brace groups that aren't a {name:Type} parameter all become ?. {name:Type} query parameters stay as they are, and the values passed in query_params are not recorded. |
db.namespace | The client's database. |
server.address, server.port | From the client's url. |
error.type, db.response.status_code | On a ClickHouse error: the error name, such as UNKNOWN_TABLE, and its numeric code. On a network failure: the Node.js error code, such as ECONNREFUSED, and no status code. |
The span is named <operation> <table>, for example SELECT events, or just the operation when there is no table. An insert() call is described as INSERT INTO <table> FORMAT <format>. The span ends when ClickHouse starts the response, so streaming a large result is not part of its duration. @clickhouse/client-web is not supported.
ClickHouse error messages quote the query, values included, so a failed call records only <TYPE> (<code>), for example UNKNOWN_TABLE (60), as its status and exception message, with no stack trace. To record the full message and stack, pass { recordErrorMessages: true } as the '@unhandled/instrumentation-clickhouse' entry in instrumentations. Network errors keep their message, such as connect ECONNREFUSED 127.0.0.1:8123.
Each request carries a traceparent header for its ClickHouse span, so ClickHouse's server-side trace log joins the same trace. The header needs @clickhouse/client 1.11 or later; older clients have no per-request headers and send none.
Calls inside suppressTracing() record nothing. Turn the instrumentation off with instrumentations: { '@unhandled/instrumentation-clickhouse': { enabled: false } }.
Plain node:http servers have no route templates. Their server spans group by url.path with identifiers, and segments the SDK replaced with REDACTED, shown as :id. The framework packages set http.route instead.
HTTP capture and redaction
Ignored requests
These requests never get a server span:
OPTIONSrequests, whileignoreOptionsRequestsistrue./health,/healthz,/internal/healthz,/ready,/readiness,/metrics,/internal/metrics,/favicon.ico,/robots.txt,/sitemap.xml,/manifest.json,/site.webmanifest,/browserconfig.xmland/ads.txt.- Static files such as scripts, stylesheets, images and fonts, recognized by their extension, and Vite dev server paths.
- Anything matching
http.ignoredUrls.
In http.ignoredUrls, a * matches any characters, and a plain path also matches every path below it. /webhooks/heartbeat skips /webhooks/heartbeat/eu too. The query string is not part of the match.
URL sanitizing
URLs often carry secrets: OAuth codes, signed links, reset tokens. The SDK rewrites them on SERVER and CLIENT spans before export, with no setup.
- A query value is replaced when its name would be redacted in a captured body (see the lists under Body capture), and also for
state,code_verifier,sig,key,jwt,reset,sso,ticket,bearer,pass,nonceandhash. These extra names match exactly, sohashtagandpassagestay. - A path segment is replaced when it is a UUID, a JWT, an email address, or 16 or more letters, digits,
_or-that include a digit. A segment with a hyphen counts only when it mixes upper- and lowercase letters, so slugs such asrelease-notes-2024andABC-DEF-GHIJ-2024stay. A;jsessionid=value inside a segment is replaced as well. - The path rule is a heuristic and errs both ways. It misses short secrets such as the code in
/verify/483920, base64 with=padding and lowercase hyphenated tokens, and it replaces some readable names that contain a digit, such as/files/quarterly_report_2024_final. AddqueryParamsor a customsanitizefor the routes it gets wrong.
A request to /reset/9f86d081884c7d659a2feaa0c55ad015?token=4f2a&page=2 is stored as /reset/REDACTED?token=REDACTED&page=2. /users/42 stays as it is.
import { init } from '@unhandled/node'
init({
http: {
sanitizeUrls: { queryParams: ['invite'] },
},
})| Option | Default | Effect |
|---|---|---|
queryParams | [] | More parameter names whose values are replaced. Case and separators are ignored, so accessToken and access_token match. |
censor | 'REDACTED' | Replacement text. |
sanitize | none | (url: URL) => string that replaces the whole URL. When set, the built-in rules, queryParams and censor are not used. |
sanitizeUrls: false turns the rewrite off and exports URLs as they were recorded. UNHANDLED_SANITIZE_URLS=false does the same when sanitizeUrls is not set in code, which is the only way to turn it off with @unhandled/node/register.
The rewrite applies to url.full, url.path, url.query, http.url and http.target on SERVER and CLIENT spans.
Upgrading from a version where sanitizing was opt-in:
- Without
sanitizeUrls, query secrets and token path segments now arrive asREDACTED. SetsanitizeUrls: falseto keep the old output. queryParamsnow adds to the built-in names instead of replacing them.- A custom
sanitizenow also rewritesurl.path.
Body capture
import { init } from '@unhandled/node'
init({
http: {
captureBodies: {
incoming: { request: { enabled: true }, response: { enabled: true, maxBodySize: 4_096 } },
outgoing: { request: { enabled: true } },
redact: { bodyKeys: ['iban'], headers: ['x-shop-signature'] },
},
},
})| Option | Default | Effect |
|---|---|---|
enabled | false | Turns on all four sides. A per-side enabled wins over it. |
incoming.request, incoming.response | off | Requests to your app and its responses. A framework package records these. |
outgoing.request, outgoing.response | off | Calls made with node:http and node:https, and their responses. |
maxBodySize | 10000 | Bytes kept per body, at the top level or per side. Longer bodies are cut. |
redact.patterns | built-in list | Substrings. Any key that contains one is redacted. Replaces the built-in patterns. |
redact.bodyKeys, redact.headers, redact.queryParams | built-in list | Exact keys whose values are replaced, added to the built-in names. |
redact.censor | 'REDACTED' | Replacement text. |
transform | none | (record) => record | null | void. Runs after redaction. Return null to skip the capture. |
Only application/json and application/x-www-form-urlencoded bodies are captured. Bodies are stored as unhandled.http.request.body and unhandled.http.response.body, and headers as http.request.header.* and http.response.header.*. Outgoing calls made with fetch or undici are not captured.
Capture costs about as much as the part of the body it keeps. A body the framework already parsed is redacted and copied only as far as maxBodySize reaches, never serialized in full. A body that arrives as text must be parsed whole, so text longer than 64 KiB (or than maxBodySize, when that is larger) is not parsed: the span gets unhandled.http.request.body_omitted or unhandled.http.response.body_omitted set to too_large instead. Performance and safety lists what else runs inside your requests.
Incoming bodies need a framework layer, because only the framework knows the parsed body. With @unhandled/node alone, only outgoing capture records anything.
Redaction lowercases each key and drops separators before it compares, so x-auth-token, X_Auth_Token and xAuthToken are the same key. A key is redacted when it contains one of these patterns: password, passwd, secret, token, auth, cookie, session, csrf, api-key / apikey, credit, card and ssn. That catches newPassword, password_confirmation, x-xsrf-token, stripeToken and creditCardNumber. It also catches harmless keys such as author or cardinality. Pass your own redact.patterns to narrow or extend the list. The built-in list is exported as BUILT_IN_REDACTED_PATTERNS.
These exact names stay redacted even when you replace the patterns: password, passwd, pwd, secret, token, code, access_token, refresh_token, id_token, auth, authorization, proxy-authorization, api_key, apikey, x-api-key, client_secret, private_key, cookie, set-cookie, session, session_id, otp, cvv, cvc, card_number, credit_card and ssn.
Users
setUser({ id, email?, name? }) writes user.id, user.email and user.name on the active span and on the request's SERVER span. Call it once you know who is signed in:
import { setUser } from '@unhandled/node'
setUser({ id: account.id, email: account.email })id may be a string or a number. The Users page counts requests per user.id.
Exceptions
What is reported
captureException()andcaptureMessage()calls.- Uncaught exceptions and unhandled rejections, through the process handlers.
- Errors that a framework package's error hook reports. Each package page says which errors those are.
With @unhandled/node alone, an error that a request handler catches and turns into a response is not reported. Call captureException() there.
Which files are read
Source lines come only from files that a real stack frame points at. Lines in the error name or message that look like frames are skipped, so an error that echoes user input cannot make the SDK read a file. A file is read only when all of these hold:
- Its real path, after symlinks, is inside the app root.
- Its name does not start with a dot, so
.envand.env.productionare never read. - No directory below the root starts with a dot, except build output:
.next,.nuxt,.output,.mastra,.svelte-kitand.vercel, plus pnpm'snode_modules/.pnpmwhenincludeNodeModulesis on..git,.ssh,.aws,.configand every other dot directory are skipped. - It ends in
.js,.mjs,.cjs,.ts,.mts,.cts,.jsxor.tsx. - It is not under
node_modules, unlesssourceContext.includeNodeModulesistrue. - It is at most 1 MiB.
/proc, /sys, /dev and /etc are never read, whatever the root.
The app root is sourceContext.root, or UNHANDLED_SOURCE_ROOT, resolved against the working directory. Without either, the SDK finds it from the entry script (process.argv[1]), not the working directory, so a service started from / by systemd or from a build directory still gets source lines:
- It takes the nearest directory above the entry script that has a
package.json. A launcher insidenode_modules, such asnext start, counts from the project that installed it. - When a directory above that one is a workspace root (it has
pnpm-workspace.yaml, or apackage.jsonwithworkspaces), the root widens to it, so frames in sibling workspace packages and source maps pointing at../srckeep their lines. - When the working directory contains that directory, as a repo root does, the working directory is the root instead.
/never counts, so a service that systemd starts from/does not expose the whole host. - Without a
package.json, the root is the working directory.
import { init } from '@unhandled/node'
init({
sourceContext: { root: '/srv/app', includeNodeModules: false },
})Each report is an OTLP log record with exception.* attributes, the cause chain and stack frames with source lines. Source files are read asynchronously, once per file, and kept in a cache. When a frame points at a file the SDK has not read yet, the report waits for that read and is sent right after it, so captureException() never blocks on the disk. The exception span event uses only files that are already cached, so the first error from a file can reach the span without source lines. Files over 1 MiB, such as large bundles, are not read. When a span is active, the error is also recorded on it as an exception event and the span is marked as failed. Unhandled stores the log record and the span event as two occurrences, so an error reported inside a span counts twice in its issue. The same error object is reported at most once.
captureException
import { captureException } from '@unhandled/node'
try {
await chargeCard(order)
} catch (error) {
captureException(error, {
user: { id: order.customerId },
tags: { area: 'billing' },
extra: { orderId: order.id },
})
}captureException(error, context?) takes user, tags (string values) and extra (any value, sent as JSON). It reports with mechanism capture_exception and handled: true.
captureMessage
import { captureMessage } from '@unhandled/node'
captureMessage('Quota almost used', 'warning')
captureMessage('Import finished with skipped rows', { level: 'info', extra: { skipped: 12 } })captureMessage(message, levelOrContext?) takes a level or a context with level, user, tags and extra. Levels are debug, info, log, warning, error and fatal. An unknown or missing level becomes info.
Process handlers
With processHandlers on, the default:
- An uncaught exception is reported with mechanism
process.uncaught_exceptionandhandled: false. The SDK prints it, flushes for up to 2 seconds and exits with code 1, the way Node does without a listener. - An unhandled rejection is reported with mechanism
process.unhandled_rejection, then behaves as Node's--unhandled-rejectionsmode says. - If your app registers its own
uncaughtExceptionorunhandledRejectionlistener, the SDK reports and leaves the rest to your listener.
Analytics events
import { track } from '@unhandled/node'
const result = track('checkout_completed', { plan: 'pro', items: 3 })track(name, properties?) sends one event to Analytics and returns { ok: true, eventId } or { ok: false, error }. It does not throw. The name and properties follow the same limits as the browser SDK: a name up to 100 bytes and not pageview, at most 20 properties, and string values up to 256 bytes, finite numbers or booleans. Keys such as email or user_id, and keys starting with user., trace., span. or unhandled., are rejected.
Custom spans
record(name, callback) runs callback inside a new active span and ends the span when the callback returns or its promise settles. A thrown error is recorded on the span and rethrown.
import { record } from '@unhandled/node'
const report = await record('report.generate', async (span) => {
span.setAttribute('report.kind', 'monthly')
return buildReport()
})The span() and spanAll() decorators wrap methods the same way. They use the TypeScript experimentalDecorators signature.
import { span, spanAll } from '@unhandled/node'
@spanAll({ prefix: 'billing' })
export class InvoiceService {
async issue(orderId: string) {
return createInvoice(orderId)
}
}
export class ReportService {
@span({ name: 'report.monthly', attributes: { 'report.kind': 'monthly' } })
async monthly() {
return buildReport()
}
}span({ name?, attributes? }) names the span ClassName.method by default. spanAll({ prefix?, attributes? }) wraps every method of the class and names each span prefix.method, with the class name as the default prefix.
The other helpers work on the active span or context:
| Function | Effect |
|---|---|
getCurrentSpan() | Returns the active span, or undefined. |
setAttributes(attributes) | Sets attributes on the active span. |
recordEvent(name, attributes?) | Adds an event to the active span. |
injectTraceContext(headers) | Writes traceparent into a headers object, for calls the instrumentations do not cover. |
extractTraceContext(headers) | Returns a context continued from incoming headers. |
handleError(span, error) | Records the error on the span, ends it and rethrows. |
Flush and shutdown
import { Unhandled } from '@unhandled/node'
await Unhandled.flush()
await Unhandled.shutdown()flush() sends every queued span, log record and metric. shutdown() removes the process handlers, flushes and stops the SDK. Both return within shutdownTimeoutMillis and never reject: when the ingest is slow or down, what is left is dropped and the diagnostic logger says so. Call shutdown() at the end of a script or a serverless handler, where the process may exit before the next batch. Both do nothing when no client is running. The client returned by init() has the same flush() and shutdown(), plus config, the resolved configuration.
Unhandled groups track, captureException, captureMessage, setUser, flush and shutdown. Each is also a named export.
Integrations
Optional instrumentations ship as their own packages and register through integrations:
import { init } from '@unhandled/node'
import { bullmq } from '@unhandled/instrumentation-bullmq/integration'
init({ integrations: [bullmq()] })If an integration fails while the SDK starts, init() throws an IntegrationSetupError that names it. BullMQ, Bentocache, Vercel AI and Mastra register this way. Drizzle, Kysely and MCP server wrap your client in code instead, so see their pages. To write your own integration, import UnhandledIntegration, IntegrationSetup and IntegrationContext from @unhandled/node/types.
Service name and version
With a DSN, Unhandled takes the service from the DSN. The local name still matters for extra destinations and dev mode. It is the first value found in:
serviceNameOTEL_SERVICE_NAMEAPP_NAME- The
nameinpackage.jsonin the working directory, without its npm scope unknown_service
The version is the first usable value of:
serviceVersionUNHANDLED_VERSIONAPP_VERSION- A platform commit:
VERCEL_GIT_COMMIT_SHA,RENDER_GIT_COMMIT,RAILWAY_GIT_COMMIT_SHA,HEROKU_SLUG_COMMIT,SOURCE_VERSION,SOURCE_COMMIT,GIT_REVorK_REVISION - A CI commit:
GITHUB_SHA,CI_COMMIT_SHA,CIRCLE_SHA1,BITBUCKET_COMMITorBUILDKITE_COMMIT - The
HEADcommit of the local.gitdirectory
The values 0.0.0, 0.0.1, 1.0.0, unknown, dev, latest and the empty string are skipped. A new version on a service's entry spans records a deployment.
Every signal carries these resource attributes:
| Attribute | Value |
|---|---|
service.version | The detected version, or 0.0.0 when none is found. |
unhandled.service.version.source | Where the version came from, such as config, env:APP_VERSION, platform:render, ci:github_actions or git. |
vcs.ref.head.revision | The commit SHA, when one is known. |
unhandled.language.name | javascript. |
unhandled.sdk.name, unhandled.sdk.version | The package that started the SDK and its version. |
unhandled.framework.name | Set by the framework packages, such as express. |
service.instance.id | HOSTNAME plus the process ID (web-1-4321), so cluster and PM2 workers get separate series. A random UUID per process when HOSTNAME is not set. |
Exports
| Entry point | Exports |
|---|---|
@unhandled/node | init, Unhandled, captureException, captureMessage, setUser, track, flush, shutdown, record, span, spanAll, getCurrentSpan, setAttributes, recordEvent, injectTraceContext, extractTraceContext, handleError, destinations, Sample, BUILT_IN_REDACTED_PATTERNS, UnhandledConfigError, IntegrationSetupError, and the types UnhandledOptions, UnhandledClient, ResolvedConfig, HttpConfig, SourceContextOptions, CaptureContext, CaptureUser, MessageContext, TrackResult |
@unhandled/node/register | Side effect only. Starts the SDK from the environment. |
@unhandled/node/hook | startWithEsmHook |
@unhandled/node/framework | The building blocks the framework packages use, among them getClient, start, resolveConfig, frameworkLayer, serverSpan, setRoute, capturesBody, captureRequest, captureResponse, reportException |
@unhandled/node/types | Types for integration authors |
If your framework has no package yet, @unhandled/node/framework is enough to add one. setRoute(route, method?) sets http.route on the request's SERVER span and renames it METHOD route. reportException(error, { mechanism, handled }) reports an error with the client's ignore rules applied.