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:
2026-06-09 17:30:16 +03:00
parent 79bbd1f1b3
commit 5903151692
3 changed files with 29 additions and 0 deletions

View File

@@ -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 };
}

View File

@@ -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
// ──────────────────────────────────────────────────────────────────────────

View File

@@ -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;
}