feat(analytics): integrate PostHog for server-side event tracking
- Added a new PostHog client for capturing server-side business events such as Stripe webhooks and subscription state changes. - Implemented various tracking functions for pricing funnel steps, character creation, and chat session starts. - Enhanced the flux meter tests to handle partial charges and report unbilled flux correctly. - Updated the CharacterDialog and Flux settings pages to track user interactions with analytics events. - Introduced a mechanism to identify users on PostHog based on authentication state to ensure accurate funnel tracking. - Added necessary dependencies for PostHog integration in the project.
This commit is contained in:
+44
-1
@@ -1,4 +1,5 @@
|
||||
import type Redis from 'ioredis'
|
||||
import type { PostHog } from 'posthog-node'
|
||||
|
||||
import type { AuthInstance } from './libs/auth'
|
||||
import type { Database } from './libs/db'
|
||||
@@ -59,6 +60,7 @@ import { createConfigKVService } from './services/config-kv'
|
||||
import { createEmailService } from './services/email'
|
||||
import { createFluxService } from './services/flux'
|
||||
import { createFluxTransactionService } from './services/flux-transaction'
|
||||
import { createPostHogClient } from './services/posthog'
|
||||
import { createProviderService } from './services/providers'
|
||||
import { createRequestLogService } from './services/request-log'
|
||||
import { createStripeService } from './services/stripe'
|
||||
@@ -85,6 +87,7 @@ interface AppDeps {
|
||||
env: Env
|
||||
otel: OtelInstance | null
|
||||
userDeletionService: UserDeletionService
|
||||
posthog: PostHog | null
|
||||
}
|
||||
|
||||
export async function buildApp(deps: AppDeps) {
|
||||
@@ -238,7 +241,7 @@ export async function buildApp(deps: AppDeps) {
|
||||
/**
|
||||
* Stripe routes.
|
||||
*/
|
||||
.route('/api/v1/stripe', createStripeRoutes(deps.fluxService, deps.stripeService, deps.billingService, deps.configKV, deps.env, deps.redis, deps.otel?.revenue, deps.otel?.rateLimit))
|
||||
.route('/api/v1/stripe', createStripeRoutes(deps.fluxService, deps.stripeService, deps.billingService, deps.configKV, deps.env, deps.redis, deps.otel?.revenue, deps.otel?.rateLimit, deps.posthog))
|
||||
|
||||
/**
|
||||
* Admin routes — guarded by `ADMIN_EMAILS` allowlist + verified email.
|
||||
@@ -352,6 +355,44 @@ export async function createApp() {
|
||||
}, undefined, dependsOn.otel?.email),
|
||||
})
|
||||
|
||||
// Webhook capture path goes through `captureSafe` → `captureImmediate`,
|
||||
// which awaits the HTTP send inline, so individual events never sit in
|
||||
// the background queue. `flush()` + `_shutdown()` on SIGTERM is the belt-
|
||||
// and-suspenders drain for any future call site that uses the regular
|
||||
// `capture()` (which only enqueues).
|
||||
//
|
||||
// NOTICE:
|
||||
// We use the underscore-prefixed `_shutdown` despite its "internal" naming
|
||||
// because the public `shutdown(timeoutMs)` returns void (`types.d.ts:580`)
|
||||
// — there is no way to await its completion. `_shutdown(timeoutMs)` returns
|
||||
// `Promise<void>` (`client.d.ts:934`) and is the only way to ensure the
|
||||
// process doesn't exit while PostHog cleanup is still running. Posthog's
|
||||
// own examples show `await client._shutdown()` as the recommended pattern.
|
||||
const posthog = injeca.provide('services:posthog', {
|
||||
dependsOn: { env: parsedEnv, lifecycle },
|
||||
build: ({ dependsOn }) => {
|
||||
const client = createPostHogClient(dependsOn.env)
|
||||
if (client) {
|
||||
dependsOn.lifecycle.appHooks.onStop(async () => {
|
||||
try {
|
||||
await client.flush()
|
||||
}
|
||||
catch {
|
||||
// Flush failures on shutdown are non-fatal; we lose at most a
|
||||
// few queued events. Fall through to shutdown anyway.
|
||||
}
|
||||
try {
|
||||
await client._shutdown(5000)
|
||||
}
|
||||
catch {
|
||||
// Shutdown errors are also non-fatal during process exit.
|
||||
}
|
||||
})
|
||||
}
|
||||
return client
|
||||
},
|
||||
})
|
||||
|
||||
const characterService = injeca.provide('services:characters', {
|
||||
dependsOn: { db, otel },
|
||||
build: ({ dependsOn }) => createCharacterService(dependsOn.db, dependsOn.otel?.engagement),
|
||||
@@ -484,6 +525,7 @@ export async function createApp() {
|
||||
env: parsedEnv,
|
||||
otel,
|
||||
userDeletionService,
|
||||
posthog,
|
||||
})
|
||||
// Register the cluster-wide ObservableGauge for active sessions. Each
|
||||
// replica polls the same DB (cached 10s, in-flight coalesced) and the
|
||||
@@ -509,6 +551,7 @@ export async function createApp() {
|
||||
env: resolved.env,
|
||||
otel: resolved.otel,
|
||||
userDeletionService: resolved.userDeletionService,
|
||||
posthog: resolved.posthog,
|
||||
})
|
||||
|
||||
logger.withFields({ hostname: resolved.env.HOST, port: resolved.env.PORT }).log('Server started')
|
||||
|
||||
Reference in New Issue
Block a user