1- // Automatic istrumentation for Express using OTel
2- import type { InstrumentationConfig } from '@opentelemetry/instrumentation' ;
3- import { InstrumentationBase , InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation' ;
41import { context } from '@opentelemetry/api' ;
52import { getRPCMetadata , RPCType } from '@opentelemetry/core' ;
63
7- import { ensureIsWrapped , generateInstrumentOnce } from '@sentry/node-core' ;
4+ import { ensureIsWrapped , registerModuleWrapper } from '@sentry/node-core' ;
85import {
96 type ExpressIntegrationOptions ,
7+ type ExpressModuleExport ,
108 type IntegrationFn ,
119 debug ,
1210 patchExpressModule ,
13- SDK_VERSION ,
1411 defineIntegration ,
1512 setupExpressErrorHandler as coreSetupExpressErrorHandler ,
1613 type ExpressHandlerOptions ,
@@ -19,6 +16,7 @@ export { expressErrorHandler } from '@sentry/core';
1916import { DEBUG_BUILD } from '../../debug-build' ;
2017
2118const INTEGRATION_NAME = 'Express' ;
19+ const MODULE_NAME = 'express' ;
2220const SUPPORTED_VERSIONS = [ '>=4.0.0 <6' ] ;
2321
2422export function setupExpressErrorHandler (
@@ -30,44 +28,44 @@ export function setupExpressErrorHandler(
3028 ensureIsWrapped ( app . use , 'express' ) ;
3129}
3230
33- export type ExpressInstrumentationConfig = InstrumentationConfig &
34- Omit < ExpressIntegrationOptions , 'express' | 'onRouteResolved' > ;
31+ export type ExpressInstrumentationConfig = Omit < ExpressIntegrationOptions , 'onRouteResolved' > ;
3532
36- export const instrumentExpress = generateInstrumentOnce (
37- INTEGRATION_NAME ,
38- ( options ?: ExpressInstrumentationConfig ) => new ExpressInstrumentation ( options ) ,
39- ) ;
40-
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 ( express , ( ) => ( {
52- ...this . getConfig ( ) ,
53- onRouteResolved ( route ) {
54- const rpcMetadata = getRPCMetadata ( context . active ( ) ) ;
55- if ( route && rpcMetadata ?. type === RPCType . HTTP ) {
56- rpcMetadata . route = route ;
57- }
58- } ,
59- } ) ) ;
60- } catch ( e ) {
61- DEBUG_BUILD && debug . error ( 'Failed to patch express module:' , e ) ;
62- }
63- return express ;
64- } ,
65- // we do not ever actually unpatch in our SDKs
66- express => express ,
67- ) ;
68- return module ;
69- }
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 < ExpressModuleExport , ExpressInstrumentationConfig > ( {
44+ moduleName : MODULE_NAME ,
45+ supportedVersions : SUPPORTED_VERSIONS ,
46+ options,
47+ patch : ( moduleExports , getOptions ) => {
48+ try {
49+ patchExpressModule ( moduleExports , ( ) => ( {
50+ ...getOptions ( ) ,
51+ onRouteResolved ( route ) {
52+ const rpcMetadata = getRPCMetadata ( context . active ( ) ) ;
53+ if ( route && rpcMetadata ?. type === RPCType . HTTP ) {
54+ rpcMetadata . route = route ;
55+ }
56+ } ,
57+ } ) ) ;
58+ } catch ( e ) {
59+ DEBUG_BUILD && debug . error ( 'Failed to patch express module:' , e ) ;
60+ }
61+ return moduleExports ;
62+ } ,
63+ } ) ;
7064}
65+
66+ // Add id property for compatibility with preloadOpenTelemetry logging
67+ instrumentExpress . id = INTEGRATION_NAME ;
68+
7169const _expressIntegration = ( ( options ?: ExpressInstrumentationConfig ) => {
7270 return {
7371 name : INTEGRATION_NAME ,
0 commit comments