Skip to content

Commit 29b7472

Browse files
committed
fix(core, node): support loading Express options lazily
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
1 parent d8dd265 commit 29b7472

File tree

9 files changed

+147
-58
lines changed

9 files changed

+147
-58
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
// First: preload the express instrumentation without calling Sentry.init().
5+
// registers OTel module hook, patches the Express module with no config.
6+
Sentry.preloadOpenTelemetry({ integrations: ['Express'] });
7+
8+
// call Sentry.init() with express integration config.
9+
// instrumentExpress is already registered, so this calls setConfig() on the
10+
// existing instrumentation to update its options. The lazy getOptions()
11+
// in patchLayer ensures the updated options are read at request time.
12+
Sentry.init({
13+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
14+
release: '1.0',
15+
tracesSampleRate: 1.0,
16+
transport: loggingTransport,
17+
// suppress the middleware layer that the cors module generates
18+
integrations: [Sentry.expressIntegration({ ignoreLayersType: ['middleware'] })],
19+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import cors from 'cors';
2+
import express from 'express';
3+
import * as Sentry from '@sentry/node';
4+
import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';
5+
6+
const app = express();
7+
8+
// cors() would normally create a 'middleware' type span, but the
9+
// ignoreLayersType: ['middleware'] option set via Sentry.init() suppresses it.
10+
app.use(cors());
11+
12+
app.get('/test/express', (_req, res) => {
13+
res.send({ response: 'response 1' });
14+
});
15+
16+
Sentry.setupExpressErrorHandler(app);
17+
18+
startExpressServerAndSendPortToRunner(app);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { afterAll, describe, expect } from 'vitest';
2+
import { assertSentryTransaction } from '../../../utils/assertions';
3+
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
4+
5+
describe('express late init', () => {
6+
afterAll(() => {
7+
cleanupChildProcesses();
8+
});
9+
10+
createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
11+
test('applies expressIntegration config set via Sentry.init() called after instrumentExpress()', async () => {
12+
const runner = createRunner()
13+
.expect({
14+
transaction: transaction => {
15+
assertSentryTransaction(transaction, {
16+
transaction: 'GET /test/express',
17+
contexts: {
18+
trace: {
19+
op: 'http.server',
20+
status: 'ok',
21+
},
22+
},
23+
});
24+
// request_handler span IS present
25+
// confirms the express patch was applied.
26+
expect(transaction.spans).toContainEqual(
27+
expect.objectContaining({
28+
data: expect.objectContaining({
29+
'express.type': 'request_handler',
30+
}),
31+
}),
32+
);
33+
// Middleware spans NOT present, ignoreLayersType: ['middleware']
34+
// configured via the Sentry.init() AFTER instrumentExpress().
35+
expect(transaction.spans).not.toContainEqual(
36+
expect.objectContaining({
37+
data: expect.objectContaining({
38+
'express.type': 'middleware',
39+
}),
40+
}),
41+
);
42+
},
43+
})
44+
.start();
45+
runner.makeRequest('get', '/test/express');
46+
await runner.completed();
47+
});
48+
});
49+
});

