Skip to content

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

Open
logaretm wants to merge 28 commits intodevelopfrom
awad/js-17931-webvitals-v2-spans
Open

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

Conversation

@logaretm
Copy link
Copy Markdown
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).

Only LCP, CLS, and INP are emitted as streamed spans — TTFB, FCP, and FP remain as attributes on the pageload span. When span streaming is enabled, standalone v1 CLS/LCP spans are automatically disabled to prevent duplicates.

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 (with MAX_PLAUSIBLE_INP_DURATION sanity check)

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

Changes

  • Emit LCP, CLS, INP as streamed spans when hasSpanStreamingEnabled(client) is true
  • Disable standalone CLS/LCP spans when span streaming is enabled (!spanStreamingEnabled && enableStandaloneClsSpans)
  • Add MAX_PLAUSIBLE_INP_DURATION (60s) sanity check to streamed INP path, matching the existing standalone handler
  • TTFB, FCP, FP are not emitted as spans — they stay as pageload span attributes

🤖 Generated with Claude Code

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 16, 2026

size-limit report 📦

Path Size % Change Change
@sentry/browser 25.72 kB added added
@sentry/browser - with treeshaking flags 24.21 kB added added
⛔️ @sentry/browser (incl. Tracing) (max: 43 kB) 43.2 kB added added
⛔️ @sentry/browser (incl. Tracing, Profiling) (max: 48 kB) 48.05 kB added added
⛔️ @sentry/browser (incl. Tracing, Replay) (max: 82 kB) 82.22 kB added added
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 71.76 kB added added
@sentry/browser (incl. Tracing, Replay with Canvas) 86.91 kB added added
⛔️ @sentry/browser (incl. Tracing, Replay, Feedback) (max: 99 kB) 99.14 kB added added
@sentry/browser (incl. Feedback) 42.51 kB added added
@sentry/browser (incl. sendFeedback) 30.39 kB added added
@sentry/browser (incl. FeedbackAsync) 35.38 kB added added
@sentry/browser (incl. Metrics) 27.03 kB added added
@sentry/browser (incl. Logs) 27.18 kB added added
@sentry/browser (incl. Metrics & Logs) 27.86 kB added added
@sentry/react 27.48 kB added added
@sentry/react (incl. Tracing) 45.49 kB added added
@sentry/vue 30.56 kB added added
⛔️ @sentry/vue (incl. Tracing) (max: 45 kB) 45.05 kB added added
@sentry/svelte 25.74 kB added added
CDN Bundle 28.39 kB added added
⛔️ CDN Bundle (incl. Tracing) (max: 44 kB) 44.23 kB added added
CDN Bundle (incl. Logs, Metrics) 29.76 kB added added
⛔️ CDN Bundle (incl. Tracing, Logs, Metrics) (max: 45 kB) 45.31 kB added added
CDN Bundle (incl. Replay, Logs, Metrics) 68.57 kB added added
⛔️ CDN Bundle (incl. Tracing, Replay) (max: 81 kB) 81.08 kB added added
⛔️ CDN Bundle (incl. Tracing, Replay, Logs, Metrics) (max: 82 kB) 82.12 kB added added
CDN Bundle (incl. Tracing, Replay, Feedback) 86.63 kB added added
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 87.66 kB added added
CDN Bundle - uncompressed 82.91 kB added added
⛔️ CDN Bundle (incl. Tracing) - uncompressed (max: 130 kB) 131.93 kB added added
CDN Bundle (incl. Logs, Metrics) - uncompressed 87.06 kB added added
⛔️ CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed (max: 134 kB) 135.34 kB added added
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 210.04 kB added added
⛔️ CDN Bundle (incl. Tracing, Replay) - uncompressed (max: 247 kB) 248.78 kB added added
⛔️ CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed (max: 250 kB) 252.18 kB added added
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 261.69 kB added added
⛔️ CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed (max: 264 kB) 265.08 kB added added
@sentry/nextjs (client) 47.96 kB added added
@sentry/sveltekit (client) 43.65 kB added added
@sentry/node-core 57.86 kB added added
@sentry/node 174.8 kB added added
@sentry/node - without tracing 97.97 kB added added
@sentry/aws-serverless 115.22 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 2 times, most recently from 1a5cfb3 to 28c0d45 Compare March 18, 2026 18:28
@github-actions
Copy link
Copy Markdown
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 ✨

