Error tracking

Every exception, grouped once, owned by someone.

Unhandled folds each exception into an issue with its stack trace, the trace that raised it and the users it reached. Route it to its owner, link a ticket, and hear about it again only if it comes back.

One issue per bug
Grouped on the type and your own frames
Regressions
A resolved issue reopens when it returns
Ownership
Rules assign by path, route or service
Linear and GitHub
Closing the ticket resolves the issue
#grouping

A thousand events, one row

Each exception gets a fingerprint from its type and the top three frames of your own code, so an ID or a timestamp in the message never splits a bug in two.

  • Frames from node_modules and the runtime are left out, so a dependency upgrade keeps your issues intact
  • A redeploy to a new directory, or new hashes in chunk names, groups with the old build
  • Similar issues sit on the issue page with a one-click merge
#occurrence

The occurrence worth reading opens first

The inspector picks the occurrence with source context, a trace and an identified user. Step to older or newer ones with the keyboard.

  • Stack frames with the surrounding source, your own code marked apart from libraries
  • The full trace of the request or job, and the logs and spans before the throw
  • Other exceptions recorded in the same trace, each linked to its issue
#source-maps

Readable stacks from minified bundles

Upload source maps from CI with the Unhandled CLI. Frames are mapped back when the exception arrives, before it is grouped, so browser errors group on your real function names.

  • Maps match on the Sentry debug id first, then on the release and the closest file path
  • Uploads authenticate with a project deploy key, never the DSN shipped in your bundle
  • Up to 1 GB of maps per project, replaced in place when you upload a file again
deploy.sh
export UNHANDLED_API_KEY=dk_live_…
export UNHANDLED_DSN=https://…@ingest.unhandled.net/…

npm run build
unhandled sourcemaps upload ./dist --release 2.4.0
#ownership

Route each issue to its owner

Ownership rules map a file path, a route or a service to a person or a team. Turn on auto-assign and the first matching rule assigns each new issue.

  • Globs work like CODEOWNERS: * stays in one segment and ** crosses them
  • Archive until escalating, until more users are hit, for a while, or forever
  • Resolve, assign, archive and step to the next issue without touching the mouse
#notifications

Told once, in the right channel

New and regressed issues are batched for a minute and sent as one message. Exception rules decide which Slack, Discord or webhook channel hears about each service and environment.

  • The most specific rule wins, and a rule can mute a channel type for noisy services
  • Members with exception summaries on get the same batch by email
  • Assignees hear about comments and status changes on their issues
#capture

Report the errors you handle

The AdonisJS agent reports what your exception handler sees, with the signed-in user attached. For an error you catch and recover from, report it yourself.

  • Attach a user, tags you can filter on, and extra data
  • Each error object is reported once, even if you rethrow it
  • Any OpenTelemetry SDK works too: recordException on a span is enough
app/services/checkout_service.ts
import { Unhandled } from '@unhandled/agent-adonisjs'

try {
  await PaymentGateway.charge(order)
} catch (error) {
  Unhandled.captureException(error, {
    user: { id: order.customerId },
    tags: { area: 'checkout', provider: 'stripe' },
    extra: { orderId: order.id },
  })
  await order.markPaymentFailed()
}
#faq

Questions

Try it on your own stack.

Every feature is on every plan, priced by the telemetry you send.