fix(surveys): hold captures until the survey event receiver is hooked

trial_urgency_banner_viewed fires ~1s into the page load and raced the lazy
surveys extension — events captured before its hook exists can never activate
an event-triggered survey (affects real users, not just e2e). capture() now
awaits surveysReceiverReady() (onSurveysLoaded, 3s safety timeout) and runs the
eligibility check after the event has gone through.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 00:55:06 +03:00
parent c1ae18e5f1
commit f29730fae0
2 changed files with 27 additions and 5 deletions

View File

@@ -1,7 +1,7 @@
// PostHog — lazily loaded so it doesn't land in the initial bundle.
// All exported functions are fire-and-forget; analytics loss on failure is acceptable.
import { resetSurveySession, scheduleSurveyCheck } from "./surveys";
import { resetSurveySession, scheduleSurveyCheck, surveysReceiverReady } from "./surveys";
type PostHog = import("posthog-js").PostHog;
@@ -79,10 +79,13 @@ export function resetUser(): void {
}
export function capture(event: string, properties?: Record<string, unknown>): void {
load().then((ph) => ph.capture(event, properties));
// Event-triggered surveys: posthog-js's event receiver sees the capture
// above; the debounced check then asks for newly eligible surveys.
scheduleSurveyCheck();
// Hold captures until the surveys extension's event receiver is hooked (≤3s)
// so event-triggered surveys never miss their trigger, then re-check
// eligibility once the event has actually gone through.
Promise.all([load(), surveysReceiverReady()]).then(([ph]) => {
ph.capture(event, properties);
scheduleSurveyCheck();
});
}
export function capturePageView(path: string): void {

View File

@@ -149,6 +149,25 @@ function show(ph: PostHog, survey: Survey): void {
notify();
}
let _readyPromise: Promise<void> | null = null;
/** Resolves once posthog-js's surveys extension has installed its capture hook
* (or after a 3s safety timeout). Events captured before that hook exists are
* invisible to event-triggered surveys — the trial survey's
* trial_urgency_banner_viewed fires ~1s into the page load and was racing the
* extension's lazy <script>, so posthog.ts awaits this before every capture. */
export function surveysReceiverReady(): Promise<void> {
if (!ENABLED || typeof window === "undefined") return Promise.resolve();
if (_readyPromise) return _readyPromise;
_readyPromise = new Promise((resolve) => {
setTimeout(resolve, 3000);
loadPh()
.then((ph) => ph.surveys?.onSurveysLoaded?.(() => resolve()))
.catch(() => resolve());
});
return _readyPromise;
}
/** Debounced eligibility check — called after captures, route changes and identify. */
export function scheduleSurveyCheck(): void {
if (!ENABLED || typeof window === "undefined") return;