Skip to content

Commit 12100c6

Browse files
Lms24claude
andcommitted
fix(nestjs): Follow the function conventions for @OnEvent span names
The `@OnEvent` handler span was named `event <name>` and set no `code.function.name`, so the `event ` prefix was derivable from nothing and Relay could infer no description at all for it. Both the name and the description conventions for the `function` op are built from `code.function.name`. Name the span after the event the handler listens to and set that as `code.function.name`. The event is the closest thing to an identity the SDK has for an `@OnEvent` handler, and it is what made the old name useful. The prefixed name moves to `sentry.description`, matching what the bullmq helper next to it already does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 5b691f0 commit 12100c6

4 files changed

Lines changed: 49 additions & 9 deletions

File tree

‎MIGRATION.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,7 @@ The following span names were adjusted:
10361036
| `function` (SvelteKit) | The route the wrapped function ran for, or the raw URL path if the SDK couldn't resolve one | `/users/[id]`, `/users/123`, `GET /api/users/[id]` | The name of the wrapped function | `load`, `GET` |
10371037
| `function` (Ember route hooks) | The full route name | `slow-loading-route.index` | The hook the span wraps, matching its `code.function.name`. The route moves to `sentry.description` | `beforeModel`, `model`, `setupController` |
10381038
| `function` (React Router route hooks) | The route the hook ran for, the raw URL path if React Router matched no pattern, or the fetcher key | `/users/:id`, `/users/123`, `Fetcher fetcher-1` | The hook the span wraps, matching its `code.function.name`. The previous name moves to `sentry.description` | `loader`, `action`, `clientLoader`, `fetcher` |
1039+
| `function` (NestJS `@OnEvent` handlers) | The event the handler listens to, prefixed with `event ` | `event user.created` | The event the handler listens to, which is also its `code.function.name` | `user.created` |
10391040
| `function.gcp` | The request method and path for HTTP functions, otherwise the trigger's event or trigger type | `POST /users`, `google.pubsub.topic.publish`, `firebase.function.http.request` | The function name, or `Serverless function execution` if the SDK cannot resolve one | `myFunction`, `Serverless function execution` |
10401041
| `function.aws` | The Lambda function name | `my-function` | Unchanged, except that the SDK now falls back to `Serverless function execution` if it cannot resolve the function name | `my-function`, `Serverless function execution` |
10411042
| `graphql` | The graphql phase and, for operations, the operation name | `query GetUser`, `graphql.parse`, `graphql.resolve user.0.name` | The operation type, or the processing type where there is none | `GraphQL query`, `GraphQL parse`, `GraphQL resolve` |

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ test('Event emitter', async () => {
1010
});
1111
const successEventSpanPromise = waitForStreamedSpan(
1212
APP_NAME,
13-
span => span.is_segment && span.name === 'event myEvent.pass',
13+
span => span.is_segment && span.name === 'myEvent.pass',
1414
);
1515

