Skip to content

Commit 5a08efb

Browse files
msonnbcodex
andcommitted
test(browser): port requests and web vitals to span streaming
Co-Authored-By: GPT-6 <codex@openai.com>
1 parent e483312 commit 5a08efb

108 files changed

Lines changed: 1080 additions & 1846 deletions

File tree

Some content is hidden

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

‎dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/init.js‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser';
33
window.Sentry = Sentry;
44

55
Sentry.init({
6-
traceLifecycle: 'static',
76
dsn: 'https://public@dsn.ingest.sentry.io/1337',
87
integrations: [Sentry.browserTracingIntegration()],
98
tracesSampleRate: 1,
Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
import { expect } from '@playwright/test';
2-
import type { Event as SentryEvent } from '@sentry/core';
32
import { sentryTest } from '../../../../utils/fixtures';
4-
import { countEnvelopes, getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
3+
import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers';
4+
import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils';
55

66
sentryTest('does not capture interaction spans without the integration', async ({ getLocalTestUrl, page }) => {
77
sentryTest.skip(shouldSkipTracingTest());
8-
8+
const spans = collectStreamedSpans(page);
9+
const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload');
910
const url = await getLocalTestUrl({ testDir: __dirname });
10-
1111
await page.goto(url);
12-
await getFirstSentryEnvelopeRequest<SentryEvent>(page);
13-
14-
const countPromise = countEnvelopes(page, { envelopeType: 'transaction', timeout: 2000 });
15-
12+
await pageloadPromise;
1613
await page.locator('[data-test-id=interaction-button]').click();
14+
await hidePage(page);
15+
await page.evaluate(() => (window as any).Sentry.flush());
1716

18-
expect(await countPromise).toBe(0);
17+
expect(spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.browser.interactions')).toHaveLength(0);
1918
});

‎dev-packages/browser-integration-tests/suites/tracing/interactions/navigation-click/init.js‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { interactionsIntegration } from '@sentry/browser';
44
window.Sentry = Sentry;
55

66
Sentry.init({
7-
traceLifecycle: 'static',
87
dsn: 'https://public@dsn.ingest.sentry.io/1337',
98
tracesSampleRate: 1,
109
integrations: [Sentry.browserTracingIntegration(), interactionsIntegration()],
Lines changed: 26 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,28 @@
11
import { expect } from '@playwright/test';
22
import { sentryTest } from '../../../../utils/fixtures';
3-
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers';
4-
5-
sentryTest(
6-
'click-triggered navigation should produce a root navigation transaction',
7-
async ({ getLocalTestUrl, page }) => {
8-
if (shouldSkipTracingTest()) {
9-
sentryTest.skip();
10-
}
11-
12-
const url = await getLocalTestUrl({ testDir: __dirname });
13-
14-
await page.goto(url);
15-
await waitForTransactionRequest(page); // "pageload" root span
16-
17-
const interactionRequestPromise = waitForTransactionRequest(
18-
page,
19-
evt => evt.contexts?.trace?.op === 'ui.action.click',
20-
);
21-
const navigationRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation');
22-
23-
await page.locator('[data-test-id=navigate-button]').click();
24-
25-
const interactionEvent = envelopeRequestParser(await interactionRequestPromise);
26-
const navigationEvent = envelopeRequestParser(await navigationRequestPromise);
27-
28-
// Navigation is root span, not a child span on the interaction
29-
expect(interactionEvent.contexts?.trace?.op).toBe('ui.action.click');
30-
expect(navigationEvent.contexts?.trace?.op).toBe('navigation');
31-
32-
expect(interactionEvent.contexts?.trace?.trace_id).not.toEqual(navigationEvent.contexts?.trace?.trace_id);
33-
34-
// does not contain a child navigation span
35-
const interactionSpans = interactionEvent.spans || [];
36-
const hasNavigationChild = interactionSpans.some(span => span.op === 'navigation' || span.op === 'http.server');
37-
expect(hasNavigationChild).toBeFalsy();
38-
},
39-
);
3+
import { shouldSkipTracingTest } from '../../../../utils/helpers';
4+
import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils';
5+
6+
sentryTest('starts a root navigation segment when a click triggers navigation', async ({ getLocalTestUrl, page }) => {
7+
sentryTest.skip(shouldSkipTracingTest());
8+
const spans = collectStreamedSpans(page);
9+
const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload');
10+
const url = await getLocalTestUrl({ testDir: __dirname });
11+
await page.goto(url);
12+
await pageloadPromise;
13+
14+
const interactionPromise = waitForStreamedSpan(
15+
page,
16+
span => span.is_segment && getSpanOp(span) === 'ui.action.click',
17+
);
18+
const navigationPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'navigation');
19+
await page.locator('[data-test-id=navigate-button]').click();
20+
const [interaction, navigation] = await Promise.all([interactionPromise, navigationPromise]);
21+
await page.evaluate(() => (window as any).Sentry.flush());
22+
23+
expect(navigation.is_segment).toBe(true);
24+
expect(navigation).not.toHaveProperty('parent_span_id');
25+
expect(navigation.trace_id).not.toBe(interaction.trace_id);
26+
const children = spans.filter(span => span.parent_span_id === interaction.span_id);
27+
expect(children.filter(span => ['navigation', 'http.server'].includes(getSpanOp(span) ?? ''))).toHaveLength(0);
28+
});

‎dev-packages/browser-integration-tests/suites/tracing/interactions/spans/init.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ window.Sentry = Sentry;
55

66
Sentry.init({
77
dsn: 'https://public@dsn.ingest.sentry.io/1337',
8-
integrations: [Sentry.browserTracingIntegration(), interactionsIntegration(), Sentry.spanStreamingIntegration()],
8+
integrations: [Sentry.browserTracingIntegration(), interactionsIntegration()],
99
tracesSampleRate: 1,
1010
});

‎dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,18 @@ import {
1616
} from '@sentry/conventions/attributes';
1717
import { sentryTest } from '../../../../utils/fixtures';
1818
import { shouldSkipTracingTest } from '../../../../utils/helpers';
19-
import { getSpanOp, waitForStreamedSpan, waitForStreamedSpans } from '../../../../utils/spanUtils';
19+
import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils';
2020

2121
sentryTest('captures streamed interaction span tree. @firefox', async ({ browserName, getLocalTestUrl, page }) => {
2222
const supportedBrowsers = ['chromium', 'firefox'];
2323

2424
sentryTest.skip(shouldSkipTracingTest() || !supportedBrowsers.includes(browserName));
2525
const url = await getLocalTestUrl({ testDir: __dirname });
2626

27-
const interactionSpansPromise = waitForStreamedSpans(page, spans =>
28-
spans.some(span => getSpanOp(span) === 'ui.action.click'),
27+
const spans = collectStreamedSpans(page);
28+
const interactionPromise = waitForStreamedSpan(
29+
page,
30+
span => span.is_segment && getSpanOp(span) === 'ui.action.click',
2931
);
3032

3133
const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload');
@@ -38,9 +40,13 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser
3840
await page.locator('[data-test-id=interaction-button]').click();
3941
await page.locator('.clicked[data-test-id=interaction-button]').isVisible();
4042

41-
const interactionSpanTree = await interactionSpansPromise;
42-
43-
const interactionSegmentSpan = interactionSpanTree.find(span => !!span.is_segment);
43+
const interactionSegmentSpan = await interactionPromise;
44+
await page.evaluate(() => (window as any).Sentry.flush());
45+
const interactionSpanTree = spans.filter(
46+
span =>
47+
span.span_id === interactionSegmentSpan.span_id ||
48+
span.attributes[SENTRY_SEGMENT_ID]?.value === interactionSegmentSpan.span_id,
49+
);
4450

4551
expect(interactionSegmentSpan).toEqual({
4652
attributes: {

‎dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt-navigation/init.js‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser';
33
window.Sentry = Sentry;
44

55
Sentry.init({
6-
traceLifecycle: 'static',
76
dsn: 'https://public@dsn.ingest.sentry.io/1337',
87
integrations: [
98
Sentry.browserTracingIntegration({
Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,20 @@
11
import { expect } from '@playwright/test';
2-
import type { Event } from '@sentry/core';
32
import { sentryTest } from '../../../../utils/fixtures';
4-
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers';
3+
import { shouldSkipTracingTest } from '../../../../utils/helpers';
4+
import { waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../../utils/spanUtils';
5+
import { NETWORK_CONNECTION_RTT } from '@sentry/conventions/attributes';
56

6-
sentryTest.beforeEach(({ browserName }) => {
7-
if (shouldSkipTracingTest() || browserName !== 'chromium') {
8-
sentryTest.skip();
9-
}
10-
});
11-
12-
// `connection.rtt` is recorded as a measurement, which is only flushed on the pageload
13-
// transaction. It must not leak onto navigation transactions.
147
sentryTest(
15-
'records `connection.rtt` as a measurement on pageload but not on navigation transactions',
16-
async ({ getLocalTestUrl, page }) => {
17-
const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload');
8+
'records connection RTT on pageload and navigation spans',
9+
async ({ getLocalTestUrl, page, browserName }) => {
10+
sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium');
1811
const url = await getLocalTestUrl({ testDir: __dirname });
19-
await page.goto(url);
20-
21-
const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise) as Event;
22-
23-
const navigationRequestPromise = waitForTransactionRequest(
24-
page,
25-
event => event.contexts?.trace?.op === 'navigation',
26-
);
27-
await page.goto(`${url}#foo`);
28-
29-
const navigationRequest = envelopeRequestParser(await navigationRequestPromise) as Event;
30-
31-
expect(pageloadRequest.contexts?.trace?.op).toBe('pageload');
32-
expect(navigationRequest.contexts?.trace?.op).toBe('navigation');
12+
const [pageload] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url);
13+
const [navigation] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, `${url}#foo`);
3314

34-
expect(pageloadRequest.measurements?.['connection.rtt']?.value).toBeDefined();
35-
expect(navigationRequest.measurements?.['connection.rtt']).toBeUndefined();
15+
expect(pageload.attributes[NETWORK_CONNECTION_RTT]).toEqual({ type: 'integer', value: 0 });
16+
expect(navigation.attributes[NETWORK_CONNECTION_RTT]).toEqual(pageload.attributes[NETWORK_CONNECTION_RTT]);
17+
expect(navigation.attributes['browser.web_vital.fcp.value']).toBeUndefined();
18+
expect(navigation.attributes['browser.web_vital.ttfb.value']).toBeUndefined();
3619
},
3720
);
Lines changed: 27 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type { Page } from '@playwright/test';
22
import { expect } from '@playwright/test';
3-
import type { Event } from '@sentry/core';
43
import { sentryTest } from '../../../../utils/fixtures';
5-
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
4+
import { shouldSkipTracingTest } from '../../../../utils/helpers';
5+
import { waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../../utils/spanUtils';
6+
import { NETWORK_CONNECTION_RTT } from '@sentry/conventions/attributes';
67

78
sentryTest.beforeEach(({ browserName }) => {
89
if (shouldSkipTracingTest() || browserName !== 'chromium') {
@@ -22,55 +23,42 @@ async function createSessionWithLatency(page: Page, latency: number) {
2223
return session;
2324
}
2425

25-
sentryTest('should capture a `connection.rtt` metric. xxx', async ({ getLocalTestUrl, page }) => {
26+
sentryTest('captures connection RTT without emulation', async ({ getLocalTestUrl, page }) => {
2627
const url = await getLocalTestUrl({ testDir: __dirname });
27-
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
28+
const eventData = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url).then(([span]) => span);
2829

29-
expect(eventData.measurements).toBeDefined();
30-
expect(eventData.measurements?.['connection.rtt']?.value).toBe(0);
30+
expect(eventData.attributes[NETWORK_CONNECTION_RTT]?.value).toBe(0);
3131
});
3232

33-
sentryTest(
34-
'should capture a `connection.rtt` metric with emulated value 200ms on Chromium.',
35-
async ({ getLocalTestUrl, page }) => {
36-
const session = await createSessionWithLatency(page, 200);
33+
sentryTest('captures connection RTT with emulated value 200ms on Chromium.', async ({ getLocalTestUrl, page }) => {
34+
const session = await createSessionWithLatency(page, 200);
3735

38-
const url = await getLocalTestUrl({ testDir: __dirname });
39-
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
36+
const url = await getLocalTestUrl({ testDir: __dirname });
37+
const eventData = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url).then(([span]) => span);
4038

41-
await session.detach();
39+
await session.detach();
4240

43-
expect(eventData.measurements).toBeDefined();
44-
expect(eventData.measurements?.['connection.rtt']?.value).toBe(200);
45-
},
46-
);
41+
expect(eventData.attributes[NETWORK_CONNECTION_RTT]?.value).toBe(200);
42+
});
4743

48-
sentryTest(
49-
'should capture a `connection.rtt` metric with emulated value 100ms on Chromium.',
50-
async ({ getLocalTestUrl, page }) => {
51-
const session = await createSessionWithLatency(page, 100);
44+
sentryTest('captures connection RTT with emulated value 100ms on Chromium.', async ({ getLocalTestUrl, page }) => {
45+
const session = await createSessionWithLatency(page, 100);
5246

53-
const url = await getLocalTestUrl({ testDir: __dirname });
54-
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
47+
const url = await getLocalTestUrl({ testDir: __dirname });
48+
const eventData = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url).then(([span]) => span);
5549

56-
await session.detach();
50+
await session.detach();
5751

58-
expect(eventData.measurements).toBeDefined();
59-
expect(eventData.measurements?.['connection.rtt']?.value).toBe(100);
60-
},
61-
);
52+
expect(eventData.attributes[NETWORK_CONNECTION_RTT]?.value).toBe(100);
53+
});
6254

63-
sentryTest(
64-
'should capture a `connection.rtt` metric with emulated value 50ms on Chromium.',
65-
async ({ getLocalTestUrl, page }) => {
66-
const session = await createSessionWithLatency(page, 50);
55+
sentryTest('captures connection RTT with emulated value 50ms on Chromium.', async ({ getLocalTestUrl, page }) => {
56+
const session = await createSessionWithLatency(page, 50);
6757

68-
const url = await getLocalTestUrl({ testDir: __dirname });
69-
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);
58+
const url = await getLocalTestUrl({ testDir: __dirname });
59+
const eventData = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url).then(([span]) => span);
7060

71-
await session.detach();
61+
await session.detach();
7262

73-
expect(eventData.measurements).toBeDefined();
74-
expect(eventData.measurements?.['connection.rtt']?.value).toBe(50);
75-
},
76-
);
63+
expect(eventData.attributes[NETWORK_CONNECTION_RTT]?.value).toBe(50);
64+
});

‎dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/init.js‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser';
33
window.Sentry = Sentry;
44

55
Sentry.init({
6-
traceLifecycle: 'static',
76
debug: true,
87
dsn: 'https://public@dsn.ingest.sentry.io/1337',
98
integrations: [Sentry.browserTracingIntegration(), Sentry.elementTimingIntegration()],

0 commit comments

Comments
 (0)