feat(flags): decouple analytics capture from flag evaluation (POSTHOG_CAPTURE_ENABLED)
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled

So a non-prod env can hold the (public) project key and evaluate flags - kill
switches, rollout - WITHOUT shipping analytics events to the shared prod
PostHog project. The posthog-node client (which powers both capture and local
flag eval) now initializes whenever the project key is present; capture() is
gated on POSTHOG_CAPTURE_ENABLED (default on; set "false" on dev). Wired into
the api + worker compose blocks.

This lets dev.sase.tr exercise the kill switches end-to-end while keeping the
prod analytics project clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 01:17:00 +03:00
parent ed45021d71
commit 7bdbe3541b
3 changed files with 23 additions and 6 deletions

View File

@@ -6,18 +6,26 @@ import type { PostHog as PostHogClient } from "posthog-node";
export class PostHogService implements OnModuleDestroy {
private readonly logger = new Logger(PostHogService.name);
private client: PostHogClient | null = null;
private enabled = false;
/** Analytics-capture gate — independent of flag eval so a non-prod env can
* evaluate flags without emitting events to the shared prod project. */
private captureEnabled = false;
/** True once a personal API key is configured → local flag evaluation works. */
private flagsEnabled = false;
constructor(private configService: ConfigService) {
const apiKey = this.configService.get<string>("POSTHOG_API_KEY");
// Capture is gated SEPARATELY from flag evaluation: a non-prod env can set the
// project key to evaluate flags (kill switches, rollout) while keeping capture
// off, so it never ships analytics to the shared prod project. Default on.
this.captureEnabled = this.configService.get<string>("POSTHOG_CAPTURE_ENABLED") !== "false";
if (apiKey) {
this.enabled = true;
// Lazily import posthog-node to avoid requiring it when not configured
// The client powers BOTH capture and local feature-flag evaluation.
// Lazily import posthog-node to avoid requiring it when not configured.
this.initClient(apiKey);
} else {
this.logger.warn("PostHog API key not configured — server-side analytics disabled");
this.logger.warn(
"PostHog project key not configured — server-side analytics & flags disabled",
);
}
}
@@ -50,7 +58,7 @@ export class PostHogService implements OnModuleDestroy {
);
} catch (err) {
this.logger.error("Failed to initialize PostHog client", (err as Error).stack);
this.enabled = false;
this.client = null;
}
}
@@ -69,7 +77,7 @@ export class PostHogService implements OnModuleDestroy {
* Uses distinctId from frontend identity when provided; otherwise uses "server".
*/
capture(event: string, properties?: Record<string, unknown>, distinctId = "server"): void {
if (!this.enabled || !this.client) return;
if (!this.captureEnabled || !this.client) return;
try {
this.client.capture({ distinctId, event, properties });

View File

@@ -71,6 +71,8 @@ services:
# Personal/secure key → LOCAL feature-flag evaluation (source kill switches,
# guarded decode rollout, remote config). Empty → flags fail open (inert).
- POSTHOG_PERSONAL_API_KEY=${POSTHOG_PERSONAL_API_KEY:-}
# "false" on non-prod → evaluate flags but DON'T emit analytics to prod project.
- POSTHOG_CAPTURE_ENABLED=${POSTHOG_CAPTURE_ENABLED:-}
- POSTHOG_HOST=${POSTHOG_HOST:-https://eu.i.posthog.com}
- OTEL_ENABLED=${OTEL_ENABLED:-false}
- OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT:-}
@@ -157,6 +159,8 @@ services:
# Personal/secure key → LOCAL feature-flag evaluation (source kill switches,
# guarded decode rollout, remote config). Empty → flags fail open (inert).
- POSTHOG_PERSONAL_API_KEY=${POSTHOG_PERSONAL_API_KEY:-}
# "false" on non-prod → evaluate flags but DON'T emit analytics to prod project.
- POSTHOG_CAPTURE_ENABLED=${POSTHOG_CAPTURE_ENABLED:-}
- POSTHOG_HOST=${POSTHOG_HOST:-https://eu.i.posthog.com}
- OTEL_ENABLED=${OTEL_ENABLED:-false}
- OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT:-}

View File

@@ -103,6 +103,11 @@ export const envSchema = z.object({
// remote-config ops tuning. Without it those flags fail open (no effect, no
// added latency). Keep secret — never expose to the browser.
POSTHOG_PERSONAL_API_KEY: z.string().optional(),
// Gate server-side analytics CAPTURE independently of flag evaluation, so a
// non-prod env can evaluate flags (kill switches, rollout) while the project
// key is set WITHOUT shipping events to the shared prod project. Default on;
// set "false" on dev. (Plain string — read as `!== "false"`.)
POSTHOG_CAPTURE_ENABLED: z.string().optional(),
// Sentry — error tracking
SENTRY_DSN: z.string().url().optional(),