docs

Configuration

Every option of config/unhandled.ts with its real default, the DSN format, environment variables, extra destinations and how export is turned off.

The agent reads config/unhandled.ts, whose default export is the result of defineConfig from @unhandled/agent-adonisjs. This page lists each option with its default, then the rules the agent follows to decide whether to export and where. Every option is optional.

config/unhandled.ts
import env from '#start/env'
import { defineConfig } from '@unhandled/agent-adonisjs'

export default defineConfig({
  dev: env.get('NODE_ENV') === 'development',
  dsn: env.get('UNHANDLED_DSN'),
  environment: env.get('APP_ENV'),
})
dsnstring

Endpoint, API key and service id in one string. See DSN.

devbooleandefault false

Dev mode. The agent ignores the DSN and sends data only to endpoint, when you set one. The generated config sets it to env.get('NODE_ENV') === 'development'.

endpointstring

Base URL of an OTLP receiver. Replaces the host from the DSN. In dev mode it is the only target.

apiKeystring

Legacy connection. The agent uses it only together with endpoint, and only when dsn is unset.

environmentproduction | staging | developmentdefault UNHANDLED_ENV, APP_ENV, NODE_ENV, else development

The environment Unhandled files your data under. Without it the agent reads UNHANDLED_ENV, then APP_ENV, then NODE_ENV when that is one of the three values, and uses development otherwise. Any other value in the option or in UNHANDLED_ENV throws.

enabledbooleandefault true

false turns the agent off. Under NODE_ENV=test the default is false, so set true to run it in tests.

compressionboolean | 'gzip' | 'none'default true

gzip for traces, logs and metrics. false or 'none' sends uncompressed bodies.

batch.maxExportBatchSizenumberdefault 512

Spans or log records in one export request.

batch.scheduledDelayMillisnumberdefault 5000

Milliseconds between batch exports.

batch.exportTimeoutMillisnumberdefault 30000

Timeout of one export request, for every signal.

batch.maxQueueSizenumberdefault 2048

Spans or log records buffered in memory. Past this, new ones are dropped.

shutdownTimeoutMillisnumberdefault 5000

Longest wait for flush() and shutdown(). Neither one rejects. When the ingest is slow or down, telemetry still unsent at the deadline is dropped and the app moves on.

hostMetricsboolean | objectdefault true

Host CPU, memory and network metrics. false or { enabled: false } turns them off.

destinationsOtlpDestination[]default []

More OTLP endpoints that receive the same data. See Extra destinations.

debugbooleandefault false

Prints every span to the console as well. The SDK starts even when nothing is exported.

batch configures both the span and the log record batch processors. Pass only the keys you change. The rest keep their defaults.

Sampling

samplingRationumberdefault 1

Head sampling ratio for traces, between 0 and 1. 0.1 keeps one trace in ten. A value outside the range throws. Below 1, each kept span carries unhandled.sampling.ratio, so Unhandled scales request counts and error rates back up to the full traffic.

samplingfunction

Per-job callback that keeps, drops or samples job traces at a fixed rate. See Job sampling.

Setting sampling forces samplingRatio to 1. Job sampling can decide to keep a failed job after it ends, and that only works when every trace was recorded.

Built-in instrumentations

mailboolean | objectdefault true

A span for each email sent with @adonisjs/mail.

queueboolean | objectdefault enabled, link mode

Spans for @adonisjs/queue dispatch and execution. executionSpanLinkMode is 'link' (default) or 'parent'.

cliboolean | objectdefault every command except make:*, generate:*, queue:work, queue:listen

Spans for Ace commands. include defaults to ['*'].

ignoreOptionsRequestsbooleandefault true

Drops spans for CORS preflight (OPTIONS) requests.

httpobject

Ignored URLs, URL sanitizing and body capture. URL sanitizing is on by default. Ignored URLs add to a built-in list, and body capture stays off until you turn it on. See HTTP tracing.

integrationsUnhandledIntegration[]default []

Integration packages. A name may appear once. A duplicate throws.

