Skip to content

Commit 936f7f0

Browse files
committed
send wv spans as child spans of pageload/inp root span
add INP tests remove adding events
1 parent 382f99b commit 936f7f0

File tree

17 files changed

+331
-110
lines changed

17 files changed

+331
-110
lines changed

.size-limit.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ module.exports = [
4040
gzip: true,
4141
limit: '43 KB',
4242
},
43+
{
44+
name: '@sentry/browser (incl. Tracing + Span Streaming)',
45+
path: 'packages/browser/build/npm/esm/prod/index.js',
46+
import: createImport('init', 'browserTracingIntegration', 'spanStreamingIntegration'),
47+
gzip: true,
48+
limit: '48 KB',
49+
},
4350
{
4451
name: '@sentry/browser (incl. Tracing, Profiling)',
4552
path: 'packages/browser/build/npm/esm/prod/index.js',

dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/init.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ window._testBaseTimestamp = performance.timeOrigin / 1000;
55

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

dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/test.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Page } from '@playwright/test';
22
import { expect } from '@playwright/test';
33
import { sentryTest } from '../../../../utils/fixtures';
4-
import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
4+
import { hidePage, shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
55
import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils';
66

77
sentryTest.beforeEach(async ({ browserName, page }) => {
@@ -20,22 +20,18 @@ function waitForLayoutShift(page: Page): Promise<void> {
2020
});
2121
}
2222

23-
function hidePage(page: Page): Promise<void> {
24-
return page.evaluate(() => {
25-
window.dispatchEvent(new Event('pagehide'));
26-
});
27-
}
28-
2923
sentryTest('captures CLS as a streamed span with source attributes', async ({ getLocalTestUrl, page }) => {
3024
const url = await getLocalTestUrl({ testDir: __dirname });
3125

3226
const clsSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.webvital.cls');
27+
const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload');
3328

3429
await page.goto(`${url}#0.15`);
3530
await waitForLayoutShift(page);
3631
await hidePage(page);
3732

3833
const clsSpan = await clsSpanPromise;
34+
const pageloadSpan = await pageloadSpanPromise;
3935

4036
expect(clsSpan.attributes?.['sentry.op']).toEqual({ type: 'string', value: 'ui.webvital.cls' });
4137
expect(clsSpan.attributes?.['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.browser.cls' });
@@ -48,13 +44,16 @@ sentryTest('captures CLS as a streamed span with source attributes', async ({ ge
4844
);
4945

5046
// Check pageload span id is present
51-
expect(clsSpan.attributes?.['sentry.pageload.span_id']?.value).toMatch(/[\da-f]{16}/);
47+
expect(clsSpan.attributes?.['sentry.pageload.span_id']?.value).toBe(pageloadSpan.span_id);
5248

5349
// CLS is a point-in-time metric
5450
expect(clsSpan.start_timestamp).toEqual(clsSpan.end_timestamp);
5551

5652
expect(clsSpan.span_id).toMatch(/^[\da-f]{16}$/);
5753
expect(clsSpan.trace_id).toMatch(/^[\da-f]{32}$/);
54+
55+
expect(clsSpan.parent_span_id).toBe(pageloadSpan.span_id);
56+
expect(clsSpan.trace_id).toBe(pageloadSpan.trace_id);
5857
});
5958

6059
sentryTest('CLS streamed span has web vital value attribute', async ({ getLocalTestUrl, page }) => {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
window._testBaseTimestamp = performance.timeOrigin / 1000;
5+
6+
Sentry.init({
7+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
8+
integrations: [
9+
Sentry.browserTracingIntegration({ idleTimeout: 4000 }),
10+
Sentry.spanStreamingIntegration(),
11+
],
12+
tracesSampleRate: 1,
13+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const blockUI =
2+
(delay = 70) =>
3+
e => {
4+
const startTime = Date.now();
5+
6+
function getElapsed() {
7+
const time = Date.now();
8+
return time - startTime;
9+
}
10+
11+
while (getElapsed() < delay) {
12+
//
13+
}
14+
15+
e.target.classList.add('clicked');
16+
};
17+
18+
document.querySelector('[data-test-id=slow-button]').addEventListener('click', blockUI(450));
19+
document.querySelector('[data-test-id=normal-button]').addEventListener('click', blockUI());
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<button data-test-id="slow-button" data-sentry-element="SlowButton">Slow</button>
8+
<button data-test-id="normal-button" data-sentry-element="NormalButton">Click Me</button>
9+
</body>
10+
</html>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../../utils/fixtures';
3+
import { hidePage, shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
4+
import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils';
5+
6+
sentryTest.beforeEach(async ({ browserName }) => {
7+
if (shouldSkipTracingTest() || testingCdnBundle() || browserName !== 'chromium') {
8+
sentryTest.skip();
9+
}
10+
});
11+
12+
sentryTest('captures INP click as a streamed span', async ({ getLocalTestUrl, page }) => {
13+
const url = await getLocalTestUrl({ testDir: __dirname });
14+
15+
const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload');
16+
const inpSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.interaction.click');
17+
18+
await page.goto(url);
19+
20+
await page.locator('[data-test-id=normal-button]').click();
21+
await page.locator('.clicked[data-test-id=normal-button]').isVisible();
22+
23+
await page.waitForTimeout(500);
24+
25+
await hidePage(page);
26+
27+
const inpSpan = await inpSpanPromise;
28+
const pageloadSpan = await pageloadSpanPromise;
29+
30+
expect(inpSpan.attributes?.['sentry.op']).toEqual({ type: 'string', value: 'ui.interaction.click' });
31+
expect(inpSpan.attributes?.['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.browser.inp' });
32+
expect(inpSpan.attributes?.['user_agent.original']?.value).toEqual(expect.stringContaining('Chrome'));
33+
34+
const inpValue = inpSpan.attributes?.['browser.web_vital.inp.value']?.value as number;
35+
expect(inpValue).toBeGreaterThan(0);
36+
37+
expect(inpSpan.attributes?.['sentry.exclusive_time']?.value).toBeGreaterThan(0);
38+
39+
expect(inpSpan.name).toBe('body > NormalButton');
40+
41+
expect(inpSpan.end_timestamp).toBeGreaterThan(inpSpan.start_timestamp);
42+
43+
expect(inpSpan.span_id).toMatch(/^[\da-f]{16}$/);
44+
expect(inpSpan.trace_id).toMatch(/^[\da-f]{32}$/);
45+
46+
expect(inpSpan.parent_span_id).toBe(pageloadSpan.span_id);
47+
expect(inpSpan.trace_id).toBe(pageloadSpan.trace_id);
48+
});
49+
50+
sentryTest('captures the slowest interaction as streamed INP span', async ({ getLocalTestUrl, page }) => {
51+
const url = await getLocalTestUrl({ testDir: __dirname });
52+
53+
await page.goto(url);
54+
55+
await page.locator('[data-test-id=normal-button]').click();
56+
await page.locator('.clicked[data-test-id=normal-button]').isVisible();
57+
58+
await page.waitForTimeout(500);
59+
60+
const inpSpanPromise = waitForStreamedSpan(page, span => {
61+
const op = getSpanOp(span);
62+
return op === 'ui.interaction.click';
63+
});
64+
65+
await page.locator('[data-test-id=slow-button]').click();
66+
await page.locator('.clicked[data-test-id=slow-button]').isVisible();
67+
68+
await page.waitForTimeout(500);
69+
70+
await hidePage(page);
71+
72+
const inpSpan = await inpSpanPromise;
73+
74+
expect(inpSpan.name).toBe('body > SlowButton');
75+
expect(inpSpan.attributes?.['sentry.exclusive_time']?.value).toBeGreaterThan(400);
76+
77+
const inpValue = inpSpan.attributes?.['browser.web_vital.inp.value']?.value as number;
78+
expect(inpValue).toBeGreaterThan(400);
79+
});

dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/init.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ window._testBaseTimestamp = performance.timeOrigin / 1000;
55

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

dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/test.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { Page, Route } from '@playwright/test';
1+
import type { Route } from '@playwright/test';
22
import { expect } from '@playwright/test';
33
import { sentryTest } from '../../../../utils/fixtures';
4-
import { shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
4+
import { hidePage, shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
55
import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils';
66

77
sentryTest.beforeEach(async ({ browserName, page }) => {
@@ -12,12 +12,6 @@ sentryTest.beforeEach(async ({ browserName, page }) => {
1212
await page.setViewportSize({ width: 800, height: 1200 });
1313
});
1414

15-
function hidePage(page: Page): Promise<void> {
16-
return page.evaluate(() => {
17-
window.dispatchEvent(new Event('pagehide'));
18-
});
19-
}
20-
2115
sentryTest('captures LCP as a streamed span with element attributes', async ({ getLocalTestUrl, page }) => {
2216
page.route('**', route => route.continue());
2317
page.route('**/my/image.png', async (route: Route) => {
@@ -29,6 +23,7 @@ sentryTest('captures LCP as a streamed span with element attributes', async ({ g
2923
const url = await getLocalTestUrl({ testDir: __dirname });
3024

3125
const lcpSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.webvital.lcp');
26+
const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload');
3227

3328
await page.goto(url);
3429

@@ -38,6 +33,7 @@ sentryTest('captures LCP as a streamed span with element attributes', async ({ g
3833
await hidePage(page);
3934

4035
const lcpSpan = await lcpSpanPromise;
36+
const pageloadSpan = await pageloadSpanPromise;
4137

4238
expect(lcpSpan.attributes?.['sentry.op']).toEqual({ type: 'string', value: 'ui.webvital.lcp' });
4339
expect(lcpSpan.attributes?.['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.browser.lcp' });
@@ -52,15 +48,18 @@ sentryTest('captures LCP as a streamed span with element attributes', async ({ g
5248
expect(lcpSpan.attributes?.['browser.web_vital.lcp.size']?.value).toEqual(expect.any(Number));
5349

5450
// Check web vital value attribute
55-
expect(lcpSpan.attributes?.['browser.web_vital.lcp.value']?.type).toBe('double');
51+
expect(lcpSpan.attributes?.['browser.web_vital.lcp.value']?.type).toMatch(/^(double)|(integer)$/);
5652
expect(lcpSpan.attributes?.['browser.web_vital.lcp.value']?.value).toBeGreaterThan(0);
5753

5854
// Check pageload span id is present
59-
expect(lcpSpan.attributes?.['sentry.pageload.span_id']?.value).toMatch(/[\da-f]{16}/);
55+
expect(lcpSpan.attributes?.['sentry.pageload.span_id']?.value).toBe(pageloadSpan.span_id);
6056

6157
// Span should have meaningful duration (navigation start -> LCP event)
6258
expect(lcpSpan.end_timestamp).toBeGreaterThan(lcpSpan.start_timestamp);
6359

6460
expect(lcpSpan.span_id).toMatch(/^[\da-f]{16}$/);
6561
expect(lcpSpan.trace_id).toMatch(/^[\da-f]{32}$/);
62+
63+
expect(lcpSpan.parent_span_id).toBe(pageloadSpan.span_id);
64+
expect(lcpSpan.trace_id).toBe(pageloadSpan.trace_id);
6665
});

packages/browser-utils/src/metrics/browserMetrics.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ interface StartTrackingWebVitalsOptions {
9494
* Start tracking web vitals.
9595
* The callback returned by this function can be used to stop tracking & ensure all measurements are final & captured.
9696
*
97+
* @deprecated this function will be removed and streamlined once we stop supporting standalone v1
9798
* @returns A function that forces web vitals collection
9899
*/
99100
export function startTrackingWebVitals({
@@ -108,25 +109,23 @@ export function startTrackingWebVitals({
108109
WINDOW.performance.mark('sentry-tracing-init');
109110
}
110111

111-
const lcpCleanupCallback =
112-
recordLcpStandaloneSpans === true
113-
? trackLcpAsStandaloneSpan(client)
114-
: recordLcpStandaloneSpans === false
115-
? _trackLCP()
116-
: undefined;
112+
const lcpCleanupCallback = recordLcpStandaloneSpans
113+
? trackLcpAsStandaloneSpan(client)
114+
: recordLcpStandaloneSpans === false
115+
? _trackLCP()
116+
: undefined;
117117

118-
const clsCleanupCallback =
119-
recordClsStandaloneSpans === true
120-
? trackClsAsStandaloneSpan(client)
121-
: recordClsStandaloneSpans === false
122-
? _trackCLS()
123-
: undefined;
118+
const clsCleanupCallback = recordClsStandaloneSpans
119+
? trackClsAsStandaloneSpan(client)
120+
: recordClsStandaloneSpans === false
121+
? _trackCLS()
122+
: undefined;
124123

125124
const ttfbCleanupCallback = _trackTtfb();
126125

127126
return (): void => {
128-
lcpCleanupCallback?.();
129127
ttfbCleanupCallback();
128+
lcpCleanupCallback?.();
130129
clsCleanupCallback?.();
131130
};
132131
}

0 commit comments

Comments
 (0)