Skip to content

fix(nestjs): Handle multiple OnEvent decorators #16306

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,11 @@ export class EventsController {

return { message: 'Events emitted' };
}

@Get('emit-multiple')
async emitMultipleEvents() {
await this.eventsService.emitMultipleEvents();

return { message: 'Events emitted' };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,11 @@ export class EventsService {

return { message: 'Events emitted' };
}

async emitMultipleEvents() {
this.eventEmitter.emit('multiple.first', { data: 'test-first' });
this.eventEmitter.emit('multiple.second', { data: 'test-second' });

return { message: 'Events emitted' };
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import * as Sentry from '@sentry/nestjs';

@Injectable()
export class TestEventListener {
Expand All @@ -13,4 +14,11 @@ export class TestEventListener {
await new Promise(resolve => setTimeout(resolve, 100));
throw new Error('Test error from event handler');
}

@OnEvent('multiple.first')
@OnEvent('multiple.second')
async handleMultipleEvents(payload: any): Promise<void> {
Sentry.setTag(payload.data, true);
await new Promise(resolve => setTimeout(resolve, 100));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,27 @@ test('Event emitter', async () => {
status: 'ok',
});
});

test('Multiple OnEvent decorators', async () => {
const firstTxPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => {
return transactionEvent.transaction === 'event multiple.first|multiple.second';
});
const secondTxPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => {
return transactionEvent.transaction === 'event multiple.first|multiple.second';
});
const rootPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => {
return transactionEvent.transaction === 'GET /events/emit-multiple';
});

const eventsUrl = `http://localhost:3050/events/emit-multiple`;
await fetch(eventsUrl);

const firstTx = await firstTxPromise;
const secondTx = await secondTxPromise;
const rootTx = await rootPromise;

expect(firstTx).toBeDefined();
expect(secondTx).toBeDefined();
// assert that the correct payloads were added
expect(rootTx.tags).toMatchObject({ 'test-first': true, 'test-second': true });
});
Original file line number Diff line number Diff line change
Expand Up @@ -58,31 +58,46 @@ export class SentryNestEventInstrumentation extends InstrumentationBase {
private _createWrapOnEvent() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return function wrapOnEvent(original: any) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return function wrappedOnEvent(event: any, options?: any) {
const eventName = Array.isArray(event)
? event.join(',')
: typeof event === 'string' || typeof event === 'symbol'
? event.toString()
: '<unknown_event>';

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 function (target: OnEventTarget, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
if (!descriptor.value || typeof descriptor.value !== 'function' || target.__SENTRY_INTERNAL__) {
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);
}

// Get the original handler
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
if (Reflect.getMetadataKeys(descriptor.value).includes('EVENT_LISTENER_METADATA')) {
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('|');
}
}

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

// 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,
Expand Down
1 change: 1 addition & 0 deletions packages/nestjs/test/integrations/nest.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'reflect-metadata';
import * as core from '@sentry/core';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { isPatched } from '../../src/integrations/helpers';
Expand Down
Loading