Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,12 @@ Affected SDKs: `@sentry/node` and all dependents.

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.

### `setupKoaErrorHandler` is deprecated (Koa errors are captured automatically)

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`).

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.

### Initializing via `--require` is no longer supported

Affected SDKs: `@sentry/node` and all dependents.
Expand Down
2 changes: 0 additions & 2 deletions dev-packages/e2e-tests/test-applications/node-koa/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ const http = require('http');
const app1 = new Koa();
app1.use(bodyParser());

Sentry.setupKoaErrorHandler(app1);

const router1 = new Router();

router1.get('/test-success', ctx => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ test('Sends an API route transaction', async ({ baseURL }) => {
},
});

expect(transactionEvent.spans).toEqual([
const spans = transactionEvent.spans || [];

expect(spans).toEqual([
{
data: {
'koa.name': 'bodyParser',
Expand All @@ -80,24 +82,6 @@ test('Sends an API route transaction', async ({ baseURL }) => {
timestamp: expect.any(Number),
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
},
{

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do not add a middleware ourselves anymore, so this disappears here.

data: {
'koa.name': 'middleware',
'code.function.name': 'middleware',
'koa.type': 'middleware',
'sentry.origin': 'auto.http.koa',
'sentry.op': 'middleware',
},
op: 'middleware',
origin: 'auto.http.koa',
description: 'middleware',
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
span_id: expect.stringMatching(/[a-f0-9]{16}/),
start_timestamp: expect.any(Number),
status: 'ok',
timestamp: expect.any(Number),
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
},
{
data: {
'http.route': '/test-transaction',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import Router from '@koa/router';
import * as Sentry from '@sentry/node';
import { sendPortToRunner } from '@sentry-internal/node-integration-tests';
import Koa from 'koa';

const port = 5698;

const app = new Koa();

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

// Plain middleware -> produces a `middleware` span named after the function.
app.use(async function simpleMiddleware(ctx, next) {
Expand Down
10 changes: 10 additions & 0 deletions dev-packages/node-integration-tests/suites/tracing/koa/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ describe('koa auto-instrumentation', () => {
const origin = isOrchestrionEnabled() ? 'auto.http.koa' : 'auto.http.otel.koa';

const EXPECTED_ERROR_EVENT = {
// The error is captured within the request's koa span, so it keeps its trace
// linkage (a `parent_span_id`) even though koa emits `error` after the
// middleware chain has unwound.
contexts: {
trace: {
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
span_id: expect.stringMatching(/[a-f0-9]{16}/),
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
},
},
exception: {
values: [
{
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/index.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export {
setAttributes,
setupExpressErrorHandler,
setupHapiErrorHandler,
// oxlint-disable-next-line typescript/no-deprecated
setupKoaErrorHandler,
setUser,
spanToBaggageHeader,
Expand Down
1 change: 1 addition & 0 deletions packages/aws-serverless/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export {
expressErrorHandler,
setupExpressErrorHandler,
koaIntegration,
// oxlint-disable-next-line typescript/no-deprecated
setupKoaErrorHandler,
fastifyIntegration,
firebaseIntegration,
Expand Down
1 change: 1 addition & 0 deletions packages/bun/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export {
setupFastifyErrorHandler,
firebaseIntegration,
koaIntegration,
// oxlint-disable-next-line typescript/no-deprecated
setupKoaErrorHandler,
genericPoolIntegration,
graphqlIntegration,
Expand Down
1 change: 1 addition & 0 deletions packages/elysia/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export {
setupFastifyErrorHandler,
firebaseIntegration,
koaIntegration,
// oxlint-disable-next-line typescript/no-deprecated
setupKoaErrorHandler,
genericPoolIntegration,
graphqlIntegration,
Expand Down
1 change: 1 addition & 0 deletions packages/google-cloud-serverless/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export {
expressErrorHandler,
setupExpressErrorHandler,
koaIntegration,
// oxlint-disable-next-line typescript/no-deprecated
setupKoaErrorHandler,
fastifyIntegration,
firebaseIntegration,
Expand Down
1 change: 1 addition & 0 deletions packages/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export {
instrumentStateGraphCompile,
} from '@sentry/server-utils';
export { setupHapiErrorHandler } from './integrations/tracing/hapi';
// oxlint-disable-next-line typescript/no-deprecated -- deprecated but still re-exported for backwards compatibility
export { setupKoaErrorHandler } from './integrations/tracing/koa';
export {
launchDarklyIntegration,
Expand Down
33 changes: 33 additions & 0 deletions packages/node/src/integrations/tracing/koa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { attachKoaErrorHandler } from '@sentry/server-utils';

/**
* Add a Koa error handler to capture errors to Sentry.
*
* @deprecated The error handler is now registered automatically when the Koa app
* starts (via the orchestrion `koa` instrumentation), so calling this is no
* longer necessary. It remains a safe, idempotent operation, and is kept for
* setups where auto-registration is unavailable. This will be removed in a
* future major version.
*
* @param app The Koa app instance
*
* @example
* ```javascript
* const Sentry = require('@sentry/node');
* const Koa = require("koa");
*
* const app = new Koa();
*
* // Add your routes, etc.
*
* app.listen(3000);
* ```
*/

export const setupKoaErrorHandler = (app: {
// oxlint-disable-next-line no-explicit-any
on: (event: 'error', listener: (...args: any[]) => void) => unknown;
}): void => {
Comment thread
cursor[bot] marked this conversation as resolved.
// oxlint-disable-next-line typescript/no-deprecated -- internal delegation to the shared implementation
attachKoaErrorHandler(app);
};
40 changes: 0 additions & 40 deletions packages/node/src/integrations/tracing/koa/index.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/remix/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export {
setAttributes,
setupExpressErrorHandler,
setupHapiErrorHandler,
// oxlint-disable-next-line typescript/no-deprecated
setupKoaErrorHandler,
setUser,
spanToBaggageHeader,
Expand Down
2 changes: 2 additions & 0 deletions packages/server-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export * from './exports';

// Exports using diagnostics channels
export { prismaIntegration } from './prisma';
// oxlint-disable-next-line typescript/no-deprecated -- re-exported so the deprecated `setupKoaErrorHandler` can delegate
export { attachKoaErrorHandler } from './integrations/koa/koa-error-handler';
export { bindTracingChannelToSpan } from './tracing-channel';
export type { TracingChannelPayloadWithSpan } from './tracing-channel';
export type { InstrumentationConfig } from './orchestrion';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as diagnosticsChannel from 'node:diagnostics_channel';
import type { IntegrationFn } from '@sentry/core';
import {
addNonEnumerableProperty,
debug,
defineIntegration,
getActiveSpan,
Expand All @@ -12,11 +13,12 @@ import {
// oxlint-disable-next-line typescript/no-deprecated
import { CODE_FUNCTION_NAME, HTTP_ROUTE, KOA_NAME, KOA_TYPE, SENTRY_OP } from '@sentry/conventions/attributes';
import { WEB_SERVER_MIDDLEWARE_SPAN_OP } from '@sentry/conventions/op';
import { DEBUG_BUILD } from '../debug-build';
import { CHANNELS } from '../orchestrion/channels';
import { koaModuleNames } from '../orchestrion/config/koa';
import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation';
import { setHttpServerSpanRouteAttribute } from '../utils/setHttpServerSpanRouteAttribute';
import { DEBUG_BUILD } from '../../debug-build';
import { CHANNELS } from '../../orchestrion/channels';
import { koaModuleNames } from '../../orchestrion/config/koa';
import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation';
import { setHttpServerSpanRouteAttribute } from '../../utils/setHttpServerSpanRouteAttribute';
import { attachKoaErrorHandler, KOA_CONTEXT_SPAN, type KoaApp } from './koa-error-handler';

// Same name as the OTel integration. When enabled, the OTel 'Koa' integration is omitted from the default set.
const INTEGRATION_NAME = 'Koa' as const;
Expand Down Expand Up @@ -67,6 +69,11 @@ interface KoaUseContext {
arguments: unknown[];
}

/** The `callback` channel `context` shape: `self` is the live app to attach the error listener to. */
interface KoaCallbackContext {
self?: KoaApp;
}

export interface KoaIntegrationOptions {
/** Ignore layers of the specified types (`'middleware'` and/or `'router'`). */
ignoreLayersType?: Array<'middleware' | 'router'>;
Expand Down Expand Up @@ -95,6 +102,24 @@ function instrumentKoa(ignoreLayersType: KoaLayerType[]): void {
asyncEnd() {},
error() {},
});

// Auto-register the error handler once the app boots.
// We act on `end` (after `callback()` ran) so
// koa's default `error` listener is already in place; `attachKoaErrorHandler`
// is idempotent, so repeated `callback()` calls add at most one listener.
diagnosticsChannel.tracingChannel(CHANNELS.KOA_CALLBACK).subscribe({
start() {},
end(rawCtx) {
const app = (rawCtx as KoaCallbackContext).self;
if (app) {
// oxlint-disable-next-line typescript/no-deprecated -- internal auto-registration entrypoint
attachKoaErrorHandler(app);
}
Comment thread
cursor[bot] marked this conversation as resolved.
},
asyncStart() {},
asyncEnd() {},
error() {},
});
}

function handleUse(ctx: KoaUseContext, ignoreLayersType: KoaLayerType[]): void {
Expand Down Expand Up @@ -186,6 +211,16 @@ function patchLayer(
},
},
() => {
// Stash the outermost koa span (first layer wins) on the koa `ctx`, so the
// error listener can capture within it — koa emits its `error` event after
// the middleware chain (and its spans) have unwound, when no span is active.
if (!context[KOA_CONTEXT_SPAN]) {
const activeSpan = getActiveSpan();
if (activeSpan) {
addNonEnumerableProperty(context, KOA_CONTEXT_SPAN, activeSpan);
}
}

const route = metadata.attributes[HTTP_ROUTE];
if (getIsolationScope() === getDefaultIsolationScope()) {
DEBUG_BUILD && debug.warn('Isolation scope is default isolation scope - skipping setting transactionName');
Expand Down
68 changes: 68 additions & 0 deletions packages/server-utils/src/integrations/koa/koa-error-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { Span } from '@sentry/core';
import { addNonEnumerableProperty, captureException, withActiveSpan } from '@sentry/core';

// Marks a koa app as already carrying the Sentry error listener, so repeat
// attachments only ever register a single listener — whether reached via the
// `callback` channel or a lingering manual `setupKoaErrorHandler` call.
const ERROR_HANDLER_ATTACHED = '__SENTRY_KOA_ERROR_HANDLER_ATTACHED__';

/**
* Key under which the koa instrumentation stashes the request's active span on
* the koa `ctx`. Koa emits its `error` event from `handleRequest`'s `.catch()`,
* *after* the middleware chain has unwound and no span is active — so we capture
* within this stashed span to keep the error linked to the request's trace.
*/
export const KOA_CONTEXT_SPAN = '__SENTRY_KOA_SPAN__';

/** The subset of a koa `Application` the error handler needs (it extends `EventEmitter`). */
export interface KoaApp {
on(event: 'error', listener: (error: unknown, context?: unknown) => void): unknown;
[key: string]: unknown;
}

type MarkedKoaApp = KoaApp & { [ERROR_HANDLER_ATTACHED]?: boolean };

/**
* Attach a Sentry error listener to a koa app's `error` event.
*
* Koa emits `'error'` for every request error that bubbles up unhandled, so a
* single `app.on('error')` listener captures the same errors a top-level
* try/catch middleware would — without depending on middleware order. The error
* is captured within the request's koa span (stashed on the koa `ctx` under
* {@link KOA_CONTEXT_SPAN}) so it keeps its trace linkage, since koa emits the
* event after the middleware spans have already ended.
*
* Idempotent — the app is marked so auto-registration (via the `callback`
* channel) and any explicit `setupKoaErrorHandler` call never stack up multiple
* listeners.
*
* @deprecated Internal. The error handler is registered automatically by the koa
* instrumentation; there is no need to call this directly. It is exported only
* so the deprecated `setupKoaErrorHandler` can delegate to it, and will be
* removed in a future major version.
*/
export function attachKoaErrorHandler(app: KoaApp): void {
const markedApp = app as MarkedKoaApp;
if (!markedApp || typeof markedApp.on !== 'function' || markedApp[ERROR_HANDLER_ATTACHED]) {
return;
}
addNonEnumerableProperty(markedApp, ERROR_HANDLER_ATTACHED, true);

markedApp.on('error', (error: unknown, context?: unknown) => {
const span = (context as { [KOA_CONTEXT_SPAN]?: Span } | undefined)?.[KOA_CONTEXT_SPAN];
const capture = (): void => {
captureException(error, {
mechanism: {
type: 'auto.middleware.koa',
handled: false,
},
});
};

if (span) {
withActiveSpan(span, capture);
} else {
capture();
}
});
Comment thread
cursor[bot] marked this conversation as resolved.
}
Loading
Loading