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
3 changes: 3 additions & 0 deletions apps/site/.dev.vars.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
# Local secrets for `wrangler dev` / platformProxy. Copy to .dev.vars (gitignored).
SANITY_API_READ_TOKEN=
# Optional: PostHog project key for visitor flows, funnels, and LMS telemetry via /ingest proxy
PUBLIC_POSTHOG_KEY=
PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
175 changes: 175 additions & 0 deletions apps/site/src/components/AnalyticsBeacon.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
---
import { env } from "cloudflare:workers";

const runtimeEnv = env as unknown as Record<string, string | undefined>;
const posthogKey =
runtimeEnv.PUBLIC_POSTHOG_KEY || import.meta.env.PUBLIC_POSTHOG_KEY || "";
const posthogUiHost =
runtimeEnv.PUBLIC_POSTHOG_UI_HOST ||
import.meta.env.PUBLIC_POSTHOG_UI_HOST ||
"https://us.posthog.com";
---

<script is:inline define:vars={{ posthogKey, posthogUiHost }}>
(function () {
function sendBeaconEvent(type, detail) {
var payload = JSON.stringify({
type: type,
path: window.location.pathname,
target: (detail && detail.target) || "",
label: (detail && detail.label) || "",
value: (detail && detail.value) || 1,
});

try {
if (navigator.sendBeacon) {
navigator.sendBeacon("/api/event", payload);
} else {
fetch("/api/event", {
method: "POST",
body: payload,
keepalive: true,
headers: { "Content-Type": "application/json" },
}).catch(function () {});
}
} catch (_e) {}

try {
if (window.posthog && typeof window.posthog.capture === "function") {
window.posthog.capture(type, {
path: window.location.pathname,
target: detail && detail.target,
label: detail && detail.label,
value: detail && detail.value,
});
}
} catch (_e) {}
}

// Global helper for future LMS course/lesson components & auth gates
window.ccTrack = sendBeaconEvent;

// Passive click delegation for [data-track], outbound links, search, and chat
document.addEventListener(
"click",
function (e) {
var target = e.target;
if (!(target instanceof Element)) return;

var trackedEl = target.closest("[data-track]");
if (trackedEl) {
var eventType = trackedEl.getAttribute("data-track") || "ui_interaction";
var label =
trackedEl.getAttribute("data-track-label") ||
trackedEl.getAttribute("aria-label") ||
(trackedEl.textContent || "").trim().slice(0, 80);
var href =
trackedEl.getAttribute("data-track-target") ||
trackedEl.getAttribute("href") ||
"";
sendBeaconEvent(eventType, { target: href, label: label });
return;
}

var anchor = target.closest("a[href]");
if (anchor) {
var rawHref = anchor.getAttribute("href") || "";
if (rawHref.indexOf("http") === 0) {
try {
var parsed = new URL(rawHref, window.location.origin);
if (parsed.hostname !== window.location.hostname) {
var linkLabel =
anchor.getAttribute("aria-label") ||
(anchor.textContent || "").trim().slice(0, 80) ||
parsed.hostname;
sendBeaconEvent("outbound_click", {
target: rawHref,
label: linkLabel,
});
}
} catch (_e) {}
}
}
},
{ passive: true, capture: true },
);

// Track podcast audio plays passively
document.addEventListener(
"play",
function (e) {
var el = e.target;
if (el instanceof HTMLAudioElement) {
sendBeaconEvent("podcast_listen", {
target: el.currentSrc || el.src || window.location.pathname,
label: "html5_audio",
});
}
},
{ passive: true, capture: true },
);

// Optional PostHog initialization via first-party /ingest reverse proxy
if (posthogKey) {
!(function (t, e) {
var o, n, p, r;
e.__SV ||
((window.posthog = e),
(e._i = []),
(e.init = function (i, s, a) {
function g(t, e) {
var o = e.split(".");
2 == o.length && ((t = t[o[0]]), (e = o[1])),
(t[e] = function () {
t.push([e].concat(Array.prototype.slice.call(arguments, 0)));
});
}
((p = t.createElement("script")).type = "text/javascript"),
(p.crossOrigin = "anonymous"),
(p.async = !0),
(p.src =
s.api_host.replace(".i.posthog.com", "-assets.i.posthog.com") +
"/static/array.js"),
(r = t.getElementsByTagName("script")[0]).parentNode.insertBefore(
p,
r,
);
var u = e;
for (
void 0 !== a ? (u = e[a] = []) : (a = "posthog"),
u.people = u.people || [],
u.toString = function (t) {
var e = "posthog";
return (
"posthog" !== a && (e += "." + a), t || (e += " (stub)"), e
);
},
u.people.toString = function () {
return u.toString(1) + ".people (stub)";
},
o =
"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(
" ",
),
n = 0;
n < o.length;
n++
)
g(u, o[n]);
e._i.push([i, s, a]);
}),
(e.__SV = 1));
})(document, window.posthog || []);

window.posthog.init(posthogKey, {
api_host: "/ingest",
ui_host: posthogUiHost,
person_profiles: "identified_only",
capture_pageview: true,
capture_pageleave: true,
disable_session_recording:
window.location.pathname.indexOf("/courses") !== 0,
});
}
})();
</script>
2 changes: 2 additions & 0 deletions apps/site/src/components/Header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const desktopLinks = navLinks.filter((link) => link?.sideOnly !== true);
<button
type="button"
id="search-header-trigger"
data-track="search_open"
data-track-label="header_search"
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"
aria-label="Search CodingCat.dev (⌘K)"
>
Expand Down
9 changes: 9 additions & 0 deletions apps/site/src/components/PodcastListen.astro
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ const buttonClass =
rel="noreferrer noopener"
class={buttonClass}
aria-label="Search for this episode on Spotify"
data-track="podcast_listen"
data-track-label="spotify"
data-track-target={spotifyUrl}
>
<svg class="size-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<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" />
Expand All @@ -54,6 +57,9 @@ const buttonClass =
rel="noreferrer noopener"
class={buttonClass}
aria-label="Listen on Apple Podcasts"
data-track="podcast_listen"
data-track-label="apple_podcasts"
data-track-target={appleUrl}
>
<svg class="size-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<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" />
Expand All @@ -67,6 +73,9 @@ const buttonClass =
rel="noreferrer noopener"
class={buttonClass}
aria-label="Watch on YouTube"
data-track="podcast_listen"
data-track-label="youtube"
data-track-target={youtubeUrl}
>
<svg class="size-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<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" />
Expand Down
3 changes: 3 additions & 0 deletions apps/site/src/components/SponsorCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const items = sponsors ?? [];
href={href}
target={external ? "_blank" : undefined}
rel={external ? "noopener noreferrer sponsored" : undefined}
data-track="sponsor_click"
data-track-label={sponsor.title}
data-track-target={href}
class="flex"
>
<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">
Expand Down
2 changes: 2 additions & 0 deletions apps/site/src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Font } from "astro:assets";
import "@/styles/global.css";

import ChatModal from "@/components/ChatModal.astro";
import AnalyticsBeacon from "@/components/AnalyticsBeacon.astro";
import DraftModeToggle from "@/components/DraftModeToggle.astro";
import Footer from "@/components/Footer.astro";
import Header from "@/components/Header.astro";
Expand Down Expand Up @@ -192,5 +193,6 @@ const graph = buildGraph([
</div>
{preview && <DraftModeToggle />}
<ChatModal />
<AnalyticsBeacon />
</body>
</html>
Loading
Loading