Skip to content

Commit 7c8de61

Browse files
committed
fix(core): avoid breaking change in express integration
1 parent 29b7472 commit 7c8de61

File tree

2 files changed

+71
-9
lines changed

2 files changed

+71
-9
lines changed

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

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,24 @@ import { setSDKProcessingMetadata } from './set-sdk-processing-metadata';
6060
const getExpressExport = (express: ExpressModuleExport): ExpressExport =>
6161
hasDefaultProp(express) ? express.default : (express as ExpressExport);
6262

63+
function isLegacyOptions(
64+
options: ExpressModuleExport | (ExpressIntegrationOptions & { express: ExpressModuleExport }),
65+
): options is ExpressIntegrationOptions & { express: ExpressModuleExport } {
66+
return !!(options as { express: ExpressModuleExport }).express;
67+
}
68+
69+
// TODO: remove this deprecation handling in v11
70+
let didLegacyDeprecationWarning = false;
71+
function deprecationWarning() {
72+
if (!didLegacyDeprecationWarning) {
73+
didLegacyDeprecationWarning = true;
74+
DEBUG_BUILD &&
75+
debug.warn(
76+
'[Express] `patchExpressModule(options)` is deprecated. Use `patchExpressModule(moduleExports, getOptions)` instead.',
77+
);
78+
}
79+
}
80+
6381
/**
6482
* This is a portable instrumentatiton function that works in any environment
6583
* where Express can be loaded, without depending on OpenTelemetry.
@@ -72,7 +90,34 @@ const getExpressExport = (express: ExpressModuleExport): ExpressExport =>
7290
* Sentry.patchExpressModule(express, () => ({}));
7391
* ```
7492
*/
75-
export const patchExpressModule = (moduleExports: ExpressModuleExport, getOptions: () => ExpressIntegrationOptions) => {
93+
export function patchExpressModule(
94+
moduleExports: ExpressModuleExport,
95+
getOptions: () => ExpressIntegrationOptions,
96+
): ExpressModuleExport;
97+
/**
98+
* @deprecated Pass the Express module export as the first argument and options getter as the second argument.
99+
*/
100+
export function patchExpressModule(
101+
options: ExpressIntegrationOptions & { express: ExpressModuleExport },
102+
): ExpressModuleExport;
103+
export function patchExpressModule(
104+
optionsOrExports: ExpressModuleExport | (ExpressIntegrationOptions & { express: ExpressModuleExport }),
105+
maybeGetOptions?: () => ExpressIntegrationOptions,
106+
): ExpressModuleExport {
107+
let getOptions: () => ExpressIntegrationOptions;
108+
let moduleExports: ExpressModuleExport;
109+
if (!maybeGetOptions && isLegacyOptions(optionsOrExports)) {
110+
const { express, ...options } = optionsOrExports;
111+
moduleExports = express;
112+
getOptions = () => options;
113+
deprecationWarning();
114+
} else if (typeof maybeGetOptions !== 'function') {
115+
throw new TypeError('`patchExpressModule(moduleExports, getOptions)` requires a `getOptions` callback');
116+
} else {
117+
getOptions = maybeGetOptions;
118+
moduleExports = optionsOrExports as ExpressModuleExport;
119+
}
120+
76121
// pass in the require() or import() result of express
77122
const express = getExpressExport(moduleExports);
78123
const routerProto: ExpressRouterv4 | ExpressRouterv5 | undefined = isExpressWithRouterPrototype(express)
@@ -153,7 +198,7 @@ export const patchExpressModule = (moduleExports: ExpressModuleExport, getOption
153198
}
154199

155200
return express;
156-
};
201+
}
157202

