Skip to content

Commit 035ea0c

Browse files
committed
test(e2e): Port React Router data router E2E apps to span streaming
Removes the `traceLifecycle: 'static'` pin from `react-create-browser-router`, `react-create-hash-router` and `react-create-memory-router`, and rewrites their specs against streamed span v2. Two attribute groups moved under streaming and the assertions follow them: the navigator data is now `device.memory.estimated_capacity`, `device.processor_count` and `network.connection.effective_type`, and the pageload timings are `browser.performance.*`. LCP is streamed as its own `ui.webvital.lcp` span once the page hides, so it is no longer asserted on the pageload span.
1 parent 37956c8 commit 035ea0c

9 files changed

Lines changed: 581 additions & 816 deletions

File tree

dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import User from './pages/User';
1515
const replay = Sentry.replayIntegration();
1616

1717
Sentry.init({
18-
traceLifecycle: 'static',
1918
// environment: 'qa', // dynamic sampling bias to keep transactions
2019
dsn: process.env.REACT_APP_E2E_TEST_DSN,
2120
integrations: [
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { expect, test } from '@playwright/test';
2+
import {
3+
collectStreamedSpans,
4+
getSpanOp,
5+
waitForStreamedSpan,
6+
waitForStreamedSpans,
7+
} from '@sentry-internal/test-utils';
8+
9+
const PREVIOUS_TRACE_LINK = [
10+
{
11+
attributes: {
12+
'sentry.link.type': { value: 'previous_trace', type: 'string' },
13+
},
14+
sampled: true,
15+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
16+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
17+
},
18+
];
19+
20+
test('Captures a pageload span', async ({ page }) => {
21+
const spanPromise = waitForStreamedSpan('react-create-browser-router', span => {
22+
return getSpanOp(span) === 'pageload' && span.is_segment;
23+
});
24+
25+
await page.goto('/');
26+
27+
const span = await spanPromise;
28+
29+
expect(span.name).toBe('/');
30+
expect(span.status).toBe('ok');
31+
expect(span.span_id).toMatch(/[a-f0-9]{16}/);
32+
expect(span.trace_id).toMatch(/[a-f0-9]{32}/);
33+
34+
expect(span.attributes).toMatchObject({
35+
'device.memory.estimated_capacity': { value: expect.any(Number), type: expect.any(String) },
36+
'device.processor_count': { value: expect.any(Number), type: 'integer' },
37+
'network.connection.effective_type': { value: expect.any(String), type: 'string' },
38+
'sentry.idle_span_finish_reason': { value: 'idleTimeout', type: 'string' },
39+
'sentry.op': { value: 'pageload', type: 'string' },
40+
'sentry.origin': { value: 'auto.pageload.react.reactrouter_v6', type: 'string' },
41+
'sentry.sample_rate': { value: 1, type: 'integer' },
42+
'sentry.segment.name.source': { value: 'route', type: 'string' },
43+
'url.template': { value: '/', type: 'string' },
44+
'url.path': { value: '/', type: 'string' },
45+
'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), type: 'string' },
46+
});
47+
});
48+
49+
test('Captures a navigation span', async ({ page }) => {
50+
const spansPromise = waitForStreamedSpans('react-create-browser-router', spans => {
51+
return spans.some(span => getSpanOp(span) === 'navigation' && span.is_segment);
52+
});
53+
54+
await page.goto('/');
55+
const linkElement = page.locator('id=navigation');
56+
await linkElement.click();
57+
58+
const spans = await spansPromise;
59+
const navigationSpan = spans.find(span => span.is_segment)!;
60+
61+
expect(navigationSpan.name).toBe('/user/:id');
62+
expect(navigationSpan.status).toBe('ok');
63+
expect(navigationSpan.span_id).toMatch(/[a-f0-9]{16}/);
64+
expect(navigationSpan.trace_id).toMatch(/[a-f0-9]{32}/);
65+
66+
expect(navigationSpan.attributes).toMatchObject({
67+
'sentry.idle_span_finish_reason': { value: 'idleTimeout', type: 'string' },
68+
'sentry.op': { value: 'navigation', type: 'string' },
69+
'sentry.origin': { value: 'auto.navigation.react.reactrouter_v6', type: 'string' },
70+
'sentry.sample_rate': { value: 1, type: 'integer' },
71+
'sentry.segment.name.source': { value: 'route', type: 'string' },
72+
'url.template': { value: '/user/:id', type: 'string' },
73+
'url.path': { value: '/user/5', type: 'string' },
74+
'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/), type: 'string' },
75+
});
76+
77+
expect(navigationSpan.links).toEqual(PREVIOUS_TRACE_LINK);
78+
79+
// Filter out favicon spans which may or may not be present depending on the browser version
80+
const childSpans = spans.filter(
81+
span => !span.is_segment && !(span.attributes['url.full']?.value as string | undefined)?.includes('favicon'),
82+
);
83+
expect(childSpans).toEqual([]);
84+
});
85+
86+
test('Captures a lazy pageload span', async ({ page }) => {
87+
const spansPromise = collectStreamedSpans('react-create-browser-router', spans => {
88+
return spans.some(span => getSpanOp(span) === 'pageload' && span.is_segment);
89+
});
90+
91+
await page.goto('/lazy-loaded-user/5/foo');
92+
93+
const spans = await spansPromise;
94+
const pageloadSpan = spans.find(span => getSpanOp(span) === 'pageload' && span.is_segment)!;
95+
96+
expect(pageloadSpan.name).toBe('/lazy-loaded-user/:id/:innerId');
97+
expect(pageloadSpan.status).toBe('ok');
98+
99+
expect(pageloadSpan.attributes).toMatchObject({
100+
'sentry.idle_span_finish_reason': { value: 'idleTimeout', type: 'string' },
101+
'sentry.op': { value: 'pageload', type: 'string' },
102+
'sentry.origin': { value: 'auto.pageload.react.reactrouter_v6', type: 'string' },
103+
'sentry.sample_rate': { value: 1, type: 'integer' },
104+
'sentry.segment.name.source': { value: 'route', type: 'string' },
105+
'url.template': { value: '/lazy-loaded-user/:id/:innerId', type: 'string' },
106+
'url.path': { value: '/lazy-loaded-user/5/foo', type: 'string' },
107+
'url.full': {
108+
value: expect.stringMatching(/^https?:\/\/localhost:\d+\/lazy-loaded-user\/5\/foo$/),
109+
type: 'string',
110+
},
111+
});
112+
113+
expect(await page.innerText('id=content')).toContain('I am a lazy loaded user');
114+
115+
// One span for the outer lazy route, one for the inner one
116+
const resourceSpans = spans.filter(
117+
span =>
118+
span.trace_id === pageloadSpan.trace_id &&
119+
getSpanOp(span) === 'resource.script' &&
120+
span.attributes['sentry.origin']?.value === 'auto.resource.browser.metrics',
121+
);
122+
expect(resourceSpans.length).toBeGreaterThanOrEqual(2);
123+
});
124+
125+
test('Captures a lazy navigation span', async ({ page }) => {
126+
const spansPromise = collectStreamedSpans('react-create-browser-router', spans => {
127+
return spans.some(span => getSpanOp(span) === 'navigation' && span.is_segment);
128+
});
129+
130+
await page.goto('/');
131+
const linkElement = page.locator('id=lazy-navigation');
132+
await linkElement.click();
133+
134+
const spans = await spansPromise;
135+
const navigationSpan = spans.find(span => getSpanOp(span) === 'navigation' && span.is_segment)!;
136+
137+
expect(navigationSpan.name).toBe('/lazy-loaded-user/:id/:innerId');
138+
expect(navigationSpan.status).toBe('ok');
139+
140+
expect(navigationSpan.attributes).toMatchObject({
141+
'sentry.idle_span_finish_reason': { value: 'idleTimeout', type: 'string' },
142+
'sentry.op': { value: 'navigation', type: 'string' },
143+
'sentry.origin': { value: 'auto.navigation.react.reactrouter_v6', type: 'string' },
144+
'sentry.sample_rate': { value: 1, type: 'integer' },
145+
'sentry.segment.name.source': { value: 'route', type: 'string' },
146+
'url.template': { value: '/lazy-loaded-user/:id/:innerId', type: 'string' },
147+
'url.path': { value: '/lazy-loaded-user/5/foo', type: 'string' },
148+
'url.full': {
149+
value: expect.stringMatching(/^https?:\/\/localhost:\d+\/lazy-loaded-user\/5\/foo$/),
150+
type: 'string',
151+
},
152+
});
153+
154+
expect(navigationSpan.links).toEqual(PREVIOUS_TRACE_LINK);
155+
156+
expect(await page.innerText('id=content')).toContain('I am a lazy loaded user');
157+
158+
// The pageload of `/` is its own trace, so the lazy-route chunks are scoped to the navigation
159+
// trace. One span for the outer lazy route, one for the inner one.
160+
const resourceSpans = spans.filter(
161+
span =>
162+
span.trace_id === navigationSpan.trace_id &&
163+
getSpanOp(span) === 'resource.script' &&
164+
span.attributes['sentry.origin']?.value === 'auto.resource.browser.metrics',
165+
);
166+
expect(resourceSpans.length).toBeGreaterThanOrEqual(2);
167+
});

dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts

Lines changed: 0 additions & 218 deletions
This file was deleted.

0 commit comments

Comments
 (0)