Skip to content

Commit 9a569c9

Browse files
chelojimenezclaude
andauthored
Use actor key for PostHog identification instead of auth state (#2044)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent d9c0187 commit 9a569c9

3 files changed

Lines changed: 125 additions & 33 deletions

File tree

mcpjam-inspector/client/src/App.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -650,16 +650,19 @@ export default function App() {
650650

651651
usePostHogIdentify();
652652

653+
const lastLaunchedActorRef = useRef<string | null>(null);
653654
useEffect(() => {
654-
if (isAuthLoading) return;
655+
if (!actorKey) return;
656+
if (lastLaunchedActorRef.current === actorKey) return;
657+
lastLaunchedActorRef.current = actorKey;
655658
posthog.capture("app_launched", {
656659
platform: detectPlatform(),
657660
environment: detectEnvironment(),
658661
user_agent: navigator.userAgent,
659662
version: __APP_VERSION__,
660-
is_authenticated: isAuthenticated,
663+
is_authenticated: Boolean(workOsUser),
661664
});
662-
}, [isAuthLoading, isAuthenticated]);
665+
}, [actorKey, workOsUser, posthog]);
663666

664667
// Set the initial theme mode and preset on page load
665668
const initialThemeMode = getInitialThemeMode();

mcpjam-inspector/client/src/hooks/__tests__/usePostHogIdentify.test.ts

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const mockState = vi.hoisted(() => ({
2020
isAuthenticated: false,
2121
},
2222
convexUser: null as { occupation?: string } | null,
23+
actorKey: null as string | null,
2324
detectPlatform: vi.fn(() => "mac"),
2425
}));
2526

@@ -40,13 +41,18 @@ vi.mock("@/lib/PosthogUtils", () => ({
4041
detectPlatform: mockState.detectPlatform,
4142
}));
4243