158203
/**
159204
* An Express-compatible error handler, used by setupExpressErrorHandler

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

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ vi.mock('../../../../src/debug-build', () => ({
5252
DEBUG_BUILD: true,
5353
}));
5454
const debugErrors: [string, Error][] = [];
55+
const debugWarnings: string[] = [];
5556
vi.mock('../../../../src/utils/debug-logger', () => ({
5657
debug: {
58+
warn: (msg: string) => debugWarnings.push(msg),
5759
error: (msg: string, er: Error) => {
5860
debugErrors.push([msg, er]);
5961
},
@@ -129,43 +131,58 @@ function getExpress5(): ExpressExportv5 & { spies: ExpressSpies } {
129131
}
130132

131133
describe('patchExpressModule', () => {
132-
it('throws trying to patch/unpatch the wrong thing', () => {
134+
it('throws trying to patch the wrong thing', () => {
133135
expect(() => {
134136
patchExpressModule({} as unknown as ExpressModuleExport, () => ({}));
135137
}).toThrowError('no valid Express route function to instrument');
136138
});
137139

138-
it('can patch and restore expressv4 style module', () => {
140+
it('throws trying to patch without a getOptions getter', () => {
141+
const express = getExpress4();
142+
expect(() => {
143+
//@ts-expect-error The type error prevents this, by design
144+
patchExpressModule(express);
145+
}).toThrowError('`patchExpressModule(moduleExports, getOptions)` requires a `getOptions` callback');
146+
});
147+
148+
it('can patch expressv4 style module', () => {
139149
for (const useDefault of [false, true]) {
140150
const express = getExpress4();
141151
const moduleExports = useDefault ? { default: express } : express;
142152
const r = express.Router as ExpressRouterv4;
143153
const a = express.application;
144-
const getOptions = () => ({});
145154
expect((r.use as WrappedFunction).__sentry_original__).toBe(undefined);
146155
expect((r.route as WrappedFunction).__sentry_original__).toBe(undefined);
147156
expect((a.use as WrappedFunction).__sentry_original__).toBe(undefined);
148157

149-
patchExpressModule(moduleExports, getOptions);
158+
patchExpressModule({ express: moduleExports });
159+
expect(debugWarnings).toStrictEqual([
160+
'[Express] `patchExpressModule(options)` is deprecated. Use `patchExpressModule(moduleExports, getOptions)` instead.',
161+
]);
150162

151163
expect(typeof (r.use as WrappedFunction).__sentry_original__).toBe('function');
152164
expect(typeof (r.route as WrappedFunction).__sentry_original__).toBe('function');
153165
expect(typeof (a.use as WrappedFunction).__sentry_original__).toBe('function');
154166
}
155167
});
156168

157-
it('can patch and restore expressv5 style module', () => {
169+
it('can patch expressv5 style module', () => {
158170
for (const useDefault of [false, true]) {
159171
const express = getExpress5();
160172
const r = express.Router as ExpressRouterv5;
161173
const a = express.application;
162174
const moduleExports = useDefault ? { default: express } : express;
163-
const getOptions = () => ({});
164175
expect((r.prototype.use as WrappedFunction).__sentry_original__).toBe(undefined);
165176
expect((r.prototype.route as WrappedFunction).__sentry_original__).toBe(undefined);
166177
expect((a.use as WrappedFunction).__sentry_original__).toBe(undefined);
167178

168-
patchExpressModule(moduleExports, getOptions);
179+
// verify that the debug warning doesn't fire a second time
180+
// vitest doesn't guarantee test ordering, so just verify
181+
// in both places that there's only one warning.
182+
patchExpressModule({ express: moduleExports });
183+
expect(debugWarnings).toStrictEqual([
184+
'[Express] `patchExpressModule(options)` is deprecated. Use `patchExpressModule(moduleExports, getOptions)` instead.',
185+
]);
169186

170187
expect(typeof (r.prototype.use as WrappedFunction).__sentry_original__).toBe('function');
171188
expect(typeof (r.prototype.route as WrappedFunction).__sentry_original__).toBe('function');

0 commit comments

Comments
 (0)