Skip to content

Commit 6c2f752

Browse files
committed
WIP express example
1 parent a10178e commit 6c2f752

File tree

4 files changed

+49
-51
lines changed

4 files changed

+49
-51
lines changed

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ const getExpressExport = (express: ExpressModuleExport): ExpressExport =>
7171
*
7272
* Sentry.patchExpressModule({ express })
7373
*/
74-
export const patchExpressModule = (options: ExpressIntegrationOptions) => {
74+
export const patchExpressModule = (moduleExports: ExpressModuleExport, getOptions: () => ExpressIntegrationOptions) => {
7575
// pass in the require() or import() result of express
76-
const express = getExpressExport(options.express);
76+
const express = getExpressExport(moduleExports);
7777
const routerProto: ExpressRouterv4 | ExpressRouterv5 | undefined = isExpressWithRouterPrototype(express)
7878
? express.Router.prototype // Express v5
7979
: isExpressWithoutRouterPrototype(express)
@@ -93,7 +93,7 @@ export const patchExpressModule = (options: ExpressIntegrationOptions) => {
9393
function routeTrace(this: ExpressRouter, ...args: Parameters<typeof originalRouteMethod>[]) {
9494
const route = originalRouteMethod.apply(this, args);
9595
const layer = this.stack[this.stack.length - 1] as ExpressLayer;
96-
patchLayer(options, layer, getLayerPath(args));
96+
patchLayer(getOptions, layer, getLayerPath(args));
9797
return route;
9898
},
9999
);
@@ -113,7 +113,7 @@ export const patchExpressModule = (options: ExpressIntegrationOptions) => {
113113
if (!layer) {
114114
return route;
115115
}
116-
patchLayer(options, layer, getLayerPath(args));
116+
patchLayer(getOptions, layer, getLayerPath(args));
117117
return route;
118118
},
119119
);
@@ -141,7 +141,7 @@ export const patchExpressModule = (options: ExpressIntegrationOptions) => {
141141
if (router) {
142142
const layer = router.stack[router.stack.length - 1];
143143
if (layer) {
144-
patchLayer(options, layer, getLayerPath(args));
144+
patchLayer(getOptions, layer, getLayerPath(args));
145145
}
146146
}
147147
return route;

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ 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(getOptions: () => ExpressPatchLayerOptions, maybeLayer?: ExpressLayer, layerPath?: string): void {
6565
if (!maybeLayer?.handle) {
6666
return;
6767
}
@@ -86,6 +86,8 @@ export function patchLayer(options: ExpressPatchLayerOptions, maybeLayer?: Expre
8686
//oxlint-disable-next-line no-explicit-any
8787
...otherArgs: any[]
8888
) {
89+
const options = getOptions();
90+
8991
// Set normalizedRequest here because expressRequestHandler middleware
9092
// (registered via setupExpressErrorHandler) is added after routes and
9193
// 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 */
Lines changed: 41 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
1-
// Automatic istrumentation for Express using OTel
2-
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
3-
import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
41
import { context } from '@opentelemetry/api';
52
import { getRPCMetadata, RPCType } from '@opentelemetry/core';
63

7-
import { ensureIsWrapped, generateInstrumentOnce } from '@sentry/node-core';
4+
import { ensureIsWrapped, registerModuleWrapper } from '@sentry/node-core';
85
import {
96
type ExpressIntegrationOptions,
107
type IntegrationFn,
118
debug,
129
patchExpressModule,
13-
SDK_VERSION,
1410
defineIntegration,
1511
setupExpressErrorHandler as coreSetupExpressErrorHandler,
1612
type ExpressHandlerOptions,
1713
} from '@sentry/core';
1814
export { expressErrorHandler } from '@sentry/core';
1915
import { DEBUG_BUILD } from '../../debug-build';
2016

17+
type ExpressModuleExport = Parameters<typeof patchExpressModule>[0];
18+
2119
const INTEGRATION_NAME = 'Express';
2220
const SUPPORTED_VERSIONS = ['>=4.0.0 <6'];
2321

@@ -30,47 +28,46 @@ export function setupExpressErrorHandler(
3028
ensureIsWrapped(app.use, 'express');
3129
}
3230

33-
export type ExpressInstrumentationConfig = InstrumentationConfig &
34-
Omit<ExpressIntegrationOptions, 'express' | 'onRouteResolved'>;
35-
36-
export const instrumentExpress = generateInstrumentOnce(
37-
INTEGRATION_NAME,
38-
(options?: ExpressInstrumentationConfig) => new ExpressInstrumentation(options),
39-
);
31+
export type ExpressInstrumentationConfig = Omit<ExpressIntegrationOptions, 'onRouteResolved'>;
4032

41-
export class ExpressInstrumentation extends InstrumentationBase<ExpressInstrumentationConfig> {
42-
public constructor(config: ExpressInstrumentationConfig = {}) {
43-
super('sentry-express', SDK_VERSION, config);
44-
}
45-
public init(): InstrumentationNodeModuleDefinition {
46-
const module = new InstrumentationNodeModuleDefinition(
47-
'express',
48-
SUPPORTED_VERSIONS,
49-
express => {
50-
try {
51-
patchExpressModule({
52-
...this.getConfig(),
53-
express,
54-
onRouteResolved(route) {
55-
const rpcMetadata = getRPCMetadata(context.active());
56-
if (route && rpcMetadata?.type === RPCType.HTTP) {
57-
rpcMetadata.route = route;
58-
}
59-
},
60-
});
61-
} catch (e) {
62-
DEBUG_BUILD && debug.error('Failed to patch express module:', e);
63-
}
64-
return express;
65-
},
66-
// we do not ever actually unpatch in our SDKs
67-
express => express,
68-
);
69-
return module;
70-
}
33+
/**
34+
* Instrument Express using registerModuleWrapper.
35+
* This registers hooks for both CJS and ESM module loading.
36+
*
37+
* Calling this multiple times is safe:
38+
* - Hooks are only registered once (first call)
39+
* - Options are updated on each call
40+
* - Use getOptions() in the patch to access current options at runtime
41+
*/
42+
export function instrumentExpress(options: ExpressInstrumentationConfig = {}): void {
43+
registerModuleWrapper<ExpressInstrumentationConfig>({
44+
moduleName: 'express',
45+
supportedVersions: SUPPORTED_VERSIONS,
46+
options,
47+
patch: (moduleExports, getOptions) => {
48+
const express = moduleExports as ExpressModuleExport;
49+
try {
50+
patchExpressModule(express, () => ({
51+
...getOptions(),
52+
onRouteResolved(route) {
53+
const rpcMetadata = getRPCMetadata(context.active());
54+
if (route && rpcMetadata?.type === RPCType.HTTP) {
55+
rpcMetadata.route = route;
56+
}
57+
},
58+
}));
59+
} catch (e) {
60+
DEBUG_BUILD && debug.error('Failed to patch express module:', e);
61+
}
62+
return moduleExports;
63+
},
64+
});
7165
}
7266

73-
const _expressInstrumentation = ((options?: ExpressInstrumentationConfig) => {
67+
// Add id property for compatibility with preloadOpenTelemetry logging
68+
instrumentExpress.id = INTEGRATION_NAME;
69+
70+
const _expressIntegration = ((options?: ExpressInstrumentationConfig) => {
7471
return {
7572
name: INTEGRATION_NAME,
7673
setupOnce() {
@@ -79,4 +76,4 @@ const _expressInstrumentation = ((options?: ExpressInstrumentationConfig) => {
7976
};
8077
}) satisfies IntegrationFn;
8178

82-
export const expressIntegration = defineIntegration(_expressInstrumentation);
79+
export const expressIntegration = defineIntegration(_expressIntegration);

0 commit comments

Comments
 (0)