Analytics Review
Pull live data from PostHog and the project's database, compare against saved baselines in memory, and surface actionable insights.
Arguments
- No arguments: full review (PostHog funnel + database usage + comparison)
funnel: PostHog funnel metrics onlyusage: Database message/usage volume onlyusers: Per-user activity breakdownsave: Save current snapshot as a new baseline to memory
Step 1/8: Find credentials
Look for credentials in this order:
PostHog
- Check environment:
env | grep POSTHOG - Check project
.envrcfiles - Check memory files for PostHog project ID and API key
- The PostHog API key starts with
phx_(personal API key, not the project keyphc_)
PostHog API base: https://us.i.posthog.com (US) or https://eu.i.posthog.com (EU)
Database
- Check
server/.envor.env.localforDATABASE_DSN,DATABASE_URL,POSTGRES_URL, orSUPABASE_URL - For Supabase projects, the DSN format is:
postgresql://postgres:<password>@db.<project-ref>.supabase.co:5432/postgres - Use the project's Python environment (
uv run python3) withasyncpgto query
If credentials are missing, ask the user.
Step 2/8: Read event architecture
Check the project's PostHog events doc (usually POSTHOG_EVENTS.md) or memory for the event architecture. This tells you which events exist, what properties they carry, and what funnels they power.
Step 3/8: Pull PostHog data
Use the HogQL query endpoint (legacy insight endpoints may be blocked):
curl -s 'https://us.i.posthog.com/api/projects/<PROJECT_ID>/query/' \
-H "Authorization: Bearer <POSTHOG_API_KEY>" \
-H 'Content-Type: application/json' \
-d '{"query": {"kind": "HogQLQuery", "query": "<HOGQL>"}}'
Queries to run
1. Event counts by period — Compare current period vs previous baseline:
SELECT event, count() as cnt, count(DISTINCT person_id) as users,
multiIf(
toDate(timestamp) < toDate('SPLIT_DATE_1'), 'period_1',
toDate(timestamp) < toDate('SPLIT_DATE_2'), 'period_2',
'period_3'
) as period
FROM events
WHERE timestamp >= toDateTime('START_DATE')
AND event IN ('$pageview', 'cta_click', ... )
GROUP BY event, period
ORDER BY event, period
2. Pageview breakdown by path:
SELECT properties.$pathname as path, count() as cnt, count(DISTINCT person_id) as users
FROM events
WHERE timestamp >= toDateTime('START_DATE') AND event = '$pageview'
GROUP BY path ORDER BY cnt DESC LIMIT 25
3. All custom events (to check what's firing):
SELECT event, count() as cnt
FROM events
WHERE timestamp >= toDateTime('START_DATE')
AND event NOT IN ('$pageview', '$pageleave', '$autocapture', '$feature_flag_called', '$web_vitals', '$identify', '$rageclick')
GROUP BY event ORDER BY cnt DESC LIMIT 30
Step 4/8: Pull database usage data
Query the project's database for:
- Message/usage volume by period (daily, weekly)
- Per-user activity (messages, actions, last active date)
- New signups/subscriptions in the current period
- Active vs churned users
Adapt queries to the project's schema. Common patterns:
-- Daily volume
SELECT DATE(created_at) as day, COUNT(*) as total, COUNT(DISTINCT user_id) as users
FROM <activity_table>
WHERE created_at >= '<START_DATE>'
GROUP BY DATE(created_at) ORDER BY day
-- Per-user breakdown
SELECT u.email, u.plan, u.status,
COUNT(*) FILTER (WHERE a.created_at < '<SPLIT>') as before,
COUNT(*) FILTER (WHERE a.created_at >= '<SPLIT>') as after,
MAX(a.created_at) as last_active
FROM <users_table> u
JOIN <activity_table> a ON a.user_id = u.id
GROUP BY u.email, u.plan, u.status
ORDER BY after DESC
Step 5/8: Pull recent PRs and deploys
Run git log --oneline --since="<BASELINE_DATE>" --merges (or without --merges if no merge commits) to see what shipped since the last baseline. For each PR/commit that touched user-facing code:
- Note the merge date
- Summarize what changed (from commit message or PR title)
- Categorize: bug fix, new feature, SEO/content, UI change, pricing change, infrastructure
This becomes the "what changed" context for interpreting metric movements. Structure as:
### Changes since last baseline
| Date | PR/Commit | Category | Summary |
When comparing metrics, correlate timing: if a metric spiked on Mar 30 and PR #11 merged Mar 31, note the connection. Metric changes without a corresponding code change suggest external factors (marketing, organic growth, seasonality).
Step 6/8: Compare against baseline
Check memory for previous analytics snapshots (files matching *analytics* or *baseline*). If a baseline exists:
- Calculate daily rates for each metric in both periods
- Show percentage change and absolute change
- Flag metrics that moved significantly (>2x or <0.5x)
- Flag users who went silent (active before, zero activity now)
- Flag users who are ramping up (increasing activity)
If no baseline exists, this IS the baseline — note that in the output.
Step 7/8: Surface insights
Always structure output as:
Funnel Metrics (table)
- Period comparison with daily rates and trend multipliers
Usage Analytics (table)
- Volume, active users, per-user breakdown
PR Impact (table)
- Which PRs shipped since last baseline, what category, what metric moved
Actionable Insights (numbered list)
Focus on:
- Conversion changes — Did any funnel step improve or degrade?
- Activation signals — Are new users actually using the product?
- Churn signals — Who went silent? How long ago?
- Power users — Who's approaching plan limits?
- New behavior — Events firing for the first time?
- Anomalies — Unexpected spikes, drops, or patterns?
Recommendations (numbered list)
Specific actions the user can take based on the data.
Step 8/8: Save baseline (if requested or if none exists)
If the user passes save argument, or if no baseline exists in memory:
- Write a memory file (
<project>_analytics_<date>.md) with:- Snapshot date
- Changes since last baseline (PRs merged, deploys)
- Key metrics (user count, daily volume, conversion rates)
- Per-user status summary
- Top-line comparison vs previous baseline
- Update MEMORY.md index
Keep baselines concise — just the numbers needed for future comparison, not the full analysis.
Notes
- Always calculate daily rates (events / days in period) for fair comparison across unequal periods
- PostHog
count(DISTINCT person_id)gives unique users,count()gives total events - For Supabase/Postgres: use
uv run python3withasyncpgin the server directory - Exclude internal/test accounts from user-facing metrics
- If the project has a
POSTHOG_EVENTS.md, read it first to understand the event architecture