Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/mobile/src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { useNetworkStatus } from "@/hooks/useNetworkStatus";
import {
POSTHOG_API_KEY,
POSTHOG_OPTIONS,
useIdentifyUser,
useRegisterAppVersion,
useScreenTracking,
} from "@/lib/posthog";
Expand All @@ -37,6 +38,7 @@ function RootLayoutNav({ isConnected }: RootLayoutNavProps) {
const pathname = usePathname();

useScreenTracking();
useIdentifyUser();
useRegisterAppVersion();

useEffect(() => {
Expand Down
12 changes: 12 additions & 0 deletions apps/mobile/src/lib/posthog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ vi.mock("expo-constants", () => ({
},
}));

// posthog.ts imports the auth store and user query for useIdentifyUser. Their
// real modules transitively pull in native expo modules (expo-secure-store,
// expo-auth-session) that can't load under the node test environment, so mock
// them — these app-version tests don't exercise identification.
vi.mock("@/features/auth/stores/authStore", () => ({
useAuthStore: () => false,
}));

vi.mock("@/features/auth/hooks/useUserQuery", () => ({
useUserQuery: () => ({ data: undefined }),
}));

beforeEach(() => {
vi.clearAllMocks();
expoApplication.nativeApplicationVersion = null;
Expand Down
67 changes: 67 additions & 0 deletions apps/mobile/src/lib/posthog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import Constants from "expo-constants";
import { usePathname, useSegments } from "expo-router";
import { usePostHog } from "posthog-react-native";
import { useEffect, useRef } from "react";
import { useUserQuery } from "@/features/auth/hooks/useUserQuery";
import { useAuthStore } from "@/features/auth/stores/authStore";

/**
* PostHog configuration - used by PostHogProvider in _layout.tsx
Expand All @@ -18,6 +20,12 @@ export const POSTHOG_OPTIONS = {
captureLog: true,
captureNetworkTelemetry: true,
},
errorTracking: {
autocapture: {
uncaughtExceptions: true,
unhandledRejections: true,
},
},
};

/**
Expand Down Expand Up @@ -88,3 +96,62 @@ export function useScreenTracking() {
}
}, [pathname, segments, posthog]);
}

/**
* Associates captured events (and session replays) with the signed-in user.
* Re-identifies whenever the user's identifying properties change (email, name,
* staff status, organization) so mid-session updates are forwarded, and resets
* on logout so the next session starts anonymous and events don't bleed across
* accounts. Must be used inside PostHogProvider.
*/
export function useIdentifyUser() {
const posthog = usePostHog();
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const { data: user } = useUserQuery();
// Signature of the last forwarded payload, so we re-identify on real changes
// but don't spam identify()/group() on every render with identical data.
const lastIdentity = useRef<string | null>(null);

useEffect(() => {
if (!posthog) return;

if (!isAuthenticated) {
// Reset only if we previously identified, otherwise we'd churn the
// anonymous distinct id on every render before sign-in.
if (lastIdentity.current) {
posthog.reset();
lastIdentity.current = null;
}
return;
}

if (!user) return;

const name = [user.first_name, user.last_name].filter(Boolean).join(" ");
const isStaff = Boolean(user.is_staff);
const signature = JSON.stringify([
user.uuid,
user.email,
name,
isStaff,
user.organization?.id ?? null,
user.organization?.name ?? null,
]);

if (lastIdentity.current === signature) return;

posthog.identify(user.uuid, {
email: user.email,
name,
is_staff: isStaff,
});

if (user.organization) {
posthog.group("organization", user.organization.id, {
name: user.organization.name,
});
}

lastIdentity.current = signature;
}, [posthog, isAuthenticated, user]);
}
Loading