From 5bcfe1a949f91fc6173e9a7662f2d7bbeee98ff6 Mon Sep 17 00:00:00 2001 From: Agustin Delgado Date: Fri, 25 Sep 2026 08:36:18 -0300 Subject: [PATCH] feat(docs): add telemetry for page views, referrers and clicks Two layers, because one script cannot answer both questions. The counts of the readers, of where they came from and of the speed of a page come from the host of the site. Those scripts are served from this domain, so a content blocker does not stop them, and they write nothing to the device of the reader. The clicks come from an analytics client that loads after the page hydrates. It goes through a proxy on this domain for the same reason a blocker matches the host of a request. It keeps the identity of a reader in the tab and writes nothing to the device, so the site needs no consent dialog. Six events are named by hand: the page view, a search with the number of results it found, a copy with what was copied, the first touch of a demo, the link to the repository and the change of theme. A search that finds nothing is the most valuable of them: the query is the name of a component that is missing. Without VITE_POSTHOG_KEY each function does nothing, and the client is not in the bundle. See docs/src/lib/docs/telemetry/README.md. --- docs/package.json | 3 + docs/src/hooks.server.ts | 25 +++ .../components/code-copy/code-copy.svelte | 1 + .../components/copy-button/copy-button.svelte | 18 +- docs/src/lib/docs/components/demo/demo.svelte | 22 ++- .../lib/docs/components/header/header.svelte | 2 + .../install-command/install-command.svelte | 8 +- .../lib/docs/components/search/search.svelte | 27 +++ .../theme-toggle/theme-toggle.svelte | 8 +- docs/src/lib/docs/telemetry/README.md | 67 ++++++++ docs/src/lib/docs/telemetry/index.ts | 17 ++ docs/src/lib/docs/telemetry/proxy.test.ts | 113 ++++++++++++ docs/src/lib/docs/telemetry/proxy.ts | 160 +++++++++++++++++ docs/src/lib/docs/telemetry/telemetry.ts | 143 ++++++++++++++++ docs/src/routes/+layout.svelte | 10 ++ docs/src/routes/+layout.ts | 21 +++ pnpm-lock.yaml | 162 ++++++++++++++++++ pnpm-workspace.yaml | 2 + 18 files changed, 804 insertions(+), 5 deletions(-) create mode 100644 docs/src/hooks.server.ts create mode 100644 docs/src/lib/docs/telemetry/README.md create mode 100644 docs/src/lib/docs/telemetry/index.ts create mode 100644 docs/src/lib/docs/telemetry/proxy.test.ts create mode 100644 docs/src/lib/docs/telemetry/proxy.ts create mode 100644 docs/src/lib/docs/telemetry/telemetry.ts create mode 100644 docs/src/routes/+layout.ts diff --git a/docs/package.json b/docs/package.json index 34c4c2ba..422b3160 100644 --- a/docs/package.json +++ b/docs/package.json @@ -18,7 +18,10 @@ "@human-kit/ui": "workspace:*", "@lucide/svelte": "^0.574.0", "@sveltejs/adapter-vercel": "^6.3.1", + "@vercel/analytics": "^2.0.1", + "@vercel/speed-insights": "^2.0.0", "minisearch": "^7.2.0", + "posthog-js": "^1.434.12", "svelte-render-scan": "^1.1.0", "tailwind-variants": "^3.2.2", "vite-plugin-devtools-json": "^1.0.0" diff --git a/docs/src/hooks.server.ts b/docs/src/hooks.server.ts new file mode 100644 index 00000000..ad0c5e0b --- /dev/null +++ b/docs/src/hooks.server.ts @@ -0,0 +1,25 @@ +import type { Handle } from '@sveltejs/kit'; +import { isTelemetryProxyPath, proxyTelemetry } from '$lib/docs/telemetry/proxy'; + +/** + * Answers the telemetry path before the router sees it. + * + * The path is not a route of this site: it is the front of a proxy, and + * `proxy.ts` says why the events do not go straight to the vendor. A request + * that is not for the proxy goes on to the router, untouched. + */ +export const handle: Handle = async ({ event, resolve }) => { + if (isTelemetryProxyPath(event.url.pathname)) { + const method = event.request.method; + const hasBody = method !== 'GET' && method !== 'HEAD'; + return proxyTelemetry({ + url: event.url, + method, + headers: event.request.headers, + body: hasBody ? await event.request.arrayBuffer() : null, + clientAddress: event.getClientAddress() + }); + } + + return resolve(event); +}; diff --git a/docs/src/lib/docs/components/code-copy/code-copy.svelte b/docs/src/lib/docs/components/code-copy/code-copy.svelte index 2cff798a..0339affd 100644 --- a/docs/src/lib/docs/components/code-copy/code-copy.svelte +++ b/docs/src/lib/docs/components/code-copy/code-copy.svelte @@ -36,6 +36,7 @@ props: { text, label: 'Copy code', + source: 'code-block', class: 'absolute top-1.5 right-1.5' } }); diff --git a/docs/src/lib/docs/components/copy-button/copy-button.svelte b/docs/src/lib/docs/components/copy-button/copy-button.svelte index fc8caef8..33d6723c 100644 --- a/docs/src/lib/docs/components/copy-button/copy-button.svelte +++ b/docs/src/lib/docs/components/copy-button/copy-button.svelte @@ -2,16 +2,28 @@ import Button from '../button/button.svelte'; import Check from '../icons/check.svelte'; import Copy from '../icons/copy.svelte'; + import { track } from '$lib/docs/telemetry'; import type { ButtonSize, ButtonVariant } from '../button/recipe'; // Reusable copy-to-clipboard button built on the docs Button component. Shows a // check for a moment after copying (and disables itself so it can't re-fire). + // + // Every copy on the site goes through this button, so the `copy` event is sent + // from here: one place, and no call site can forget it. interface Props { text: string; label?: string; variant?: ButtonVariant; size?: ButtonSize; class?: string; + /** + * What the reader copied, for the telemetry: a code block of a guide, the + * source of a demo, an install command. A button with no source sends no + * event. + */ + source?: 'code-block' | 'demo-source' | 'install-command'; + /** More about the copy, e.g. which package manager the command was for. */ + detail?: string; } let { @@ -19,7 +31,9 @@ label = 'Copy', variant = 'ghost', size = 'icon-sm', - class: className = '' + class: className = '', + source, + detail }: Props = $props(); let copied = $state(false); @@ -30,6 +44,8 @@ copied = true; clearTimeout(timeout); timeout = setTimeout(() => (copied = false), 1500); + // After the write: a copy that the browser refused is not a copy. + if (source) track('copy', { source, detail: detail ?? null }); } diff --git a/docs/src/lib/docs/components/demo/demo.svelte b/docs/src/lib/docs/components/demo/demo.svelte index 8a47686a..5c4c161b 100644 --- a/docs/src/lib/docs/components/demo/demo.svelte +++ b/docs/src/lib/docs/components/demo/demo.svelte @@ -5,6 +5,7 @@ import { buttonVariants } from '../button/recipe'; import Surface from '../surface/surface.svelte'; import Code from '../icons/code.svelte'; + import { track } from '$lib/docs/telemetry'; interface Props { source: { code: string; html: string }; @@ -14,18 +15,35 @@ let { source, children }: Props = $props(); let expanded = $state(false); + + // One event for each demo, not one for each click: the question is whether the + // reader tried the component, and the answer does not become more true with + // the second press. A plain variable, because no markup reads it. + let interacted = false; + + // In the capture phase: a primitive inside the demo can stop a press from + // going up, and the read of this signal must not depend on which one does. + function onPreviewInteraction() { + if (interacted) return; + interacted = true; + track('demo_interact'); + }
-
+
{@render children()}
(expanded = next)}> - + {expanded ? 'Hide code' : 'Show code'} diff --git a/docs/src/lib/docs/components/header/header.svelte b/docs/src/lib/docs/components/header/header.svelte index e80ce85e..695ddddb 100644 --- a/docs/src/lib/docs/components/header/header.svelte +++ b/docs/src/lib/docs/components/header/header.svelte @@ -5,6 +5,7 @@ import { Frame } from '../frame/index.js'; import { buttonVariants } from '../button/recipe'; import Github from '../icons/github.svelte'; + import { track } from '$lib/docs/telemetry'; interface Props { title?: string; @@ -109,6 +110,7 @@ rel="noreferrer" aria-label="GitHub repository" class={buttonVariants({ variant: 'ghost', size: 'icon' })} + onclick={() => track('github_click')} > diff --git a/docs/src/lib/docs/components/install-command/install-command.svelte b/docs/src/lib/docs/components/install-command/install-command.svelte index a74ac7e4..ef7b7c3e 100644 --- a/docs/src/lib/docs/components/install-command/install-command.svelte +++ b/docs/src/lib/docs/components/install-command/install-command.svelte @@ -75,7 +75,13 @@ {/each} - +