Skip to content

Commit 3ad740c

Browse files
committed
test(e2e): Port the remix-hydrogen E2E app to span streaming
Drops the `traceLifecycle: 'static'` pin and rewrites the specs to assert on streamed spans.
1 parent 8b99a06 commit 3ad740c

6 files changed

Lines changed: 64 additions & 89 deletions

File tree

dev-packages/e2e-tests/test-applications/remix-hydrogen/app/entry.client.tsx

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

77
Sentry.init({
8-
traceLifecycle: 'static',
98
environment: 'qa', // dynamic sampling bias to keep transactions
109
// Could not find a working way to set the DSN in the browser side from the environment variables
1110
dsn: 'https://public@dsn.ingest.sentry.io/1337',

dev-packages/e2e-tests/test-applications/remix-hydrogen/app/functions/_middleware.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import * as build from '../build/server';
99
export const onRequest = [
1010
(context: EventPluginContext<any, any, any, any>) =>
1111
sentryPagesPlugin({
12-
traceLifecycle: 'static',
1312
dsn: context.env.E2E_TEST_DSN,
1413
tracesSampleRate: 1.0,
1514
})(context),

dev-packages/e2e-tests/test-applications/remix-hydrogen/server.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ export default {
3636
return wrapRequestHandler(
3737
{
3838
options: {
39-
traceLifecycle: 'static',
4039
environment: 'qa', // dynamic sampling bias to keep transactions
4140
dsn: 'https://public@dsn.ingest.sentry.io/1337',
4241
tracesSampleRate: 1.0,

dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/client-errors.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
2+
import { getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';
33

44
test('Sends a client-side exception to Sentry', async ({ page }) => {
5-
// The pageload transaction only completes once the client SDK and Remix have hydrated.
5+
// The pageload span only completes once the client SDK and Remix have hydrated.
66
// Awaiting it before clicking guarantees the button's onClick handler is attached — a click
77
// that lands before hydration would do nothing, and the exception would never be captured.
8-
const pageloadTransactionPromise = waitForTransaction('remix-hydrogen', transactionEvent => {
9-
return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/';
8+
const pageloadSpanPromise = waitForStreamedSpan('remix-hydrogen', span => {
9+
return getSpanOp(span) === 'pageload' && span.is_segment && span.name === '/';
1010
});
1111

1212
const errorPromise = waitForError('remix-hydrogen', errorEvent => {
@@ -15,7 +15,7 @@ test('Sends a client-side exception to Sentry', async ({ page }) => {
1515

1616
await page.goto('/');
1717

18-
await pageloadTransactionPromise;
18+
await pageloadSpanPromise;
1919

2020
const exceptionButton = page.locator('id=exception-button');
2121
await exceptionButton.click();

dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/client-transactions.test.ts

Lines changed: 28 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,50 @@
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

4-
test('Sends a pageload transaction to Sentry', async ({ page }) => {
5-
const transactionPromise = waitForTransaction('remix-hydrogen', transactionEvent => {
6-
return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/';
4+
test('Sends a pageload span to Sentry', async ({ page }) => {
5+
const spanPromise = waitForStreamedSpan('remix-hydrogen', span => {
6+
return getSpanOp(span) === 'pageload' && span.is_segment && span.name === '/';
77
});
88

99
await page.goto('/');
1010

11-
const transactionEvent = await transactionPromise;
12-
13-
expect(transactionEvent).toBeDefined();
14-
expect(transactionEvent).toMatchObject({
15-
transaction: '/',
16-
contexts: {
17-
trace: {
18-
data: {
19-
'sentry.segment.name.source': 'route',
20-
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/),
21-
'url.path': '/',
22-
'url.template': '/',
23-
},
24-
},
25-
},
11+
const span = await spanPromise;
12+
13+
expect(span.attributes).toMatchObject({
14+
'sentry.segment.name.source': { value: 'route', type: 'string' },
15+
'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), type: 'string' },
16+
'url.path': { value: '/', type: 'string' },
17+
'url.template': { value: '/', type: 'string' },
2618
});
2719
});
2820

29-
test('Sends a navigation transaction to Sentry', async ({ page }) => {
30-
// Wait for the initial pageload transaction first. This ensures the client SDK and
31-
// Remix router are fully hydrated before we click the link. Clicking before hydration
32-
// completes makes the `<Link>` behave like a plain anchor, triggering a full page
33-
// navigation (a `pageload` transaction) instead of a client-side `navigation` one,
34-
// which makes this test flaky.
35-
const pageloadTransactionPromise = waitForTransaction('remix-hydrogen', transactionEvent => {
36-
return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/';
21+
test('Sends a navigation span to Sentry', async ({ page }) => {
22+
// Wait for the initial pageload span first. This ensures the client SDK and Remix router are
23+
// fully hydrated before we click the link. Clicking before hydration completes makes the `<Link>`
24+
// behave like a plain anchor, triggering a full page navigation (a `pageload` span) instead of a
25+
// client-side `navigation` one, which makes this test flaky.
26+
const pageloadSpanPromise = waitForStreamedSpan('remix-hydrogen', span => {
27+
return getSpanOp(span) === 'pageload' && span.is_segment && span.name === '/';
3728
});
3829

39-
const transactionPromise = waitForTransaction('remix-hydrogen', transactionEvent => {
40-
return transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === '/user/:id';
30+
const spanPromise = waitForStreamedSpan('remix-hydrogen', span => {
31+
return getSpanOp(span) === 'navigation' && span.is_segment && span.name === '/user/:id';
4132
});
4233

4334
await page.goto('/');
4435

45-
await pageloadTransactionPromise;
36+
await pageloadSpanPromise;
4637

4738
const linkElement = page.locator('id=navigation');
4839
await linkElement.click();
4940

50-
const transactionEvent = await transactionPromise;
51-
52-
expect(transactionEvent).toBeDefined();
53-
expect(transactionEvent).toMatchObject({
54-
transaction: '/user/:id',
55-
contexts: {
56-
trace: {
57-
data: {
58-
'sentry.segment.name.source': 'route',
59-
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/),
60-
'url.path': '/user/5',
61-
'url.template': '/user/:id',
62-
},
63-
},
64-
},
41+
const span = await spanPromise;
42+
43+
expect(span.attributes).toMatchObject({
44+
'sentry.segment.name.source': { value: 'route', type: 'string' },
45+
'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/), type: 'string' },
46+
'url.path': { value: '/user/5', type: 'string' },
47+
'url.template': { value: '/user/:id', type: 'string' },
6548
});
6649
});
6750

Lines changed: 31 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,48 @@
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';
4+
5+
const APP_NAME = 'remix-hydrogen';
36

47
test.describe.configure({ mode: 'serial' });
58

6-
test('Sends parameterized transaction name to Sentry', async ({ page }) => {
7-
const transactionPromise = waitForTransaction('remix-hydrogen', transactionEvent => {
8-
return transactionEvent.contexts?.trace?.op === 'http.server';
9+
test('Sends a parameterized span name to Sentry', async ({ page }) => {
10+
const spanPromise = waitForStreamedSpan(APP_NAME, span => {
11+
// The span name is parameterized (route pattern, not the actual URL).
12+
return getSpanOp(span) === 'http.server' && span.is_segment && span.name === 'GET /user/:id';
913
});
1014

1115
await page.goto('/user/123');
1216

13-
const transaction = await transactionPromise;
17+
const span = await spanPromise;
1418

15-
expect(transaction).toBeDefined();
16-
expect(transaction.transaction).toBe('GET /user/:id');
19+
expect(span.attributes['sentry.segment.name.source']?.value).toBe('route');
1720
});
1821

19-
test('Sends two linked transactions (server & client) to Sentry', async ({ page }) => {
20-
// We use this to identify the transactions
21-
const testTag = crypto.randomUUID();
22-
23-
const httpServerTransactionPromise = waitForTransaction('remix-hydrogen', transactionEvent => {
24-
return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.tags?.['sentry_test'] === testTag;
25-
});
26-
27-
const pageLoadTransactionPromise = waitForTransaction('remix-hydrogen', transactionEvent => {
28-
return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.tags?.['sentry_test'] === testTag;
22+
test('Sends two linked spans (server & client) to Sentry', async ({ page }) => {
23+
// Streamed spans are buffered before they flush, so spans from an earlier page load can still be
24+
// arriving here. The document advertises its own trace in the `sentry-trace` meta tag, so that is
25+
// what tells this page load's spans apart rather than the op or the URL.
26+
const streamedSpans: SerializedStreamedSpan[] = [];
27+
void waitForStreamedSpans(APP_NAME, spans => {
28+
streamedSpans.push(...spans);
29+
return false;
2930
});
3031

31-
page.goto(`/?tag=${testTag}`);
32-
33-
const pageloadTransaction = await pageLoadTransactionPromise;
34-
const httpServerTransaction = await httpServerTransactionPromise;
35-
36-
expect(pageloadTransaction).toBeDefined();
37-
expect(httpServerTransaction).toBeDefined();
38-
39-
const httpServerTraceId = httpServerTransaction.contexts?.trace?.trace_id;
40-
const httpServerSpanId = httpServerTransaction.contexts?.trace?.span_id;
41-
42-
const pageLoadTraceId = pageloadTransaction.contexts?.trace?.trace_id;
43-
const pageLoadSpanId = pageloadTransaction.contexts?.trace?.span_id;
32+
await page.goto('/');
4433

45-
expect(httpServerTransaction.transaction).toBe('GET /');
46-
expect(pageloadTransaction.transaction).toBe('/');
34+
const sentryTrace = await page.getAttribute('meta[name="sentry-trace"]', 'content');
35+
const [traceId] = (sentryTrace ?? '').split('-');
36+
expect(traceId).toMatch(/^[a-f0-9]{32}$/);
4737

48-
expect(httpServerTraceId).toBeDefined();
49-
expect(httpServerSpanId).toBeDefined();
38+
const findServerSegmentSpan = () =>
39+
streamedSpans.find(span => getSpanOp(span) === 'http.server' && span.is_segment && span.trace_id === traceId);
40+
await expect.poll(findServerSegmentSpan).toBeDefined();
41+
expect(findServerSegmentSpan()!.name).toBe('GET /');
5042

51-
expect(pageLoadTraceId).toEqual(httpServerTraceId);
52-
expect(pageLoadSpanId).not.toEqual(httpServerSpanId);
43+
const findPageloadSpan = () =>
44+
streamedSpans.find(span => getSpanOp(span) === 'pageload' && span.is_segment && span.trace_id === traceId);
45+
await expect.poll(findPageloadSpan).toBeDefined();
46+
expect(findPageloadSpan()!.name).toBe('/');
47+
expect(findPageloadSpan()!.span_id).not.toBe(findServerSegmentSpan()!.span_id);
5348
});

0 commit comments

Comments
 (0)