Skip to content

fix(core, node): support loading Express options lazily#20211

Open
isaacs wants to merge 2 commits intodevelopfrom
isaacs/express-late-init
Open

fix(core, node): support loading Express options lazily#20211
isaacs wants to merge 2 commits intodevelopfrom
isaacs/express-late-init

Conversation

@isaacs
Copy link
Copy Markdown
Member

@isaacs isaacs commented Apr 10, 2026

Update the Express integration to accept the module export and a configuration function, rather than a configuration object. This is needed to support lazily calling Sentry.init after the module has been instrumented, without re-wrapping the methods to get the new config.

via: @mydea in #20188

Before submitting a pull request, please take a look at our
Contributing guidelines and verify:

  • If you've added code that should be tested, please add tests.
  • Ensure your code lints and the test suite passes (yarn lint) & (yarn test).
  • Link an issue if there is one related to your pull request. If no issue is linked, one will be auto-generated and linked.

Closes #JS-2117

Update the Express integration to accept the module export and a configuration
function, rather than a configuration object. This is needed to support lazily
calling Sentry.init *after* the module has been instrumented, without re-wrapping
the methods to get the new config.

via: @mydea in #20188
@isaacs isaacs requested a review from mydea April 10, 2026 15:35
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 10, 2026

Semver Impact of This PR

🟢 Patch (bug fixes)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

Core

  • Add enableTruncation option to Google GenAI integration by andreiborza in #20184
  • Add enableTruncation option to Anthropic AI integration by andreiborza in #20181
  • Add enableTruncation option to LangGraph integration by andreiborza in #20183
  • Add enableTruncation option to LangChain integration by andreiborza in #20182
  • Add enableTruncation option to OpenAI integration by andreiborza in #20167
  • Export a reusable function to add tracing headers by JPeer264 in #20076

Deps

  • Bump axios from 1.13.5 to 1.15.0 by dependabot in #20180
  • Bump hono from 4.12.7 to 4.12.12 by dependabot in #20118
  • Bump defu from 6.1.4 to 6.1.6 by dependabot in #20104

Other

  • (cloudflare) Propagate traceparent to RPC calls - via fetch by JPeer264 in #19991

Bug Fixes 🐛

Deno

  • Handle reader.closed rejection from releaseLock() in streaming by andreiborza in #20187
  • Avoid inferring invalid span op from Deno tracer by Lms24 in #20128

Other

  • (ci) Prevent command injection in ci-metadata workflow by fix-it-felix-sentry in #19899
  • (core, node) Support loading Express options lazily by isaacs in #20211
  • (e2e) Add op check to waitForTransaction in React Router e2e tests by copilot-swe-agent in #20193
  • (node-integration-tests) Fix flaky kafkajs test race condition by copilot-swe-agent in #20189

Internal Changes 🔧

Deps

  • Bump hono from 4.12.7 to 4.12.12 in /dev-packages/e2e-tests/test-applications/cloudflare-hono by dependabot in #20119
  • Bump axios from 1.13.5 to 1.15.0 in /dev-packages/e2e-tests/test-applications/nestjs-basic by dependabot in #20179

Other

  • (bugbot) Add rules to flag test-flake-provoking patterns by Lms24 in #20192
  • (deps-dev) Bump vite from 7.2.0 to 7.3.2 in /dev-packages/e2e-tests/test-applications/tanstackstart-react by dependabot in #20107
  • (react) Remove duplicated test mock by s1gr1d in #20200
  • (size-limit) Bump failing size limit scenario by Lms24 in #20186
  • Add automatic flaky test detector by nicohrubec in #18684

🤖 This preview updates automatically when you update the PR.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Breaking public API change without deprecation notice
    • I restored backward compatibility by reintroducing the deprecated legacy patchExpressModule(options) overload and ExpressIntegrationOptions.express field with a deprecation warning, and added a regression test for the old call pattern.

Create PR

Or push these changes by commenting:

@cursor push eebe164287
Preview (eebe164287)
diff --git a/packages/core/src/integrations/express/index.ts b/packages/core/src/integrations/express/index.ts
--- a/packages/core/src/integrations/express/index.ts
+++ b/packages/core/src/integrations/express/index.ts
@@ -60,6 +60,12 @@
 const getExpressExport = (express: ExpressModuleExport): ExpressExport =>
   hasDefaultProp(express) ? express.default : (express as ExpressExport);
 
+type LegacyPatchExpressModuleOptions = ExpressIntegrationOptions & {
+  express: ExpressModuleExport;
+};
+
+let _didWarnLegacyPatchExpressModule = false;
+
 /**
  * This is a portable instrumentatiton function that works in any environment
  * where Express can be loaded, without depending on OpenTelemetry.
@@ -72,7 +78,46 @@
  * Sentry.patchExpressModule(express, () => ({}));
  * ```
  */
