Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(browser): Emit web vitals as streamed spans#19827
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
56b5617d7ae26313670f7f093a6d9eed4c96cba655accc960e92046f86aadbbe973a844f8aaf5dde4ad75dec7ce382f99b936f7f002ac8fd43037a5b307949a0bfb4670a2ca41aa3b6e5bba109File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| 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'); | ||
Lms24 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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); | ||
| }); | ||
cursor[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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> |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.