Skip to content

Commit

Permalink
chore(curriculum): add metrics to measure scrimba funnel (#11972)
Browse files Browse the repository at this point in the history
* remove scrimba discount fallback banner
* refactor placement view logic into useViewed hook
* simplify/clean up useViewed code
* add view pings for scrimba
* add click pings for curriculum landing scrimba banner and scrim click outs
  • Loading branch information
LeoMcA authored Oct 21, 2024
1 parent e1b7b5f commit 8d51b73
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 70 deletions.
11 changes: 10 additions & 1 deletion client/src/curriculum/landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import "./index.scss";
import "./landing.scss";
import { ProseSection } from "../../../libs/types/document";
import { PartnerBanner } from "./partner-banner";
import { useIsServer } from "../hooks";
import { useIsServer, useViewed } from "../hooks";
import scrimBg from "../assets/curriculum/landing-scrim.png";
import { useGleanClick } from "../telemetry/glean-context";
import { CURRICULUM } from "../telemetry/constants";

const ScrimInline = lazy(() => import("./scrim-inline"));

Expand Down Expand Up @@ -133,6 +135,12 @@ function About({ section }) {
const { title, content, id } = section.value;
const html = useMemo(() => ({ __html: content }), [content]);
const isServer = useIsServer();
const gleanClick = useGleanClick();
const observedNode = useViewed(() => {
const url = new URL(SCRIM_URL);
const id = url.pathname.slice(1);
gleanClick(`${CURRICULUM}: scrim view id:${id}`);
});

return (
<section key={id} className="landing-about-container">
Expand All @@ -148,6 +156,7 @@ function About({ section }) {
url={SCRIM_URL}
img={scrimBg}
scrimTitle="MDN + Scrimba partnership announcement scrim"
ref={observedNode}
/>
)}
</Suspense>
Expand Down
17 changes: 16 additions & 1 deletion client/src/curriculum/partner-banner.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import ThemedPicture from "../ui/atoms/themed-picture";
import { useGleanClick } from "../telemetry/glean-context";
import { useViewed } from "../hooks";
import { CURRICULUM } from "../telemetry/constants";

import bannerDark from "../../public/assets/curriculum/curriculum-partner-banner-illustration-large-dark.svg";
import bannerLight from "../../public/assets/curriculum/curriculum-partner-banner-illustration-large-light.svg";

import "./partner-banner.scss";

export function PartnerBanner() {
const gleanClick = useGleanClick();
const observedNode = useViewed(() => {
gleanClick(`${CURRICULUM}: partner banner view`);
});

return (
<section className="curriculum-partner-banner-container">
<section className="curriculum-partner-banner-container" ref={observedNode}>
<div className="partner-banner">
<section>
<h2>Learn the curriculum with Scrimba and become job ready</h2>
Expand All @@ -16,6 +25,9 @@ export function PartnerBanner() {
target="_blank"
rel="origin noreferrer"
className="external"
onClick={() => {
gleanClick(`${CURRICULUM}: partner banner click`);
}}
>
Scrimba's Frontend Developer Career Path
</a>{" "}
Expand All @@ -28,6 +40,9 @@ export function PartnerBanner() {
target="_blank"
rel="origin noreferrer"
className="external"
onClick={() => {
gleanClick(`${CURRICULUM}: partner banner click`);
}}
>
Find out more
</a>
Expand Down
1 change: 1 addition & 0 deletions client/src/curriculum/scrim-inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class ScrimInline extends LitElement {
target="_blank"
rel="origin noreferrer"
class="external"
data-glean="${CURRICULUM}: scrim link id:${this._scrimId}"
>
<span class="visually-hidden">Open on Scrimba</span>
</a>
Expand Down
51 changes: 50 additions & 1 deletion client/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import React, { useEffect, useMemo, useState } from "react";
import React, {
useCallback,
useEffect,
useRef,
useState,
useMemo,
} from "react";
import { useLocation, useNavigationType, useParams } from "react-router-dom";
import { DEFAULT_LOCALE } from "../../libs/constants";
import { isValidLocale } from "../../libs/locale-utils";
Expand Down Expand Up @@ -269,3 +275,46 @@ export const useScrollToAnchor = () => {
}
});
};

interface ViewedTimer {
timeout: number | null;
}

export function useViewed(callback: Function) {
const timer = useRef<ViewedTimer>({ timeout: null });
const isVisible = usePageVisibility();
const [node, setNode] = useState<HTMLElement>();
const isIntersecting = useIsIntersecting(node, {
root: null,
rootMargin: "0px",
threshold: 0.5,
});

useEffect(() => {
if (timer.current.timeout !== -1) {
// timeout !== -1 means the viewed has not been sent
if (isVisible && isIntersecting) {
if (timer.current.timeout === null) {
timer.current = {
timeout: window.setTimeout(() => {
timer.current = { timeout: -1 };
callback();
}, 1000),
};
}
}
}
return () => {
if (timer.current.timeout !== null && timer.current.timeout !== -1) {
clearTimeout(timer.current.timeout);
timer.current = { timeout: null };
}
};
}, [isVisible, isIntersecting, callback]);

return useCallback((node: HTMLElement | null) => {
if (node) {
setNode(node);
}
}, []);
}
1 change: 1 addition & 0 deletions client/src/telemetry/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const BANNER_BLOG_LAUNCH_CLICK = "banner_blog_launch_click";
export const AI_HELP = "ai_help";
export const BANNER_AI_HELP_CLICK = "banner_ai_help_click";
export const BANNER_SCRIMBA_CLICK = "banner_scrimba_click";
export const BANNER_SCRIMBA_VIEW = "banner_scrimba_view";
export const PLAYGROUND = "play_action";
export const AI_EXPLAIN = "ai_explain";
export const SETTINGS = "settings";
Expand Down
79 changes: 12 additions & 67 deletions client/src/ui/organisms/placement/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
import { useCallback, useEffect, useRef, useState } from "react";
import {
useIsIntersecting,
useIsServer,
usePageVisibility,
} from "../../../hooks";
import { useIsServer, useViewed } from "../../../hooks";
import { User, useUserData } from "../../../user-context";

import "./index.scss";
import { useGleanClick } from "../../../telemetry/glean-context";
import { Status, usePlacement } from "../../../placement-context";
import { Payload as PlacementData } from "../../../../../libs/pong/types";
import { BANNER_SCRIMBA_CLICK } from "../../../telemetry/constants";

interface Timer {
timeout: number | null;
}
import {
BANNER_SCRIMBA_CLICK,
BANNER_SCRIMBA_VIEW,
} from "../../../telemetry/constants";

interface PlacementRenderArgs {
place: any;
Expand All @@ -33,12 +27,6 @@ interface PlacementRenderArgs {
heading?: string;
}

const INTERSECTION_OPTIONS = {
root: null,
rootMargin: "0px",
threshold: 0.5,
};

function viewed(pong?: PlacementData) {
pong?.view &&
navigator.sendBeacon?.(
Expand Down Expand Up @@ -91,29 +79,18 @@ export function SidePlacement() {

function TopPlacementFallbackContent() {
const gleanClick = useGleanClick();
const observedNode = useViewed(() => {
gleanClick(BANNER_SCRIMBA_VIEW);
});

return Date.now() < Date.parse("2024-10-12") ? (
<p className="fallback-copy">
Learn front-end development with a 30% discount on{" "}
<a
href="https://scrimba.com/learn/frontend?via=mdn"
target="_blank"
rel="noreferrer"
onClick={() => {
gleanClick(BANNER_SCRIMBA_CLICK);
}}
>
Scrimba
</a>{" "}
&mdash; limited time offer!
</p>
) : (
return (
<p className="fallback-copy">
Learn front-end development with high quality, interactive courses from{" "}
<a
href="https://scrimba.com/learn/frontend?via=mdn"
target="_blank"
rel="noreferrer"
ref={observedNode}
onClick={() => {
gleanClick(BANNER_SCRIMBA_CLICK);
}}
Expand Down Expand Up @@ -277,44 +254,12 @@ export function PlacementInner({
}) {
const isServer = useIsServer();
const user = useUserData();
const isVisible = usePageVisibility();
const gleanClick = useGleanClick();

const timer = useRef<Timer>({ timeout: null });

const [node, setNode] = useState<HTMLElement>();
const isIntersecting = useIsIntersecting(node, INTERSECTION_OPTIONS);

const sendViewed = useCallback(() => {
const place = useViewed(() => {
viewed(pong);
gleanClick(`pong: pong->viewed ${typ}`);
timer.current = { timeout: -1 };
}, [pong, gleanClick, typ]);

const place = useCallback((node: HTMLElement | null) => {
if (node) {
setNode(node);
}
}, []);

useEffect(() => {
if (timer.current.timeout !== -1) {
// timeout !== -1 means the viewed has not been sent
if (isVisible && isIntersecting) {
if (timer.current.timeout === null) {
timer.current = {
timeout: window.setTimeout(sendViewed, 1000),
};
}
}
}
return () => {
if (timer.current.timeout !== null && timer.current.timeout !== -1) {
clearTimeout(timer.current.timeout);
timer.current = { timeout: null };
}
};
}, [isVisible, isIntersecting, sendViewed]);
});

const { image, copy, alt, click, version, heading } = pong || {};
return (
Expand Down

0 comments on commit 8d51b73

Please sign in to comment.