diff --git a/mcpjam-inspector/client/src/hooks/__tests__/usePostHogIdentify.test.ts b/mcpjam-inspector/client/src/hooks/__tests__/usePostHogIdentify.test.ts new file mode 100644 index 000000000..7aab33a52 --- /dev/null +++ b/mcpjam-inspector/client/src/hooks/__tests__/usePostHogIdentify.test.ts @@ -0,0 +1,117 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { renderHook } from "@testing-library/react"; +import { usePostHogIdentify } from "../usePostHogIdentify"; + +const mockState = vi.hoisted(() => ({ + posthog: { + identify: vi.fn(), + register: vi.fn(), + reset: vi.fn(), + }, + auth: { + user: null as { + id: string; + email: string; + firstName?: string | null; + lastName?: string | null; + } | null, + }, + convexAuth: { + isAuthenticated: false, + }, + detectPlatform: vi.fn(() => "mac"), +})); + +vi.mock("posthog-js/react", () => ({ + usePostHog: () => mockState.posthog, +})); + +vi.mock("@workos-inc/authkit-react", () => ({ + useAuth: () => mockState.auth, +})); + +vi.mock("convex/react", () => ({ + useConvexAuth: () => mockState.convexAuth, +})); + +vi.mock("@/lib/PosthogUtils", () => ({ + detectPlatform: mockState.detectPlatform, +})); + +describe("usePostHogIdentify", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.stubGlobal("__APP_VERSION__", "2.0.13-test"); + mockState.auth.user = null; + mockState.convexAuth.isAuthenticated = false; + mockState.detectPlatform.mockReturnValue("mac"); + }); + + it("identifies authenticated users and registers their user_id", () => { + mockState.auth.user = { + id: "user_123", + email: "user@example.com", + firstName: "Taylor", + lastName: "Smith", + }; + mockState.convexAuth.isAuthenticated = true; + + renderHook(() => usePostHogIdentify()); + + expect(mockState.posthog.identify).toHaveBeenCalledWith("user_123", { + email: "user@example.com", + name: "Taylor Smith", + first_name: "Taylor", + last_name: "Smith", + }); + expect(mockState.posthog.register).toHaveBeenCalledWith({ + user_id: "user_123", + }); + expect(mockState.posthog.reset).not.toHaveBeenCalled(); + }); + + it("re-registers static telemetry properties after logout reset", () => { + renderHook(() => usePostHogIdentify()); + + expect(mockState.posthog.reset).toHaveBeenCalledTimes(1); + expect(mockState.posthog.register).toHaveBeenCalledWith({ + environment: import.meta.env.MODE, + platform: "mac", + version: "2.0.13-test", + }); + }); + + it("resets and re-registers static telemetry properties when auth changes from logged in to logged out", () => { + mockState.auth.user = { + id: "user_123", + email: "user@example.com", + firstName: "Taylor", + lastName: "Smith", + }; + mockState.convexAuth.isAuthenticated = true; + + const { rerender } = renderHook(() => usePostHogIdentify()); + + expect(mockState.posthog.identify).toHaveBeenCalledWith("user_123", { + email: "user@example.com", + name: "Taylor Smith", + first_name: "Taylor", + last_name: "Smith", + }); + + vi.clearAllMocks(); + + mockState.auth.user = null; + mockState.convexAuth.isAuthenticated = false; + + rerender(); + + expect(mockState.posthog.reset).toHaveBeenCalledTimes(1); + expect(mockState.posthog.register).toHaveBeenCalledWith({ + environment: import.meta.env.MODE, + platform: "mac", + version: "2.0.13-test", + }); + expect(mockState.posthog.identify).not.toHaveBeenCalled(); + }); +}); diff --git a/mcpjam-inspector/client/src/hooks/usePostHogIdentify.ts b/mcpjam-inspector/client/src/hooks/usePostHogIdentify.ts index 5f3864c52..8f0eebf2a 100644 --- a/mcpjam-inspector/client/src/hooks/usePostHogIdentify.ts +++ b/mcpjam-inspector/client/src/hooks/usePostHogIdentify.ts @@ -2,6 +2,7 @@ import { useEffect } from "react"; import { usePostHog } from "posthog-js/react"; import { useAuth } from "@workos-inc/authkit-react"; import { useConvexAuth } from "convex/react"; +import { detectPlatform } from "@/lib/PosthogUtils"; /** * Automatically identify users in PostHog when they log in/out @@ -35,6 +36,12 @@ export function usePostHogIdentify() { } else { // User logged out - reset PostHog posthog.reset(); + // Re-register static props after reset so anonymous events still have them + posthog.register({ + environment: import.meta.env.MODE, + platform: detectPlatform(), + version: __APP_VERSION__, + }); } }, [posthog, isAuthenticated, user]); } diff --git a/mcpjam-inspector/client/src/lib/PosthogUtils.ts b/mcpjam-inspector/client/src/lib/PosthogUtils.ts index e5f7e7388..ab3e4a51e 100644 --- a/mcpjam-inspector/client/src/lib/PosthogUtils.ts +++ b/mcpjam-inspector/client/src/lib/PosthogUtils.ts @@ -12,6 +12,7 @@ export const options = { posthog.register({ environment: import.meta.env.MODE, // "development" or "production" platform: detectPlatform(), + version: __APP_VERSION__, }); }, }; diff --git a/mcpjam-inspector/client/src/lib/__tests__/posthog-utils.test.ts b/mcpjam-inspector/client/src/lib/__tests__/posthog-utils.test.ts new file mode 100644 index 000000000..f5e113105 --- /dev/null +++ b/mcpjam-inspector/client/src/lib/__tests__/posthog-utils.test.ts @@ -0,0 +1,22 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { options } from "../PosthogUtils"; + +describe("PosthogUtils", () => { + beforeEach(() => { + vi.stubGlobal("__APP_VERSION__", "2.0.13-test"); + }); + + it("registers static telemetry properties on PostHog load", () => { + const posthog = { + register: vi.fn(), + }; + + options.loaded(posthog); + + expect(posthog.register).toHaveBeenCalledWith({ + environment: import.meta.env.MODE, + platform: expect.any(String), + version: "2.0.13-test", + }); + }); +});