Skip to content

🐛 fix(web): skip web-vitals CLS/INP without Array.prototype.at, keep SDK-internal errors out of error.count - #10

Merged
nitinstp23 merged 2 commits into
mainfrom
fix/web-vitals-at-and-sdk-errors
Sep 22, 2026
Merged

nitinstp23 merged 2 commits into
mainfrom
fix/web-vitals-at-and-sdk-errors

Conversation

@nitinstp23

Copy link
Copy Markdown
Contributor

Summary

Fixes B14-2082 (snabbit app-webview-rum: TypeError: this.o.at is not a function from scout-LfkEtGwo.js on Android 8.1 / Chrome 87, counted as an application error).

  • Where .at() comes from. Not the SDK source (tsconfig lib: ES2020 would reject it). web-vitals@5.2.0 calls Array.prototype.at in its layout-shift (CLS) and interaction (INP) managers. tsup leaves web-vitals external, so the customer's Vite build inlines it next to the SDK in the scout-<hash>.js chunk. Chrome < 92 has no at, so the observer callback threw on every layout shift for the page's lifetime. The SDK otherwise kept working; CLS was silently never reported.
  • Fix. CLS and INP are registered only when Array.prototype.at exists; LCP, FCP and TTFB still report. No polyfill is shipped (a global shim from an SDK is a footgun for host apps, and esbuild targets lower syntax, never prototype methods).
  • SDK-internal errors. An uncaught error whose top frame is in the SDK bundle or web-vitals (src/core/sdk-origin.ts) now carries error.origin: sdk and error.category: sdk_internal, is emitted once per distinct fingerprint per page, and is excluded from error.count, view.error.count and error_click frustration detection. Every other error carries error.origin: app. The plugin can filter on error.origin.
  • Init resilience. Each install*() in Scout.initialize is mounted in its own guard; a synchronous throw used to leave _providers unset (so flush()/shutdown() became no-ops) and skip every later instrumentation.
  • Browser floor. make check-compat (now in make ci) greps dist/ for runtime APIs newer than Chrome / Android WebView 87 (.at(, structuredClone, Object.hasOwn, findLast, …). README gains a "Browser support" section.

Tests

  • web-vitals.test.ts: CLS/INP skipped when Array.prototype.at is deleted, all five registered otherwise.
  • sdk-origin.test.ts: V8 and JavaScriptCore stack shapes, Vite chunk, unbundled files, app error passing through the SDK stays app.
  • error.test.ts: sdk-origin error flagged and excluded from error.count / view.error.count, reported once per page; app errors counted.
  • frustration.test.ts: app error after a click → error_click; SDK error → not.
  • make ci green locally (build + check-compat + export checks).

Note on ticket wording

The ticket proposed error.source_type: sdk; that field carries the platform (browser / react-native) and the plugin classifies native crashes on it, so the origin lives in a new error.origin attribute instead.

…K-internal errors out of error.count

B14-2082 (snabbit app-webview, Android 8.1 / Chrome 87). web-vitals 5
calls Array.prototype.at while tracking CLS and INP; Chrome < 92 has no
`at`, so its observer callback threw "this.o.at is not a function" on
every layout shift for the life of the page, and our own error tracker
filed each one as an uncaught application error. The SDK never calls
`at` itself: web-vitals is a runtime dependency the app's bundler
inlines next to us, which is why the stack named a scout-<hash>.js
chunk. CLS and INP are now registered only where `at` exists; LCP, FCP
and TTFB keep reporting.

Errors whose top frame is inside the SDK bundle (or web-vitals) now
carry error.origin=sdk and error.category=sdk_internal, are emitted
once per distinct failure per page, and are excluded from error.count,
view.error.count and error_click detection; everything else carries
error.origin=app. Each instrumentation is mounted in its own guard so a
throwing installer cannot abort initialize() before the providers are
registered. New `make check-compat` (in `make ci`) fails the build when
dist/ calls a runtime API newer than Chrome / Android WebView 87; README
documents that floor.
@nitinstp23

Copy link
Copy Markdown
Contributor Author

Verification against scout-rum-example-apps/webview-react-web

Integration build = #9 + #10 + #11 merged locally (make ci green, 345 tests), packed and installed into the harness.

Headless Chrome 153 via Playwright, 29/29 (sessionSampleRate: 100, OTLP exports intercepted and parsed):

  • Marker: written on load with sampled; visibilitychange:hidden clears it and the heartbeat keeps it cleared for 12.5 s; visible/freeze/resume re-arm; a seeded dead sampled session exports exactly one app_unclean_exit attributed to it (session.id = crash.previous_session_id), never app_crash; marker records reportedSessionId; an already-reported session and an unsampled session export nothing.
  • Android WebView UA: no marker written, a pending marker is not filed, other spans still export.
  • Array.prototype.at deleted before init: no TypeError, no error span, CLS/INP absent while TTFB/FCP/LCP report; an app error carries error.origin=app; an error whose top frame is the SDK module carries error.origin=sdk + sdk_internal, is exported once for three throws, and error.count stays at the one app error.
  • 1 s stall → one long_task (LoAF only) + one frozen_frame (1.0 s); a stall overlapping a hidden interval is dropped; an 11 s stall reports 10 s with frozen_frame.capped=true.

Real Android System WebView (emulator API 34, Chrome 113, webview-bridge-flutter mode=off, DevTools over adb), 7/7: UA has wv; Scout.instance.config.enableUncleanExitDetection === false; no marker key in localStorage; 1 s stall → one long_task + one frozen_frame; HOME → visibilityState=hidden + app_paused; return → app_resumed; no app_unclean_exit/app_crash.

Kill cycle with the marker forced on (VITE_SCOUT_UNCLEAN_EXIT=true), 6/6: first launch reports nothing; HOME → 16 s → am force-stop → relaunch: no app_unclean_exit; foreground force-stop → relaunch: exactly one; a second foreground kill of the same (resumed) session: none; never app_crash.

…-sdk-errors

# Conflicts:
#	CHANGELOG.md
#	src/web/index.ts
@nitinstp23
nitinstp23 merged commit 574d43e into main Sep 22, 2026
6 checks passed
nitinstp23 added a commit that referenced this pull request Sep 22, 2026
The merge of origin/main into fix/frozen-frame-background left conflict
markers in src/web/index.ts and CHANGELOG.md, and #11 was merged with
them. index.ts keeps the guarded mount() installer and passes
frozenFrameMaxMs to the long-task tracker; the CHANGELOG stacks the
#9, #10 and #11 entries under [Unreleased]. make ci green (345 tests).
@nitinstp23
nitinstp23 deleted the fix/web-vitals-at-and-sdk-errors branch September 22, 2026 10:08
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