diff --git a/frontends/main/src/app-pages/HomePage/HomePage.test.tsx b/frontends/main/src/app-pages/HomePage/HomePage.test.tsx
index 6778a7d094..b91ce75662 100644
--- a/frontends/main/src/app-pages/HomePage/HomePage.test.tsx
+++ b/frontends/main/src/app-pages/HomePage/HomePage.test.tsx
@@ -17,8 +17,9 @@ import {
import invariant from "tiny-invariant"
import * as routes from "@/common/urls"
import { FeatureFlags } from "@/common/feature_flags"
-import { assertHeadings } from "ol-test-utilities"
+import { assertHeadings, allowConsoleErrors } from "ol-test-utilities"
import { useFeatureFlagEnabled, usePostHog } from "posthog-js/react"
+import { PostHogEvents } from "@/common/constants"
jest.mock("posthog-js/react")
const mockedUseFeatureFlagEnabled = jest.mocked(useFeatureFlagEnabled)
@@ -128,6 +129,28 @@ describe("UAI Announcement Card", () => {
})
assertLinksTo(cta, { pathname: "/programs/program-v1:UAI+B2C" })
})
+
+ test("CTA click fires cta_clicked with label and readableId", async () => {
+ allowConsoleErrors()
+ setupAPIs()
+ mockedUseFeatureFlagEnabled.mockImplementation(
+ (flag) => flag === FeatureFlags.UniversalAI,
+ )
+ process.env.NEXT_PUBLIC_POSTHOG_API_KEY = "test-key"
+ renderWithProviders()
+ const cta = await screen.findByRole("link", {
+ name: "Learn about Universal AI",
+ })
+ await user.click(cta)
+ expect(mockedPostHogCapture).toHaveBeenCalledWith(
+ PostHogEvents.CallToActionClicked,
+ expect.objectContaining({
+ label: "Learn about Universal AI",
+ readableId: "program-v1:UAI+B2C",
+ }),
+ )
+ delete process.env.NEXT_PUBLIC_POSTHOG_API_KEY
+ })
})
describe("Home Page Hero", () => {
diff --git a/frontends/main/src/app-pages/HomePage/UAIAnnouncementCard.tsx b/frontends/main/src/app-pages/HomePage/UAIAnnouncementCard.tsx
index dde506e649..d95cc93d78 100644
--- a/frontends/main/src/app-pages/HomePage/UAIAnnouncementCard.tsx
+++ b/frontends/main/src/app-pages/HomePage/UAIAnnouncementCard.tsx
@@ -1,10 +1,12 @@
+"use client"
import React from "react"
import { Typography, styled } from "ol-components"
import { ButtonLink } from "@mitodl/smoot-design"
import { RiArrowRightLine } from "@remixicon/react"
-import { useFeatureFlagEnabled } from "posthog-js/react"
+import { useFeatureFlagEnabled, usePostHog } from "posthog-js/react"
import { FeatureFlags } from "@/common/feature_flags"
import { programPageView } from "@/common/urls"
+import { PostHogEvents } from "@/common/constants"
const UAI_PROGRAM_READABLE_ID = "program-v1:UAI+B2C"
@@ -243,15 +245,26 @@ const CTAButton = styled(ButtonLink)(({ theme }) => ({
const UAIAnnouncementCard: React.FC = () => {
const showUAICard = useFeatureFlagEnabled(FeatureFlags.UniversalAI)
- if (!showUAICard) {
- return null
- }
+ const posthog = usePostHog()
const ctaHref = programPageView({
readable_id: UAI_PROGRAM_READABLE_ID,
display_mode: null,
})
+ const handleCTAClick = () => {
+ if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) {
+ posthog.capture(PostHogEvents.CallToActionClicked, {
+ label: "Learn about Universal AI",
+ readableId: UAI_PROGRAM_READABLE_ID,
+ })
+ }
+ }
+
+ if (!showUAICard) {
+ return null
+ }
+
return (
@@ -275,6 +288,7 @@ const UAIAnnouncementCard: React.FC = () => {
variant="primary"
href={ctaHref}
endIcon={}
+ onClick={handleCTAClick}
>
Learn about Universal AI
@@ -294,6 +308,7 @@ const UAIAnnouncementCard: React.FC = () => {
variant="primary"
href={ctaHref}
endIcon={}
+ onClick={handleCTAClick}
>
Learn about Universal AI
diff --git a/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.test.tsx b/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.test.tsx
index b1d76e9dd9..7832598f0d 100644
--- a/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.test.tsx
+++ b/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.test.tsx
@@ -15,6 +15,18 @@ import {
} from "api/mitxonline-test-utils"
import { mitxonlineLegacyUrl } from "@/common/mitxonline"
import * as routes from "@/common/urls"
+import { usePostHog } from "posthog-js/react"
+import { PostHogEvents } from "@/common/constants"
+
+jest.mock("posthog-js/react", () => ({
+ ...jest.requireActual("posthog-js/react"),
+ usePostHog: jest.fn(),
+}))
+const mockCapture = jest.fn()
+jest.mocked(usePostHog).mockReturnValue(
+ // @ts-expect-error Not mocking all of posthog
+ { capture: mockCapture },
+)
const makeCourse = mitxFactories.courses.course
const makeRun = mitxFactories.courses.courseRun
@@ -505,4 +517,70 @@ describe("CourseEnrollmentButton", () => {
expect(button).toBeInTheDocument()
expect(button).toBeDisabled()
})
+
+ describe("PostHog tracking", () => {
+ beforeEach(() => {
+ process.env.NEXT_PUBLIC_POSTHOG_API_KEY = "test-key"
+ mockCapture.mockClear()
+ })
+ afterEach(() => {
+ delete process.env.NEXT_PUBLIC_POSTHOG_API_KEY
+ })
+
+ test("fires cta_clicked when authenticated user clicks enroll", async () => {
+ const run = makeRun({
+ is_archived: false,
+ is_enrollable: true,
+ enrollment_modes: [makeEnrollmentMode({ requires_payment: false })],
+ })
+ const course = makeCourse({ next_run_id: run.id, courseruns: [run] })
+ setMockResponse.get(
+ urls.userMe.get(),
+ makeUser({ is_authenticated: true }),
+ )
+ // Clicking enroll triggers the enrollment API call; mock it to avoid failing on console.error.
+ setMockResponse.post(mitxUrls.enrollment.enrollmentsListV1(), {})
+
+ renderWithProviders()
+ await user.click(
+ await screen.findByRole("button", { name: "Enroll for Free" }),
+ )
+
+ expect(mockCapture).toHaveBeenCalledWith(
+ PostHogEvents.CallToActionClicked,
+ expect.objectContaining({
+ resourceId: course.id,
+ readableId: course.readable_id,
+ resourceType: "course",
+ }),
+ )
+ })
+
+ test("fires cta_clicked when unauthenticated user clicks enroll", async () => {
+ const run = makeRun({
+ is_archived: false,
+ is_enrollable: true,
+ enrollment_modes: [makeEnrollmentMode({ requires_payment: false })],
+ })
+ const course = makeCourse({ next_run_id: run.id, courseruns: [run] })
+ setMockResponse.get(
+ urls.userMe.get(),
+ makeUser({ is_authenticated: false }),
+ )
+
+ renderWithProviders()
+ await user.click(
+ await screen.findByRole("button", { name: "Enroll for Free" }),
+ )
+
+ expect(mockCapture).toHaveBeenCalledWith(
+ PostHogEvents.CallToActionClicked,
+ expect.objectContaining({
+ resourceId: course.id,
+ readableId: course.readable_id,
+ resourceType: "course",
+ }),
+ )
+ })
+ })
})
diff --git a/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx b/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx
index 2652f21bf1..8fde426a37 100644
--- a/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx
+++ b/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx
@@ -21,6 +21,8 @@ import { productQueries } from "api/mitxonline-hooks/products"
import { useReplaceBasketItem } from "api/mitxonline-hooks/baskets"
import { useCreateEnrollment } from "api/mitxonline-hooks/enrollment"
import { useRouter } from "next-nprogress-bar"
+import { usePostHog } from "posthog-js/react"
+import { PostHogEvents } from "@/common/constants"
const DiscountedPriceContent = styled.span({
display: "inline-flex",
@@ -60,6 +62,7 @@ const CourseEnrollmentButton: React.FC = ({
const replaceBasketItem = useReplaceBasketItem()
const createEnrollment = useCreateEnrollment()
const router = useRouter()
+ const posthog = usePostHog()
const nextRunId = course.next_run_id
const nextRun = course.courseruns.find((run) => run.id === nextRunId)
const enrollmentDecision = getCourseEnrollmentAction(course)
@@ -90,7 +93,16 @@ const CourseEnrollmentButton: React.FC = ({
const handleClick: React.MouseEventHandler = (e) => {
if (me.isLoading) {
return
- } else if (me.data?.is_authenticated) {
+ }
+ if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) {
+ posthog.capture(PostHogEvents.CallToActionClicked, {
+ resourceId: course.id,
+ readableId: course.readable_id,
+ resourceType: "course",
+ label: getButtonText(nextRun, price?.finalPrice),
+ })
+ }
+ if (me.data?.is_authenticated) {
if (enrollmentDecision.type === "dialog") {
NiceModal.show(CourseEnrollmentDialog, { course })
} else if (enrollmentDecision.type === "checkout") {
diff --git a/frontends/main/src/app-pages/ProductPages/MitxOnlineResourceCard.tsx b/frontends/main/src/app-pages/ProductPages/MitxOnlineResourceCard.tsx
index 1fa1082ca0..5564aaa800 100644
--- a/frontends/main/src/app-pages/ProductPages/MitxOnlineResourceCard.tsx
+++ b/frontends/main/src/app-pages/ProductPages/MitxOnlineResourceCard.tsx
@@ -1,7 +1,9 @@
"use client"
import React from "react"
+import { usePostHog } from "posthog-js/react"
import { BaseLearningResourceCard } from "ol-components"
+import { PostHogEvents } from "@/common/constants"
import type {
CourseWithCourseRunsSerializerV2,
V2ProgramDetail,
@@ -17,6 +19,7 @@ type CommonCardProps = {
headingLevel?: number
className?: string
list?: boolean
+ label?: string
}
type MitxOnlineCourseCardProps = CommonCardProps & {
@@ -156,6 +159,7 @@ const extractCardData = (
const MitxOnlineResourceCard: React.FC = (
props,
) => {
+ const posthog = usePostHog()
const {
href,
size = "small",
@@ -163,6 +167,7 @@ const MitxOnlineResourceCard: React.FC = (
headingLevel = 6,
className,
list,
+ label,
} = props
if (isLoading) {
@@ -198,6 +203,16 @@ const MitxOnlineResourceCard: React.FC = (
startDate={data.startDate}
ariaLabel={`${data.displayType}: ${data.title}`}
list={list}
+ onClick={() => {
+ if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) {
+ posthog.capture(PostHogEvents.CourseCardClicked, {
+ label,
+ resourceId: props.resource?.id,
+ readableId: props.resource?.readable_id,
+ resourceType: props.resourceType,
+ })
+ }
+ }}
/>
)
}
diff --git a/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.test.tsx b/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.test.tsx
index 118619bc68..e8ac36ce99 100644
--- a/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.test.tsx
+++ b/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.test.tsx
@@ -13,15 +13,21 @@ import {
urls as mitxUrls,
factories as mitxFactories,
} from "api/mitxonline-test-utils"
-import { useFeatureFlagEnabled } from "posthog-js/react"
+import { useFeatureFlagEnabled, usePostHog } from "posthog-js/react"
import { programView } from "@/common/urls"
import { mitxonlineLegacyUrl } from "@/common/mitxonline"
import * as routes from "@/common/urls"
+import { PostHogEvents } from "@/common/constants"
jest.mock("posthog-js/react")
const mockedUseFeatureFlagEnabled = jest
.mocked(useFeatureFlagEnabled)
.mockImplementation(() => false)
+const mockCapture = jest.fn()
+jest.mocked(usePostHog).mockReturnValue(
+ // @ts-expect-error Not mocking all of posthog
+ { capture: mockCapture },
+)
const makeProgram = mitxFactories.programs.program
const makeProgramEnrollment = mitxFactories.enrollment.programEnrollmentV3
@@ -399,4 +405,75 @@ describe("ProgramEnrollmentButton", () => {
screen.getByTestId("signup-popover")
})
+
+ describe("PostHog tracking", () => {
+ beforeEach(() => {
+ process.env.NEXT_PUBLIC_POSTHOG_API_KEY = "test-key"
+ mockCapture.mockClear()
+ })
+ afterEach(() => {
+ delete process.env.NEXT_PUBLIC_POSTHOG_API_KEY
+ })
+
+ test("fires cta_clicked when authenticated user clicks enroll", async () => {
+ const program = makeProgram({
+ enrollment_modes: [makeEnrollmentMode({ requires_payment: false })],
+ })
+ setMockResponse.get(mitxUrls.programEnrollments.enrollmentsListV3(), [])
+ setMockResponse.get(
+ urls.userMe.get(),
+ makeUser({ is_authenticated: true }),
+ )
+ setMockResponse.post(
+ mitxUrls.programEnrollments.enrollmentsListV3(),
+ null,
+ { code: 201 },
+ )
+
+ renderWithProviders(
+ ,
+ )
+ await user.click(
+ await screen.findByRole("button", { name: "Enroll for Free" }),
+ )
+
+ expect(mockCapture).toHaveBeenCalledWith(
+ PostHogEvents.CallToActionClicked,
+ expect.objectContaining({
+ resourceId: program.id,
+ readableId: program.readable_id,
+ resourceType: "program",
+ }),
+ )
+ })
+
+ test("fires cta_clicked when unauthenticated user clicks enroll", async () => {
+ const program = makeProgram({
+ enrollment_modes: [makeEnrollmentMode({ requires_payment: false })],
+ })
+ setMockResponse.get(mitxUrls.programEnrollments.enrollmentsListV3(), [], {
+ code: 403,
+ })
+ setMockResponse.get(
+ urls.userMe.get(),
+ makeUser({ is_authenticated: false }),
+ )
+
+ renderWithProviders(
+ ,
+ )
+ await user.click(
+ await screen.findByRole("button", { name: "Enroll for Free" }),
+ )
+
+ expect(mockCapture).toHaveBeenCalledWith(
+ PostHogEvents.CallToActionClicked,
+ expect.objectContaining({
+ resourceId: program.id,
+ readableId: program.readable_id,
+ resourceType: "program",
+ }),
+ )
+ })
+ })
})
diff --git a/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx b/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx
index 72cd7f674d..3722ba6361 100644
--- a/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx
+++ b/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx
@@ -19,7 +19,7 @@ import NiceModal from "@ebay/nice-modal-react"
import { userQueries } from "api/hooks/user"
import { SignupPopover } from "@/page-components/SignupPopover/SignupPopover"
import { programView } from "@/common/urls"
-import { useFeatureFlagEnabled } from "posthog-js/react"
+import { useFeatureFlagEnabled, usePostHog } from "posthog-js/react"
import { FeatureFlags } from "@/common/feature_flags"
import {
enrollmentAlertSuccessUrl,
@@ -28,6 +28,7 @@ import {
} from "@/common/mitxonline"
import { useReplaceBasketItem } from "api/mitxonline-hooks/baskets"
import { useRouter } from "next-nprogress-bar"
+import { PostHogEvents } from "@/common/constants"
const ButtonLinkWithDisabled = styled(ButtonLink)(({ href }) => [
!href && {
@@ -81,11 +82,21 @@ const ProgramEnrollmentButton: React.FC = ({
const isPending =
replaceBasketItem.isPending || createProgramEnrollment.isPending
const isError = replaceBasketItem.isError || createProgramEnrollment.isError
+ const posthog = usePostHog()
const handleClick: React.MouseEventHandler = (e) => {
if (enrollments.isLoading || me.isLoading) {
return
- } else if (me.data?.is_authenticated) {
+ }
+ if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) {
+ posthog.capture(PostHogEvents.CallToActionClicked, {
+ resourceId: program.id,
+ readableId: program.readable_id,
+ resourceType: "program",
+ label: getEnrollButtonText(),
+ })
+ }
+ if (me.data?.is_authenticated) {
if (enrollmentType === "paid" && program.products[0]) {
replaceBasketItem.mutate(program.products[0].id)
} else if (enrollmentType === "free") {
diff --git a/frontends/main/src/app-pages/ProductPages/ProgramPage.test.tsx b/frontends/main/src/app-pages/ProductPages/ProgramPage.test.tsx
index bbe5be5621..c05a23a0c5 100644
--- a/frontends/main/src/app-pages/ProductPages/ProgramPage.test.tsx
+++ b/frontends/main/src/app-pages/ProductPages/ProgramPage.test.tsx
@@ -16,23 +16,35 @@ import type {
CourseWithCourseRunsSerializerV2,
} from "@mitodl/mitxonline-api-axios/v2"
import { DisplayModeEnum } from "@mitodl/mitxonline-api-axios/v2"
-import { renderWithProviders, waitFor, screen, within } from "@/test-utils"
+import {
+ renderWithProviders,
+ waitFor,
+ screen,
+ within,
+ user,
+} from "@/test-utils"
import ProgramPage from "./ProgramPage"
-import { assertHeadings } from "ol-test-utilities"
+import { assertHeadings, allowConsoleErrors } from "ol-test-utilities"
import { notFound } from "next/navigation"
import {
useStayUpdatedEnv,
PROGRAM_HIDE_STAY_UPDATED_CASES,
} from "./test-utils/stayUpdated"
-import { useFeatureFlagEnabled } from "posthog-js/react"
+import { useFeatureFlagEnabled, usePostHog } from "posthog-js/react"
import invariant from "tiny-invariant"
import { useFeatureFlagsLoaded } from "@/common/useFeatureFlagsLoaded"
import { getIdsFromReqTree } from "@/common/mitxonline"
import { faker } from "@faker-js/faker/locale/en"
+import { PostHogEvents } from "@/common/constants"
jest.mock("posthog-js/react")
const mockedUseFeatureFlagEnabled = jest.mocked(useFeatureFlagEnabled)
+const mockCapture = jest.fn()
+jest.mocked(usePostHog).mockReturnValue(
+ // @ts-expect-error Not mocking all of posthog
+ { capture: mockCapture },
+)
jest.mock("@/common/useFeatureFlagsLoaded")
const mockedUseFeatureFlagsLoaded = jest.mocked(useFeatureFlagsLoaded)
@@ -640,6 +652,32 @@ describe("ProgramPage", () => {
})
})
+ test("clicking a requirement card fires course_card_clicked", async () => {
+ allowConsoleErrors()
+ const program = makeProgram({
+ ...makeReqs({ required: { count: 1, title: "Required" } }),
+ })
+ const page = makePage({ program_details: program })
+ const { courses } = setupApis({ program, page })
+ process.env.NEXT_PUBLIC_POSTHOG_API_KEY = "test-key"
+ mockCapture.mockClear()
+
+ renderWithProviders()
+
+ const course = courses[0]
+ const card = await screen.findByRole("heading", { name: course.title })
+ await user.click(card)
+
+ expect(mockCapture).toHaveBeenCalledWith(
+ PostHogEvents.CourseCardClicked,
+ expect.objectContaining({
+ resourceId: course.id,
+ readableId: course.readable_id,
+ }),
+ )
+ delete process.env.NEXT_PUBLIC_POSTHOG_API_KEY
+ })
+
describe("Stay Updated button", () => {
useStayUpdatedEnv()
diff --git a/frontends/main/src/app-pages/ProductPages/ProgramPage.tsx b/frontends/main/src/app-pages/ProductPages/ProgramPage.tsx
index e17659d664..e4f32169d6 100644
--- a/frontends/main/src/app-pages/ProductPages/ProgramPage.tsx
+++ b/frontends/main/src/app-pages/ProductPages/ProgramPage.tsx
@@ -179,6 +179,7 @@ const RequirementsSection: React.FC = ({
}
size="small"
isLoading={isLoading}
+ label={req.title}
list
/>
@@ -201,6 +202,7 @@ const RequirementsSection: React.FC = ({
}
size="small"
isLoading={isLoading}
+ label={req.title}
list
/>
diff --git a/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx b/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx
index 028bde95ed..dd7a8dfd17 100644
--- a/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx
+++ b/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx
@@ -14,17 +14,27 @@ import type {
} from "api"
import invariant from "tiny-invariant"
import { Permission } from "api/hooks/user"
-import { assertHeadings, ControlledPromise } from "ol-test-utilities"
+import {
+ assertHeadings,
+ ControlledPromise,
+ allowConsoleErrors,
+} from "ol-test-utilities"
import { act } from "@testing-library/react"
-import { useFeatureFlagEnabled } from "posthog-js/react"
+import { useFeatureFlagEnabled, usePostHog } from "posthog-js/react"
+import { PostHogEvents } from "@/common/constants"
jest.mock("posthog-js/react", () => ({
...jest.requireActual("posthog-js/react"),
useFeatureFlagEnabled: jest.fn(),
- usePostHog: jest.fn(() => ({ capture: jest.fn() })),
+ usePostHog: jest.fn(),
}))
const mockedUseFeatureFlagEnabled = jest.mocked(useFeatureFlagEnabled)
+const mockCapture = jest.fn()
+jest.mocked(usePostHog).mockReturnValue(
+ // @ts-expect-error Not mocking all of posthog
+ { capture: mockCapture },
+)
const DEFAULT_SEARCH_RESPONSE: LearningResourcesSearchResponse = {
count: 0,
@@ -1073,4 +1083,25 @@ describe("UniversalAIBanner", () => {
])
})
})
+
+ test("clicking Learn More fires cta_clicked with label and readableId", async () => {
+ allowConsoleErrors()
+ mockedUseFeatureFlagEnabled.mockReturnValue(true)
+ mockCapture.mockClear()
+ process.env.NEXT_PUBLIC_POSTHOG_API_KEY = "test-key"
+ setMockApiResponses({ search: { count: 10 } })
+ renderWithProviders()
+ const link = await screen.findByRole("link", {
+ name: "Learn more about Universal AI",
+ })
+ await user.click(link)
+ expect(mockCapture).toHaveBeenCalledWith(
+ PostHogEvents.CallToActionClicked,
+ expect.objectContaining({
+ label: "Learn more about Universal AI",
+ readableId: "program-v1:UAI+B2C",
+ }),
+ )
+ delete process.env.NEXT_PUBLIC_POSTHOG_API_KEY
+ })
})
diff --git a/frontends/main/src/common/constants.ts b/frontends/main/src/common/constants.ts
index 68fb74ab33..98d72436f9 100644
--- a/frontends/main/src/common/constants.ts
+++ b/frontends/main/src/common/constants.ts
@@ -1,6 +1,8 @@
export const PostHogEvents = {
SearchUpdate: "search_update",
CallToActionClicked: "cta_clicked",
+ CourseCardClicked: "course_card_clicked",
+ AskTimClicked: "asktim_clicked",
LearningResourceDrawerOpen: "lrd_open",
LearningResourceDrawerView: "lrd_view",
HeroBrowseTopics: "clicked_hero_browse_topics",
diff --git a/frontends/main/src/page-components/AiChat/AskTimDrawerButton.test.tsx b/frontends/main/src/page-components/AiChat/AskTimDrawerButton.test.tsx
index 106d45493f..5751d22709 100644
--- a/frontends/main/src/page-components/AiChat/AskTimDrawerButton.test.tsx
+++ b/frontends/main/src/page-components/AiChat/AskTimDrawerButton.test.tsx
@@ -2,6 +2,19 @@ import React from "react"
import { renderWithProviders, screen, user, waitFor } from "@/test-utils"
import AskTIMButton from "./AskTimDrawerButton"
import { RECOMMENDER_QUERY_PARAM } from "@/common/urls"
+import { usePostHog } from "posthog-js/react"
+import { PostHogEvents } from "@/common/constants"
+import { allowConsoleErrors } from "ol-test-utilities"
+
+jest.mock("posthog-js/react", () => ({
+ ...jest.requireActual("posthog-js/react"),
+ usePostHog: jest.fn(),
+}))
+const mockCapture = jest.fn()
+jest.mocked(usePostHog).mockReturnValue(
+ // @ts-expect-error Not mocking all of posthog
+ { capture: mockCapture },
+)
describe("AskTIMButton", () => {
it.each([
@@ -39,4 +52,16 @@ describe("AskTIMButton", () => {
)
})
})
+
+ test("clicking Ask TIM fires asktim_clicked with type recommendation_bot", async () => {
+ allowConsoleErrors()
+ process.env.NEXT_PUBLIC_POSTHOG_API_KEY = "test-key"
+ mockCapture.mockClear()
+ renderWithProviders()
+ await user.click(screen.getByRole("link", { name: /ask tim/i }))
+ expect(mockCapture).toHaveBeenCalledWith(PostHogEvents.AskTimClicked, {
+ type: "recommendation_bot",
+ })
+ delete process.env.NEXT_PUBLIC_POSTHOG_API_KEY
+ })
})
diff --git a/frontends/main/src/page-components/AiChat/AskTimDrawerButton.tsx b/frontends/main/src/page-components/AiChat/AskTimDrawerButton.tsx
index 015f202225..bcb980d82a 100644
--- a/frontends/main/src/page-components/AiChat/AskTimDrawerButton.tsx
+++ b/frontends/main/src/page-components/AiChat/AskTimDrawerButton.tsx
@@ -1,8 +1,12 @@
+"use client"
+
import React from "react"
import { Typography, styled, LinkAdapter } from "ol-components"
import { RiSparkling2Line } from "@remixicon/react"
+import { usePostHog } from "posthog-js/react"
import AiRecommendationBotDrawer from "./AiRecommendationBotDrawer"
import { RECOMMENDER_QUERY_PARAM } from "@/common/urls"
+import { PostHogEvents } from "@/common/constants"
const StyledButton = styled(LinkAdapter)(({ theme }) => ({
display: "flex",
@@ -21,9 +25,21 @@ const StyledButton = styled(LinkAdapter)(({ theme }) => ({
}))
const AskTIMButton = () => {
+ const posthog = usePostHog()
+
return (
<>
-
+ {
+ if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) {
+ posthog.capture(PostHogEvents.AskTimClicked, {
+ type: "recommendation_bot",
+ })
+ }
+ }}
+ >
AskTIM
diff --git a/frontends/main/src/page-components/LearningResourceExpanded/AiChatSyllabusSlideDown.test.tsx b/frontends/main/src/page-components/LearningResourceExpanded/AiChatSyllabusSlideDown.test.tsx
index c3b7450c15..52ce0c3b5f 100644
--- a/frontends/main/src/page-components/LearningResourceExpanded/AiChatSyllabusSlideDown.test.tsx
+++ b/frontends/main/src/page-components/LearningResourceExpanded/AiChatSyllabusSlideDown.test.tsx
@@ -1,5 +1,6 @@
import React from "react"
import AiChatSyllabusSlideDown, {
+ AiChatSyllabusOpener,
ChatTransitionState,
STARTERS,
} from "./AiChatSyllabusSlideDown"
@@ -7,6 +8,18 @@ import { renderWithProviders, screen, user } from "@/test-utils"
import { factories } from "api/test-utils"
import { ResourceTypeEnum, ResourceTypeGroupEnum } from "api"
import invariant from "tiny-invariant"
+import { usePostHog } from "posthog-js/react"
+import { PostHogEvents } from "@/common/constants"
+
+jest.mock("posthog-js/react", () => ({
+ ...jest.requireActual("posthog-js/react"),
+ usePostHog: jest.fn(),
+}))
+const mockCapture = jest.fn()
+jest.mocked(usePostHog).mockReturnValue(
+ // @ts-expect-error Not mocking all of posthog
+ { capture: mockCapture },
+)
const mockAiChat = jest.fn]>(() => (
@@ -114,4 +127,31 @@ describe("AiChatSyllabus", () => {
"include",
)
})
+
+ test("clicking Ask TIM opener fires asktim_clicked with syllabus payload", async () => {
+ const resource = factories.learningResources.course()
+ process.env.NEXT_PUBLIC_POSTHOG_API_KEY = "test-key"
+ mockCapture.mockClear()
+
+ renderWithProviders(
+ ,
+ )
+
+ await user.click(screen.getByRole("button", { name: /ask tim/i }))
+
+ expect(mockCapture).toHaveBeenCalledWith(
+ PostHogEvents.AskTimClicked,
+ expect.objectContaining({
+ type: "syllabus_bot",
+ resourceId: resource.id,
+ readableId: resource.readable_id,
+ resourceType: resource.resource_type,
+ }),
+ )
+ delete process.env.NEXT_PUBLIC_POSTHOG_API_KEY
+ })
})
diff --git a/frontends/main/src/page-components/LearningResourceExpanded/AiChatSyllabusSlideDown.tsx b/frontends/main/src/page-components/LearningResourceExpanded/AiChatSyllabusSlideDown.tsx
index 35464766bf..fa70037fbe 100644
--- a/frontends/main/src/page-components/LearningResourceExpanded/AiChatSyllabusSlideDown.tsx
+++ b/frontends/main/src/page-components/LearningResourceExpanded/AiChatSyllabusSlideDown.tsx
@@ -5,6 +5,8 @@ import { RiSparkling2Line, RiArrowDownSLine } from "@remixicon/react"
import type { AiChatProps } from "@mitodl/smoot-design/ai"
import { LearningResource, ResourceTypeGroupEnum } from "api"
import { AiChat } from "@mitodl/smoot-design/ai"
+import { usePostHog } from "posthog-js/react"
+import { PostHogEvents } from "@/common/constants"
export enum ChatTransitionState {
Closed = "Closed",
@@ -140,6 +142,8 @@ export const AiChatSyllabusOpener = ({
onToggleOpen: (open: boolean) => void
resource: LearningResource
}) => {
+ const posthog = usePostHog()
+
return (
onToggleOpen(!open)}
+ onClick={() => {
+ if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) {
+ posthog.capture(PostHogEvents.AskTimClicked, {
+ type: "syllabus_bot",
+ resourceId: resource.id,
+ readableId: resource.readable_id,
+ resourceType: resource.resource_type,
+ platformCode: resource.platform?.code,
+ })
+ }
+ onToggleOpen(!open)
+ }}
>
diff --git a/frontends/main/src/page-components/LearningResourceExpanded/CallToActionSection.test.tsx b/frontends/main/src/page-components/LearningResourceExpanded/CallToActionSection.test.tsx
index fcb372b00d..4cd980fa21 100644
--- a/frontends/main/src/page-components/LearningResourceExpanded/CallToActionSection.test.tsx
+++ b/frontends/main/src/page-components/LearningResourceExpanded/CallToActionSection.test.tsx
@@ -349,6 +349,7 @@ describe("CallToActionSection", () => {
expect(mockCapture).toHaveBeenCalledWith("cta_clicked", {
resource,
+ label: "Learn More",
})
// Restore original value
diff --git a/frontends/main/src/page-components/LearningResourceExpanded/CallToActionSection.tsx b/frontends/main/src/page-components/LearningResourceExpanded/CallToActionSection.tsx
index 88817cc87a..a006809575 100644
--- a/frontends/main/src/page-components/LearningResourceExpanded/CallToActionSection.tsx
+++ b/frontends/main/src/page-components/LearningResourceExpanded/CallToActionSection.tsx
@@ -414,7 +414,10 @@ const CallToActionSection = ({
href={url}
onClick={() => {
if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) {
- posthog.capture(PostHogEvents.CallToActionClicked, { resource })
+ posthog.capture(PostHogEvents.CallToActionClicked, {
+ resource,
+ label: cta,
+ })
}
}}
data-ph-action="click-cta"
diff --git a/frontends/main/src/page-components/ResourceCard/ResourceCard.tsx b/frontends/main/src/page-components/ResourceCard/ResourceCard.tsx
index fa45036067..8f0e2c1ebb 100644
--- a/frontends/main/src/page-components/ResourceCard/ResourceCard.tsx
+++ b/frontends/main/src/page-components/ResourceCard/ResourceCard.tsx
@@ -82,6 +82,7 @@ type ResourceCardProps = Omit<
> & {
headingLevel?: number
parentHeadingEl?: HeadingElement
+ onCardClick?: () => void
}
/**
@@ -94,6 +95,7 @@ type ResourceCardProps = Omit<
const ResourceCard: React.FC = ({
resource,
parentHeadingEl,
+ onCardClick,
...others
}) => {
const {
@@ -107,11 +109,18 @@ const ResourceCard: React.FC = ({
onClick,
} = useResourceCard(resource)
+ const composedOnClick = onCardClick
+ ? () => {
+ onClick?.()
+ onCardClick()
+ }
+ : onClick
+
const headingLevel = parentHeadingEl ? subheadingMap[parentHeadingEl] : 6
return (
<>
({
+ ...jest.requireActual("posthog-js/react"),
+ usePostHog: jest.fn(),
+}))
+const mockCapture = jest.fn()
+jest.mocked(usePostHog).mockReturnValue(
+ // @ts-expect-error Not mocking all of posthog
+ { capture: mockCapture },
+)
jest.mock("ol-components", () => {
const actual = jest.requireActual("ol-components")
@@ -328,4 +340,44 @@ describe("ResourceCarousel", () => {
)
},
)
+
+ test("clicking a card fires course_card_clicked with key payload fields", async () => {
+ allowConsoleErrors()
+ const config: ResourceCarouselProps["config"] = [
+ {
+ label: "Test Carousel",
+ data: {
+ type: "resources",
+ params: { resource_type: ["course"] },
+ },
+ },
+ ]
+ const { resources } = setupApis({ count: 2 })
+ process.env.NEXT_PUBLIC_POSTHOG_API_KEY = "test-key"
+ mockCapture.mockClear()
+
+ renderWithProviders(
+ ,
+ )
+
+ const firstResource = resources.list.results[0]
+ const card = await screen.findByRole("heading", {
+ name: firstResource.title,
+ })
+ await user.click(card)
+
+ expect(mockCapture).toHaveBeenCalledWith(
+ PostHogEvents.CourseCardClicked,
+ expect.objectContaining({
+ label: "Test Carousel",
+ resourceId: firstResource.id,
+ readableId: firstResource.readable_id,
+ }),
+ )
+ delete process.env.NEXT_PUBLIC_POSTHOG_API_KEY
+ })
})
diff --git a/frontends/main/src/page-components/ResourceCarousel/ResourceCarousel.tsx b/frontends/main/src/page-components/ResourceCarousel/ResourceCarousel.tsx
index af8f3e9872..22e7d2c075 100644
--- a/frontends/main/src/page-components/ResourceCarousel/ResourceCarousel.tsx
+++ b/frontends/main/src/page-components/ResourceCarousel/ResourceCarousel.tsx
@@ -19,6 +19,8 @@ import {
UseQueryResult,
UseQueryOptions,
} from "@tanstack/react-query"
+import { usePostHog } from "posthog-js/react"
+import { PostHogEvents } from "@/common/constants"
const StyledCarouselV2 = styled(CarouselV2)({
margin: "24px 0",
@@ -224,6 +226,7 @@ const ResourceCarousel: React.FC = ({
titleVariant = "h4",
excludeResourceId,
}) => {
+ const posthog = usePostHog()
const [tab, setTab] = React.useState("0")
const [ref, setRef] = React.useState(null)
const queries = useQueries({
@@ -316,12 +319,24 @@ const ResourceCarousel: React.FC = ({
))
: resources
.filter((resource) => resource.id !== excludeResourceId)
- .map((resource) => (
+ .map((resource, index) => (
{
+ if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) {
+ posthog.capture(PostHogEvents.CourseCardClicked, {
+ label: title,
+ resourceId: resource.id,
+ readableId: resource.readable_id,
+ resourceType: resource.resource_type,
+ platformCode: resource.platform?.code,
+ position: index,
+ })
+ }
+ }}
/>
))}
diff --git a/frontends/main/src/page-components/SearchDisplay/UniversalAIBanner.tsx b/frontends/main/src/page-components/SearchDisplay/UniversalAIBanner.tsx
index 4bb34cab25..3972f078d3 100644
--- a/frontends/main/src/page-components/SearchDisplay/UniversalAIBanner.tsx
+++ b/frontends/main/src/page-components/SearchDisplay/UniversalAIBanner.tsx
@@ -1,9 +1,10 @@
import React, { useMemo } from "react"
import Link from "next/link"
import { styled, Typography } from "ol-components"
-import { useFeatureFlagEnabled } from "posthog-js/react"
+import { useFeatureFlagEnabled, usePostHog } from "posthog-js/react"
import { FeatureFlags } from "@/common/feature_flags"
import { programPageView } from "@/common/urls"
+import { PostHogEvents } from "@/common/constants"
const BANNER_SEARCH_TERMS = [
"artificial intelligence",
@@ -92,6 +93,7 @@ const UniversalAIBanner: React.FC = ({
const featureFlagEnabled = useFeatureFlagEnabled(
FeatureFlags.UniversalAISearchBanner,
)
+ const posthog = usePostHog()
const matchesSearchTerm = useMemo(() => {
if (!searchTerm) return true // If no search term, show the banner by default
@@ -102,14 +104,23 @@ const UniversalAIBanner: React.FC = ({
const showBanner = featureFlagEnabled && matchesSearchTerm
- if (!showBanner) return null
-
const UAI_PROGRAM_READABLE_ID = "program-v1:UAI+B2C"
const ctaHref = programPageView({
readable_id: UAI_PROGRAM_READABLE_ID,
display_mode: null,
})
+ const handleCTAClick = () => {
+ if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) {
+ posthog.capture(PostHogEvents.CallToActionClicked, {
+ label: "Learn more about Universal AI",
+ readableId: UAI_PROGRAM_READABLE_ID,
+ })
+ }
+ }
+
+ if (!showBanner) return null
+
return (
@@ -121,7 +132,11 @@ const UniversalAIBanner: React.FC = ({
required.
-
+
Learn More
diff --git a/frontends/ol-components/src/components/LinkAdapter/LinkAdapter.tsx b/frontends/ol-components/src/components/LinkAdapter/LinkAdapter.tsx
index 6e95f372cf..09736223ba 100644
--- a/frontends/ol-components/src/components/LinkAdapter/LinkAdapter.tsx
+++ b/frontends/ol-components/src/components/LinkAdapter/LinkAdapter.tsx
@@ -36,11 +36,10 @@ const LinkAdapter = ({ shallow, href = "", ...props }: LinkAdapterProps) => {
href={href}
{...props}
onClick={(e) => {
+ props.onClick?.(e)
if (shallow) {
e.preventDefault()
window.history.pushState({}, "", href)
- } else {
- props.onClick?.(e)
}
}}
/>