Skip to content

Commit c145409

Browse files
chargomeclaude
andauthored
test(e2e): Port the nestjs-bullmq E2E app to span streaming (#24107)
Removes the `traceLifecycle: 'static'` pin and rewrites the specs against streamed spans. The queue span is named `process test-queue` under span streaming, the other way around from the transaction name. Streamed spans carry no breadcrumbs, so `/check-isolation` reports which of the processor's breadcrumbs leaked into its isolation scope as a span attribute. Ref: #23801 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <[email protected]>
1 parent 85a4d11 commit c145409

3 files changed

Lines changed: 67 additions & 95 deletions

File tree

‎dev-packages/e2e-tests/test-applications/nestjs-bullmq/src/app.controller.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Controller, Get, Param } from '@nestjs/common';
22
import { InjectQueue } from '@nestjs/bullmq';
3+
import { getActiveSpan, getIsolationScope, getRootSpan } from '@sentry/nestjs';
34
import { Queue } from 'bullmq';
45

56
@Controller()
@@ -14,8 +15,18 @@ export class AppController {
1415

1516
@Get('check-isolation')
1617
checkIsolation() {
17-
// This endpoint is called after the processor adds a breadcrumb.
18-
// The test verifies that breadcrumbs from the processor do NOT leak here.
18+
// This endpoint is called after the processor adds a breadcrumb. Streamed spans carry no
19+
// breadcrumbs, so the tests read from this attribute which of the processor's breadcrumbs
20+
// leaked into this request's isolation scope.
21+
const activeSpan = getActiveSpan();
22+
if (activeSpan) {
23+
const breadcrumbs = getIsolationScope().getScopeData().breadcrumbs;
24+
getRootSpan(activeSpan).setAttribute(
25+
'isolation_scope.leaked_breadcrumbs',
26+
breadcrumbs.map(breadcrumb => breadcrumb.message ?? '').filter(message => message.startsWith('leaked-')),
27+
);
28+
}
29+
1930
return { message: 'ok' };
2031
}
2132
}

‎dev-packages/e2e-tests/test-applications/nestjs-bullmq/src/instrument.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as Sentry from '@sentry/nestjs';
22