packages/core/src/integrations/express/index.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,12 @@ const getExpressExport = (express: ExpressModuleExport): ExpressExport =>
6969
* import express from 'express';
7070
* import * as Sentry from '@sentry/deno'; // or any SDK that extends core
7171
*
72-
* Sentry.patchExpressModule({ express })
72+
* Sentry.patchExpressModule(express, () => ({}));
73+
* ```
7374
*/
74-
export const patchExpressModule = (options: ExpressIntegrationOptions) => {
75+
export const patchExpressModule = (moduleExports: ExpressModuleExport, getOptions: () => ExpressIntegrationOptions) => {
7576
// pass in the require() or import() result of express
76-
const express = getExpressExport(options.express);
77+
const express = getExpressExport(moduleExports);
7778
const routerProto: ExpressRouterv4 | ExpressRouterv5 | undefined = isExpressWithRouterPrototype(express)
7879
? express.Router.prototype // Express v5
7980
: isExpressWithoutRouterPrototype(express)
@@ -93,7 +94,7 @@ export const patchExpressModule = (options: ExpressIntegrationOptions) => {
9394
function routeTrace(this: ExpressRouter, ...args: Parameters<typeof originalRouteMethod>[]) {
9495
const route = originalRouteMethod.apply(this, args);
9596
const layer = this.stack[this.stack.length - 1] as ExpressLayer;
96-
patchLayer(options, layer, getLayerPath(args));
97+
patchLayer(getOptions, layer, getLayerPath(args));
9798
return route;
9899
},
99100
);
@@ -113,7 +114,7 @@ export const patchExpressModule = (options: ExpressIntegrationOptions) => {
113114
if (!layer) {
114115
return route;
115116
}
116-
patchLayer(options, layer, getLayerPath(args));
117+
patchLayer(getOptions, layer, getLayerPath(args));
117118
return route;
118119
},
119120
);
@@ -141,7 +142,7 @@ export const patchExpressModule = (options: ExpressIntegrationOptions) => {
141142
if (router) {
142143
const layer = router.stack[router.stack.length - 1];
143144
if (layer) {
144-
patchLayer(options, layer, getLayerPath(args));
145+
patchLayer(getOptions, layer, getLayerPath(args));
145146
}
146147
}
147148
return route;

packages/core/src/integrations/express/patch-layer.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ export type ExpressPatchLayerOptions = Pick<
6161
'onRouteResolved' | 'ignoreLayers' | 'ignoreLayersType'
6262
>;
6363

64-
export function patchLayer(options: ExpressPatchLayerOptions, maybeLayer?: ExpressLayer, layerPath?: string): void {
64+
export function patchLayer(
65+
getOptions: () => ExpressPatchLayerOptions,
66+
maybeLayer?: ExpressLayer,
67+
layerPath?: string,
68+
): void {
6569
if (!maybeLayer?.handle) {
6670
return;
6771
}
@@ -86,6 +90,8 @@ export function patchLayer(options: ExpressPatchLayerOptions, maybeLayer?: Expre
8690
//oxlint-disable-next-line no-explicit-any
8791
...otherArgs: any[]
8892
) {
93+
const options = getOptions();
94+
8995
// Set normalizedRequest here because expressRequestHandler middleware
9096
// (registered via setupExpressErrorHandler) is added after routes and
9197
// therefore never runs for successful requests — route handlers typically

packages/core/src/integrations/express/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,6 @@ export type ExpressRouter = {
136136
export type IgnoreMatcher = string | RegExp | ((name: string) => boolean);
137137

138138
export type ExpressIntegrationOptions = {
139-
express: ExpressModuleExport; //Express
140139
/** Ignore specific based on their name */
141140
ignoreLayers?: IgnoreMatcher[];
142141
/** Ignore specific layers based on their type */

packages/core/test/lib/integrations/express/index.test.ts

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,12 @@ vi.mock('../../../../src/utils/debug-logger', () => ({
6161
}));
6262

6363
beforeEach(() => (patchLayerCalls.length = 0));
64-
const patchLayerCalls: [options: ExpressIntegrationOptions, layer: ExpressLayer, layerPath?: string][] = [];
64+
const patchLayerCalls: [getOptions: () => ExpressIntegrationOptions, layer: ExpressLayer, layerPath?: string][] = [];
6565

6666
vi.mock('../../../../src/integrations/express/patch-layer', () => ({
67-
patchLayer: (options: ExpressIntegrationOptions, layer?: ExpressLayer, layerPath?: string) => {
67+
patchLayer: (getOptions: () => ExpressIntegrationOptions, layer?: ExpressLayer, layerPath?: string) => {
6868
if (layer) {
69-
patchLayerCalls.push([options, layer, layerPath]);
69+
patchLayerCalls.push([getOptions, layer, layerPath]);
7070
}
7171
},
7272
}));
@@ -131,24 +131,22 @@ function getExpress5(): ExpressExportv5 & { spies: ExpressSpies } {
131131
describe('patchExpressModule', () => {
132132
it('throws trying to patch/unpatch the wrong thing', () => {
133133
expect(() => {
134-
patchExpressModule({
135-
express: {} as unknown as ExpressModuleExport,
136-
} as unknown as ExpressIntegrationOptions);
134+
patchExpressModule({} as unknown as ExpressModuleExport, () => ({}));
137135
}).toThrowError('no valid Express route function to instrument');
138136
});
139137

140138
it('can patch and restore expressv4 style module', () => {
141139
for (const useDefault of [false, true]) {
142140
const express = getExpress4();
143-
const module = useDefault ? { default: express } : express;
141+
const moduleExports = useDefault ? { default: express } : express;
144142
const r = express.Router as ExpressRouterv4;
145143
const a = express.application;
146-
const options = { express: module } as unknown as ExpressIntegrationOptions;
144+
const getOptions = () => ({});
147145
expect((r.use as WrappedFunction).__sentry_original__).toBe(undefined);
148146
expect((r.route as WrappedFunction).__sentry_original__).toBe(undefined);
149147
expect((a.use as WrappedFunction).__sentry_original__).toBe(undefined);
150148

151-
patchExpressModule(options);
149+
patchExpressModule(moduleExports, getOptions);
152150

153151
expect(typeof (r.use as WrappedFunction).__sentry_original__).toBe('function');
154152
expect(typeof (r.route as WrappedFunction).__sentry_original__).toBe('function');
@@ -161,13 +159,13 @@ describe('patchExpressModule', () => {
161159
const express = getExpress5();
162160
const r = express.Router as ExpressRouterv5;
163161
const a = express.application;
164-
const module = useDefault ? { default: express } : express;
165-
const options = { express: module } as unknown as ExpressIntegrationOptions;
162+
const moduleExports = useDefault ? { default: express } : express;
163+
const getOptions = () => ({});
166164
expect((r.prototype.use as WrappedFunction).__sentry_original__).toBe(undefined);
167165
expect((r.prototype.route as WrappedFunction).__sentry_original__).toBe(undefined);
168166
expect((a.use as WrappedFunction).__sentry_original__).toBe(undefined);
169167

170-
patchExpressModule(options);
168+
patchExpressModule(moduleExports, getOptions);
171169

172170
expect(typeof (r.prototype.use as WrappedFunction).__sentry_original__).toBe('function');
173171
expect(typeof (r.prototype.route as WrappedFunction).__sentry_original__).toBe('function');
@@ -178,27 +176,27 @@ describe('patchExpressModule', () => {
178176
it('calls patched and original Router.route', () => {
179177
const expressv4 = getExpress4();
180178
const { spies } = expressv4;
181-
const options = { express: expressv4 };
182-
patchExpressModule(options);
179+
const getOptions = () => ({});
180+
patchExpressModule(expressv4, getOptions);
183181
expressv4.Router.route('a');
184182
expect(spies.routerRoute).toHaveBeenCalledExactlyOnceWith('a');
185183
});
186184

187185
it('calls patched and original Router.use', () => {
188186
const expressv4 = getExpress4();
189187
const { spies } = expressv4;
190-
const options = { express: expressv4 };
191-
patchExpressModule(options);
188+
const getOptions = () => ({});
189+
patchExpressModule(expressv4, getOptions);
192190
expressv4.Router.use('a');
193-
expect(patchLayerCalls).toStrictEqual([[options, { name: 'layerFinal' }, 'a']]);
191+
expect(patchLayerCalls).toStrictEqual([[getOptions, { name: 'layerFinal' }, 'a']]);
194192
expect(spies.routerUse).toHaveBeenCalledExactlyOnceWith('a');
195193
});
196194

197195
it('skips patchLayer call in Router.use if no layer in the stack', () => {
198196
const expressv4 = getExpress4();
199197
const { spies } = expressv4;
200-
const options = { express: expressv4 };
201-
patchExpressModule(options);
198+
const getOptions = () => ({});
199+
patchExpressModule(expressv4, getOptions);
202200
const { stack } = expressv4.Router;
203201
expressv4.Router.stack = [];
204202
expressv4.Router.use('a');
@@ -210,28 +208,28 @@ describe('patchExpressModule', () => {
210208
it('calls patched and original application.use', () => {
211209
const expressv4 = getExpress4();
212210
const { spies } = expressv4;
213-
const options = { express: expressv4 };
214-
patchExpressModule(options);
211+
const getOptions = () => ({});
212+
patchExpressModule(expressv4, getOptions);
215213
expressv4.application.use('a');
216-
expect(patchLayerCalls).toStrictEqual([[options, { name: 'layerFinal' }, 'a']]);
214+
expect(patchLayerCalls).toStrictEqual([[getOptions, { name: 'layerFinal' }, 'a']]);
217215
expect(spies.appUse).toHaveBeenCalledExactlyOnceWith('a');
218216
});
219217

220218
it('calls patched and original application.use on express v5', () => {
221219
const expressv5 = getExpress5();
222220
const { spies } = expressv5;
223-
const options = { express: expressv5 };
224-
patchExpressModule(options);
221+
const getOptions = () => ({});
222+
patchExpressModule(expressv5, getOptions);
225223
expressv5.application.use('a');
226-
expect(patchLayerCalls).toStrictEqual([[options, { name: 'layerFinal' }, 'a']]);
224+
expect(patchLayerCalls).toStrictEqual([[getOptions, { name: 'layerFinal' }, 'a']]);
227225
expect(spies.appUse).toHaveBeenCalledExactlyOnceWith('a');
228226
});
229227

230228
it('skips patchLayer on application.use if no router found', () => {
231229
const expressv4 = getExpress4();
232230
const { spies } = expressv4;
233-
const options = { express: expressv4 };
234-
patchExpressModule(options);
231+
const getOptions = () => ({});
232+
patchExpressModule(expressv4, getOptions);
235233
const app = expressv4.application as {
236234
_router?: ExpressRoute;
237235
};
@@ -246,8 +244,9 @@ describe('patchExpressModule', () => {
246244

247245
it('debug error when patching fails', () => {
248246
const expressv5 = getExpress5();
249-
patchExpressModule({ express: expressv5 });
250-
patchExpressModule({ express: expressv5 });
247+
const getOptions = () => ({});
248+
patchExpressModule(expressv5, getOptions);
249+
patchExpressModule(expressv5, getOptions);
251250
expect(debugErrors).toStrictEqual([
252251
['Failed to patch express route method:', new Error('Attempting to wrap method route multiple times')],
253252
['Failed to patch express use method:', new Error('Attempting to wrap method use multiple times')],

0 commit comments

Comments
 (0)