-export const patchExpressModule = (moduleExports: ExpressModuleExport, getOptions: () => ExpressIntegrationOptions) => {
+export function patchExpressModule(
+  moduleExports: ExpressModuleExport,
+  getOptions: () => ExpressIntegrationOptions,
+): ExpressExport;
+/**
+ * @deprecated Pass the Express module export as the first argument and options getter as the second argument.
+ */
+export function patchExpressModule(options: LegacyPatchExpressModuleOptions): ExpressExport;
+export function patchExpressModule(
+  moduleExportsOrOptions: ExpressModuleExport | LegacyPatchExpressModuleOptions,
+  getOptions?: () => ExpressIntegrationOptions,
+): ExpressExport {
+  const isLegacyOptionsObject =
+    !getOptions &&
+    typeof moduleExportsOrOptions === 'object' &&
+    moduleExportsOrOptions !== null &&
+    'express' in moduleExportsOrOptions;
+
+  let moduleExports: ExpressModuleExport;
+  let getExpressOptions: () => ExpressIntegrationOptions;
+
+  if (isLegacyOptionsObject) {
+    moduleExports = moduleExportsOrOptions.express;
+    getExpressOptions = () => moduleExportsOrOptions;
+  } else {
+    moduleExports = moduleExportsOrOptions;
+    if (!getOptions) {
+      throw new TypeError('`patchExpressModule(moduleExports, getOptions)` requires a `getOptions` callback');
+    }
+    getExpressOptions = getOptions;
+  }
+
+  if (isLegacyOptionsObject && !_didWarnLegacyPatchExpressModule) {
+    _didWarnLegacyPatchExpressModule = true;
+    DEBUG_BUILD &&
+      debug.warn(
+        '[Express] `patchExpressModule(options)` is deprecated. Use `patchExpressModule(moduleExports, getOptions)` instead.',
+      );
+  }
+
   // pass in the require() or import() result of express
   const express = getExpressExport(moduleExports);
   const routerProto: ExpressRouterv4 | ExpressRouterv5 | undefined = isExpressWithRouterPrototype(express)
@@ -94,7 +139,7 @@
       function routeTrace(this: ExpressRouter, ...args: Parameters<typeof originalRouteMethod>[]) {
         const route = originalRouteMethod.apply(this, args);
         const layer = this.stack[this.stack.length - 1] as ExpressLayer;
-        patchLayer(getOptions, layer, getLayerPath(args));
+        patchLayer(getExpressOptions, layer, getLayerPath(args));
         return route;
       },
     );
@@ -114,7 +159,7 @@
         if (!layer) {
           return route;
         }
-        patchLayer(getOptions, layer, getLayerPath(args));
+        patchLayer(getExpressOptions, layer, getLayerPath(args));
         return route;
       },
     );
@@ -142,7 +187,7 @@
         if (router) {
           const layer = router.stack[router.stack.length - 1];
           if (layer) {
-            patchLayer(getOptions, layer, getLayerPath(args));
+            patchLayer(getExpressOptions, layer, getLayerPath(args));
           }
         }
         return route;
@@ -153,7 +198,7 @@
   }
 
   return express;
-};
+}
 
 /**
  * An Express-compatible error handler, used by setupExpressErrorHandler

diff --git a/packages/core/src/integrations/express/types.ts b/packages/core/src/integrations/express/types.ts
--- a/packages/core/src/integrations/express/types.ts
+++ b/packages/core/src/integrations/express/types.ts
@@ -136,6 +136,10 @@
 export type IgnoreMatcher = string | RegExp | ((name: string) => boolean);
 
 export type ExpressIntegrationOptions = {
+  /**
+   * @deprecated Pass the Express module export as the first argument to `patchExpressModule` instead.
+   */
+  express?: ExpressModuleExport;
   /** Ignore specific based on their name */
   ignoreLayers?: IgnoreMatcher[];
   /** Ignore specific layers based on their type */

diff --git a/packages/core/test/lib/integrations/express/index.test.ts b/packages/core/test/lib/integrations/express/index.test.ts
--- a/packages/core/test/lib/integrations/express/index.test.ts
+++ b/packages/core/test/lib/integrations/express/index.test.ts
@@ -52,15 +52,22 @@
   DEBUG_BUILD: true,
 }));
 const debugErrors: [string, Error][] = [];
+const debugWarnings: string[] = [];
 vi.mock('../../../../src/utils/debug-logger', () => ({
   debug: {
     error: (msg: string, er: Error) => {
       debugErrors.push([msg, er]);
     },
+    warn: (msg: string) => {
+      debugWarnings.push(msg);
+    },
   },
 }));
 
