Skip to content

feat(browser): Emit web vitals as streamed spans#19827

Open
logaretm wants to merge 3 commits intolms/feat-span-firstfrom
awad/js-17931-webvitals-v2-spans
Open

feat(browser): Emit web vitals as streamed spans#19827
logaretm wants to merge 3 commits intolms/feat-span-firstfrom
awad/js-17931-webvitals-v2-spans

Conversation

@logaretm
Copy link
Member

@logaretm logaretm commented Mar 16, 2026

Summary

Closes #17931

When span streaming is enabled (traceLifecycle: 'stream'), emit web vital values as non-standalone spans that flow through the v2 pipeline (afterSpanEndcaptureSpan()SpanBuffer).

This is additive — existing standalone spans and pageload measurements remain untouched. The new spans are only emitted when hasSpanStreamingEnabled(client) is true.

Each web vital span carries browser.web_vital.* attributes per sentry-conventions PRs 229, 233-235:

  • LCP: browser.web_vital.lcp.{value,element,id,url,size,load_time,render_time}
  • CLS: browser.web_vital.cls.{value,source.<N>}
  • INP: browser.web_vital.inp.value
  • TTFB: browser.web_vital.ttfb.{value,request_time}
  • FCP: browser.web_vital.fcp.value
  • FP: browser.web_vital.fp.value

Spans have meaningful durations (navigation start → event time) instead of being point-in-time, except CLS which is a score.

@github-actions
Copy link
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 25.72 kB added added
@sentry/browser - with treeshaking flags 24.22 kB added added
⛔️ @sentry/browser (incl. Tracing) (max: 43 kB) 43.68 kB added added
⛔️ @sentry/browser (incl. Tracing, Profiling) (max: 48 kB) 48.63 kB added added
⛔️ @sentry/browser (incl. Tracing, Replay) (max: 82 kB) 82.73 kB added added
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 72.24 kB added added
⛔️ @sentry/browser (incl. Tracing, Replay with Canvas) (max: 87 kB) 87.44 kB added added
⛔️ @sentry/browser (incl. Tracing, Replay, Feedback) (max: 99 kB) 99.69 kB added added
@sentry/browser (incl. Feedback) 42.52 kB added added
@sentry/browser (incl. sendFeedback) 30.39 kB added added
@sentry/browser (incl. FeedbackAsync) 35.44 kB added added
@sentry/browser (incl. Metrics) 27 kB added added
@sentry/browser (incl. Logs) 27.15 kB added added
@sentry/browser (incl. Metrics & Logs) 27.81 kB added added
@sentry/react 27.48 kB added added
@sentry/react (incl. Tracing) 45.98 kB added added
@sentry/vue 30.38 kB added added
⛔️ @sentry/vue (incl. Tracing) (max: 45 kB) 45.51 kB added added
@sentry/svelte 25.75 kB added added
CDN Bundle 28.35 kB added added
⛔️ CDN Bundle (incl. Tracing) (max: 44 kB) 44.57 kB added added
CDN Bundle (incl. Logs, Metrics) 29.21 kB added added
⛔️ CDN Bundle (incl. Tracing, Logs, Metrics) (max: 45 kB) 45.45 kB added added
CDN Bundle (incl. Replay, Logs, Metrics) 68.29 kB added added
⛔️ CDN Bundle (incl. Tracing, Replay) (max: 81 kB) 81.41 kB added added
⛔️ CDN Bundle (incl. Tracing, Replay, Logs, Metrics) (max: 82 kB) 82.31 kB added added
CDN Bundle (incl. Tracing, Replay, Feedback) 86.95 kB added added
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 87.84 kB added added
CDN Bundle - uncompressed 82.76 kB added added
⛔️ CDN Bundle (incl. Tracing) - uncompressed (max: 130 kB) 132.8 kB added added
CDN Bundle (incl. Logs, Metrics) - uncompressed 85.63 kB added added
⛔️ CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed (max: 133 kB) 135.66 kB added added
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 209.26 kB added added
⛔️ CDN Bundle (incl. Tracing, Replay) - uncompressed (max: 247 kB) 249.62 kB added added
⛔️ CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed (max: 250 kB) 252.47 kB added added
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 262.53 kB added added
⛔️ CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed (max: 264 kB) 265.38 kB added added
⛔️ @sentry/nextjs (client) (max: 48 kB) 48.46 kB added added
⛔️ @sentry/sveltekit (client) (max: 44 kB) 44.1 kB added added
⛔️ @sentry/node-core (max: 57 kB) 58.27 kB added added
@sentry/node 175.15 kB added added
@sentry/node - without tracing 98.18 kB added added
@sentry/aws-serverless 115.28 kB added added

@Lms24 Lms24 force-pushed the lms/feat-span-first branch from 8bf8eaf to c966a4a Compare March 18, 2026 17:14
@logaretm logaretm force-pushed the awad/js-17931-webvitals-v2-spans branch from ce4aa43 to 1a5cfb3 Compare March 18, 2026 18:27
logaretm and others added 3 commits March 18, 2026 14:28
…NTRY_MAP

