Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion frontends/main/src/app-pages/HomePage/HomePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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(<HomePage heroImageIndex={1} />)
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", () => {
Expand Down
23 changes: 19 additions & 4 deletions frontends/main/src/app-pages/HomePage/UAIAnnouncementCard.tsx
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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 (
<Card>
<CardHeader>
Expand All @@ -275,6 +288,7 @@ const UAIAnnouncementCard: React.FC = () => {
variant="primary"
href={ctaHref}
endIcon={<RiArrowRightLine />}
onClick={handleCTAClick}
>
Learn about Universal AI
</CTAButton>
Expand All @@ -294,6 +308,7 @@ const UAIAnnouncementCard: React.FC = () => {
variant="primary"
href={ctaHref}
endIcon={<RiArrowRightLine />}
onClick={handleCTAClick}
>
Learn about Universal AI
</CTAButton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(<CourseEnrollmentButton course={course} />)
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(<CourseEnrollmentButton course={course} />)
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",
}),
)
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -60,6 +62,7 @@ const CourseEnrollmentButton: React.FC<CourseEnrollmentButtonProps> = ({
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)
Expand Down Expand Up @@ -90,7 +93,16 @@ const CourseEnrollmentButton: React.FC<CourseEnrollmentButtonProps> = ({
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (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") {
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -17,6 +19,7 @@ type CommonCardProps = {
headingLevel?: number
className?: string
list?: boolean
label?: string
}

type MitxOnlineCourseCardProps = CommonCardProps & {
Expand Down Expand Up @@ -156,13 +159,15 @@ const extractCardData = (
const MitxOnlineResourceCard: React.FC<MitxOnlineResourceCardProps> = (
props,
) => {
const posthog = usePostHog()
const {
href,
size = "small",
isLoading,
headingLevel = 6,
className,
list,
label,
} = props

if (isLoading) {
Expand Down Expand Up @@ -198,6 +203,16 @@ const MitxOnlineResourceCard: React.FC<MitxOnlineResourceCardProps> = (
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,
})
}
}}
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
<ProgramEnrollmentButton program={program} variant="primary" />,
)
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(
<ProgramEnrollmentButton program={program} variant="primary" />,
)
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",
}),
)
})
})
})
Loading
Loading