Skip to content

Commit 683f190

Browse files
authored
feat(node): Auto-register Koa error handler on app start (#23463)
The Koa error handler is now registered automatically when the app starts, so `setupKoaErrorHandler` no longer needs to be called. This mirrors the Hapi change and removes the last manual setup step for Koa error capture. ### How it works Koa's `Application` is an `EventEmitter` and emits `'error'` for every request error that bubbles up unhandled — the same set of errors a top-level try/catch middleware would see, but **order-independent**. So instead of injecting a `try/catch` middleware (whose position in the onion stack was load-bearing), we attach a single `app.on('error')` listener. Auto-registration rides a new orchestrion channel on `Application.prototype.callback`. `app.listen()` always funnels through `callback()`, so this also covers `http.createServer(app.callback())`. ### Decisions - **Attach on the channel's `end`, not `start`.** Koa registers its own default `error` listener inside `callback()` — but only if none exist yet (`if (!this.listenerCount('error')) this.on('error', this.onerror)`). Attaching before that runs would suppress koa's built-in error logging. Acting on `end` (after the method body) means koa's default listener is already in place and ours is added alongside it. - **`app.on('error')` over a first-position `try/catch` middleware.** Event listeners are additive and order-independent, so there is no ordering conflict with a user's own error handling: if a user middleware catches and handles an error, koa never emits and we correctly don't capture; if it's unhandled, our listener and any user `app.on('error')` both run. - **`setupKoaErrorHandler` stays as an idempotent delegate** (deprecated) rather than a hard no-op, so a direct call still works without orchestrion (e.g. error capture with tracing disabled). An idempotency marker on the app means auto-registration plus a manual call never stack up duplicate listeners. - **`attachKoaErrorHandler` is also deprecated** and marked internal — it exists only so the deprecated `setupKoaErrorHandler` can delegate to it, and should not be called directly. - The Koa integration files were collocated into a `koa/` folder to match the Hapi layout. Auto-registration is exercised end-to-end by dropping the manual call from the koa integration-test scenario and the e2e app; a new unit suite covers the attach behaviour (single idempotent listener, guards, and capture mechanism). A follow-up will do the same for Express.
1 parent a28b354 commit 683f190

20 files changed

Lines changed: 185 additions & 69 deletions

File tree

MIGRATION.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,12 @@ Affected SDKs: `@sentry/node` and all dependents.
287287

288288
The new channel-based instrumentations (using `orchestrion` instead of `import-in-the-middle`) are now the default. They were available opt-in in v10. This unlocks instrumenting at run and build time, which enables instrumentation at deployment targets like Vercel and Netlify, as well as using instrumentations on non-Node runtimes like Cloudflare, Bun and Deno. For most users this requires no changes.
289289

290+
### `setupKoaErrorHandler` is deprecated (Koa errors are captured automatically)
291+
292+
Affected SDKs: `@sentry/node` and all dependents that re-export it (e.g. `@sentry/aws-serverless`, `@sentry/google-cloud-serverless`, `@sentry/astro`, `@sentry/remix`, `@sentry/solidstart`, `@sentry/sveltekit`, `@sentry/bun`, `@sentry/elysia`).
293+
294+
The Koa error handler is now registered automatically when your app starts, so you no longer need to call `setupKoaErrorHandler`. The function is deprecated and will be removed in a future major version; you should no longer call it.
295+
290296
### Initializing via `--require` is no longer supported
291297

292298
Affected SDKs: `@sentry/node` and all dependents.

dev-packages/e2e-tests/test-applications/node-koa/index.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ const http = require('http');
2222
const app1 = new Koa();
2323
app1.use(bodyParser());
2424

25-
Sentry.setupKoaErrorHandler(app1);
26-
2725
const router1 = new Router();
2826

2927
router1.get('/test-success', ctx => {

dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ test('Sends an API route transaction', async ({ baseURL }) => {
6666
},
6767
});
6868

69-
expect(transactionEvent.spans).toEqual([
69+
const spans = transactionEvent.spans || [];
70+
71+
expect(spans).toEqual([
7072
{
7173
data: {
7274
'koa.name': 'bodyParser',
@@ -85,24 +87,6 @@ test('Sends an API route transaction', async ({ baseURL }) => {
8587
timestamp: expect.any(Number),
8688
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
8789
},
88-
{
89-
data: {
90-
'koa.name': 'middleware',
91-
'code.function.name': 'middleware',
92-
'koa.type': 'middleware',
93-
'sentry.origin': 'auto.http.koa',
94-
'sentry.op': 'middleware',
95-
},
96-
op: 'middleware',
97-
origin: 'auto.http.koa',
98-
description: 'middleware',
99-
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
100-
span_id: expect.stringMatching(/[a-f0-9]{16}/),
101-
start_timestamp: expect.any(Number),
102-
status: 'ok',
103-
timestamp: expect.any(Number),
104-
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
105-
},
10690
{
10791
data: {
10892
'http.route': '/test-transaction',

dev-packages/node-integration-tests/suites/tracing/koa/scenario.mjs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import Router from '@koa/router';
2-
import * as Sentry from '@sentry/node';
32
import { sendPortToRunner } from '@sentry-internal/node-integration-tests';
43
import Koa from 'koa';
54

65
const port = 5698;
76

87
const app = new Koa();
98

10-
// Registered first so it wraps every downstream middleware/route in its try/catch.
11-
Sentry.setupKoaErrorHandler(app);
9+
// The error handler is auto-registered by the koa instrumentation on app start,
10+
// so `setupKoaErrorHandler` is intentionally not called here.
1211

1312
// Plain middleware -> produces a `middleware` span named after the function.
1413
app.use(async function simpleMiddleware(ctx, next) {

dev-packages/node-integration-tests/suites/tracing/koa/test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ describe('koa auto-instrumentation', () => {
99
const origin = 'auto.http.koa';
1010

1111
const EXPECTED_ERROR_EVENT = {
12+
// The error is captured within the request's koa span, so it keeps its trace
13+
// linkage (a `parent_span_id`) even though koa emits `error` after the
14+
// middleware chain has unwound.
15+
contexts: {
16+
trace: {
17+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
18+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
19+
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
20+
},
21+
},
1222
exception: {
1323
values: [
1424
{

packages/astro/src/index.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export {
126126
setAttributes,
127127
setupExpressErrorHandler,
128128
setupHapiErrorHandler,
129+
// oxlint-disable-next-line typescript/no-deprecated
129130
setupKoaErrorHandler,
130131
setUser,
131132
spanToBaggageHeader,

packages/aws-serverless/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export {
9696
expressErrorHandler,
9797
setupExpressErrorHandler,
9898
koaIntegration,
99+
// oxlint-disable-next-line typescript/no-deprecated
99100
setupKoaErrorHandler,
100101
fastifyIntegration,
101102
firebaseIntegration,

packages/bun/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export {
120120
setupFastifyErrorHandler,
121121
firebaseIntegration,
122122
koaIntegration,
123+
// oxlint-disable-next-line typescript/no-deprecated
123124
setupKoaErrorHandler,
124125
genericPoolIntegration,
125126
graphqlIntegration,

packages/elysia/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export {
9999
setupFastifyErrorHandler,
100100
firebaseIntegration,
101101
koaIntegration,
102+
// oxlint-disable-next-line typescript/no-deprecated
102103
setupKoaErrorHandler,
103104
genericPoolIntegration,
104105
graphqlIntegration,

packages/google-cloud-serverless/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export {
9797
expressErrorHandler,
9898
setupExpressErrorHandler,
9999
koaIntegration,
100+
// oxlint-disable-next-line typescript/no-deprecated
100101
setupKoaErrorHandler,
101102
fastifyIntegration,
102103
firebaseIntegration,

0 commit comments

Comments
 (0)