diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..aa94f55 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# PostHog Analytics (optional) +# Set PUBLIC_POSTHOG_KEY to enable analytics. Leave unset to disable. +# PUBLIC_POSTHOG_KEY= +# PUBLIC_POSTHOG_HOST=https://eu.posthog.com +# PUBLIC_POSTHOG_DISABLED=false diff --git a/.gitignore b/.gitignore index 8bc7996..e949ecb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,11 @@ # Local Netlify build directory .netlify +# Environment and secrets +.env +.env.local +.env.*.local + # AI assistant and IDE config .cursor .codex diff --git a/backlog/PHASES.md b/backlog/PHASES.md index acb09ed..49172e1 100644 --- a/backlog/PHASES.md +++ b/backlog/PHASES.md @@ -5,15 +5,15 @@ This file chunks `backlog/active` work items into implementation phases with dep ## Phase 0: Documentation Foundation (Completed) Purpose: Establish canonical docs, archive legacy material, and lock documentation governance. -Work items: -1. `backlog/active/create-canonical-docs-ia.md` -2. `backlog/active/author-merged-prd.md` -3. `backlog/active/write-architecture-and-sitemap.md` -4. `backlog/active/establish-adr-system.md` -5. `backlog/active/archive-legacy-docs-and-add-stubs.md` -6. `backlog/active/remove-nimbalyst-artifacts.md` -7. `backlog/active/normalize-doc-links-and-validate.md` -8. `backlog/active/publish-doc-governance.md` +Work items (moved to `backlog/done/`): +1. `backlog/done/create-canonical-docs-ia.md` +2. `backlog/done/author-merged-prd.md` +3. `backlog/done/write-architecture-and-sitemap.md` +4. `backlog/done/establish-adr-system.md` +5. `backlog/done/archive-legacy-docs-and-add-stubs.md` +6. `backlog/done/remove-nimbalyst-artifacts.md` +7. `backlog/done/normalize-doc-links-and-validate.md` +8. `backlog/done/publish-doc-governance.md` Exit criteria: - Canonical docs exist and are linked from `docs/README.md`. diff --git a/backlog/active/add-consent-and-privacy-guards-for-analytics.md b/backlog/done/add-consent-and-privacy-guards-for-analytics.md similarity index 100% rename from backlog/active/add-consent-and-privacy-guards-for-analytics.md rename to backlog/done/add-consent-and-privacy-guards-for-analytics.md diff --git a/backlog/active/decide-analytics-scope-and-privacy-model.md b/backlog/done/decide-analytics-scope-and-privacy-model.md similarity index 100% rename from backlog/active/decide-analytics-scope-and-privacy-model.md rename to backlog/done/decide-analytics-scope-and-privacy-model.md diff --git a/backlog/active/implement-analytics-wrapper-and-event-schema.md b/backlog/done/implement-analytics-wrapper-and-event-schema.md similarity index 100% rename from backlog/active/implement-analytics-wrapper-and-event-schema.md rename to backlog/done/implement-analytics-wrapper-and-event-schema.md diff --git a/backlog/active/instrument-brief-contact-and-cta-funnels.md b/backlog/done/instrument-brief-contact-and-cta-funnels.md similarity index 100% rename from backlog/active/instrument-brief-contact-and-cta-funnels.md rename to backlog/done/instrument-brief-contact-and-cta-funnels.md diff --git a/backlog/active/integrate-posthog-env-gated.md b/backlog/done/integrate-posthog-env-gated.md similarity index 100% rename from backlog/active/integrate-posthog-env-gated.md rename to backlog/done/integrate-posthog-env-gated.md diff --git a/backlog/active/validate-analytics-events-and-create-operator-runbook.md b/backlog/done/validate-analytics-events-and-create-operator-runbook.md similarity index 100% rename from backlog/active/validate-analytics-events-and-create-operator-runbook.md rename to backlog/done/validate-analytics-events-and-create-operator-runbook.md diff --git a/docs/README.md b/docs/README.md index 540d235..c75bfd9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,6 +7,7 @@ This folder is the canonical source of truth for the agency website. - [Architecture](architecture.md) - [Sitemap](sitemap.md) - [Documentation Workflow](contributing-docs.md) +- [Analytics Runbook](analytics-runbook.md) - [ADRs](adr/README.md) ## Ownership and Update Policy diff --git a/docs/adr/ADR-0004-analytics-scope-and-privacy.md b/docs/adr/ADR-0004-analytics-scope-and-privacy.md new file mode 100644 index 0000000..d071937 --- /dev/null +++ b/docs/adr/ADR-0004-analytics-scope-and-privacy.md @@ -0,0 +1,51 @@ +# ADR-0004: Analytics Scope and Privacy Model + +## Status +Accepted + +## Context +The agency website needs analytics to measure funnel performance (brief start/completion, contact conversion, CTA engagement) per PRD success metrics. Analytics must be privacy-first: no free-text or direct identifiers, explicit consent, and minimal scope. + +## Decision + +### Scope +**Minimal funnel.** Track only business-critical events: +- Brief: started, step completed, gap detected, gap resolved, completed, export (markdown/JSON) +- Contact: form submitted (subject only, no body) +- CTAs: clicked (label, source) +- Book a Call: clicked (source) + +No page views, scroll depth, or broad instrumentation. Aligns with PRD success metrics. + +### Consent +**Opt-in gated.** Analytics do not run until the user has given explicit consent. No tracking before consent. Consent state stored in localStorage; no cookies for analytics preference. + +### Replay +**Disabled.** Session replay is not enabled. Replay would require separate approval and ADR. + +### Retention +**12 months.** Event data retained for 12 months. Configurable in PostHog project settings. + +### Environment Defaults +| Environment | Analytics | +|-------------|-----------| +| Local dev | Disabled by default. Enable via `PUBLIC_POSTHOG_KEY` + `PUBLIC_POSTHOG_DISABLED=false` | +| Staging | Enabled when key present; consent required | +| Production | Enabled when key present; consent required | + +### PII Exclusions +The following must never be sent as event properties: +- `email`, `name`, `message`, `problem`, `users`, `successCriteria`, `constraints` +- Any free-text user input (brief answers, contact body) +- Direct identifiers (phone, address, IP-derived identifiers beyond session) + +Allowlist enforcement: only approved event names and property shapes are emitted. Unknown properties are stripped. + +### brief_gap_resolved Semantics +Fire when the user clicks "Start Over" after seeing gaps in the brief results. Captures intent to improve the brief. + +## Consequences +- Funnel visibility without PII risk. +- Opt-in may reduce event volume; acceptable for privacy posture. +- Wrapper must enforce allowlist and consent check before any backend call. +- Runbook must document env setup and consent behavior for maintainers. diff --git a/docs/adr/README.md b/docs/adr/README.md index dc7613c..2535584 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,3 +10,4 @@ - [ADR-0001: Lean Canonical Documentation Set](ADR-0001-lean-canonical-documentation.md) - Accepted - [ADR-0002: Archive Legacy Docs with Stubs](ADR-0002-archive-legacy-docs-with-stubs.md) - Accepted - [ADR-0003: File-Based Backlog Convention](ADR-0003-file-based-backlog-convention.md) - Accepted +- [ADR-0004: Analytics Scope and Privacy Model](ADR-0004-analytics-scope-and-privacy.md) - Accepted diff --git a/docs/analytics-runbook.md b/docs/analytics-runbook.md new file mode 100644 index 0000000..a954ff0 --- /dev/null +++ b/docs/analytics-runbook.md @@ -0,0 +1,79 @@ +# Analytics Runbook + +Operational guide for WE3 agency website analytics. See [ADR-0004](adr/ADR-0004-analytics-scope-and-privacy.md) for scope and privacy decisions. + +## Environment Variables + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `PUBLIC_POSTHOG_KEY` | Yes (to enable) | — | PostHog project API key | +| `PUBLIC_POSTHOG_HOST` | No | `https://eu.posthog.com` | PostHog API host | +| `PUBLIC_POSTHOG_DISABLED` | No | `false` | Set to `true` to force-disable analytics | + +## Enable/Disable + +- **Local dev:** Analytics disabled by default (`import.meta.env.DEV`). To test locally, set `PUBLIC_POSTHOG_KEY` and `PUBLIC_POSTHOG_DISABLED=false`. +- **Staging/Production:** Analytics enabled when `PUBLIC_POSTHOG_KEY` is set and `PUBLIC_POSTHOG_DISABLED` is not `true`. +- **Force disable:** Set `PUBLIC_POSTHOG_DISABLED=true` in any environment. + +## Consent (Opt-In) + +Per ADR-0004, analytics are opt-in. No events are sent until the user clicks "Accept" in the consent notice. + +- **Storage:** Consent stored in `localStorage` under `we3_analytics_consent` (`true` = accepted, `false` = declined). +- **No cookies** for the consent preference itself. +- **Session replay:** Disabled. Not enabled regardless of consent. + +## Property Sanitization + +The wrapper strips unknown properties and blocks PII-like keys. See ADR-0004 for the full blocklist. Only allowlisted properties per event are sent. + +## Event Catalog + +| Event | Props | Trigger | +|-------|-------|---------| +| `brief_started` | — | User lands on brief page, first step shown | +| `brief_step_completed` | `stepId`, `stepIndex` | User completes a brief step | +| `brief_gap_detected` | `gapCount`, `gapTypes` | Brief results show gaps (warnings/critical) | +| `brief_gap_resolved` | — | User clicks "Start Over" after seeing gaps | +| `brief_completed` | `engagement`, `confidence` | Brief flow completes, results shown | +| `brief_export_markdown` | — | User copies brief as Markdown | +| `brief_export_json` | — | User downloads brief as JSON | +| `book_call_clicked` | `source` | User clicks Book a Call / Schedule Call link | +| `contact_submitted` | `subject` | User submits contact form (validation passed) | +| `cta_clicked` | `label`, `source` | User clicks Start Your Brief, Contact, Email Us, etc. | + +## Manual Verification Checklist + +With `PUBLIC_POSTHOG_KEY` set and consent accepted: + +1. **brief_started** — Open `/brief`, confirm event in PostHog. +2. **brief_step_completed** — Complete one step, confirm `stepId` and `stepIndex`. +3. **brief_gap_detected** — Complete brief with short problem/success criteria, confirm `gapCount` and `gapTypes`. +4. **brief_gap_resolved** — With gaps shown, click "Start Over", confirm event. +5. **brief_completed** — Complete brief, confirm `engagement` and `confidence`. +6. **brief_export_markdown** — Click "Copy as Markdown", confirm event. +7. **brief_export_json** — Click "Download JSON", confirm event. +8. **book_call_clicked** — Click Book a Call (brief or contact), confirm `source`. +9. **contact_submitted** — Submit contact form, confirm `subject` only (no PII). +10. **cta_clicked** — Click Start Your Brief, Contact, or Email Us, confirm `label` and `source`. + +## Negative Tests (PII Blocking) + +In browser console with consent accepted: + +```javascript +window.analytics?.track('contact_submitted', { subject: 'project', email: 'test@example.com' }); +``` + +Confirm in PostHog: event has `subject` but not `email`. Repeat for `name`, `message`, `problem`, `users`, `successCriteria`, `constraints`. + +## Troubleshooting + +| Issue | Check | +|-------|-------| +| No events in PostHog | Consent accepted? `localStorage.getItem('we3_analytics_consent') === 'true'` | +| No events in PostHog | `PUBLIC_POSTHOG_KEY` set in build env? | +| No events in dev | Analytics disabled in dev by default. Set `PUBLIC_POSTHOG_DISABLED=false` and ensure key is set. | +| Wrong host | `PUBLIC_POSTHOG_HOST` — default `https://eu.posthog.com` | +| Events but wrong props | Check allowlist in `website/src/lib/analytics.ts` | diff --git a/docs/contributing-docs.md b/docs/contributing-docs.md index ec3c595..7599c8a 100644 --- a/docs/contributing-docs.md +++ b/docs/contributing-docs.md @@ -1,6 +1,8 @@ # Documentation Workflow ## Principles +- Analytics env setup and operations: see [Analytics Runbook](analytics-runbook.md). + - Keep one canonical source for each durable topic. - Prefer links over duplicated explanations. - Archive historical context rather than deleting it. diff --git a/website/package.json b/website/package.json index f7bb40e..74712ab 100644 --- a/website/package.json +++ b/website/package.json @@ -15,7 +15,8 @@ "astro": "^5.17.1", "astro-icon": "^1.1.5", "gray-matter": "^4.0.3", - "markdown-it": "^14.1.0" + "markdown-it": "^14.1.0", + "posthog-js": "^1.347.1" }, "devDependencies": { "style-dictionary": "^5.2.0" diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml index c92e5fe..8a64d12 100644 --- a/website/pnpm-lock.yaml +++ b/website/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: markdown-it: specifier: ^14.1.0 version: 14.1.0 + posthog-js: + specifier: ^1.347.1 + version: 1.347.1 devDependencies: style-dictionary: specifier: ^5.2.0 @@ -530,9 +533,117 @@ packages: peerDependencies: tslib: '2' + '@opentelemetry/api-logs@0.208.0': + resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/core@2.2.0': + resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.5.1': + resolution: {integrity: sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-logs-otlp-http@0.208.0': + resolution: {integrity: sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.208.0': + resolution: {integrity: sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.208.0': + resolution: {integrity: sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@2.2.0': + resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/resources@2.5.1': + resolution: {integrity: sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.208.0': + resolution: {integrity: sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.2.0': + resolution: {integrity: sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.2.0': + resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.39.0': + resolution: {integrity: sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==} + engines: {node: '>=14'} + '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + '@posthog/core@1.22.0': + resolution: {integrity: sha512-WkmOnq95aAOu6yk6r5LWr5cfXsQdpVbWDCwOxQwxSne8YV6GuZET1ziO5toSQXgrgbdcjrSz2/GopAfiL6iiAA==} + + '@posthog/types@1.347.1': + resolution: {integrity: sha512-ovHPNb09il/jObIfja42Ldg8des6oYqo6RwaduEHkGgpwrxWo7XmjHIoQpL0Qh7WKSnOnkE3Mm7hz9860i2REg==} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -713,6 +824,9 @@ packages: '@types/node@25.2.0': resolution: {integrity: sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -912,6 +1026,9 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + core-js@3.48.0: + resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1012,6 +1129,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -1122,6 +1242,9 @@ packages: picomatch: optional: true + fflate@0.4.8: + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + flattie@1.1.1: resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} engines: {node: '>=8'} @@ -1367,6 +1490,9 @@ packages: resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} engines: {node: '>=14'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -1687,6 +1813,12 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + posthog-js@1.347.1: + resolution: {integrity: sha512-PBloBIlcIPuZywJQBzSsTxY7oJYMevr2SwOa8f7T1h1YjnHN6coyh0wZMAAhqqrsVHXQj/9qaj3NOCJPHoZJEg==} + + preact@10.28.3: + resolution: {integrity: sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==} + prettier@3.8.1: resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} engines: {node: '>=14'} @@ -1707,6 +1839,10 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} @@ -1724,6 +1860,9 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + query-selector-shadow-dom@1.0.1: + resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} + radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} @@ -2154,6 +2293,9 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + web-vitals@5.1.0: + resolution: {integrity: sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==} + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} @@ -2697,8 +2839,113 @@ snapshots: '@jsonjoy.com/codegen': 17.65.0(tslib@2.8.1) tslib: 2.8.1 + '@opentelemetry/api-logs@0.208.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/exporter-logs-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-exporter-base@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + protobufjs: 7.5.4 + + '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/resources@2.5.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/sdk-logs@0.208.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/semantic-conventions@1.39.0': {} + '@oslojs/encoding@1.1.0': {} + '@posthog/core@1.22.0': + dependencies: + cross-spawn: 7.0.6 + + '@posthog/types@1.347.1': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@rollup/pluginutils@5.3.0(rollup@4.57.0)': dependencies: '@types/estree': 1.0.8 @@ -2840,6 +3087,8 @@ snapshots: '@types/node@25.2.0': dependencies: undici-types: 7.16.0 + + '@types/trusted-types@2.0.7': optional: true '@types/unist@3.0.3': {} @@ -3126,6 +3375,8 @@ snapshots: cookie@1.1.1: {} + core-js@3.48.0: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3224,6 +3475,10 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -3340,6 +3595,8 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fflate@0.4.8: {} + flattie@1.1.1: {} fontace@0.4.1: @@ -3638,6 +3895,8 @@ snapshots: pkg-types: 2.3.0 quansync: 0.2.11 + long@5.3.2: {} + longest-streak@3.1.0: {} lru-cache@11.2.5: {} @@ -4161,6 +4420,24 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + posthog-js@1.347.1: + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/exporter-logs-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + '@posthog/core': 1.22.0 + '@posthog/types': 1.347.1 + core-js: 3.48.0 + dompurify: 3.3.1 + fflate: 0.4.8 + preact: 10.28.3 + query-selector-shadow-dom: 1.0.1 + web-vitals: 5.1.0 + + preact@10.28.3: {} + prettier@3.8.1: {} prismjs@1.30.0: {} @@ -4174,6 +4451,21 @@ snapshots: property-information@7.1.0: {} + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 25.2.0 + long: 5.3.2 + pump@3.0.3: dependencies: end-of-stream: 1.4.5 @@ -4189,6 +4481,8 @@ snapshots: quansync@0.2.11: {} + query-selector-shadow-dom@1.0.1: {} + radix3@1.1.2: {} readdirp@5.0.0: {} @@ -4569,8 +4863,7 @@ snapshots: uncrypto@0.1.3: {} - undici-types@7.16.0: - optional: true + undici-types@7.16.0: {} undici@7.20.0: {} @@ -4693,6 +4986,8 @@ snapshots: web-namespaces@2.0.1: {} + web-vitals@5.1.0: {} + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 diff --git a/website/src/components/AnalyticsConsent.astro b/website/src/components/AnalyticsConsent.astro new file mode 100644 index 0000000..5a88b1e --- /dev/null +++ b/website/src/components/AnalyticsConsent.astro @@ -0,0 +1,70 @@ +--- +/** + * Minimal analytics consent notice. ADR-0004: opt-in gated. + * Shown when user has not yet made a choice. Dismisses on Accept/Decline. + */ +--- +
+ + + + diff --git a/website/src/layouts/Layout.astro b/website/src/layouts/Layout.astro index a0a4d44..e4cbae0 100644 --- a/website/src/layouts/Layout.astro +++ b/website/src/layouts/Layout.astro @@ -2,6 +2,7 @@ import "../styles/global.css"; import { SITE } from "../lib/site"; import { Icon } from "astro-icon/components"; +import AnalyticsConsent from "../components/AnalyticsConsent.astro"; interface Props { title?: string; @@ -51,6 +52,11 @@ function isActive(href: string): boolean { + + + @@ -216,6 +222,39 @@ function isActive(href: string): boolean { +