@@ -7,40 +7,103 @@ import {
77} from '@sentry/core' ;
88import type { CatchTarget , InjectableTarget , NextFunction , Observable , Subscription } from './types' ;
99
10- const sentryPatched = 'sentryPatched' ;
10+ /** A function of unknown signature, matching the methods/handlers we wrap. */
11+ export type AnyFn = ( this : unknown , ...args : unknown [ ] ) => unknown ;
1112
1213/**
13- * Helper checking if a concrete target class is already patched.
14- *
15- * We already guard duplicate patching with isWrapped. However, isWrapped checks whether a file has been patched, whereas we use this check for concrete target classes.
16- * This check might not be necessary, but better to play it safe.
14+ * Marks a function as already wrapped so repeated subscriptions/decoration
15+ * don't double-wrap it.
1716 */
18- export function isPatched ( target : InjectableTarget | CatchTarget ) : boolean {
19- if ( target . sentryPatched ) {
20- return true ;
17+ const SENTRY_WRAPPED = Symbol . for ( 'sentry.nestjs.wrapped' ) ;
18+
19+ /** Whether `fn` has already been wrapped by this integration. */
20+ export function isWrapped ( fn : AnyFn ) : boolean {
21+ return ! ! ( fn as AnyFn & Record < symbol , unknown > ) [ SENTRY_WRAPPED ] ;
22+ }
23+
24+ /** Mark `fn` as wrapped (see {@link isWrapped}). */
25+ export function markWrapped ( fn : AnyFn ) : void {
26+ ( fn as AnyFn & Record < symbol , unknown > ) [ SENTRY_WRAPPED ] = true ;
27+ }
28+
29+ /**
30+ * The subset of `reflect-metadata`'s `Reflect` augmentation that NestJS
31+ * relies on. Methods are optional because `reflect-metadata` may not be
32+ * loaded; guard each before use.
33+ */
34+ export interface ReflectWithMetadata {
35+ getMetadataKeys ?: ( target : object ) => unknown [ ] ;
36+ getMetadata ?: ( key : unknown , target : object ) => unknown ;
37+ defineMetadata ?: ( key : unknown , value : unknown , target : object ) => void ;
38+ }
39+
40+ /**
41+ * Copy NestJS reflect-metadata from one object onto another so decorators
42+ * (param decorators, guards, `@EventPattern`, ...) that read it keep working
43+ * No-op when `reflect-metadata` isn't loaded.
44+ */
45+ export function copyReflectMetadata ( from : object , to : object ) : void {
46+ const R = Reflect as unknown as ReflectWithMetadata ;
47+ if (
48+ typeof R . getMetadataKeys !== 'function' ||
49+ typeof R . getMetadata !== 'function' ||
50+ typeof R . defineMetadata !== 'function'
51+ ) {
52+ return ;
2153 }
54+ for ( const key of R . getMetadataKeys ( from ) ) {
55+ R . defineMetadata ( key , R . getMetadata ( key , from ) , to ) ;
56+ }
57+ }
2258
23- addNonEnumerableProperty ( target , sentryPatched , true ) ;
59+ /**
60+ * Mark a target class as patched (for the given pass) so it's instrumented
61+ * only once, and to stay idempotent across repeated subscriptions/decoration.
62+ */
63+ export function isTargetPatched ( target : object , flag : 'sentryPatchedInjectable' | 'sentryPatchedCatch' ) : boolean {
64+ if ( ( target as Record < string , unknown > ) [ flag ] ) {
65+ return true ;
66+ }
67+ addNonEnumerableProperty ( target , flag , true ) ;
2468 return false ;
2569}
2670
71+ /** Origin for middleware/guard/pipe/interceptor/exception_filter spans. */
72+ function middlewareOrigin ( componentType ?: string ) : string {
73+ return componentType ? `auto.middleware.nestjs.${ componentType } ` : 'auto.middleware.nestjs' ;
74+ }
75+
76+ /**
77+ * Origin for the app-creation / request-context / request-handler HTTP spans.
78+ */
79+ export function httpOrigin ( ) : string {
80+ return 'auto.http.otel.nestjs' ;
81+ }
82+
83+ /** Origin for `@OnEvent` spans. */
84+ function eventOrigin ( ) : string {
85+ return 'auto.event.nestjs' ;
86+ }
87+
88+ /** Origin for BullMQ `@Processor` `process` spans. */
89+ function bullmqOrigin ( ) : string {
90+ return 'auto.queue.nestjs.bullmq' ;
91+ }
92+
2793/**
2894 * Returns span options for nest middleware spans.
95+ * name = provided name or class name.
2996 */
30- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
3197export function getMiddlewareSpanOptions (
32- target : InjectableTarget | CatchTarget ,
98+ target : InjectableTarget | CatchTarget | { name ?: string } ,
3399 name : string | undefined = undefined ,
34100 componentType : string | undefined = undefined ,
35- ) {
36- const span_name = name ?? target . name ; // fallback to class name if no name is provided
37- const origin = componentType ? `auto.middleware.nestjs.${ componentType } ` : 'auto.middleware.nestjs' ;
38-
101+ ) : { name : string ; attributes : Record < string , string > } {
39102 return {
40- name : span_name ,
103+ name : name ?? target . name ?? 'unknown' ,
41104 attributes : {
42105 [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : 'middleware.nestjs' ,
43- [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : origin ,
106+ [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : middlewareOrigin ( componentType ) ,
44107 } ,
45108 } ;
46109}
@@ -57,7 +120,7 @@ export function getEventSpanOptions(event: string): {
57120 name : `event ${ event } ` ,
58121 attributes : {
59122 [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : 'event.nestjs' ,
60- [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : 'auto.event.nestjs' ,
123+ [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : eventOrigin ( ) ,
61124 } ,
62125 forceTransaction : true ,
63126 } ;
@@ -75,7 +138,7 @@ export function getBullMQProcessSpanOptions(queueName: string): {
75138 name : `${ queueName } process` ,
76139 attributes : {
77140 [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : 'queue.process' ,
78- [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : 'auto.queue.nestjs.bullmq' ,
141+ [ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : bullmqOrigin ( ) ,
79142 'messaging.system' : 'bullmq' ,
80143 'messaging.destination.name' : queueName ,
81144 } ,
0 commit comments