mail, queue and cli also take false or { enabled: false }. Mail, queue and Ace commands has the details.

Integrations

Optional instrumentations ship as separate packages. Register each one in integrations:

config/unhandled.ts
import env from '#start/env'
import { defineConfig } from '@unhandled/agent-adonisjs'
import { bullmq } from '@unhandled/instrumentation-bullmq/integration'
import { bentocache } from '@unhandled/instrumentation-bentocache/integration'

export default defineConfig({
  dsn: env.get('UNHANDLED_DSN'),
  environment: env.get('APP_ENV'),
  integrations: [bullmq(), bentocache()],
})

An integration missing from the list records nothing. When one fails during startup, the agent throws an IntegrationSetupError whose message names it. The packages: BullMQ, Bentocache, Vercel AI and Mastra.

Removed options

Version 2.0 removed four top-level options. Passing one throws an UnhandledConfigError that names its replacement package.

airemoved

Use @unhandled/instrumentation-vercel-ai.

mastraremoved

Use @unhandled/instrumentation-mastra.

bullmqremoved

Use @unhandled/instrumentation-bullmq.

cacheremoved

Use @unhandled/instrumentation-bentocache.

ignoreExceptionsErrorClass[]default []

Error classes the exception handler hook skips, matched with instanceof.

ignoreStatusesnumber[]default []

HTTP statuses, read from the error's status, that the hook skips.

ignoreCodesstring[]default []

Error codes, read from the error's code, that the hook skips.

sourceContextobject

{ root?, includeNodeModules? }. Where source lines may come from. Only .js, .ts and similar source files under root (the app root by default) are read, never dotfiles such as .env, and never node_modules unless includeNodeModules is true.

These filters cover only errors that reach the Adonis exception handler. Unhandled.captureException sends regardless. See Users and exceptions.

Options from @unhandled/node

The agent is a thin layer on @unhandled/node. Its config type extends the Node SDK options, and the agent passes them straight through. The common ones:

serviceNamestring

Service name for extra destinations and local setups.

serviceVersionstring

Code version. The agent skips placeholder values.

resourceAttributesRecord<string, string>

Extra resource attributes on every signal. They override the attributes the agent sets.

samplerSampler

Your own OpenTelemetry sampler. It wins over samplingRatio.

instrumentationsobject

Per-instrumentation config keyed by package name. See Instrumentation entries.

spanProcessorsSpanProcessor[]

Extra OpenTelemetry span processors. logRecordProcessors takes log processors the same way, and metricReaders takes metric readers. For a processor that lives in your app, pass a lazy import such as () => import('#services/redacting_processor'): the module's default export is a class with no constructor arguments, and the agent creates it before the SDK starts. The config file then doesn't import app code.

Options on the Node SDK page lists every one. userContext is the only option the agent adds. It controls automatic user extraction in the middleware: the user id and role by default, the email only with userContext: { email: true }, and nothing with false.

Instrumentation entries

Each instrumentations entry takes one of three shapes:

  • A config object. It merges with the built-in config for that package.
  • An Instrumentation instance. It replaces the built-in instrumentation.
  • { enabled: false }. It turns the instrumentation off.

The @opentelemetry/instrumentation-http entry also accepts ignoredUrls, mergeIgnoredUrls, ignoreStaticFiles and ignoreOptionsRequests. A config object there keeps the agent's ignored URLs and outgoing body capture. Your ignoreIncomingRequestHook, requestHook and responseHook run alongside the built-in ones.

DSN

A DSN has this shape:

https://[email protected]/0b4f6c2e-8a1d-4c3e-9f7a-1234567890ab
PartRule
Schemehttps or http.
UserThe API key. uh_live_ or uh_test_, then 16 to 56 letters, digits, _ or -.
HostThe ingest endpoint. The agent posts to /v1/traces, /v1/logs and /v1/metrics on it.
PathOne service UUID and nothing else.

A password, a query string or a fragment makes the DSN invalid. With a valid DSN, every export carries x-api-key, x-unhandled-service-id and x-unhandled-env, the last one set to the resolved environment.

