From 0c821a215c0e66e2cc2d544384d92d98260777c3 Mon Sep 17 00:00:00 2001 From: midenotch Date: Fri, 26 Jun 2026 11:29:15 +0100 Subject: [PATCH 1/2] feat: privacy-compliant cookieless analytics via Plausible --- .gitignore | 8 ++ index.html | 22 +++++ package.json | 3 +- pnpm-lock.yaml | 45 +++++++++ src/App.tsx | 15 ++- src/analytics.ts | 31 +++++++ src/components/CtaStrip.tsx | 6 +- src/components/Footer.tsx | 8 +- src/components/Hero.tsx | 10 +- src/pages/Privacy.tsx | 178 ++++++++++++++++++++++++++++++++++++ tsconfig.tsbuildinfo | 1 - 11 files changed, 319 insertions(+), 8 deletions(-) create mode 100644 src/analytics.ts create mode 100644 src/pages/Privacy.tsx delete mode 100644 tsconfig.tsbuildinfo diff --git a/.gitignore b/.gitignore index 7c2bc8f..926fca6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,11 @@ dist/ .env reference/ *.local + +# TypeScript build cache — regenerated on every build, not source +tsconfig.tsbuildinfo +*.tsbuildinfo + +# IDE / agent local config +.vscode/ +.kiro/ diff --git a/index.html b/index.html index f66e2ee..ef4c7f3 100644 --- a/index.html +++ b/index.html @@ -29,6 +29,28 @@ + + + + + =18'} hasBin: true + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + cosmiconfig-typescript-loader@6.3.0: resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==} engines: {node: '>=v18'} @@ -721,6 +728,23 @@ packages: peerDependencies: react: ^19.2.5 + react-router-dom@7.18.0: + resolution: {integrity: sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.18.0: + resolution: {integrity: sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + react@19.2.5: resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} engines: {node: '>=0.10.0'} @@ -754,6 +778,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1225,6 +1252,8 @@ snapshots: '@simple-libs/stream-utils': 1.2.0 meow: 13.2.0 + cookie@1.1.1: {} + cosmiconfig-typescript-loader@6.3.0(@types/node@25.6.0)(cosmiconfig@9.0.1(typescript@6.0.2))(typescript@6.0.2): dependencies: '@types/node': 25.6.0 @@ -1423,6 +1452,20 @@ snapshots: react: 19.2.5 scheduler: 0.27.0 + react-router-dom@7.18.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-router: 7.18.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + + react-router@7.18.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + cookie: 1.1.1 + react: 19.2.5 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.5(react@19.2.5) + react@19.2.5: {} require-directory@2.1.1: {} @@ -1458,6 +1501,8 @@ snapshots: semver@7.7.4: {} + set-cookie-parser@2.7.2: {} + source-map-js@1.2.1: {} string-width@4.2.3: diff --git a/src/App.tsx b/src/App.tsx index 56e14f0..a3dd1f0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,4 @@ +import { BrowserRouter, Routes, Route } from 'react-router-dom'; import Header from './components/Header'; import Hero from './components/Hero'; import Features from './components/Features'; @@ -6,8 +7,9 @@ import ForDevelopers from './components/ForDevelopers'; import Chains from './components/Chains'; import CtaStrip from './components/CtaStrip'; import Footer from './components/Footer'; +import Privacy from './pages/Privacy'; -export default function App() { +function Home() { return (
@@ -21,3 +23,14 @@ export default function App() {
); } + +export default function App() { + return ( + + + } /> + } /> + + + ); +} diff --git a/src/analytics.ts b/src/analytics.ts new file mode 100644 index 0000000..cff1aff --- /dev/null +++ b/src/analytics.ts @@ -0,0 +1,31 @@ +/** + * Thin wrapper around window.plausible() so the rest of the app stays + * decoupled from the analytics provider. + * + * Plausible sets no cookies and collects no personal data, so no consent + * banner is required under GDPR / ePrivacy. + * + * Usage: + * import { trackEvent } from '../analytics'; + * trackEvent('Read the Docs'); + * trackEvent('Code Tab Change', { props: { tab: 'scan.ts' } }); + */ + +declare global { + interface Window { + plausible?: ( + event: string, + options?: { props?: Record }, + ) => void; + } +} + +/** Fires a Plausible custom event. Safe to call even before the script loads. */ +export function trackEvent( + event: string, + options?: { props?: Record }, +): void { + if (typeof window.plausible === 'function') { + window.plausible(event, options); + } +} diff --git a/src/components/CtaStrip.tsx b/src/components/CtaStrip.tsx index 234355c..42491df 100644 --- a/src/components/CtaStrip.tsx +++ b/src/components/CtaStrip.tsx @@ -1,3 +1,5 @@ +import { trackEvent } from '../analytics'; + export default function CtaStrip() { return (
@@ -14,14 +16,16 @@ export default function CtaStrip() { href="https://console.usewraith.xyz" target="_blank" rel="noopener noreferrer" + onClick={() => trackEvent('Get API Key')} className="flex h-12 items-center justify-center bg-primary px-7 font-heading text-[13px] font-semibold uppercase tracking-[1.5px] text-surface transition-[filter] duration-150 hover:brightness-110" > - Get API Key + Get API Keys trackEvent('Read the Docs')} className="flex h-12 items-center justify-center border border-outline-variant px-7 font-heading text-[13px] font-semibold uppercase tracking-[1.5px] text-primary transition-colors duration-150 hover:bg-surface-bright" > Read the Docs diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 0b312a1..418dfcd 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,3 +1,5 @@ +import { Link } from 'react-router-dom'; + const columns = [ { title: 'PRODUCT', @@ -71,12 +73,12 @@ export default function Footer() { BUILT ON HORIZEN · ERC-5564 · OPEN SOURCE
- Privacy - + ('send.ts'); const [copied, setCopied] = useState(false); + const handleTabChange = (tab: Tab) => { + setActiveTab(tab); + trackEvent('Code Tab Change', { props: { tab } }); + }; + const lines = codeByTab[activeTab]; const handleCopy = () => { @@ -114,6 +120,7 @@ export default function Hero() { href="https://docs.usewraith.xyz" target="_blank" rel="noopener noreferrer" + onClick={() => trackEvent('Read the Docs')} className="flex h-12 items-center justify-center bg-primary px-7 font-heading text-[13px] font-semibold uppercase tracking-[1.5px] text-surface transition-[filter] duration-150 hover:brightness-110" > Read the Docs @@ -122,6 +129,7 @@ export default function Hero() { href="https://demo.usewraith.xyz" target="_blank" rel="noopener noreferrer" + onClick={() => trackEvent('Try the Demo')} className="flex h-12 items-center justify-center border border-outline-variant px-7 font-heading text-[13px] font-semibold uppercase tracking-[1.5px] text-primary transition-colors duration-150 hover:bg-surface-bright" > Try the Demo @@ -156,7 +164,7 @@ export default function Hero() { {tabs.map((tab) => (