Skip to content

Nest-js instrumentation breaks @nestjs/event-emitter when using multiple @OnEvent on the same method #15218

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

Open
3 tasks done
matthieuMay opened this issue Jan 29, 2025 · 6 comments
Assignees

Comments

@matthieuMay
Copy link

Is there an existing issue for this?

How do you use Sentry?

Sentry Saas (sentry.io)

Which SDK are you using?

@sentry/nestjs

SDK Version

8.52.0

Framework Version

Nestjs 10.4.7

Link to Sentry event

No response

Reproduction Example/SDK Setup

I set up a minimal repro here :
https://github.com/matthieuMay/sentry_nest_event_repro

This is a minimal nestjs + nestjs/event-emitter + sentry configuration.

It contains:

  • instrument.ts : sentry initialization
  • app.module.ts: main nestjs module
  • app.controller.ts: main nestjs module
  • app.service.ts: service setting a event listener on two events and emitting the two events when application is bootstrapped

Steps to Reproduce

  1. npm install
  2. set environnement SENTRY_DSN variable
  3. run npm run start:dev
  4. see that event handler is called only once
Image
  1. comment the import of instrument.js or unset SENTRY_DSN variable
  2. run npm run start:dev
  3. see that event handler is called twice as expected
Image

Expected Result

We should see the handler being called twice

Explanation of the bug

Nestjs initialize in two steps : first the decorators set metadata on the decorated functions and once all decorators have been read, this metadata is traducted in method modifications.

Sentry instrumentation overrides the OnEvent decorator in SentryNestEventInstrumentation._createWrapOnEvent method. This new decorator ovverides descriptor.value on every call.
Therefore, when decorating the same handler with multiple @onevent, descriptor.value changes on every call changing the target of the metadata on every call. As a consequence, only the last called decorator is actually applied

Actual Result

Image
@getsantry getsantry bot moved this to Waiting for: Product Owner in GitHub Issues with 👀 3 Jan 29, 2025
@github-actions github-actions bot added the Package: nestjs Issues related to the Sentry Nestjs SDK label Jan 29, 2025
@mydea
Copy link
Member

mydea commented Jan 29, 2025

Hey, thanks for writing in. We will look into it!

@MustagheesButt
Copy link

MustagheesButt commented Mar 11, 2025

Facing same issue. Using a custom decorator for multiple OnEvent like:

import { OnEvent } from '@nestjs/event-emitter';

export const OnEvents = (events: string[]) =>
  applyDecorators(...events.map((e) => OnEvent(e)));

When used like @OnEvents(['eventA', 'eventB']), only the last works when instrument.js is imported.

Thanks @matthieuMay , I could've never guessed sentry sdk was causing it. How do you even debug something like this

@getsantry getsantry bot moved this to Waiting for: Product Owner in GitHub Issues with 👀 3 Mar 11, 2025
@Lms24
Copy link
Member

Lms24 commented Mar 12, 2025

Hi, thanks for reminding us of this issue. cc @chargome I see you're already assigned but maybe you could take another look at this?

Heass-up: Most team members are out for the rest of the week so might only get to it next week.

If anyone has ideas how to fix this, PRs are also welcome :)

@matthieuMay
Copy link
Author

Hi all

I came up with a patch package but it is not perfect as all spans are merged into one span.
Not a good enough fix to be released but it may give some ideas to fix this

