Skip to content

Commit 41938e2

Browse files
authored
feat(analytics): add Cloudflare Workers Analytics Engine edge telemetry, sendBeacon click tracking, and first-party PostHog /ingest proxy (#724)
1 parent a24576d commit 41938e2

11 files changed

Lines changed: 567 additions & 1 deletion

File tree

‎apps/site/.dev.vars.example‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
# Local secrets for `wrangler dev` / platformProxy. Copy to .dev.vars (gitignored).
22
SANITY_API_READ_TOKEN=
3+
# Optional: PostHog project key for visitor flows, funnels, and LMS telemetry via /ingest proxy
4+
PUBLIC_POSTHOG_KEY=
5+
PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
---
2+
import { env } from "cloudflare:workers";
3+
4+
const runtimeEnv = env as unknown as Record<string, string | undefined>;
5+
const posthogKey =
6+
runtimeEnv.PUBLIC_POSTHOG_KEY || import.meta.env.PUBLIC_POSTHOG_KEY || "";
7+
const posthogUiHost =
8+
runtimeEnv.PUBLIC_POSTHOG_UI_HOST ||
9+
import.meta.env.PUBLIC_POSTHOG_UI_HOST ||
10+
"https://us.posthog.com";
11+
---
12+
13+
<script is:inline define:vars={{ posthogKey, posthogUiHost }}>
14+
(function () {
15+
function sendBeaconEvent(type, detail) {
16+
var payload = JSON.stringify({
17+
type: type,
18+
path: window.location.pathname,
19+
target: (detail && detail.target) || "",
20+
label: (detail && detail.label) || "",
21+
value: (detail && detail.value) || 1,
22+
});
23+
24+
try {
25+
if (navigator.sendBeacon) {
26+
navigator.sendBeacon("/api/event", payload);
27+
} else {
28+
fetch("/api/event", {
29+
method: "POST",
30+
body: payload,
31+
keepalive: true,
32+
headers: { "Content-Type": "application/json" },
33+
}).catch(function () {});
34+
}
35+
} catch (_e) {}
36+
37+
try {
38+
if (window.posthog && typeof window.posthog.capture === "function") {
39+
window.posthog.capture(type, {
40+
path: window.location.pathname,
41+
target: detail && detail.target,
42+
label: detail && detail.label,
43+
value: detail && detail.value,
44+
});
45+
}
46+
} catch (_e) {}
47+
}
48+
49+
// Global helper for future LMS course/lesson components & auth gates
50+
window.ccTrack = sendBeaconEvent;
51+
52+
// Passive click delegation for [data-track], outbound links, search, and chat
53+
document.addEventListener(
54+
"click",
55+
function (e) {
56+
var target = e.target;
57+
if (!(target instanceof Element)) return;
58+
59+
var trackedEl = target.closest("[data-track]");
60+
if (trackedEl) {
61+
var eventType = trackedEl.getAttribute("data-track") || "ui_interaction";
62+
var label =
63+
trackedEl.getAttribute("data-track-label") ||
64+
trackedEl.getAttribute("aria-label") ||
65+
(trackedEl.textContent || "").trim().slice(0, 80);
66+
var href =
67+
trackedEl.getAttribute("data-track-target") ||
68+
trackedEl.getAttribute("href") ||
69+
"";
70+
sendBeaconEvent(eventType, { target: href, label: label });
71+
return;
72+
}
73+
74+
var anchor = target.closest("a[href]");
75+
if (anchor) {
76+
var rawHref = anchor.getAttribute("href") || "";
77+
if (rawHref.indexOf("http") === 0) {
78+
try {
79+
var parsed = new URL(rawHref, window.location.origin);
80+
if (parsed.hostname !== window.location.hostname) {
81+
var linkLabel =
82+
anchor.getAttribute("aria-label") ||
83+
(anchor.textContent || "").trim().slice(0, 80) ||
84+
parsed.hostname;
85+
sendBeaconEvent("outbound_click", {
86+
target: rawHref,
87+
label: linkLabel,
88+
});
89+
}
90+
} catch (_e) {}
91+
}
92+
}
93+
},
94+
{ passive: true, capture: true },
95+
);
96+
97+
// Track podcast audio plays passively
98+
document.addEventListener(
99+
"play",
100+
function (e) {
101+
var el = e.target;
102+
if (el instanceof HTMLAudioElement) {
103+
sendBeaconEvent("podcast_listen", {
104+
target: el.currentSrc || el.src || window.location.pathname,
105+
label: "html5_audio",
106+
});
107+
}
108+
},
109+
{ passive: true, capture: true },
110+
);
111+
112+
// Optional PostHog initialization via first-party /ingest reverse proxy
113+
if (posthogKey) {
114+
!(function (t, e) {
115+
var o, n, p, r;
116+
e.__SV ||
117+
((window.posthog = e),
118+
(e._i = []),
119+
(e.init = function (i, s, a) {
120+
function g(t, e) {
121+
var o = e.split(".");
122+
2 == o.length && ((t = t[o[0]]), (e = o[1])),
123+
(t[e] = function () {
124+
t.push([e].concat(Array.prototype.slice.call(arguments, 0)));
125+
});
126+
}
127+
((p = t.createElement("script")).type = "text/javascript"),
128+
(p.crossOrigin = "anonymous"),
129+
(p.async = !0),
130+
(p.src =
131+
s.api_host.replace(".i.posthog.com", "-assets.i.posthog.com") +
132+
"/static/array.js"),
133+
(r = t.getElementsByTagName("script")[0]).parentNode.insertBefore(
134+
p,
135+
r,
136+
);
137+
var u = e;
138+
for (
139+
void 0 !== a ? (u = e[a] = []) : (a = "posthog"),
140+
u.people = u.people || [],
141+
u.toString = function (t) {
142+
var e = "posthog";
143+
return (
144+
"posthog" !== a && (e += "." + a), t || (e += " (stub)"), e
145+
);
146+
},
147+
u.people.toString = function () {
148+
return u.toString(1) + ".people (stub)";
149+
},
150+
o =
151+
"init capture register register_once register_for_session unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId setPersonPropertiesForFlags".split(
152+
" ",
153+
),
154+
n = 0;
155+
n < o.length;
156+
n++
157+
)
158+
g(u, o[n]);
159+
e._i.push([i, s, a]);
160+
}),
161+
(e.__SV = 1));
162+
})(document, window.posthog || []);
163+
164+
window.posthog.init(posthogKey, {
165+
api_host: "/ingest",
166+
ui_host: posthogUiHost,
167+
person_profiles: "identified_only",
168+
capture_pageview: true,
169+
capture_pageleave: true,
170+
disable_session_recording:
171+
window.location.pathname.indexOf("/courses") !== 0,
172+
});
173+
}
174+
})();
175+
</script>

‎apps/site/src/components/Header.astro‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ const desktopLinks = navLinks.filter((link) => link?.sideOnly !== true);
2929
<button
3030
type="button"
3131
id="search-header-trigger"
32+
data-track="search_open"
33+
data-track-label="header_search"
3234
class="inline-flex items-center justify-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors border border-border/60 cursor-pointer"
3335
aria-label="Search CodingCat.dev (⌘K)"
3436
>

‎apps/site/src/components/PodcastListen.astro‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ const buttonClass =
4242
rel="noreferrer noopener"
4343
class={buttonClass}
4444
aria-label="Search for this episode on Spotify"
45+
data-track="podcast_listen"
46+
data-track-label="spotify"
47+
data-track-target={spotifyUrl}
4548
>
4649
<svg class="size-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
4750
<path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0Zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02Zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2Zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3Z" />
@@ -54,6 +57,9 @@ const buttonClass =
5457
rel="noreferrer noopener"
5558
class={buttonClass}
5659
aria-label="Listen on Apple Podcasts"
60+
data-track="podcast_listen"
61+
data-track-label="apple_podcasts"
62+
data-track-target={appleUrl}
5763
>
5864
<svg class="size-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
5965
<path d="M5.34 0A5.328 5.328 0 0 0 0 5.34v13.32A5.328 5.328 0 0 0 5.34 24h13.32A5.328 5.328 0 0 0 24 18.66V5.34A5.328 5.328 0 0 0 18.66 0Zm6.525 2.568c2.336 0 4.448.902 6.056 2.587 1.224 1.272 1.912 2.619 2.264 4.392.12.59.12 2.2.007 2.864a8.506 8.506 0 0 1-3.24 5.296c-.608.46-2.096 1.261-2.336 1.261-.088 0-.096-.091-.056-.46.072-.592.144-.715.48-.856.535-.224 1.447-.874 2.008-1.435a7.644 7.644 0 0 0 2.008-3.536c.208-.824.184-2.656-.048-3.504-.728-2.672-2.512-4.544-5.096-5.336-.784-.24-2.44-.336-3.384-.2-2.36.32-4.448 1.664-5.616 3.616-1.096 1.832-1.264 4.24-.44 6.224.6 1.448 1.784 2.792 3.048 3.472.28.152.352.28.416.72.048.36.024.44-.056.44-.048 0-.416-.16-.816-.36l-.024-.008c-1.472-.72-2.72-2.032-3.448-3.648-1.472-3.248-.688-7.02 1.928-9.352 1.72-1.53 3.88-2.28 6.344-2.28z" />
@@ -67,6 +73,9 @@ const buttonClass =
6773
rel="noreferrer noopener"
6874
class={buttonClass}
6975
aria-label="Watch on YouTube"
76+
data-track="podcast_listen"
77+
data-track-label="youtube"
78+
data-track-target={youtubeUrl}
7079
>
7180
<svg class="size-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
7281
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814ZM9.545 15.568V8.432L15.818 12l-6.273 3.568Z" />

‎apps/site/src/components/SponsorCard.astro‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ const items = sponsors ?? [];
3131
href={href}
3232
target={external ? "_blank" : undefined}
3333
rel={external ? "noopener noreferrer sponsored" : undefined}
34+
data-track="sponsor_click"
35+
data-track-label={sponsor.title}
36+
data-track-target={href}
3437
class="flex"
3538
>
3639
<article class="flex max-w-xl grow flex-col overflow-hidden rounded-xl border border-border bg-card shadow-md transition-all hover:scale-[1.02] hover:shadow-lg">

‎apps/site/src/layouts/BaseLayout.astro‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Font } from "astro:assets";
33
import "@/styles/global.css";
44
55
import ChatModal from "@/components/ChatModal.astro";
6+
import AnalyticsBeacon from "@/components/AnalyticsBeacon.astro";
67
import DraftModeToggle from "@/components/DraftModeToggle.astro";
78
import Footer from "@/components/Footer.astro";
89
import Header from "@/components/Header.astro";
@@ -192,5 +193,6 @@ const graph = buildGraph([
192193
</div>
193194
{preview && <DraftModeToggle />}
194195
<ChatModal />
196+
<AnalyticsBeacon />
195197
</body>
196198
</html>

0 commit comments

Comments
 (0)