A Capacitor/WebView app stuck on its splash with ZERO error telemetry has exactly three mechanism classes, and you can discriminate them without touching the device:
- Pre-hydration main-thread wedge — JS blocks synchronously during module-graph eval. CSS animations keep running (compositor thread), so the splash 'pulses' forever. Crucially, a JS boot watchdog (
setTimeout(showFailure, 10s)) categorically CANNOT fire during a wedge: timers need the event loop. Signature: zero events in BOTH the error tracker and the analytics tool (both init post-bundle-exec). - Reload loop that hydrates each lap (JS-driven
location.reload, version-skew auto-reload, etc.) — each lap re-arms the timeout and clears any 'consecutive failed boots' counter that is cleared at mount. Signature: NO error events but MANY rapid-fire analytics pageviews from one device. - Renderer crash → Activity recreate loop — resets JS timers every lap; a localStorage counter incremented at parse-time catches it (see Boot-failure timeout screens are defeated by WebView renderer crash loops; persist a cross-reload failure counter). Signature: no analytics, but the counter-driven failure screen appears within a few launches.
So the one-query remote discriminator is the ANALYTICS channel (PostHog/GA), not the error tracker: hydration fires pageviews, a wedge fires none, a reload loop fires many. Query analytics by device UA for the reporter's device before theorizing.
Two design rules that fall out:
- The consecutive-failure check must run SYNCHRONOUSLY at HTML parse time (
if (fails >= 3) showFailure()inline, before the module graph loads) — that makes it fire even under a wedge, on the Nth launch, when no timer ever could. - Clearing the failure counter at hydration/mount leaves a gap: a hydrate-then-die loop never accumulates. Clear it only after the app survives N seconds past mount (or after the boot route settles).
Ultimate fix for observability: the only watchdog that can SEE a wedge live is native-side (a Java/Swift timer checking whether JS reported 'hydrated' over the bridge by T+15s), paired with manifest/native auto-init of the error SDK — with bridge-initialized-only native SDKs (e.g. @sentry/capacitor default), a boot that never runs JS is invisible on every channel.