33
Sentry.init({
4-
traceLifecycle: 'static',
54
environment: 'qa', // dynamic sampling bias to keep transactions
65
dsn: process.env.E2E_TEST_DSN,
76
tunnel: `http://localhost:3031/`, // proxy server
Lines changed: 54 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,34 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
2+
import { getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';
3+
4+
const APP_NAME = 'nestjs-bullmq';
5+
6+
function waitForProcessSpan(): Promise<unknown> {
7+
return waitForStreamedSpan(APP_NAME, span => span.is_segment && getSpanOp(span) === 'queue.process');
8+
}
9+
10+
/**
11+
* The `/check-isolation` route reports the leaked breadcrumbs it can see as a span attribute,
12+
* because streamed spans carry no breadcrumbs of their own.
13+
*/
14+
async function getLeakedBreadcrumbs(baseURL: string): Promise<string[]> {
15+
const segmentSpanPromise = waitForStreamedSpan(APP_NAME, span => {
16+
return span.is_segment && span.name === 'GET /check-isolation';
17+
});
18+
19+
await fetch(`${baseURL}/check-isolation`);
20+
21+
const attribute = (await segmentSpanPromise).attributes['isolation_scope.leaked_breadcrumbs'];
22+
23+
// A route that stopped reporting would make every `not.toContain` below pass for the wrong
24+
// reason, so require the attribute rather than defaulting it away.
25+
expect(attribute).toEqual({ type: 'array', value: expect.any(Array) });
26+
27+
return attribute!.value as string[];
28+
}
329

430
test('Sends exception to Sentry on error in @Processor process method', async ({ baseURL }) => {
5-
const errorEventPromise = waitForError('nestjs-bullmq', event => {
31+
const errorEventPromise = waitForError(APP_NAME, event => {
632
return (
733
!event.type &&
834
event.exception?.values?.[0]?.value === 'Test error from BullMQ processor' &&
@@ -22,43 +48,31 @@ test('Sends exception to Sentry on error in @Processor process method', async ({
2248
});
2349
});
2450

25-
test('Creates a transaction for successful job processing', async ({ baseURL }) => {
26-
const transactionPromise = waitForTransaction('nestjs-bullmq', transactionEvent => {
27-
return transactionEvent.contexts?.trace?.op === 'queue.process';
51+
test('Creates a segment span for successful job processing', async ({ baseURL }) => {
52+
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
53+
return span.is_segment && getSpanOp(span) === 'queue.process';
2854
});
2955

3056
// Enqueue a job that will succeed
3157
await fetch(`${baseURL}/enqueue/success`);
3258

33-
const transaction = await transactionPromise;
59+
const span = await spanPromise;
3460

35-
expect(transaction.transaction).toBe('test-queue process');
36-
expect(transaction.contexts?.trace?.op).toBe('queue.process');
37-
expect(transaction.contexts?.trace?.origin).toBe('auto.queue.nestjs.bullmq');
61+
// Streamed messaging spans are named `<operation> <destination>`, the other way around from the
62+
// transaction name.
63+
expect(span.name).toBe('process test-queue');
64+
expect(span.attributes['sentry.origin']).toEqual({ value: 'auto.queue.nestjs.bullmq', type: 'string' });
3865
});
3966

4067
test('BullMQ processor breadcrumbs do not leak into subsequent HTTP requests', async ({ baseURL }) => {
41-
const processTransactionPromise = waitForTransaction('nestjs-bullmq', transactionEvent => {
42-
return transactionEvent.contexts?.trace?.op === 'queue.process';
43-
});
68+
const processSpanPromise = waitForProcessSpan();
4469

4570
// Enqueue a job that adds a breadcrumb during processing
4671
await fetch(`${baseURL}/enqueue/breadcrumb-test`);
4772

48-
await processTransactionPromise;
73+
await processSpanPromise;
4974

50-
const transactionPromise = waitForTransaction('nestjs-bullmq', transactionEvent => {
51-
return transactionEvent.transaction === 'GET /check-isolation';
52-
});
53-
54-
await fetch(`${baseURL}/check-isolation`);
55-
56-
const transaction = await transactionPromise;
57-
58-
const leakedBreadcrumb = (transaction.breadcrumbs || []).find(
59-
(b: any) => b.message === 'leaked-breadcrumb-from-bullmq-processor',
60-
);
61-
expect(leakedBreadcrumb).toBeUndefined();
75+
expect(await getLeakedBreadcrumbs(baseURL!)).not.toContain('leaked-breadcrumb-from-bullmq-processor');
6276
});
6377

6478
// TODO: @OnWorkerEvent('completed') handlers run outside the isolation scope created by process().
@@ -67,84 +81,45 @@ test('BullMQ processor breadcrumbs do not leak into subsequent HTTP requests', a
6781
test('BullMQ @OnWorkerEvent completed lifecycle breadcrumbs currently leak into subsequent HTTP requests', async ({
6882
baseURL,
6983
}) => {
70-
const processTransactionPromise = waitForTransaction('nestjs-bullmq', transactionEvent => {
71-
return transactionEvent.contexts?.trace?.op === 'queue.process';
72-
});
84+
const processSpanPromise = waitForProcessSpan();
7385

7486
// Enqueue a job (the completed event fires right after the job is processed)
7587
await fetch(`${baseURL}/enqueue/lifecycle-breadcrumb-test`);
7688

77-
await processTransactionPromise;
78-
79-
const transactionPromise = waitForTransaction('nestjs-bullmq', transactionEvent => {
80-
return transactionEvent.transaction === 'GET /check-isolation';
81-
});
82-
83-
await fetch(`${baseURL}/check-isolation`);
89+
await processSpanPromise;
8490

85-
const transaction = await transactionPromise;
86-
87-
const leakedBreadcrumb = (transaction.breadcrumbs || []).find(
88-
(b: any) => b.message === 'leaked-breadcrumb-from-lifecycle-event',
89-
);
90-
// This SHOULD be toBeUndefined() once lifecycle event isolation is implemented.
91-
expect(leakedBreadcrumb).toBeDefined();
91+
// This SHOULD be not.toContain() once lifecycle event isolation is implemented.
92+
expect(await getLeakedBreadcrumbs(baseURL!)).toContain('leaked-breadcrumb-from-lifecycle-event');
9293
});
9394

9495
// TODO: @OnWorkerEvent('active') handlers run outside the isolation scope created by process().
9596
// Breadcrumbs set there leak into the default isolation scope and appear on subsequent HTTP requests.
9697
test('BullMQ @OnWorkerEvent active lifecycle breadcrumbs currently leak into subsequent HTTP requests', async ({
9798
baseURL,
9899
}) => {
99-
const processTransactionPromise = waitForTransaction('nestjs-bullmq', transactionEvent => {
100-
return transactionEvent.contexts?.trace?.op === 'queue.process';
101-
});
100+
const processSpanPromise = waitForProcessSpan();
102101

103102
await fetch(`${baseURL}/enqueue/lifecycle-active-breadcrumb-test`);
104103

105-
await processTransactionPromise;
106-
107-
const transactionPromise = waitForTransaction('nestjs-bullmq', transactionEvent => {
108-
return transactionEvent.transaction === 'GET /check-isolation';
109-
});
110-
111-
await fetch(`${baseURL}/check-isolation`);
112-
113-
const transaction = await transactionPromise;
104+
await processSpanPromise;
114105

115-
const leakedBreadcrumb = (transaction.breadcrumbs || []).find(
116-
(b: any) => b.message === 'leaked-breadcrumb-from-active-event',
117-
);
118-
// This SHOULD be toBeUndefined() once lifecycle event isolation is implemented.
119-
expect(leakedBreadcrumb).toBeDefined();
106+
// This SHOULD be not.toContain() once lifecycle event isolation is implemented.
107+
expect(await getLeakedBreadcrumbs(baseURL!)).toContain('leaked-breadcrumb-from-active-event');
120108
});
121109

122110
// TODO: @OnWorkerEvent('failed') handlers run outside the isolation scope created by process().
123111
// Breadcrumbs set there leak into the default isolation scope and appear on subsequent HTTP requests.
124112
test('BullMQ @OnWorkerEvent failed lifecycle breadcrumbs currently leak into subsequent HTTP requests', async ({
125113
baseURL,
126114
}) => {
127-
const processTransactionPromise = waitForTransaction('nestjs-bullmq', transactionEvent => {
128-
return transactionEvent.contexts?.trace?.op === 'queue.process';
129-
});
115+
const processSpanPromise = waitForProcessSpan();
130116

131117
await fetch(`${baseURL}/enqueue/lifecycle-failed-breadcrumb-test`);
132118

133-
await processTransactionPromise;
134-
135-
const transactionPromise = waitForTransaction('nestjs-bullmq', transactionEvent => {
136-
return transactionEvent.transaction === 'GET /check-isolation';
137-
});
119+
await processSpanPromise;
138120

139-
await fetch(`${baseURL}/check-isolation`);
140-
141-
const transaction = await transactionPromise;
142-
143-
const leakedBreadcrumb = (transaction.breadcrumbs || []).find(
144-
(b: any) => b.message === 'leaked-breadcrumb-from-failed-event',
145-
);
146-
// This SHOULD be toBeUndefined() once lifecycle event isolation is implemented.
147-
expect(leakedBreadcrumb).toBeDefined();
121+
// This SHOULD be not.toContain() once lifecycle event isolation is implemented.
122+
expect(await getLeakedBreadcrumbs(baseURL!)).toContain('leaked-breadcrumb-from-failed-event');
148123
});
149124

150125
// The 'progress' event does NOT leak breadcrumbs — unlike 'active', 'completed', and 'failed',
@@ -153,24 +128,11 @@ test('BullMQ @OnWorkerEvent failed lifecycle breadcrumbs currently leak into sub
153128
test('BullMQ @OnWorkerEvent progress lifecycle breadcrumbs do not leak into subsequent HTTP requests', async ({
154129
baseURL,
155130
}) => {
156-
const processTransactionPromise = waitForTransaction('nestjs-bullmq', transactionEvent => {
157-
return transactionEvent.contexts?.trace?.op === 'queue.process';
158-
});
131+
const processSpanPromise = waitForProcessSpan();
159132

160133
await fetch(`${baseURL}/enqueue/lifecycle-progress-breadcrumb-test`);
161134

162-
await processTransactionPromise;
163-
164-
const transactionPromise = waitForTransaction('nestjs-bullmq', transactionEvent => {
165-
return transactionEvent.transaction === 'GET /check-isolation';
166-
});
167-
168-
await fetch(`${baseURL}/check-isolation`);
169-
170-
const transaction = await transactionPromise;
135+
await processSpanPromise;
171136

172-
const leakedBreadcrumb = (transaction.breadcrumbs || []).find(
173-
(b: any) => b.message === 'leaked-breadcrumb-from-progress-event',
174-
);
175-
expect(leakedBreadcrumb).toBeUndefined();
137+
expect(await getLeakedBreadcrumbs(baseURL!)).not.toContain('leaked-breadcrumb-from-progress-event');
176138
});

0 commit comments

Comments
 (0)