Core

  • Support registerTool/registerResource/registerPrompt in MCP integration by betegon in #20071
  • Support embeddings in langchain by nicohrubec in #20017

Deps

  • Bump @hapi/content from 6.0.0 to 6.0.1 by dependabot in #20102
  • Bump bundler plugins to 5.2.0 by chargome in #20122
  • Bump lodash.template from 4.5.0 to 4.18.1 by dependabot in #20085
  • Bump @xmldom/xmldom from 0.8.3 to 0.8.12 by dependabot in #20066

Other

  • (aws-serverless) Add lambda extension to npm package by andreiborza in #20133
  • (browser) Emit web vitals as streamed spans by logaretm in #19827
  • (cloudflare) Support basic WorkerEntrypoint by JPeer264 in #19884
  • (core, node) Portable Express integration by isaacs in #19928
  • (deno) Add denoRuntimeMetricsIntegration by chargome in #20023
  • (node, bun) Enforce minimum collection interval in runtime metrics integrations by chargome in #20068
  • (react-router) Export sentryOnError by chargome in #20120
  • Span Streaming by Lms24 in #19119

Bug Fixes 🐛

Core

  • Only attach flags context to error events by Lms24 in #20116
  • Replace regex with string check in stack parser to prevent main thread blocking by chargome in #20089
  • Set span.status to error when MCP tool returns JSON-RPC error response by betegon in #20082

Other

  • (angular) Bump TypeScript to ~6.0.0 in angular-21 E2E test app by andreiborza in #20134
  • (aws-serverless) Add timeout to _endSpan forceFlush to prevent Lambda hanging by logaretm in #20064
  • (cloudflare) Ensure every request instruments functions by JPeer264 in #20044
  • (gatsby) Fix errorHandler signature to match bundler-plugin-core API by JPeer264 in #20048

Internal Changes 🔧

Core

  • Do not emit spans for chats.create in google-genai by nicohrubec in #19990
  • Unify .do* span ops to gen_ai.generate_content by nicohrubec in #20074
  • Simplify addResponseAttributes in openai integration by nicohrubec in #20013
  • Extract shared endStreamSpan for AI integrations by nicohrubec in #20021
  • Remove provider-specific AI span attributes in favor of gen_ai attributes in sentry conventions by nicohrubec in #20011

Deps

  • Bump mshick/add-pr-comment from dd126dd8c253650d181ad9538d8b4fa218fc31e8 to e7516d74559b5514092f5b096ed29a629a1237c6 by dependabot in #20078
  • Bump getsentry/craft/.github/workflows/changelog-preview.yml from 2.24.1 to 2.25.2 by dependabot in #20081

Other

  • (node) Add node integration tests for Vercel ToolLoopAgent by nicohrubec in #20087
  • (nuxt) Make Nuxt 5 (nightly) E2E optional by s1gr1d in #20113
  • (oxlint) Add typeawareness into oxlintrc by JPeer264 in #20075
  • Update validate-pr workflow by stephanie-anderson in #20072
  • Remove unused tsconfig-template folder by mydea in #20067

🤖 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
@Lms24 Lms24 force-pushed the lms/feat-span-first branch from c966a4a to 5963170 Compare March 23, 2026 09:41
@logaretm logaretm force-pushed the awad/js-17931-webvitals-v2-spans branch from 28c0d45 to af2969e Compare March 23, 2026 18:03
Copilot AI review requested due to automatic review settings March 23, 2026 18:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds support for emitting certain Web Vitals as streamed (v2 pipeline) spans when traceLifecycle: 'stream' / span streaming is enabled, while keeping existing pageload measurements in place.

Changes:

  • Gate standalone CLS/LCP spans off when span streaming is enabled, and wire up streamed LCP/CLS/INP emission from the browser tracing integration.
  • Introduce webVitalSpans.ts helpers + unit tests for emitting streamed Web Vital spans.
  • Add Playwright integration tests for streamed LCP and CLS spans; export INP_ENTRY_MAP; add (currently-unused) FCP metric instrumentation.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/browser/src/tracing/browserTracingIntegration.ts Enables streamed Web Vital span tracking when span streaming is enabled; disables standalone CLS/LCP in that mode
packages/browser-utils/src/metrics/webVitalSpans.ts Implements streamed span emitters for LCP/CLS/INP
packages/browser-utils/test/metrics/webVitalSpans.test.ts Unit tests for streamed web vital span emission helpers
packages/browser-utils/src/metrics/instrument.ts Adds FCP metric instrumentation plumbing (fcp observer + handler)
packages/browser-utils/src/metrics/inp.ts Exports INP_ENTRY_MAP for reuse by streamed INP span logic
packages/browser-utils/src/index.ts Re-exports streamed web vital span trackers from browser-utils
dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/test.ts Playwright test validating streamed LCP span + attributes
dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/template.html Test page for streamed LCP
dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/init.js Initializes SDK with span streaming enabled for LCP test
dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/assets/sentry-logo-600x179.png Asset used to trigger LCP
dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/test.ts Playwright test validating streamed CLS span + attributes
dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/template.html Test page for streamed CLS
dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/subject.js Simulates CLS for the CLS streamed span test
dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/init.js Initializes SDK with span streaming enabled for CLS test

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@logaretm logaretm force-pushed the awad/js-17931-webvitals-v2-spans branch from d764f2e to b80ddd4 Compare March 24, 2026 14:28
@logaretm logaretm force-pushed the awad/js-17931-webvitals-v2-spans branch from a74fec7 to 7206304 Compare March 24, 2026 15:00
@logaretm logaretm force-pushed the awad/js-17931-webvitals-v2-spans branch 2 times, most recently from 86d1929 to 4bf129b Compare March 24, 2026 15:38
Copy link
Copy Markdown

@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.

There are 2 total unresolved issues (including 1 from previous review).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Redundant CLS/LCP tracking when streaming is enabled
    • Added recordClsOnPageloadSpan and recordLcpOnPageloadSpan parameters to startTrackingWebVitals to prevent redundant handler registration when span streaming is enabled, eliminating the double-handler issue where one handler did throwaway work.

Create PR

Or push these changes by commenting:

@cursor push d3ccbaa211
Preview (d3ccbaa211)
diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts
--- a/packages/browser-utils/src/metrics/browserMetrics.ts
+++ b/packages/browser-utils/src/metrics/browserMetrics.ts
@@ -77,6 +77,8 @@
 interface StartTrackingWebVitalsOptions {
   recordClsStandaloneSpans: boolean;
   recordLcpStandaloneSpans: boolean;
+  recordClsOnPageloadSpan?: boolean;
+  recordLcpOnPageloadSpan?: boolean;
   client: Client;
 }
 
@@ -89,6 +91,8 @@
 export function startTrackingWebVitals({
   recordClsStandaloneSpans,
   recordLcpStandaloneSpans,
+  recordClsOnPageloadSpan = true,
+  recordLcpOnPageloadSpan = true,
   client,
 }: StartTrackingWebVitalsOptions): () => void {
   const performance = getBrowserPerformanceAPI();
@@ -97,10 +101,22 @@
     if (performance.mark) {
       WINDOW.performance.mark('sentry-tracing-init');
     }
-    const lcpCleanupCallback = recordLcpStandaloneSpans ? trackLcpAsStandaloneSpan(client) : _trackLCP();
+    let lcpCleanupCallback: (() => void) | undefined;
+    if (recordLcpStandaloneSpans) {
+      trackLcpAsStandaloneSpan(client);
+    } else if (recordLcpOnPageloadSpan) {
+      lcpCleanupCallback = _trackLCP();
+    }
+
     const ttfbCleanupCallback = _trackTtfb();
-    const clsCleanupCallback = recordClsStandaloneSpans ? trackClsAsStandaloneSpan(client) : _trackCLS();
 
+    let clsCleanupCallback: (() => void) | undefined;
+    if (recordClsStandaloneSpans) {
+      trackClsAsStandaloneSpan(client);
+    } else if (recordClsOnPageloadSpan) {
+      clsCleanupCallback = _trackCLS();
+    }
+
     return (): void => {
       lcpCleanupCallback?.();
       ttfbCleanupCallback();

diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts
--- a/packages/browser/src/tracing/browserTracingIntegration.ts
+++ b/packages/browser/src/tracing/browserTracingIntegration.ts
@@ -519,6 +519,8 @@
       _collectWebVitals = startTrackingWebVitals({
         recordClsStandaloneSpans: !spanStreamingEnabled && (enableStandaloneClsSpans || false),
         recordLcpStandaloneSpans: !spanStreamingEnabled && (enableStandaloneLcpSpans || false),
+        recordClsOnPageloadSpan: !spanStreamingEnabled,
+        recordLcpOnPageloadSpan: !spanStreamingEnabled,
         client,
       });

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Lms24 and others added 11 commits April 2, 2026 16:32
This PR adds span JSON conversion and serialization helpers for span
streaming:

* `spanToStreamedSpanJSON`: Converts a `Span` instance to a JSON object
used as intermediate representation as outlined in
#19100
* Adds `SentrySpan::getStreamedSpanJSON` method to convert our own spans
  * Directly converts any OTel spans
  * This is analogous to how `spanToJSON` works today.
* `spanJsonToSerializedSpan`: Converts a `StreamedSpanJSON` into the
final `SerializedSpan` to be sent to Sentry.

This PR also adds unit tests for both helpers.

ref #17836

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Jan Peer Stöcklmair <jan.peer@sentry.io>
This PR adds the `captureSpan` pipeline, which takes a `Span` instance,
processes it and ultimately returns a `SerializedStreamedSpan` which can
then be enqueued into the span buffer.

ref #17836
This PR adds a simple span buffer implementation to be used for
buffering streamed spans.

Behaviour:
- buckets incoming spans by `traceId`, as we must not mix up spans of
different traces in one envelope
- flushes the entire buffer every 5s by default
- flushes the specific trace bucket if the max span limit (1000) is
reached. Relay accepts at max. 1000 spans per envelope
- computes the DSC when flushing the first span of a trace. This is the
latest time we can do it as once we flushed we have to freeze the DSC
for Dynamic Sampling consistency
- debounces the flush interval whenever we flush
- flushes the entire buffer if `Sentry.flush()` is called
- shuts down the interval-based flushing when `Sentry.close()` is called
- [implicit] Client report generation for dropped envelopes is handled
in the transport

Methods:
- `add` accepts a new span to be enqueued into the buffer
- `drain` flushes the entire buffer
- `flush(traceId)` flushes a specific traceId bucket. This can be used
by e.g. the browser span streaming implementation to flush out the trace
of a segment span directly once it ends.

Options:
- `maxSpanLimit` - allows to configure a 0 < maxSpanLimit < 1000 custom
span limit. Useful for testing but we could also expose this to users if
we see a need
- `flushInterval`- allows to configure a >0 flush interval

Limitations/edge cases:
- No maximum limit of concurrently buffered traces. I'd tend to accept
this for now and see where this leads us in terms of memory pressure but
at the end of the day, the interval based flushing, in combination with
our promise buffer _should_ avoid an ever-growing map of trace buckets.
Happy to change this if reviewers have strong opinions or I'm missing
something important!
- There's no priority based scheduling relative to other telemetry
items. Just like with our other log and metric buffers.
- since `Map` is [insertion order
preserving](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#description),
we apply a FIFO strategy when`drain`ing the trace buckets. This is in
line with our [develop
spec](https://develop.sentry.dev/sdk/telemetry/telemetry-processor/backend-telemetry-processor/#:~:text=The%20span%20buffer,in%20the%20buffer.)
for the telemetry processor but might lead to cases where new traces are
dropped by the promise buffer if a lof of concurrently running traces
are flushed. I think that's a fine trade off.

ref #19119
This PR adds the final big building block for span streaming
functionality in the browser SDK: `spanStreamingIntegation`.

This integration:
- enables `traceLifecycle: 'stream'` if not already set by users. This
allows us to avoid the double-opt-in problem we usually have in browser
SDKs because we want to keep integration tree-shakeable but also support
the runtime-agnostic `traceLifecycle` option.
- to do this properly, I decided to introduce a new integration hook:
`beforeSetup`. This is allows us to safely modify client options before
other integrations read it. We'll need this because
`browserTracingIntegration` needs to check for span streaming later on.
Let me know what you think!
- validates that `beforeSendSpan` is compatible with span streaming. If
not, it falls back to static tracing (transactions).
- listens to a new `afterSpanEnd` hook. Once called, it will capture the
span and hand it off to the span buffer.
- listens to a new `afterSegmentSpanEnd` hook. Once called it will flush
the trace from the buffer to ensure we flush out the trace as soon as
possible. In browser, it's more likely that users refresh or close the
tab/window before our buffer's internal flush interval triggers. We
don't _have_ to do this but I figured it would be a good trigger point.

While "final building block" sounds nice, there's still a lot of stuff
to take care of in the browser. But with this in place we can also start
integration-testing the browser SDKs.

ref #17836

---------

Co-authored-by: Jan Peer Stöcklmair <jan.peer@sentry.io>
Adds weight-based flushing and span size estimation to the span buffer.

Behaviour:
- tracks weight independently per trace
- weight estimation follows the same strategy we use for logs and
metrics. I optimized the calculation, adding fixed sizes for as many
fields as possible. Only span name, attributes and links are computed
dynamically, with the same assumptions and considerations as in logs and
metrics.
- My tests show that the size estimation roughly compares to factor 0.8
to 1.2 to the real sizes, depending on data on spans (no, few, many,
primitive, array attributes and links, etc.)
- For now, the limit is set to 5MB which is half of the 10MB Relay
accepts for span envelopes.
This PR adds browser integration to test testing span streaming:

- Added test helpers:
  - `waitForStreamedSpan`: Returns a promise of a single matching span
- `waitForStreamedSpans`: Returns a promise of all spans in an array
whenever the callback returns true
- `waitForStreamedSpanEnvelope`: Returns an entire streamed span (v2)
envelope (including headers)
- `observeStreamedSpan`: Can be used to observe sent span envelopes
without blocking the test if no envelopes are sent (good for testing
that spans are _not_ sent)
- `getSpanOp`: Small helper to easily get the op of a span which we
almost always need for the `waitFor*` function callbacks

Added 50+ tests, mostly converted from transaction integration tests
around spans from `browserTracingIntegration`:
- tests asserting the entire span v2 envelope payloads of manually
started, pageload and navigation span trees
- tests for trace linking and trace lifetime
- tests for spans coming from browserTracingIntegration (fetch, xhr,
long animation frame, long tasks)

Also, this PR fixes two bugs discovered through tests:
- negatively sampled spans were still sent (because non-recording spans
go through the same span life cycle)
- cancelled spans received status `error` instead of `ok`. We want them
to have status `ok` but an attribute detailing the cancellation reason.

Lastly, I discovered a problem with timing data on fetch and XHR spans.
Will try to fix as a follow-up. Tracked in #19613

ref #17836
…spans (#19643)

This PR fixes a temporary bug in span streaming where we didn't add Http
timing attributes (see
#19613). We can fix
this by following OTels approach:

- delay the ending of `http.client` spans until either 300ms pass by or
we receive the PerformanceResourceTiming entry with the respective
timing information. Of course we end the span with the original
timestamp then.
- Unfortunately, we can only do this for streamed span because
transaction-based spans cannot stay open longer than their parent (e.g.
a pageload or navigation). Otherwise they'd get dropped. So we have to
differentiate between the two modes here (RIP bundle size 😢)
- To ensure we don't flush unnecessarily often, we also now delay
flushing the span buffer for 500ms after a segment span ends. This
slightly changed test semantics in a few integration tests because
manually consecutively segments are now also sent in one envelope. This
is completely fine (actually preferred) because we flush less often
(i.e. fewer requests).

closes #19613
… flushing (#19686)

As discussed yesterday with @cleptric, we don't want a global
interval-based flushing but the interval shall be set per trace bucket.
This PR makes that change.
…19741)

This PR adds a server-side span streaming implementation, for now scoped
to POtel SDKs. However we can reuse some stuff from this PR to very
easily enable span streaming on Cloudflare, Vercel Edge and other
OTel-less platforms.

Main changes:

- added `spanStreamingIntegration` to `@sentry/core`: This orchestrates
the span streaming life cycle via the client and the span buffer. It's
very similar to the already existing `spanStreamingIntegration` in
browser but doesn't expose some of the behaviour that we need only in
browser.
- adjusted `SentrySpanProcessor` to emit the right client hooks instead
of passing the span to the `SpanExporter`.
- adjusted the SDKs' default integrations to include
`spanStreamingIntegration` when users set `traceLifecycle: 'stream'` in
their SDK init.

Rest are tests and small refactors. I'll follow up with Node integration
tests once this is merged to avoid bloating this PR further.

ref #17836
Extends our Node (core) integration test runner API to expect `span`
envelopes with both an API to test against span envelope headers as well
as the container. In addition, this adds a couple of integration tests
testing manually started spans, span name updates and span
relationships. Luckily, all of the span relationship logic is
independent from span streaming so the tests all pass. I still believe
it's valuable to have a `span` version of them.
This PR Implements applying the `ignoreSpans` option to streamed spans
(previously this option had no effect). It covers both, our
core/browser- as well as the OTel/Node-based implementation.
See PR for details
@Lms24 Lms24 force-pushed the lms/feat-span-first branch from 8613474 to e360c3b Compare April 2, 2026 14:32
logaretm and others added 14 commits April 2, 2026 10:36
…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>
…dalone spans when streaming

TTFB, FCP, and FP should remain as attributes on the pageload span rather than
separate streamed spans. Also ensures standalone CLS/LCP spans are disabled when
span streaming is enabled to prevent duplicate spans.

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

The standalone INP handler filters out unrealistically long INP values
(>60s) but the streamed span path was missing this sanity check.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Gate standalone INP (`startTrackingINP`) behind `!spanStreamingEnabled`
and gate streamed INP (`trackInpAsSpan`) behind `enableInp` so both
paths respect the user's preference and don't produce duplicate data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove `addFcpInstrumentationHandler`, `instrumentFcp`, and
`_previousFcp` which were added to support FCP streamed spans but are
no longer called after FCP spans were removed from the implementation.

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

Use `|| 0` fallback instead of `as number` cast, consistent with the
LCP and CLS span handlers that already guard against undefined.

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

Avoid calling browserPerformanceTimeOrigin() twice by caching the
result in a local variable.

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

The streamed INP path does not use INTERACTIONS_SPAN_MAP or
ELEMENT_NAME_TIMESTAMP_MAP, so registering the listeners is unnecessary overhead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When span streaming is enabled, CLS and LCP are emitted as streamed
spans. Previously they were also recorded as measurements on the
pageload span because the flags only checked enableStandaloneClsSpans
and enableStandaloneLcpSpans, which default to undefined.

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

Export the constant from inp.ts and import it in webVitalSpans.ts to
avoid the two definitions drifting apart.

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

spanStreamingEnabled was declared in setup() but referenced in
afterAllSetup(), a separate scope. Replace with inline
hasSpanStreamingEnabled(client) call.

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

When span streaming handles CLS/LCP, `startTrackingWebVitals` no longer
registers throwaway `_trackCLS()`/`_trackLCP()` handlers. Instead of
adding a separate skip flag, the existing `recordClsStandaloneSpans` and
`recordLcpStandaloneSpans` options now accept `undefined` to mean "skip
entirely" — three states via two flags instead of three flags.

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 1417584 to b94c3b3 Compare April 2, 2026 14:36
Copy link
Copy Markdown
Member

@Lms24 Lms24 left a comment

Choose a reason for hiding this comment

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

Still had some final questions but most of it LGTM!

Comment on lines +79 to +83
span.addEvent(metricName, {
[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: unit,
[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: value,
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

l: This code path is only used for v2 spans, correct? In this case, we can remove the addEvent call (it no-ops for v2 spans)

Suggested change
span.addEvent(metricName, {
[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: unit,
[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: value,
});

Comment on lines +61 to +62
[`browser.web_vital.${metricName}.value`]: value,
transaction: routeName,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(Still need to double-check with Michi): I tested this and we still need to send the short lcp attributes so that the web vital pills and and lines show up. Let me check if we actually do this or if we don't on purpose and let the data browsing team fix the pipeline

Suggested change
[`browser.web_vital.${metricName}.value`]: value,
transaction: routeName,
[`browser.web_vital.${metricName}.value`]: value,
[metricName]: value,
transaction: routeName,

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');
Copy link
Copy Markdown
Member

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

/**
* Tracks INP as a streamed span.
*/
export function trackInpAsSpan(_client: Client): void {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

m: I might be missing something but it seems like the INP logic isn't the same as the _onInp callback for v1 standalone INP spans

@Lms24 Lms24 force-pushed the lms/feat-span-first branch from e360c3b to 595985b Compare April 8, 2026 12:15
Base automatically changed from lms/feat-span-first to develop April 9, 2026 12:46
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.

Emit web vital spans as v2 spans

3 participants