fix(api): await PostHog flush at conversion chokepoints (webhook events were lost)
payment_success / payment_failed / subscription_activated are captured inside the Stripe webhook handler and activateSubscription — short requests that return immediately. posthog-node's fire-and-forget flush was abandoned before the send completed, so these events were written to the DB but never reached PostHog (DB had 4 completed Stripe payments in 30d; PostHog had 1 payment_success and 0 payment_failed). payment_initiated, fired in a normal user request, landed fine — which is what isolated the cause to the webhook/short-request context. Add PostHogService.flush() and await it at the end of handleWebhook and after the subscription_activated capture in activateSubscription (the shared Stripe+EFT chokepoint). Restores server-side paid-conversion visibility so trial→paid ROI is measurable in PostHog instead of only the DB. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -222,6 +222,12 @@ export class StripeService {
|
||||
this.logger.debug(`Unhandled Stripe event type: ${event.type}`);
|
||||
}
|
||||
|
||||
// Await delivery of any conversion events captured during webhook processing
|
||||
// (payment_success / payment_failed / subscription_activated). Without this,
|
||||
// the webhook returns and the fire-and-forget flush is abandoned, so these
|
||||
// events landed in the DB but never in PostHog.
|
||||
await this.posthog.flush();
|
||||
|
||||
return { received: true };
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,24 @@ export class PostHogService implements OnModuleDestroy {
|
||||
this.capture(event, { ...properties, $user_id: userId }, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-deliver queued events and await the network send. MUST be awaited at
|
||||
* critical conversion chokepoints (Stripe webhook, subscription activation):
|
||||
* those run in a webhook/short request that returns immediately, so the default
|
||||
* fire-and-forget flush was dropping the events — `payment_success`,
|
||||
* `payment_failed`, and `subscription_activated` were written to the DB but never
|
||||
* reached PostHog (whereas `payment_initiated`, fired in a normal request, did).
|
||||
* Best-effort: never throws, so analytics can't break a payment.
|
||||
*/
|
||||
async flush(): Promise<void> {
|
||||
if (!this.captureEnabled || !this.client) return;
|
||||
try {
|
||||
await this.client.flush();
|
||||
} catch (err) {
|
||||
this.logger.warn(`PostHog flush failed: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Feature flags — server-side, local evaluation
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -192,6 +192,11 @@ export class SubscriptionsService {
|
||||
referral_credit_days: creditDays,
|
||||
});
|
||||
|
||||
// Await delivery: activateSubscription is the shared chokepoint for Stripe
|
||||
// (webhook) AND EFT/manual activation, both of which run in short requests
|
||||
// whose fire-and-forget flush was dropping this revenue event.
|
||||
await this.posthog.flush();
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user