Skip to content

Commit

Permalink
identify users in posthog via user id (#1799)
Browse files Browse the repository at this point in the history
* identify users in posthog via user id

* add posthog to frontend.env default file
  • Loading branch information
ChristopherChudzicki authored Nov 6, 2024
1 parent 87b36b2 commit bcf2f10
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 38 deletions.
2 changes: 2 additions & 0 deletions env/frontend.env
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ NEXT_PUBLIC_MITOL_API_BASE_URL=${MITOL_API_BASE_URL}
NEXT_PUBLIC_CSRF_COOKIE_NAME=${CSRF_COOKIE_NAME}
NEXT_PUBLIC_MITOL_SUPPORT_EMAIL=${MITOL_SUPPORT_EMAIL}

NEXT_PUBLIC_POSTHOG_API_KEY=${POSTHOG_PROJECT_API_KEY}

NEXT_PUBLIC_SITE_NAME="MIT Learn"
NEXT_PUBLIC_MITOL_AXIOS_WITH_CREDENTIALS=true

Expand Down
10 changes: 5 additions & 5 deletions frontends/main/src/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@ import { getQueryClient } from "./getQueryClient"
import { QueryClientProvider } from "@tanstack/react-query"
import { ThemeProvider, NextJsAppRouterCacheProvider } from "ol-components"
import { Provider as NiceModalProvider } from "@ebay/nice-modal-react"
import ConfiguredPostHogProvider from "@/components/ConfiguredPostHogProvider/ConfiguredPostHogProvider"
import ConfiguredPostHogProvider from "@/page-components/ConfiguredPostHogProvider/ConfiguredPostHogProvider"

export default function Providers({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient()

return (
<ConfiguredPostHogProvider>
<QueryClientProvider client={queryClient}>
<QueryClientProvider client={queryClient}>
<ConfiguredPostHogProvider>
<NextJsAppRouterCacheProvider>
<ThemeProvider>
<NiceModalProvider>{children}</NiceModalProvider>
</ThemeProvider>
</NextJsAppRouterCacheProvider>
</QueryClientProvider>
</ConfiguredPostHogProvider>
</ConfiguredPostHogProvider>
</QueryClientProvider>
)
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from "react"

import { render, waitFor } from "@testing-library/react"
import { PosthogIdenifier } from "./ConfiguredPostHogProvider"
import { QueryClientProvider, QueryClient } from "@tanstack/react-query"

// mock stuff
import { setMockResponse, urls } from "api/test-utils"
import type { User } from "api/hooks/user"
import { makeUserSettings } from "@/test-utils/factories"
import { usePostHog } from "posthog-js/react"
import type { PostHog } from "posthog-js"

jest.mock("posthog-js/react", () => {
return {
__esModule: true,
usePostHog: jest.fn(),
}
})
const mockUsePostHog = jest.mocked(usePostHog)
const posthog: Pick<PostHog, "identify" | "reset" | "get_property"> = {
identify: jest.fn(),
reset: jest.fn(),
get_property: jest.fn(),
}
mockUsePostHog.mockReturnValue(posthog as PostHog)
const mockPosthog = jest.mocked(posthog)

describe("PosthogIdenifier", () => {
const setup = (user: Partial<User>) => {
const queryClient = new QueryClient()
const userData = makeUserSettings(user)

setMockResponse.get(urls.userMe.get(), userData)
render(
<QueryClientProvider client={queryClient}>
<PosthogIdenifier />
</QueryClientProvider>,
)
return userData
}
test.each([
{ posthogUserState: "anonymous", resetCalls: 0 },
{ posthogUserState: "anything_else", resetCalls: 1 },
])(
"If user is NOT authenticated, calls `reset` if and only if not already anonymous",
async ({ posthogUserState, resetCalls }) => {
setup({ is_authenticated: false })
mockPosthog.get_property.mockReturnValue(posthogUserState)
await waitFor(() => {
expect(mockPosthog.get_property).toHaveBeenCalledWith("$user_state")
})
expect(mockPosthog.reset).toHaveBeenCalledTimes(resetCalls)
expect(mockPosthog.identify).not.toHaveBeenCalled()
},
)

test("If authenticated, calls `identify` with user id and username", async () => {
const user = setup({ is_authenticated: true })
await waitFor(() => {
expect(mockPosthog.identify).toHaveBeenCalledWith(String(user.id))
})
expect(mockPosthog.reset).not.toHaveBeenCalled()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { useEffect } from "react"
import { PostHogProvider, usePostHog } from "posthog-js/react"
import { useUserMe } from "api/hooks/user"
import type { PostHogConfig } from "posthog-js"

const PosthogIdenifier = () => {
const { data: user } = useUserMe()
const posthog = usePostHog()
/**
* Posthog docs recommend calling `posthog.identify` on signin and
* `posthog.reset` on signout. But signin and signout generally occur on the
* SSO server, which users could get to via other means.
*
* So instead, when page first loads:
* 1. Identify user (noop if user already identified)
* 2. If user is not authenticated AND posthog thinks they are not anonymous,
* then reset their posthog state.
*/
useEffect(() => {
if (!user) return
const anonymous = posthog.get_property("$user_state") === "anonymous"
if (user.is_authenticated && user.id) {
posthog.identify(String(user.id))
} else if (!anonymous) {
posthog.reset()
}
}, [user, posthog])
return null
}

const ConfiguredPostHogProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const apiKey = process.env.NEXT_PUBLIC_POSTHOG_API_KEY || ""
const apiHost =
process.env.NEXT_PUBLIC_POSTHOG_API_HOST || "https://us.i.posthog.com"
const featureFlags = JSON.parse(process.env.FEATURE_FLAGS || "")

const postHogOptions: Partial<PostHogConfig> = {
api_host: apiHost,
bootstrap: {
featureFlags: featureFlags,
},
}

return apiKey ? (
<PostHogProvider apiKey={apiKey} options={postHogOptions}>
<PosthogIdenifier />
{children}
</PostHogProvider>
) : (
children
)
}

export default ConfiguredPostHogProvider
export { PosthogIdenifier }
5 changes: 0 additions & 5 deletions frontends/main/src/test-utils/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,6 @@ const renderWithProviders = (
return { view, queryClient, location }
}

const renderTestApp = () => {
throw new Error("not supported")
}

/**
* Assert that a functional component was called at some point with the given
* props.
Expand Down Expand Up @@ -231,7 +227,6 @@ const assertPartialMetas = (expected: Partial<TestableMetas>) => {
}

export {
renderTestApp,
renderWithProviders,
expectProps,
expectLastProps,
Expand Down

0 comments on commit bcf2f10

Please sign in to comment.