diff --git a/docs/app/(home)/sections/BuildChatSection/BuildChatSection.tsx b/docs/app/(home)/sections/BuildChatSection/BuildChatSection.tsx index 96e25639e..a447e633b 100644 --- a/docs/app/(home)/sections/BuildChatSection/BuildChatSection.tsx +++ b/docs/app/(home)/sections/BuildChatSection/BuildChatSection.tsx @@ -1,6 +1,6 @@ "use client"; -import { captureCreateCliCommandCopied } from "@/lib/create-cli-copy-analytics"; +import { captureCreateCliCommandCopied } from "@/lib/analytics"; import { ClipboardCommandButton } from "../../components/Button/Button"; import styles from "./BuildChatSection.module.css"; diff --git a/docs/app/(home)/sections/HeroSection/HeroSection.tsx b/docs/app/(home)/sections/HeroSection/HeroSection.tsx index 360cc65ae..43b5d6737 100644 --- a/docs/app/(home)/sections/HeroSection/HeroSection.tsx +++ b/docs/app/(home)/sections/HeroSection/HeroSection.tsx @@ -1,6 +1,6 @@ "use client"; -import { captureCreateCliCommandCopied } from "@/lib/create-cli-copy-analytics"; +import { captureCreateCliCommandCopied } from "@/lib/analytics"; import { ArrowRight } from "lucide-react"; import { useTheme } from "next-themes"; import { useEffect, useRef, useState, type ReactNode } from "react"; diff --git a/docs/app/_components/build-for-free-menu.tsx b/docs/app/_components/build-for-free-menu.tsx index 8c6bd0e4a..b2c7bad1d 100644 --- a/docs/app/_components/build-for-free-menu.tsx +++ b/docs/app/_components/build-for-free-menu.tsx @@ -1,7 +1,7 @@ "use client"; import { ClipboardCommandButton } from "@/app/(home)/components/Button/Button"; -import { captureCreateCliCommandCopied } from "@/lib/create-cli-copy-analytics"; +import { captureCreateCliCommandCopied } from "@/lib/analytics"; import { ArrowRight } from "lucide-react"; import styles from "./build-for-free-menu.module.css"; import { useHeaderDropdown } from "./use-header-dropdown"; diff --git a/docs/app/layout.tsx b/docs/app/layout.tsx index 7f8afaff8..8b5523a42 100644 --- a/docs/app/layout.tsx +++ b/docs/app/layout.tsx @@ -93,14 +93,16 @@ export default function Layout({ children }: LayoutProps<"/">) { + ); diff --git a/docs/app/providers.tsx b/docs/app/providers.tsx index 253ebec37..0c1d0f549 100644 --- a/docs/app/providers.tsx +++ b/docs/app/providers.tsx @@ -1,20 +1,35 @@ "use client"; +import { loadPostHog } from "@/lib/analytics"; import { addThesysLinkAttribution } from "@/lib/thesys-link-attribution"; -import posthog from "posthog-js"; -import { PostHogProvider } from "posthog-js/react"; import { useEffect } from "react"; export function PHProvider({ children }: { children: React.ReactNode }) { useEffect(() => { - posthog.init("phc_3OLW53x09ZTVZSV6BEpj5uycj3ooqR6KOemOjx04e3D", { - api_host: "https://dgoeivjus9jfp.cloudfront.net", - capture_pageview: "history_change", - disable_session_recording: false, - session_recording: { - sampleRate: 0.1, - }, - }); + let posthogDistinctId: string | undefined; + let posthogSessionId: string | undefined; + + const load = () => { + void loadPostHog() + .then((posthog) => { + posthogDistinctId = posthog.get_distinct_id(); + posthogSessionId = posthog.get_session_id(); + }) + .catch(() => { + // Analytics failures must not affect navigation. + }); + }; + + let cancelLoad: () => void; + if (typeof window.requestIdleCallback === "function") { + const idleCallbackId = window.requestIdleCallback(load, { + timeout: 5_000, + }); + cancelLoad = () => window.cancelIdleCallback(idleCallbackId); + } else { + const timeoutId = setTimeout(load, 0); + cancelLoad = () => clearTimeout(timeoutId); + } const decorateLink = (anchor: HTMLAnchorElement) => { const href = anchor.getAttribute("href"); @@ -23,39 +38,14 @@ export function PHProvider({ children }: { children: React.ReactNode }) { const attributedHref = addThesysLinkAttribution( href, window.location.href, - posthog.get_distinct_id(), - posthog.get_session_id(), + posthogDistinctId, + posthogSessionId, ); if (attributedHref !== href) anchor.setAttribute("href", attributedHref); }; - const decorateLinksWithin = (root: ParentNode) => { - if (root instanceof HTMLAnchorElement) decorateLink(root); - root.querySelectorAll("a[href]").forEach(decorateLink); - }; - - decorateLinksWithin(document); - - const observer = new MutationObserver((mutations) => { - for (const mutation of mutations) { - if (mutation.type === "attributes") { - if (mutation.target instanceof HTMLAnchorElement) decorateLink(mutation.target); - continue; - } - - for (const node of mutation.addedNodes) { - if (node instanceof Element) decorateLinksWithin(node); - } - } - }); - - observer.observe(document.body, { - attributes: true, - attributeFilter: ["href"], - childList: true, - subtree: true, - }); + document.querySelectorAll("a[href]").forEach(decorateLink); const refreshLinkAtInteraction = (event: Event) => { if (!(event.target instanceof Element)) return; @@ -67,10 +57,10 @@ export function PHProvider({ children }: { children: React.ReactNode }) { document.addEventListener("click", refreshLinkAtInteraction, true); return () => { - observer.disconnect(); + cancelLoad(); document.removeEventListener("pointerdown", refreshLinkAtInteraction, true); document.removeEventListener("click", refreshLinkAtInteraction, true); }; }, []); - return {children}; + return children; } diff --git a/docs/lib/create-cli-copy-analytics.ts b/docs/lib/analytics.ts similarity index 60% rename from docs/lib/create-cli-copy-analytics.ts rename to docs/lib/analytics.ts index 46be70f9a..a850389f8 100644 --- a/docs/lib/create-cli-copy-analytics.ts +++ b/docs/lib/analytics.ts @@ -1,4 +1,33 @@ -import posthog from "posthog-js"; +type PostHog = (typeof import("posthog-js/dist/module.slim"))["default"]; + +let posthogPromise: Promise | undefined; + +export function loadPostHog(): Promise { + posthogPromise ??= import("posthog-js/dist/module.slim").then(({ default: posthog }) => { + posthog.init("phc_3OLW53x09ZTVZSV6BEpj5uycj3ooqR6KOemOjx04e3D", { + api_host: "https://dgoeivjus9jfp.cloudfront.net", + capture_pageview: "history_change", + advanced_disable_flags: true, + disable_external_dependency_loading: true, + disable_session_recording: true, + disable_surveys: true, + }); + + return posthog; + }); + + return posthogPromise; +} + +export const analytics = { + capture(eventName: string, properties: object = {}): void { + void loadPostHog() + .then((posthog) => posthog.capture(eventName, properties as Record)) + .catch(() => { + // Analytics must never interfere with the user action being measured. + }); + }, +}; export const CREATE_CLI_COMMAND_COPIED_EVENT = "create_cli_command_copied"; @@ -49,9 +78,5 @@ export function captureCreateCliCommandCopied( const properties = getCreateCliCommandCopiedProperties(command, context); if (!properties || typeof window === "undefined") return; - try { - posthog.capture(CREATE_CLI_COMMAND_COPIED_EVENT, properties); - } catch { - // Analytics must never interfere with a successful clipboard action. - } + analytics.capture(CREATE_CLI_COMMAND_COPIED_EVENT, properties); }