Skip to content

Commit d575638

Browse files
authored
fix(nestjs): Handle multiple OnEvent decorators (#16306)
1 parent bad34c8 commit d575638

File tree

6 files changed

+79
-14
lines changed

6 files changed

+79
-14
lines changed

dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.controller.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,11 @@ export class EventsController {
1111

1212
return { message: 'Events emitted' };
1313
}
14+
15+
@Get('emit-multiple')
16+
async emitMultipleEvents() {
17+
await this.eventsService.emitMultipleEvents();
18+
19+
return { message: 'Events emitted' };
20+
}
1421
}

dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.service.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,11 @@ export class EventsService {
1111

1212
return { message: 'Events emitted' };
1313
}
14+
15+
async emitMultipleEvents() {
16+
this.eventEmitter.emit('multiple.first', { data: 'test-first' });
17+
this.eventEmitter.emit('multiple.second', { data: 'test-second' });
18+
19+
return { message: 'Events emitted' };
20+
}
1421
}

dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/listeners/test-event.listener.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Injectable } from '@nestjs/common';
22
import { OnEvent } from '@nestjs/event-emitter';
3+
import * as Sentry from '@sentry/nestjs';
34

45
@Injectable()
56
export class TestEventListener {
@@ -13,4 +14,11 @@ export class TestEventListener {
1314
await new Promise(resolve => setTimeout(resolve, 100));
1415
throw new Error('Test error from event handler');
1516
}
17+
18+
@OnEvent('multiple.first')
19+
@OnEvent('multiple.second')
20+
async handleMultipleEvents(payload: any): Promise<void> {
21+
Sentry.setTag(payload.data, true);
22+
await new Promise(resolve => setTimeout(resolve, 100));
23+
}
1624
}

dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/events.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,27 @@ test('Event emitter', async () => {
4040
status: 'ok',
4141
});
4242
});
43+
44+
test('Multiple OnEvent decorators', async () => {
45+
const firstTxPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => {
46+
return transactionEvent.transaction === 'event multiple.first|multiple.second';
47+
});
48+
const secondTxPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => {
49+
return transactionEvent.transaction === 'event multiple.first|multiple.second';
50+
});
51+
const rootPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => {
52+
return transactionEvent.transaction === 'GET /events/emit-multiple';
53+
});
54+
55+
const eventsUrl = `http://localhost:3050/events/emit-multiple`;
56+
await fetch(eventsUrl);
57+
58+
const firstTx = await firstTxPromise;
59+
const secondTx = await secondTxPromise;
60+
const rootTx = await rootPromise;
61+
62+
expect(firstTx).toBeDefined();
63+
expect(secondTx).toBeDefined();
64+
// assert that the correct payloads were added
65+
expect(rootTx.tags).toMatchObject({ 'test-first': true, 'test-second': true });
66+
});

packages/nestjs/src/integrations/sentry-nest-event-instrumentation.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,31 +58,46 @@ export class SentryNestEventInstrumentation extends InstrumentationBase {
5858
private _createWrapOnEvent() {
5959
// eslint-disable-next-line @typescript-eslint/no-explicit-any
6060
return function wrapOnEvent(original: any) {
61-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
62-
return function wrappedOnEvent(event: any, options?: any) {
63-
const eventName = Array.isArray(event)
64-
? event.join(',')
65-
: typeof event === 'string' || typeof event === 'symbol'
66-
? event.toString()
67-
: '<unknown_event>';
68-
61+
return function wrappedOnEvent(event: unknown, options?: unknown) {
6962
// Get the original decorator result
7063
const decoratorResult = original(event, options);
7164

7265
// Return a new decorator function that wraps the handler
73-
return function (target: OnEventTarget, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
74-
if (!descriptor.value || typeof descriptor.value !== 'function' || target.__SENTRY_INTERNAL__) {
66+
return (target: OnEventTarget, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
67+
if (
68+
!descriptor.value ||
69+
typeof descriptor.value !== 'function' ||
70+
target.__SENTRY_INTERNAL__ ||
71+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
72+
descriptor.value.__SENTRY_INSTRUMENTED__
73+
) {
7574
return decoratorResult(target, propertyKey, descriptor);
7675
}
7776

78-
// Get the original handler
7977
const originalHandler = descriptor.value;
8078
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
8179
const handlerName = originalHandler.name || propertyKey;
80+
let eventName = typeof event === 'string' ? event : String(event);
81+
82+
// Instrument the actual handler
83+
descriptor.value = async function (...args: unknown[]) {
84+
// When multiple @OnEvent decorators are used on a single method, we need to get all event names
85+
// from the reflector metadata as there is no information during execution which event triggered it
86+
if (Reflect.getMetadataKeys(descriptor.value).includes('EVENT_LISTENER_METADATA')) {
87+
const eventData = Reflect.getMetadata('EVENT_LISTENER_METADATA', descriptor.value);
88+
if (Array.isArray(eventData)) {
89+
eventName = eventData
90+
.map((data: unknown) => {
91+
if (data && typeof data === 'object' && 'event' in data && data.event) {
92+
return data.event;
93+
}
94+
return '';
95+
})
96+
.reverse() // decorators are evaluated bottom to top
97+
.join('|');
98+
}
99+
}
82100

83-
// Instrument the handler
84-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
85-
descriptor.value = async function (...args: any[]) {
86101
return startSpan(getEventSpanOptions(eventName), async () => {
87102
try {
88103
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
@@ -96,6 +111,9 @@ export class SentryNestEventInstrumentation extends InstrumentationBase {
96111
});
97112
};
98113

114+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
115+
descriptor.value.__SENTRY_INSTRUMENTED__ = true;
116+
99117
// Preserve the original function name
100118
Object.defineProperty(descriptor.value, 'name', {
101119
value: handlerName,

packages/nestjs/test/integrations/nest.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'reflect-metadata';
12
import * as core from '@sentry/core';
23
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
34
import { isPatched } from '../../src/integrations/helpers';

0 commit comments

Comments
 (0)