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.
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'),
})dsnstringEndpoint, API key and service id in one string. See DSN.
devbooleandefaultfalseDev mode. The agent ignores the DSN and sends data only to
endpoint, when you set one. The generated config sets it toenv.get('NODE_ENV') === 'development'.endpointstringBase URL of an OTLP receiver. Replaces the host from the DSN. In dev mode it is the only target.
apiKeystringLegacy connection. The agent uses it only together with
endpoint, and only whendsnis unset.environmentproduction | staging | developmentdefaultUNHANDLED_ENV, APP_ENV, NODE_ENV, else developmentThe environment Unhandled files your data under. Without it the agent reads
UNHANDLED_ENV, thenAPP_ENV, thenNODE_ENVwhen that is one of the three values, and usesdevelopmentotherwise. Any other value in the option or inUNHANDLED_ENVthrows.enabledbooleandefaulttruefalseturns the agent off. UnderNODE_ENV=testthe default isfalse, so settrueto run it in tests.
compressionboolean | 'gzip' | 'none'defaulttruegzip for traces, logs and metrics.
falseor'none'sends uncompressed bodies.batch.maxExportBatchSizenumberdefault512Spans or log records in one export request.
batch.scheduledDelayMillisnumberdefault5000Milliseconds between batch exports.
batch.exportTimeoutMillisnumberdefault30000Timeout of one export request, for every signal.
batch.maxQueueSizenumberdefault2048Spans or log records buffered in memory. Past this, new ones are dropped.
shutdownTimeoutMillisnumberdefault5000Longest wait for
flush()andshutdown(). Neither one rejects. When the ingest is slow or down, telemetry still unsent at the deadline is dropped and the app moves on.hostMetricsboolean | objectdefaulttrueHost CPU, memory and network metrics.
falseor{ enabled: false }turns them off.destinationsOtlpDestination[]default[]More OTLP endpoints that receive the same data. See Extra destinations.
debugbooleandefaultfalsePrints 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
samplingRationumberdefault1Head sampling ratio for traces, between
0and1.0.1keeps one trace in ten. A value outside the range throws. Below1, each kept span carriesunhandled.sampling.ratio, so Unhandled scales request counts and error rates back up to the full traffic.samplingfunctionPer-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 | objectdefaulttrueA span for each email sent with
@adonisjs/mail.queueboolean | objectdefaultenabled, link modeSpans for
@adonisjs/queuedispatch and execution.executionSpanLinkModeis'link'(default) or'parent'.cliboolean | objectdefaultevery command except make:*, generate:*, queue:work, queue:listenSpans for Ace commands.
includedefaults to['*'].ignoreOptionsRequestsbooleandefaulttrueDrops spans for CORS preflight (
OPTIONS) requests.httpobjectIgnored 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:
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.
airemovedUse
@unhandled/instrumentation-vercel-ai.mastraremovedUse
@unhandled/instrumentation-mastra.bullmqremovedUse
@unhandled/instrumentation-bullmq.cacheremovedUse
@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,.tsand similar source files underroot(the app root by default) are read, never dotfiles such as.env, and nevernode_modulesunlessincludeNodeModulesistrue.
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:
serviceNamestringService name for extra destinations and local setups.
serviceVersionstringCode version. The agent skips placeholder values.
resourceAttributesRecord<string, string>Extra resource attributes on every signal. They override the attributes the agent sets.
samplerSamplerYour own OpenTelemetry sampler. It wins over
samplingRatio.instrumentationsobjectPer-instrumentation config keyed by package name. See Instrumentation entries.
spanProcessorsSpanProcessor[]Extra OpenTelemetry span processors.
logRecordProcessorstakes log processors the same way, andmetricReaderstakes 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
Instrumentationinstance. 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| Part | Rule |
|---|---|
| Scheme | https or http. |
| User | The API key. uh_live_ or uh_test_, then 16 to 56 letters, digits, _ or -. |
| Host | The ingest endpoint. The agent posts to /v1/traces, /v1/logs and /v1/metrics on it. |
| Path | One 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:
Store the DSN
Copy the service DSN into
UNHANDLED_DSN, and addUNHANDLED_DSN: Env.schema.string.optional()tostart/env.ts.Switch the config
Replace
apiKeywithdsn: env.get('UNHANDLED_DSN')and deleteendpoint, since the DSN carries both. DeleteserviceNametoo, unless an extra destination needs it.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.
| Situation | Reason |
|---|---|
enabled: false | Turned off on purpose. |
NODE_ENV=test, enabled unset | Tests stay quiet by default. |
No DSN (or UNHANDLED_DSN= empty), and no apiKey with endpoint | No target. Use this to switch one environment off. |
dev: true, no endpoint | Dev mode never reads the DSN. |
Invalid DSN or invalid apiKey | A 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:
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:
namestringrequiredLabel for the destination.
urlstringrequiredBase URL. The agent appends
/v1/traces,/v1/logsand/v1/metrics.headersRecord<string, string>Headers sent with every export.
enabledbooleandefaulttruefalseskips 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:
serviceNameOTEL_SERVICE_NAMEAPP_NAME- The
nameinpackage.json, minus its npm scope (@shop/apibecomesapi) unknown_service
For the version, it takes the first usable value of:
serviceVersionUNHANDLED_VERSIONAPP_VERSION- A platform revision from Vercel, Render, Railway, Heroku, Coolify, Dokku or Cloud Run
- A CI commit from GitHub Actions, GitLab CI, CircleCI, Bitbucket Pipelines or Buildkite
- The
HEADcommit in the local.gitdirectory
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:
| Attribute | Value |
|---|---|
service.version | The detected version. |
unhandled.service.version.source | The source of the version: config, env:UNHANDLED_VERSION, env:APP_VERSION, platform:render, ci:github_actions, git and others. |
vcs.ref.head.revision | The commit SHA, when known. |
service.instance.id | HOSTNAME 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.name | javascript and adonisjs. |