docs

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

Terminal
npm install @unhandled/node

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

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

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

ts
// instrument.ts
import { startWithEsmHook } from '@unhandled/node/hook'

await startWithEsmHook(async () => {
  const { init } = await import('@unhandled/node')
  init({ serviceName: 'checkout', http: { captureBodies: { enabled: true } } })
})
Terminal
node --import ./instrument.ts server.js

startWithEsmHook(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

  1. Start the app with UNHANDLED_DSN set and NODE_ENV not set to test.
  2. Send a few requests to it.
  3. 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.

VariableOption it fillsEffect
UNHANDLED_DSNdsnEndpoint, API key and service ID. See DSN.
UNHANDLED_ENVenvironmentproduction, staging or development.
APP_ENVenvironmentUsed when UNHANDLED_ENV is not set and it is one of the three names.
NODE_ENVenvironment, enabledUsed 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_NAMEserviceNameLocal service name.
APP_NAMEserviceNameFallback after OTEL_SERVICE_NAME.
UNHANDLED_VERSION, APP_VERSIONserviceVersionPins the release. See Service name and version.
UNHANDLED_SOURCE_ROOTsourceContext.rootDirectory whose source files get source lines. See Which files are read.
UNHANDLED_SANITIZE_URLShttp.sanitizeUrlsfalse 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

OptionDefaultEffect
dsnUNHANDLED_DSNEndpoint, API key and service ID in one string.
environmentUNHANDLED_ENV, then APP_ENV or NODE_ENV when it is a valid name, else developmentAny other value throws UnhandledConfigError from init().
enabledtrue, or false when NODE_ENV=testfalse turns the SDK off. true runs it in tests.
devfalseIgnores the DSN. Data goes only to endpoint, if set.
endpointnoneBase URL of an OTLP receiver. Replaces the DSN host. In dev mode it is the only target.
apiKeynoneLegacy 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

OptionDefaultEffect
compressiontruegzip on every signal. false or 'none' sends uncompressed.
batch.maxExportBatchSize512Spans or log records per export request.
batch.scheduledDelayMillis5000Delay between exports.
batch.exportTimeoutMillis30000Timeout of one export request.
batch.maxQueueSize2048Records held in memory before new ones are dropped.
shutdownTimeoutMillis5000Longest flush() and shutdown() wait for the ingest before they give up.
hostMetricstrueHost CPU, memory and network metrics. false or { enabled: false } turns them off.
debugfalseAlso prints every span to the console. Starts the SDK even when nothing is exported.

Resource

OptionDefaultEffect
serviceNamedetectedLocal service.name. With a DSN, Unhandled files data under the DSN's service whatever this says.
serviceVersiondetectedThe release. Placeholders such as 0.0.1 are skipped.
resourceAttributes{}Extra resource attributes. They win over the ones the SDK sets.

Sampling

OptionDefaultEffect
samplingRatio1Head 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.
samplernoneYour own OpenTelemetry Sampler. Wins over samplingRatio.
samplingnonePer-job callback built with Sample. See Job sampling. Forces samplingRatio to 1.

HTTP

OptionDefaultEffect
ignoreOptionsRequeststrueDrops CORS preflight (OPTIONS) server spans.
http.ignoredUrls[]Paths that never get a server span, added to the built-in list.
http.sanitizeUrlsonRewrites secrets out of URLs on SERVER and CLIENT spans.
http.captureBodiesoffRecords request and response bodies and headers, with redaction.

HTTP capture and redaction covers all three.

Instrumentations and integrations

OptionDefaultEffect
instrumentationssee belowConfig 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

OptionDefaultEffect
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.
processHandlerstrueReports uncaught exceptions and unhandled rejections. See Process handlers.
sourceContext.rootentry script's projectOnly source files inside this directory get source lines. See Which files are read.
sourceContext.includeNodeModulesfalseAlso 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:

SituationWhy
enabled: falseExplicitly off.
NODE_ENV=test and enabled not setTests stay quiet by default.
No DSN, an empty UNHANDLED_DSN=, and no apiKey with endpointNothing to send to.
dev: true without endpointDev mode never uses the DSN.
Invalid DSN or invalid apiKeyThe process warning says why.

Extra destinations still receive data in the no-DSN and dev cases.

Extra destinations

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

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

AttributeValue
db.system.nameclickhouse
db.operation.nameThe statement's first keyword, such as SELECT, INSERT or ALTER.
db.collection.nameThe table after FROM, INTO or TABLE, when there is one.
db.query.textThe 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.namespaceThe client's database.
server.address, server.portFrom the client's url.
error.type, db.response.status_codeOn 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:

  • OPTIONS requests, while ignoreOptionsRequests is true.
  • /health, /healthz, /internal/healthz, /ready, /readiness, /metrics, /internal/metrics, /favicon.ico, /robots.txt, /sitemap.xml, /manifest.json, /site.webmanifest, /browserconfig.xml and /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, nonce and hash. These extra names match exactly, so hashtag and passage stay.
  • 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 as release-notes-2024 and ABC-DEF-GHIJ-2024 stay. 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. Add queryParams or a custom sanitize for 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.

ts
import { init } from '@unhandled/node'

init({
  http: {
    sanitizeUrls: { queryParams: ['invite'] },
  },
})
OptionDefaultEffect
queryParams[]More parameter names whose values are replaced. Case and separators are ignored, so accessToken and access_token match.
censor'REDACTED'Replacement text.
sanitizenone(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 as REDACTED. Set sanitizeUrls: false to keep the old output.
  • queryParams now adds to the built-in names instead of replacing them.
  • A custom sanitize now also rewrites url.path.

Body capture

ts
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'] },
    },
  },
})
OptionDefaultEffect
enabledfalseTurns on all four sides. A per-side enabled wins over it.
incoming.request, incoming.responseoffRequests to your app and its responses. A framework package records these.
outgoing.request, outgoing.responseoffCalls made with node:http and node:https, and their responses.
maxBodySize10000Bytes kept per body, at the top level or per side. Longer bodies are cut.
redact.patternsbuilt-in listSubstrings. Any key that contains one is redacted. Replaces the built-in patterns.
redact.bodyKeys, redact.headers, redact.queryParamsbuilt-in listExact keys whose values are replaced, added to the built-in names.
redact.censor'REDACTED'Replacement text.
transformnone(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:

ts
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() and captureMessage() 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 .env and .env.production are never read.
  • No directory below the root starts with a dot, except build output: .next, .nuxt, .output, .mastra, .svelte-kit and .vercel, plus pnpm's node_modules/.pnpm when includeNodeModules is on. .git, .ssh, .aws, .config and every other dot directory are skipped.
  • It ends in .js, .mjs, .cjs, .ts, .mts, .cts, .jsx or .tsx.
  • It is not under node_modules, unless sourceContext.includeNodeModules is true.
  • 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:

  1. It takes the nearest directory above the entry script that has a package.json. A launcher inside node_modules, such as next start, counts from the project that installed it.
  2. When a directory above that one is a workspace root (it has pnpm-workspace.yaml, or a package.json with workspaces), the root widens to it, so frames in sibling workspace packages and source maps pointing at ../src keep their lines.
  3. 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.
  4. Without a package.json, the root is the working directory.
ts
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

ts
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

ts
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_exception and handled: 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-rejections mode says.
  • If your app registers its own uncaughtException or unhandledRejection listener, the SDK reports and leaves the rest to your listener.

Analytics events

ts
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.

ts
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.

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

FunctionEffect
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

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

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

  1. serviceName
  2. OTEL_SERVICE_NAME
  3. APP_NAME
  4. The name in package.json in the working directory, without its npm scope
  5. unknown_service

The version is the first usable value of:

  1. serviceVersion
  2. UNHANDLED_VERSION
  3. APP_VERSION
  4. A platform commit: VERCEL_GIT_COMMIT_SHA, RENDER_GIT_COMMIT, RAILWAY_GIT_COMMIT_SHA, HEROKU_SLUG_COMMIT, SOURCE_VERSION, SOURCE_COMMIT, GIT_REV or K_REVISION
  5. A CI commit: GITHUB_SHA, CI_COMMIT_SHA, CIRCLE_SHA1, BITBUCKET_COMMIT or BUILDKITE_COMMIT
  6. The HEAD commit of the local .git directory

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:

AttributeValue
service.versionThe detected version, or 0.0.0 when none is found.
unhandled.service.version.sourceWhere the version came from, such as config, env:APP_VERSION, platform:render, ci:github_actions or git.
vcs.ref.head.revisionThe commit SHA, when one is known.
unhandled.language.namejavascript.
unhandled.sdk.name, unhandled.sdk.versionThe package that started the SDK and its version.
unhandled.framework.nameSet by the framework packages, such as express.
service.instance.idHOSTNAME 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 pointExports
@unhandled/nodeinit, 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/registerSide effect only. Starts the SDK from the environment.
@unhandled/node/hookstartWithEsmHook
@unhandled/node/frameworkThe building blocks the framework packages use, among them getClient, start, resolveConfig, frameworkLayer, serverSpan, setRoute, capturesBody, captureRequest, captureResponse, reportException
@unhandled/node/typesTypes 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.