Skip to content

spec(007): platform adoption metrics + community stats (PostHog, GDPR-aligned)#70

Merged
rmjoia merged 3 commits into
mainfrom
claude/spec-007-analytics
May 29, 2026
Merged

spec(007): platform adoption metrics + community stats (PostHog, GDPR-aligned)#70
rmjoia merged 3 commits into
mainfrom
claude/spec-007-analytics

Conversation

@rmjoia

@rmjoia rmjoia commented May 28, 2026

Copy link
Copy Markdown
Owner

What this PR delivers

Spec + task breakdown only — no implementation. Discussion artefact for the PostHog / community-metrics work, GDPR-aligned per the conversation about it.

Two surfaces, one privacy posture:

  • /community — public Cosmos-derived stats page (codepals total, profiles public, skills represented, top 10 skills, active-last-week). Anonymous-accessible, no third-party in the read path, 5-min server-side cache.
  • PostHog Cloud EU — operator dashboards for product analytics + investor-shareable funnels/retention. Cookieless, no identify(), autocapture off, session replay disabled + double-locked, IP anonymised. Single src/lib/analytics.ts wrapper, lint + grep invariants prevent any other file from importing posthog-js directly. Five explicit events to start; no autocapture means no surprise data.

GDPR posture (the bit most projects skip):

  • Lawful basis: legitimate interest with a documented balancing test (.specify/privacy/posthog-lia.md) committed alongside the implementation PR
  • PRIVACY.md updated in the same PR — analytics doesn't ship without the disclosure
  • Opt-out via localStorage.codepals_no_analytics (first-party preference, not a tracker)
  • PostHog DPA + EU host hardcoded

Answers your two product asks explicitly:

  • "metrics visible on the webapp for the community" → US1 + the /community page (Cosmos-derived, ~5min-cached, public)
  • "metrics shareable with investors" → US4, leveraging PostHog's standard share-dashboard feature

Slicing for implementation (separate PRs):

  • 007-A — community stats (Cosmos-derived, lowest risk, ships first)
  • 007-B — PostHog wrapper + privacy docs + initial events + CSP additions (privacy-critical, reviewed with LIA + PRIVACY.md open)
  • 007-C — opt-out toggle in profile-edit (bundleable with 007-B if scope allows)
  • 007-D — operator dashboards (no code, runbook only)

Verified by

Docs-only; no behavioural change.

Constitution compliance

  • P1 — N/A (no UI yet)
  • P2 Code Quality — prettier-checked
  • P3 Security (NON-NEGOTIABLE) — spec mandates: typed-payload guard rails, hardcoded EU host, no identify() lint rule, CSP additions explicit + pinned by test
  • P4 — N/A
  • P5 Privacy (NON-NEGOTIABLE) — entire spec is structured around this. LIA + PRIVACY.md updates are mandatory items in 007-B, not "nice to have"
  • P6/P7/P8 — N/A for docs
  • P9 Verified Quality (NON-NEGOTIABLE) — see "Verified by" exception phrase per .github/pull_request_template.md

Platform constraints

  • No new platform constraint discovered.

Operator action items

  • Review the spec — particularly the "What's NOT collected" list and the event surface (FR-726). Push back on any signal you want or don't want before implementation starts.
  • When 007-B's PR opens: sign the PostHog DPA, create the EU project, record the public key as the PUBLIC_POSTHOG_KEY SWA app setting (documented in the spec).

Open questions (also listed in the spec)

  • Top-skills cap: 10 vs 20?
  • Country / timezone distribution on /community? (Privacy risk for tiny populations — deferred.)
  • Online-now count on /community (leverages spec 006 lastSeenAt)? Queued.
  • Final dashboard list for the investor pack — decide alongside the first real conversation.

Test plan

  • prettier --check on both files — clean
  • Full unit suite still green (no source changes)
  • API audit clean
  • No CI gates apply to spec markdown beyond format — implementation gates come with the slice PRs

https://claude.ai/code/session_018WA3SvALW1EmxJKVmRUGrR


Generated by Claude Code

…-aligned)

Draft spec + task breakdown for the analytics work the user has been
discussing. Two surfaces sharing one privacy posture:

1. /community — public Cosmos-derived stats page (codepals total,
   profiles public, skills represented, top skills, active-last-week).
   Anonymous-accessible, no third-party in the read path, 5-min
   server-side cache.

2. PostHog Cloud EU — operator dashboards for product analytics +
   investor-shareable funnels/retention. Cookieless, no identify(),
   autocapture off, session replay disabled + double-locked, IP
   anonymised, 5 explicit events to start. Single src/lib/analytics.ts
   wrapper enforced by lint + grep invariants.

GDPR posture (the bit most projects skip):
  - Lawful basis: legitimate interest with a documented balancing
    test in .specify/privacy/posthog-lia.md (committed alongside the
    implementation PR)
  - PRIVACY.md updated in the same PR — analytics doesn't ship without
    the privacy disclosure
  - Opt-out via localStorage flag (not a cookie — first-party
    preference, not a tracker)
  - PostHog DPA + EU host

Sliced into PRs:
  007-A: community stats (Cosmos-derived, lowest risk, ships first)
  007-B: PostHog wrapper + privacy docs + initial events + CSP
  007-C: opt-out toggle in profile-edit (bundleable with 007-B if scope allows)
  007-D: operator dashboard runbook (no code)

