Skip to content

Commit f3d3c73

Browse files
committed
test(browser): Add span streaming integration tests (#19581)
This PR adds browser integration to test testing span streaming: - Added test helpers: - `waitForStreamedSpan`: Returns a promise of a single matching span - `waitForStreamedSpans`: Returns a promise of all spans in an array whenever the callback returns true - `waitForStreamedSpanEnvelope`: Returns an entire streamed span (v2) envelope (including headers) - `observeStreamedSpan`: Can be used to observe sent span envelopes without blocking the test if no envelopes are sent (good for testing that spans are _not_ sent) - `getSpanOp`: Small helper to easily get the op of a span which we almost always need for the `waitFor*` function callbacks Added 50+ tests, mostly converted from transaction integration tests around spans from `browserTracingIntegration`: - tests asserting the entire span v2 envelope payloads of manually started, pageload and navigation span trees - tests for trace linking and trace lifetime - tests for spans coming from browserTracingIntegration (fetch, xhr, long animation frame, long tasks) Also, this PR fixes two bugs discovered through tests: - negatively sampled spans were still sent (because non-recording spans go through the same span life cycle) - cancelled spans received status `error` instead of `ok`. We want them to have status `ok` but an attribute detailing the cancellation reason. Lastly, I discovered a problem with timing data on fetch and XHR spans. Will try to fix as a follow-up. Tracked in #19613 ref #17836
1 parent 25b7283 commit f3d3c73

File tree

121 files changed

+4504
-6
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

121 files changed

+4504
-6
lines changed

.size-limit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ module.exports = [
148148
import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'),
149149
ignore: ['react/jsx-runtime'],
150150
gzip: true,
151-
limit: '45.1 KB',
151+
limit: '46 KB',
152152
},
153153
// Vue SDK (ESM)
154154
{
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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: [Sentry.spanStreamingIntegration()],
8+
tracesSampleRate: 1.0,
9+
debug: true,
10+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Sentry.startSpan({ name: 'test-span', op: 'test' }, () => {
2+
Sentry.startSpan({ name: 'test-child-span', op: 'test-child' }, () => {
3+
// noop
4+
});
5+
6+
const inactiveSpan = Sentry.startInactiveSpan({ name: 'test-inactive-span' });
7+
inactiveSpan.end();
8+
9+
Sentry.startSpanManual({ name: 'test-manual-span' }, span => {
10+
// noop
11+
span.end();
12+
});
13+
});
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import { expect } from '@playwright/test';
2+
import {
3+
SDK_VERSION,
4+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
5+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
6+
SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
7+
SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME,
8+
SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION,
9+
SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID,
10+
SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME,
11+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
12+
} from '@sentry/core';
13+
import { sentryTest } from '../../../../utils/fixtures';
14+
import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
15+
import { waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils';
16+
17+
sentryTest(
18+
'sends a streamed span envelope if spanStreamingIntegration is enabled',
19+
async ({ getLocalTestUrl, page }) => {
20+
sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
21+
22+
const spanEnvelopePromise = waitForStreamedSpanEnvelope(page);
23+
24+
const url = await getLocalTestUrl({ testDir: __dirname });
25+
await page.goto(url);
26+
27+
const spanEnvelope = await spanEnvelopePromise;
28+
29+
const envelopeHeader = spanEnvelope[0];
30+
const envelopeItem = spanEnvelope[1];
31+
const spans = envelopeItem[0][1].items;
32+
33+
expect(envelopeHeader).toEqual({
34+
sdk: {
35+
name: 'sentry.javascript.browser',
36+
version: SDK_VERSION,
37+
},
38+
sent_at: expect.any(String),
39+
trace: {
40+
environment: 'production',
41+
public_key: 'public',
42+
sample_rand: expect.any(String),
43+
sample_rate: '1',
44+
sampled: 'true',
45+
trace_id: expect.stringMatching(/^[\da-f]{32}$/),
46+
transaction: 'test-span',
47+
},
48+
});
49+
50+
const numericSampleRand = parseFloat(envelopeHeader.trace!.sample_rand!);
51+
const traceId = envelopeHeader.trace!.trace_id;
52+
53+
expect(Number.isNaN(numericSampleRand)).toBe(false);
54+
55+
expect(envelopeItem).toEqual([
56+
[
57+
{ content_type: 'application/vnd.sentry.items.span.v2+json', item_count: 4, type: 'span' },
58+
{
59+
items: expect.any(Array),
60+
},
61+
],
62+
]);
63+
64+
const segmentSpanId = spans.find(s => !!s.is_segment)?.span_id;
65+
expect(segmentSpanId).toBeDefined();
66+
67+
expect(spans).toEqual([
68+
{
69+
attributes: {
70+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: {
71+
type: 'string',
72+
value: 'test-child',
73+
},
74+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: {
75+
type: 'string',
76+
value: 'manual',
77+
},
78+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: {
79+
type: 'string',
80+
value: 'sentry.javascript.browser',
81+
},
82+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: {
83+
type: 'string',
84+
value: SDK_VERSION,
85+
},
86+
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: {
87+
type: 'string',
88+
value: segmentSpanId,
89+
},
90+
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: {
91+
type: 'string',
92+
value: 'test-span',
93+
},
94+
},
95+
end_timestamp: expect.any(Number),
96+
is_segment: false,
97+
name: 'test-child-span',
98+
parent_span_id: segmentSpanId,
99+
span_id: expect.stringMatching(/^[\da-f]{16}$/),
100+
start_timestamp: expect.any(Number),
101+
status: 'ok',
102+
trace_id: traceId,
103+
},
104+
{
105+
attributes: {
106+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: {
107+
type: 'string',
108+
value: 'manual',
109+
},
110+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: {
111+
type: 'string',
112+
value: 'sentry.javascript.browser',
113+
},
114+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: {
115+
type: 'string',
116+
value: SDK_VERSION,
117+
},
118+
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: {
119+
type: 'string',
120+
value: segmentSpanId,
121+
},
122+
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: {
123+
type: 'string',
124+
value: 'test-span',
125+
},
126+
},
127+
end_timestamp: expect.any(Number),
128+
is_segment: false,
129+
name: 'test-inactive-span',
130+
parent_span_id: segmentSpanId,
131+
span_id: expect.stringMatching(/^[\da-f]{16}$/),
132+
start_timestamp: expect.any(Number),
133+
status: 'ok',
134+
trace_id: traceId,
135+
},
136+
{
137+
attributes: {
138+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: {
139+
type: 'string',
140+
value: 'manual',
141+
},
142+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: {
143+
type: 'string',
144+
value: 'sentry.javascript.browser',
145+
},
146+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: {
147+
type: 'string',
148+
value: SDK_VERSION,
149+
},
150+
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: {
151+
type: 'string',
152+
value: segmentSpanId,
153+
},
154+
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: {
155+
type: 'string',
156+
value: 'test-span',
157+
},
158+
},
159+
end_timestamp: expect.any(Number),
160+
is_segment: false,
161+
name: 'test-manual-span',
162+
parent_span_id: segmentSpanId,
163+
span_id: expect.stringMatching(/^[\da-f]{16}$/),
164+
start_timestamp: expect.any(Number),
165+
status: 'ok',
166+
trace_id: traceId,
167+
},
168+
{
169+
attributes: {
170+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: {
171+
type: 'string',
172+
value: 'test',
173+
},
174+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: {
175+
type: 'string',
176+
value: 'manual',
177+
},
178+
[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: {
179+
type: 'integer',
180+
value: 1,
181+
},
182+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: {
183+
type: 'string',
184+
value: 'sentry.javascript.browser',
185+
},
186+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: {
187+
type: 'string',
188+
value: SDK_VERSION,
189+
},
190+
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: {
191+
type: 'string',
192+
value: segmentSpanId,
193+
},
194+
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: {
195+
type: 'string',
196+
value: 'test-span',
197+
},
198+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: {
199+
type: 'string',
200+
value: 'custom',
201+
},
202+
'sentry.span.source': {
203+
type: 'string',
204+
value: 'custom',
205+
},
206+
},
207+
end_timestamp: expect.any(Number),
208+
is_segment: true,
209+
name: 'test-span',
210+
span_id: segmentSpanId,
211+
start_timestamp: expect.any(Number),
212+
status: 'ok',
213+
trace_id: traceId,
214+
},
215+
]);
216+
},
217+
);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()],
8+
tracesSampleRate: 1,
9+
debug: true,
10+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
document.getElementById('go-background').addEventListener('click', () => {
2+
setTimeout(() => {
3+
Object.defineProperty(document, 'hidden', { value: true, writable: true });
4+
const ev = document.createEvent('Event');
5+
ev.initEvent('visibilitychange');
6+
document.dispatchEvent(ev);
7+
}, 250);
8+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<button id="go-background">New Tab</button>
8+
</body>
9+
</html>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../../utils/fixtures';
3+
import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
4+
import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils';
5+
6+
sentryTest('finishes streamed pageload span when the page goes background', async ({ getLocalTestUrl, page }) => {
7+
sentryTest.skip(shouldSkipTracingTest() || testingCdnBundle());
8+
const url = await getLocalTestUrl({ testDir: __dirname });
9+
const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload');
10+
11+
await page.goto(url);
12+
await page.locator('#go-background').click();
13+
const pageloadSpan = await pageloadSpanPromise;
14+
15+
// TODO: Is this what we want?
16+
expect(pageloadSpan.status).toBe('ok');
17+
expect(pageloadSpan.attributes?.['sentry.cancellation_reason']?.value).toBe('document.hidden');
18+
});
Lines changed: 19 additions & 0 deletions
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+
idleTimeout: 1000,
10+
_experiments: {
11+
enableHTTPTimings: true,
12+
},
13+
}),
14+
Sentry.spanStreamingIntegration(),
15+
],
16+
tracesSampleRate: 1,
17+
traceLifecycle: 'stream',
18+
debug: true,
19+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fetch('http://sentry-test-site.example/0').then(
2+
fetch('http://sentry-test-site.example/1').then(fetch('http://sentry-test-site.example/2')),
3+
);

0 commit comments

Comments
 (0)