Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
56b5617
feat(browser-utils): Add FCP instrumentation handler and export INP_E…
logaretm Mar 16, 2026
d7ae263
feat(browser): Emit web vitals as streamed spans when span streaming …
logaretm Mar 16, 2026
13670f7
test(browser): Add integration tests for streamed web vital spans
logaretm Mar 16, 2026
f093a6d
fix(browser): Only emit LCP, CLS, INP as streamed spans; disable stan…
logaretm Mar 23, 2026
9eed4c9
fix(browser): Add MAX_PLAUSIBLE_INP_DURATION check to streamed INP sp…
logaretm Mar 23, 2026
6cba655
fix(browser): Prevent duplicate INP spans when span streaming is enabled
logaretm Mar 23, 2026
accc960
fix(browser-utils): Remove dead FCP instrumentation code
logaretm Mar 23, 2026
e92046f
fix(browser-utils): Add fallback for browserPerformanceTimeOrigin in …
logaretm Mar 23, 2026
86aadbb
fix(browser-utils): Cache browserPerformanceTimeOrigin call in _sendL…
logaretm Mar 23, 2026
e973a84
fix(browser): Skip INP interaction listeners when span streaming is e…
logaretm Mar 24, 2026
4f8aaf5
fix(browser): Skip CLS/LCP measurements on pageload span when streaming
logaretm Mar 24, 2026
dde4ad7
refactor(browser-utils): Share MAX_PLAUSIBLE_INP_DURATION between INP…
logaretm Mar 24, 2026
5dec7ce
fix(browser): Fix ReferenceError for spanStreamingEnabled in afterAll…
logaretm Mar 24, 2026
382f99b
fix(browser): Skip redundant CLS/LCP handlers when span streaming is …
logaretm Mar 24, 2026
936f7f0
send wv spans as child spans of pageload/inp root span
Lms24 Apr 14, 2026
02ac8fd
fix ttfb, fb, fco, measurements, connection attributes
Lms24 Apr 15, 2026
43037a5
fix integration tests after attribute name changes
Lms24 Apr 15, 2026
b307949
guard DEBUG_BUILD && fix size limit
Lms24 Apr 15, 2026
a0bfb46
cleanup
Lms24 Apr 15, 2026
70a2ca4
fix size limit again + unit tests
Lms24 Apr 15, 2026
1aa3b6e
cleanup
Lms24 Apr 15, 2026
5bba109
another cleanup lol
Lms24 Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 26 additions & 19 deletions .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,28 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'browserTracingIntegration'),
gzip: true,
limit: '43 KB',
limit: '44 KB',
Comment thread
Lms24 marked this conversation as resolved.
},
{
name: '@sentry/browser (incl. Tracing + Span Streaming)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'browserTracingIntegration', 'spanStreamingIntegration'),
gzip: true,
limit: '48 KB',
},
{
name: '@sentry/browser (incl. Tracing, Profiling)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'browserTracingIntegration', 'browserProfilingIntegration'),
gzip: true,
limit: '48 KB',
limit: '49 KB',
},
{
name: '@sentry/browser (incl. Tracing, Replay)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration'),
gzip: true,
limit: '82 KB',
limit: '83 KB',
},
{
name: '@sentry/browser (incl. Tracing, Replay) - with treeshaking flags',
Expand Down Expand Up @@ -82,14 +89,14 @@ module.exports = [
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'),
gzip: true,
limit: '87 KB',
limit: '88 KB',
},
{
name: '@sentry/browser (incl. Tracing, Replay, Feedback)',
path: 'packages/browser/build/npm/esm/prod/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'feedbackIntegration'),
gzip: true,
limit: '99 KB',
limit: '100 KB',
},
{
name: '@sentry/browser (incl. Feedback)',
Expand Down Expand Up @@ -163,7 +170,7 @@ module.exports = [
path: 'packages/vue/build/esm/index.js',
import: createImport('init', 'browserTracingIntegration'),
gzip: true,
limit: '45 KB',
limit: '46 KB',
},
// Svelte SDK (ESM)
{
Expand All @@ -184,7 +191,7 @@ module.exports = [
name: 'CDN Bundle (incl. Tracing)',
path: createCDNPath('bundle.tracing.min.js'),
gzip: true,
limit: '44 KB',
limit: '45 KB',
},
{
name: 'CDN Bundle (incl. Logs, Metrics)',
Expand All @@ -196,7 +203,7 @@ module.exports = [
name: 'CDN Bundle (incl. Tracing, Logs, Metrics)',
path: createCDNPath('bundle.tracing.logs.metrics.min.js'),
gzip: true,
limit: '45 KB',
limit: '46 KB',
},
{
name: 'CDN Bundle (incl. Replay, Logs, Metrics)',
Expand All @@ -208,25 +215,25 @@ module.exports = [
name: 'CDN Bundle (incl. Tracing, Replay)',
path: createCDNPath('bundle.tracing.replay.min.js'),
gzip: true,
limit: '81 KB',
limit: '82 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay, Logs, Metrics)',
path: createCDNPath('bundle.tracing.replay.logs.metrics.min.js'),
gzip: true,
limit: '82 KB',
limit: '83 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay, Feedback)',
path: createCDNPath('bundle.tracing.replay.feedback.min.js'),
gzip: true,
limit: '87 KB',
limit: '88 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)',
path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'),
gzip: true,
limit: '88 KB',
limit: '89 KB',
},
// browser CDN bundles (non-gzipped)
{
Expand All @@ -241,7 +248,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.min.js'),
gzip: false,
brotli: false,
limit: '130 KB',
limit: '134 KB',
},
{
name: 'CDN Bundle (incl. Logs, Metrics) - uncompressed',
Expand All @@ -255,7 +262,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.logs.metrics.min.js'),
gzip: false,
brotli: false,
limit: '134 KB',
limit: '138 KB',
},
{
name: 'CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed',
Expand All @@ -269,14 +276,14 @@ module.exports = [
path: createCDNPath('bundle.tracing.replay.min.js'),
gzip: false,
brotli: false,
limit: '248 KB',
limit: '251 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed',
path: createCDNPath('bundle.tracing.replay.logs.metrics.min.js'),
gzip: false,
brotli: false,
limit: '251 KB',
limit: '255 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed',
Expand All @@ -290,7 +297,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'),
gzip: false,
brotli: false,
limit: '264 KB',
limit: '268 KB',
},
// Next.js SDK (ESM)
{
Expand All @@ -299,7 +306,7 @@ module.exports = [
import: createImport('init'),
ignore: ['next/router', 'next/constants'],
gzip: true,
limit: '48 KB',
limit: '49 KB',
},
// SvelteKit SDK (ESM)
{
Expand All @@ -308,7 +315,7 @@ module.exports = [
import: createImport('init'),
ignore: ['$app/stores'],
gzip: true,
limit: '44 KB',
limit: '45 KB',
},
// Node-Core SDK (ESM)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,17 @@ sentryTest('starts a streamed navigation span on page navigation', async ({ getL

expect(navigationSpan).toEqual({
attributes: {
effectiveConnectionType: {
'network.connection.effective_type': {
type: 'string',
value: expect.any(String),
},
hardwareConcurrency: {
type: 'string',
value: expect.any(String),
'device.processor_count': {
type: expect.stringMatching(/^(integer)|(double)$/),
value: expect.any(Number),
},
'network.connection.rtt': {
type: expect.stringMatching(/^(integer)|(double)$/),
value: expect.any(Number),
},
'sentry.idle_span_finish_reason': {
type: 'string',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,20 +62,34 @@ sentryTest(

expect(pageloadSpan).toEqual({
attributes: {
effectiveConnectionType: {
// formerly known as 'effectiveConnectionType'
'network.connection.effective_type': {
type: 'string',
value: expect.any(String),
},
hardwareConcurrency: {
type: 'string',
value: expect.any(String),
// formerly known as 'hardwareConcurrency'
'device.processor_count': {
type: expect.stringMatching(/^(integer)|(double)$/),
value: expect.any(Number),
},
'performance.activationStart': {
type: 'integer',
'browser.performance.navigation.activation_start': {
type: expect.stringMatching(/^(integer)|(double)$/),
value: expect.any(Number),
},
'browser.performance.time_origin': {
type: expect.stringMatching(/^(integer)|(double)$/),
value: expect.any(Number),
},
'network.connection.rtt': {
type: expect.stringMatching(/^(integer)|(double)$/),
value: expect.any(Number),
},
'browser.web_vital.ttfb.request_time': {
type: expect.stringMatching(/^(integer)|(double)$/),
value: expect.any(Number),
},
'performance.timeOrigin': {
type: 'double',
'browser.web_vital.ttfb.value': {
type: expect.stringMatching(/^(integer)|(double)$/),
value: expect.any(Number),
},
'sentry.idle_span_finish_reason': {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;
window._testBaseTimestamp = performance.timeOrigin / 1000;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()],
traceLifecycle: 'stream',
tracesSampleRate: 1,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { simulateCLS } from '../../../../utils/web-vitals/cls.ts';

// Simulate Layout shift right at the beginning of the page load, depending on the URL hash
// don't run if expected CLS is NaN
const expectedCLS = Number(location.hash.slice(1));
if (expectedCLS && expectedCLS >= 0) {
simulateCLS(expectedCLS).then(() => window.dispatchEvent(new Event('cls-done')));
}

// Simulate layout shift whenever the trigger-cls event is dispatched
// Cannot trigger via a button click because expected layout shift after
// an interaction doesn't contribute to CLS.
window.addEventListener('trigger-cls', () => {
simulateCLS(0.1).then(() => {
window.dispatchEvent(new Event('cls-done'));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<div id="content"></div>
<p>Some content</p>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
import { hidePage, shouldSkipTracingTest, testingCdnBundle } from '../../../../utils/helpers';
import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils';

sentryTest.beforeEach(async ({ browserName, page }) => {
if (shouldSkipTracingTest() || testingCdnBundle() || browserName !== 'chromium') {
sentryTest.skip();
}

await page.setViewportSize({ width: 800, height: 1200 });
});

function waitForLayoutShift(page: Page): Promise<void> {
return page.evaluate(() => {
return new Promise(resolve => {
window.addEventListener('cls-done', () => resolve());
});
});
}

sentryTest('captures CLS as a streamed span with source attributes', async ({ getLocalTestUrl, page }) => {
const url = await getLocalTestUrl({ testDir: __dirname });

const clsSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.webvital.cls');
Comment thread
Lms24 marked this conversation as resolved.
const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload');

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

const clsSpan = await clsSpanPromise;
const pageloadSpan = await pageloadSpanPromise;

expect(clsSpan.attributes?.['sentry.op']).toEqual({ type: 'string', value: 'ui.webvital.cls' });
expect(clsSpan.attributes?.['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.browser.cls' });
expect(clsSpan.attributes?.['sentry.exclusive_time']).toEqual({ type: 'integer', value: 0 });
expect(clsSpan.attributes?.['user_agent.original']?.value).toEqual(expect.stringContaining('Chrome'));

// Check browser.web_vital.cls.source attributes
expect(clsSpan.attributes?.['browser.web_vital.cls.source.1']?.value).toEqual(
expect.stringContaining('body > div#content > p'),
);

// Check pageload span id is present
expect(clsSpan.attributes?.['sentry.pageload.span_id']?.value).toBe(pageloadSpan.span_id);

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

expect(clsSpan.span_id).toMatch(/^[\da-f]{16}$/);
expect(clsSpan.trace_id).toMatch(/^[\da-f]{32}$/);

expect(clsSpan.parent_span_id).toBe(pageloadSpan.span_id);
expect(clsSpan.trace_id).toBe(pageloadSpan.trace_id);
});

sentryTest('CLS streamed span has web vital value attribute', async ({ getLocalTestUrl, page }) => {
const url = await getLocalTestUrl({ testDir: __dirname });

const clsSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.webvital.cls');

await page.goto(`${url}#0.1`);
await waitForLayoutShift(page);
await hidePage(page);

const clsSpan = await clsSpanPromise;

// The CLS value should be set as a browser.web_vital.cls.value attribute
expect(clsSpan.attributes?.['browser.web_vital.cls.value']?.type).toBe('double');
// Flakey value dependent on timings -> we check for a range
const clsValue = clsSpan.attributes?.['browser.web_vital.cls.value']?.value as number;
expect(clsValue).toBeGreaterThan(0.05);
expect(clsValue).toBeLessThan(0.15);
});
Comment thread
cursor[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;
window._testBaseTimestamp = performance.timeOrigin / 1000;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration({ idleTimeout: 4000 }), Sentry.spanStreamingIntegration()],
tracesSampleRate: 1,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const blockUI =
(delay = 70) =>
e => {
const startTime = Date.now();

function getElapsed() {
const time = Date.now();
return time - startTime;
}

while (getElapsed() < delay) {
//
}

e.target.classList.add('clicked');
};

document.querySelector('[data-test-id=slow-button]').addEventListener('click', blockUI(450));
document.querySelector('[data-test-id=normal-button]').addEventListener('click', blockUI());
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<button data-test-id="slow-button" data-sentry-element="SlowButton">Slow</button>
<button data-test-id="normal-button" data-sentry-element="NormalButton">Click Me</button>
</body>
</html>
Loading
Loading