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
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:
@@ -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 });
|
||||
|
||||
@@ -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:-}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user