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 {
|
export class PostHogService implements OnModuleDestroy {
|
||||||
private readonly logger = new Logger(PostHogService.name);
|
private readonly logger = new Logger(PostHogService.name);
|
||||||
private client: PostHogClient | null = null;
|
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. */
|
/** True once a personal API key is configured → local flag evaluation works. */
|
||||||
private flagsEnabled = false;
|
private flagsEnabled = false;
|
||||||
|
|
||||||
constructor(private configService: ConfigService) {
|
constructor(private configService: ConfigService) {
|
||||||
const apiKey = this.configService.get<string>("POSTHOG_API_KEY");
|
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) {
|
if (apiKey) {
|
||||||
this.enabled = true;
|
// The client powers BOTH capture and local feature-flag evaluation.
|
||||||
// Lazily import posthog-node to avoid requiring it when not configured
|
// Lazily import posthog-node to avoid requiring it when not configured.
|
||||||
this.initClient(apiKey);
|
this.initClient(apiKey);
|
||||||
} else {
|
} 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) {
|
} catch (err) {
|
||||||
this.logger.error("Failed to initialize PostHog client", (err as Error).stack);
|
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".
|
* Uses distinctId from frontend identity when provided; otherwise uses "server".
|
||||||
*/
|
*/
|
||||||
capture(event: string, properties?: Record<string, unknown>, distinctId = "server"): void {
|
capture(event: string, properties?: Record<string, unknown>, distinctId = "server"): void {
|
||||||
if (!this.enabled || !this.client) return;
|
if (!this.captureEnabled || !this.client) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.client.capture({ distinctId, event, properties });
|
this.client.capture({ distinctId, event, properties });
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ services:
|
|||||||
# Personal/secure key → LOCAL feature-flag evaluation (source kill switches,
|
# Personal/secure key → LOCAL feature-flag evaluation (source kill switches,
|
||||||
# guarded decode rollout, remote config). Empty → flags fail open (inert).
|
# guarded decode rollout, remote config). Empty → flags fail open (inert).
|
||||||
- POSTHOG_PERSONAL_API_KEY=${POSTHOG_PERSONAL_API_KEY:-}
|
- 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}
|
- POSTHOG_HOST=${POSTHOG_HOST:-https://eu.i.posthog.com}
|
||||||
- OTEL_ENABLED=${OTEL_ENABLED:-false}
|
- OTEL_ENABLED=${OTEL_ENABLED:-false}
|
||||||
- OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT:-}
|
- OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT:-}
|
||||||
@@ -157,6 +159,8 @@ services:
|
|||||||
# Personal/secure key → LOCAL feature-flag evaluation (source kill switches,
|
# Personal/secure key → LOCAL feature-flag evaluation (source kill switches,
|
||||||
# guarded decode rollout, remote config). Empty → flags fail open (inert).
|
# guarded decode rollout, remote config). Empty → flags fail open (inert).
|
||||||
- POSTHOG_PERSONAL_API_KEY=${POSTHOG_PERSONAL_API_KEY:-}
|
- 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}
|
- POSTHOG_HOST=${POSTHOG_HOST:-https://eu.i.posthog.com}
|
||||||
- OTEL_ENABLED=${OTEL_ENABLED:-false}
|
- OTEL_ENABLED=${OTEL_ENABLED:-false}
|
||||||
- OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT:-}
|
- 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
|
// remote-config ops tuning. Without it those flags fail open (no effect, no
|
||||||
// added latency). Keep secret — never expose to the browser.
|
// added latency). Keep secret — never expose to the browser.
|
||||||
POSTHOG_PERSONAL_API_KEY: z.string().optional(),
|
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 — error tracking
|
||||||
SENTRY_DSN: z.string().url().optional(),
|
SENTRY_DSN: z.string().url().optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user