Skip to content
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
1 change: 1 addition & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,7 @@ The following span names were adjusted:
| `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` |
| `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` |
| `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` |
| `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` |
| `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` |
| `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` |
| `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` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ test('Event emitter', async () => {
});
const successEventSpanPromise = waitForStreamedSpan(
APP_NAME,
span => span.is_segment && span.name === 'event myEvent.pass',
span => span.is_segment && span.name === 'myEvent.pass',
);

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

const findHandlerSpans = () =>
streamedSpans.filter(span => span.is_segment && span.name === 'event multiple.first|multiple.second');
streamedSpans.filter(span => span.is_segment && span.name === 'multiple.first|multiple.second');
await expect.poll(() => findHandlerSpans().length).toBe(2);

// Streamed spans carry no scope tags, so the app reports its isolation scope as an attribute.
Expand Down
13 changes: 12 additions & 1 deletion packages/nestjs/src/integrations/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {
CODE_FUNCTION_NAME,
MESSAGING_DESTINATION_NAME,
MESSAGING_OPERATION_TYPE,
MESSAGING_SYSTEM,
SENTRY_DESCRIPTION,
SENTRY_OP,
} from '@sentry/conventions/attributes';
import { FUNCTION, MIDDLEWARE, QUEUE_PROCESS } from '@sentry/conventions/op';
Expand Down Expand Up @@ -111,11 +113,20 @@ export function getEventSpanOptions(event: string): {
attributes: Record<string, string>;
forceTransaction: boolean;
} {
const client = getClient();
const isStreamed = !!client && hasSpanStreamingEnabled(client);
const description = `event ${event}`;

return {
name: `event ${event}`,
// With span streaming, a `function` span is named after what it wraps. An `@OnEvent` handler is
// identified by the event it listens to, so that doubles as its `code.function.name`.
name: isStreamed ? event : description,

@JPeer264 JPeer264 Sep 23, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: Should we maybe keep one test for transactions? (not a blocker for this PR)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup, we probably should. let me follow up with a test

attributes: {
[SENTRY_OP]: FUNCTION,
[CODE_FUNCTION_NAME]: event,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.event.nestjs',
// Relay infers a `function` span's description from `code.function.name` alone, which drops the prefix.
...(isStreamed && { [SENTRY_DESCRIPTION]: description }),
},
// oxlint-disable-next-line typescript/no-deprecated
forceTransaction: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -876,11 +876,37 @@ describe('NestJS orchestrion subscriber: schedule / event / bullmq', () => {
await (descriptor.value as AnyFn)();

const json = spanToJSON(spanInside!);
expect(json.name).toBe('event user.created');
expect(json.name).toBe('user.created');
expect(json.attributes['sentry.op']).toBe('function');
expect(json.attributes['code.function.name']).toBe('user.created');
expect(json.attributes['sentry.description']).toBe('event user.created');
expect(json.attributes['sentry.origin']).toBe('auto.event.nestjs');
Comment thread
Lms24 marked this conversation as resolved.
});

it('event @OnEvent: keeps the transaction-mode span name when span streaming is off', async () => {
installTestAsyncContextStrategy();
initTestClient('static');
subscribeToNestChannels();

const wrappedDecorator = driveFactory(CHANNELS.NESTJS_ONEVENT, ['user.created'], (_t, _k, d) => d);

let spanInside: Span | undefined;
const descriptor: PropertyDescriptor = {
value: async function onUserCreated(): Promise<string> {
spanInside = getActiveSpan();
return 'ok';
},
configurable: true,
};
wrappedDecorator({}, 'onUserCreated', descriptor);

await (descriptor.value as AnyFn)();

const json = spanToJSON(spanInside!);
expect(json.name).toBe('event user.created');
expect(json.attributes['sentry.description']).toBeUndefined();
});

it('bullmq @Processor: patches `process` into a queue.process transaction (string queue name)', async () => {
installTestAsyncContextStrategy();
initTestClient();
Expand Down Expand Up @@ -1014,12 +1040,12 @@ describe('NestJS orchestrion subscriber: schedule / event / bullmq', () => {
});

it.each([
{ label: 'symbol', event: Symbol('user.created'), expected: 'event Symbol(user.created)' },
{ label: 'string array', event: ['user.created', 'user.updated'], expected: 'event user.created,user.updated' },
{ label: 'symbol', event: Symbol('user.created'), expected: 'Symbol(user.created)' },
{ label: 'string array', event: ['user.created', 'user.updated'], expected: 'user.created,user.updated' },
{
label: 'mixed array',
event: [Symbol('user.created'), 'user.updated'],
expected: 'event Symbol(user.created),user.updated',
expected: 'Symbol(user.created),user.updated',
},
])('event @OnEvent: names the transaction from a $label event', async ({ event, expected }) => {
installTestAsyncContextStrategy();
Expand Down
Loading