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
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const CourseEnrollArea: React.FC<CourseEnrollAreaProps> = ({

const { state, scenario, isStatusLoading, isPending, isError } =
useCourseEnrollment(course, selectedRun, {
tracking: { placement: "infobox" },
onRequireSignup: (el) => setAnchor(el),
})

Expand Down
5 changes: 4 additions & 1 deletion frontends/main/src/app-pages/ProductPages/CoursePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ const CourseHeaderEnrollButton: React.FC<{
const { state, isStatusLoading, isPending } = useCourseEnrollment(
course,
selectedRun,
{ onRequireSignup: (el) => setAnchor(el) },
{
tracking: { placement: "header" },
onRequireSignup: (el) => setAnchor(el),
},
)

if (state.status === "enrolled") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,11 @@ describe("useCourseEnrollment — actions", () => {
const onRequireSignup = jest.fn()

const { result } = renderHook(
() => useCourseEnrollment(course, run, { onRequireSignup }),
() =>
useCourseEnrollment(course, run, {
tracking: { placement: "infobox" },
onRequireSignup,
}),
{ wrapper },
)

Expand All @@ -533,7 +537,7 @@ describe("useCourseEnrollment — actions", () => {
expect(onRequireSignup).toHaveBeenCalledWith(anchorButton)
})

test("fires PostHog cta_clicked on paid action click", async () => {
test("fires PostHog enroll_cta_clicked on paid action click", async () => {
const product = makeProduct()
const run = makeRun({
is_enrollable: true,
Expand All @@ -550,9 +554,13 @@ describe("useCourseEnrollment — actions", () => {
items: [],
})

const { result } = renderHook(() => useCourseEnrollment(course, run), {
wrapper,
})
const { result } = renderHook(
() =>
useCourseEnrollment(course, run, {
tracking: { placement: "infobox" },
}),
{ wrapper },
)

await waitFor(() => expect(result.current.isStatusLoading).toBe(false))

Expand All @@ -567,10 +575,54 @@ describe("useCourseEnrollment — actions", () => {
paidOption.onClick!(fakeEvent)

expect(mockCapture).toHaveBeenCalledWith(
PostHogEvents.CallToActionClicked,
PostHogEvents.EnrollCtaClicked,
expect.objectContaining({
placement: "infobox",
enrollmentMode: "verified",
resourceType: "course",
readableId: course.readable_id,
}),
)
})

test("fires PostHog enroll_cta_clicked with audit mode on free action click", async () => {
// Unauthenticated: the event fires before the auth gate, then we return to
// the signup flow — so the fire is observable without an enrollment mock.
setMockResponse.get(
urls.userMe.get(),
makeUser({ is_authenticated: false }),
)
const run = makeRun({
is_enrollable: true,
is_upgradable: false,
is_archived: false,
enrollment_modes: [makeMode({ requires_payment: false })],
})
const course = makeCourse({ next_run_id: run.id, courseruns: [run] })

const { result } = renderHook(
() =>
useCourseEnrollment(course, run, { tracking: { placement: "header" } }),
{ wrapper },
)

await waitFor(() => expect(result.current.isStatusLoading).toBe(false))

const state = result.current.state
if (state.status !== "options") return
const freeOption = state.options.find((o) => o.kind === "free")!

freeOption.onClick!({
currentTarget: document.createElement("button"),
} as React.MouseEvent<HTMLButtonElement>)

expect(mockCapture).toHaveBeenCalledWith(
PostHogEvents.EnrollCtaClicked,
expect.objectContaining({
placement: "header",
enrollmentMode: "audit",
resourceType: "course",
readableId: course.readable_id,
}),
)
})
Expand Down
25 changes: 20 additions & 5 deletions frontends/main/src/app-pages/ProductPages/useCourseEnrollment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
CourseRunV2,
CourseWithCourseRunsSerializerV2,
} from "@mitodl/mitxonline-api-axios/v2"
import { PlatformEnum } from "api"
import { userQueries } from "api/hooks/user"
import { useCreateEnrollment } from "api/mitxonline-hooks/enrollment"
import { useReplaceBasketItem } from "@/common/mitxonline/useReplaceBasketItem"
Expand Down Expand Up @@ -42,10 +43,17 @@ export type UseCourseEnrollment = {
isError: boolean
}

type UseCourseEnrollmentOptions = {
/** Analytics-only metadata for the `enroll_cta_clicked` event; no behavior. */
tracking: { placement: "header" | "infobox" }
/** Behavioral: called when an unauthenticated user clicks an enroll action. */
onRequireSignup?: (anchor: HTMLButtonElement) => void
}

export const useCourseEnrollment = (
course: CourseWithCourseRunsSerializerV2,
selectedRun: CourseRunV2 | undefined,
opts?: { onRequireSignup?: (anchor: HTMLButtonElement) => void },
opts?: UseCourseEnrollmentOptions,
): UseCourseEnrollment => {
const me = useQuery({
...userQueries.me(),
Expand Down Expand Up @@ -77,11 +85,18 @@ export const useCourseEnrollment = (
// a misleading "problem processing your enrollment" message.
const isError = replaceBasketItem.isError || createEnrollment.isError

const firePostHog = (label: string) => {
// Dedicated, semantically-named enrollment event (hq#11941). Replaces the
// generic cta_clicked for enroll CTAs so analytics can slice by placement /
// enrollment_mode instead of the drifting button copy. `label` is retained
// for human readability, not as the analytics key.
const firePostHog = (kind: EnrollActionKind, label: string) => {
if (env("NEXT_PUBLIC_POSTHOG_API_KEY")) {
posthog.capture(PostHogEvents.CallToActionClicked, {
readableId: course.readable_id,
posthog.capture(PostHogEvents.EnrollCtaClicked, {
placement: opts?.tracking.placement,
enrollmentMode: kind === "paid" ? "verified" : "audit",
resourceType: "course",
readableId: course.readable_id,
platform: PlatformEnum.Mitxonline,
label,
})
}
Expand All @@ -90,7 +105,7 @@ export const useCourseEnrollment = (
const makeOnClick =
(kind: EnrollActionKind, label: string): EnrollAction["onClick"] =>
(e) => {
firePostHog(label)
firePostHog(kind, label)
if (!me.data?.is_authenticated) {
opts?.onRequireSignup?.(e.currentTarget)
return
Expand Down
1 change: 1 addition & 0 deletions frontends/main/src/common/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const PostHogEvents = {
SearchUpdate: "search_update",
CallToActionClicked: "cta_clicked",
EnrollCtaClicked: "enroll_cta_clicked",
CourseCardClicked: "course_card_clicked",
AskTimClicked: "asktim_clicked",
LearningResourceDrawerOpen: "lrd_open",
Expand Down
Loading