1616
const eventsUrl = `http://localhost:3050/events/emit`;
@@ -36,7 +36,7 @@ test('Event emitter', async () => {
3636
// A segment span also carries the scope contexts and the SDK's integration list, which vary by
3737
// machine, so only the event-handler attributes are pinned here.
3838
expect(successEventSpan).toEqual({
39-
name: 'event myEvent.pass',
39+
name: 'myEvent.pass',
4040
span_id: expect.stringMatching(/^[a-f0-9]{16}$/),
4141
trace_id: expect.stringMatching(/^[a-f0-9]{32}$/),
4242
parent_span_id: expect.stringMatching(/^[a-f0-9]{16}$/),
@@ -49,7 +49,9 @@ test('Event emitter', async () => {
4949
'sentry.origin': { type: 'string', value: 'auto.event.nestjs' },
5050
'sentry.segment.name.source': { type: 'string', value: 'custom' },
5151
'sentry.trace_lifecycle': { type: 'string', value: 'stream' },
52-
'sentry.segment.name': { type: 'string', value: 'event myEvent.pass' },
52+
'sentry.segment.name': { type: 'string', value: 'myEvent.pass' },
53+
'sentry.description': { type: 'string', value: 'event myEvent.pass' },
54+
'code.function.name': { type: 'string', value: 'myEvent.pass' },
5355
'sentry.sdk.name': { type: 'string', value: 'sentry.javascript.nestjs' },
5456
'sentry.environment': { type: 'string', value: 'qa' },
5557
}),
@@ -100,7 +102,7 @@ test('Multiple OnEvent decorators', async () => {
100102
const rootSpan = await rootSpanPromise;
101103

102104
const findHandlerSpans = () =>
103-
streamedSpans.filter(span => span.is_segment && span.name === 'event multiple.first|multiple.second');
105+
streamedSpans.filter(span => span.is_segment && span.name === 'multiple.first|multiple.second');
104106
await expect.poll(() => findHandlerSpans().length).toBe(2);
105107

106108
// Streamed spans carry no scope tags, so the app reports its isolation scope as an attribute.

‎packages/nestjs/src/integrations/helpers.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import {
2+
CODE_FUNCTION_NAME,
23
MESSAGING_DESTINATION_NAME,
34
MESSAGING_OPERATION_TYPE,
45
MESSAGING_SYSTEM,
6+
SENTRY_DESCRIPTION,
57
SENTRY_OP,
68
} from '@sentry/conventions/attributes';
79
import { FUNCTION, MIDDLEWARE, QUEUE_PROCESS } from '@sentry/conventions/op';
@@ -111,11 +113,20 @@ export function getEventSpanOptions(event: string): {
111113
attributes: Record<string, string>;
112114
forceTransaction: boolean;
113115
} {
116+
const client = getClient();
117+
const isStreamed = !!client && hasSpanStreamingEnabled(client);
118+
const description = `event ${event}`;
119+
114120
return {
115-
name: `event ${event}`,
121+
// With span streaming, a `function` span is named after what it wraps. An `@OnEvent` handler is
122+
// identified by the event it listens to, so that doubles as its `code.function.name`.
123+
name: isStreamed ? event : description,
116124
attributes: {
117125
[SENTRY_OP]: FUNCTION,
126+
[CODE_FUNCTION_NAME]: event,
118127
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.event.nestjs',
128+
// Relay infers a `function` span's description from `code.function.name` alone, which drops the prefix.
129+
...(isStreamed && { [SENTRY_DESCRIPTION]: description }),
119130
},
120131
// oxlint-disable-next-line typescript/no-deprecated
121132
forceTransaction: true,

‎packages/nestjs/test/integrations/orchestrion-subscriber.test.ts‎

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -876,11 +876,37 @@ describe('NestJS orchestrion subscriber: schedule / event / bullmq', () => {
876876
await (descriptor.value as AnyFn)();
877877

878878
const json = spanToJSON(spanInside!);
879-
expect(json.name).toBe('event user.created');
879+
expect(json.name).toBe('user.created');
880880
expect(json.attributes['sentry.op']).toBe('function');
881+
expect(json.attributes['code.function.name']).toBe('user.created');
882+
expect(json.attributes['sentry.description']).toBe('event user.created');
881883
expect(json.attributes['sentry.origin']).toBe('auto.event.nestjs');
882884
});
883885

886+
it('event @OnEvent: keeps the transaction-mode span name when span streaming is off', async () => {
887+
installTestAsyncContextStrategy();
888+
initTestClient('static');
889+
subscribeToNestChannels();
890+
891+
const wrappedDecorator = driveFactory(CHANNELS.NESTJS_ONEVENT, ['user.created'], (_t, _k, d) => d);
892+
893+
let spanInside: Span | undefined;
894+
const descriptor: PropertyDescriptor = {
895+
value: async function onUserCreated(): Promise<string> {
896+
spanInside = getActiveSpan();
897+
return 'ok';
898+
},
899+
configurable: true,
900+
};
901+
wrappedDecorator({}, 'onUserCreated', descriptor);
902+
903+
await (descriptor.value as AnyFn)();
904+
905+
const json = spanToJSON(spanInside!);
906+
expect(json.name).toBe('event user.created');
907+
expect(json.attributes['sentry.description']).toBeUndefined();
908+
});
909+
884910
it('bullmq @Processor: patches `process` into a queue.process transaction (string queue name)', async () => {
885911
installTestAsyncContextStrategy();
886912
initTestClient();
@@ -1014,12 +1040,12 @@ describe('NestJS orchestrion subscriber: schedule / event / bullmq', () => {
10141040
});
10151041

10161042
it.each([
1017-
{ label: 'symbol', event: Symbol('user.created'), expected: 'event Symbol(user.created)' },
1018-
{ label: 'string array', event: ['user.created', 'user.updated'], expected: 'event user.created,user.updated' },
1043+
{ label: 'symbol', event: Symbol('user.created'), expected: 'Symbol(user.created)' },
1044+
{ label: 'string array', event: ['user.created', 'user.updated'], expected: 'user.created,user.updated' },
10191045
{
10201046
label: 'mixed array',
10211047
event: [Symbol('user.created'), 'user.updated'],
1022-
expected: 'event Symbol(user.created),user.updated',
1048+
expected: 'Symbol(user.created),user.updated',
10231049
},
10241050
])('event @OnEvent: names the transaction from a $label event', async ({ event, expected }) => {
10251051
installTestAsyncContextStrategy();

0 commit comments

Comments
 (0)