mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: defer PostHog/Sentry loading until user consents to telemetry
PostHog SDK was initialized on app mount based only on the server-level config flag, ignoring user consent. This caused network requests to us-assets.i.posthog.com (config.js, web-vitals.js, dead-clicks-autocapture.js) even when the user had not opted in or had explicitly declined telemetry. - Replace static imports of posthog-js and @sentry/react with dynamic import() so the SDK bundles are not downloaded until consent is granted - Gate initAnalytics on analyticsConsent.analyticsEnabled === true, not just server config.enabled - Add consent re-check after each await import() to handle revocation during the async load - Add shutdownAnalytics() that calls opt_out_capturing() + reset() for mid-session consent revocation - setAnalyticsConsent(false) now triggers full SDK shutdown automatically - Rewrite analytics test suite with 44 tests covering init gating, shutdown lifecycle, consent toggle, race conditions, and Sentry callbacks Closes #98
This commit is contained in:
+7
-10
@@ -173,12 +173,6 @@ export function App() {
|
||||
fetchAnalyticsConfig();
|
||||
}, [fetchAnalyticsConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (analyticsConfigLoaded && analyticsConfig?.enabled) {
|
||||
initAnalytics(analyticsConfig);
|
||||
}
|
||||
}, [analyticsConfigLoaded, analyticsConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!analyticsConfigLoaded ||
|
||||
@@ -186,10 +180,13 @@ export function App() {
|
||||
analyticsConsent.analyticsEnabled !== true
|
||||
)
|
||||
return;
|
||||
identify(analyticsConfig.instanceId, {
|
||||
$set: { version: APP_VERSION },
|
||||
$set_once: { instance_id: analyticsConfig.instanceId },
|
||||
});
|
||||
void (async () => {
|
||||
await initAnalytics(analyticsConfig);
|
||||
identify(analyticsConfig.instanceId, {
|
||||
$set: { version: APP_VERSION },
|
||||
$set_once: { instance_id: analyticsConfig.instanceId },
|
||||
});
|
||||
})();
|
||||
}, [analyticsConfigLoaded, analyticsConfig, analyticsConsent.analyticsEnabled]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as Sentry from "@sentry/react";
|
||||
import type { AnalyticsConfig } from "@snapotter/shared";
|
||||
import posthogJs from "posthog-js";
|
||||
|
||||
let posthog: import("posthog-js").PostHog | null = null;
|
||||
type PostHogInstance = import("posthog-js").PostHog;
|
||||
|
||||
let posthog: PostHogInstance | null = null;
|
||||
let initialized = false;
|
||||
let consentGranted = false;
|
||||
|
||||
@@ -14,11 +14,16 @@ function scrubString(str: string): string {
|
||||
return str.replace(FILE_EXT_PATTERN, ".[REDACTED]").replace(FILE_PATH_PATTERN, "/[REDACTED]/");
|
||||
}
|
||||
|
||||
export function initAnalytics(config: AnalyticsConfig): void {
|
||||
export async function initAnalytics(config: AnalyticsConfig): Promise<void> {
|
||||
if (initialized || !config.enabled) return;
|
||||
initialized = true;
|
||||
|
||||
try {
|
||||
const posthogJs = (await import("posthog-js")).default;
|
||||
if (!consentGranted) {
|
||||
initialized = false;
|
||||
return;
|
||||
}
|
||||
posthog =
|
||||
posthogJs.init(config.posthogApiKey, {
|
||||
api_host: config.posthogHost,
|
||||
@@ -35,11 +40,16 @@ export function initAnalytics(config: AnalyticsConfig): void {
|
||||
persistence: "localStorage",
|
||||
}) ?? null;
|
||||
} catch {
|
||||
// SDK blocked or unavailable — use null provider
|
||||
// SDK blocked or unavailable
|
||||
}
|
||||
|
||||
try {
|
||||
if (config.sentryDsn) {
|
||||
const Sentry = await import("@sentry/react");
|
||||
if (!consentGranted) {
|
||||
initialized = false;
|
||||
return;
|
||||
}
|
||||
Sentry.init({
|
||||
dsn: config.sentryDsn,
|
||||
sendDefaultPii: false,
|
||||
@@ -81,8 +91,25 @@ export function initAnalytics(config: AnalyticsConfig): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function shutdownAnalytics(): void {
|
||||
if (posthog) {
|
||||
try {
|
||||
posthog.opt_out_capturing();
|
||||
posthog.reset();
|
||||
} catch {
|
||||
// never throw
|
||||
}
|
||||
}
|
||||
posthog = null;
|
||||
initialized = false;
|
||||
consentGranted = false;
|
||||
}
|
||||
|
||||
export function setAnalyticsConsent(enabled: boolean): void {
|
||||
consentGranted = enabled;
|
||||
if (!enabled) {
|
||||
shutdownAnalytics();
|
||||
}
|
||||
}
|
||||
|
||||
export function identify(instanceId: string, properties: Record<string, unknown>): void {
|
||||
|
||||
Reference in New Issue
Block a user