spec(007): platform adoption metrics + community stats (PostHog, GDPR-aligned)#70
Merged
Conversation
…-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
There was a problem hiding this comment.
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.
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.identify(), autocapture off, session replay disabled + double-locked, IP anonymised. Singlesrc/lib/analytics.tswrapper, lint + grep invariants prevent any other file from importingposthog-jsdirectly. Five explicit events to start; no autocapture means no surprise data.GDPR posture (the bit most projects skip):
.specify/privacy/posthog-lia.md) committed alongside the implementation PRPRIVACY.mdupdated in the same PR — analytics doesn't ship without the disclosurelocalStorage.codepals_no_analytics(first-party preference, not a tracker)Answers your two product asks explicitly:
/communitypage (Cosmos-derived, ~5min-cached, public)Slicing for implementation (separate PRs):
Verified by
Docs-only; no behavioural change.
Constitution compliance
identify()lint rule, CSP additions explicit + pinned by testPRIVACY.mdupdates are mandatory items in 007-B, not "nice to have".github/pull_request_template.mdPlatform constraints
Operator action items
PUBLIC_POSTHOG_KEYSWA app setting (documented in the spec).Open questions (also listed in the spec)
/community? (Privacy risk for tiny populations — deferred.)/community(leverages spec 006lastSeenAt)? Queued.Test plan
prettier --checkon both files — cleanhttps://claude.ai/code/session_018WA3SvALW1EmxJKVmRUGrR
Generated by Claude Code