From 55e28fc3840f32c73b9d8db7ab90a8bbdfe6635e Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 20 Aug 2026 16:23:25 +0200 Subject: [PATCH 01/12] feat(node): Capture Express errors automatically via `expressIntegration` `expressIntegration()` now captures errors thrown from route handlers on its own, at the throw site (before user error-handling middleware runs), gated by a new `shouldHandleError` option. Passing `shouldHandleError: false` opts out entirely. This moves Express error capture into `@sentry/server-utils` (alongside the channel-based tracing) and deprecates the now-superseded core Express exports (`setupExpressErrorHandler`, `expressErrorHandler`, `patchExpressModule` and the related types), to be removed in the next major. Co-Authored-By: Claude Opus 4.8 (1M context) --- MIGRATION.md | 10 +++ .../tests/errors.test.ts | 2 +- .../node-express/tests/errors.test.ts | 2 +- .../instrument-should-handle-error.mjs | 16 ++++ .../scenario-should-handle-error.mjs | 8 +- .../suites/express/handle-error/test.ts | 61 +++++++------- packages/astro/src/index.server.ts | 2 + packages/aws-serverless/src/index.ts | 2 + packages/bun/src/index.ts | 2 + .../core/src/integrations/express/index.ts | 14 ++++ .../src/integrations/express/patch-layer.ts | 4 + .../core/src/integrations/express/types.ts | 16 ++++ .../core/src/integrations/express/utils.ts | 4 + packages/core/src/server-exports.ts | 3 +- packages/elysia/src/index.ts | 2 + packages/google-cloud-serverless/src/index.ts | 2 + packages/node/src/index.ts | 1 + .../node/src/integrations/tracing/express.ts | 11 +++ packages/remix/src/server/index.ts | 2 + .../integrations/express/instrumentation.ts | 45 ++++++++++- .../src/integrations/express/types.ts | 40 ++++++++++ .../src/integrations/express/utils.ts | 15 ++++ .../express-error-handler.test.ts | 80 +++++++++++++++++++ packages/solidstart/src/server/index.ts | 2 + packages/sveltekit/src/server/index.ts | 2 + 25 files changed, 309 insertions(+), 39 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/express/handle-error/instrument-should-handle-error.mjs create mode 100644 packages/server-utils/src/integrations/express/utils.ts create mode 100644 packages/server-utils/test/integrations/express-error-handler.test.ts diff --git a/MIGRATION.md b/MIGRATION.md index 75a8e327d62e..07a21a647602 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -819,6 +819,16 @@ Affected SDKs: All server-side SDKs. The LangGraph instrumentation no longer emits `gen_ai.create_agent` spans when a graph is compiled. `gen_ai.invoke_agent` and `gen_ai.execute_tool` spans are unaffected. If you reference `create_agent` spans in dashboards or alerts, update them accordingly. +### Express: errors are captured automatically + +Affected SDKs: All server-side SDKs that support Express. + +`expressIntegration()` now captures errors thrown from your route handlers automatically, so calling `setupExpressErrorHandler(app)` is no longer necessary — the call can be removed. It is deprecated and will be removed in the next major version. To customize which errors are captured, pass `shouldHandleError` to `expressIntegration()` (by default, 5xx errors and errors without a resolvable status are captured, while 3xx/4xx errors are not). + +If you prefer to capture errors yourself, set `shouldHandleError: false` on `expressIntegration()` to opt out of automatic capture entirely, and call `Sentry.captureException` from your own error-handling middleware. + +The `expressErrorHandler` and `patchExpressModule` exports are deprecated for the same reason and will be removed in the next major version. + ### Span name changes Affected SDKs: All SDKs. diff --git a/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/errors.test.ts index 21f0c4e2c30f..8b24ed9039d8 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/errors.test.ts @@ -21,7 +21,7 @@ test('Sends correct error event', async ({ baseURL }) => { const exception = errorEvent.exception?.values?.[0]; expect(exception?.value).toBe('This is an exception with id 123'); expect(exception?.mechanism).toEqual({ - type: 'auto.middleware.express', + type: 'auto.http.express', handled: false, }); diff --git a/dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts index a791bbc29189..7fa0c63f037f 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts @@ -19,7 +19,7 @@ test('Sends correct error event', async ({ baseURL }) => { const exception = errorEvent.exception?.values?.[0]; expect(exception?.value).toBe('This is an exception with id 123'); expect(exception?.mechanism).toEqual({ - type: 'auto.middleware.express', + type: 'auto.http.express', handled: false, }); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error/instrument-should-handle-error.mjs b/dev-packages/node-integration-tests/suites/express/handle-error/instrument-should-handle-error.mjs new file mode 100644 index 000000000000..db661c5bf89b --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/handle-error/instrument-should-handle-error.mjs @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + integrations: [ + Sentry.expressIntegration({ + shouldHandleError: error => { + return error.message === 'error_2'; + }, + }), + ], +}); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error/scenario-should-handle-error.mjs b/dev-packages/node-integration-tests/suites/express/handle-error/scenario-should-handle-error.mjs index 335e89107e58..dfbec70cdea2 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error/scenario-should-handle-error.mjs +++ b/dev-packages/node-integration-tests/suites/express/handle-error/scenario-should-handle-error.mjs @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; import cors from 'cors'; import express from 'express'; @@ -15,10 +14,7 @@ app.get('/test2', (_req, _res) => { throw new Error('error_2'); }); -Sentry.setupExpressErrorHandler(app, { - shouldHandleError: error => { - return error.message === 'error_2'; - }, -}); +// `shouldHandleError` is configured on `expressIntegration` (see the instrument file); no +// error handler needs to be registered on the app anymore. startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error/test.ts b/dev-packages/node-integration-tests/suites/express/handle-error/test.ts index 5819a322e0c6..1e6879d281d1 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error/test.ts +++ b/dev-packages/node-integration-tests/suites/express/handle-error/test.ts @@ -31,7 +31,7 @@ describe('express error handling', () => { values: [ { mechanism: { - type: 'auto.middleware.express', + type: 'auto.http.express', handled: false, }, type: 'Error', @@ -68,7 +68,7 @@ describe('express error handling', () => { values: [ { mechanism: { - type: 'auto.middleware.express', + type: 'auto.http.express', handled: false, }, type: 'Error', @@ -106,7 +106,7 @@ describe('express error handling', () => { values: [ { mechanism: { - type: 'auto.middleware.express', + type: 'auto.http.express', handled: false, }, type: 'Error', @@ -148,7 +148,7 @@ describe('express error handling', () => { values: [ { mechanism: { - type: 'auto.middleware.express', + type: 'auto.http.express', handled: false, }, type: 'Error', @@ -187,7 +187,7 @@ describe('express error handling', () => { values: [ { mechanism: { - type: 'auto.middleware.express', + type: 'auto.http.express', handled: false, }, type: 'Error', @@ -235,7 +235,7 @@ describe('express error handling', () => { values: [ { mechanism: { - type: 'auto.middleware.express', + type: 'auto.http.express', handled: false, }, type: 'Error', @@ -265,30 +265,35 @@ describe('express error handling', () => { }); }); - describe('setupExpressErrorHandler options', () => { - createCjsTests(__dirname, 'scenario-should-handle-error.mjs', 'instrument-no-tracing.mjs', (createRunner, test) => { - test('allows to pass options to setupExpressErrorHandler', async () => { - const runner = createRunner() - .expect({ - event: { - exception: { - values: [ - { - value: 'error_2', - }, - ], + describe('expressIntegration shouldHandleError option', () => { + createCjsTests( + __dirname, + 'scenario-should-handle-error.mjs', + 'instrument-should-handle-error.mjs', + (createRunner, test) => { + test('captures only errors for which shouldHandleError returns true', async () => { + const runner = createRunner() + .expect({ + event: { + exception: { + values: [ + { + value: 'error_2', + }, + ], + }, }, - }, - }) - .start(); + }) + .start(); - // this error is filtered & ignored - runner.makeRequest('get', '/test1', { expectError: true }); - // this error is actually captured - runner.makeRequest('get', '/test2', { expectError: true }); + // this error is filtered & ignored + runner.makeRequest('get', '/test1', { expectError: true }); + // this error is actually captured + runner.makeRequest('get', '/test2', { expectError: true }); - await runner.completed(); - }); - }); + await runner.completed(); + }); + }, + ); }); }); diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index e3c3c6ec5f1f..7e34a1727574 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -37,6 +37,7 @@ export { dedupeIntegration, defaultStackParser, endSession, + // oxlint-disable-next-line typescript/no-deprecated expressErrorHandler, expressIntegration, extraErrorDataIntegration, @@ -124,6 +125,7 @@ export { setTags, setAttribute, setAttributes, + // oxlint-disable-next-line typescript/no-deprecated setupExpressErrorHandler, // oxlint-disable-next-line typescript/no-deprecated setupHapiErrorHandler, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 994ac3e54d34..d48be039ba2f 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -93,7 +93,9 @@ export { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, dataloaderIntegration, expressIntegration, + // oxlint-disable-next-line typescript/no-deprecated expressErrorHandler, + // oxlint-disable-next-line typescript/no-deprecated setupExpressErrorHandler, koaIntegration, // oxlint-disable-next-line typescript/no-deprecated diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 3d311e4ff70a..5bae62b97c69 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -114,7 +114,9 @@ export { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, dataloaderIntegration, expressIntegration, + // oxlint-disable-next-line typescript/no-deprecated expressErrorHandler, + // oxlint-disable-next-line typescript/no-deprecated setupExpressErrorHandler, fastifyIntegration, // oxlint-disable-next-line typescript/no-deprecated diff --git a/packages/core/src/integrations/express/index.ts b/packages/core/src/integrations/express/index.ts index af6e72f8fd26..ce17912fe1d5 100644 --- a/packages/core/src/integrations/express/index.ts +++ b/packages/core/src/integrations/express/index.ts @@ -27,6 +27,10 @@ * limitations under the License. */ +// This whole module backs the deprecated Express exports (superseded by `expressIntegration()`), so it +// references its own deprecated types/functions throughout. +/* oxlint-disable typescript/no-deprecated */ + import { debug } from '../../utils/debug-logger'; import { captureException } from '../../exports'; import { DEBUG_BUILD } from '../../debug-build'; @@ -67,6 +71,9 @@ import { getDefaultExport } from '../../utils/get-default-export'; * * Sentry.patchExpressModule(express, () => ({})); * ``` + * + * @deprecated Express is now instrumented automatically via `expressIntegration()`. This export is + * no longer used and will be removed in the next major version. */ export function patchExpressModule( moduleExports: ExpressModuleExport, @@ -160,6 +167,9 @@ export function patchExpressModule( /** * An Express-compatible error handler, used by setupExpressErrorHandler + * + * @deprecated `expressIntegration()` now captures errors automatically. This export is deprecated + * and will be removed in the next major version. */ export function expressErrorHandler(options?: ExpressHandlerOptions): ExpressErrorMiddleware { return function sentryErrorMiddleware( @@ -208,6 +218,10 @@ export function expressErrorHandler(options?: ExpressHandlerOptions): ExpressErr * * app.listen(3000); * ``` + * + * @deprecated `expressIntegration()` now captures errors automatically, so calling this is no longer + * necessary. To customize which errors are captured, pass `shouldHandleError` to `expressIntegration()`. + * This export is deprecated and will be removed in the next major version. */ export function setupExpressErrorHandler( app: { diff --git a/packages/core/src/integrations/express/patch-layer.ts b/packages/core/src/integrations/express/patch-layer.ts index 5318c7494732..2d3168cde17f 100644 --- a/packages/core/src/integrations/express/patch-layer.ts +++ b/packages/core/src/integrations/express/patch-layer.ts @@ -27,6 +27,10 @@ * limitations under the License. */ +// This module backs the deprecated Express exports (superseded by `expressIntegration()`), so it +// references the deprecated `ExpressIntegrationOptions` type. +/* oxlint-disable typescript/no-deprecated */ + import { HTTP_METHOD, HTTP_REQUEST_METHOD, diff --git a/packages/core/src/integrations/express/types.ts b/packages/core/src/integrations/express/types.ts index fbc2f1563359..3affb2d51284 100644 --- a/packages/core/src/integrations/express/types.ts +++ b/packages/core/src/integrations/express/types.ts @@ -135,6 +135,10 @@ export type ExpressRouter = { export type IgnoreMatcher = string | RegExp | ((name: string) => boolean); +/** + * @deprecated The core Express integration is superseded by `expressIntegration()`. This type is + * deprecated and will be removed in the next major version. + */ export type ExpressIntegrationOptions = { /** Ignore specific based on their name */ ignoreLayers?: IgnoreMatcher[]; @@ -167,8 +171,16 @@ export interface MiddlewareError extends Error { }; } +/** + * @deprecated `expressIntegration()` captures errors automatically. This type is deprecated and will + * be removed in the next major version. + */ export type ExpressMiddleware = (req: ExpressRequest, res: ExpressResponse, next: () => void) => void; +/** + * @deprecated `expressIntegration()` captures errors automatically. This type is deprecated and will + * be removed in the next major version. + */ export type ExpressErrorMiddleware = ( error: MiddlewareError, req: ExpressRequest, @@ -176,6 +188,10 @@ export type ExpressErrorMiddleware = ( next: (error: MiddlewareError) => void, ) => void; +/** + * @deprecated `expressIntegration()` captures errors automatically; pass `shouldHandleError` to it to + * customize capture. This type is deprecated and will be removed in the next major version. + */ export interface ExpressHandlerOptions { /** * Callback method deciding whether error should be captured and sent to Sentry diff --git a/packages/core/src/integrations/express/utils.ts b/packages/core/src/integrations/express/utils.ts index 55a3325ad172..80dc13af7c34 100644 --- a/packages/core/src/integrations/express/utils.ts +++ b/packages/core/src/integrations/express/utils.ts @@ -27,6 +27,10 @@ * limitations under the License. */ +// This module backs the deprecated Express exports (superseded by `expressIntegration()`), so it +// references the deprecated `ExpressIntegrationOptions` type. +/* oxlint-disable typescript/no-deprecated */ + import type { SpanAttributes } from '../../types/span'; import { getStoredLayers } from './request-layer-store'; import type { diff --git a/packages/core/src/server-exports.ts b/packages/core/src/server-exports.ts index ed17b27df890..876d53068fc7 100644 --- a/packages/core/src/server-exports.ts +++ b/packages/core/src/server-exports.ts @@ -18,7 +18,7 @@ export { vercelWaitUntil } from './utils/vercelWaitUntil'; export { flushIfServerless } from './utils/flushIfServerless'; export { callFrameToStackFrame, watchdogTimer } from './utils/anr'; export { safeUnref as _INTERNAL_safeUnref } from './utils/timer'; -// eslint-disable-next-line typescript/no-deprecated +/* oxlint-disable typescript/no-deprecated -- deprecated Express exports, kept until the next major */ export { patchExpressModule, setupExpressErrorHandler, expressErrorHandler } from './integrations/express/index'; export type { ExpressIntegrationOptions, @@ -26,6 +26,7 @@ export type { ExpressMiddleware, ExpressErrorMiddleware, } from './integrations/express/types'; +/* oxlint-enable typescript/no-deprecated */ export { instrumentPostgresJsSql, _sanitizeSqlQuery as _INTERNAL_sanitizeSqlQuery, diff --git a/packages/elysia/src/index.ts b/packages/elysia/src/index.ts index e62b2ec1be7c..dcdb8b70c135 100644 --- a/packages/elysia/src/index.ts +++ b/packages/elysia/src/index.ts @@ -93,7 +93,9 @@ export { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, dataloaderIntegration, expressIntegration, + // oxlint-disable-next-line typescript/no-deprecated expressErrorHandler, + // oxlint-disable-next-line typescript/no-deprecated setupExpressErrorHandler, fastifyIntegration, // oxlint-disable-next-line typescript/no-deprecated diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index f2bda4d03851..49f441e653d1 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -94,7 +94,9 @@ export { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, dataloaderIntegration, expressIntegration, + // oxlint-disable-next-line typescript/no-deprecated expressErrorHandler, + // oxlint-disable-next-line typescript/no-deprecated setupExpressErrorHandler, koaIntegration, // oxlint-disable-next-line typescript/no-deprecated diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 110b24495a4c..c5705054e952 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -1,6 +1,7 @@ export { httpIntegration } from './integrations/http'; export { nativeNodeFetchIntegration } from './integrations/node-fetch'; export { fsIntegration } from './integrations/fs'; +// oxlint-disable-next-line typescript/no-deprecated export { expressErrorHandler, setupExpressErrorHandler } from './integrations/tracing/express'; export { amqplibIntegration, diff --git a/packages/node/src/integrations/tracing/express.ts b/packages/node/src/integrations/tracing/express.ts index 7590c8c6bfd6..cdcb142195a7 100644 --- a/packages/node/src/integrations/tracing/express.ts +++ b/packages/node/src/integrations/tracing/express.ts @@ -1,10 +1,21 @@ +// oxlint-disable-next-line typescript/no-deprecated import { setupExpressErrorHandler as coreSetupExpressErrorHandler, type ExpressHandlerOptions } from '@sentry/core'; +// oxlint-disable-next-line typescript/no-deprecated export { expressErrorHandler } from '@sentry/core'; +/** + * Add an Express error handler to capture errors to Sentry. + * + * @deprecated `expressIntegration()` now captures errors automatically, so calling this is no longer + * necessary. To customize which errors are captured, pass `shouldHandleError` to `expressIntegration()`. + * This export is deprecated and will be removed in the next major version. + */ export function setupExpressErrorHandler( //oxlint-disable-next-line no-explicit-any app: { use: (middleware: any) => unknown }, + // oxlint-disable-next-line typescript/no-deprecated options?: ExpressHandlerOptions, ): void { + // oxlint-disable-next-line typescript/no-deprecated coreSetupExpressErrorHandler(app, options); } diff --git a/packages/remix/src/server/index.ts b/packages/remix/src/server/index.ts index 41ed4c21f5ad..f89345ae2de5 100644 --- a/packages/remix/src/server/index.ts +++ b/packages/remix/src/server/index.ts @@ -29,6 +29,7 @@ export { dedupeIntegration, defaultStackParser, endSession, + // oxlint-disable-next-line typescript/no-deprecated expressErrorHandler, expressIntegration, extraErrorDataIntegration, @@ -95,6 +96,7 @@ export { setTags, setAttribute, setAttributes, + // oxlint-disable-next-line typescript/no-deprecated setupExpressErrorHandler, // oxlint-disable-next-line typescript/no-deprecated setupHapiErrorHandler, diff --git a/packages/server-utils/src/integrations/express/instrumentation.ts b/packages/server-utils/src/integrations/express/instrumentation.ts index bb1991a659e0..b8d10052ff62 100644 --- a/packages/server-utils/src/integrations/express/instrumentation.ts +++ b/packages/server-utils/src/integrations/express/instrumentation.ts @@ -3,6 +3,7 @@ import { HTTP_ROUTE, SENTRY_OP } from '@sentry/conventions/attributes'; import { MIDDLEWARE } from '@sentry/conventions/op'; import type { Span } from '@sentry/core'; import { + captureException, debug, getActiveSpan, getClient, @@ -32,9 +33,12 @@ import type { ExpressLayerType, ExpressRequest, ExpressResponse, + ExpressShouldHandleError, HandleChannelContext, + MiddlewareError, RegistrationChannelContext, } from './types'; +import { defaultShouldHandleError } from './utils'; import { setHttpServerSpanRouteAttribute } from '../../utils/setHttpServerSpanRouteAttribute'; const ORIGIN = 'auto.http.express'; @@ -91,17 +95,54 @@ export function instrumentExpress( // Pop the layer path when the layer hands off via `next`. `asyncStart` fires // when `next` is called and *before* the downstream layer runs, so the // per-request path chain reflects only the current chain when each layer - // reconstructs its route. Only `asyncStart` is relevant here. + // reconstructs its route. The `error` event captures throws at the layer + // level (see `captureLayerError`), before any user error-handling middleware. channel.subscribe({ start: NOOP, asyncEnd: NOOP, end: NOOP, - error: NOOP, + error: data => captureLayerError(data, options.shouldHandleError), asyncStart: popLayerPathForLayer, }); } } +/** + * Capture an error surfaced on a layer's `handle_request` channel — the throw + * site, which runs before any user error-handling middleware. Duplicate captures + * (the error bubbling through parent layers, or a user also calling + * `setupExpressErrorHandler`) are collapsed by `captureException`'s per-object + * dedup, so only the first send survives. + * + * `shouldHandleError` is the raw integration option: `false` disables capture + * entirely, a function customizes the gate, and `undefined` falls back to + * {@link defaultShouldHandleError}. + */ +export function captureLayerError( + data: HandleChannelContext, + shouldHandleError: ExpressShouldHandleError | undefined, +): void { + if (shouldHandleError === false) { + return; + } + + const error = (data as { error?: unknown }).error; + + // `next('route')` / `next('router')` are Express control-flow signals, not errors. + if (!error || error === 'route' || error === 'router') { + return; + } + + if ((shouldHandleError ?? defaultShouldHandleError)(error as MiddlewareError)) { + captureException(error, { + mechanism: { + type: 'auto.http.express', + handled: false, + }, + }); + } +} + /** Record the freshly-registered layer's path pattern from a `route`/`use` call. */ function captureRegisteredLayerPath(data: RegistrationChannelContext): void { const stack = data.self?.stack; diff --git a/packages/server-utils/src/integrations/express/types.ts b/packages/server-utils/src/integrations/express/types.ts index 1cd56b104083..9116ba951dd2 100644 --- a/packages/server-utils/src/integrations/express/types.ts +++ b/packages/server-utils/src/integrations/express/types.ts @@ -54,10 +54,50 @@ export interface RegistrationChannelContext { arguments?: unknown[]; } +/** An Express error carrying an optional HTTP status, in the various shapes middleware use. */ +export interface MiddlewareError extends Error { + status?: number | string; + statusCode?: number | string; + status_code?: number | string; + output?: { + statusCode?: number | string; + }; +} + +/** Callback deciding whether an error should be captured; `false` disables capture entirely. */ +export type ExpressShouldHandleError = ((error: MiddlewareError) => boolean) | false; + type IgnoreMatcher = string | RegExp | ((name: string) => boolean); export interface ExpressIntegrationOptions { /** Ignore specific based on their name */ ignoreLayers?: IgnoreMatcher[]; /** Ignore specific layers based on their type */ ignoreLayersType?: ExpressLayerType[]; + /** + * Callback deciding whether an error thrown from a route handler should be + * captured and sent to Sentry. + * + * By default, 5xx errors (and errors without a resolvable status) are sent, + * while 3xx and 4xx errors are not. Errors are captured as soon as they are + * thrown — before any user error-handling middleware runs. + * + * Set to `false` to disable Sentry's automatic error capture entirely; you can + * then capture errors yourself from your own error handler via + * `Sentry.captureException`. + * + * @example + * + * ```javascript + * Sentry.init({ + * integrations: [ + * Sentry.expressIntegration({ + * shouldHandleError(error) { + * return (error.statusCode ?? 500) >= 500; + * }, + * }), + * ], + * }); + * ``` + */ + shouldHandleError?: ExpressShouldHandleError; } diff --git a/packages/server-utils/src/integrations/express/utils.ts b/packages/server-utils/src/integrations/express/utils.ts new file mode 100644 index 000000000000..a76d0b4abf61 --- /dev/null +++ b/packages/server-utils/src/integrations/express/utils.ts @@ -0,0 +1,15 @@ +import type { MiddlewareError } from './types'; + +function getStatusCodeFromResponse(error: MiddlewareError): number { + const statusCode = error.status || error.statusCode || error.status_code || error.output?.statusCode; + return statusCode ? parseInt(statusCode as string, 10) : 500; +} + +/** + * Default function deciding whether an error should be sent to Sentry: captures + * 5xx errors, and treats an error without a resolvable status as a 500. Errors + * carrying a 3xx/4xx status are skipped (client errors / redirects). + */ +export function defaultShouldHandleError(error: MiddlewareError): boolean { + return getStatusCodeFromResponse(error) >= 500; +} diff --git a/packages/server-utils/test/integrations/express-error-handler.test.ts b/packages/server-utils/test/integrations/express-error-handler.test.ts new file mode 100644 index 000000000000..33fc542cd431 --- /dev/null +++ b/packages/server-utils/test/integrations/express-error-handler.test.ts @@ -0,0 +1,80 @@ +import * as SentryCore from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, it, type MockInstance, vi } from 'vitest'; +import { captureLayerError } from '../../../src/integrations/express/instrumentation'; +import type { HandleChannelContext } from '../../../src/integrations/express/types'; + +function makeErrorData(error: unknown): HandleChannelContext { + return { error } as unknown as HandleChannelContext; +} + +describe('captureLayerError', () => { + let captureExceptionSpy: MockInstance; + + beforeEach(() => { + captureExceptionSpy = vi.spyOn(SentryCore, 'captureException').mockImplementation(() => 'id'); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('captures a 5xx error by default', () => { + const error = Object.assign(new Error('boom'), { statusCode: 500 }); + + captureLayerError(makeErrorData(error), undefined); + + expect(captureExceptionSpy).toHaveBeenCalledWith(error, { + mechanism: { type: 'auto.http.express', handled: false }, + }); + }); + + it('captures an error without a resolvable status by default', () => { + const error = new Error('boom'); + + captureLayerError(makeErrorData(error), undefined); + + expect(captureExceptionSpy).toHaveBeenCalledWith(error, { + mechanism: { type: 'auto.http.express', handled: false }, + }); + }); + + it('does not capture a 4xx error by default', () => { + const error = Object.assign(new Error('bad request'), { statusCode: 400 }); + + captureLayerError(makeErrorData(error), undefined); + + expect(captureExceptionSpy).not.toHaveBeenCalled(); + }); + + it.each(['route', 'router'])('ignores the Express `next(%s)` control signal', signal => { + captureLayerError(makeErrorData(signal), undefined); + + expect(captureExceptionSpy).not.toHaveBeenCalled(); + }); + + it('does not capture when there is no error', () => { + captureLayerError(makeErrorData(undefined), undefined); + + expect(captureExceptionSpy).not.toHaveBeenCalled(); + }); + + it('honors a custom shouldHandleError', () => { + const shouldHandleError = vi.fn().mockReturnValue(true); + const error = Object.assign(new Error('teapot'), { statusCode: 418 }); + + captureLayerError(makeErrorData(error), shouldHandleError); + + expect(shouldHandleError).toHaveBeenCalledWith(error); + expect(captureExceptionSpy).toHaveBeenCalledWith(error, { + mechanism: { type: 'auto.http.express', handled: false }, + }); + }); + + it('captures nothing when shouldHandleError is false', () => { + const error = Object.assign(new Error('boom'), { statusCode: 500 }); + + captureLayerError(makeErrorData(error), false); + + expect(captureExceptionSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index 7eb31148ce1b..da4972e07590 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -32,6 +32,7 @@ export { dedupeIntegration, defaultStackParser, endSession, + // oxlint-disable-next-line typescript/no-deprecated expressErrorHandler, expressIntegration, extraErrorDataIntegration, @@ -99,6 +100,7 @@ export { setTags, setAttribute, setAttributes, + // oxlint-disable-next-line typescript/no-deprecated setupExpressErrorHandler, // oxlint-disable-next-line typescript/no-deprecated setupHapiErrorHandler, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 804f992c86d1..0d9160e9b341 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -30,6 +30,7 @@ export { dedupeIntegration, defaultStackParser, endSession, + // oxlint-disable-next-line typescript/no-deprecated expressErrorHandler, expressIntegration, extraErrorDataIntegration, @@ -96,6 +97,7 @@ export { setTags, setAttribute, setAttributes, + // oxlint-disable-next-line typescript/no-deprecated setupExpressErrorHandler, // oxlint-disable-next-line typescript/no-deprecated setupHapiErrorHandler, From eab371909bd55ec17d36632b46062b539e89eb78 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 20 Aug 2026 16:46:50 +0200 Subject: [PATCH 02/12] fix(node): Parent captured Express errors to the layer span The channel `error` event runs outside the layer span's async context, so `captureException` was recording events with no `parent_span_id`. Re-activate the span bound by `bindTracingChannelToSpan` (now typed on `HandleChannelContext`) around the capture so the error event is parented to the request's trace. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../integrations/express/instrumentation.ts | 17 +++++++++-- .../src/integrations/express/types.ts | 8 ++++- .../express-error-handler.test.ts | 29 +++++++++++++++++-- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/packages/server-utils/src/integrations/express/instrumentation.ts b/packages/server-utils/src/integrations/express/instrumentation.ts index b8d10052ff62..ff42148885c4 100644 --- a/packages/server-utils/src/integrations/express/instrumentation.ts +++ b/packages/server-utils/src/integrations/express/instrumentation.ts @@ -14,6 +14,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, stringMatchesSomePattern, + withActiveSpan, } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; import { CHANNELS } from '../../orchestrion/channels'; @@ -126,20 +127,32 @@ export function captureLayerError( return; } - const error = (data as { error?: unknown }).error; + const error = data.error; // `next('route')` / `next('router')` are Express control-flow signals, not errors. if (!error || error === 'route' || error === 'router') { return; } - if ((shouldHandleError ?? defaultShouldHandleError)(error as MiddlewareError)) { + if (!(shouldHandleError ?? defaultShouldHandleError)(error as MiddlewareError)) { + return; + } + + const capture = (): string => captureException(error, { mechanism: { type: 'auto.http.express', handled: false, }, }); + + // The channel's `error` event runs outside the layer span's async context, so + // re-activate the bound span (when present) to parent the error event to the + // request's trace instead of capturing it context-free. + if (data._sentrySpan) { + withActiveSpan(data._sentrySpan, capture); + } else { + capture(); } } diff --git a/packages/server-utils/src/integrations/express/types.ts b/packages/server-utils/src/integrations/express/types.ts index 9116ba951dd2..5536f524e849 100644 --- a/packages/server-utils/src/integrations/express/types.ts +++ b/packages/server-utils/src/integrations/express/types.ts @@ -1,3 +1,5 @@ +import type { Span } from '@sentry/core'; + export type ExpressLayerType = 'router' | 'middleware' | 'request_handler'; /** @@ -34,13 +36,17 @@ export interface ExpressResponse { * `_sentryCleanup` is ours: a teardown for the `res.on('finish')` listener we * register, invoked from `beforeSpanEnd` when the span ends via `next()`. * `_sentryStoredLayer` marks that this invocation pushed a layer path (so the - * matching pop on `asyncStart` stays symmetric). + * matching pop on `asyncStart` stays symmetric). `_sentrySpan` is the span bound + * for this layer by `bindTracingChannelToSpan`, and `error` is present on the + * channel's `error` event. */ export interface HandleChannelContext { self?: ExpressLayer; arguments?: unknown[]; _sentryCleanup?: () => void; _sentryStoredLayer?: boolean; + _sentrySpan?: Span; + error?: unknown; } /** diff --git a/packages/server-utils/test/integrations/express-error-handler.test.ts b/packages/server-utils/test/integrations/express-error-handler.test.ts index 33fc542cd431..05c81b35fcc8 100644 --- a/packages/server-utils/test/integrations/express-error-handler.test.ts +++ b/packages/server-utils/test/integrations/express-error-handler.test.ts @@ -3,8 +3,8 @@ import { afterEach, beforeEach, describe, expect, it, type MockInstance, vi } fr import { captureLayerError } from '../../../src/integrations/express/instrumentation'; import type { HandleChannelContext } from '../../../src/integrations/express/types'; -function makeErrorData(error: unknown): HandleChannelContext { - return { error } as unknown as HandleChannelContext; +function makeErrorData(error: unknown, span?: unknown): HandleChannelContext { + return { error, _sentrySpan: span } as unknown as HandleChannelContext; } describe('captureLayerError', () => { @@ -77,4 +77,29 @@ describe('captureLayerError', () => { expect(captureExceptionSpy).not.toHaveBeenCalled(); }); + + it('re-activates the bound layer span so the event is parented to the trace', () => { + const withActiveSpanSpy = vi + .spyOn(SentryCore, 'withActiveSpan') + .mockImplementation((_span, fn) => (fn as () => unknown)(undefined as never) as never); + const span = { id: 'layer-span' }; + const error = Object.assign(new Error('boom'), { statusCode: 500 }); + + captureLayerError(makeErrorData(error, span), undefined); + + expect(withActiveSpanSpy).toHaveBeenCalledWith(span, expect.any(Function)); + expect(captureExceptionSpy).toHaveBeenCalledWith(error, { + mechanism: { type: 'auto.http.express', handled: false }, + }); + }); + + it('captures without a span when none is bound (e.g. unsampled request)', () => { + const withActiveSpanSpy = vi.spyOn(SentryCore, 'withActiveSpan'); + const error = Object.assign(new Error('boom'), { statusCode: 500 }); + + captureLayerError(makeErrorData(error), undefined); + + expect(withActiveSpanSpy).not.toHaveBeenCalled(); + expect(captureExceptionSpy).toHaveBeenCalledTimes(1); + }); }); From e416f86261028a6c425d7b591ca6b182ca038f94 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 08:53:44 +0200 Subject: [PATCH 03/12] fix tests --- .../node-express-streaming/tests/errors.test.ts | 1 + .../test-applications/node-express-v5/tests/errors.test.ts | 1 + .../test-applications/node-express/tests/errors.test.ts | 1 + .../e2e-tests/test-applications/tsx-express/tests/errors.test.ts | 1 + 4 files changed, 4 insertions(+) diff --git a/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/errors.test.ts index 8b24ed9039d8..3697502984e1 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-streaming/tests/errors.test.ts @@ -37,6 +37,7 @@ test('Sends correct error event', async ({ baseURL }) => { expect(errorEvent.contexts?.trace).toEqual({ 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}/), }); // The error is attached to the same trace as the streamed request spans, and to a diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/errors.test.ts index a7bc9b497c63..f7187956f817 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/errors.test.ts @@ -30,6 +30,7 @@ test('Sends correct error event', async ({ baseURL }) => { expect(errorEvent.contexts?.trace).toEqual({ 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}/), }); // The error is attached to the same trace as the request transaction, and to a diff --git a/dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts index 7fa0c63f037f..02e96f9fab50 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-express/tests/errors.test.ts @@ -35,6 +35,7 @@ test('Sends correct error event', async ({ baseURL }) => { expect(errorEvent.contexts?.trace).toEqual({ 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}/), }); // The error is attached to the same trace as the request transaction, and to a diff --git a/dev-packages/e2e-tests/test-applications/tsx-express/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/tsx-express/tests/errors.test.ts index 1c59e2173092..d272a1dd8906 100644 --- a/dev-packages/e2e-tests/test-applications/tsx-express/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/tsx-express/tests/errors.test.ts @@ -30,6 +30,7 @@ test('Sends correct error event', async ({ baseURL }) => { expect(errorEvent.contexts?.trace).toEqual({ 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}/), }); // The error is attached to the same trace as the request transaction, and to a From eed35f7ef0db3146f91375e70d2856516e41e0f0 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 09:53:19 +0200 Subject: [PATCH 04/12] wip tests --- .../suites/esm/warn-esm/server.js | 2 -- .../suites/esm/warn-esm/server.mjs | 2 -- .../scenario-setup-error-handler.mjs | 35 ++++++++++++++++++ .../suites/express/handle-error/scenario.mjs | 2 -- .../suites/express/handle-error/test.ts | 36 +++++++++++++++++++ .../express/ignore-layers-type/scenario.mjs | 3 -- .../suites/express/multiple-init/scenario.mjs | 2 -- .../scenario-common-infix-parameterized.mjs | 2 -- .../scenario-common-infix.mjs | 2 -- .../scenario-common-prefix-reverse.mjs | 2 -- .../scenario-common-prefix-same-length.mjs | 2 -- .../scenario-common-prefix.mjs | 2 -- .../scenario-complex-router.mjs | 2 -- .../scenario-middle-layer.mjs | 2 -- .../suites/express/requestUser/scenario.mjs | 2 -- .../suites/express/sentry-trace/scenario.mjs | 2 -- .../express/span-isolationScope/scenario.mjs | 2 -- .../tracing/scenario-filterStatusCode.mjs | 3 -- .../suites/express/tracing/scenario.mjs | 3 -- .../scenario-normalized-request.mjs | 3 -- .../tracing/tracesSampler/scenario.mjs | 3 -- .../express/tracing/updateName/scenario.mjs | 2 -- .../express/tracing/withError/scenario.mjs | 2 -- .../express/with-http/base/scenario.mjs | 3 -- .../maxIncomingRequestBodySize/scenario.mjs | 3 -- .../express/without-tracing/scenario.mjs | 2 -- .../suites/fs-instrumentation/scenario.mjs | 3 -- .../suites/modules/server.js | 2 -- .../suites/modules/server.mjs | 3 -- .../suites/sessions/server.ts | 2 -- .../sampleRate-propagation/server.js | 2 -- .../httpIntegration-streamed/server.mjs | 3 -- .../server-ignoreIncomingRequests.js | 2 -- .../server-ignoreOutgoingRequests.js | 2 -- .../server-ignoreStaticAssets.js | 2 -- .../server-traceStaticAssets.js | 2 -- .../suites/tracing/httpIntegration/server.mjs | 3 -- .../attributes/server.mjs | 2 -- .../ignoreSpans-streamed/children/server.mjs | 2 -- .../ignoreSpans-streamed/segments/server.mjs | 2 -- .../tracing/meta-tags-twp-errors/server.js | 2 -- .../suites/tracing/meta-tags-twp/server.js | 2 -- .../tracing/meta-tags/server-sdk-disabled.js | 2 -- .../meta-tags/server-tracesSampleRate-zero.js | 2 -- .../suites/tracing/meta-tags/server.js | 2 -- .../tracing/requestData-streamed/server.mjs | 3 -- .../tracing/sample-rand-propagation/server.js | 2 -- .../no-tracing-enabled/server.js | 2 -- .../tracesSampleRate-0/server.js | 2 -- .../tracesSampleRate/server.js | 2 -- .../tracesSampler/server.js | 2 -- .../suites/tracing/sampling-static/server.mjs | 3 -- .../tracing/sampling-streamed/server.mjs | 3 -- .../server-no-explicit-org-id.ts | 2 -- .../baggage-org-id/server-no-org-id.ts | 2 -- .../baggage-org-id/server.ts | 2 -- .../scenario-error-in-tool-express.mjs | 1 - 57 files changed, 71 insertions(+), 123 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/express/handle-error/scenario-setup-error-handler.mjs diff --git a/dev-packages/node-integration-tests/suites/esm/warn-esm/server.js b/dev-packages/node-integration-tests/suites/esm/warn-esm/server.js index 13ed60f0d3b5..1bcb3b642b5a 100644 --- a/dev-packages/node-integration-tests/suites/esm/warn-esm/server.js +++ b/dev-packages/node-integration-tests/suites/esm/warn-esm/server.js @@ -17,6 +17,4 @@ app.get('/test/success', (req, res) => { res.send({ response: 'response 3' }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/esm/warn-esm/server.mjs b/dev-packages/node-integration-tests/suites/esm/warn-esm/server.mjs index f4e014f6ba63..a42337b2a47c 100644 --- a/dev-packages/node-integration-tests/suites/esm/warn-esm/server.mjs +++ b/dev-packages/node-integration-tests/suites/esm/warn-esm/server.mjs @@ -15,6 +15,4 @@ app.get('/test/success', (req, res) => { res.send({ response: 'response 3' }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error/scenario-setup-error-handler.mjs b/dev-packages/node-integration-tests/suites/express/handle-error/scenario-setup-error-handler.mjs new file mode 100644 index 000000000000..6f2cb6e2108d --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/handle-error/scenario-setup-error-handler.mjs @@ -0,0 +1,35 @@ +import * as Sentry from '@sentry/node'; +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import express from 'express'; + +const app = express(); + +Sentry.setTag('global', 'tag'); + +app.get('/test/express/:id', req => { + throw new Error(`test_error with id ${req.params.id}`); +}); + +app.get('/test/withScope', () => { + Sentry.withScope(scope => { + scope.setTag('local', 'tag'); + throw new Error('test_error'); + }); +}); + +app.get('/test/isolationScope', () => { + Sentry.getIsolationScope().setTag('isolation-scope', 'tag'); + throw new Error('isolation_test_error'); +}); + +app.get('/test/withIsolationScope', () => { + Sentry.withIsolationScope(iScope => { + iScope.setTag('with-isolation-scope', 'tag'); + throw new Error('with_isolation_scope_test_error'); + }); +}); + +// Deprecated but still supported +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error/scenario.mjs b/dev-packages/node-integration-tests/suites/express/handle-error/scenario.mjs index 2ec840387f91..75604e4248b6 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/express/handle-error/scenario.mjs @@ -29,6 +29,4 @@ app.get('/test/withIsolationScope', () => { }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error/test.ts b/dev-packages/node-integration-tests/suites/express/handle-error/test.ts index 1e6879d281d1..d5a3650218ae 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error/test.ts +++ b/dev-packages/node-integration-tests/suites/express/handle-error/test.ts @@ -296,4 +296,40 @@ describe('express error handling', () => { }, ); }); + + describe('setupExpressErrorHandler', () => { + createCjsTests( + __dirname, + 'scenario-setup-error-handler.mjs', + 'instrument-should-handle-error.mjs', + (createRunner, test) => { + test('captures only errors for which shouldHandleError returns true', async () => { + const runner = createRunner() + .expect({ + event: { + exception: { + values: [ + { + mechanism: { + type: 'auto.middleware.express', + handled: false, + }, + value: 'error_2', + }, + ], + }, + }, + }) + .start(); + + // this error is filtered & ignored + runner.makeRequest('get', '/test1', { expectError: true }); + // this error is actually captured + runner.makeRequest('get', '/test2', { expectError: true }); + + await runner.completed(); + }); + }, + ); + }); }); diff --git a/dev-packages/node-integration-tests/suites/express/ignore-layers-type/scenario.mjs b/dev-packages/node-integration-tests/suites/express/ignore-layers-type/scenario.mjs index bd775c17fcee..c94d13f02275 100644 --- a/dev-packages/node-integration-tests/suites/express/ignore-layers-type/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/express/ignore-layers-type/scenario.mjs @@ -1,6 +1,5 @@ import cors from 'cors'; import express from 'express'; -import * as Sentry from '@sentry/node'; import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; const app = express(); @@ -13,6 +12,4 @@ app.get('/test/express', (_req, res) => { res.send({ response: 'response 1' }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-init/scenario.mjs b/dev-packages/node-integration-tests/suites/express/multiple-init/scenario.mjs index 15e90638889a..6edc7f33fdbd 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-init/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/express/multiple-init/scenario.mjs @@ -57,6 +57,4 @@ app.get('/test/error/:id', (req, res) => { }, 1); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-common-infix-parameterized.mjs b/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-common-infix-parameterized.mjs index 593aa0df7f16..e81ab08ac7b7 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-common-infix-parameterized.mjs +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-common-infix-parameterized.mjs @@ -19,6 +19,4 @@ const root = express.Router(); app.use('/api2/v1', root); app.use('/api/v1', APIv1); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-common-infix.mjs b/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-common-infix.mjs index 61b328023429..40e0e995e348 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-common-infix.mjs +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-common-infix.mjs @@ -19,6 +19,4 @@ const root = express.Router(); app.use('/api/v1', root); app.use('/api2/v1', APIv1); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-common-prefix-reverse.mjs b/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-common-prefix-reverse.mjs index f65e2e1e2fcd..4c0d1e91cc18 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-common-prefix-reverse.mjs +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-common-prefix-reverse.mjs @@ -19,6 +19,4 @@ const root = express.Router(); app.use('/api/v1', APIv1); app.use('/api', root); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-common-prefix-same-length.mjs b/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-common-prefix-same-length.mjs index 4e0230a7e374..37bda29b35c4 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-common-prefix-same-length.mjs +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-common-prefix-same-length.mjs @@ -19,6 +19,4 @@ const root = express.Router(); app.use('/api', root); app.use('/api/v1', APIv1); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-common-prefix.mjs b/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-common-prefix.mjs index 77dd64e0c502..60102883e4f3 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-common-prefix.mjs +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-common-prefix.mjs @@ -24,6 +24,4 @@ const root = express.Router(); app.use('/api', root); app.use('/api/v1', APIv1); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-complex-router.mjs b/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-complex-router.mjs index 9348e16d3334..23b505f29cca 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-complex-router.mjs +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-complex-router.mjs @@ -19,6 +19,4 @@ const router = express.Router(); app.use('/api', router); app.use('/api/api/v1', APIv1.use('/sub-router', APIv1)); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-middle-layer.mjs b/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-middle-layer.mjs index 074ba2a68389..aed3006390c9 100644 --- a/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-middle-layer.mjs +++ b/dev-packages/node-integration-tests/suites/express/multiple-routers/scenario-middle-layer.mjs @@ -19,6 +19,4 @@ const root = express.Router(); app.use('/api/v1', APIv1); app.use('/api', root); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/requestUser/scenario.mjs b/dev-packages/node-integration-tests/suites/express/requestUser/scenario.mjs index 601f73716557..9b927cc6d107 100644 --- a/dev-packages/node-integration-tests/suites/express/requestUser/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/express/requestUser/scenario.mjs @@ -34,6 +34,4 @@ app.get('/test2', (_req, _res) => { throw new Error('error_2'); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/scenario.mjs b/dev-packages/node-integration-tests/suites/express/sentry-trace/scenario.mjs index 77a79b9f71b5..260087b258b4 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/scenario.mjs @@ -61,6 +61,4 @@ app.get('/test/express-property-values', (req, res) => { res.send({ test_data: headers }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/span-isolationScope/scenario.mjs b/dev-packages/node-integration-tests/suites/express/span-isolationScope/scenario.mjs index 543de8de355b..328945cd6714 100644 --- a/dev-packages/node-integration-tests/suites/express/span-isolationScope/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/express/span-isolationScope/scenario.mjs @@ -15,6 +15,4 @@ app.get('/test/isolationScope', (_req, res) => { res.send({}); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/scenario-filterStatusCode.mjs b/dev-packages/node-integration-tests/suites/express/tracing/scenario-filterStatusCode.mjs index c53a72951970..17dadc5d8e57 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/scenario-filterStatusCode.mjs +++ b/dev-packages/node-integration-tests/suites/express/tracing/scenario-filterStatusCode.mjs @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; import express from 'express'; @@ -32,6 +31,4 @@ app.get('/399', (_req, res) => { res.status(399).send({ response: 'response 399' }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/scenario.mjs b/dev-packages/node-integration-tests/suites/express/tracing/scenario.mjs index c6467cb0672e..daa0666b0ebd 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/express/tracing/scenario.mjs @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; import bodyParser from 'body-parser'; import cors from 'cors'; @@ -73,6 +72,4 @@ versionedRouter.get('/user', (_req, res) => { }); app.use('/test/version/:version', versionedRouter); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/scenario-normalized-request.mjs b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/scenario-normalized-request.mjs index ec306bb4680f..844b40a7a51c 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/scenario-normalized-request.mjs +++ b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/scenario-normalized-request.mjs @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; import cors from 'cors'; import express from 'express'; @@ -11,6 +10,4 @@ app.get('/test-normalized-request', (_req, res) => { res.send('Success'); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/scenario.mjs b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/scenario.mjs index 4edf6a151ede..a6238e6776e6 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/express/tracing/tracesSampler/scenario.mjs @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; import cors from 'cors'; import express from 'express'; @@ -15,6 +14,4 @@ app.get('/test2', (_req, res) => { res.send('Success'); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/updateName/scenario.mjs b/dev-packages/node-integration-tests/suites/express/tracing/updateName/scenario.mjs index 1ce6eb4833c9..b9de7003e94c 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/updateName/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/express/tracing/updateName/scenario.mjs @@ -33,6 +33,4 @@ app.get('/test/:id/updateSpanNameAndSource', (_req, res) => { res.send({ response: 'response 4' }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/tracing/withError/scenario.mjs b/dev-packages/node-integration-tests/suites/express/tracing/withError/scenario.mjs index c29ce8662df9..3384d7cec175 100644 --- a/dev-packages/node-integration-tests/suites/express/tracing/withError/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/express/tracing/withError/scenario.mjs @@ -12,6 +12,4 @@ app.get('/test/:id1/:id2', (_req, res) => { res.send('Success'); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/with-http/base/scenario.mjs b/dev-packages/node-integration-tests/suites/express/with-http/base/scenario.mjs index 4ecdd158f785..e8b63d116f78 100644 --- a/dev-packages/node-integration-tests/suites/express/with-http/base/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/express/with-http/base/scenario.mjs @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; import express from 'express'; import http from 'http'; @@ -23,6 +22,4 @@ app.get('/test3', (_req, res) => { res.send({ response: 'response 3' }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/scenario.mjs b/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/scenario.mjs index 0d6b85eb9fe3..f3e1888b908e 100644 --- a/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/express/with-http/maxIncomingRequestBodySize/scenario.mjs @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; import bodyParser from 'body-parser'; import express from 'express'; @@ -26,6 +25,4 @@ app.post('/ignore-request-body', (req, res) => { }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/without-tracing/scenario.mjs b/dev-packages/node-integration-tests/suites/express/without-tracing/scenario.mjs index b58374f29aff..4ab9cb2b41e6 100644 --- a/dev-packages/node-integration-tests/suites/express/without-tracing/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/express/without-tracing/scenario.mjs @@ -27,6 +27,4 @@ app.post('/test-post', function (req, res) { res.send({ status: 'ok', body: req.body }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/fs-instrumentation/scenario.mjs b/dev-packages/node-integration-tests/suites/fs-instrumentation/scenario.mjs index 4cf58c2fa122..966fa53fe4eb 100644 --- a/dev-packages/node-integration-tests/suites/fs-instrumentation/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/fs-instrumentation/scenario.mjs @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; import express from 'express'; import * as fs from 'fs'; @@ -127,6 +126,4 @@ app.get('/symlink', async (_, res) => { res.send('done'); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/modules/server.js b/dev-packages/node-integration-tests/suites/modules/server.js index 2b49a43c16f4..ebdfc5a607a0 100644 --- a/dev-packages/node-integration-tests/suites/modules/server.js +++ b/dev-packages/node-integration-tests/suites/modules/server.js @@ -18,6 +18,4 @@ app.get('/test1', () => { throw new Error('error_1'); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/modules/server.mjs b/dev-packages/node-integration-tests/suites/modules/server.mjs index 6edeb78c703f..d34710e81dad 100644 --- a/dev-packages/node-integration-tests/suites/modules/server.mjs +++ b/dev-packages/node-integration-tests/suites/modules/server.mjs @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; import express from 'express'; @@ -8,6 +7,4 @@ app.get('/test1', () => { throw new Error('error_1'); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/sessions/server.ts b/dev-packages/node-integration-tests/suites/sessions/server.ts index abfaafe7a852..1b2fbab10df3 100644 --- a/dev-packages/node-integration-tests/suites/sessions/server.ts +++ b/dev-packages/node-integration-tests/suites/sessions/server.ts @@ -46,6 +46,4 @@ app.get('/test/error_handled', (_req, res) => { res.send('Crash!'); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/server.js b/dev-packages/node-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/server.js index 0dd8e905aa4b..9d1af464ce56 100644 --- a/dev-packages/node-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/server.js +++ b/dev-packages/node-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/server.js @@ -28,6 +28,4 @@ app.get('/test', (req, res) => { res.send({ headers: req.headers }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration-streamed/server.mjs b/dev-packages/node-integration-tests/suites/tracing/httpIntegration-streamed/server.mjs index 4b86f31cb860..3a1c01e83b77 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration-streamed/server.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration-streamed/server.mjs @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; import cors from 'cors'; import express from 'express'; @@ -11,6 +10,4 @@ app.get('/test', (_req, res) => { res.send({ response: 'ok' }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreIncomingRequests.js b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreIncomingRequests.js index b3146d3009eb..fa075766b2b0 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreIncomingRequests.js +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreIncomingRequests.js @@ -44,6 +44,4 @@ app.post('/readiness', (_req, res) => { res.send({ response: 'readiness' }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreOutgoingRequests.js b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreOutgoingRequests.js index 01e9cf90c0e5..79f6023fe441 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreOutgoingRequests.js +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreOutgoingRequests.js @@ -53,8 +53,6 @@ app.get('/testRequest', (_req, response) => { }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); function makeHttpRequest(url) { diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreStaticAssets.js b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreStaticAssets.js index c6e9dc440e0b..d6c17309f66b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreStaticAssets.js +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreStaticAssets.js @@ -35,6 +35,4 @@ app.get('/assets/app.js', (_req, res) => { res.type('application/javascript').send('/* js */'); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-traceStaticAssets.js b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-traceStaticAssets.js index 6caece96a44e..e1fd410500a1 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-traceStaticAssets.js +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-traceStaticAssets.js @@ -34,6 +34,4 @@ app.get('/assets/app.js', (_req, res) => { res.type('application/javascript').send('/* js */'); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server.mjs b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server.mjs index 37e629758828..57f4c96fc33e 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server.mjs @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; import cors from 'cors'; import express from 'express'; @@ -15,6 +14,4 @@ app.post('/test', (_req, res) => { res.send({ response: 'response 2' }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/attributes/server.mjs b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/attributes/server.mjs index 116c27711ef7..afce8f8961da 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/attributes/server.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/attributes/server.mjs @@ -20,6 +20,4 @@ app.post('/drop', (_req, res) => { res.send({ status: 'dropped' }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/children/server.mjs b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/children/server.mjs index e8af8b8c92c6..2bec7ff1304d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/children/server.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/children/server.mjs @@ -46,6 +46,4 @@ app.get('/test/express', (_req, res) => { }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/segments/server.mjs b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/segments/server.mjs index b9ff3398b158..3bca9a3eb156 100644 --- a/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/segments/server.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/ignoreSpans-streamed/segments/server.mjs @@ -20,6 +20,4 @@ app.get('/ok', (_req, res) => { }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/meta-tags-twp-errors/server.js b/dev-packages/node-integration-tests/suites/tracing/meta-tags-twp-errors/server.js index 86052e78d3b5..f7a3ce3b9380 100644 --- a/dev-packages/node-integration-tests/suites/tracing/meta-tags-twp-errors/server.js +++ b/dev-packages/node-integration-tests/suites/tracing/meta-tags-twp-errors/server.js @@ -23,6 +23,4 @@ app.get('/test', (_req, res) => { res.status(200).send(); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/meta-tags-twp/server.js b/dev-packages/node-integration-tests/suites/tracing/meta-tags-twp/server.js index 01650a0752d8..0b47fbdf999d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/meta-tags-twp/server.js +++ b/dev-packages/node-integration-tests/suites/tracing/meta-tags-twp/server.js @@ -28,6 +28,4 @@ app.get('/test', (_req, res) => { }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/meta-tags/server-sdk-disabled.js b/dev-packages/node-integration-tests/suites/tracing/meta-tags/server-sdk-disabled.js index a16df5d798a0..b226bf98efdb 100644 --- a/dev-packages/node-integration-tests/suites/tracing/meta-tags/server-sdk-disabled.js +++ b/dev-packages/node-integration-tests/suites/tracing/meta-tags/server-sdk-disabled.js @@ -30,6 +30,4 @@ app.get('/test', (_req, res) => { }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/meta-tags/server-tracesSampleRate-zero.js b/dev-packages/node-integration-tests/suites/tracing/meta-tags/server-tracesSampleRate-zero.js index 072b3f22a5d7..32260783df21 100644 --- a/dev-packages/node-integration-tests/suites/tracing/meta-tags/server-tracesSampleRate-zero.js +++ b/dev-packages/node-integration-tests/suites/tracing/meta-tags/server-tracesSampleRate-zero.js @@ -29,6 +29,4 @@ app.get('/test', (_req, res) => { }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/meta-tags/server.js b/dev-packages/node-integration-tests/suites/tracing/meta-tags/server.js index 0dc93728a518..5f85d27e1918 100644 --- a/dev-packages/node-integration-tests/suites/tracing/meta-tags/server.js +++ b/dev-packages/node-integration-tests/suites/tracing/meta-tags/server.js @@ -29,6 +29,4 @@ app.get('/test', (_req, res) => { }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/requestData-streamed/server.mjs b/dev-packages/node-integration-tests/suites/tracing/requestData-streamed/server.mjs index 07398392cb75..818ef8e4feeb 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requestData-streamed/server.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/requestData-streamed/server.mjs @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; import express from 'express'; @@ -8,6 +7,4 @@ app.get('/test', (_req, res) => { res.send({ response: 'ok' }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rand-propagation/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rand-propagation/server.js index 44a23e533619..26699a3d642d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/sample-rand-propagation/server.js +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rand-propagation/server.js @@ -36,6 +36,4 @@ app.get('/bounce', (req, res) => { }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.js index 6ef604458481..828606a12514 100644 --- a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.js +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.js @@ -35,6 +35,4 @@ app.get('/bounce', (req, res) => { }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.js index 5af50c4cce54..453a43dd48f5 100644 --- a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.js +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.js @@ -36,6 +36,4 @@ app.get('/bounce', (req, res) => { }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.js index b4be05c27de1..54a61482e577 100644 --- a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.js +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.js @@ -36,6 +36,4 @@ app.get('/bounce', (req, res) => { }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/server.js index 2e4245656a0d..e7c89eda341b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/server.js +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/server.js @@ -38,6 +38,4 @@ app.get('/bounce', (req, res) => { }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/sampling-static/server.mjs b/dev-packages/node-integration-tests/suites/tracing/sampling-static/server.mjs index f9c7f136aef2..3b5522b8e9b7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/sampling-static/server.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/sampling-static/server.mjs @@ -1,6 +1,5 @@ import express from 'express'; import cors from 'cors'; -import * as Sentry from '@sentry/node'; import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; const app = express(); @@ -15,6 +14,4 @@ app.get('/ok', (_req, res) => { res.send({ status: 'ok' }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/sampling-streamed/server.mjs b/dev-packages/node-integration-tests/suites/tracing/sampling-streamed/server.mjs index f9c7f136aef2..3b5522b8e9b7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/sampling-streamed/server.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/sampling-streamed/server.mjs @@ -1,6 +1,5 @@ import express from 'express'; import cors from 'cors'; -import * as Sentry from '@sentry/node'; import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; const app = express(); @@ -15,6 +14,4 @@ app.get('/ok', (_req, res) => { res.send({ status: 'ok' }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-explicit-org-id.ts b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-explicit-org-id.ts index b297ed55cc65..2446388da0fd 100644 --- a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-explicit-org-id.ts +++ b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-explicit-org-id.ts @@ -30,6 +30,4 @@ app.get('/test/express', (_req, res) => { res.send({ test_data: headers }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-org-id.ts b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-org-id.ts index 51f52b97efb0..82ede5bb97b3 100644 --- a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-org-id.ts +++ b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-org-id.ts @@ -30,6 +30,4 @@ app.get('/test/express', (_req, res) => { res.send({ test_data: headers }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server.ts b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server.ts index 15eed55abcef..5f9aa1397b38 100644 --- a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server.ts +++ b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server.ts @@ -31,6 +31,4 @@ app.get('/test/express', (_req, res) => { res.send({ test_data: headers }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-error-in-tool-express.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-error-in-tool-express.mjs index 82bfe3c35445..0565e1221b2d 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-error-in-tool-express.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario-error-in-tool-express.mjs @@ -45,6 +45,5 @@ app.get('/test/error-in-tool', async (_req, res, next) => { res.send({ message: 'OK' }); }); -Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); From 0de5fc7f40c51784746a1ee306e08a40708994b7 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 21 Aug 2026 11:11:46 +0200 Subject: [PATCH 05/12] test fixes --- .../handle-error/instrument-no-tracing.mjs | 3 ++ .../instrument-setup-error-handler.mjs | 12 ++++++++ .../scenario-setup-error-handler.mjs | 30 ++++++------------- .../suites/express/handle-error/test.ts | 2 +- .../suites/express/requestUser/instrument.mjs | 3 ++ .../suites/modules/instrument.mjs | 3 ++ .../suites/modules/server.js | 3 ++ .../crashed-session-aggregate/test.ts | 4 ++- .../errored-session-aggregate/test.ts | 4 ++- .../sessions/exited-session-aggregate/test.ts | 4 ++- .../suites/sessions/instrument.mjs | 18 +++++++++++ .../suites/sessions/{server.ts => server.mjs} | 16 +--------- .../suites/tracing/vercelai/test.ts | 8 ++++- 13 files changed, 69 insertions(+), 41 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/express/handle-error/instrument-setup-error-handler.mjs create mode 100644 dev-packages/node-integration-tests/suites/sessions/instrument.mjs rename dev-packages/node-integration-tests/suites/sessions/{server.ts => server.mjs} (61%) diff --git a/dev-packages/node-integration-tests/suites/express/handle-error/instrument-no-tracing.mjs b/dev-packages/node-integration-tests/suites/express/handle-error/instrument-no-tracing.mjs index 0418e09c174f..b9b18f93016f 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error/instrument-no-tracing.mjs +++ b/dev-packages/node-integration-tests/suites/express/handle-error/instrument-no-tracing.mjs @@ -6,4 +6,7 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, + // With tracing off, `expressIntegration()` is not a default integration, so opt in explicitly to + // get automatic error capture. + integrations: [Sentry.expressIntegration()], }); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error/instrument-setup-error-handler.mjs b/dev-packages/node-integration-tests/suites/express/handle-error/instrument-setup-error-handler.mjs new file mode 100644 index 000000000000..0c119ab586a0 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/handle-error/instrument-setup-error-handler.mjs @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +// Intentionally no `expressIntegration()` and no tracing: this isolates the deprecated +// `setupExpressErrorHandler` middleware so it is the sole error capturer (mechanism +// `auto.middleware.express`), rather than the channel-based auto capture (`auto.http.express`). +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error/scenario-setup-error-handler.mjs b/dev-packages/node-integration-tests/suites/express/handle-error/scenario-setup-error-handler.mjs index 6f2cb6e2108d..d01838ce5dbf 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error/scenario-setup-error-handler.mjs +++ b/dev-packages/node-integration-tests/suites/express/handle-error/scenario-setup-error-handler.mjs @@ -1,35 +1,23 @@ import * as Sentry from '@sentry/node'; import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; import express from 'express'; const app = express(); -Sentry.setTag('global', 'tag'); +app.use(cors()); -app.get('/test/express/:id', req => { - throw new Error(`test_error with id ${req.params.id}`); +app.get('/test1', (_req, _res) => { + throw new Error('error_1'); }); -app.get('/test/withScope', () => { - Sentry.withScope(scope => { - scope.setTag('local', 'tag'); - throw new Error('test_error'); - }); +app.get('/test2', (_req, _res) => { + throw new Error('error_2'); }); -app.get('/test/isolationScope', () => { - Sentry.getIsolationScope().setTag('isolation-scope', 'tag'); - throw new Error('isolation_test_error'); +// Deprecated but still supported: capture route errors via the error-handling middleware. +Sentry.setupExpressErrorHandler(app, { + shouldHandleError: error => error.message === 'error_2', }); -app.get('/test/withIsolationScope', () => { - Sentry.withIsolationScope(iScope => { - iScope.setTag('with-isolation-scope', 'tag'); - throw new Error('with_isolation_scope_test_error'); - }); -}); - -// Deprecated but still supported -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error/test.ts b/dev-packages/node-integration-tests/suites/express/handle-error/test.ts index d5a3650218ae..badf2f09004b 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error/test.ts +++ b/dev-packages/node-integration-tests/suites/express/handle-error/test.ts @@ -301,7 +301,7 @@ describe('express error handling', () => { createCjsTests( __dirname, 'scenario-setup-error-handler.mjs', - 'instrument-should-handle-error.mjs', + 'instrument-setup-error-handler.mjs', (createRunner, test) => { test('captures only errors for which shouldHandleError returns true', async () => { const runner = createRunner() diff --git a/dev-packages/node-integration-tests/suites/express/requestUser/instrument.mjs b/dev-packages/node-integration-tests/suites/express/requestUser/instrument.mjs index ef202cf6629c..288b02d3ba69 100644 --- a/dev-packages/node-integration-tests/suites/express/requestUser/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/express/requestUser/instrument.mjs @@ -7,4 +7,7 @@ Sentry.init({ release: '1.0', transport: loggingTransport, debug: true, + // With tracing off, `expressIntegration()` is not a default integration, so opt in explicitly to + // get automatic error capture. + integrations: [Sentry.expressIntegration()], }); diff --git a/dev-packages/node-integration-tests/suites/modules/instrument.mjs b/dev-packages/node-integration-tests/suites/modules/instrument.mjs index 0418e09c174f..432e6ae5475b 100644 --- a/dev-packages/node-integration-tests/suites/modules/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/modules/instrument.mjs @@ -6,4 +6,7 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, + // Tracing is off, so `expressIntegration()` is not a default integration; opt in explicitly to + // capture the thrown route error this test inspects. + integrations: [Sentry.expressIntegration()], }); diff --git a/dev-packages/node-integration-tests/suites/modules/server.js b/dev-packages/node-integration-tests/suites/modules/server.js index ebdfc5a607a0..c57515443a5e 100644 --- a/dev-packages/node-integration-tests/suites/modules/server.js +++ b/dev-packages/node-integration-tests/suites/modules/server.js @@ -6,6 +6,9 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, + // Tracing is off, so `expressIntegration()` is not a default integration; opt in explicitly to + // capture the thrown route error this test inspects. + integrations: [Sentry.expressIntegration()], }); // express must be required after Sentry is initialized diff --git a/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts index ad8166e3163c..b422aca7d289 100644 --- a/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/crashed-session-aggregate/test.ts @@ -1,4 +1,5 @@ import { afterAll, expect, test } from 'vitest'; +import { join } from 'path'; import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; afterAll(() => { @@ -6,7 +7,8 @@ afterAll(() => { }); test('should aggregate successful and crashed sessions', async () => { - const runner = createRunner(__dirname, '..', 'server.ts') + const runner = createRunner(__dirname, '..', 'server.mjs') + .withInstrument(join(__dirname, '..', 'instrument.mjs')) .ignore('transaction', 'event') .unignore('sessions') .expect({ diff --git a/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts index d2c83f5d30fa..8a3a91e6997a 100644 --- a/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/errored-session-aggregate/test.ts @@ -1,4 +1,5 @@ import { afterAll, expect, test } from 'vitest'; +import { join } from 'path'; import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; afterAll(() => { @@ -6,7 +7,8 @@ afterAll(() => { }); test('should aggregate successful, crashed and erroneous sessions', async () => { - const runner = createRunner(__dirname, '..', 'server.ts') + const runner = createRunner(__dirname, '..', 'server.mjs') + .withInstrument(join(__dirname, '..', 'instrument.mjs')) .ignore('transaction', 'event') .unignore('sessions') .expect({ diff --git a/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts b/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts index 152861e87765..79ebc5bf5566 100644 --- a/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts +++ b/dev-packages/node-integration-tests/suites/sessions/exited-session-aggregate/test.ts @@ -1,4 +1,5 @@ import { afterAll, expect, test } from 'vitest'; +import { join } from 'path'; import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; afterAll(() => { @@ -6,7 +7,8 @@ afterAll(() => { }); test('should aggregate successful sessions', async () => { - const runner = createRunner(__dirname, '..', 'server.ts') + const runner = createRunner(__dirname, '..', 'server.mjs') + .withInstrument(join(__dirname, '..', 'instrument.mjs')) .ignore('transaction', 'event') .unignore('sessions') .expect({ diff --git a/dev-packages/node-integration-tests/suites/sessions/instrument.mjs b/dev-packages/node-integration-tests/suites/sessions/instrument.mjs new file mode 100644 index 000000000000..c8a21c59d4b2 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/sessions/instrument.mjs @@ -0,0 +1,18 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + integrations: [ + Sentry.httpIntegration({ + // Flush after 2 seconds (to avoid waiting for the default 60s) + sessionFlushingDelayMS: 2_000, + }), + // Tracing is off, so `expressIntegration()` is not a default integration; opt in explicitly so + // unhandled route errors are captured and their request sessions are marked as crashed. + Sentry.expressIntegration(), + ], +}); diff --git a/dev-packages/node-integration-tests/suites/sessions/server.ts b/dev-packages/node-integration-tests/suites/sessions/server.mjs similarity index 61% rename from dev-packages/node-integration-tests/suites/sessions/server.ts rename to dev-packages/node-integration-tests/suites/sessions/server.mjs index 1b2fbab10df3..f2f8eb5bc05e 100644 --- a/dev-packages/node-integration-tests/suites/sessions/server.ts +++ b/dev-packages/node-integration-tests/suites/sessions/server.mjs @@ -1,19 +1,5 @@ import * as Sentry from '@sentry/node'; -import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; - -Sentry.init({ - traceLifecycle: 'static', - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - transport: loggingTransport, - integrations: [ - Sentry.httpIntegration({ - // Flush after 2 seconds (to avoid waiting for the default 60s) - sessionFlushingDelayMS: 2_000, - }), - ], -}); - +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; import express from 'express'; const app = express(); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts index 3216a2f2bd2e..75945701e92f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts @@ -388,7 +388,13 @@ describe('Vercel AI integration (v4)', () => { }), ]), ); - expect(errorEvent!.contexts!.trace!.span_id).toBe(transactionEvent!.contexts!.trace!.span_id); + // The error is captured on the Express layer span (a child of the request transaction), so its + // `span_id` is one of the transaction's spans rather than the transaction's own (root) span. + const transactionSpanIds = [ + transactionEvent!.contexts!.trace!.span_id, + ...(transactionEvent!.spans ?? []).map(span => span.span_id), + ]; + expect(transactionSpanIds).toContain(errorEvent!.contexts!.trace!.span_id); }); }); From 054cbb2a45cb31f1e2296e700469e67955aba17f Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 26 Aug 2026 11:14:40 +0200 Subject: [PATCH 06/12] fixes --- .../node-express-esm-loader/src/app.mjs | 2 - .../src/app.mjs | 2 - .../node-express-mcp-v2/src/app.ts | 2 - .../node-express-streaming/src/app.ts | 2 - .../node-express-v5/src/app.ts | 2 - .../test-applications/node-express/src/app.ts | 2 - .../test-applications/tsx-express/src/app.ts | 2 - .../instrument-setup-error-handler.mjs | 7 +- .../scenario-setup-error-handler-fallback.mjs | 24 +++++ .../scenario-setup-error-handler.mjs | 9 +- .../suites/express/handle-error/test.ts | 42 ++++++++- .../httpIntegration/server-outgoingHooks.js | 2 - .../core/src/integrations/express/index.ts | 92 +------------------ packages/core/src/server-exports.ts | 2 +- packages/node/src/index.ts | 2 +- .../node/src/integrations/tracing/express.ts | 21 ----- packages/server-utils/src/index.ts | 4 + .../src/integrations/express/error-handled.ts | 31 +++++++ .../src/integrations/express/error-handler.ts | 90 ++++++++++++++++++ .../integrations/express/instrumentation.ts | 36 ++++++-- .../src/integrations/express/types.ts | 6 ++ .../express-error-handler.test.ts | 4 +- 22 files changed, 240 insertions(+), 146 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/express/handle-error/scenario-setup-error-handler-fallback.mjs delete mode 100644 packages/node/src/integrations/tracing/express.ts create mode 100644 packages/server-utils/src/integrations/express/error-handled.ts create mode 100644 packages/server-utils/src/integrations/express/error-handler.ts diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-loader/src/app.mjs b/dev-packages/e2e-tests/test-applications/node-express-esm-loader/src/app.mjs index 377bc713525c..9b20d08d7818 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-esm-loader/src/app.mjs +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-loader/src/app.mjs @@ -42,8 +42,6 @@ app.get('/test-error', function (req, res) { }, 100); }); -Sentry.setupExpressErrorHandler(app); - app.use(function onError(err, req, res, next) { // The error id is attached to `res.sentry` to be returned // and optionally displayed to the user for support. diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/src/app.mjs b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/src/app.mjs index 0d318ab5fc13..2ad604228ae5 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/src/app.mjs +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-without-loader/src/app.mjs @@ -32,8 +32,6 @@ app.get('/test-error', function (req, res) { }, 100); }); -Sentry.setupExpressErrorHandler(app); - app.use(function onError(err, req, res, next) { // The error id is attached to `res.sentry` to be returned // and optionally displayed to the user for support. diff --git a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/src/app.ts index 0fa1366dd2d6..3ed8df4a040a 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-mcp-v2/src/app.ts @@ -12,8 +12,6 @@ app.get('/test-success', function (_req, res) { res.send({ version: 'v1' }); }); -Sentry.setupExpressErrorHandler(app); - app.listen(port, () => { console.log(`Example app listening on port ${port}`); }); diff --git a/dev-packages/e2e-tests/test-applications/node-express-streaming/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express-streaming/src/app.ts index f02a6afff084..d4744361ad0b 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-streaming/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-streaming/src/app.ts @@ -97,8 +97,6 @@ app.get('/test-local-variables-caught', function (req, res) { res.send({ exceptionId, randomVariableToRecord }); }); -Sentry.setupExpressErrorHandler(app); - // @ts-ignore app.use(function onError(err, req, res, next) { // The error id is attached to `res.sentry` to be returned diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express-v5/src/app.ts index b5771bd02612..c4b7d08c2f91 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-v5/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-v5/src/app.ts @@ -104,8 +104,6 @@ app.get('/test-local-variables-caught', function (req, res) { res.send({ exceptionId, randomVariableToRecord }); }); -Sentry.setupExpressErrorHandler(app); - // @ts-ignore app.use(function onError(err, req, res, next) { // The error id is attached to `res.sentry` to be returned diff --git a/dev-packages/e2e-tests/test-applications/node-express/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express/src/app.ts index 5c93ad9ce05f..eaf7e8a5d50c 100644 --- a/dev-packages/e2e-tests/test-applications/node-express/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-express/src/app.ts @@ -105,8 +105,6 @@ app.get('/test-local-variables-caught', function (req, res) { res.send({ exceptionId, randomVariableToRecord }); }); -Sentry.setupExpressErrorHandler(app); - // @ts-ignore app.use(function onError(err, req, res, next) { // The error id is attached to `res.sentry` to be returned diff --git a/dev-packages/e2e-tests/test-applications/tsx-express/src/app.ts b/dev-packages/e2e-tests/test-applications/tsx-express/src/app.ts index 83b8d28b77a4..c13201b0d30c 100644 --- a/dev-packages/e2e-tests/test-applications/tsx-express/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/tsx-express/src/app.ts @@ -74,8 +74,6 @@ app.get('/test-local-variables-caught', function (req, res) { res.send({ exceptionId, randomVariableToRecord }); }); -Sentry.setupExpressErrorHandler(app); - // @ts-ignore app.use(function onError(err, req, res, next) { // The error id is attached to `res.sentry` to be returned diff --git a/dev-packages/node-integration-tests/suites/express/handle-error/instrument-setup-error-handler.mjs b/dev-packages/node-integration-tests/suites/express/handle-error/instrument-setup-error-handler.mjs index 0c119ab586a0..c6a1aa2833e7 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error/instrument-setup-error-handler.mjs +++ b/dev-packages/node-integration-tests/suites/express/handle-error/instrument-setup-error-handler.mjs @@ -1,12 +1,13 @@ import * as Sentry from '@sentry/node'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; -// Intentionally no `expressIntegration()` and no tracing: this isolates the deprecated -// `setupExpressErrorHandler` middleware so it is the sole error capturer (mechanism -// `auto.middleware.express`), rather than the channel-based auto capture (`auto.http.express`). +// Disable the channel-based `expressIntegration()` so the deprecated `setupExpressErrorHandler` +// middleware is the sole error capturer (mechanism `auto.middleware.express`) — the fallback for +// setups where the channel-based auto capture is unavailable. Sentry.init({ traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', transport: loggingTransport, + integrations: integrations => integrations.filter(integration => integration.name !== 'Express'), }); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error/scenario-setup-error-handler-fallback.mjs b/dev-packages/node-integration-tests/suites/express/handle-error/scenario-setup-error-handler-fallback.mjs new file mode 100644 index 000000000000..e0ea1168ee0f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express/handle-error/scenario-setup-error-handler-fallback.mjs @@ -0,0 +1,24 @@ +import * as Sentry from '@sentry/node'; +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); + +app.use(cors()); + +app.get('/test1', (_req, _res) => { + throw new Error('error_1'); +}); + +app.get('/test2', (_req, _res) => { + throw new Error('error_2'); +}); + +// With `expressIntegration` disabled (see the instrument file), the deprecated middleware is the sole +// capturer and its own `shouldHandleError` applies. +Sentry.setupExpressErrorHandler(app, { + shouldHandleError: error => error.message === 'error_2', +}); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error/scenario-setup-error-handler.mjs b/dev-packages/node-integration-tests/suites/express/handle-error/scenario-setup-error-handler.mjs index d01838ce5dbf..9e681d25561c 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error/scenario-setup-error-handler.mjs +++ b/dev-packages/node-integration-tests/suites/express/handle-error/scenario-setup-error-handler.mjs @@ -15,9 +15,10 @@ app.get('/test2', (_req, _res) => { throw new Error('error_2'); }); -// Deprecated but still supported: capture route errors via the error-handling middleware. -Sentry.setupExpressErrorHandler(app, { - shouldHandleError: error => error.message === 'error_2', -}); +// Deprecated, and here with a permissive (default) predicate that would capture both errors. But the +// channel-based `expressIntegration` (configured with `shouldHandleError: error_2` in the instrument) +// takes precedence: it is the single registered handler, so its predicate decides what is captured and +// this middleware must neither capture `error_1` nor double-capture `error_2`. +Sentry.setupExpressErrorHandler(app); startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express/handle-error/test.ts b/dev-packages/node-integration-tests/suites/express/handle-error/test.ts index badf2f09004b..3e6f8ce1feac 100644 --- a/dev-packages/node-integration-tests/suites/express/handle-error/test.ts +++ b/dev-packages/node-integration-tests/suites/express/handle-error/test.ts @@ -298,12 +298,52 @@ describe('express error handling', () => { }); describe('setupExpressErrorHandler', () => { + // Precedence: with both the channel-based integration and the deprecated middleware present, the + // integration is the single registered handler. Its `shouldHandleError` (error_2 only) decides + // what is captured (mechanism `auto.http.express`), so `error_1` is dropped even though the + // deprecated middleware's default predicate would capture it, and `error_2` is captured once. createCjsTests( __dirname, 'scenario-setup-error-handler.mjs', + 'instrument-should-handle-error.mjs', + (createRunner, test) => { + test('expressIntegration takes precedence over the deprecated handler', async () => { + const runner = createRunner() + .expect({ + event: { + exception: { + values: [ + { + mechanism: { + type: 'auto.http.express', + handled: false, + }, + value: 'error_2', + }, + ], + }, + }, + }) + .start(); + + // dropped by the integration's shouldHandleError; the deprecated handler must NOT capture it either + runner.makeRequest('get', '/test1', { expectError: true }); + // captured once, by the integration + runner.makeRequest('get', '/test2', { expectError: true }); + + await runner.completed(); + }); + }, + ); + + // Fallback: with `expressIntegration` disabled, the deprecated middleware is the sole capturer and + // its own `shouldHandleError` applies (mechanism `auto.middleware.express`). + createCjsTests( + __dirname, + 'scenario-setup-error-handler-fallback.mjs', 'instrument-setup-error-handler.mjs', (createRunner, test) => { - test('captures only errors for which shouldHandleError returns true', async () => { + test('deprecated handler captures with its own shouldHandleError when expressIntegration is disabled', async () => { const runner = createRunner() .expect({ event: { diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-outgoingHooks.js b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-outgoingHooks.js index 63d708ef7a75..23a9006a63b5 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-outgoingHooks.js +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-outgoingHooks.js @@ -44,8 +44,6 @@ app.get('/testOutgoing', (_req, response) => { }); }); -Sentry.setupExpressErrorHandler(app); - startExpressServerAndSendPortToRunner(app); function makeHttpRequest(url) { diff --git a/packages/core/src/integrations/express/index.ts b/packages/core/src/integrations/express/index.ts index ce17912fe1d5..70f798ceb0f0 100644 --- a/packages/core/src/integrations/express/index.ts +++ b/packages/core/src/integrations/express/index.ts @@ -32,32 +32,19 @@ /* oxlint-disable typescript/no-deprecated */ import { debug } from '../../utils/debug-logger'; -import { captureException } from '../../exports'; import { DEBUG_BUILD } from '../../debug-build'; import type { ExpressApplication, - ExpressErrorMiddleware, - ExpressHandlerOptions, ExpressIntegrationOptions, ExpressLayer, - ExpressMiddleware, ExpressModuleExport, - ExpressRequest, - ExpressResponse, ExpressRouter, ExpressRouterv4, ExpressRouterv5, - MiddlewareError, } from './types'; -import { - defaultShouldHandleError, - getLayerPath, - isExpressWithoutRouterPrototype, - isExpressWithRouterPrototype, -} from './utils'; +import { getLayerPath, isExpressWithoutRouterPrototype, isExpressWithRouterPrototype } from './utils'; import { wrapMethod } from '../../utils/object'; import { patchLayer } from './patch-layer'; -import { setSDKProcessingMetadata } from './set-sdk-processing-metadata'; import { getDefaultExport } from '../../utils/get-default-export'; /** @@ -165,78 +152,5 @@ export function patchExpressModule( return express; } -/** - * An Express-compatible error handler, used by setupExpressErrorHandler - * - * @deprecated `expressIntegration()` now captures errors automatically. This export is deprecated - * and will be removed in the next major version. - */ -export function expressErrorHandler(options?: ExpressHandlerOptions): ExpressErrorMiddleware { - return function sentryErrorMiddleware( - error: MiddlewareError, - request: ExpressRequest, - res: ExpressResponse, - next: (error: MiddlewareError) => void, - ): void { - // When an error happens, the `expressRequestHandler` middleware does not run, so we set it here too - setSDKProcessingMetadata(request); - const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError; - - if (shouldHandleError(error)) { - const eventId = captureException(error, { - mechanism: { type: 'auto.middleware.express', handled: false }, - }); - (res as { sentry?: string }).sentry = eventId; - } - - next(error); - }; -} - -/** - * Add an Express error handler to capture errors to Sentry. - * - * The error handler must be before any other middleware and after all controllers. - * - * @param app The Express instances - * @param options {ExpressHandlerOptions} Configuration options for the handler - * - * @example - * ```javascript - * import * as Sentry from 'sentry/deno'; // or any other @sentry/ - * import * as express from 'express'; - * - * Sentry.instrumentExpress(express); - * - * const app = express(); - * - * // Add your routes, etc. - * - * // Add this after all routes, - * // but before any and other error-handling middlewares are defined - * Sentry.setupExpressErrorHandler(app); - * - * app.listen(3000); - * ``` - * - * @deprecated `expressIntegration()` now captures errors automatically, so calling this is no longer - * necessary. To customize which errors are captured, pass `shouldHandleError` to `expressIntegration()`. - * This export is deprecated and will be removed in the next major version. - */ -export function setupExpressErrorHandler( - app: { - //oxlint-disable-next-line no-explicit-any - use: (middleware: any) => unknown; - }, - options?: ExpressHandlerOptions, -): void { - app.use(expressRequestHandler()); - app.use(expressErrorHandler(options)); -} - -function expressRequestHandler(): ExpressMiddleware { - return function sentryRequestMiddleware(request: ExpressRequest, _res: ExpressResponse, next: () => void): void { - setSDKProcessingMetadata(request); - next(); - }; -} +// The deprecated `expressErrorHandler` / `setupExpressErrorHandler` now live in `@sentry/server-utils` +// (alongside the channel-based `expressIntegration()`), so they are not defined here anymore. diff --git a/packages/core/src/server-exports.ts b/packages/core/src/server-exports.ts index 876d53068fc7..5037dc9cb784 100644 --- a/packages/core/src/server-exports.ts +++ b/packages/core/src/server-exports.ts @@ -19,7 +19,7 @@ export { flushIfServerless } from './utils/flushIfServerless'; export { callFrameToStackFrame, watchdogTimer } from './utils/anr'; export { safeUnref as _INTERNAL_safeUnref } from './utils/timer'; /* oxlint-disable typescript/no-deprecated -- deprecated Express exports, kept until the next major */ -export { patchExpressModule, setupExpressErrorHandler, expressErrorHandler } from './integrations/express/index'; +export { patchExpressModule } from './integrations/express/index'; export type { ExpressIntegrationOptions, ExpressHandlerOptions, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index c5705054e952..810e290df15c 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -2,7 +2,7 @@ export { httpIntegration } from './integrations/http'; export { nativeNodeFetchIntegration } from './integrations/node-fetch'; export { fsIntegration } from './integrations/fs'; // oxlint-disable-next-line typescript/no-deprecated -export { expressErrorHandler, setupExpressErrorHandler } from './integrations/tracing/express'; +export { expressErrorHandler, setupExpressErrorHandler } from '@sentry/server-utils'; export { amqplibIntegration, anthropicAIIntegration, diff --git a/packages/node/src/integrations/tracing/express.ts b/packages/node/src/integrations/tracing/express.ts deleted file mode 100644 index cdcb142195a7..000000000000 --- a/packages/node/src/integrations/tracing/express.ts +++ /dev/null @@ -1,21 +0,0 @@ -// oxlint-disable-next-line typescript/no-deprecated -import { setupExpressErrorHandler as coreSetupExpressErrorHandler, type ExpressHandlerOptions } from '@sentry/core'; -// oxlint-disable-next-line typescript/no-deprecated -export { expressErrorHandler } from '@sentry/core'; - -/** - * Add an Express error handler to capture errors to Sentry. - * - * @deprecated `expressIntegration()` now captures errors automatically, so calling this is no longer - * necessary. To customize which errors are captured, pass `shouldHandleError` to `expressIntegration()`. - * This export is deprecated and will be removed in the next major version. - */ -export function setupExpressErrorHandler( - //oxlint-disable-next-line no-explicit-any - app: { use: (middleware: any) => unknown }, - // oxlint-disable-next-line typescript/no-deprecated - options?: ExpressHandlerOptions, -): void { - // oxlint-disable-next-line typescript/no-deprecated - coreSetupExpressErrorHandler(app, options); -} diff --git a/packages/server-utils/src/index.ts b/packages/server-utils/src/index.ts index a44b09938f76..88af16be373e 100644 --- a/packages/server-utils/src/index.ts +++ b/packages/server-utils/src/index.ts @@ -51,6 +51,10 @@ export { postgresJsIntegration } from './integrations/postgres-js'; export { tediousIntegration } from './integrations/tedious'; export { vercelAIIntegration } from './integrations/vercel-ai'; export { expressIntegration } from './integrations/express'; +/* oxlint-disable typescript/no-deprecated -- deprecated Express error-handler exports, kept until the next major */ +export { expressErrorHandler, setupExpressErrorHandler } from './integrations/express/error-handler'; +export type { ExpressHandlerOptions } from './integrations/express/types'; +/* oxlint-enable typescript/no-deprecated */ export { firebaseIntegration } from './integrations/firebase'; export { getTracingIntegrations, getErrorIntegrations } from './integrations'; diff --git a/packages/server-utils/src/integrations/express/error-handled.ts b/packages/server-utils/src/integrations/express/error-handled.ts new file mode 100644 index 000000000000..89efc98a08a7 --- /dev/null +++ b/packages/server-utils/src/integrations/express/error-handled.ts @@ -0,0 +1,31 @@ +import { addNonEnumerableProperty } from '@sentry/core'; + +// Non-enumerable marker set on the Express *request* once the channel-based `expressIntegration()` has +// taken responsibility for an error on it — either captured it, or deliberately skipped it per +// `shouldHandleError`. The deprecated `expressErrorHandler` middleware reads this to defer to the +// integration, so an Express error is only ever handled once and the integration's `shouldHandleError` +// decision always wins. +// +// The marker lives on the request (not the error) on purpose: the request is always a mutable object +// and is the same instance across every layer the error bubbles through and the error-handling +// middleware, whereas the thrown value may be frozen or a primitive. It also does not rely on +// `captureException`'s global `__sentry_captured__` dedup, which is only set when an error is actually +// captured and so cannot express a "deliberately skipped" decision. +const EXPRESS_ERROR_HANDLED = '__sentry_express_error_handled__'; + +/** + * Mark an Express request as having had its error handled by the channel-based `expressIntegration()`, + * so the deprecated `expressErrorHandler` middleware defers to that decision. + */ +export function markExpressErrorHandled(request: unknown): void { + if (request && typeof request === 'object') { + addNonEnumerableProperty(request, EXPRESS_ERROR_HANDLED, true); + } +} + +/** + * Whether the channel-based `expressIntegration()` has already handled an error on this Express request. + */ +export function isExpressErrorHandled(request: unknown): boolean { + return !!(request && typeof request === 'object' && (request as Record)[EXPRESS_ERROR_HANDLED]); +} diff --git a/packages/server-utils/src/integrations/express/error-handler.ts b/packages/server-utils/src/integrations/express/error-handler.ts new file mode 100644 index 000000000000..b081bd207402 --- /dev/null +++ b/packages/server-utils/src/integrations/express/error-handler.ts @@ -0,0 +1,90 @@ +import { captureException, getIsolationScope, httpRequestToRequestData } from '@sentry/core'; +import { isExpressErrorHandled } from './error-handled'; +import type { ExpressHandlerOptions, ExpressRequest, ExpressResponse, MiddlewareError } from './types'; +import { defaultShouldHandleError } from './utils'; + +type ExpressErrorMiddleware = ( + error: MiddlewareError, + request: ExpressRequest, + res: ExpressResponse, + next: (error: MiddlewareError) => void, +) => void; + +type ExpressMiddleware = (request: ExpressRequest, res: ExpressResponse, next: () => void) => void; + +/** + * Set request data on the isolation scope so a captured error carries request context. Mirrors the + * request handler middleware, which does not run once an error short-circuits the middleware chain. + */ +function setSDKProcessingMetadata(request: ExpressRequest): void { + const sdkProcMeta = getIsolationScope()?.getScopeData()?.sdkProcessingMetadata; + if (!sdkProcMeta?.normalizedRequest) { + const normalizedRequest = httpRequestToRequestData(request); + getIsolationScope().setSDKProcessingMetadata({ normalizedRequest }); + } +} + +/** + * An Express-compatible error handler, used by {@link setupExpressErrorHandler}. + * + * @deprecated `expressIntegration()` now captures errors automatically. This export is deprecated and + * will be removed in the next major version. + */ +export function expressErrorHandler(options?: ExpressHandlerOptions): ExpressErrorMiddleware { + return function sentryErrorMiddleware(error, request, res, next): void { + // When an error happens, the request handler middleware does not run, so we set it here too. + setSDKProcessingMetadata(request); + + // The channel-based `expressIntegration()` captures at the throw site, before this middleware runs, + // and marks the request when it does. If it already handled this request's error (captured it, or + // deliberately skipped it per its own `shouldHandleError`), defer to that decision: the integration + // is the single registered handler and its `shouldHandleError` wins, so we never double-capture or + // override it here. + if (isExpressErrorHandled(request)) { + next(error); + return; + } + + const shouldHandleError = options?.shouldHandleError || defaultShouldHandleError; + + if (shouldHandleError(error)) { + const eventId = captureException(error, { + mechanism: { type: 'auto.middleware.express', handled: false }, + }); + (res as { sentry?: string }).sentry = eventId; + } + + next(error); + }; +} + +function expressRequestHandler(): ExpressMiddleware { + return function sentryRequestMiddleware(request, _res, next): void { + setSDKProcessingMetadata(request); + next(); + }; +} + +/** + * Add an Express error handler to capture errors to Sentry. + * + * The error handler must be before any other middleware and after all controllers. + * + * @param app The Express instance + * @param options {ExpressHandlerOptions} Configuration options for the handler + * + * @deprecated `expressIntegration()` now captures errors automatically, so calling this is no longer + * necessary. To customize which errors are captured, pass `shouldHandleError` to `expressIntegration()`. + * This export is deprecated and will be removed in the next major version. + */ +export function setupExpressErrorHandler( + app: { + // oxlint-disable-next-line no-explicit-any + use: (middleware: any) => unknown; + }, + options?: ExpressHandlerOptions, +): void { + app.use(expressRequestHandler()); + // oxlint-disable-next-line typescript/no-deprecated + app.use(expressErrorHandler(options)); +} diff --git a/packages/server-utils/src/integrations/express/instrumentation.ts b/packages/server-utils/src/integrations/express/instrumentation.ts index ff42148885c4..59b7f7fdfb98 100644 --- a/packages/server-utils/src/integrations/express/instrumentation.ts +++ b/packages/server-utils/src/integrations/express/instrumentation.ts @@ -40,6 +40,7 @@ import type { RegistrationChannelContext, } from './types'; import { defaultShouldHandleError } from './utils'; +import { isExpressErrorHandled, markExpressErrorHandled } from './error-handled'; import { setHttpServerSpanRouteAttribute } from '../../utils/setHttpServerSpanRouteAttribute'; const ORIGIN = 'auto.http.express'; @@ -110,10 +111,17 @@ export function instrumentExpress( /** * Capture an error surfaced on a layer's `handle_request` channel — the throw - * site, which runs before any user error-handling middleware. Duplicate captures - * (the error bubbling through parent layers, or a user also calling - * `setupExpressErrorHandler`) are collapsed by `captureException`'s per-object - * dedup, so only the first send survives. + * site, which runs before any user error-handling middleware. + * + * Each request's error is handled exactly once: the same error surfaces on every + * parent layer's `error` event as it bubbles, and a user may also still call the + * deprecated `setupExpressErrorHandler`. We mark the request the first time we + * see it, so later layers and that middleware defer to this decision — the + * integration is the single registered handler and its `shouldHandleError` wins. + * This deliberately does not rely on `captureException`'s global dedup, which is + * only set when an error is actually captured and so cannot express a + * "deliberately skipped" decision (leaving the deprecated middleware free to + * capture it and override `shouldHandleError`). * * `shouldHandleError` is the raw integration option: `false` disables capture * entirely, a function customizes the gate, and `undefined` falls back to @@ -123,10 +131,6 @@ export function captureLayerError( data: HandleChannelContext, shouldHandleError: ExpressShouldHandleError | undefined, ): void { - if (shouldHandleError === false) { - return; - } - const error = data.error; // `next('route')` / `next('router')` are Express control-flow signals, not errors. @@ -134,6 +138,22 @@ export function captureLayerError( return; } + // Take responsibility for this request's error exactly once (see the doc comment above). The marker + // lives on the request — the same instance across every bubbling layer and the error middleware, + // and always a mutable object, unlike the thrown value which may be frozen or a primitive. Mark + // before the `shouldHandleError` gate so a "skip" decision also suppresses the deprecated middleware. + const request = data.arguments?.[0] as ExpressRequest | undefined; + if (request) { + if (isExpressErrorHandled(request)) { + return; + } + markExpressErrorHandled(request); + } + + if (shouldHandleError === false) { + return; + } + if (!(shouldHandleError ?? defaultShouldHandleError)(error as MiddlewareError)) { return; } diff --git a/packages/server-utils/src/integrations/express/types.ts b/packages/server-utils/src/integrations/express/types.ts index 5536f524e849..c5c038ba243f 100644 --- a/packages/server-utils/src/integrations/express/types.ts +++ b/packages/server-utils/src/integrations/express/types.ts @@ -73,6 +73,12 @@ export interface MiddlewareError extends Error { /** Callback deciding whether an error should be captured; `false` disables capture entirely. */ export type ExpressShouldHandleError = ((error: MiddlewareError) => boolean) | false; +/** Options for the deprecated `setupExpressErrorHandler` / `expressErrorHandler`. */ +export interface ExpressHandlerOptions { + /** Callback deciding whether an error should be captured and sent to Sentry. */ + shouldHandleError?: (error: MiddlewareError) => boolean; +} + type IgnoreMatcher = string | RegExp | ((name: string) => boolean); export interface ExpressIntegrationOptions { /** Ignore specific based on their name */ diff --git a/packages/server-utils/test/integrations/express-error-handler.test.ts b/packages/server-utils/test/integrations/express-error-handler.test.ts index 05c81b35fcc8..3457b3c04289 100644 --- a/packages/server-utils/test/integrations/express-error-handler.test.ts +++ b/packages/server-utils/test/integrations/express-error-handler.test.ts @@ -1,7 +1,7 @@ import * as SentryCore from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, type MockInstance, vi } from 'vitest'; -import { captureLayerError } from '../../../src/integrations/express/instrumentation'; -import type { HandleChannelContext } from '../../../src/integrations/express/types'; +import { captureLayerError } from '../../src/integrations/express/instrumentation'; +import type { HandleChannelContext } from '../../src/integrations/express/types'; function makeErrorData(error: unknown, span?: unknown): HandleChannelContext { return { error, _sentrySpan: span } as unknown as HandleChannelContext; From c42ffe93936b50746f539f178098a0a8d471cb93 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 26 Aug 2026 12:56:42 +0200 Subject: [PATCH 07/12] fix tests --- .../lib/integrations/express/index.test.ts | 138 +----------------- 1 file changed, 1 insertion(+), 137 deletions(-) diff --git a/packages/core/test/lib/integrations/express/index.test.ts b/packages/core/test/lib/integrations/express/index.test.ts index 7f1b1e9d1078..78fe8b8f2648 100644 --- a/packages/core/test/lib/integrations/express/index.test.ts +++ b/packages/core/test/lib/integrations/express/index.test.ts @@ -1,8 +1,4 @@ -import { - patchExpressModule, - expressErrorHandler, - setupExpressErrorHandler, -} from '../../../../src/integrations/express/index'; +import { patchExpressModule } from '../../../../src/integrations/express/index'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { Mock } from 'vitest'; @@ -15,39 +11,9 @@ import type { ExpressRoute, ExpressRouterv4, ExpressRouterv5, - ExpressResponse, - ExpressRequest, - ExpressMiddleware, - ExpressErrorMiddleware, - ExpressHandlerOptions, } from '../../../../src/integrations/express/types'; import type { WrappedFunction } from '../../../../src/types/wrappedfunction'; -const sdkProcessingMetadata: unknown[] = []; -const isolationScope = { - _scopeData: {} as { sdkProcessingMetadata?: unknown }, - getScopeData() { - return this._scopeData; - }, - setSDKProcessingMetadata({ normalizedRequest }: { normalizedRequest: unknown }) { - sdkProcessingMetadata.push(normalizedRequest); - }, -}; - -vi.mock('../../../../src/currentScopes', () => ({ - getIsolationScope() { - return isolationScope; - }, -})); - -const capturedExceptions: [unknown, unknown][] = []; -vi.mock('../../../../src/exports', () => ({ - captureException(error: unknown, hint: unknown) { - capturedExceptions.push([error, hint]); - return 'eventId'; - }, -})); - vi.mock('../../../../src/debug-build', () => ({ DEBUG_BUILD: true, })); @@ -261,105 +227,3 @@ describe('patchExpressModule', () => { ]); }); }); - -describe('expressErrorHandler', () => { - it('handles the error if it should', () => { - const errorMiddleware = expressErrorHandler(); - const res = { status: 500 } as unknown as ExpressResponse; - const next = vi.fn(); - const err = new Error('err'); - const req = { headers: { request: 'headers' } } as unknown as ExpressRequest; - errorMiddleware(err, req, res, next); - expect((res as unknown as { sentry: string }).sentry).toBe('eventId'); - expect(capturedExceptions).toStrictEqual([ - [ - new Error('err'), - { - mechanism: { - handled: false, - type: 'auto.middleware.express', - }, - }, - ], - ]); - capturedExceptions.length = 0; - expect(sdkProcessingMetadata).toStrictEqual([ - { - url: undefined, - method: undefined, - query_string: undefined, - headers: Object.assign(Object.create(null), { request: 'headers' }), - cookies: undefined, - data: undefined, - }, - ]); - sdkProcessingMetadata.length = 0; - expect(next).toHaveBeenCalledExactlyOnceWith(err); - next.mockReset(); - }); - - it('does not the error if it should not', () => { - const errorMiddleware = expressErrorHandler({ - shouldHandleError: () => false, - }); - const res = { status: 500 } as unknown as ExpressResponse; - const req = { headers: { request: 'headers' } } as unknown as ExpressRequest; - const next = vi.fn(); - const err = new Error('err'); - errorMiddleware(err, req, res, next); - expect((res as unknown as { sentry?: string }).sentry).toBe(undefined); - expect(capturedExceptions).toStrictEqual([]); - expect(sdkProcessingMetadata).toStrictEqual([ - { - url: undefined, - method: undefined, - query_string: undefined, - headers: Object.assign(Object.create(null), { request: 'headers' }), - cookies: undefined, - data: undefined, - }, - ]); - sdkProcessingMetadata.length = 0; - expect(next).toHaveBeenCalledExactlyOnceWith(err); - next.mockReset(); - }); -}); - -describe('setupExpressErrorHandler', () => { - const appUseCalls: unknown[] = []; - const app = { - use: vi.fn((fn: unknown) => appUseCalls.push(fn)) as ( - middleware: ExpressMiddleware | ExpressErrorMiddleware, - ) => unknown, - }; - const options = {} as ExpressHandlerOptions; - it('should have a test here lolz', () => { - setupExpressErrorHandler(app, options); - expect(app.use).toHaveBeenCalledTimes(2); - const reqHandler = appUseCalls[0]; - expect(typeof reqHandler).toBe('function'); - const next = vi.fn(); - (reqHandler as (request: ExpressRequest, _res: ExpressResponse, next: () => void) => void)( - { - method: 'GET', - headers: { request: 'headers' }, - } as unknown as ExpressRequest, - {} as unknown as ExpressResponse, - next, - ); - expect(next).toHaveBeenCalledOnce(); - expect(sdkProcessingMetadata).toStrictEqual([ - { - cookies: undefined, - data: undefined, - headers: Object.assign(Object.create(null), { - request: 'headers', - }), - method: 'GET', - query_string: undefined, - url: undefined, - }, - ]); - sdkProcessingMetadata.length = 0; - }); -}); From 0688da43bea5b7ba5a41da0ae2e32bc81bb3e7a4 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Aug 2026 09:02:04 +0200 Subject: [PATCH 08/12] add to migration guide --- MIGRATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MIGRATION.md b/MIGRATION.md index 07a21a647602..ed7fcd0e8729 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -827,7 +827,7 @@ Affected SDKs: All server-side SDKs that support Express. If you prefer to capture errors yourself, set `shouldHandleError: false` on `expressIntegration()` to opt out of automatic capture entirely, and call `Sentry.captureException` from your own error-handling middleware. -The `expressErrorHandler` and `patchExpressModule` exports are deprecated for the same reason and will be removed in the next major version. +The `expressErrorHandler` and `patchExpressModule` exports are deprecated for the same reason and will be removed in the next major version. The export of `expressErrroHandler` and `setupExpressErrorHandler` is moved from `@sentry/core` to `@sentry/server-utils`. ### Span name changes From d4ca9db35ce5b7a6910476c37a26c52f90700672 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Thu, 27 Aug 2026 09:02:35 +0200 Subject: [PATCH 09/12] Update MIGRATION.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jan Peer Stöcklmair --- MIGRATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MIGRATION.md b/MIGRATION.md index ed7fcd0e8729..a61ea12c0620 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -825,7 +825,7 @@ Affected SDKs: All server-side SDKs that support Express. `expressIntegration()` now captures errors thrown from your route handlers automatically, so calling `setupExpressErrorHandler(app)` is no longer necessary — the call can be removed. It is deprecated and will be removed in the next major version. To customize which errors are captured, pass `shouldHandleError` to `expressIntegration()` (by default, 5xx errors and errors without a resolvable status are captured, while 3xx/4xx errors are not). -If you prefer to capture errors yourself, set `shouldHandleError: false` on `expressIntegration()` to opt out of automatic capture entirely, and call `Sentry.captureException` from your own error-handling middleware. +If you prefer to capture errors yourself, set `expressIntegration({ shouldHandleError: false })` to opt out of automatic capture entirely, and call `Sentry.captureException` from your own error-handling middleware. The `expressErrorHandler` and `patchExpressModule` exports are deprecated for the same reason and will be removed in the next major version. The export of `expressErrroHandler` and `setupExpressErrorHandler` is moved from `@sentry/core` to `@sentry/server-utils`. From aee661d1b689d939d23625ab97102fa33c2a787a Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Aug 2026 13:14:36 +0200 Subject: [PATCH 10/12] more tests --- .../test/integrations/express-error-handler.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/server-utils/test/integrations/express-error-handler.test.ts b/packages/server-utils/test/integrations/express-error-handler.test.ts index 3457b3c04289..569827880a02 100644 --- a/packages/server-utils/test/integrations/express-error-handler.test.ts +++ b/packages/server-utils/test/integrations/express-error-handler.test.ts @@ -52,8 +52,11 @@ describe('captureLayerError', () => { expect(captureExceptionSpy).not.toHaveBeenCalled(); }); - it('does not capture when there is no error', () => { - captureLayerError(makeErrorData(undefined), undefined); + it.each([ + ['undefined', undefined], + ['null', null], + ])('does not capture when there is no error (%s)', (_label, error) => { + captureLayerError(makeErrorData(error), undefined); expect(captureExceptionSpy).not.toHaveBeenCalled(); }); From 028c80fe96121ce4f33f96f9e405586d26236508 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Aug 2026 13:19:20 +0200 Subject: [PATCH 11/12] more --- .../test/integrations/express-error-handler.test.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/server-utils/test/integrations/express-error-handler.test.ts b/packages/server-utils/test/integrations/express-error-handler.test.ts index 569827880a02..e64a847d4867 100644 --- a/packages/server-utils/test/integrations/express-error-handler.test.ts +++ b/packages/server-utils/test/integrations/express-error-handler.test.ts @@ -52,15 +52,20 @@ describe('captureLayerError', () => { expect(captureExceptionSpy).not.toHaveBeenCalled(); }); - it.each([ - ['undefined', undefined], - ['null', null], - ])('does not capture when there is no error (%s)', (_label, error) => { + it.each([[undefined], [null], [false], [0], ['']])('does not capture when there is no error (%j)', error => { captureLayerError(makeErrorData(error), undefined); expect(captureExceptionSpy).not.toHaveBeenCalled(); }); + it.each([[1], [true], ['asdasdas']])('captures primitive errors (%j)', error => { + captureLayerError(makeErrorData(error), undefined); + + expect(captureExceptionSpy).toHaveBeenCalledWith(error, { + mechanism: { type: 'auto.http.express', handled: false }, + }); + }); + it('honors a custom shouldHandleError', () => { const shouldHandleError = vi.fn().mockReturnValue(true); const error = Object.assign(new Error('teapot'), { statusCode: 418 }); From d72ff90a2487b4e004e01b4dc365651a7bb5d01d Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Thu, 27 Aug 2026 16:03:37 +0200 Subject: [PATCH 12/12] Update MIGRATION.md Co-authored-by: Sigrid <32902192+s1gr1d@users.noreply.github.com> --- MIGRATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MIGRATION.md b/MIGRATION.md index a61ea12c0620..2afea27733a4 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -827,7 +827,7 @@ Affected SDKs: All server-side SDKs that support Express. If you prefer to capture errors yourself, set `expressIntegration({ shouldHandleError: false })` to opt out of automatic capture entirely, and call `Sentry.captureException` from your own error-handling middleware. -The `expressErrorHandler` and `patchExpressModule` exports are deprecated for the same reason and will be removed in the next major version. The export of `expressErrroHandler` and `setupExpressErrorHandler` is moved from `@sentry/core` to `@sentry/server-utils`. +The `expressErrorHandler` and `patchExpressModule` exports are deprecated for the same reason and will be removed in the next major version. The export of `expressErrorHandler` and `setupExpressErrorHandler` is moved from `@sentry/core` to `@sentry/server-utils`. ### Span name changes