-
-
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.
+897
−13
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
54b2d66
feat(browser-utils): Add FCP instrumentation handler and export INP_E…
logaretm 728f0ec
feat(browser): Emit web vitals as streamed spans when span streaming …
logaretm 65b22b0
test(browser): Add integration tests for streamed web vital spans
logaretm 6f0add9
fix(browser): Only emit LCP, CLS, INP as streamed spans; disable stan…
logaretm 9a11f1d
fix(browser): Add MAX_PLAUSIBLE_INP_DURATION check to streamed INP sp…
logaretm 41d2d5a
fix(browser): Prevent duplicate INP spans when span streaming is enabled
logaretm 1ad9e35
fix(browser-utils): Remove dead FCP instrumentation code
logaretm 949df64
fix(browser-utils): Add fallback for browserPerformanceTimeOrigin in …
logaretm 211790f
fix(browser-utils): Cache browserPerformanceTimeOrigin call in _sendL…
logaretm 739fcf4
fix(browser): Skip INP interaction listeners when span streaming is e…
logaretm 6d2401f
fix(browser): Skip CLS/LCP measurements on pageload span when streaming
logaretm 7729456
refactor(browser-utils): Share MAX_PLAUSIBLE_INP_DURATION between INP…
logaretm 8e5fa78
fix(browser): Fix ReferenceError for spanStreamingEnabled in afterAll…
logaretm b94c3b3
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); | ||
| }); | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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.
l: can we also wait on the pageload span and assert that both have the same traceId? same for the LCP test