Skip to content

Commit e2c7572

Browse files
committed
test(e2e): Port React Router framework E2E apps to span streaming
Removes the `traceLifecycle: 'static'` pin from `react-router-7-framework`, `react-router-8-framework` and `react-router-7-framework-spa`, and rewrites their specs against streamed span v2. Envelope-level metadata has no span equivalent, so the SDK assertions read the `sentry.sdk.*` and `sentry.environment` attributes instead, and the `runtime` tag assertions are dropped. The trace connection spec is keyed off the `sentry-trace` meta tag. Spans are buffered before they flush, so an earlier page load on `/` can still be arriving here, and matching the two halves independently paired spans from different loads. The document advertises its own trace, and the client span is selected by the parent it continues, so a pageload that failed to continue the trace still fails the test rather than passing vacuously.
1 parent d33d48c commit e2c7572

21 files changed

Lines changed: 657 additions & 1045 deletions

‎dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/app/entry.client.tsx‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { hydrateRoot } from 'react-dom/client';
44
import { HydratedRouter } from 'react-router/dom';
55

66
Sentry.init({
7-
traceLifecycle: 'static',
87
environment: 'qa', // dynamic sampling bias to keep transactions
98
// todo: get this from env
109
dsn: 'https://username@domain/123',
Lines changed: 29 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,46 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForTransaction } from '@sentry-internal/test-utils';
2+
import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
33
import { APP_NAME } from '../constants';
44

55
test.describe('client - navigation performance', () => {
6-
test('should update navigation transaction for dynamic routes', async ({ page }) => {
7-
const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
8-
return (
9-
transactionEvent.transaction === '/performance/with/:param' &&
10-
transactionEvent.contexts?.trace?.op === 'navigation'
11-
);
6+
test('should update navigation span for dynamic routes', async ({ page }) => {
7+
const navigationSpanPromise = waitForStreamedSpan(APP_NAME, span => {
8+
return span.name === '/performance/with/:param' && getSpanOp(span) === 'navigation' && span.is_segment;
129
});
1310

14-
const pageloadTxPromise = waitForTransaction(APP_NAME, async transactionEvent => {
15-
return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload';
11+
const pageloadSpanPromise = waitForStreamedSpan(APP_NAME, span => {
12+
return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment;
1613
});
1714

1815
await page.goto(`/performance`); // pageload
19-
await pageloadTxPromise;
16+
await pageloadSpanPromise;
2017
await page.getByRole('link', { name: 'With Param Page' }).click(); // navigation
2118

22-
const transaction = await txPromise;
19+
const span = await navigationSpanPromise;
2320

24-
expect(transaction).toMatchObject({
25-
contexts: {
26-
trace: {
27-
span_id: expect.any(String),
28-
trace_id: expect.any(String),
29-
data: {
30-
'sentry.origin': 'auto.navigation.react_router',
31-
'sentry.op': 'navigation',
32-
'sentry.segment.name.source': 'route',
33-
'url.template': '/performance/with/:param',
34-
'url.path': '/performance/with/sentry',
35-
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/sentry$/),
36-
},
37-
op: 'navigation',
38-
origin: 'auto.navigation.react_router',
39-
},
40-
},
41-
spans: expect.any(Array),
21+
expect(span).toMatchObject({
22+
span_id: expect.any(String),
23+
trace_id: expect.any(String),
4224
start_timestamp: expect.any(Number),
43-
timestamp: expect.any(Number),
44-
transaction: '/performance/with/:param',
45-
type: 'transaction',
46-
transaction_info: { source: 'route' },
47-
platform: 'javascript',
48-
request: {
49-
url: expect.stringContaining('/performance/with/sentry'),
50-
headers: expect.any(Object),
51-
},
52-
event_id: expect.any(String),
53-
environment: 'qa',
54-
sdk: {
55-
integrations: expect.arrayContaining([expect.any(String)]),
56-
name: 'sentry.javascript.react-router',
57-
version: expect.any(String),
58-
packages: [
59-
{ name: 'npm:@sentry/react-router', version: expect.any(String) },
60-
{ name: 'npm:@sentry/browser', version: expect.any(String) },
61-
],
25+
end_timestamp: expect.any(Number),
26+
is_segment: true,
27+
status: 'ok',
28+
});
29+
30+
expect(span.attributes).toMatchObject({
31+
'sentry.origin': { value: 'auto.navigation.react_router', type: 'string' },
32+
'sentry.op': { value: 'navigation', type: 'string' },
33+
'sentry.segment.name.source': { value: 'route', type: 'string' },
34+
'sentry.environment': { value: 'qa', type: 'string' },
35+
'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' },
36+
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
37+
'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' },
38+
'url.template': { value: '/performance/with/:param', type: 'string' },
39+
'url.path': { value: '/performance/with/sentry', type: 'string' },
40+
'url.full': {
41+
value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/sentry$/),
42+
type: 'string',
6243
},
63-
tags: { runtime: 'browser' },
6444
});
6545
});
6646
});
Lines changed: 40 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,63 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForTransaction } from '@sentry-internal/test-utils';
2+
import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
33
import { APP_NAME } from '../constants';
44

55
test.describe('client - pageload performance', () => {
6-
test('should send pageload transaction', async ({ page }) => {
7-
const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
8-
return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload';
6+
test('should send pageload span', async ({ page }) => {
7+
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
8+
return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment;
99
});
1010

1111
await page.goto(`/performance`);
1212
await page.getByRole('heading', { name: 'Performance Page' }).waitFor();
1313

14-
const transaction = await txPromise;
14+
const span = await spanPromise;
1515

16-
expect(transaction).toMatchObject({
17-
contexts: {
18-
trace: {
19-
span_id: expect.any(String),
20-
trace_id: expect.any(String),
21-
data: {
22-
'sentry.origin': 'auto.pageload.react_router',
23-
'sentry.op': 'pageload',
24-
'sentry.segment.name.source': 'route',
25-
'url.template': '/performance',
26-
'url.path': '/performance',
27-
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance$/),
28-
},
29-
op: 'pageload',
30-
origin: 'auto.pageload.react_router',
31-
},
32-
},
33-
spans: expect.any(Array),
16+
expect(span).toMatchObject({
17+
span_id: expect.any(String),
18+
trace_id: expect.any(String),
3419
start_timestamp: expect.any(Number),
35-
timestamp: expect.any(Number),
36-
transaction: '/performance',
37-
type: 'transaction',
38-
transaction_info: { source: 'route' },
39-
measurements: expect.any(Object),
40-
platform: 'javascript',
41-
request: {
42-
url: expect.stringContaining('/performance'),
43-
headers: expect.any(Object),
44-
},
45-
event_id: expect.any(String),
46-
environment: 'qa',
47-
sdk: {
48-
integrations: expect.arrayContaining([expect.any(String)]),
49-
name: 'sentry.javascript.react-router',
50-
version: expect.any(String),
51-
packages: [
52-
{ name: 'npm:@sentry/react-router', version: expect.any(String) },
53-
{ name: 'npm:@sentry/browser', version: expect.any(String) },
54-
],
55-
},
56-
tags: { runtime: 'browser' },
20+
end_timestamp: expect.any(Number),
21+
is_segment: true,
22+
status: 'ok',
23+
});
24+
25+
expect(span.attributes).toMatchObject({
26+
'sentry.origin': { value: 'auto.pageload.react_router', type: 'string' },
27+
'sentry.op': { value: 'pageload', type: 'string' },
28+
'sentry.segment.name.source': { value: 'route', type: 'string' },
29+
'sentry.environment': { value: 'qa', type: 'string' },
30+
'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' },
31+
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
32+
'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' },
33+
'url.template': { value: '/performance', type: 'string' },
34+
'url.path': { value: '/performance', type: 'string' },
35+
'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance$/), type: 'string' },
5736
});
5837
});
5938

60-
test('should update pageload transaction for dynamic routes', async ({ page }) => {
61-
const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
62-
return (
63-
transactionEvent.transaction === '/performance/with/:param' &&
64-
transactionEvent.contexts?.trace?.op === 'pageload'
65-
);
39+
test('should update pageload span for dynamic routes', async ({ page }) => {
40+
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
41+
return span.name === '/performance/with/:param' && getSpanOp(span) === 'pageload' && span.is_segment;
6642
});
6743

6844
await page.goto(`/performance/with/sentry`);
6945
await page.getByRole('heading', { name: 'Dynamic Parameter Page' }).waitFor();
7046

71-
const transaction = await txPromise;
72-
73-
expect(transaction).toMatchObject({
74-
contexts: {
75-
trace: {
76-
span_id: expect.any(String),
77-
trace_id: expect.any(String),
78-
data: {
79-
'sentry.origin': 'auto.pageload.react_router',
80-
'sentry.op': 'pageload',
81-
'sentry.segment.name.source': 'route',
82-
'url.template': '/performance/with/:param',
83-
'url.path': '/performance/with/sentry',
84-
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/sentry$/),
85-
},
86-
op: 'pageload',
87-
origin: 'auto.pageload.react_router',
88-
},
89-
},
90-
spans: expect.any(Array),
91-
start_timestamp: expect.any(Number),
92-
timestamp: expect.any(Number),
93-
transaction: '/performance/with/:param',
94-
type: 'transaction',
95-
transaction_info: { source: 'route' },
96-
measurements: expect.any(Object),
97-
platform: 'javascript',
98-
request: {
99-
url: expect.stringContaining('/performance/with/sentry'),
100-
headers: expect.any(Object),
101-
},
102-
event_id: expect.any(String),
103-
environment: 'qa',
104-
sdk: {
105-
integrations: expect.arrayContaining([expect.any(String)]),
106-
name: 'sentry.javascript.react-router',
107-
version: expect.any(String),
108-
packages: [
109-
{ name: 'npm:@sentry/react-router', version: expect.any(String) },
110-
{ name: 'npm:@sentry/browser', version: expect.any(String) },
111-
],
47+
const span = await spanPromise;
48+
49+
expect(span.attributes).toMatchObject({
50+
'sentry.origin': { value: 'auto.pageload.react_router', type: 'string' },
51+
'sentry.op': { value: 'pageload', type: 'string' },
52+
'sentry.segment.name.source': { value: 'route', type: 'string' },
53+
'sentry.environment': { value: 'qa', type: 'string' },
54+
'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' },
55+
'url.template': { value: '/performance/with/:param', type: 'string' },
56+
'url.path': { value: '/performance/with/sentry', type: 'string' },
57+
'url.full': {
58+
value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/sentry$/),
59+
type: 'string',
11260
},
113-
tags: { runtime: 'browser' },
11461
});
11562
});
11663
});

‎dev-packages/e2e-tests/test-applications/react-router-7-framework/app/entry.client.tsx‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { hydrateRoot } from 'react-dom/client';
44
import { HydratedRouter } from 'react-router/dom';
55

66
Sentry.init({
7-
traceLifecycle: 'static',
87
environment: 'qa', // dynamic sampling bias to keep transactions
98
// todo: get this from env
109
dsn: 'https://username@domain/123',

‎dev-packages/e2e-tests/test-applications/react-router-7-framework/instrument.mjs‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as Sentry from '@sentry/react-router';
22

33
Sentry.init({
4-
traceLifecycle: 'static',
54
dsn: 'https://username@domain/123',
65
environment: 'qa', // dynamic sampling bias to keep transactions
76
tracesSampleRate: 1.0,
Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForTransaction } from '@sentry-internal/test-utils';
2+
import type { SerializedStreamedSpan } from '@sentry-internal/test-utils';
3+
import { getSpanOp, waitForStreamedSpan, waitForStreamedSpans } from '@sentry-internal/test-utils';
34
import { APP_NAME } from '../constants';
45

5-
test.describe('low-quality transaction filter', () => {
6-
test('does not send a server transaction for /__manifest? requests', async ({ page }) => {
7-
const serverTxns: Array<{ contexts?: { trace?: { data?: Record<string, unknown> } } }> = [];
6+
test.describe('low-quality span filter', () => {
7+
test('does not send a server span for /__manifest? requests', async ({ page }) => {
8+
const streamedSpans: SerializedStreamedSpan[] = [];
89

9-
const navigationPromise = waitForTransaction(APP_NAME, async transactionEvent => {
10-
return (
11-
transactionEvent.transaction === '/performance/ssr' && transactionEvent.contexts?.trace?.op === 'navigation'
12-
);
10+
const navigationPromise = waitForStreamedSpan(APP_NAME, span => {
11+
return span.name === '/performance/ssr' && getSpanOp(span) === 'navigation' && span.is_segment;
1312
});
1413

15-
waitForTransaction(APP_NAME, async evt => {
16-
serverTxns.push(evt);
14+
waitForStreamedSpans(APP_NAME, spans => {
15+
streamedSpans.push(...spans);
1716
return false;
1817
});
1918

@@ -23,12 +22,13 @@ test.describe('low-quality transaction filter', () => {
2322

2423
await navigationPromise;
2524

26-
// Force the server to flush any in-flight transactions before we assert
25+
// Force the server to flush any in-flight spans before we assert
2726
await page.evaluate(() => fetch('/__sentry-flush'));
2827

29-
const targetIsManifest = (t: (typeof serverTxns)[number]) =>
30-
typeof t.contexts?.trace?.data?.['url.path'] === 'string' &&
31-
(t.contexts.trace.data['url.path'] as string).includes('/__manifest');
32-
expect(serverTxns.some(targetIsManifest)).toBe(false);
28+
const targetIsManifest = (span: SerializedStreamedSpan) => {
29+
const urlPath = span.attributes['url.path']?.value;
30+
return typeof urlPath === 'string' && urlPath.includes('/__manifest');
31+
};
32+
expect(streamedSpans.some(targetIsManifest)).toBe(false);
3333
});
3434
});
Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,33 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForTransaction } from '@sentry-internal/test-utils';
2+
import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
33
import { APP_NAME } from '../constants';
44

55
test.describe('server - middleware', () => {
6-
test('should send middleware transaction on pageload', async ({ page }) => {
7-
const serverTxPromise = waitForTransaction(APP_NAME, async transactionEvent => {
8-
return transactionEvent.transaction === 'GET /performance/with-middleware';
9-
});
10-
11-
const pageloadTxPromise = waitForTransaction(APP_NAME, async transactionEvent => {
12-
return transactionEvent.transaction === '/performance/with-middleware';
13-
});
14-
15-
const customMiddlewareTxPromise = waitForTransaction(APP_NAME, async transactionEvent => {
16-
return transactionEvent.transaction === 'authMiddleware';
6+
test('should send middleware span on pageload', async ({ page }) => {
7+
const serverSpansPromise = collectStreamedSpans(
8+
APP_NAME,
9+
spansOfTrace =>
10+
spansOfTrace.some(span => span.name === 'GET /performance/with-middleware' && span.is_segment) &&
11+
spansOfTrace.some(span => span.name === 'authMiddleware'),
12+
);
13+
14+
const pageloadSpanPromise = waitForStreamedSpan(APP_NAME, span => {
15+
return span.name === '/performance/with-middleware' && getSpanOp(span) === 'pageload' && span.is_segment;
1716
});
1817

1918
await page.goto(`/performance/with-middleware`);
2019

21-
const serverTx = await serverTxPromise;
22-
const pageloadTx = await pageloadTxPromise;
23-
const customMiddlewareTx = await customMiddlewareTxPromise;
20+
const serverSpans = await serverSpansPromise;
21+
const pageloadSpan = await pageloadSpanPromise;
2422

25-
const traceIds = {
26-
server: serverTx?.contexts?.trace?.trace_id,
27-
pageload: pageloadTx?.contexts?.trace?.trace_id,
28-
customMiddleware: customMiddlewareTx?.contexts?.trace?.trace_id,
29-
};
23+
const serverSpan = serverSpans.find(span => span.is_segment)!;
24+
const customMiddlewareSpan = serverSpans.find(span => span.name === 'authMiddleware')!;
3025

31-
expect(pageloadTx).toBeDefined();
32-
expect(customMiddlewareTx).toBeDefined();
26+
expect(pageloadSpan).toBeDefined();
27+
expect(customMiddlewareSpan).toBeDefined();
3328

34-
// Assert that all transactions belong to the same trace
35-
expect(traceIds.server).toBe(traceIds.pageload);
36-
expect(traceIds.server).toBe(traceIds.customMiddleware);
29+
// Assert that all spans belong to the same trace
30+
expect(serverSpan.trace_id).toBe(pageloadSpan.trace_id);
31+
expect(serverSpan.trace_id).toBe(customMiddlewareSpan.trace_id);
3732
});
3833
});

0 commit comments

Comments
 (0)