Add `addFcpInstrumentationHandler` using the existing `onFCP` web-vitals
library integration, following the same pattern as the other metric handlers.
Export `INP_ENTRY_MAP` from inp.ts for reuse in the new web vital spans module.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…is enabled

Add non-standalone web vital spans that flow through the v2 span streaming
pipeline (afterSpanEnd -> captureSpan -> SpanBuffer). Each web vital gets
`browser.web_vital.<metric>.value` attributes and span events for measurement
extraction. Spans have meaningful durations showing time from navigation start
to the web vital event (except CLS which is a score, not a duration).

New tracking functions: trackLcpAsSpan, trackClsAsSpan, trackInpAsSpan,
trackTtfbAsSpan, trackFcpAsSpan, trackFpAsSpan — wired up in
browserTracingIntegration.setup() when hasSpanStreamingEnabled(client).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Playwright integration tests verifying CLS, LCP, FCP, FP, and TTFB
are emitted as streamed spans with correct attributes, value attributes,
and meaningful durations when span streaming is enabled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@logaretm logaretm force-pushed the awad/js-17931-webvitals-v2-spans branch from 1a5cfb3 to 28c0d45 Compare March 18, 2026 18:28
@github-actions
Copy link
Contributor

github-actions bot commented Mar 18, 2026

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

  • (browser) Emit web vitals as streamed spans by logaretm in #19827
  • (remix) Server Timing Headers Trace Propagation by onurtemizkan in #18653
  • Span Streaming (WIP) by Lms24 in #19119

Bug Fixes 🐛

Deps

  • Bump devalue 5.6.3 to 5.6.4 to fix CVE-2026-30226 by chargome in #19849
  • Bump file-type to 21.3.2 and @nestjs/common to 11.1.17 by chargome in #19847
  • Bump unhead 2.1.4 to 2.1.12 to fix CVE-2026-31860 and CVE-2026-31873 by chargome in #19848
  • Bump flatted 3.3.1 to 3.4.2 to fix CVE-2026-32141 by chargome in #19842
  • Bump tar 7.5.10 to 7.5.11 to fix CVE-2026-31802 by chargome in #19846
  • Bump hono 4.12.5 to 4.12.7 in cloudflare-hono E2E test app by chargome in #19850
  • Bump undici 6.23.0 to 6.24.1 to fix multiple CVEs by chargome in #19841

Other

  • (deno) Clear pre-existing OTel global before registering TracerProvider by sergical in #19723
  • (node-core) Recycle propagationContext for each request by Lms24 in #19835

Internal Changes 🔧

  • (deps) Bump next from 16.1.5 to 16.1.7 in /dev-packages/e2e-tests/test-applications/nextjs-16 by dependabot in #19851
  • (react) Add gql tests for react router by chargome in #19844
  • (release) Switch from action-prepare-release to Craft by BYK in #18763

🤖 This preview updates automatically when you update the PR.

@logaretm logaretm marked this pull request as ready for review March 18, 2026 18:29
@logaretm logaretm requested review from Lms24 and Copilot and removed request for Copilot March 18, 2026 18:29
Comment on lines +273 to +277
export function trackTtfbAsSpan(client: Client): void {
addTtfbInstrumentationHandler(({ metric }) => {
_sendTtfbSpan(metric.value, client);
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The trackTtfbAsSpan function immediately emits a TTFB span during setup() using a cached metric value, before the parent pageload span is created in afterAllSetup().
Severity: HIGH

Suggested Fix

To fix this, the addMetricObserver function should be modified to not immediately invoke the callback with a previousValue. Alternatively, the calls to trackTtfbAsSpan and other web vital span tracking functions should be moved from the setup hook to the afterAllSetup hook, ensuring they run after the pageload span has been created and is active.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/browser-utils/src/metrics/webVitalSpans.ts#L273-L277

Potential issue: During the `setup()` lifecycle hook, `startTrackingWebVitals()` is
called, which instruments and caches the Time to First Byte (TTFB) metric in a
`_previousTtfb` variable. Subsequently, if span streaming is enabled,
`trackTtfbAsSpan()` is called within the same `setup()` hook. This function calls
`addMetricObserver`, which detects the cached `_previousTtfb` value and immediately
invokes its callback to create a TTFB span. This occurs synchronously during setup,
before the main pageload span is created in the `afterAllSetup()` hook. As a result, the
TTFB span is emitted prematurely without a parent span, leading to an incorrect trace
hierarchy and missing context.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

};

addInpInstrumentationHandler(onInp);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing INP duration sanity check in streamed span path

Medium Severity

The existing standalone INP handler in inp.ts includes a MAX_PLAUSIBLE_INP_DURATION (60 seconds) sanity check to guard against "occasional reports of hour-long INP values." The new trackInpAsSpan in webVitalSpans.ts omits this check, meaning it will emit streamed spans for unrealistically long INP durations that the standalone path already filters out.

Additional Locations (1)
Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant