@@ -30,8 +30,11 @@ export type ExpressPatchLayerOptions = Pick<
3030 'onRouteResolved' | 'ignoreLayers' | 'ignoreLayersType'
3131> ;
3232
33- export function patchLayer ( options : ExpressPatchLayerOptions , layer ?: ExpressLayer , layerPath ?: string ) : void {
34- if ( ! layer ) return ;
33+ export function patchLayer ( options : ExpressPatchLayerOptions , maybeLayer ?: ExpressLayer , layerPath ?: string ) : void {
34+ if ( ! maybeLayer ) {
35+ return ;
36+ }
37+ const layer = maybeLayer ;
3538
3639 // avoid patching multiple times the same layer
3740 if ( layer [ kLayerPatched ] === true ) return ;
@@ -43,146 +46,165 @@ export function patchLayer(options: ExpressPatchLayerOptions, layer?: ExpressLay
4346 return ;
4447 }
4548
46- Object . defineProperty ( layer , 'handle' , {
47- enumerable : true ,
48- configurable : true ,
49- value : function layerHandlePatched (
50- this : ExpressLayer ,
51- req : ExpressRequest ,
52- res : ExpressResponse ,
53- //oxlint-disable-next-line no-explicit-any
54- ...otherArgs : any [ ]
55- ) {
56- // Only create spans when there's an active parent span
57- // Without a parent span, this request is being ignored, so skip it
58- const parentSpan = getActiveSpan ( ) ;
59- if ( ! parentSpan ) {
60- return originalHandle . apply ( this , [ req , res , ...otherArgs ] ) ;
49+ function layerHandlePatched (
50+ this : ExpressLayer ,
51+ req : ExpressRequest ,
52+ res : ExpressResponse ,
53+ //oxlint-disable-next-line no-explicit-any
54+ ...otherArgs : any [ ]
55+ ) {
56+ // Only create spans when there's an active parent span
57+ // Without a parent span, this request is being ignored, so skip it
58+ const parentSpan = getActiveSpan ( ) ;
59+ if ( ! parentSpan ) {
60+ return originalHandle . apply ( this , [ req , res , ...otherArgs ] ) ;
61+ }
62+
63+ if ( layerPath ) storeLayer ( req , layerPath ) ;
64+ const storedLayers = getStoredLayers ( req ) ;
65+ const isLayerPathStored = ! ! layerPath ;
66+
67+ const constructedRoute = getConstructedRoute ( req ) ;
68+ const actualMatchedRoute = getActualMatchedRoute ( req , constructedRoute ) ;
69+
70+ options . onRouteResolved ?.( actualMatchedRoute ) ;
71+
72+ const metadata = getLayerMetadata ( constructedRoute , layer , layerPath ) ;
73+ const name = metadata . attributes [ ATTR_EXPRESS_NAME ] as string ?? metadata . name ;
74+ const type = metadata . attributes [ ATTR_EXPRESS_TYPE ] as ExpressLayerType ;
75+ const attributes : SpanAttributes = Object . assign ( metadata . attributes , {
76+ [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : 'auto.http.express' ,
77+ [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : `${ type } .express` ,
78+ } ) ;
79+ if ( actualMatchedRoute ) {
80+ attributes [ ATTR_HTTP_ROUTE ] = actualMatchedRoute ;
81+ }
82+
83+ // verify against the config if the layer should be ignored
84+ if ( isLayerIgnored ( metadata . name , type , options ) ) {
85+ // XXX: the isLayerPathStored guard here is *not* present in the
86+ // original @opentelemetry /instrumentation-express impl, but was
87+ // suggested by the Sentry code review bot. It appears to correctly
88+ // prevent improper layer calculation in the case where there's a
89+ // middleware without a layerPath argument. It's unclear whether
90+ // that's possible, or if any existing code depends on that "bug".
91+ if ( isLayerPathStored && type === ExpressLayerType_MIDDLEWARE ) {
92+ storedLayers . pop ( ) ;
6193 }
62-
63- if ( layerPath ) storeLayer ( req , layerPath ) ;
64- const storedLayers = getStoredLayers ( req ) ;
65- const isLayerPathStored = ! ! layerPath ;
66-
67- const constructedRoute = getConstructedRoute ( req ) ;
68- const actualMatchedRoute = getActualMatchedRoute ( req , constructedRoute ) ;
69-
70- options . onRouteResolved ?.( actualMatchedRoute ) ;
71-
72- const metadata = getLayerMetadata ( constructedRoute , layer , layerPath ) ;
73- const type = metadata . attributes [ ATTR_EXPRESS_TYPE ] as ExpressLayerType ;
74- const attributes : SpanAttributes = Object . assign ( metadata . attributes , {
75- [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : 'auto.http.express' ,
76- [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : `${ type } .express` ,
77- } ) ;
78- if ( actualMatchedRoute ) {
79- attributes [ ATTR_HTTP_ROUTE ] = actualMatchedRoute ;
80- }
81-
82- // verify against the config if the layer should be ignored
83- if ( isLayerIgnored ( metadata . name , type , options ) ) {
84- // XXX: the isLayerPathStored guard here is *not* present in the
85- // original @opentelemetry /instrumentation-express impl, but was
86- // suggested by the Sentry code review bot. It appears to correctly
87- // prevent improper layer calculation in the case where there's a
88- // middleware without a layerPath argument. It's unclear whether
89- // that's possible, or if any existing code depends on that "bug".
90- if ( isLayerPathStored && type === ExpressLayerType_MIDDLEWARE ) {
91- storedLayers . pop ( ) ;
92- }
93- return originalHandle . apply ( this , [ req , res , ...otherArgs ] ) ;
94+ return originalHandle . apply ( this , [ req , res , ...otherArgs ] ) ;
95+ }
96+
97+ const spanName = getSpanName (
98+ {
99+ request : req ,
100+ layerType : type ,
101+ route : constructedRoute ,
102+ } ,
103+ name ,
104+ ) ;
105+ return startSpanManual ( { name : spanName , attributes } , span => {
106+ let spanHasEnded = false ;
107+ // TODO: Fix router spans (getRouterPath does not work properly) to
108+ // have useful names before removing this branch
109+ if ( metadata . attributes [ ATTR_EXPRESS_TYPE ] === ExpressLayerType_ROUTER ) {
110+ span . end ( ) ;
111+ spanHasEnded = true ;
94112 }
95-
96- const spanName = getSpanName (
97- {
98- request : req ,
99- layerType : type ,
100- route : constructedRoute ,
101- } ,
102- metadata . name ,
103- ) ;
104- return startSpanManual ( { name : spanName , attributes } , span => {
105- // Also update the name, we don't need to "middleware - " prefix
106- const name = attributes [ ATTR_EXPRESS_NAME ] ;
107- if ( typeof name === 'string' ) {
108- // should this be updateSpanName?
109- span . updateName ( name ) ;
110- }
111-
112- let spanHasEnded = false ;
113- // TODO: Fix router spans (getRouterPath does not work properly) to
114- // have useful names before removing this branch
115- if ( metadata . attributes [ ATTR_EXPRESS_TYPE ] === ExpressLayerType_ROUTER ) {
116- span . end ( ) ;
113+ // listener for response.on('finish')
114+ const onResponseFinish = ( ) => {
115+ if ( spanHasEnded === false ) {
117116 spanHasEnded = true ;
117+ span . end ( ) ;
118118 }
119- // listener for response.on('finish')
120- const onResponseFinish = ( ) => {
119+ } ;
120+
121+ // verify we have a callback
122+ for ( let i = 0 ; i < otherArgs . length ; i ++ ) {
123+ const callback = otherArgs [ i ] as Function ;
124+ if ( typeof callback !== 'function' ) continue ;
125+
126+ //oxlint-disable-next-line no-explicit-any
127+ otherArgs [ i ] = function ( ...args : any [ ] ) {
128+ // express considers anything but an empty value, "route" or "router"
129+ // passed to its callback to be an error
130+ const maybeError = args [ 0 ] ;
131+ const isError = ! [ undefined , null , 'route' , 'router' ] . includes ( maybeError ) ;
132+ if ( ! spanHasEnded && isError ) {
133+ const [ _ , message ] = asErrorAndMessage ( maybeError ) ;
134+ // intentionally do not record the exception here, because
135+ // the error handler we assign does that
136+ span . setStatus ( {
137+ code : SPAN_STATUS_ERROR ,
138+ message,
139+ } ) ;
140+ }
141+
121142 if ( spanHasEnded === false ) {
122143 spanHasEnded = true ;
144+ res . removeListener ( 'finish' , onResponseFinish ) ;
123145 span . end ( ) ;
124146 }
147+ if ( ! ( req . route && isError ) && isLayerPathStored ) {
148+ storedLayers . pop ( ) ;
149+ }
150+ // execute the callback back in the parent's scope, so that
151+ // we bubble up each level as next() is called.
152+ return withActiveSpan ( parentSpan , ( ) => callback . apply ( this , args ) ) ;
125153 } ;
154+ break ;
155+ }
126156
127- // verify we have a callback
128- for ( let i = 0 ; i < otherArgs . length ; i ++ ) {
129- const callback = otherArgs [ i ] as Function ;
130- if ( typeof callback !== 'function' ) continue ;
131-
132- //oxlint-disable-next-line no-explicit-any
133- otherArgs [ i ] = function ( ...args : any [ ] ) {
134- // express considers anything but an empty value, "route" or "router"
135- // passed to its callback to be an error
136- const maybeError = args [ 0 ] ;
137- const isError = ! [ undefined , null , 'route' , 'router' ] . includes ( maybeError ) ;
138- if ( ! spanHasEnded && isError ) {
139- const [ _ , message ] = asErrorAndMessage ( maybeError ) ;
140- // intentionally do not record the exception here, because
141- // the error handler we assign does that
142- span . setStatus ( {
143- code : SPAN_STATUS_ERROR ,
144- message,
145- } ) ;
146- }
147-
148- if ( spanHasEnded === false ) {
149- spanHasEnded = true ;
150- res . removeListener ( 'finish' , onResponseFinish ) ;
151- span . end ( ) ;
152- }
153- if ( ! ( req . route && isError ) && isLayerPathStored ) {
154- storedLayers . pop ( ) ;
155- }
156- // execute the callback back in the parent's scope, so that
157- // we bubble up each level as next() is called.
158- return withActiveSpan ( parentSpan , ( ) => callback . apply ( this , args ) ) ;
159- } ;
160- break ;
157+ try {
158+ return originalHandle . apply ( this , [ req , res , ...otherArgs ] ) ;
159+ } catch ( anyError ) {
160+ const [ _ , message ] = asErrorAndMessage ( anyError ) ;
161+ // intentionally do not record the exception here, because
162+ // the error handler we assign does that
163+ span . setStatus ( {
164+ code : SPAN_STATUS_ERROR ,
165+ message,
166+ } ) ;
167+ throw anyError ;
168+ /* v8 ignore next - it sees the block end at the throw */
169+ } finally {
170+ // At this point if the callback wasn't called, that means
171+ // either the layer is asynchronous (so it will call the
172+ // callback later on) or that the layer directly ends the
173+ // http response, so we'll hook into the "finish" event to
174+ // handle the later case.
175+ if ( ! spanHasEnded ) {
176+ res . once ( 'finish' , onResponseFinish ) ;
161177 }
178+ }
179+ } ) ;
180+ }
162181
163- try {
164- return originalHandle . apply ( this , [ req , res , ...otherArgs ] ) ;
165- } catch ( anyError ) {
166- const [ _ , message ] = asErrorAndMessage ( anyError ) ;
167- // intentionally do not record the exception here, because
168- // the error handler we assign does that
169- span . setStatus ( {
170- code : SPAN_STATUS_ERROR ,
171- message,
172- } ) ;
173- throw anyError ;
174- /* v8 ignore next - it sees the block end at the throw */
175- } finally {
176- // At this point if the callback wasn't called, that means
177- // either the layer is asynchronous (so it will call the
178- // callback later on) or that the layer directly ends the
179- // http response, so we'll hook into the "finish" event to
180- // handle the later case.
181- if ( ! spanHasEnded ) {
182- res . once ( 'finish' , onResponseFinish ) ;
183- }
184- }
185- } ) ;
186- } ,
182+ // `handle` isn't just a regular function in some cases. It also contains
183+ // some properties holding metadata and state so we need to proxy them
184+ // through through patched function. Use a for-in to also pick up properties
185+ // that other libraries might add to the prototype before we instrument.
186+ // ref: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/1950
187+ // ref: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/2271
188+ // oxlint-disable-next-line guard-for-in
189+ for ( const key in originalHandle as Function & Record < string , unknown > ) {
190+ // skip standard function prototype fields that both have
191+ if ( key in layerHandlePatched ) {
192+ continue ;
193+ }
194+ Object . defineProperty ( layerHandlePatched , key , {
195+ get ( ) {
196+ return originalHandle [ key ] ;
197+ } ,
198+ set ( value ) {
199+ originalHandle [ key ] = value ;
200+ } ,
201+ } ) ;
202+ }
203+
204+ Object . defineProperty ( layer , 'handle' , {
205+ enumerable : true ,
206+ configurable : true ,
207+ writable : true ,
208+ value : layerHandlePatched ,
187209 } ) ;
188210}
0 commit comments