| name | analytics-setup |
| description | Plan and instrument PostHog analytics into a Cloudflare Workers + Neon + React/Vite project — produces a reviewed event plan, then wires backend captureEvent calls and a consent-gated frontend AnalyticsContext. |
| user-invocable | true |
| allowed-tools | Read, Write, Edit, Bash, Glob, Grep, WebFetch, WebSearch |
/analytics-setup
Model routing: Sonnet for implementation; Haiku for verification/scoring; Opus only for explicit architectural decisions.
Figure out what to instrument in PostHog for a Cloudflare Workers + Neon +
React/Vite project, then wire up the infrastructure and implement the events.
The hard part is the event plan — reading the product and deciding what matters
before writing any code. This skill produces a recommended event plan, presents
it for review, then implements it.
Step 0 — Resume check
Before doing anything else, check whether .analytics-setup-progress.md exists
in the working directory.
If it exists:
- Read it.
- If
collected_inputs: true is present, extract the stored inputs (and the
approved event plan, if recorded) — do not re-ask or re-derive them.
- Find the first step checkbox that is still
[ ] (unchecked).
- Print:
Resuming from [step name].
- Skip Step 1 entirely and jump directly to the first unchecked step.
If it does not exist: continue to Step 1 as normal.
Step execution policy (applies to Steps 2–14):
After completing each step, mark its checkbox [x] in
.analytics-setup-progress.md. If a step's verify check fails, stop and report
the full error — do not continue to the next step. The user resolves the issue
and resumes (Step 0 picks up from the first unchecked step on the next run).
Step 1 — Gather inputs
Ask these before doing any work:
- Has
compliance-setup been run? — determines whether PostHog must be
gated behind Termly cookie consent. Yes/No.
- Has
auth-setup been run? — determines whether identify() can use
the session-backed user object. Yes/No.
- Has
payments-setup been run? — confirms whether payment events should
be included. Yes/No.
- What is the core user action? — in one sentence, what does a user do
that makes this product valuable? (Examples: "run a live poll", "submit a
form", "generate a report"). This anchors the funnel.
- What questions do you most want to answer about user behaviour? — free
text. Examples: "Why do users drop off before starting their first poll?",
"Which plan size do power users buy first?", "How often do users return?"
- Are there multiple channels users come from? (add-in, web, email link,
referral) — used to recommend
utm_source / $referrer properties.
After all questions are answered, write .analytics-setup-progress.md in the
working directory before doing any implementation work:
# Analytics Setup Progress
collected_inputs: true
## Inputs
<record each collected input as a key: value line>
## Approved event plan
<recorded after Step 3 approval>
## Steps
- [ ] read-project
- [ ] event-plan
- [ ] read-templates
- [ ] verify-posthog-docs
- [ ] install-posthog
- [ ] backend-service
- [ ] analytics-event-constants
- [ ] frontend-context
- [ ] pageviewtracker
- [ ] wire-app-root
- [ ] backend-events
- [ ] frontend-events
- [ ] env-vars
- [ ] write-tests
- [ ] verify
Step 2 — Read the project
Read the following to build a map of user actions before producing the event
plan. Do not skip this step — the event plan must be grounded in the actual
routes and pages, not guesses.
Backend — read all route files (src/routes/*.ts):
- For each HTTP handler: note the method, path, what it does, and whether a
logged-in user is required.
- Mark handlers that represent meaningful user intent (creating, starting,
closing, sharing, purchasing) vs. infrastructure (health checks, webhooks,
token exchange).
Frontend — read all page components (ui/src/pages/*.tsx):
- Note what UI actions (button clicks, form submits, navigation) trigger API
calls or represent user intent.
- Note where the user is in the product journey (onboarding, core loop,
monetization, sharing).
Existing instrumentation — check if captureEvent or posthog.capture
already exists anywhere. If so, list what's already covered so the plan only
adds what's missing.
Shared constants — check if AnalyticsEvent already exists. If so, read
it to understand the current vocabulary.
Step 3 — Produce the event plan
Using the product understanding from step 2 and the answers from step 1,
produce a structured event plan. Present this for review before writing any
code.
Event plan format
## Analytics Event Plan
### Funnel
<Describe the core user journey in 3-5 steps, e.g.:
1. Sign up → 2. Create item → 3. Start item → 4. Share result>
### Identify call
When: <on login / on session restore>
Properties to set on the user profile:
- <trait>: <why it's useful for segmentation>
- ...
### Events
| Event name (constant) | Trigger | Properties | Why it matters |
|--------------------------|--------------------------------|-------------------------------------|---------------------------------------|
| user_signed_up | OAuth callback, new user only | { oauth_provider } | Acquisition channel mix |
| <core_action>_created | POST /<route> | { <shape>, rehearsal_mode? } | Funnel entry |
| <core_action>_started | PUT /<route>/start | { <relevant_counts> } | Activation |
| <core_action>_completed | PUT /<route>/close | { participant_count, ... } | Core value delivered |
| report_generated | After completion | { item_id } | Value confirmation |
| payment_completed | Stripe webhook | { pack_size, amount_cents } | Revenue |
| $pageview | Route change | { $current_url } | Navigation funnel |
| api_error | HTTP ≥400 response | { status, path, method } | Error visibility |
### Events NOT recommended
<List actions that could be tracked but aren't worth the noise — e.g.
button hovers, intermediate form steps, admin-only actions.>
### Properties to add to existing events
<If the project already has events, note where properties are thin
and what should be added. E.g. "EVENT_CLOSED currently sends no
properties — add { poll_count, total_participants }.">
### Questions this plan answers
- <Q from step 1 Q5> → answered by <event(s)>
- ...
### Questions this plan cannot answer yet
- <What would require additional instrumentation or a separate tool>
Wait for explicit approval of the event plan before proceeding.
Step 4 — Read the template files
After the plan is approved, read all templates:
backend/services_analytics.ts
frontend/AnalyticsContext.tsx
frontend/PageViewTracker.tsx
shared/constants_analytics.ts
Step 4b — Verify the PostHog API surface against current docs
The backend service posts to PostHog's capture API directly (no SDK). Before
wiring those calls, WebFetch the current docs and confirm the endpoint path,
request body shape (api_key, event, distinct_id, properties), and host:
If the capture endpoint path or payload keys have changed, adapt
backend/services_analytics.ts (and the frontend init) before relying on them.
Step 5 — Install PostHog
npm install posthog-js
Check ui/package.json first; skip if already installed.
Step 6 — Backend analytics service
If src/services/analytics.ts (or equivalent) doesn't exist, write it from
backend/services_analytics.ts. No adaptation needed — the function is
generic.
Update the Env interface (src/types.ts) with:
POSTHOG_API_KEY?: string;
POSTHOG_HOST?: string;
Step 7 — AnalyticsEvent constants
If AnalyticsEvent doesn't exist, create it in the shared constants file
(e.g. shared/constants.ts or shared/constants_analytics.ts) from
shared/constants_analytics.ts, replacing the placeholder events with the
exact events from the approved plan.
If AnalyticsEvent already exists, add missing events from the plan to it.
Rule: Every event name used in any captureEvent() or posthog.capture()
call must be a key in AnalyticsEvent. No raw string literals at call sites.
Step 8 — Frontend analytics context
Write ui/src/contexts/AnalyticsContext.tsx from frontend/AnalyticsContext.tsx,
adapting:
- If
compliance-setup was not run (step 1 Q1 = No): replace the
Termly consent block with a direct init() call in useEffect.
- If
compliance-setup was run: keep the Termly consent gate as-is.
Step 9 — PageViewTracker
Write ui/src/components/PageViewTracker.tsx from frontend/PageViewTracker.tsx.
No adaptation needed.
Step 10 — Wire into the app root
Find the app root (ui/src/main.tsx or ui/src/App.tsx) and:
- Wrap the router with
<AnalyticsProvider>.
- Place
<PageViewTracker /> inside the router (so it has access to
useLocation), outside any route-specific components.
- If
auth-setup is in place, add an identify() call when the user loads:
const { user } = useAuth();
const { identify, reset } = useAnalytics();
useEffect(() => {
if (user) {
identify(user.id, {
});
}
}, [user]);
Also call reset() on sign-out so the PostHog identity is cleared.
Step 11 — Implement backend events
For each backend event in the approved plan, find the appropriate route handler
and add a captureEvent() call. Follow this pattern:
import { captureEvent } from '../services/analytics';
import { AnalyticsEvent } from '../../shared/constants';
captureEvent(
env,
user.id,
AnalyticsEvent.ITEM_STARTED,
{ rehearsal_mode: body.rehearsal_mode, item_count: items.length },
ctx
);
Rules:
- Call
captureEvent after the primary operation succeeds, never before.
- Always pass
ctx when the handler receives it — this prevents premature
Worker shutdown cutting off the fetch.
- Never
await the call — fire and forget.
- If the event is on a webhook (e.g. Stripe),
ctx may not be available;
the .catch(() => {}) in the service handles this safely.
Step 12 — Implement frontend events
For each frontend event in the approved plan, find the relevant component or
API client location and add a capture() call via useAnalytics().
API error tracking — add to the fetch wrapper (if one exists):
const { capture } = useAnalytics();
if (!res.ok) {
capture(AnalyticsEvent.API_ERROR, {
status: res.status,
path: url,
method: init?.method ?? 'GET',
});
}
Step 13 — Environment variables
Backend — wrangler.toml:
[vars]
POSTHOG_API_KEY = ""
POSTHOG_HOST = "https://us.i.posthog.com"
Frontend — ui/.env.example (create if missing):
# PostHog analytics (optional — omit to disable)
VITE_POSTHOG_API_KEY=
VITE_POSTHOG_HOST=https://us.i.posthog.com
Set production values:
npx wrangler secret put POSTHOG_API_KEY
For the frontend, set VITE_POSTHOG_API_KEY in your hosting provider's
environment variables (Cloudflare Pages, Vercel, etc.) — it is baked in at
build time, not a secret.
Step 13b — Write tests
Write at minimum:
- Event emission test: call the core action API — assert the backend analytics event is queued/sent.
- No-consent test: if compliance-setup is in place, assert events do NOT fire when consent is absent.
- Identify test: call login endpoint — assert
identify() fires with the user ID.
Step 14 — Verify
- Run
npx tsc --noEmit — fix any type errors before proceeding.
- Open the app locally and perform the core action (create → start → complete
an item). Check PostHog Live Events — confirm events arrive with the
expected properties.
- Sign in → confirm
identify() call appears in PostHog with the user ID.
- Navigate between routes → confirm
$pageview fires on each change.
- Trigger an API error (e.g. call a protected route without a session) →
confirm
api_error appears.
- If compliance-setup is in place, confirm that events do NOT fire before
analytics consent is given in the Termly banner.
Operational Readiness
SLI examples:
- Event ingestion rate: PostHog events captured per minute
- Identify call success rate: % of
identify() calls that return 2xx
- Page/screen event error rate: % of analytics calls that fail or time out
Key failure modes:
- PostHog capture blocked by ad blocker → detected by near-zero event rate in PostHog dashboard; mitigation: use PostHog reverse proxy
- Consent gate misconfigured → analytics fires before consent; detected by comparing event timestamps to consent_granted events; mitigation: audit consent gate logic
- Missing
identify() calls → anonymous events not tied to users; detected by high anonymous event % in PostHog; mitigation: check auth middleware ordering
Rollback classification: Reversible — removing PostHog calls and reverting event handlers restores prior state with no data migration needed.
Summary output
## analytics-setup complete
### Event plan implemented
| Event | Trigger location | Properties |
|-------------------------|----------------------------------|-----------------------------------|
| <event> | <file:handler> | { <props> } |
| ...
identify(): called on login/session restore, traits: { <list> }
$pageview: PageViewTracker, fires on every route change
api_error: api.ts fetch wrapper, status ≥ 400
Infrastructure:
src/services/analytics.ts — backend captureEvent (fire-and-forget)
ui/src/contexts/AnalyticsContext.tsx — React context, consent-gated: <yes/no>
ui/src/components/PageViewTracker.tsx
Next steps:
- Set POSTHOG_API_KEY via: npx wrangler secret put POSTHOG_API_KEY
- Set VITE_POSTHOG_API_KEY in your frontend hosting env vars
- In PostHog: create a funnel from <event_1> → <event_2> → <event_3>
- In PostHog: create a retention chart on <core_action>_completed