fix(analytics): stop dropping client events captured before PostHog init - #6988
Conversation
posthog.capture is a no-op until init() has run — its body sits behind if (this.__loaded) with no buffer and no warning. PostHogProvider reaches init through two dynamic imports while a caller needs only one, so mount-time events and error-boundary crash reports during first paint landed in that dead window and were discarded silently. Also routes the workflow canvas error boundary through captureException so its crashes reach error tracking with a parsed stack.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview The workflow canvas error boundary also calls Adds a Reviewed by Cursor Bugbot for commit 0ba6c70. Configure here. |
Greptile SummaryThe PR prevents client analytics captured before PostHog initialization from being discarded and improves browser exception reporting.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/posthog/client.ts | Introduces a one-shot readiness promise used by event and exception capture helpers. |
| apps/sim/app/_shell/providers/posthog-provider.tsx | Settles analytics readiness on enabled, disabled, and load-failure paths and installs exception filtering. |
| apps/sim/lib/posthog/exception-filter.ts | Adds fail-open filtering for known browser-generated exception noise. |
| apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/error/index.tsx | Sends canvas crashes to Error Tracking while preserving the existing named event. |
| apps/sim/lib/posthog/client.test.ts | Tests the complete readiness lifecycle in one self-contained case, resolving the prior ordering issue. |
| apps/sim/lib/posthog/client-disabled.test.ts | Tests the disabled path in an isolated file using static imports, resolving the prior convention issue. |
Sequence Diagram
sequenceDiagram
participant Caller
participant Gate as PostHog readiness gate
participant Provider as PostHogProvider
participant PH as PostHog
Caller->>Gate: capture event or exception
alt Analytics initializes
Provider->>PH: init()
Provider->>Gate: settle(PH)
Gate->>PH: flush queued captures
else Disabled or load fails
Provider->>Gate: settle(null)
Gate-->>Caller: discard queued captures safely
end
Caller->>Gate: subsequent capture
Gate->>PH: send directly
Reviews (2): Last reviewed commit: "test(analytics): make posthog client tes..." | Re-trigger Greptile
ResizeObserver loop notices were 92% of everything client-side exception autocapture reported. They are specified behaviour, not faults — the observer defers the remaining notifications to the next frame — and they carry no stack that points anywhere. Alongside opaque cross-origin "Script error." reports and cancellation signals (fetch AbortError, Monaco's CancellationError), they bury the handful of real crashes. Filters them in before_send so they never leave the browser. Fails open on anything unrecognized: a wrong guess here would delete product analytics, not just over-report.
Two holes in the exception filter, both found reading PostHog's coercers rather than trusting the shape. The DOMException coercer always reports type "DOMException" and folds the name into the value as "AbortError: signal is aborted without reason", so matching AbortError on type alone never fired for a real aborted fetch — the filter was dead code for exactly the events it was written for. Match the error name wherever the coercer put it. The filter also applied to deliberate captureException reports, which our error boundaries make. Only exceptions the browser raised itself (mechanism.handled === false) are now eligible; a report we chose to send is never dropped.
Readiness is module-level and settles once, so the capture and exception cases silently depended on an earlier case having settled the gate and failed when run in isolation. Walk the lifecycle in one case instead. Moves the disabled-analytics case to its own file, where vitest's per-file isolation supplies an unsettled gate — dropping the vi.resetModules() and dynamic import the repo's testing conventions rule out. Replaces the hand-rolled setTimeout flush with vi.waitFor.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 0ba6c70. Configure here.
Summary
Two fixes to client-side exception logging, found while working out why captured crashes weren't diagnosable.
Events captured before init were silently discarded.
posthog.captureis a no-op untilposthog.init()has run — its entire body sits behindif (this.__loaded)with no buffer and no warning.PostHogProviderreaches init through two dynamic imports (posthog-jsandposthog-js/react) while a caller needs only one, so anything captured on mount resolved first and landed in that dead window:login_page_viewed,signup_page_viewed,landing_page_viewed, and — worst — a crash report from an error boundary that fired during first paint. The oldtypeof posthog.capture === 'function'guard could not catch this; the method exists from module load. Added a readiness gate thatPostHogProvidersettles on all three branches (enabled, disabled, load-failed), so pre-init events are held and flushed instead of dropped.Canvas crashes never reached Error Tracking. The workflow canvas error boundary reported them as a custom event with the message copied into a string property — no stack, no grouping, no issue, no session-replay link. It now also calls
captureException, which parses the stack into$exception_list. The named event stays: it answers a different question ("how often does the canvas fall over") against a stable name that survives the error tracker's own grouping and resolution.Browser noise was drowning the real crashes.
ResizeObserver loop completed with undelivered notificationswas 92% of everything autocapture reported. It is specified behaviour rather than a fault — the observer defers the remaining notifications to the next frame — and it carries no stack that points anywhere useful. Alongside opaque cross-originScript error.reports and cancellation signals (fetchAbortError, Monaco'sCancellationError), it left a handful of genuine crashes unfindable. Filtered inbefore_sendso these never leave the browser. The filter fails open on anything it does not recognize: it runs on every event, so a wrong guess would delete product analytics rather than merely over-report, and a chained exception is dropped only when every link is noise.Also dropped the now-unnecessary runtime
import('posthog-js')from the capture helpers — they take the instance from the provider, so that module is type-only at runtime.Type of Change
Testing
Added
apps/sim/lib/posthog/client.test.ts(pre-init hold-and-flush, post-init path,captureExceptionroute, analytics-disabled path) andapps/sim/lib/posthog/exception-filter.test.ts(each filtered class, non-exception passthrough, chained exceptions, and the fail-open cases).bun run lint,bun run check:audits(32 audits),check-block-registry, andtype-checkall pass.Checklist