-
-
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
Open
logaretm
wants to merge
14
commits into
lms/feat-span-first
Choose a base branch
from
awad/js-17931-webvitals-v2-spans
base: lms/feat-span-first
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
4caaa7b
feat(browser-utils): Add FCP instrumentation handler and export INP_E…
logaretm 8eaf2ef
feat(browser): Emit web vitals as streamed spans when span streaming …
logaretm 060024d
test(browser): Add integration tests for streamed web vital spans
logaretm fa6b8ec
fix(browser): Only emit LCP, CLS, INP as streamed spans; disable stan…
logaretm 086f73f
fix(browser): Add MAX_PLAUSIBLE_INP_DURATION check to streamed INP sp…
logaretm abfc1cd
fix(browser): Prevent duplicate INP spans when span streaming is enabled
logaretm 813ba34
fix(browser-utils): Remove dead FCP instrumentation code
logaretm ac9a2b2
fix(browser-utils): Add fallback for browserPerformanceTimeOrigin in …
logaretm 0d40b7c
fix(browser-utils): Cache browserPerformanceTimeOrigin call in _sendL…
logaretm 5daf97f
fix(browser): Skip INP interaction listeners when span streaming is e…
logaretm 04a626a
fix(browser): Skip CLS/LCP measurements on pageload span when streaming
logaretm 0e19b37
refactor(browser-utils): Share MAX_PLAUSIBLE_INP_DURATION between INP…
logaretm 4bf129b
fix(browser): Fix ReferenceError for spanStreamingEnabled in afterAll…
logaretm 7adff97
fix(browser): Skip redundant CLS/LCP handlers when span streaming is …
logaretm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
11 changes: 11 additions & 0 deletions
11
...es/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/init.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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({ idleTimeout: 9000 }), Sentry.spanStreamingIntegration()], | ||
| traceLifecycle: 'stream', | ||
| tracesSampleRate: 1, | ||
| }); |
17 changes: 17 additions & 0 deletions
17
...browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/subject.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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')); | ||
| }); | ||
| }); |
10 changes: 10 additions & 0 deletions
10
...wser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/template.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
77 changes: 77 additions & 0 deletions
77
...es/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import type { Page } from '@playwright/test'; | ||
| import { expect } from '@playwright/test'; | ||
| import { sentryTest } from '../../../../utils/fixtures'; | ||
| import { 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()); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| function hidePage(page: Page): Promise<void> { | ||
| return page.evaluate(() => { | ||
| window.dispatchEvent(new Event('pagehide')); | ||
| }); | ||
| } | ||
|
|
||
| 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'); | ||
|
|
||
| await page.goto(`${url}#0.15`); | ||
| await waitForLayoutShift(page); | ||
| await hidePage(page); | ||
|
|
||
| const clsSpan = await clsSpanPromise; | ||
|
|
||
| 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).toMatch(/[\da-f]{16}/); | ||
|
|
||
| // 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}$/); | ||
| }); | ||
|
|
||
| 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); | ||
| }); | ||
Binary file added
BIN
+15.7 KB
...es/tracing/metrics/web-vitals-lcp-streamed-spans/assets/sentry-logo-600x179.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions
11
...es/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/init.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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({ idleTimeout: 9000 }), Sentry.spanStreamingIntegration()], | ||
| traceLifecycle: 'stream', | ||
| tracesSampleRate: 1, | ||
| }); |
10 changes: 10 additions & 0 deletions
10
...wser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/template.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| <img src="https://sentry-test-site.example/my/image.png" /> | ||
| </body> | ||
| </html> |
66 changes: 66 additions & 0 deletions
66
...es/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import type { Page, Route } from '@playwright/test'; | ||
| import { expect } from '@playwright/test'; | ||
| import { sentryTest } from '../../../../utils/fixtures'; | ||
| import { 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 hidePage(page: Page): Promise<void> { | ||
| return page.evaluate(() => { | ||
| window.dispatchEvent(new Event('pagehide')); | ||
| }); | ||
| } | ||
|
|
||
| sentryTest('captures LCP as a streamed span with element attributes', async ({ getLocalTestUrl, page }) => { | ||
| page.route('**', route => route.continue()); | ||
| page.route('**/my/image.png', async (route: Route) => { | ||
| return route.fulfill({ | ||
| path: `${__dirname}/assets/sentry-logo-600x179.png`, | ||
| }); | ||
| }); | ||
|
|
||
| const url = await getLocalTestUrl({ testDir: __dirname }); | ||
|
|
||
| const lcpSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.webvital.lcp'); | ||
|
|
||
| await page.goto(url); | ||
|
|
||
| // Wait for LCP to be captured | ||
| await page.waitForTimeout(1000); | ||
|
|
||
| await hidePage(page); | ||
|
|
||
| const lcpSpan = await lcpSpanPromise; | ||
|
|
||
| expect(lcpSpan.attributes?.['sentry.op']).toEqual({ type: 'string', value: 'ui.webvital.lcp' }); | ||
| expect(lcpSpan.attributes?.['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.browser.lcp' }); | ||
| expect(lcpSpan.attributes?.['sentry.exclusive_time']).toEqual({ type: 'integer', value: 0 }); | ||
| expect(lcpSpan.attributes?.['user_agent.original']?.value).toEqual(expect.stringContaining('Chrome')); | ||
|
|
||
| // Check browser.web_vital.lcp.* attributes | ||
| expect(lcpSpan.attributes?.['browser.web_vital.lcp.element']?.value).toEqual(expect.stringContaining('body > img')); | ||
| expect(lcpSpan.attributes?.['browser.web_vital.lcp.url']?.value).toBe( | ||
| 'https://sentry-test-site.example/my/image.png', | ||
| ); | ||
| expect(lcpSpan.attributes?.['browser.web_vital.lcp.size']?.value).toEqual(expect.any(Number)); | ||
|
|
||
| // Check web vital value attribute | ||
| expect(lcpSpan.attributes?.['browser.web_vital.lcp.value']?.type).toBe('double'); | ||
| expect(lcpSpan.attributes?.['browser.web_vital.lcp.value']?.value).toBeGreaterThan(0); | ||
|
|
||
| // Check pageload span id is present | ||
| expect(lcpSpan.attributes?.['sentry.pageload.span_id']?.value).toMatch(/[\da-f]{16}/); | ||
|
|
||
| // Span should have meaningful duration (navigation start -> LCP event) | ||
| expect(lcpSpan.end_timestamp).toBeGreaterThan(lcpSpan.start_timestamp); | ||
|
|
||
| expect(lcpSpan.span_id).toMatch(/^[\da-f]{16}$/); | ||
| expect(lcpSpan.trace_id).toMatch(/^[\da-f]{32}$/); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing INP streamed span E2E test coverage
Low Severity
This
featPR adds three new streamed span code paths (LCP, CLS, INP), but only LCP and CLS have E2E/integration tests. The INP streaming path intrackInpAsSpan/_sendInpSpan— which has its own distinct logic (nolistenForWebVitalReportEvents, nopageloadSpanId, noregisterInpInteractionListenerfallback for element names) — only has unit test coverage. Adding an E2E test for the INP streaming path would help catch integration issues betweenaddInpInstrumentationHandlerand the_emitWebVitalSpanpipeline.Additional Locations (1)
dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/test.ts#L1-L66Triggered by project rule: PR Review Guidelines for Cursor Bot