-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathsentry-nest-event-instrumentation.ts
More file actions
133 lines (120 loc) · 5.25 KB
/
sentry-nest-event-instrumentation.ts
File metadata and controls
133 lines (120 loc) · 5.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
import {
InstrumentationBase,
InstrumentationNodeModuleDefinition,
InstrumentationNodeModuleFile,
isWrapped,
} from '@opentelemetry/instrumentation';
import { captureException, SDK_VERSION, startSpan } from '@sentry/core';
import { getEventSpanOptions } from './helpers';
import type { OnEventTarget } from './types';
const supportedVersions = ['>=2.0.0'];
const COMPONENT = '@nestjs/event-emitter';
/**
* Custom instrumentation for nestjs event-emitter
*
* This hooks into the `OnEvent` decorator, which is applied on event handlers.
*/
export class SentryNestEventInstrumentation extends InstrumentationBase {
public constructor(config: InstrumentationConfig = {}) {
super('sentry-nestjs-event', SDK_VERSION, config);
}
/**
* Initializes the instrumentation by defining the modules to be patched.
*/
public init(): InstrumentationNodeModuleDefinition {
const moduleDef = new InstrumentationNodeModuleDefinition(COMPONENT, supportedVersions);
moduleDef.files.push(this._getOnEventFileInstrumentation(supportedVersions));
return moduleDef;
}
/**
* Wraps the @OnEvent decorator.
*/
private _getOnEventFileInstrumentation(versions: string[]): InstrumentationNodeModuleFile {
return new InstrumentationNodeModuleFile(
'@nestjs/event-emitter/dist/decorators/on-event.decorator.js',
versions,
(moduleExports: { OnEvent: OnEventTarget }) => {
if (isWrapped(moduleExports.OnEvent)) {
this._unwrap(moduleExports, 'OnEvent');
}
this._wrap(moduleExports, 'OnEvent', this._createWrapOnEvent());
return moduleExports;
},
(moduleExports: { OnEvent: OnEventTarget }) => {
this._unwrap(moduleExports, 'OnEvent');
},
);
}
/**
* Creates a wrapper function for the @OnEvent decorator.
*/
private _createWrapOnEvent() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return function wrapOnEvent(original: any) {
return function wrappedOnEvent(event: unknown, options?: unknown) {
// Get the original decorator result
const decoratorResult = original(event, options);
// Return a new decorator function that wraps the handler
return (target: OnEventTarget, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
if (
!descriptor.value ||
typeof descriptor.value !== 'function' ||
target.__SENTRY_INTERNAL__ ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
descriptor.value.__SENTRY_INSTRUMENTED__
) {
return decoratorResult(target, propertyKey, descriptor);
}
const originalHandler = descriptor.value;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const handlerName = originalHandler.name || propertyKey;
let eventName = typeof event === 'string' ? event : String(event);
// Instrument the actual handler
descriptor.value = async function (...args: unknown[]) {
// When multiple @OnEvent decorators are used on a single method, we need to get all event names
// from the reflector metadata as there is no information during execution which event triggered it
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - reflect-metadata of nestjs adds these methods to Reflect
if (Reflect.getMetadataKeys(descriptor.value).includes('EVENT_LISTENER_METADATA')) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - reflect-metadata of nestjs adds these methods to Reflect
const eventData = Reflect.getMetadata('EVENT_LISTENER_METADATA', descriptor.value);
if (Array.isArray(eventData)) {
eventName = eventData
.map((data: unknown) => {
if (data && typeof data === 'object' && 'event' in data && data.event) {
return data.event;
}
return '';
})
.reverse() // decorators are evaluated bottom to top
.join('|');
}
}
return startSpan(getEventSpanOptions(eventName), async () => {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const result = await originalHandler.apply(this, args);
return result;
} catch (error) {
// exceptions from event handlers are not caught by global error filter
captureException(error);
throw error;
}
});
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
descriptor.value.__SENTRY_INSTRUMENTED__ = true;
// Preserve the original function name
Object.defineProperty(descriptor.value, 'name', {
value: handlerName,
configurable: true,
});
// Apply the original decorator
return decoratorResult(target, propertyKey, descriptor);
};
};
};
}
}