Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(curriculum): add metrics to measure scrimba funnel #11972

Merged
merged 5 commits into from
Oct 21, 2024
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
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
Loading