fix: remove App Tracking Transparency (ATT) from native analytics flow

- Drop `expo-tracking-transparency` dependency and plugin config from `app.json`
- Replace `applyTrackingTransparency` with `initAnalytics()` that simply syncs PostHog opt-in/out from the stored preference on startup
- Remove ATT permission prompt, IDFA/AAID identification, and one-time migration logic from `posthog.ts`
- Analytics now starts disabled on both iOS and Android; user opts in via Settings toggle
- Update telemetry docs and privacy policy to reflect the simplified consent flow (no ATT prompt, no advertising IDs)
This commit is contained in:
2026-03-22 19:03:28 -04:00
parent ddaa194f5d
commit 9df1075040
7 changed files with 23 additions and 100 deletions
-6
View File
@@ -108,12 +108,6 @@
}
],
"expo-localization",
[
"expo-tracking-transparency",
{
"userTrackingPermission": "This identifier will be used to measure app performance and improve your experience."
}
],
[
"expo-widgets",
{
-1
View File
@@ -63,7 +63,6 @@
"expo-splash-screen": "55.0.12",
"expo-status-bar": "55.0.4",
"expo-system-ui": "55.0.10",
"expo-tracking-transparency": "55.0.9",
"expo-updates": "55.0.15",
"expo-web-browser": "55.0.10",
"expo-widgets": "55.0.7",
+6 -33
View File
@@ -10,11 +10,6 @@ import {
import { Stack, useGlobalSearchParams, usePathname } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import { StatusBar } from "expo-status-bar";
import {
getAdvertisingId,
getTrackingPermissionsAsync,
requestTrackingPermissionsAsync,
} from "expo-tracking-transparency";
import { PostHogErrorBoundary, PostHogProvider } from "posthog-react-native";
import { useEffect, useRef, useState } from "react";
import { GestureHandlerRootView } from "react-native-gesture-handler";
@@ -28,7 +23,7 @@ import { ServerUnreachableBanner } from "@/components/ui/server-unreachable-bann
import { useServerConnection } from "@/hooks/use-server-connection";
import { useWidgetRefresh } from "@/hooks/use-widget-refresh";
import { initLocale } from "@/lib/i18n";
import { applyTrackingTransparency, posthog } from "@/lib/posthog";
import { initAnalytics, posthog } from "@/lib/posthog";
import { queryClient } from "@/lib/query-client";
import { getScopeKey, initialize, onStorageScopeChange, queryPersister } from "@/lib/server";
import { sofaTheme } from "@/lib/theme";
@@ -67,42 +62,20 @@ function AppContent() {
localeReady.then(() => setLocaleReady(true)).catch(() => setLocaleReady(true));
}, []);
// --- App Tracking Transparency (must resolve before screen tracking) ---
const [trackingReady, setTrackingReady] = useState(false);
// --- Analytics init (sync PostHog opt-in/out from stored preference) ---
useEffect(() => {
(async () => {
const { status } = await getTrackingPermissionsAsync();
const granted =
status === "undetermined"
? (await requestTrackingPermissionsAsync()).granted
: status === "granted";
const enabled = applyTrackingTransparency(granted);
// Use the platform advertising ID (IDFA / AAID) as the PostHog
// distinct ID, but only when the resolved state is actually enabled
// (respects both ATT result and the user's settings override).
if (enabled && posthog) {
const advertisingId = getAdvertisingId();
if (advertisingId) {
posthog.identify(advertisingId);
}
}
setTrackingReady(true);
})();
initAnalytics();
}, []);
// --- PostHog screen tracking (waits for ATT to resolve) ---
// --- PostHog screen tracking ---
const pathname = usePathname();
const params = useGlobalSearchParams();
useEffect(() => {
if (trackingReady && posthog && pathname) {
if (posthog && pathname) {
posthog.screen(pathname, params);
}
}, [trackingReady, pathname, params]);
}, [pathname, params]);
useEffect(() => {
Uniwind.setTheme("dark");
+6 -36
View File
@@ -8,7 +8,6 @@ const host = process.env.EXPO_PUBLIC_POSTHOG_HOST ?? "https://us.i.posthog.com";
const ANALYTICS_ENABLED_KEY = "sofa_analytics_enabled";
const ANALYTICS_EXPLICIT_KEY = "sofa_analytics_explicit";
const ATT_MIGRATED_KEY = "sofa_att_migrated";
const posthogStorage: PostHogCustomStorage = {
getItem: (key: string) => globalStorage.getString(key) ?? null,
@@ -16,7 +15,7 @@ const posthogStorage: PostHogCustomStorage = {
};
// PostHog throws if apiKey is empty, so only construct when configured.
// Start opted-out; the ATT check in root layout will opt in if appropriate.
// Start opted-out; initAnalytics() in root layout will opt in based on stored preference.
export const posthog: PostHog | null = posthogApiKey
? new PostHog(posthogApiKey, {
host,
@@ -52,7 +51,7 @@ export function setAnalyticsEnabled(enabled: boolean): void {
/**
* Sync PostHog opt-in/out state based on the resolved analytics flag.
* Called after ATT check and from the settings toggle.
* Called from initAnalytics() and from the settings toggle.
*/
export function syncPosthog(enabled: boolean): void {
if (enabled) {
@@ -63,38 +62,9 @@ export function syncPosthog(enabled: boolean): void {
}
/**
* Resolve analytics state after an ATT permission check.
* If the user has an explicit preference (or a legacy preference from before
* ATT was introduced), that wins. Otherwise the ATT result is stored as the
* current default and PostHog is synced.
*
* Returns the resolved enabled state so callers can decide whether to proceed
* with identifying, etc.
* Initialize analytics on app startup.
* Reads the stored preference and syncs PostHog opt-in/out state.
*/
export function applyTrackingTransparency(granted: boolean): boolean {
// One-time migration: on the first launch after the ATT update, check if
// the user had previously toggled analytics via the settings switch (which
// was the only way ANALYTICS_ENABLED_KEY got set before ATT). If so,
// promote it to an explicit preference so the ATT result doesn't overwrite
// it. This runs exactly once — subsequent launches skip it because
// ATT_MIGRATED_KEY is set, preventing applyTrackingTransparency's own
// writes to ANALYTICS_ENABLED_KEY from being misidentified as legacy.
if (!globalStorage.getBoolean(ATT_MIGRATED_KEY)) {
globalStorage.set(ATT_MIGRATED_KEY, true);
if (globalStorage.getBoolean(ANALYTICS_ENABLED_KEY) !== undefined && !hasExplicitPreference()) {
globalStorage.set(ANALYTICS_EXPLICIT_KEY, true);
}
}
if (hasExplicitPreference()) {
// User already made a choice in settings — honour it.
const enabled = isAnalyticsEnabled();
syncPosthog(enabled);
return enabled;
}
// No explicit preference yet — follow the ATT result.
globalStorage.set(ANALYTICS_ENABLED_KEY, granted);
syncPosthog(granted);
return granted;
export function initAnalytics(): void {
syncPosthog(isAnalyticsEnabled());
}