From 2529dd92da94babbec34debffd9fee084af35fd6 Mon Sep 17 00:00:00 2001 From: rlougee Date: Thu, 16 Apr 2026 09:04:02 -0400 Subject: [PATCH 01/11] feat: implement PostHog event tracking for user interactions --- .../ProductPages/CourseEnrollmentButton.tsx | 9 ++++++ .../ProductPages/MitxOnlineResourceCard.tsx | 13 +++++++++ .../ProductPages/ProgramEnrollmentButton.tsx | 10 ++++++- .../app-pages/ProductPages/ProgramPage.tsx | 2 ++ frontends/main/src/common/constants.ts | 2 ++ .../AiChat/AskTimDrawerButton.tsx | 16 +++++++++- .../AiChatSyllabusSlideDown.tsx | 15 +++++++++- .../CallToActionSection.tsx | 5 +++- .../ResourceCarousel/ResourceCarousel.tsx | 29 +++++++++++++++---- .../components/LinkAdapter/LinkAdapter.tsx | 3 +- 10 files changed, 92 insertions(+), 12 deletions(-) diff --git a/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx b/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx index 2652f21bf1..5e889142da 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) @@ -91,6 +94,12 @@ const CourseEnrollmentButton: React.FC = ({ if (me.isLoading) { return } else if (me.data?.is_authenticated) { + if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) { + posthog.capture(PostHogEvents.CallToActionClicked, { + course, + label: course.title, + }) + } 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..edbb3b84b6 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,14 @@ const MitxOnlineResourceCard: React.FC = ( startDate={data.startDate} ariaLabel={`${data.displayType}: ${data.title}`} list={list} + onClick={() => + 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.tsx b/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx index 72cd7f674d..d0dbb1fb4a 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,18 @@ 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, { + program, + label: program.title, + }) + } 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.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/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.tsx b/frontends/main/src/page-components/AiChat/AskTimDrawerButton.tsx index 015f202225..8f40e67b8c 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,19 @@ const StyledButton = styled(LinkAdapter)(({ theme }) => ({ })) const AskTIMButton = () => { + const posthog = usePostHog() + return ( <> - + + posthog.capture(PostHogEvents.AskTimClicked, { + type: "recommendation_bot", + }) + } + > AskTIM diff --git a/frontends/main/src/page-components/LearningResourceExpanded/AiChatSyllabusSlideDown.tsx b/frontends/main/src/page-components/LearningResourceExpanded/AiChatSyllabusSlideDown.tsx index 35464766bf..90e3e27786 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={() => { + 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.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/ResourceCarousel/ResourceCarousel.tsx b/frontends/main/src/page-components/ResourceCarousel/ResourceCarousel.tsx index af8f3e9872..80dea7f7b4 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,13 +319,27 @@ const ResourceCarousel: React.FC = ({ )) : resources .filter((resource) => resource.id !== excludeResourceId) - .map((resource) => ( - ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions +
+ onClick={() => + 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/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) } }} /> From f026374acb2e1a045e7b1b4d3aa242a633430dea Mon Sep 17 00:00:00 2001 From: rlougee Date: Thu, 16 Apr 2026 09:27:59 -0400 Subject: [PATCH 02/11] fix code style issues --- .../src/app-pages/ProductPages/CourseEnrollmentButton.tsx | 6 +++--- .../src/app-pages/ProductPages/ProgramEnrollmentButton.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx b/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx index 5e889142da..e0b6b25e17 100644 --- a/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx +++ b/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx @@ -96,9 +96,9 @@ const CourseEnrollmentButton: React.FC = ({ } else if (me.data?.is_authenticated) { if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) { posthog.capture(PostHogEvents.CallToActionClicked, { - course, - label: course.title, - }) + course, + label: course.title, + }) } if (enrollmentDecision.type === "dialog") { NiceModal.show(CourseEnrollmentDialog, { course }) diff --git a/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx b/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx index d0dbb1fb4a..22a2cc5a17 100644 --- a/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx +++ b/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx @@ -90,9 +90,9 @@ const ProgramEnrollmentButton: React.FC = ({ } else if (me.data?.is_authenticated) { if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) { posthog.capture(PostHogEvents.CallToActionClicked, { - program, - label: program.title, - }) + program, + label: program.title, + }) } if (enrollmentType === "paid" && program.products[0]) { replaceBasketItem.mutate(program.products[0].id) From cd444b9ebce82ac905ce10ec39f27d89b254fd93 Mon Sep 17 00:00:00 2001 From: rlougee Date: Thu, 16 Apr 2026 09:44:54 -0400 Subject: [PATCH 03/11] test: add label to event tracking for CallToActionSection --- .../LearningResourceExpanded/CallToActionSection.test.tsx | 1 + 1 file changed, 1 insertion(+) 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 From 73c3ec1cf8280080083a1ea8b63c45d8fdc20de8 Mon Sep 17 00:00:00 2001 From: rlougee Date: Thu, 16 Apr 2026 10:44:02 -0400 Subject: [PATCH 04/11] feedback --- .../ProductPages/MitxOnlineResourceCard.tsx | 18 +++++----- .../AiChat/AskTimDrawerButton.tsx | 12 ++++--- .../AiChatSyllabusSlideDown.tsx | 16 +++++---- .../ResourceCard/ResourceCard.tsx | 11 +++++- .../ResourceCarousel/ResourceCarousel.tsx | 36 +++++++++---------- 5 files changed, 53 insertions(+), 40 deletions(-) diff --git a/frontends/main/src/app-pages/ProductPages/MitxOnlineResourceCard.tsx b/frontends/main/src/app-pages/ProductPages/MitxOnlineResourceCard.tsx index edbb3b84b6..5564aaa800 100644 --- a/frontends/main/src/app-pages/ProductPages/MitxOnlineResourceCard.tsx +++ b/frontends/main/src/app-pages/ProductPages/MitxOnlineResourceCard.tsx @@ -203,14 +203,16 @@ const MitxOnlineResourceCard: React.FC = ( startDate={data.startDate} ariaLabel={`${data.displayType}: ${data.title}`} list={list} - onClick={() => - posthog.capture(PostHogEvents.CourseCardClicked, { - label, - resourceId: props.resource?.id, - readableId: props.resource?.readable_id, - resourceType: props.resourceType, - }) - } + 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/page-components/AiChat/AskTimDrawerButton.tsx b/frontends/main/src/page-components/AiChat/AskTimDrawerButton.tsx index 8f40e67b8c..bcb980d82a 100644 --- a/frontends/main/src/page-components/AiChat/AskTimDrawerButton.tsx +++ b/frontends/main/src/page-components/AiChat/AskTimDrawerButton.tsx @@ -32,11 +32,13 @@ const AskTIMButton = () => { - posthog.capture(PostHogEvents.AskTimClicked, { - type: "recommendation_bot", - }) - } + onClick={() => { + if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) { + posthog.capture(PostHogEvents.AskTimClicked, { + type: "recommendation_bot", + }) + } + }} > diff --git a/frontends/main/src/page-components/LearningResourceExpanded/AiChatSyllabusSlideDown.tsx b/frontends/main/src/page-components/LearningResourceExpanded/AiChatSyllabusSlideDown.tsx index 90e3e27786..fa70037fbe 100644 --- a/frontends/main/src/page-components/LearningResourceExpanded/AiChatSyllabusSlideDown.tsx +++ b/frontends/main/src/page-components/LearningResourceExpanded/AiChatSyllabusSlideDown.tsx @@ -152,13 +152,15 @@ export const AiChatSyllabusOpener = ({ aria-pressed={open} open={open} onClick={() => { - posthog.capture(PostHogEvents.AskTimClicked, { - type: "syllabus_bot", - resourceId: resource.id, - readableId: resource.readable_id, - resourceType: resource.resource_type, - platformCode: resource.platform?.code, - }) + 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/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 ( <> = ({ : resources .filter((resource) => resource.id !== excludeResourceId) .map((resource, index) => ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -
- posthog.capture(PostHogEvents.CourseCardClicked, { - label: title, - resourceId: resource.id, - readableId: resource.readable_id, - resourceType: resource.resource_type, - platformCode: resource.platform?.code, - position: index, - }) - } - > - -
+ resource={resource} + parentHeadingEl={titleComponent} + {...tabConfig.cardProps} + onCardClick={() => { + 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, + }) + } + }} + /> ))} ) From 8cb4d6b62008ccbfbacc8af7fb83b36ed6fc97dd Mon Sep 17 00:00:00 2001 From: rlougee Date: Thu, 16 Apr 2026 11:26:14 -0400 Subject: [PATCH 05/11] feat: enhance PostHog event tracking for course and program enrollment buttons --- .../src/app-pages/ProductPages/CourseEnrollmentButton.tsx | 6 ++++-- .../src/app-pages/ProductPages/ProgramEnrollmentButton.tsx | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx b/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx index e0b6b25e17..d814110085 100644 --- a/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx +++ b/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx @@ -96,8 +96,10 @@ const CourseEnrollmentButton: React.FC = ({ } else if (me.data?.is_authenticated) { if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) { posthog.capture(PostHogEvents.CallToActionClicked, { - course, - label: course.title, + resourceId: course.id, + readableId: course.readable_id, + resourceType: "course", + label: getButtonText(nextRun, price?.displayPrice), }) } if (enrollmentDecision.type === "dialog") { diff --git a/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx b/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx index 22a2cc5a17..1c11480df8 100644 --- a/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx +++ b/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx @@ -90,8 +90,10 @@ const ProgramEnrollmentButton: React.FC = ({ } else if (me.data?.is_authenticated) { if (process.env.NEXT_PUBLIC_POSTHOG_API_KEY) { posthog.capture(PostHogEvents.CallToActionClicked, { - program, - label: program.title, + resourceId: program.id, + readableId: program.readable_id, + resourceType: "program", + label: getEnrollButtonText(), }) } if (enrollmentType === "paid" && program.products[0]) { From a92d723c5c0930bc44e1c415f6d73f5f4b152aaf Mon Sep 17 00:00:00 2001 From: rlougee Date: Thu, 16 Apr 2026 11:36:27 -0400 Subject: [PATCH 06/11] fix test --- .../main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx b/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx index d814110085..9df7690539 100644 --- a/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx +++ b/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx @@ -99,7 +99,7 @@ const CourseEnrollmentButton: React.FC = ({ resourceId: course.id, readableId: course.readable_id, resourceType: "course", - label: getButtonText(nextRun, price?.displayPrice), + label: getButtonText(nextRun, price?.finalPrice), }) } if (enrollmentDecision.type === "dialog") { From 46cb32065ebe30def6acb3704f3fd25b8df17bf7 Mon Sep 17 00:00:00 2001 From: rlougee Date: Thu, 16 Apr 2026 14:52:12 -0400 Subject: [PATCH 07/11] feat: add PostHog event tracking for CTA clicks in UAIAnnouncementCard and UniversalAIBanner --- .../HomePage/UAIAnnouncementCard.tsx | 24 +++++++++++++++---- .../SearchDisplay/UniversalAIBanner.tsx | 23 ++++++++++++++---- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/frontends/main/src/app-pages/HomePage/UAIAnnouncementCard.tsx b/frontends/main/src/app-pages/HomePage/UAIAnnouncementCard.tsx index dde506e649..583c55dd3d 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,27 @@ 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, + resourceType: "program", + }) + } + } + + if (!showUAICard) { + return null + } + return ( @@ -275,6 +289,7 @@ const UAIAnnouncementCard: React.FC = () => { variant="primary" href={ctaHref} endIcon={} + onClick={handleCTAClick} > Learn about Universal AI @@ -294,6 +309,7 @@ const UAIAnnouncementCard: React.FC = () => { variant="primary" href={ctaHref} endIcon={} + onClick={handleCTAClick} > Learn about Universal AI 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 From 15175a6bd350c2ff40570b40b12cceeeb1540319 Mon Sep 17 00:00:00 2001 From: rlougee Date: Thu, 16 Apr 2026 15:00:42 -0400 Subject: [PATCH 08/11] feat: remove resourceType from PostHog event tracking in UAIAnnouncementCard --- frontends/main/src/app-pages/HomePage/UAIAnnouncementCard.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontends/main/src/app-pages/HomePage/UAIAnnouncementCard.tsx b/frontends/main/src/app-pages/HomePage/UAIAnnouncementCard.tsx index 583c55dd3d..d95cc93d78 100644 --- a/frontends/main/src/app-pages/HomePage/UAIAnnouncementCard.tsx +++ b/frontends/main/src/app-pages/HomePage/UAIAnnouncementCard.tsx @@ -257,7 +257,6 @@ const UAIAnnouncementCard: React.FC = () => { posthog.capture(PostHogEvents.CallToActionClicked, { label: "Learn about Universal AI", readableId: UAI_PROGRAM_READABLE_ID, - resourceType: "program", }) } } From f587709e42ad557439d92eb8d9e6af03c2a9e912 Mon Sep 17 00:00:00 2001 From: rlougee Date: Thu, 16 Apr 2026 16:10:57 -0400 Subject: [PATCH 09/11] feedback --- .../src/app-pages/HomePage/HomePage.test.tsx | 22 ++++++ .../CourseEnrollmentButton.test.tsx | 76 +++++++++++++++++++ .../ProductPages/CourseEnrollmentButton.tsx | 19 ++--- .../ProgramEnrollmentButton.test.tsx | 74 +++++++++++++++++- .../ProductPages/ProgramEnrollmentButton.tsx | 19 ++--- .../ProductPages/ProgramPage.test.tsx | 41 +++++++++- .../app-pages/SearchPage/SearchPage.test.tsx | 30 +++++++- .../AiChat/AskTimDrawerButton.test.tsx | 23 ++++++ .../AiChatSyllabusSlideDown.test.tsx | 40 ++++++++++ .../ResourceCarousel.test.tsx | 51 +++++++++++++ 10 files changed, 372 insertions(+), 23 deletions(-) diff --git a/frontends/main/src/app-pages/HomePage/HomePage.test.tsx b/frontends/main/src/app-pages/HomePage/HomePage.test.tsx index 6778a7d094..f5c2f2052d 100644 --- a/frontends/main/src/app-pages/HomePage/HomePage.test.tsx +++ b/frontends/main/src/app-pages/HomePage/HomePage.test.tsx @@ -19,6 +19,7 @@ import * as routes from "@/common/urls" import { FeatureFlags } from "@/common/feature_flags" import { assertHeadings } 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,27 @@ describe("UAI Announcement Card", () => { }) assertLinksTo(cta, { pathname: "/programs/program-v1:UAI+B2C" }) }) + + test("CTA click fires cta_clicked with label and readableId", async () => { + 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/ProductPages/CourseEnrollmentButton.test.tsx b/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.test.tsx index b1d76e9dd9..f1badb1550 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,68 @@ 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 }), + ) + + 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 9df7690539..8fde426a37 100644 --- a/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx +++ b/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.tsx @@ -93,15 +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 (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/ProgramEnrollmentButton.test.tsx b/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.test.tsx index 118619bc68..4002813bca 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,70 @@ 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 }), + ) + + 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 1c11480df8..3722ba6361 100644 --- a/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx +++ b/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.tsx @@ -87,15 +87,16 @@ const ProgramEnrollmentButton: React.FC = ({ 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 (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..d48d09aa66 100644 --- a/frontends/main/src/app-pages/ProductPages/ProgramPage.test.tsx +++ b/frontends/main/src/app-pages/ProductPages/ProgramPage.test.tsx @@ -16,7 +16,13 @@ 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 { notFound } from "next/navigation" @@ -25,14 +31,20 @@ import { 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,31 @@ describe("ProgramPage", () => { }) }) + test("clicking a requirement card fires course_card_clicked", async () => { + 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/SearchPage/SearchPage.test.tsx b/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx index 028bde95ed..e6f84f573c 100644 --- a/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx +++ b/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx @@ -16,15 +16,21 @@ import invariant from "tiny-invariant" import { Permission } from "api/hooks/user" import { assertHeadings, ControlledPromise } 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 +1079,24 @@ describe("UniversalAIBanner", () => { ]) }) }) + + test("clicking Learn More fires cta_clicked with label and readableId", async () => { + 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", + readableId: "program-v1:UAI+B2C", + }), + ) + delete process.env.NEXT_PUBLIC_POSTHOG_API_KEY + }) }) diff --git a/frontends/main/src/page-components/AiChat/AskTimDrawerButton.test.tsx b/frontends/main/src/page-components/AiChat/AskTimDrawerButton.test.tsx index 106d45493f..5fec0c6801 100644 --- a/frontends/main/src/page-components/AiChat/AskTimDrawerButton.test.tsx +++ b/frontends/main/src/page-components/AiChat/AskTimDrawerButton.test.tsx @@ -2,6 +2,18 @@ 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" + +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 +51,15 @@ describe("AskTIMButton", () => { ) }) }) + + test("clicking Ask TIM fires asktim_clicked with type recommendation_bot", async () => { + 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/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/ResourceCarousel/ResourceCarousel.test.tsx b/frontends/main/src/page-components/ResourceCarousel/ResourceCarousel.test.tsx index cd976834b3..05949bfa88 100644 --- a/frontends/main/src/page-components/ResourceCarousel/ResourceCarousel.test.tsx +++ b/frontends/main/src/page-components/ResourceCarousel/ResourceCarousel.test.tsx @@ -13,6 +13,18 @@ import { factories, setMockResponse, makeRequest, urls } from "api/test-utils" import { LearningResourceCard } from "ol-components" import { ControlledPromise } from "ol-test-utilities" 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 }, +) jest.mock("ol-components", () => { const actual = jest.requireActual("ol-components") @@ -328,4 +340,43 @@ describe("ResourceCarousel", () => { ) }, ) + + test("clicking a card fires course_card_clicked with key payload fields", async () => { + 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 + }) }) From 9922d8175717a7b794c66b25b3ba71de9f20d53e Mon Sep 17 00:00:00 2001 From: rlougee Date: Thu, 16 Apr 2026 16:34:43 -0400 Subject: [PATCH 10/11] fix: add allowConsoleErrors to posthog tracking tests and fix label assertion Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- frontends/main/src/app-pages/HomePage/HomePage.test.tsx | 3 ++- .../main/src/app-pages/ProductPages/ProgramPage.test.tsx | 3 ++- .../main/src/app-pages/SearchPage/SearchPage.test.tsx | 9 +++++++-- .../page-components/AiChat/AskTimDrawerButton.test.tsx | 2 ++ .../ResourceCarousel/ResourceCarousel.test.tsx | 3 ++- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/frontends/main/src/app-pages/HomePage/HomePage.test.tsx b/frontends/main/src/app-pages/HomePage/HomePage.test.tsx index f5c2f2052d..b91ce75662 100644 --- a/frontends/main/src/app-pages/HomePage/HomePage.test.tsx +++ b/frontends/main/src/app-pages/HomePage/HomePage.test.tsx @@ -17,7 +17,7 @@ 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" @@ -131,6 +131,7 @@ describe("UAI Announcement Card", () => { }) test("CTA click fires cta_clicked with label and readableId", async () => { + allowConsoleErrors() setupAPIs() mockedUseFeatureFlagEnabled.mockImplementation( (flag) => flag === FeatureFlags.UniversalAI, diff --git a/frontends/main/src/app-pages/ProductPages/ProgramPage.test.tsx b/frontends/main/src/app-pages/ProductPages/ProgramPage.test.tsx index d48d09aa66..c05a23a0c5 100644 --- a/frontends/main/src/app-pages/ProductPages/ProgramPage.test.tsx +++ b/frontends/main/src/app-pages/ProductPages/ProgramPage.test.tsx @@ -24,7 +24,7 @@ import { 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, @@ -653,6 +653,7 @@ describe("ProgramPage", () => { }) test("clicking a requirement card fires course_card_clicked", async () => { + allowConsoleErrors() const program = makeProgram({ ...makeReqs({ required: { count: 1, title: "Required" } }), }) diff --git a/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx b/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx index e6f84f573c..dd7a8dfd17 100644 --- a/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx +++ b/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx @@ -14,7 +14,11 @@ 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, usePostHog } from "posthog-js/react" import { PostHogEvents } from "@/common/constants" @@ -1081,6 +1085,7 @@ 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" @@ -1093,7 +1098,7 @@ describe("UniversalAIBanner", () => { expect(mockCapture).toHaveBeenCalledWith( PostHogEvents.CallToActionClicked, expect.objectContaining({ - label: "Learn More", + label: "Learn more about Universal AI", readableId: "program-v1:UAI+B2C", }), ) diff --git a/frontends/main/src/page-components/AiChat/AskTimDrawerButton.test.tsx b/frontends/main/src/page-components/AiChat/AskTimDrawerButton.test.tsx index 5fec0c6801..5751d22709 100644 --- a/frontends/main/src/page-components/AiChat/AskTimDrawerButton.test.tsx +++ b/frontends/main/src/page-components/AiChat/AskTimDrawerButton.test.tsx @@ -4,6 +4,7 @@ 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"), @@ -53,6 +54,7 @@ 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() diff --git a/frontends/main/src/page-components/ResourceCarousel/ResourceCarousel.test.tsx b/frontends/main/src/page-components/ResourceCarousel/ResourceCarousel.test.tsx index 05949bfa88..470594010d 100644 --- a/frontends/main/src/page-components/ResourceCarousel/ResourceCarousel.test.tsx +++ b/frontends/main/src/page-components/ResourceCarousel/ResourceCarousel.test.tsx @@ -11,7 +11,7 @@ import { } from "@/test-utils" import { factories, setMockResponse, makeRequest, urls } from "api/test-utils" import { LearningResourceCard } from "ol-components" -import { ControlledPromise } from "ol-test-utilities" +import { ControlledPromise, allowConsoleErrors } from "ol-test-utilities" import invariant from "tiny-invariant" import { usePostHog } from "posthog-js/react" import { PostHogEvents } from "@/common/constants" @@ -342,6 +342,7 @@ describe("ResourceCarousel", () => { ) test("clicking a card fires course_card_clicked with key payload fields", async () => { + allowConsoleErrors() const config: ResourceCarouselProps["config"] = [ { label: "Test Carousel", From 07311246e29d367100f15fa8c2c12f6f04fb4fb3 Mon Sep 17 00:00:00 2001 From: rlougee Date: Thu, 16 Apr 2026 16:49:25 -0400 Subject: [PATCH 11/11] fix: mock enrollment POST in PostHog tracking tests to prevent console.error Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../app-pages/ProductPages/CourseEnrollmentButton.test.tsx | 2 ++ .../app-pages/ProductPages/ProgramEnrollmentButton.test.tsx | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.test.tsx b/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.test.tsx index f1badb1550..7832598f0d 100644 --- a/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.test.tsx +++ b/frontends/main/src/app-pages/ProductPages/CourseEnrollmentButton.test.tsx @@ -538,6 +538,8 @@ describe("CourseEnrollmentButton", () => { 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( diff --git a/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.test.tsx b/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.test.tsx index 4002813bca..e8ac36ce99 100644 --- a/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.test.tsx +++ b/frontends/main/src/app-pages/ProductPages/ProgramEnrollmentButton.test.tsx @@ -424,6 +424,11 @@ describe("ProgramEnrollmentButton", () => { urls.userMe.get(), makeUser({ is_authenticated: true }), ) + setMockResponse.post( + mitxUrls.programEnrollments.enrollmentsListV3(), + null, + { code: 201 }, + ) renderWithProviders( ,