Moving from apiKey to dsn

Older setups pass apiKey, endpoint and serviceName. They still work, and the agent sends x-api-key and x-unhandled-env to endpoint. Move one service at a time:

  1. Store the DSN

    Copy the service DSN into UNHANDLED_DSN, and add UNHANDLED_DSN: Env.schema.string.optional() to start/env.ts.

  2. Switch the config

    Replace apiKey with dsn: env.get('UNHANDLED_DSN') and delete endpoint, since the DSN carries both. Delete serviceName too, unless an extra destination needs it.

  3. Deploy and check

    Deploy, send a request, and confirm the data lands under the right service and environment.

When both dsn and apiKey are set, dsn wins. An endpoint set next to dsn replaces the DSN host, and the DSN still supplies the key and the service.

Turning export off

The agent exports nothing in the cases below. It also skips starting the SDK, unless debug is on.

SituationReason
enabled: falseTurned off on purpose.
NODE_ENV=test, enabled unsetTests stay quiet by default.
No DSN (or UNHANDLED_DSN= empty), and no apiKey with endpointNo target. Use this to switch one environment off.
dev: true, no endpointDev mode never reads the DSN.
Invalid DSN or invalid apiKeyA process warning names the problem.

In the no-DSN and dev-mode cases, extra destinations still receive data. They are separate targets.

Extra destinations

Send the same traces, logs and metrics to a second OTLP backend:

config/unhandled.ts
import env from '#start/env'
import { defineConfig, destinations } from '@unhandled/agent-adonisjs'

export default defineConfig({
  dsn: env.get('UNHANDLED_DSN'),
  environment: env.get('APP_ENV'),
  serviceName: 'shop-api',
  destinations: [
    destinations.otlp({
      name: 'collector',
      url: 'https://otel-collector.internal:4318',
      headers: { authorization: 'Bearer <collector-token>' },
    }),
  ],
})

destinations.otlp takes:

namestringrequired

Label for the destination.

urlstringrequired

Base URL. The agent appends /v1/traces, /v1/logs and /v1/metrics.

headersRecord<string, string>

Headers sent with every export.

enabledbooleandefault true

false skips this destination.

Each destination uses the shared compression and batch settings. The agent always keeps the Unhandled destination built from the DSN and adds the extra ones next to it.

Service name and version

With a DSN, Unhandled takes the service from the DSN. The local name matters for extra destinations, dev mode and legacy setups. The agent takes the first of:

  1. serviceName
  2. OTEL_SERVICE_NAME
  3. APP_NAME
  4. The name in package.json, minus its npm scope (@shop/api becomes api)
  5. unknown_service

For the version, it takes the first usable value of:

  1. serviceVersion
  2. UNHANDLED_VERSION
  3. APP_VERSION
  4. A platform revision from Vercel, Render, Railway, Heroku, Coolify, Dokku or Cloud Run
  5. A CI commit from GitHub Actions, GitLab CI, CircleCI, Bitbucket Pipelines or Buildkite
  6. The HEAD commit in the local .git directory

0.0.0, 0.0.1, 1.0.0, unknown, dev, latest and empty strings count as placeholders, and the agent skips them. So serviceVersion: env.get('APP_VERSION') is safe. If the variable holds 0.0.1, the agent moves to the next source.

Every signal carries these resource attributes:

AttributeValue
service.versionThe detected version.
unhandled.service.version.sourceThe source of the version: config, env:UNHANDLED_VERSION, env:APP_VERSION, platform:render, ci:github_actions, git and others.
vcs.ref.head.revisionThe commit SHA, when known.
service.instance.idHOSTNAME plus the process id, such as web-1-4127, so cluster and PM2 workers on one host report separate series. A random UUID when HOSTNAME is unset.
unhandled.sdk.name, unhandled.sdk.version@unhandled/agent-adonisjs and its version.
unhandled.language.name, unhandled.framework.namejavascript and adonisjs.