Skip to content

Commit d8d9324

Browse files
authored
test: Add tests to demonstrate root trace ID behavior (#14426)
This adds node & browser integration tests to demonstrate the current behavior of "trace propagation" based on the current scope. It shows that the behavior is not really consistent and we should probably adjust it, but for now this PR does no changes. ## Browser In browser, the propagation context of the current scope is always picked up. This means that if you call `startSpan` multiple times in the root, both started spans will have the same trace ID, and possibly the same `parentSpanId` if there was an incoming trace. ## Node In node, the propagation context is ignored for the root context. So in the root, started spans will have different traces, and will also ignore parentSpanId (which is only theoretical, there will never really be a parentSpanId on the root scope in node, realistically). Outside of the root (so e.g. within any route handler, or even any `withScope` call), the behavior is the same as in browser - any started spans will share the same trace ID/parent span ID. ## What should we take away from this? In node, I would align the behavior to ignore the trace ID when we have no incoming trace (so no parentSpanId), and always use the traceId & parentSpanId if there is a parentSpanId on the scope (even the root scope, to be consistent). In browser, we cannot make this change because we rely on this behavior for the extended traces after pageload/navigation spans have ended.
1 parent 05479b8 commit d8d9324

File tree

14 files changed

+313
-3
lines changed

14 files changed

+313
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Sentry.withScope(() => {
2+
Sentry.startSpan({ name: 'test_span_1' }, () => undefined);
3+
Sentry.startSpan({ name: 'test_span_2' }, () => undefined);
4+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { expect } from '@playwright/test';
2+
3+
import type { TransactionEvent } from '@sentry/types';
4+
import { sentryTest } from '../../../../utils/fixtures';
5+
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers';
6+
7+
sentryTest(
8+
'should send manually started parallel root spans outside of root context',
9+
async ({ getLocalTestUrl, page }) => {
10+
if (shouldSkipTracingTest()) {
11+
sentryTest.skip();
12+
}
13+
14+
const url = await getLocalTestUrl({ testDir: __dirname });
15+
16+
const transaction1ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_1');
17+
const transaction2ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_2');
18+
19+
await page.goto(url);
20+
21+
const [transaction1Req, transaction2Req] = await Promise.all([transaction1ReqPromise, transaction2ReqPromise]);
22+
23+
const transaction1 = envelopeRequestParser<TransactionEvent>(transaction1Req);
24+
const transaction2 = envelopeRequestParser<TransactionEvent>(transaction2Req);
25+
26+
expect(transaction1).toBeDefined();
27+
expect(transaction2).toBeDefined();
28+
29+
const trace1Id = transaction1.contexts?.trace?.trace_id;
30+
const trace2Id = transaction2.contexts?.trace?.trace_id;
31+
32+
expect(trace1Id).toBeDefined();
33+
expect(trace2Id).toBeDefined();
34+
35+
// We use the same traceID from the root propagation context here
36+
expect(trace1Id).toBe(trace2Id);
37+
38+
expect(transaction1.contexts?.trace?.parent_span_id).toBeUndefined();
39+
expect(transaction2.contexts?.trace?.parent_span_id).toBeUndefined();
40+
},
41+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Sentry.getCurrentScope().setPropagationContext({
2+
parentSpanId: '1234567890123456',
3+
spanId: '123456789012345x',
4+
traceId: '12345678901234567890123456789012',
5+
});
6+
7+
Sentry.startSpan({ name: 'test_span_1' }, () => undefined);
8+
Sentry.startSpan({ name: 'test_span_2' }, () => undefined);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { expect } from '@playwright/test';
2+
3+
import type { TransactionEvent } from '@sentry/types';
4+
import { sentryTest } from '../../../../utils/fixtures';
5+
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers';
6+
7+
sentryTest(
8+
'should send manually started parallel root spans in root context with parentSpanId',
9+
async ({ getLocalTestUrl, page }) => {
10+
if (shouldSkipTracingTest()) {
11+
sentryTest.skip();
12+
}
13+
14+
const url = await getLocalTestUrl({ testDir: __dirname });
15+
16+
const transaction1ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_1');
17+
const transaction2ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_2');
18+
19+
await page.goto(url);
20+
21+
const [transaction1Req, transaction2Req] = await Promise.all([transaction1ReqPromise, transaction2ReqPromise]);
22+
23+
const transaction1 = envelopeRequestParser<TransactionEvent>(transaction1Req);
24+
const transaction2 = envelopeRequestParser<TransactionEvent>(transaction2Req);
25+
26+
expect(transaction1).toBeDefined();
27+
expect(transaction2).toBeDefined();
28+
29+
const trace1Id = transaction1.contexts?.trace?.trace_id;
30+
const trace2Id = transaction2.contexts?.trace?.trace_id;
31+
32+
expect(trace1Id).toBe('12345678901234567890123456789012');
33+
expect(trace2Id).toBe('12345678901234567890123456789012');
34+
35+
expect(transaction1.contexts?.trace?.parent_span_id).toBe('1234567890123456');
36+
expect(transaction2.contexts?.trace?.parent_span_id).toBe('1234567890123456');
37+
},
38+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Sentry.startSpan({ name: 'test_span_1' }, () => undefined);
2+
Sentry.startSpan({ name: 'test_span_2' }, () => undefined);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { expect } from '@playwright/test';
2+
3+
import type { TransactionEvent } from '@sentry/types';
4+
import { sentryTest } from '../../../../utils/fixtures';
5+
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers';
6+
7+
sentryTest('should send manually started parallel root spans in root context', async ({ getLocalTestUrl, page }) => {
8+
if (shouldSkipTracingTest()) {
9+
sentryTest.skip();
10+
}
11+
12+
const url = await getLocalTestUrl({ testDir: __dirname });
13+
14+
const transaction1ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_1');
15+
const transaction2ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_2');
16+
17+
await page.goto(url);
18+
19+
const [transaction1Req, transaction2Req] = await Promise.all([transaction1ReqPromise, transaction2ReqPromise]);
20+
21+
const transaction1 = envelopeRequestParser<TransactionEvent>(transaction1Req);
22+
const transaction2 = envelopeRequestParser<TransactionEvent>(transaction2Req);
23+
24+
expect(transaction1).toBeDefined();
25+
expect(transaction2).toBeDefined();
26+
27+
const trace1Id = transaction1.contexts?.trace?.trace_id;
28+
const trace2Id = transaction2.contexts?.trace?.trace_id;
29+
30+
expect(trace1Id).toBeDefined();
31+
expect(trace2Id).toBeDefined();
32+
33+
// We use the same traceID from the root propagation context here
34+
expect(trace1Id).toBe(trace2Id);
35+
36+
expect(transaction1.contexts?.trace?.parent_span_id).toBeUndefined();
37+
expect(transaction2.contexts?.trace?.parent_span_id).toBeUndefined();
38+
});

dev-packages/browser-integration-tests/utils/helpers.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
Event,
88
EventEnvelope,
99
EventEnvelopeHeaders,
10+
TransactionEvent,
1011
} from '@sentry/types';
1112

1213
export const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//;
@@ -224,7 +225,10 @@ export function waitForErrorRequest(page: Page, callback?: (event: Event) => boo
224225
});
225226
}
226227

227-
export function waitForTransactionRequest(page: Page): Promise<Request> {
228+
export function waitForTransactionRequest(
229+
page: Page,
230+
callback?: (event: TransactionEvent) => boolean,
231+
): Promise<Request> {
228232
return page.waitForRequest(req => {
229233
const postData = req.postData();
230234
if (!postData) {
@@ -234,7 +238,15 @@ export function waitForTransactionRequest(page: Page): Promise<Request> {
234238
try {
235239
const event = envelopeRequestParser(req);
236240

237-
return event.type === 'transaction';
241+
if (event.type !== 'transaction') {
242+
return false;
243+
}
244+
245+
if (callback) {
246+
return callback(event as TransactionEvent);
247+
}
248+
249+
return true;
238250
} catch {
239251
return false;
240252
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
2+
import * as Sentry from '@sentry/node';
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
});
10+
11+
Sentry.getCurrentScope().setPropagationContext({
12+
parentSpanId: '1234567890123456',
13+
spanId: '123456789012345x',
14+
traceId: '12345678901234567890123456789012',
15+
});
16+
17+
const spanIdTraceId = Sentry.startSpan(
18+
{
19+
name: 'test_span_1',
20+
},
21+
span1 => span1.spanContext().traceId,
22+
);
23+
24+
Sentry.startSpan(
25+
{
26+
name: 'test_span_2',
27+
attributes: { spanIdTraceId },
28+
},
29+
() => undefined,
30+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
2+
3+
afterAll(() => {
4+
cleanupChildProcesses();
5+
});
6+
7+
test('should send manually started parallel root spans in root context', done => {
8+
expect.assertions(7);
9+
10+
createRunner(__dirname, 'scenario.ts')
11+
.expect({ transaction: { transaction: 'test_span_1' } })
12+
.expect({
13+
transaction: transaction => {
14+
expect(transaction).toBeDefined();
15+
const traceId = transaction.contexts?.trace?.trace_id;
16+
expect(traceId).toBeDefined();
17+
18+
// It ignores propagation context of the root context
19+
expect(traceId).not.toBe('12345678901234567890123456789012');
20+
expect(transaction.contexts?.trace?.parent_span_id).toBeUndefined();
21+
22+
// Different trace ID than the first span
23+
const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId;
24+
expect(trace1Id).toBeDefined();
25+
expect(trace1Id).not.toBe(traceId);
26+
},
27+
})
28+
.start(done);
29+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
2+
import * as Sentry from '@sentry/node';
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
});
10+
11+
Sentry.withScope(scope => {
12+
scope.setPropagationContext({
13+
parentSpanId: '1234567890123456',
14+
spanId: '123456789012345x',
15+
traceId: '12345678901234567890123456789012',
16+
});
17+
18+
Sentry.startSpan({ name: 'test_span_1' }, () => undefined);
19+
Sentry.startSpan({ name: 'test_span_2' }, () => undefined);
20+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
2+
3+
afterAll(() => {
4+
cleanupChildProcesses();
5+
});
6+
7+
test('should send manually started parallel root spans outside of root context with parentSpanId', done => {
8+
createRunner(__dirname, 'scenario.ts')
9+
.expect({
10+
transaction: {
11+
transaction: 'test_span_1',
12+
contexts: {
13+
trace: {
14+
span_id: expect.any(String),
15+
parent_span_id: '1234567890123456',
16+
trace_id: '12345678901234567890123456789012',
17+
},
18+
},
19+
},
20+
})
21+
.expect({
22+
transaction: {
23+
transaction: 'test_span_2',
24+
contexts: {
25+
trace: {
26+
span_id: expect.any(String),
27+
parent_span_id: '1234567890123456',
28+
trace_id: '12345678901234567890123456789012',
29+
},
30+
},
31+
},
32+
})
33+
.start(done);
34+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
2+
import * as Sentry from '@sentry/node';
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
});
10+
11+
Sentry.withScope(() => {
12+
const spanIdTraceId = Sentry.startSpan(
13+
{
14+
name: 'test_span_1',
15+
},
16+
span1 => span1.spanContext().traceId,
17+
);
18+
19+
Sentry.startSpan(
20+
{
21+
name: 'test_span_2',
22+
attributes: { spanIdTraceId },
23+
},
24+
() => undefined,
25+
);
26+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
2+
3+
afterAll(() => {
4+
cleanupChildProcesses();
5+
});
6+
7+
test('should send manually started parallel root spans outside of root context', done => {
8+
expect.assertions(6);
9+
10+
createRunner(__dirname, 'scenario.ts')
11+
.expect({ transaction: { transaction: 'test_span_1' } })
12+
.expect({
13+
transaction: transaction => {
14+
expect(transaction).toBeDefined();
15+
const traceId = transaction.contexts?.trace?.trace_id;
16+
expect(traceId).toBeDefined();
17+
expect(transaction.contexts?.trace?.parent_span_id).toBeUndefined();
18+
19+
const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId;
20+
expect(trace1Id).toBeDefined();
21+
22+
// Same trace ID as the first span
23+
expect(trace1Id).toBe(traceId);
24+
},
25+
})
26+
.start(done);
27+
});

packages/opentelemetry/src/trace.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,9 @@ function ensureTimestampInMilliseconds(timestamp: number): number {
176176

177177
function getContext(scope: Scope | undefined, forceTransaction: boolean | undefined): Context {
178178
const ctx = getContextForScope(scope);
179+
// Note: If the context is the ROOT_CONTEXT, no scope is attached
180+
// Thus we will not use the propagation context in this case, which is desired
179181
const actualScope = getScopesFromContext(ctx)?.scope;
180-
181182
const parentSpan = trace.getSpan(ctx);
182183

183184
// In the case that we have no parent span, we need to "simulate" one to ensure the propagation context is correct

0 commit comments

Comments
 (0)