-beforeEach(() => (patchLayerCalls.length = 0));
+beforeEach(() => {
+  patchLayerCalls.length = 0;
+  debugWarnings.length = 0;
+});
 const patchLayerCalls: [getOptions: () => ExpressIntegrationOptions, layer: ExpressLayer, layerPath?: string][] = [];
 
 vi.mock('../../../../src/integrations/express/patch-layer', () => ({
@@ -129,6 +136,18 @@
 }
 
 describe('patchExpressModule', () => {
+  it('supports deprecated options signature', () => {
+    const expressv4 = getExpress4();
+    const options = { express: expressv4, ignoreLayers: [/foo/] };
+    patchExpressModule(options);
+    expressv4.Router.use('a');
+
+    expect(patchLayerCalls[0]?.[0]()).toStrictEqual(options);
+    expect(debugWarnings).toStrictEqual([
+      '[Express] `patchExpressModule(options)` is deprecated. Use `patchExpressModule(moduleExports, getOptions)` instead.',
+    ]);
+  });
+
   it('throws trying to patch/unpatch the wrong thing', () => {
     expect(() => {
       patchExpressModule({} as unknown as ExpressModuleExport, () => ({}));

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 10, 2026

size-limit report 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Path Size % Change Change
@sentry/browser 25.72 kB - -
@sentry/browser - with treeshaking flags 24.21 kB - -
@sentry/browser (incl. Tracing) 42.73 kB - -
@sentry/browser (incl. Tracing, Profiling) 47.35 kB - -
@sentry/browser (incl. Tracing, Replay) 81.54 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 71.11 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 86.25 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 98.45 kB - -
@sentry/browser (incl. Feedback) 42.51 kB - -
@sentry/browser (incl. sendFeedback) 30.39 kB - -
@sentry/browser (incl. FeedbackAsync) 35.38 kB - -
@sentry/browser (incl. Metrics) 27.04 kB - -
@sentry/browser (incl. Logs) 27.18 kB - -
@sentry/browser (incl. Metrics & Logs) 27.86 kB - -
@sentry/react 27.48 kB - -
@sentry/react (incl. Tracing) 45.05 kB - -
@sentry/vue 30.56 kB - -
@sentry/vue (incl. Tracing) 44.59 kB - -
@sentry/svelte 25.74 kB - -
CDN Bundle 28.41 kB - -
CDN Bundle (incl. Tracing) 43.75 kB - -
CDN Bundle (incl. Logs, Metrics) 29.78 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 44.83 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 68.59 kB - -
CDN Bundle (incl. Tracing, Replay) 80.64 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 81.66 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 86.17 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 87.2 kB - -
CDN Bundle - uncompressed 82.99 kB - -
CDN Bundle (incl. Tracing) - uncompressed 129.77 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 87.14 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 133.19 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 210.12 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 246.65 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 250.05 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 259.56 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 262.95 kB - -
@sentry/nextjs (client) 47.47 kB - -
@sentry/sveltekit (client) 43.2 kB - -
@sentry/node-core 57.86 kB +0.02% +6 B 🔺
@sentry/node 174.99 kB +0.09% +144 B 🔺
@sentry/node - without tracing 97.97 kB +0.03% +21 B 🔺
@sentry/aws-serverless 115.22 kB +0.02% +18 B 🔺

View base workflow run

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 10, 2026

node-overhead report 🧳

Note: This is a synthetic benchmark with a minimal express app and does not necessarily reflect the real-world performance impact in an application.

Scenario Requests/s % of Baseline Prev. Requests/s Change %
GET Baseline 8,990 - 9,286 -3%
GET With Sentry 1,711 19% 1,793 -5%
GET With Sentry (error only) 6,080 68% 6,241 -3%
POST Baseline 1,198 - 1,215 -1%
POST With Sentry 615 51% 609 +1%
POST With Sentry (error only) 1,058 88% 1,072 -1%
MYSQL Baseline 3,287 - 3,348 -2%
MYSQL With Sentry 517 16% 517 -
MYSQL With Sentry (error only) 2,696 82% 2,739 -2%

View base workflow run

@isaacs
Copy link
Copy Markdown
Member Author

isaacs commented Apr 10, 2026

Cursor makes a good point, it is technically breaking, even though it's an internal and undocumented method. I can add a bit to handle that.

@isaacs isaacs force-pushed the isaacs/express-late-init branch from 7c8de61 to 0be7618 Compare April 10, 2026 17:43
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0be7618. Configure here.

} else {
getOptions = maybeGetOptions;
moduleExports = optionsOrExports as ExpressModuleExport;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Public API function signature changed for exported function

Low Severity

Flagging per the review rules file: patchExpressModule is publicly exported from @sentry/core and its function signature changed from a single-argument form to a two-argument overloaded form. Additionally, the publicly exported ExpressIntegrationOptions type changed the express field from required to optional. While backward compatibility is maintained via a deprecated overload and runtime deprecation warning, consumers who stored options in a variable typed as ExpressIntegrationOptions and passed it to patchExpressModule will encounter a TypeScript error, since the deprecated overload requires ExpressIntegrationOptions & { express: ExpressModuleExport } which the now-optional express field no longer satisfies.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 0be7618. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant