Skip to content

feat(browser): Core Web Vitals as browser.web_vital log events - #1150

Merged
Makisuo merged 4 commits into
feat/browser-sdk-xhr-pageloadfrom
feat/browser-sdk-web-vitals
Sep 29, 2026
Merged

Makisuo merged 4 commits into
feat/browser-sdk-xhr-pageloadfrom
feat/browser-sdk-web-vitals

Conversation

@Makisuo

@Makisuo Makisuo commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Part 4 of the browser SDK stack. Based on #1147.

What changes

  • LCP, CLS, INP, FCP and TTFB, reported with the web-vitals library (same major as apps/web), each as an OTel log-based event named browser.web_vital, with the browser.web_vital.{name,value,delta,id,rating,navigation_type} attributes from the browser semantic conventions, plus url.path.
  • Each event carries session.id and links to the document's pageload span (the deferred chunk now remembers it when the pageload hook fires).
  • On by default; webVitals: false turns it off.

Why events and not OTel metrics

A MeterProvider + metric exporter would add a second SDK to every page and would aggregate away the per-page and per-trace detail that makes a slow LCP debuggable. Percentiles per route come from the warehouse over these events (a rollup is a follow-up on the backend side; until then they are queryable in the logs explorer).

Details worth reviewing

  • The reporter registers before the logs pipeline, so vitals that settle on page hide (CLS, INP, LCP) are emitted before the log processor's own visibilitychange flush listener runs.
  • web-vitals has no unsubscribe, so it registers once per page and shutdown() only gates reporting.
  • Lives in the deferred chunk: deferred budget 8 → 12 kB (web-vitals is ~3.3 kB); the eager bundle is unchanged.

Testing

  • Browser test through the real init() → deferred chunk → exporter path: TTFB arrives as a browser.web_vital event with semconv attributes, session.id, url.path and a pageload trace link.
  • The logs tests turn vitals off, since they report once per page and would leak across tests in the same file.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

LCP, CLS, INP, FCP and TTFB, reported with the web-vitals library, each
as an OTel log-based event named browser.web_vital with the
browser.web_vital.{name,value,delta,id,rating,navigation_type} attributes
from the browser semantic conventions, plus url.path.

Events, not OTel metrics: a MeterProvider and exporter would add another
SDK to the page and lose the per-page, per-trace detail. Aggregates come
from the warehouse. Each event carries session.id and links to the
document's pageload span, which the deferred chunk now remembers.

The reporter registers before the logs pipeline so vitals reported on page
hide are emitted before its flush listener runs. web-vitals has no
unsubscribe, so it registers once per page and shutdown only gates it.

On by default; webVitals: false turns it off. Lives in the deferred chunk
(deferred budget 8 -> 12 kB); eager is unchanged.
@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 58 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 88dc4005-22e7-456b-bd37-a62cd9d71200

📥 Commits

Reviewing files that changed from the base of the PR and between 36fdc83 and ab3f6b9.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • docs/browser-sdk.md
  • packages/browser/README.md
  • packages/browser/package.json
  • packages/browser/scripts/size.ts
  • packages/browser/src/config.ts
  • packages/browser/src/deferred/index.ts
  • packages/browser/src/deferred/web-vitals.ts
  • packages/browser/src/logs.browser.test.ts
  • packages/browser/src/navigation.test.ts
  • packages/browser/src/tracing.browser.test.ts
  • packages/browser/src/web-vitals-untraced.browser.test.ts
  • packages/browser/src/web-vitals.browser.test.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@maple-review-bot

maple-review-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Maple review

Confidence 3/5 · needs attention
The new reporter's pageload gate silently kills the feature when tracing is off or before the first navigation, and no test covers that path.
quality 90/100 · 1 warning · tests partial · risk low · 1/1 new units observable

Adds Core Web Vitals reporting to the browser SDK: LCP, CLS, INP, FCP and TTFB land as browser.web_vital log events from the deferred chunk, gated by a new webVitals option (default true) and linked to the document's pageload span. The wiring is sound apart from the span-dependent guard on the reporter.

  • resolveConfig defaults the new webVitals option to true (config.ts:222)
  • startDeferred captures the pageload SpanContext and starts the vitals reporter before startLogs (deferred/index.ts:11-19)
  • startWebVitals registers web-vitals handlers once and emits browser.web_vital events (web-vitals.ts:33)
  • web-vitals@^6.2.2 added to runtime dependencies (package.json:57)

Findings

Warning · F1 · report drops every web vital while no pageload span exists

correctness · packages/browser/src/deferred/web-vitals.ts:13-30

if (!pageload) return keys the whole event on a span that only exists when tracing is live: with tracing: { enabled: false }, setupTracing never runs, liveMapleTracer is undefined and startNavigation returns before creating a pageload span (navigation.ts:87), so webVitals (default true) emits nothing at all. A vital that settles before the app's first startNavigation — TTFB and FCP typically do, the test only passes because it starts the navigation first — is dropped the same way. The doc comment above promises only the trace link is conditional, and emitLog already accepts an absent spanContext.

function report(metric: Metric): void {
	emitLog({
		eventName: "browser.web_vital",
		severityNumber: Severity.INFO,
		severityText: "INFO",
		attributes: {
			"browser.web_vital.name": metric.name.toLowerCase(),
			"browser.web_vital.value": metric.value,
			"browser.web_vital.delta": metric.delta,
			"browser.web_vital.id": metric.id,
			"browser.web_vital.rating": metric.rating,
			"browser.web_vital.navigation_type": metric.navigationType,
			"url.path": scrubUrl(location.pathname),
		},
		spanContext: pageload?.(),
	})
}
What was checked
  • Vitals reported on page hide precede the log processor's own flush listener: startWebVitals runs before startLogs (deferred/index.ts:17-19)
  • url.path goes through the existing scrubUrl redactor (url-privacy.ts:104), not a raw location.pathname
  • severity text "INFO" and Severity.INFO match logger.ts:28
Observability coverage: 1 of 1 changes observable
Change Kind Observable Evidence
Core Web Vitals reporter (startWebVitals) log-event emitter to ingest yes Emits structured OTLP log events browser.web_vital with semconv attributes, session.id and a pageload trace link (web-vitals.ts:15-29)
Copy all findings (1)
Findings from an automated review of commit 59c5f2c9174718c577f40455edced4f8c36253d9. Verify each one against the current code before changing anything, fix only those that still apply, and keep each fix to the lines it names.

---

F1 · Warning · correctness · packages/browser/src/deferred/web-vitals.ts:13-30
`report` drops every web vital while no `pageload` span exists
`if (!pageload) return` keys the whole event on a span that only exists when tracing is live: with `tracing: { enabled: false }`, `setupTracing` never runs, `liveMapleTracer` is undefined and `startNavigation` returns before creating a pageload span (navigation.ts:87), so `webVitals` (default true) emits nothing at all. A vital that settles before the app's first `startNavigation` — TTFB and FCP typically do, the test only passes because it starts the navigation first — is dropped the same way. The doc comment above promises only the trace link is conditional, and `emitLog` already accepts an absent `spanContext`.
Replace those lines with:
function report(metric: Metric): void {
	emitLog({
		eventName: "browser.web_vital",
		severityNumber: Severity.INFO,
		severityText: "INFO",
		attributes: {
			"browser.web_vital.name": metric.name.toLowerCase(),
			"browser.web_vital.value": metric.value,
			"browser.web_vital.delta": metric.delta,
			"browser.web_vital.id": metric.id,
			"browser.web_vital.rating": metric.rating,
			"browser.web_vital.navigation_type": metric.navigationType,
			"url.path": scrubUrl(location.pathname),
		},
		spanContext: pageload?.(),
	})
}

59c5f2c · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple to ask about one.

@maple-review-bot maple-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 inline note from Maple's review. The score and summary are in the review comment above.

Comment thread packages/browser/src/deferred/web-vitals.ts
report() returned early without a pageload span, so vitals were dropped
with tracing disabled and before the app's first startNavigation (TTFB and
FCP usually settle then). Only the trace link is conditional now.
@maple-review-bot

maple-review-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Maple review

Confidence 5/5 · safe to merge
Contained to the browser SDK's deferred chunk, default-on but gated by webVitals: false, with browser tests through the real init → exporter path.
quality 100/100 · no findings · tests covered · risk low

Adds Core Web Vitals (LCP/CLS/INP/FCP/TTFB) to the browser SDK as browser.web_vital log events, in the deferred chunk, linked to the document's pageload span when one exists. Small, gated by the new webVitals config flag, and covered by two browser tests; safe to merge.

  • startWebVitals emits one browser.web_vital log per metric with semconv attributes plus url.path
  • startDeferred remembers the pageload SpanContext and passes it as the event's link
  • New webVitals config flag, default true, threaded through ResolvedConfig
  • web-vitals@^6.2.2 added to the deferred bundle
What was checked
  • Vitals are reported with tracing off: report no longer keys on a pageload span (web-vitals.ts:30), and web-vitals-untraced.browser.test.ts exercises it
  • deferred/index.ts:11 pageload is function-local, so a second init() cannot link to the previous document's span
  • Ordering: startWebVitals registers its onHidden listeners before startLogs builds the provider (deferred/index.ts:17-19)

e47e51f · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple to ask about one.

@Makisuo
Makisuo added this pull request to stack #1161 September 29, 2026 21:22
@maple-review-bot

maple-review-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Maple review

Confidence 5/5 · safe to merge
The web-vitals code itself is contained and tested; the two files the kickoff named are not part of this PR's diff against its base and could not be reviewed here.
quality 100/100 · no findings · tests covered · risk low · 1/1 new units observable

Adds Core Web Vitals to the browser SDK as browser.web_vital log events emitted from the deferred chunk, each linked to the document's pageload span and gated by a new webVitals flag. Contained and tested; safe to merge.

  • startWebVitals reports LCP/CLS/INP/FCP/TTFB as browser.web_vital log events
  • The deferred chunk links each vital to the document pageload span
  • New webVitals config flag, default true, gates reporting
What was checked
  • Earlier F1 is fixed: report no longer gates emission on a pageload span (web-vitals.ts:14-31), and the untraced browser test asserts an event with no spanContext.
  • webVitals: false is threaded through resolveConfig (config.ts:222) and set in both browser test configs, so vitals cannot leak between tests in one file.
  • startWebVitals keeps the once-per-page library registration and only gates reporting, so a later init() resumes reporting (web-vitals.ts:38-45).
Observability coverage: 1 of 1 changes observable
Change Kind Observable Evidence
Web vital reporting (LCP, CLS, INP, FCP, TTFB) → browser.web_vital log event log-based event yes web-vitals.ts:16-31 emits a structured event with semconv attributes, session.id, url.path and the pageload span link

ab3f6b9 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple to ask about one.

@Makisuo
Makisuo merged commit 2564a36 into main Sep 29, 2026
44 checks passed
@Makisuo
Makisuo deleted the feat/browser-sdk-web-vitals branch September 29, 2026 21:44
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