diff --git a/.env.example b/.env.example index 330ef31..21f9722 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,8 @@ NEXT_PUBLIC_DOCS_URL= # Optional. Enables the private BufferDash analytics tracker when both values are set. -# Create the site in BufferDash, then use the generated public site key here. +# Create the site with domain buffer.lol in BufferDash, then use its public site key here. +# BufferDash rejects browser tracking requests from other origins by default. BUFFERDASH_URL= BUFFERDASH_SITE_ID= diff --git a/README.md b/README.md index 005f688..3f4850c 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ The worker container needs `NET_RAW` for low-level network diagnostics. See `dia ## BufferDash Analytics -buffer.lol can send page views, outbound clicks, sessions, and custom product events to a private BufferDash instance. +buffer.lol can send page views, outbound clicks, sessions, and custom product events to a private BufferDash instance. Server-backed and browser-latency product events contain only the tool slug, outcome, and request duration; tool inputs are never included. 1. Deploy BufferDash at a private dashboard URL, such as `https://dash.buffer.lol`. 2. In BufferDash, open `/sites`, create a site for `buffer.lol`, and copy the generated public site key. diff --git a/app/privacy/page.tsx b/app/privacy/page.tsx index 9766d5e..b9ba356 100644 --- a/app/privacy/page.tsx +++ b/app/privacy/page.tsx @@ -21,7 +21,7 @@ export default function PrivacyPage() { diff --git a/components/tools/ToolExperience.tsx b/components/tools/ToolExperience.tsx index 6f6a535..f5b9046 100644 --- a/components/tools/ToolExperience.tsx +++ b/components/tools/ToolExperience.tsx @@ -4,6 +4,18 @@ import { useMemo, useState, useSyncExternalStore } from "react"; import type { Tool } from "@/data/tools"; import { ResultPanel } from "./ResultPanel"; +type BufferDashWindow = Window & { + bufferdash?: { + track: (type: string, metadata?: Record) => void; + }; +}; + +function trackToolUse(tool: string, outcome: "success" | "error", durationMs?: number) { + const metadata: Record = { tool, outcome }; + if (typeof durationMs === "number") metadata.durationMs = Math.round(durationMs); + (window as BufferDashWindow).bufferdash?.track("tool_used", metadata); +} + export function ToolExperience({ tool }: { tool: Tool }) { switch (tool.slug) { case "ping": return ; @@ -518,9 +530,12 @@ function BrowserLatencyTool({ mode }: { mode: "ping" | "stability" }) { if (index < sampleCount - 1) await wait(160); } - setResult({ kind: "success", samples, summary: summarizeLatencySamples(samples) }); + const summary = summarizeLatencySamples(samples); + setResult({ kind: "success", samples, summary }); + trackToolUse(mode, summary.received > 0 ? "success" : "error", summary.avgMs ?? undefined); } catch (error) { setResult({ kind: "error", message: error instanceof Error ? error.message : "The latency test failed." }); + trackToolUse(mode, "error"); } } @@ -703,6 +718,7 @@ function BackendPlaceholder({ tool }: { tool: Tool }) { durationMs: payload.durationMs, requestId: payload.requestId }); + trackToolUse(tool.slug, "error", payload.durationMs); return; } @@ -712,11 +728,13 @@ function BackendPlaceholder({ tool }: { tool: Tool }) { durationMs: payload.durationMs, requestId: payload.requestId }); + trackToolUse(tool.slug, "success", payload.durationMs); } catch (error) { setResult({ kind: "error", message: error instanceof Error ? error.message : "The backend request failed." }); + trackToolUse(tool.slug, "error"); } }