diff --git a/node_modules/@sentry/node/build/cjs/integrations/tracing/nest/sentry-nest-event-instrumentation.js b/node_modules/@sentry/node/build/cjs/integrations/tracing/nest/sentry-nest-event-instrumentation.js
index 0a0bb50..f673733 100644
--- a/node_modules/@sentry/node/build/cjs/integrations/tracing/nest/sentry-nest-event-instrumentation.js
+++ b/node_modules/@sentry/node/build/cjs/integrations/tracing/nest/sentry-nest-event-instrumentation.js
@@ -64,7 +64,7 @@ class SentryNestEventInstrumentation extends instrumentation.InstrumentationBase
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       return function wrappedOnEvent(event, options) {
         const eventName = Array.isArray(event)
-          ? event.join(',')
+          ? event.map(e => e.toString()).join(',')
           : typeof event === 'string' || typeof event === 'symbol'
             ? event.toString()
             : '<unknown_event>';
@@ -83,21 +83,31 @@ class SentryNestEventInstrumentation extends instrumentation.InstrumentationBase
           // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
           const handlerName = originalHandler.name || propertyKey;
 
-          // Instrument the handler
-          // eslint-disable-next-line @typescript-eslint/no-explicit-any
-          descriptor.value = async function (...args) {
-            return core.startSpan(helpers.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
-                core.captureException(error);
-                throw error;
-              }
-            });
-          };
+          const isOnEventInstrumented= Reflect.getMetadata('SentryNestEventInstrumentation.isOnEventInstrumented', descriptor.value) || false;
+
+          if (!isOnEventInstrumented) {
+            // Instrument the handler
+            // eslint-disable-next-line @typescript-eslint/no-explicit-any
+            descriptor.value = async function (...args) {
+              return core.startSpan(helpers.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
+                  core.captureException(error);
+                  throw error;
+                }
+              });
+            };
+          }
+          else {
+            console.error(
+              'Cannot apply multiple nestjs OnEvent decorators on the same function when using sentry instrumentation. All spans will be merged into one span'
+            );
+          }
+
 
           // Preserve the original function name
           Object.defineProperty(descriptor.value, 'name', {
@@ -105,6 +115,9 @@ class SentryNestEventInstrumentation extends instrumentation.InstrumentationBase
             configurable: true,
           });
           
+          Reflect.defineMetadata('SentryNestEventInstrumentation.isOnEventInstrumented', true, descriptor.value);
+        
+
           // Apply the original decorator
           return decoratorResult(target, propertyKey, descriptor);
         };
diff --git a/node_modules/@sentry/node/build/esm/integrations/tracing/nest/sentry-nest-event-instrumentation.js b/node_modules/@sentry/node/build/esm/integrations/tracing/nest/sentry-nest-event-instrumentation.js
index 5d95019..358932a 100644
--- a/node_modules/@sentry/node/build/esm/integrations/tracing/nest/sentry-nest-event-instrumentation.js
+++ b/node_modules/@sentry/node/build/esm/integrations/tracing/nest/sentry-nest-event-instrumentation.js
@@ -62,7 +62,7 @@ class SentryNestEventInstrumentation extends InstrumentationBase {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       return function wrappedOnEvent(event, options) {
         const eventName = Array.isArray(event)
-          ? event.join(',')
+          ? event.map(e => e.toString()).join(',')
           : typeof event === 'string' || typeof event === 'symbol'
             ? event.toString()
             : '<unknown_event>';
@@ -81,21 +81,30 @@ class SentryNestEventInstrumentation extends InstrumentationBase {
           // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
           const handlerName = originalHandler.name || propertyKey;
 
-          // Instrument the handler
-          // eslint-disable-next-line @typescript-eslint/no-explicit-any
-          descriptor.value = async function (...args) {
-            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;
-              }
-            });
-          };
+          const isOnEventInstrumented= Reflect.getMetadata('SentryNestEventInstrumentation.isOnEventInstrumented', descriptor.value) || false;
+
+          if (!isOnEventInstrumented) {
+            // Instrument the handler
+            // eslint-disable-next-line @typescript-eslint/no-explicit-any
+            descriptor.value = async function (...args) {
+              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;
+                }
+              });
+            };
+          }
+          else {
+            console.error(
+              'Cannot apply multiple nestjs OnEvent decorators on the same function when using sentry instrumentation. All spans will be merged into one span'
+            );
+          }
 
           // Preserve the original function name
           Object.defineProperty(descriptor.value, 'name', {
@@ -103,6 +112,9 @@ class SentryNestEventInstrumentation extends InstrumentationBase {
             configurable: true,
           });
 
+          Reflect.defineMetadata('SentryNestEventInstrumentation.isOnEventInstrumented', true, descriptor.value);
+
+        
           // Apply the original decorator
           return decoratorResult(target, propertyKey, descriptor);
         };

@getsantry getsantry bot removed the status in GitHub Issues with 👀 3 May 7, 2025
@NiceChildHQ
Copy link

Facing same issue ~

@getsantry getsantry bot moved this to Waiting for: Product Owner in GitHub Issues with 👀 3 May 13, 2025
@chargome
Copy link
Member

@NiceChildHQ thanks for reporting, sorry this fell under the radar – contributions are welcome if anyone wants a go at this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: No status
Development

No branches or pull requests

7 participants