-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(nestjs): Instrument event handlers (#14307)
- Loading branch information
Showing
14 changed files
with
380 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.controller.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { Controller, Get } from '@nestjs/common'; | ||
import { EventsService } from './events.service'; | ||
|
||
@Controller('events') | ||
export class EventsController { | ||
constructor(private readonly eventsService: EventsService) {} | ||
|
||
@Get('emit') | ||
async emitEvents() { | ||
await this.eventsService.emitEvents(); | ||
|
||
return { message: 'Events emitted' }; | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { APP_FILTER } from '@nestjs/core'; | ||
import { EventEmitterModule } from '@nestjs/event-emitter'; | ||
import { SentryGlobalFilter, SentryModule } from '@sentry/nestjs/setup'; | ||
import { EventsController } from './events.controller'; | ||
import { EventsService } from './events.service'; | ||
import { TestEventListener } from './listeners/test-event.listener'; | ||
|
||
@Module({ | ||
imports: [SentryModule.forRoot(), EventEmitterModule.forRoot()], | ||
controllers: [EventsController], | ||
providers: [ | ||
{ | ||
provide: APP_FILTER, | ||
useClass: SentryGlobalFilter, | ||
}, | ||
EventsService, | ||
TestEventListener, | ||
], | ||
}) | ||
export class EventsModule {} |
14 changes: 14 additions & 0 deletions
14
dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/events.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { Injectable } from '@nestjs/common'; | ||
import { EventEmitter2 } from '@nestjs/event-emitter'; | ||
|
||
@Injectable() | ||
export class EventsService { | ||
constructor(private readonly eventEmitter: EventEmitter2) {} | ||
|
||
async emitEvents() { | ||
await this.eventEmitter.emit('myEvent.pass', { data: 'test' }); | ||
await this.eventEmitter.emit('myEvent.throw'); | ||
|
||
return { message: 'Events emitted' }; | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
...e-tests/test-applications/nestjs-distributed-tracing/src/listeners/test-event.listener.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { Injectable } from '@nestjs/common'; | ||
import { OnEvent } from '@nestjs/event-emitter'; | ||
|
||
@Injectable() | ||
export class TestEventListener { | ||
@OnEvent('myEvent.pass') | ||
async handlePassEvent(payload: any): Promise<void> { | ||
await new Promise(resolve => setTimeout(resolve, 100)); | ||
} | ||
|
||
@OnEvent('myEvent.throw') | ||
async handleThrowEvent(): Promise<void> { | ||
await new Promise(resolve => setTimeout(resolve, 100)); | ||
throw new Error('Test error from event handler'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 43 additions & 0 deletions
43
dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/events.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { expect, test } from '@playwright/test'; | ||
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; | ||
|
||
test('Event emitter', async () => { | ||
const eventErrorPromise = waitForError('nestjs-distributed-tracing', errorEvent => { | ||
return errorEvent.exception.values[0].value === 'Test error from event handler'; | ||
}); | ||
const successEventTransactionPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => { | ||
return transactionEvent.transaction === 'event myEvent.pass'; | ||
}); | ||
|
||
const eventsUrl = `http://localhost:3050/events/emit`; | ||
await fetch(eventsUrl); | ||
|
||
const eventError = await eventErrorPromise; | ||
const successEventTransaction = await successEventTransactionPromise; | ||
|
||
expect(eventError.exception).toEqual({ | ||
values: [ | ||
{ | ||
type: 'Error', | ||
value: 'Test error from event handler', | ||
stacktrace: expect.any(Object), | ||
mechanism: expect.any(Object), | ||
}, | ||
], | ||
}); | ||
|
||
expect(successEventTransaction.contexts.trace).toEqual({ | ||
parent_span_id: expect.any(String), | ||
span_id: expect.any(String), | ||
trace_id: expect.any(String), | ||
data: { | ||
'sentry.source': 'custom', | ||
'sentry.sample_rate': 1, | ||
'sentry.op': 'event.nestjs', | ||
'sentry.origin': 'auto.event.nestjs', | ||
}, | ||
origin: 'auto.event.nestjs', | ||
op: 'event.nestjs', | ||
status: 'ok', | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
packages/node/src/integrations/tracing/nest/sentry-nest-event-instrumentation.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import { isWrapped } from '@opentelemetry/core'; | ||
import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; | ||
import { | ||
InstrumentationBase, | ||
InstrumentationNodeModuleDefinition, | ||
InstrumentationNodeModuleFile, | ||
} from '@opentelemetry/instrumentation'; | ||
import { captureException, startSpan } from '@sentry/core'; | ||
import { SDK_VERSION } from '@sentry/utils'; | ||
import { getEventSpanOptions } from './helpers'; | ||
import type { OnEventTarget } from './types'; | ||
|
||
const supportedVersions = ['>=2.0.0']; | ||
|
||
/** | ||
* Custom instrumentation for nestjs event-emitter | ||
* | ||
* This hooks into the `OnEvent` decorator, which is applied on event handlers. | ||
*/ | ||
export class SentryNestEventInstrumentation extends InstrumentationBase { | ||
public static readonly COMPONENT = '@nestjs/event-emitter'; | ||
public static readonly COMMON_ATTRIBUTES = { | ||
component: SentryNestEventInstrumentation.COMPONENT, | ||
}; | ||
|
||
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( | ||
SentryNestEventInstrumentation.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) { | ||
// 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>'; | ||
|
||
// 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 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; | ||
|
||
// 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 | ||
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; | ||
} | ||
}); | ||
}; | ||
|
||
// Preserve the original function name | ||
Object.defineProperty(descriptor.value, 'name', { | ||
value: handlerName, | ||
configurable: true, | ||
}); | ||
|
||
// Apply the original decorator | ||
return decoratorResult(target, propertyKey, descriptor); | ||
}; | ||
}; | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.