Skip to content

Commit b188ce7

Browse files
msonnbcodex
andcommitted
test(browser): retain static request and metric counterparts
Co-Authored-By: GPT-6 <codex@openai.com>
1 parent 2fa8f35 commit b188ce7

21 files changed

Lines changed: 685 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
(() => {})();
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
p {
2+
color: red;
3+
text-align: center;
4+
}
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+
traceLifecycle: 'static',
7+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
8+
integrations: [Sentry.browserTracingIntegration()],
9+
tracesSampleRate: 1,
10+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<img src="https://sentry-test-site.example/path/to/image.svg" />
8+
<script src="https://sentry-test-site.example/path/to/script.js"></script>
9+
<link href="https://sentry-test-site.example/path/to/style.css" type="text/css" rel="stylesheet" />
10+
<span>Rendered</span>
11+
</body>
12+
</html>
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes';
2+
import type { Route } from '@playwright/test';
3+
import { expect } from '@playwright/test';
4+
import { type Event } from '@sentry/core';
5+
import { sentryTest } from '../../../../utils/fixtures';
6+
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
7+
8+
sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestUrl, page, browserName }) => {
9+
if (shouldSkipTracingTest()) {
10+
sentryTest.skip();
11+
}
12+
13+
const isWebkitRun = browserName === 'webkit';
14+
15+
// Intercepting asset requests to avoid network-related flakiness and random retries (on Firefox).
16+
await page.route('https://sentry-test-site.example/path/to/image.svg', (route: Route) =>
17+
route.fulfill({
18+
path: `${__dirname}/assets/image.svg`,
19+
headers: {
20+
'Timing-Allow-Origin': '*',
21+
'Content-Type': 'image/svg+xml',
22+
},
23+
}),
24+
);
25+
await page.route('https://sentry-test-site.example/path/to/script.js', (route: Route) =>
26+
route.fulfill({
27+
path: `${__dirname}/assets/script.js`,
28+
headers: {
29+
'Timing-Allow-Origin': '*',
30+
'Content-Type': 'application/javascript',
31+
},
32+
}),
33+
);
34+
await page.route('https://sentry-test-site.example/path/to/style.css', (route: Route) =>
35+
route.fulfill({
36+
path: `${__dirname}/assets/style.css`,
37+
headers: {
38+
'Timing-Allow-Origin': '*',
39+
'Content-Type': 'text/css',
40+
},
41+
}),
42+
);
43+
44+
const url = await getLocalTestUrl({ testDir: __dirname });
45+
46+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
47+
const resourceSpans = eventData.spans?.filter(({ op }) => op?.startsWith('resource'));
48+
49+
const scriptSpans = resourceSpans?.filter(({ op }) => op === 'resource.script');
50+
const linkSpan = resourceSpans?.filter(({ op }) => op === 'resource.link')[0];
51+
const imgSpan = resourceSpans?.filter(({ op }) => op === 'resource.img')[0];
52+
53+
const spanId = eventData.contexts?.trace?.span_id;
54+
const traceId = eventData.contexts?.trace?.trace_id;
55+
56+
expect(spanId).toBeDefined();
57+
expect(traceId).toBeDefined();
58+
59+
const hasCdnBundle = (process.env.PW_BUNDLE || '').startsWith('bundle');
60+
61+
const expectedScripts = ['/init.bundle.js', 'https://sentry-test-site.example/path/to/script.js'];
62+
if (hasCdnBundle) {
63+
expectedScripts.unshift('/cdn.bundle.js');
64+
}
65+
66+
expect(scriptSpans?.map(({ description }) => description).sort()).toEqual(expectedScripts);
67+
expect(scriptSpans?.map(({ parent_span_id }) => parent_span_id)).toEqual(expectedScripts.map(() => spanId));
68+
69+
// The init bundle script is served from the test origin: its description is origin-relative,
70+
// but `url.full` retains the full absolute URL (needed for span description inference).
71+
const sameOriginScriptSpan = scriptSpans?.find(({ description }) => description === '/init.bundle.js');
72+
expect(sameOriginScriptSpan?.data?.['http.request.same_origin']).toBe(true);
73+
expect(sameOriginScriptSpan?.data?.['url.full']).toMatch(/^https?:\/\/.+\/init\.bundle\.js$/);
74+
75+
const customScriptSpan = scriptSpans?.find(
76+
({ description }) => description === 'https://sentry-test-site.example/path/to/script.js',
77+
);
78+
79+
expect(imgSpan).toEqual({
80+
data: {
81+
'http.response.body.decoded_size': expect.any(Number),
82+
'http.response.body.size': expect.any(Number),
83+
'http.response.size': expect.any(Number),
84+
'http.request.connect_start': expect.any(Number),
85+
'http.request.connection_end': expect.any(Number),
86+
'http.request.domain_lookup_end': expect.any(Number),
87+
'http.request.domain_lookup_start': expect.any(Number),
88+
'http.request.fetch_start': expect.any(Number),
89+
'http.request.redirect_end': expect.any(Number),
90+
'http.request.redirect_start': expect.any(Number),
91+
'http.request.request_start': expect.any(Number),
92+
'http.request.secure_connection_start': expect.any(Number),
93+
'http.request.worker_start': expect.any(Number),
94+
'http.request.response_end': expect.any(Number),
95+
'http.request.response_start': expect.any(Number),
96+
'http.request.time_to_first_byte': expect.any(Number),
97+
'network.protocol.name': '',
98+
'network.protocol.version': 'unknown',
99+
[SENTRY_OP]: 'resource.img',
100+
[SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
101+
'server.address': 'sentry-test-site.example',
102+
'url.domain': 'sentry-test-site.example',
103+
'http.request.same_origin': false,
104+
'url.scheme': 'https',
105+
'url.full': 'https://sentry-test-site.example/path/to/image.svg',
106+
// WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status
107+
// or render blocking status.
108+
'http.response_delivery_type': '',
109+
...(!isWebkitRun && {
110+
'http.response.status_code': expect.any(Number),
111+
'resource.render_blocking_status': 'non-blocking',
112+
}),
113+
},
114+
description: 'https://sentry-test-site.example/path/to/image.svg',
115+
op: 'resource.img',
116+
origin: 'auto.resource.browser.metrics',
117+
status: 'ok',
118+
parent_span_id: spanId,
119+
span_id: expect.stringMatching(/^[a-f\d]{16}$/),
120+
start_timestamp: expect.any(Number),
121+
timestamp: expect.any(Number),
122+
trace_id: traceId,
123+
});
124+
125+
// range check: TTFB is reasonably <10 seconds, which is really a check that we report it in
126+
// seconds rather than milliseconds. WebKit resolves these intercepted routes without measurable
127+
// delay, so only the other engines are held to a non-zero value.
128+
const imgSpanTtfb = imgSpan?.data['http.request.time_to_first_byte'];
129+
expect(imgSpanTtfb).toBeGreaterThan(isWebkitRun ? -1 : 0);
130+
expect(imgSpanTtfb).toBeLessThan(10);
131+
132+
expect(linkSpan).toEqual({
133+
data: {
134+
'http.response.body.decoded_size': expect.any(Number),
135+
'http.response.body.size': expect.any(Number),
136+
'http.response.size': expect.any(Number),
137+
'http.request.connect_start': expect.any(Number),
138+
'http.request.connection_end': expect.any(Number),
139+
'http.request.domain_lookup_end': expect.any(Number),
140+
'http.request.domain_lookup_start': expect.any(Number),
141+
'http.request.fetch_start': expect.any(Number),
142+
'http.request.redirect_end': expect.any(Number),
143+
'http.request.redirect_start': expect.any(Number),
144+
'http.request.request_start': expect.any(Number),
145+
'http.request.secure_connection_start': expect.any(Number),
146+
'http.request.worker_start': expect.any(Number),
147+
'http.request.response_end': expect.any(Number),
148+
'http.request.response_start': expect.any(Number),
149+
'http.request.time_to_first_byte': expect.any(Number),
150+
'network.protocol.name': '',
151+
'network.protocol.version': 'unknown',
152+
[SENTRY_OP]: 'resource.link',
153+
[SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
154+
'server.address': 'sentry-test-site.example',
155+
'url.domain': 'sentry-test-site.example',
156+
'http.request.same_origin': false,
157+
'url.scheme': 'https',
158+
'url.full': 'https://sentry-test-site.example/path/to/style.css',
159+
// WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status
160+
// or render blocking status.
161+
'http.response_delivery_type': '',
162+
...(!isWebkitRun && {
163+
'http.response.status_code': expect.any(Number),
164+
'resource.render_blocking_status': 'non-blocking',
165+
}),
166+
},
167+
description: 'https://sentry-test-site.example/path/to/style.css',
168+
op: 'resource.link',
169+
origin: 'auto.resource.browser.metrics',
170+
status: 'ok',
171+
parent_span_id: spanId,
172+
span_id: expect.stringMatching(/^[a-f\d]{16}$/),
173+
start_timestamp: expect.any(Number),
174+
timestamp: expect.any(Number),
175+
trace_id: traceId,
176+
});
177+
178+
expect(customScriptSpan).toEqual({
179+
data: {
180+
'http.response.body.decoded_size': expect.any(Number),
181+
'http.response.body.size': expect.any(Number),
182+
'http.response.size': expect.any(Number),
183+
'http.request.connection_end': expect.any(Number),
184+
'http.request.connect_start': expect.any(Number),
185+
'http.request.domain_lookup_end': expect.any(Number),
186+
'http.request.domain_lookup_start': expect.any(Number),
187+
'http.request.fetch_start': expect.any(Number),
188+
'http.request.redirect_end': expect.any(Number),
189+
'http.request.redirect_start': expect.any(Number),
190+
'http.request.request_start': expect.any(Number),
191+
'http.request.secure_connection_start': expect.any(Number),
192+
'http.request.worker_start': expect.any(Number),
193+
'http.request.response_end': expect.any(Number),
194+
'http.request.response_start': expect.any(Number),
195+
'http.request.time_to_first_byte': expect.any(Number),
196+
'network.protocol.name': '',
197+
'network.protocol.version': 'unknown',
198+
'sentry.op': 'resource.script',
199+
'sentry.origin': 'auto.resource.browser.metrics',
200+
'server.address': 'sentry-test-site.example',
201+
'url.domain': 'sentry-test-site.example',
202+
'http.request.same_origin': false,
203+
'url.scheme': 'https',
204+
'url.full': 'https://sentry-test-site.example/path/to/script.js',
205+
// WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status
206+
// or render blocking status.
207+
'http.response_delivery_type': '',
208+
...(!isWebkitRun && {
209+
'http.response.status_code': expect.any(Number),
210+
'resource.render_blocking_status': 'non-blocking',
211+
}),
212+
},
213+
description: 'https://sentry-test-site.example/path/to/script.js',
214+
op: 'resource.script',
215+
origin: 'auto.resource.browser.metrics',
216+
status: 'ok',
217+
parent_span_id: spanId,
218+
span_id: expect.stringMatching(/^[a-f\d]{16}$/),
219+
start_timestamp: expect.any(Number),
220+
timestamp: expect.any(Number),
221+
trace_id: traceId,
222+
});
223+
});
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+
traceLifecycle: 'static',
7+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
8+
integrations: [Sentry.browserTracingIntegration()],
9+
tracesSampleRate: 1,
10+
});
Lines changed: 9 additions & 0 deletions
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+
<div>Rendered</div>
8+
</body>
9+
</html>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/core';
3+
import { sentryTest } from '../../../../utils/fixtures';
4+
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
5+
6+
sentryTest('should capture TTFB vital.', async ({ getLocalTestUrl, page }) => {
7+
if (shouldSkipTracingTest()) {
8+
sentryTest.skip();
9+
}
10+
11+
const url = await getLocalTestUrl({ testDir: __dirname });
12+
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
13+
14+
expect(eventData.measurements).toBeDefined();
15+
16+
// If responseStart === 0, ttfb is not reported
17+
// This seems to happen somewhat randomly, so we just ignore this in that case
18+
const responseStart = await page.evaluate("performance.getEntriesByType('navigation')[0].responseStart;");
19+
if (responseStart !== 0) {
20+
expect(eventData.measurements?.ttfb?.value).toBeDefined();
21+
}
22+
23+
expect(eventData.measurements?.['ttfb.requestTime']?.value).toBeDefined();
24+
});
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+
traceLifecycle: 'static',
7+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
8+
integrations: [Sentry.browserTracingIntegration()],
9+
tracesSampleRate: 1,
10+
});

0 commit comments

Comments
 (0)