diff --git a/env/frontend.env b/env/frontend.env
index b6d13aa462..dac69ec966 100644
--- a/env/frontend.env
+++ b/env/frontend.env
@@ -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
diff --git a/frontends/main/src/app/providers.tsx b/frontends/main/src/app/providers.tsx
index f0e66a07ff..7c0506a702 100644
--- a/frontends/main/src/app/providers.tsx
+++ b/frontends/main/src/app/providers.tsx
@@ -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 (
-
-
+
+
{children}
-
-
+
+
)
}
diff --git a/frontends/main/src/components/ConfiguredPostHogProvider/ConfiguredPostHogProvider.tsx b/frontends/main/src/components/ConfiguredPostHogProvider/ConfiguredPostHogProvider.tsx
deleted file mode 100644
index e69b9afc5d..0000000000
--- a/frontends/main/src/components/ConfiguredPostHogProvider/ConfiguredPostHogProvider.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import React from "react"
-import { PostHogProvider } from "posthog-js/react"
-
-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 = {
- api_host: apiHost,
- bootstrap: {
- featureFlags: featureFlags,
- },
- }
-
- return apiKey ? (
-
- {children}
-
- ) : (
- children
- )
-}
-
-export default ConfiguredPostHogProvider
diff --git a/frontends/main/src/page-components/ConfiguredPostHogProvider/ConfiguredPostHogProvider.test.tsx b/frontends/main/src/page-components/ConfiguredPostHogProvider/ConfiguredPostHogProvider.test.tsx
new file mode 100644
index 0000000000..0015b1ea23
--- /dev/null
+++ b/frontends/main/src/page-components/ConfiguredPostHogProvider/ConfiguredPostHogProvider.test.tsx
@@ -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 = {
+ 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) => {
+ const queryClient = new QueryClient()
+ const userData = makeUserSettings(user)
+
+ setMockResponse.get(urls.userMe.get(), userData)
+ render(
+
+
+ ,
+ )
+ 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()
+ })
+})
diff --git a/frontends/main/src/page-components/ConfiguredPostHogProvider/ConfiguredPostHogProvider.tsx b/frontends/main/src/page-components/ConfiguredPostHogProvider/ConfiguredPostHogProvider.tsx
new file mode 100644
index 0000000000..b39e87ef77
--- /dev/null
+++ b/frontends/main/src/page-components/ConfiguredPostHogProvider/ConfiguredPostHogProvider.tsx
@@ -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 = {
+ api_host: apiHost,
+ bootstrap: {
+ featureFlags: featureFlags,
+ },
+ }
+
+ return apiKey ? (
+
+
+ {children}
+
+ ) : (
+ children
+ )
+}
+
+export default ConfiguredPostHogProvider
+export { PosthogIdenifier }
diff --git a/frontends/main/src/test-utils/index.tsx b/frontends/main/src/test-utils/index.tsx
index 155f46fd02..877d8f684e 100644
--- a/frontends/main/src/test-utils/index.tsx
+++ b/frontends/main/src/test-utils/index.tsx
@@ -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.
@@ -231,7 +227,6 @@ const assertPartialMetas = (expected: Partial) => {
}
export {
- renderTestApp,
renderWithProviders,
expectProps,
expectLastProps,