44+
vi.mock("@/hooks/use-actor-key", () => ({
45+
useActorKey: () => mockState.actorKey,
46+
}));
47+
4348
describe("usePostHogIdentify", () => {
4449
beforeEach(() => {
4550
vi.clearAllMocks();
4651
vi.stubGlobal("__APP_VERSION__", "2.0.13-test");
4752
mockState.auth.user = null;
4853
mockState.convexAuth.isAuthenticated = false;
4954
mockState.convexUser = null;
55+
mockState.actorKey = null;
5056
mockState.detectPlatform.mockReturnValue("mac");
5157
});
5258

@@ -58,6 +64,7 @@ describe("usePostHogIdentify", () => {
5864
lastName: "Smith",
5965
};
6066
mockState.convexAuth.isAuthenticated = true;
67+
mockState.actorKey = "user_123";
6168

6269
renderHook(() => usePostHogIdentify());
6370

@@ -73,25 +80,57 @@ describe("usePostHogIdentify", () => {
7380
expect(mockState.posthog.reset).not.toHaveBeenCalled();
7481
});
7582

76-
it("re-registers static telemetry properties after logout reset", () => {
83+
it("identifies guests with their guestId and does not call reset", () => {
84+
mockState.auth.user = null;
85+
mockState.convexAuth.isAuthenticated = false;
86+
mockState.actorKey = "guest_abc";
87+
7788
renderHook(() => usePostHogIdentify());
7889

79-
expect(mockState.posthog.reset).toHaveBeenCalledTimes(1);
90+
expect(mockState.posthog.identify).toHaveBeenCalledWith("guest_abc", {});
8091
expect(mockState.posthog.register).toHaveBeenCalledWith({
81-
environment: import.meta.env.MODE,
82-
platform: "mac",
83-
version: "2.0.13-test",
92+
user_id: "guest_abc",
8493
});
94+
expect(mockState.posthog.reset).not.toHaveBeenCalled();
8595
});
8696

87-
it("resets and re-registers static telemetry properties when auth changes from logged in to logged out", () => {
97+
it("does nothing while the actor key is still resolving", () => {
98+
mockState.auth.user = null;
99+
mockState.actorKey = null;
100+
101+
renderHook(() => usePostHogIdentify());
102+
103+
expect(mockState.posthog.identify).not.toHaveBeenCalled();
104+
expect(mockState.posthog.register).not.toHaveBeenCalled();
105+
expect(mockState.posthog.reset).not.toHaveBeenCalled();
106+
});
107+
108+
it("is idempotent across re-renders with the same guest actor key", () => {
109+
mockState.auth.user = null;
110+
mockState.actorKey = "guest_abc";
111+
112+
const { rerender } = renderHook(() => usePostHogIdentify());
113+
114+
expect(mockState.posthog.identify).toHaveBeenCalledTimes(1);
115+
expect(mockState.posthog.register).toHaveBeenCalledTimes(1);
116+
117+
rerender();
118+
rerender();
119+
120+
expect(mockState.posthog.identify).toHaveBeenCalledTimes(1);
121+
expect(mockState.posthog.register).toHaveBeenCalledTimes(1);
122+
expect(mockState.posthog.reset).not.toHaveBeenCalled();
123+
});
124+
125+
it("resets and re-registers static telemetry properties when an authed user signs out into a guest session", () => {
88126
mockState.auth.user = {
89127
id: "user_123",
90128
email: "user@example.com",
91129
firstName: "Taylor",
92130
lastName: "Smith",
93131
};
94132
mockState.convexAuth.isAuthenticated = true;
133+
mockState.actorKey = "user_123";
95134

96135
const { rerender } = renderHook(() => usePostHogIdentify());
97136

@@ -106,6 +145,7 @@ describe("usePostHogIdentify", () => {
106145

107146
mockState.auth.user = null;
108147
mockState.convexAuth.isAuthenticated = false;
148+
mockState.actorKey = "guest_abc";
109149

110150
rerender();
111151

@@ -115,7 +155,44 @@ describe("usePostHogIdentify", () => {
115155
platform: "mac",
116156
version: "2.0.13-test",
117157
});
118-
expect(mockState.posthog.identify).not.toHaveBeenCalled();
158+
expect(mockState.posthog.identify).toHaveBeenCalledWith("guest_abc", {});
159+
expect(mockState.posthog.register).toHaveBeenCalledWith({
160+
user_id: "guest_abc",
161+
});
162+
});
163+
164+
it("aliases a guest into an authed user without calling reset on guest→authed promotion", () => {
165+
mockState.auth.user = null;
166+
mockState.convexAuth.isAuthenticated = false;
167+
mockState.actorKey = "guest_abc";
168+
169+
const { rerender } = renderHook(() => usePostHogIdentify());
170+
171+
expect(mockState.posthog.identify).toHaveBeenCalledWith("guest_abc", {});
172+
173+
vi.clearAllMocks();
174+
175+
mockState.auth.user = {
176+
id: "user_123",
177+
email: "user@example.com",
178+
firstName: "Taylor",
179+
lastName: "Smith",
180+
};
181+
mockState.convexAuth.isAuthenticated = true;
182+
mockState.actorKey = "user_123";
183+
184+
rerender();
185+
186+
expect(mockState.posthog.reset).not.toHaveBeenCalled();
187+
expect(mockState.posthog.identify).toHaveBeenCalledWith("user_123", {
188+
email: "user@example.com",
189+
name: "Taylor Smith",
190+
first_name: "Taylor",
191+
last_name: "Smith",
192+
});
193+
expect(mockState.posthog.register).toHaveBeenCalledWith({
194+
user_id: "user_123",
195+
});
119196
});
120197

121198
it("adds trimmed occupation when the Convex user has one", () => {
@@ -126,6 +203,7 @@ describe("usePostHogIdentify", () => {
126203
lastName: "Smith",
127204
};
128205
mockState.convexAuth.isAuthenticated = true;
206+
mockState.actorKey = "user_123";
129207
mockState.convexUser = { occupation: " Platform Engineer " };
130208

131209
renderHook(() => usePostHogIdentify());
@@ -147,6 +225,7 @@ describe("usePostHogIdentify", () => {
147225
lastName: "Smith",
148226
};
149227
mockState.convexAuth.isAuthenticated = true;
228+
mockState.actorKey = "user_123";
150229
mockState.convexUser = { occupation: " " };
151230

152231
renderHook(() => usePostHogIdentify());
Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import { useEffect } from "react";
1+
import { useEffect, useRef } from "react";
22
import { usePostHog } from "posthog-js/react";
33
import { useAuth } from "@workos-inc/authkit-react";
44
import { useConvexAuth, useQuery } from "convex/react";
55
import { detectPlatform } from "@/lib/PosthogUtils";
6+
import { useActorKey } from "@/hooks/use-actor-key";
67

78
/**
8-
* Automatically identify users in PostHog when they log in/out
9-
* and set super properties that are sent with every event.
9+
* Identify the active actor in PostHog using the same id the backend uses:
10+
* the WorkOS user id for signed-in users, the cookie-backed guestId for
11+
* guests. Reset only on a true identity switch away from an authed user, so
12+
* the same browser revisiting as a guest keeps a stable distinct_id.
1013
*/
1114
export function usePostHogIdentify() {
1215
const posthog = usePostHog();
@@ -16,13 +19,31 @@ export function usePostHogIdentify() {
1619
"users:getCurrentUser" as any,
1720
isAuthenticated ? ({} as any) : "skip"
1821
);
22+
const actorKey = useActorKey();
23+
const previousActorRef = useRef<{ key: string; wasAuthed: boolean } | null>(
24+
null
25+
);
1926

2027
useEffect(() => {
2128
if (!posthog) return;
29+
if (!actorKey) return;
30+
31+
const previous = previousActorRef.current;
32+
const isActorChange = !previous || previous.key !== actorKey;
33+
const isAuthedActor = Boolean(user) && user?.id === actorKey;
2234

23-
// User is authenticated - identify them
24-
if (isAuthenticated && user) {
25-
const personProperties: Record<string, string | null | undefined> = {
35+
if (isActorChange && previous?.wasAuthed) {
36+
posthog.reset();
37+
posthog.register({
38+
environment: import.meta.env.MODE,
39+
platform: detectPlatform(),
40+
version: __APP_VERSION__,
41+
});
42+
}
43+
44+
let personProperties: Record<string, string | null | undefined> = {};
45+
if (isAuthedActor && user) {
46+
personProperties = {
2647
email: user.email,
2748
name:
2849
user.firstName && user.lastName
@@ -31,30 +52,19 @@ export function usePostHogIdentify() {
3152
first_name: user.firstName,
3253
last_name: user.lastName,
3354
};
34-
3555
const trimmedOccupation =
3656
typeof convexUser?.occupation === "string"
3757
? convexUser.occupation.trim()
3858
: "";
3959
if (trimmedOccupation) {
4060
personProperties.occupation = trimmedOccupation;
4161
}
62+
}
4263

43-
// Identify the user with their WorkOS ID
44-
posthog.identify(user.id, personProperties);
45-
46-
posthog.register({
47-
user_id: user.id,
48-
});
49-
} else {
50-
// User logged out - reset PostHog
51-
posthog.reset();
52-
// Re-register static props after reset so anonymous events still have them
53-
posthog.register({
54-
environment: import.meta.env.MODE,
55-
platform: detectPlatform(),
56-
version: __APP_VERSION__,
57-
});
64+
posthog.identify(actorKey, personProperties);
65+
if (isActorChange) {
66+
posthog.register({ user_id: actorKey });
67+
previousActorRef.current = { key: actorKey, wasAuthed: isAuthedActor };
5868
}
59-
}, [posthog, isAuthenticated, user, convexUser]);
69+
}, [posthog, actorKey, user, convexUser]);
6070
}

0 commit comments

Comments
 (0)