Skip to content

Commit 05f531a

Browse files
committed
add linked traces and consistent trace sampling tests, fix bug with unsampled traces being sent
1 parent c2b2ce5 commit 05f531a

File tree

40 files changed

+1464
-4
lines changed

40 files changed

+1464
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
integrations: [
8+
Sentry.browserTracingIntegration({
9+
linkPreviousTrace: 'in-memory',
10+
consistentTraceSampling: true,
11+
}),
12+
Sentry.spanStreamingIntegration(),
13+
],
14+
tracePropagationTargets: ['sentry-test-external.io'],
15+
tracesSampler: ctx => {
16+
if (ctx.attributes && ctx.attributes['sentry.origin'] === 'auto.pageload.browser') {
17+
return 1;
18+
}
19+
return ctx.inheritOrSampleWith(0);
20+
},
21+
debug: true,
22+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const btn1 = document.getElementById('btn1');
2+
3+
const btn2 = document.getElementById('btn2');
4+
5+
btn1.addEventListener('click', () => {
6+
Sentry.startNewTrace(() => {
7+
Sentry.startSpan({ name: 'custom root span 1', op: 'custom' }, () => {});
8+
});
9+
});
10+
11+
btn2.addEventListener('click', () => {
12+
Sentry.startNewTrace(() => {
13+
Sentry.startSpan({ name: 'custom root span 2', op: 'custom' }, async () => {
14+
await fetch('http://sentry-test-external.io');
15+
});
16+
});
17+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<button id="btn1"></button>
7+
<button id="btn2"></button>
8+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { expect } from '@playwright/test';
2+
import { extractTraceparentData, parseBaggageHeader, SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';
3+
import { sentryTest } from '../../../../../../utils/fixtures';
4+
import { shouldSkipTracingTest, waitForTracingHeadersOnUrl } from '../../../../../../utils/helpers';
5+
import { getSpanOp, waitForStreamedSpanEnvelope } from '../../../../../../utils/spanUtils';
6+
7+
sentryTest.describe('When `consistentTraceSampling` is `true`', () => {
8+
sentryTest('continues sampling decision from initial pageload span', async ({ getLocalTestUrl, page }) => {
9+
if (shouldSkipTracingTest()) {
10+
sentryTest.skip();
11+
}
12+
13+
const url = await getLocalTestUrl({ testDir: __dirname });
14+
15+
const { pageloadSpan, pageloadSampleRand } = await sentryTest.step('Initial pageload', async () => {
16+
const pageloadEnvelopePromise = waitForStreamedSpanEnvelope(
17+
page,
18+
env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'pageload'),
19+
);
20+
await page.goto(url);
21+
22+
const envelope = await pageloadEnvelopePromise;
23+
const pageloadSampleRand = Number(envelope[0].trace?.sample_rand);
24+
const pageloadSpan = envelope[1][0][1].items.find(s => getSpanOp(s) === 'pageload')!;
25+
26+
expect(pageloadSpan.attributes?.['sentry.sample_rate']?.value).toBe(1);
27+
expect(Number.isNaN(pageloadSampleRand)).toBe(false);
28+
expect(pageloadSampleRand).toBeGreaterThanOrEqual(0);
29+
expect(pageloadSampleRand).toBeLessThanOrEqual(1);
30+
31+
return { pageloadSpan, pageloadSampleRand };
32+
});
33+
34+
const customTraceSpan = await sentryTest.step('Custom trace', async () => {
35+
const customEnvelopePromise = waitForStreamedSpanEnvelope(
36+
page,
37+
env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'custom'),
38+
);
39+
await page.locator('#btn1').click();
40+
const envelope = await customEnvelopePromise;
41+
const span = envelope[1][0][1].items.find(s => getSpanOp(s) === 'custom')!;
42+
43+
expect(span.trace_id).not.toEqual(pageloadSpan.trace_id);
44+
// although we "continue the trace" from pageload, this is actually a root span,
45+
// so there must not be a parent span id
46+
expect(span.parent_span_id).toBeUndefined();
47+
48+
expect(Number(envelope[0].trace?.sample_rand)).toBe(pageloadSampleRand);
49+
50+
return span;
51+
});
52+
53+
await sentryTest.step('Navigation', async () => {
54+
const navigationEnvelopePromise = waitForStreamedSpanEnvelope(
55+
page,
56+
env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'),
57+
);
58+
await page.goto(`${url}#foo`);
59+
const envelope = await navigationEnvelopePromise;
60+
const navSpan = envelope[1][0][1].items.find(s => getSpanOp(s) === 'navigation')!;
61+
62+
expect(navSpan.trace_id).not.toEqual(customTraceSpan.trace_id);
63+
expect(navSpan.trace_id).not.toEqual(pageloadSpan.trace_id);
64+
65+
expect(navSpan.links).toEqual([
66+
{
67+
trace_id: customTraceSpan.trace_id,
68+
span_id: customTraceSpan.span_id,
69+
sampled: true,
70+
attributes: {
71+
[SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: {
72+
type: 'string',
73+
value: 'previous_trace',
74+
},
75+
},
76+
},
77+
]);
78+
expect(navSpan.parent_span_id).toBeUndefined();
79+
80+
expect(Number(envelope[0].trace?.sample_rand)).toBe(pageloadSampleRand);
81+
});
82+
});
83+
84+
sentryTest('Propagates continued sampling decision to outgoing requests', async ({ page, getLocalTestUrl }) => {
85+
if (shouldSkipTracingTest()) {
86+
sentryTest.skip();
87+
}
88+
89+
const url = await getLocalTestUrl({ testDir: __dirname });
90+
91+
const { pageloadSpan, pageloadSampleRand } = await sentryTest.step('Initial pageload', async () => {
92+
const pageloadEnvelopePromise = waitForStreamedSpanEnvelope(
93+
page,
94+
env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'pageload'),
95+
);
96+
await page.goto(url);
97+
98+
const envelope = await pageloadEnvelopePromise;
99+
const pageloadSampleRand = Number(envelope[0].trace?.sample_rand);
100+
101+
expect(Number(envelope[0].trace?.sample_rand)).toBe(pageloadSampleRand);
102+
expect(pageloadSampleRand).toBeGreaterThanOrEqual(0);
103+
expect(pageloadSampleRand).toBeLessThanOrEqual(1);
104+
expect(Number.isNaN(pageloadSampleRand)).toBe(false);
105+
106+
const pageloadSpan = envelope[1][0][1].items.find(s => getSpanOp(s) === 'pageload')!;
107+
108+
expect(pageloadSpan.attributes?.['sentry.sample_rate']?.value).toBe(1);
109+
110+
return { pageloadSpan, pageloadSampleRand };
111+
});
112+
113+
await sentryTest.step('Make fetch request', async () => {
114+
const fetchEnvelopePromise = waitForStreamedSpanEnvelope(
115+
page,
116+
env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'custom'),
117+
);
118+
const tracingHeadersPromise = waitForTracingHeadersOnUrl(page, 'http://sentry-test-external.io');
119+
120+
await page.locator('#btn2').click();
121+
122+
const { baggage, sentryTrace } = await tracingHeadersPromise;
123+
const fetchEnvelope = await fetchEnvelopePromise;
124+
125+
const fetchTraceSampleRand = Number(fetchEnvelope[0].trace?.sample_rand);
126+
const fetchTraceSpans = fetchEnvelope[1][0][1].items;
127+
const fetchTraceSpan = fetchTraceSpans.find(s => getSpanOp(s) === 'custom')!;
128+
const httpClientSpan = fetchTraceSpans.find(s => getSpanOp(s) === 'http.client');
129+
130+
expect(fetchTraceSampleRand).toBe(pageloadSampleRand);
131+
132+
expect(fetchTraceSpan.attributes?.['sentry.sample_rate']?.value).toEqual(
133+
pageloadSpan.attributes?.['sentry.sample_rate']?.value,
134+
);
135+
expect(fetchTraceSpan.trace_id).not.toEqual(pageloadSpan.trace_id);
136+
137+
expect(sentryTrace).toBeDefined();
138+
expect(baggage).toBeDefined();
139+
140+
expect(extractTraceparentData(sentryTrace)).toEqual({
141+
traceId: fetchTraceSpan.trace_id,
142+
parentSpanId: httpClientSpan?.span_id,
143+
parentSampled: true,
144+
});
145+
146+
expect(parseBaggageHeader(baggage)).toEqual({
147+
'sentry-environment': 'production',
148+
'sentry-public_key': 'public',
149+
'sentry-sample_rand': `${pageloadSampleRand}`,
150+
'sentry-sample_rate': '1',
151+
'sentry-sampled': 'true',
152+
'sentry-trace_id': fetchTraceSpan.trace_id,
153+
'sentry-transaction': 'custom root span 2',
154+
});
155+
});
156+
});
157+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
integrations: [
8+
Sentry.browserTracingIntegration({
9+
linkPreviousTrace: 'in-memory',
10+
consistentTraceSampling: true,
11+
}),
12+
Sentry.spanStreamingIntegration(),
13+
],
14+
traceLifecycle: 'stream',
15+
tracePropagationTargets: ['sentry-test-external.io'],
16+
tracesSampleRate: 1,
17+
debug: true,
18+
sendClientReports: true,
19+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const btn1 = document.getElementById('btn1');
2+
3+
const btn2 = document.getElementById('btn2');
4+
5+
btn1.addEventListener('click', () => {
6+
Sentry.startNewTrace(() => {
7+
Sentry.startSpan({ name: 'custom root span 1', op: 'custom' }, () => {});
8+
});
9+
});
10+
11+
btn2.addEventListener('click', () => {
12+
Sentry.startNewTrace(() => {
13+
Sentry.startSpan({ name: 'custom root span 2', op: 'custom' }, async () => {
14+
await fetch('http://sentry-test-external.io');
15+
});
16+
});
17+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="sentry-trace" content="12345678901234567890123456789012-1234567890123456-0" />
6+
<meta
7+
name="baggage"
8+
content="sentry-trace_id=12345678901234567890123456789012,sentry-sample_rate=0.2,sentry-sampled=false,sentry-transaction=my-transaction,sentry-public_key=public,sentry-release=1.0.0,sentry-environment=prod,sentry-sample_rand=0.9"
9+
/>
10+
</head>
11+
<button id="btn1">Custom Trace</button>
12+
<button id="btn2">fetch request</button>
13+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { expect } from '@playwright/test';
2+
import type { ClientReport } from '@sentry/core';
3+
import { extractTraceparentData, parseBaggageHeader } from '@sentry/core';
4+
import type { SerializedStreamedSpan } from '@sentry/core/src';
5+
import { sentryTest } from '../../../../../../utils/fixtures';
6+
import {
7+
envelopeRequestParser,
8+
hidePage,
9+
shouldSkipTracingTest,
10+
waitForClientReportRequest,
11+
waitForTracingHeadersOnUrl,
12+
} from '../../../../../../utils/helpers';
13+
import { observeStreamedSpan } from '../../../../../../utils/spanUtils';
14+
15+
const metaTagSampleRand = 0.9;
16+
const metaTagSampleRate = 0.2;
17+
const metaTagTraceId = '12345678901234567890123456789012';
18+
19+
sentryTest.describe('When `consistentTraceSampling` is `true` and page contains <meta> tags', () => {
20+
sentryTest(
21+
'Continues negative sampling decision from meta tag across all traces and downstream propagations',
22+
async ({ getLocalTestUrl, page }) => {
23+
if (shouldSkipTracingTest()) {
24+
sentryTest.skip();
25+
}
26+
27+
const url = await getLocalTestUrl({ testDir: __dirname });
28+
29+
const spansReceived: SerializedStreamedSpan[] = [];
30+
observeStreamedSpan(page, span => {
31+
spansReceived.push(span);
32+
return false;
33+
});
34+
35+
const clientReportPromise = waitForClientReportRequest(page);
36+
37+
await sentryTest.step('Initial pageload', async () => {
38+
await page.goto(url);
39+
expect(spansReceived).toHaveLength(0);
40+
});
41+
42+
await sentryTest.step('Custom instrumented button click', async () => {
43+
await page.locator('#btn1').click();
44+
expect(spansReceived).toHaveLength(0);
45+
});
46+
47+
await sentryTest.step('Navigation', async () => {
48+
await page.goto(`${url}#foo`);
49+
expect(spansReceived).toHaveLength(0);
50+
});
51+
52+
await sentryTest.step('Make fetch request', async () => {
53+
const tracingHeadersPromise = waitForTracingHeadersOnUrl(page, 'http://sentry-test-external.io');
54+
55+
await page.locator('#btn2').click();
56+
const { baggage, sentryTrace } = await tracingHeadersPromise;
57+
58+
expect(sentryTrace).toBeDefined();
59+
expect(baggage).toBeDefined();
60+
61+
expect(extractTraceparentData(sentryTrace)).toEqual({
62+
traceId: expect.not.stringContaining(metaTagTraceId),
63+
parentSpanId: expect.stringMatching(/^[\da-f]{16}$/),
64+
parentSampled: false,
65+
});
66+
67+
expect(parseBaggageHeader(baggage)).toEqual({
68+
'sentry-environment': 'production',
69+
'sentry-public_key': 'public',
70+
'sentry-sample_rand': `${metaTagSampleRand}`,
71+
'sentry-sample_rate': `${metaTagSampleRate}`,
72+
'sentry-sampled': 'false',
73+
'sentry-trace_id': expect.not.stringContaining(metaTagTraceId),
74+
'sentry-transaction': 'custom root span 2',
75+
});
76+
77+
expect(spansReceived).toHaveLength(0);
78+
});
79+
80+
await sentryTest.step('Client report', async () => {
81+
await hidePage(page);
82+
const clientReport = envelopeRequestParser<ClientReport>(await clientReportPromise);
83+
expect(clientReport).toEqual({
84+
timestamp: expect.any(Number),
85+
discarded_events: [
86+
{
87+
category: 'transaction',
88+
quantity: 4,
89+
reason: 'sample_rate',
90+
},
91+
],
92+
});
93+
});
94+
95+
expect(spansReceived).toHaveLength(0);
96+
},
97+
);
98+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
integrations: [
8+
Sentry.browserTracingIntegration({
9+
linkPreviousTrace: 'session-storage',
10+
consistentTraceSampling: true,
11+
}),
12+
Sentry.spanStreamingIntegration(),
13+
],
14+
tracePropagationTargets: ['sentry-test-external.io'],
15+
tracesSampler: ({ inheritOrSampleWith }) => {
16+
return inheritOrSampleWith(0);
17+
},
18+
debug: true,
19+
sendClientReports: true,
20+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="sentry-trace" content="a2345678901234567890123456789012-1234567890123456-1" />
6+
<meta
7+
name="baggage"
8+
content="sentry-trace_id=a2345678901234567890123456789012,sentry-sample_rate=0.2,sentry-sampled=true,sentry-transaction=my-transaction,sentry-public_key=public,sentry-release=1.0.0,sentry-environment=prod,sentry-sample_rand=0.12"
9+
/>
10+
</head>
11+
<body>
12+
<h1>Another Page</h1>
13+
<a href="./page-2.html">Go To the next page</a>
14+
</body>
15+
</html>

0 commit comments

Comments
 (0)