mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-10-15 19:45:42 -04:00
fix(native): keep analytics off until the user opts in
Default analytics to off, and name screen events from route patterns instead of pathnames and params so they carry no content IDs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
3 files changed
+74
-13
No files matched your search
@@ -7,7 +7,7 @@ import {
|
||||
persistQueryClientSubscribe,
|
||||
} from "@tanstack/react-query-persist-client";
|
||||
import * as Application from "expo-application";
|
||||
import { Stack, useGlobalSearchParams, usePathname } from "expo-router";
|
||||
import { Stack, useSegments } from "expo-router";
|
||||
import { ThemeProvider } from "expo-router/react-navigation";
|
||||
import * as SplashScreen from "expo-splash-screen";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
@@ -28,7 +28,7 @@ import { useServerConnection } from "@/hooks/use-server-connection";
|
||||
import { useWidgetRefresh } from "@/hooks/use-widget-refresh";
|
||||
import { initLocale } from "@/lib/i18n";
|
||||
import { createScopedQueryPersister, hasScopedStorage, scopedStorage } from "@/lib/mmkv";
|
||||
import { initAnalytics, posthog } from "@/lib/posthog";
|
||||
import { initAnalytics, posthog, screenNameFromSegments } from "@/lib/posthog";
|
||||
import { queryClient } from "@/lib/query-client";
|
||||
import { QUERY_PERSIST_MAX_AGE } from "@/lib/query-config";
|
||||
import { initSentry, Sentry } from "@/lib/sentry";
|
||||
@@ -78,14 +78,14 @@ function AppContent() {
|
||||
}, []);
|
||||
|
||||
// --- PostHog screen tracking ---
|
||||
const pathname = usePathname();
|
||||
const params = useGlobalSearchParams();
|
||||
const segments = useSegments();
|
||||
const screenName = screenNameFromSegments(segments);
|
||||
|
||||
useEffect(() => {
|
||||
if (posthog && pathname) {
|
||||
posthog.screen(pathname, params);
|
||||
if (posthog) {
|
||||
posthog.screen(screenName);
|
||||
}
|
||||
}, [pathname, params]);
|
||||
}, [screenName]);
|
||||
|
||||
useEffect(() => {
|
||||
Uniwind.setTheme("dark");
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const store = new Map<string, boolean | string>();
|
||||
|
||||
vi.mock("@/lib/mmkv", () => ({
|
||||
globalStorage: {
|
||||
getBoolean: (key: string) => {
|
||||
const v = store.get(key);
|
||||
return typeof v === "boolean" ? v : undefined;
|
||||
},
|
||||
getString: (key: string) => {
|
||||
const v = store.get(key);
|
||||
return typeof v === "string" ? v : undefined;
|
||||
},
|
||||
set: (key: string, value: boolean | string) => {
|
||||
store.set(key, value);
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("posthog-react-native", () => ({ PostHog: vi.fn<() => void>() }));
|
||||
|
||||
const mod = await import("./posthog");
|
||||
|
||||
describe("analytics consent", () => {
|
||||
it("is off by default", () => {
|
||||
store.clear();
|
||||
expect(mod.isAnalyticsEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it("follows the explicit preference", () => {
|
||||
mod.setAnalyticsEnabled(true);
|
||||
expect(mod.isAnalyticsEnabled()).toBe(true);
|
||||
mod.setAnalyticsEnabled(false);
|
||||
expect(mod.isAnalyticsEnabled()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("screenNameFromSegments", () => {
|
||||
it("names screens from route patterns without dynamic values", () => {
|
||||
const f = mod.screenNameFromSegments;
|
||||
expect(f(["(tabs)", "(home)", "index"])).toBe("home");
|
||||
expect(f(["(tabs)", "(search)", "index"])).toBe("search");
|
||||
expect(f(["(tabs)", "(library)", "history"])).toBe("library/history");
|
||||
expect(f(["(tabs)", "(home)", "upcoming"])).toBe("home/upcoming");
|
||||
expect(f(["(auth)", "login"])).toBe("login");
|
||||
expect(f(["title", "[id]"])).toBe("title/[id]");
|
||||
expect(f(["person", "[id]"])).toBe("person/[id]");
|
||||
expect(f([])).toBe("home");
|
||||
});
|
||||
});
|
||||
@@ -32,14 +32,24 @@ export const posthog: PostHog | null = posthogApiKey
|
||||
})
|
||||
: null;
|
||||
|
||||
/** Whether the user has explicitly set a preference via the settings toggle. */
|
||||
export function hasExplicitPreference(): boolean {
|
||||
return globalStorage.getBoolean(ANALYTICS_EXPLICIT_KEY) === true;
|
||||
/** Current analytics enabled state: explicit preference, else off — see docs/content/docs/telemetry.mdx. */
|
||||
export function isAnalyticsEnabled(): boolean {
|
||||
return globalStorage.getBoolean(ANALYTICS_ENABLED_KEY) ?? false;
|
||||
}
|
||||
|
||||
/** Current analytics enabled state (explicit preference or default). */
|
||||
export function isAnalyticsEnabled(): boolean {
|
||||
return globalStorage.getBoolean(ANALYTICS_ENABLED_KEY) ?? true;
|
||||
/** Route groups that only wrap layouts; the tab groups inside `(tabs)` do name the screen. */
|
||||
const LAYOUT_GROUPS = new Set(["(tabs)", "(auth)"]);
|
||||
|
||||
/**
|
||||
* PostHog screen name from Expo Router segments, without dynamic values:
|
||||
* ["(tabs)", "(search)", "index"] → "search", ["(tabs)", "(library)", "history"] →
|
||||
* "library/history", ["title", "[id]"] → "title/[id]".
|
||||
*/
|
||||
export function screenNameFromSegments(segments: readonly string[]): string {
|
||||
const parts = segments
|
||||
.filter((s) => !LAYOUT_GROUPS.has(s) && s !== "index")
|
||||
.map((s) => (s.startsWith("(") && s.endsWith(")") ? s.slice(1, -1) : s));
|
||||
return parts.length > 0 ? parts.join("/") : "home";
|
||||
}
|
||||
|
||||
/** Called by the settings toggle — marks the preference as explicit. */
|
||||
|
||||
Reference in new issue
Block a user