Spec answers two of the user's product asks explicitly:
  - "metrics visible on the webapp for the community" → US1 + the
    /community page (Cosmos-derived, real-time-ish, public)
  - "metrics shareable with potential investors" → US4, leveraging
    PostHog's standard read-only-share dashboards feature

Docs-only; no behavioural change. The implementation lands in the
follow-up PRs per the slicing above.

https://claude.ai/code/session_018WA3SvALW1EmxJKVmRUGrR

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a specification and task breakdown for platform adoption metrics: a public Cosmos-backed /community stats surface and a GDPR-aligned PostHog EU analytics/dashboard workflow.

Changes:

  • Defines functional requirements, privacy posture, risks, and success criteria for community stats and analytics.
  • Breaks implementation into follow-up slices for community stats, PostHog integration, opt-out UI, and dashboards.
  • Documents required privacy, CSP, lint/test, and operator setup guardrails.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 11 comments.

File Description
.specify/spec/007-platform-analytics-and-community-metrics.md Adds the feature specification, requirements, privacy posture, and success criteria.
.specify/tasks/007-platform-analytics-and-community-metrics.md Adds task breakdown and suggested PR slicing for implementation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .specify/tasks/007-platform-analytics-and-community-metrics.md Outdated
Comment thread .specify/tasks/007-platform-analytics-and-community-metrics.md Outdated
Comment thread .specify/tasks/007-platform-analytics-and-community-metrics.md Outdated
Comment thread .specify/spec/007-platform-analytics-and-community-metrics.md Outdated
Comment thread .specify/spec/007-platform-analytics-and-community-metrics.md Outdated
Comment thread .specify/spec/007-platform-analytics-and-community-metrics.md Outdated
Comment thread .specify/spec/007-platform-analytics-and-community-metrics.md Outdated
Comment thread .specify/tasks/007-platform-analytics-and-community-metrics.md Outdated
Comment thread .specify/tasks/007-platform-analytics-and-community-metrics.md Outdated
Comment thread .specify/spec/007-platform-analytics-and-community-metrics.md Outdated
…Vault (server)

Per operator feedback: clarify upfront which keys go where, so the
implementation PR doesn't have to re-litigate.

- FR-722 (revised): PUBLIC_POSTHOG_KEY is the public project token. Lives
  in an Azure SWA application setting; read via import.meta.env at
  astro-build time. NOT in Key Vault (wrong tool for a build-time public
  constant). analytics.init() short-circuits if unset (forks / local
  preview without a PostHog project still work).
- FR-722a (new): ANY future server-side PostHog secret (e.g.
  POSTHOG_PERSONAL_API_KEY) MUST go to Key Vault and be read via the
  existing ConfigService.getSecret(...) pattern. Pinning the rule before
  scope creeps.
- T-722: AZURE_SETUP_GUIDE step revised to spell out the SWA-app-setting
  path for the public key and the Key-Vault-only path for any future
  server secret, with the SWA @Microsoft.KeyVault(SecretUri=...) syntax
  the guide already documents.

Docs-only; no behavioural change.
…istency)

All actionable. None reshape the design; each tightens a guardrail.

Privacy / payload guardrails:
- FR-725 + FR-726 + T-750: page_view's route is now a RouteName literal
  union ('home' | 'find' | 'find-detail' | ...), normalized via a
  routeNameFor() helper. Raw window.location.pathname is never sent —
  dynamic segments (usernames, ids) could carry identifiers, which would
  defeat the FR-723 'no free-form string in payloads' guarantee.

Accuracy fixes (would have broken at implementation time):
- Spec L17: drop the misleading 'shared pipeline' line — /community is
  Cosmos-derived, PostHog is the behavioural surface; they share a
  privacy contract, not a data store.
- FR-700 + T-701: codepalsTotal must filter to STARTSWITH(c.id, 'gh-')
  so the admin-roster singleton ('id: roster') is excluded; the
  active-7d query gets the same filter. Without this, every count is
  off-by-one and grows wrong with future container occupants.
- FR-741: remove the analytics.refresh() reference — the module re-reads
  localStorage on every track() per FR-727, so the opt-out toggle takes
  effect immediately without any extra API. Naming a method that
  doesn't exist would have sent implementers in circles.
- FR-730 + T-760 + T-761: pin the bundled-posthog path. PR1 uses
  posthog-js via npm, so connect-src += eu.i.posthog.com is the ONLY
  CSP change; script-src stays untouched. Test asserts the positive
  (eu.i.posthog.com in connect-src) AND the negative (no posthog.com
  in script-src) so a future silent CDN switch fails the test.
- T-760: drop 'inline-comment-justify' inside staticwebapp.config.json
  — JSON has no comment syntax. Justification moves to T-761's test.
- T-772: opt-out smoke uses **/i.posthog.com/** (matches eu.i.posthog.com
  under Playwright glob), not **/posthog.com/** which wouldn't catch
  the eu.i. subdomain. Belt-and-braces: also match the ingest-path
  regex.

Consistency:
- Tasks header: 'three PRs' → 'four PRs' (composition table has A–D).

Docs-only; no behavioural change.
@rmjoia rmjoia merged commit 85011c1 into main May 29, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants