← All Skills
AI Skill

analytics-instrumentation

Last updated: 2026-05-17

World-class product-analytics instrumentation playbook (PostHog/Mixpanel/Amplitude class, NOT GA4). Use when setting up event tracking, conversion funnels, dash

Quick Install
npx skills add analytics-instrumentation

Analytics Instrumentation Playbook

When to use this skill

Triggers: "set up analytics", "instrument events", "build tracking", "add PostHog/Mixpanel/Amplitude", "build conversion funnel", "set up dashboards", "track conversion", "revenue tracking", "alerts when X drops", "automate KPI report".

Scope: product analytics (behavior, funnels, retention). NOT for: GA4 web analytics (use seo-monitor skill), application performance monitoring (Datadog/Sentry), or pure ETL/warehouse pipelines.

Iron rules (never violate)

  1. Notifications must self-contain the data. Lark/Slack/email body MUST include the actual numbers/names/Top-N that triggered the alert. "See dashboard" alone = incomplete. (See ~/.claude/rules/lessons.md 2026-04-26.)
  2. Exclude internal accounts from every query. Founder/staff/test accounts pollute DAU 3-4x. Maintain a registry file (single source of truth). Apply at query time.
  3. Object_Action naming, snake_case, past-tense. task_created ✅ — taskCreate ❌, created_task ❌, Task Created ❌.
  4. Server-side is source-of-truth for revenue/conversion. Client events for clicks only. Stripe API direct for $ amounts.
  5. No analytics SDK in non-primary backend layer. Tycoon rule: TS only, never Python — Python emits via CLI → TS API → trackServer. Pick one identification layer per stack.
  6. Don't filter anonymous (no-email) users. They're top-of-funnel prospects, not noise. PostHog merges anon→identified on signup.
  7. Idempotency on revenue alerts. Dedupe by Stripe charge ID via PostHog marker event. Cron runs overlap; without dedup you double-page.

The 4-layer tracking stack

LayerWhatWherePurpose
1. Server SDKtrackServer(userId, event, props, workspaceId)API routes, webhooksSource of truth. Funnel, revenue, conversion.
2. Client SDKuseTrack() React hookComponentsClick-level only (autocapture handles the rest). Hero CTA, FAB, invite.
3. Source-of-truth APIStripe / DB directCron scripts$ amounts, MRR, churn. Don't trust event proxies for money.
4. Background jobsHogQL queries → Lark cardsScheduled cronSelf-contained alerts. Funnel anomaly, daily brief, stuck users, revenue signal.
File-path conventions:
lib/analytics-server.ts     # trackServer wrapper, identify/groupIdentify
lib/use-track.ts            # client hook
scripts/analytics-cron/     # 4 cron jobs: daily-brief, funnel-anomaly, revenue-signal, stuck-users
  _posthog.ts               # HogQL helper (read)
  _stripe.ts                # Stripe API helper (read)
  _internal-accounts.ts     # registry of internal emails/domains/distinct_ids
  _notify.ts                # data-rich Lark/Slack helper

Event naming convention (Object_Action)

Pattern: _ — snake_case, past-tense action.

task_createdcreateTask, tasks_created, task_create
signup_completeduserSignedUp, signup_done, Signed Up
pricing_cta_clickedclickedCTA, pricing-cta-click
first_chat_sentfirstChat, chat_first_sent
Reuse existing status/role enums verbatim — never invent parallel vocabulary (Tycoon STATUS_CONFIG, TaskStatus.DONE, etc.). If your codebase has a BACKLOG/TODO/IN_PROGRESS/DONE enum, your status_changed events use those literal strings.

Standard taxonomy library (copy-paste)

Lifecycle

  • signup_started, signup_completed
  • onboarding_started, onboarding_step_viewed, onboarding_completed
  • first__ — milestone events (one-shot per user/workspace)

Engagement

  • _viewed, _clicked, _used
  • __clicked — e.g. nav_settings_clicked, fab_clicked, invite_clicked

Revenue (server-side from webhooks)

  • checkout_started, checkout_completed
  • payment_succeeded, payment_failed
  • subscription_created, subscription_updated, subscription_canceled
  • first_paid_topup — milestone
  • revenue_alert_sent — internal marker for cron idempotency (chargeId in props)

Always include in props

{
  userId,         // who
  workspaceId,    // tenant context
  source,         // attribution: "chat" | "cron" | "manual" | "stripe_checkout" | etc
  surface,        // where in UI: "hero" | "tier-card" | "fab" | "nav"
  // event-specific props after these
}

Generic 8-step conversion funnel

Adapt to product type. The structure stays:

1. landing_view         (== $pageview for autocapture-instrumented routes)
  1. signup_completed
  2. onboarding_completed
  3. first_<key_action> (the "aha" moment for your product)
  4. <key_action>_completed
  5. first_paid_topup (or first_subscription, first_purchase)
  6. recurring_active (paying again — retention signal)
  7. referral_sent (or invite_sent — viral loop)
Product typeStep 4 (aha)Step 5
AI agent platform (Tycoon)first_chat_sentfirst_task_completed
E-commercecart_addedpurchase_completed
Dev toolfirst_api_callfirst_integration_live
SaaS B2Bfirst_doc_createdfirst_team_invited
Social/networkfirst_post_createdfirst_friend_added

4-dashboard architecture

Build these in PostHog/Mixpanel/Amplitude UI (don't build custom in-app):

🎯 Activation Funnel

  • 8-step funnel insight (last 30d + 7d)
  • Time-to-convert distribution (signup → aha)
  • Cohort breakdown by source/channel

📈 Engagement

  • DAU / WAU / MAU trend
  • Tasks/messages/key-action volume daily (stacked)
  • Top users by event count (last 7d)
  • Weekly retention cohort from signup_completed
  • Lifecycle (new/returning/resurrected/dormant)

💰 Revenue

  • Daily revenue (sum of $revenue property) — pull from Stripe direct, not PostHog events
  • New paying customers daily
  • Funnel: signup_completed → checkout_started → payment_succeeded
  • Top customers by LTV
  • MRR + active subscription count

🛡 Health

  • Failure event count by errorReason breakdown
  • API error rate proxy (any *_failed events)
  • Avg _completed.totalDurationMs
  • subscription_checkout_failed daily

Self-contained notification rule (critical)

Bad — forces user to click through:

🚨 Funnel anomaly detected. See dashboard for details.

Good — decision-ready:

🚨 Funnel anomaly: signup_completed → onboarding_completed dropped 32.4% WoW
  • This week: 41.2% (87/211)
  • Last week: 60.9% (134/220)
  • Sample: 211 vs 220
  • Top 3 stuck this week: cmo_xyz, cmn_abc, cmp_def

Required body items: trigger metric value, comparison (WoW or threshold), Top-N entities, suggested next step.

Implementation: query data source FIRST (HogQL/Stripe/DB), THEN compose message body. Never compose-then-link.

Internal-account exclusion

Maintain _internal-accounts.ts (or equivalent JSON for non-TS):

export const INTERNAL_EMAILS: readonly string[] = [
  "[email protected]",
  "[email protected]",
  // ↓ add teammates as they join ↓
] as const

export const INTERNAL_DOMAINS: readonly string[] = [ "@yourcompany.com", "@partner-agency.com", ] as const

export function excludeInternalPersons(): string { // HogQL fragment. PostHog person.properties.email is set when identify() called. const emailIns = INTERNAL_EMAILS.map(e => '${e}').join(", ") const domainOrs = INTERNAL_DOMAINS.map(d => person.properties.email LIKE '%${d}').join(" OR ") return NOT (person.properties.email IN (${emailIns}) OR (${domainOrs})) }

Apply in EVERY HogQL query: WHERE event = 'x' AND timestamp > ... AND ${excludeInternalPersons()}.

For PostHog dashboard insights: PATCH each insight's query.source.properties to add a not_icontains person-property filter. Same domain/email list.

For Mixpanel: use cohort with email property NOT contains @yourcompany.com, apply as global filter.

For Stripe path: import INTERNAL_EMAILS/DOMAINS, check customer.email, skip alerts for matches.

Cron + alert cadence (recommended)

CronSchedulePurposeChannel
daily-brief1am UTC (9am Beijing)Day-over-day KPI digestbusiness_automation
funnel-anomalyhourlyConversion drop ≥20% WoW + sample ≥50product_updates
revenue-signalevery 30 minFirst payment, ≥$50 charge, churnbusiness_automation
stuck-usersevery 2h≥10 users 24h+ on a stepproduct_updates
Idempotency for revenue-signal: dedupe by Stripe charge ID. Pattern: query PostHog for revenue_alert_sent events with same chargeId in last 7 days; if exists, skip; if sending, also fire revenue_alert_sent marker. Across cron run boundaries this prevents double-alerts. Failure policy: process.exit(0) even on error. Cron failure should NOT page; missing data should. Log to stdout for searchable GH Actions logs.

Anti-patterns (don't do these)

❌ Don't✅ Do instead
Build custom dashboards in-appUse PostHog/Mixpanel/Amplitude native UI
Filter anonymous usersTrack them — they're top-of-funnel
Add analytics SDK to multiple backend layersPick one (frontend stack), funnel others through it
camelCase/spaces in event namessnake_case, past-tense action
Track full email/PII in event propsTrack on person.properties only (PostHog/Mixpanel handle privacy)
Poll DB every 30s for revenueWebhook → trackServer real-time
Store cron seen-state in /tmpCI is ephemeral. Use marker events in PostHog for idempotency
Send "alert detected, see dashboard"Embed actual numbers in message body
Mix internal + external in DAUAlways filter by internal-account registry

Cross-tool decision matrix

NeedToolWhy
Behavioral funnel + retentionPostHog (default)Cheapest, open source, autocapture, session replay included
Same as PostHog but enterpriseMixpanel / AmplitudeBetter team features, more expensive
Real-time custom dashboardsBuilt-in product UIDon't reinvent in PostHog
Source-of-truth revenueStripe API directEvents lie, Stripe charges don't
Alerting on anomalyCron + Slack/Lark webhookPostHog/Mixpanel native alerts are weaker
Session replayPostHog (free tier) / FullStoryQuality varies
A/B testingPostHog feature flags / GrowthBookOpen source preferred

Implementation checklist (new project)

[ ] Pick analytics platform — default PostHog (cheapest + OSS)
[ ] Server SDK + client SDK installed
[ ] Reverse proxy /ph (or equivalent) for ad-block resilience
[ ] identify() called on auth callback with email + workspace group
[ ] _internal-accounts.ts registry with founder/test/team
[ ] 8-step conversion funnel mapped to product
[ ] Server events fire from webhooks (not just routes)
[ ] Client events only on what autocapture misses
[ ] Stripe API direct integration if revenue tracking matters
[ ] 4 dashboards created (Activation/Engagement/Revenue/Health)
[ ] All dashboard insights have internal-account filter
[ ] Cron jobs (4) with data-rich Lark/Slack messages
[ ] Idempotency on revenue-signal via PostHog marker events
[ ] CI run validated via workflow_dispatch (don't wait for first scheduled run)
[ ] Mandatory change notification fired (per company comms policy)

Tycoon reference (study these as canonical example)

Real shipped 2026-04-26:

  • Server: ~/tycoon/ts/src/lib/posthog-server.ts
  • Client: ~/tycoon/ts/src/lib/use-track.ts
  • Cron: ~/tycoon/ts/scripts/analytics-cron/{daily-brief,funnel-anomaly,revenue-signal,stuck-users}.ts
  • Helpers: _posthog.ts, _stripe.ts, _internal-accounts.ts
  • Notify: ~/tycoon/ts/src/lib/lark-notify.ts
  • Dashboards via API: ~/tycoon/ts/scripts/analytics-cron/posthog-dashboards.json
  • GH Actions schedule: ~/tycoon/.github/workflows/analytics-cron.yml

References

  • PostHog best practices: posthog.com/docs/product-analytics
  • Amplitude taxonomy: amplitude.com/docs/data/taxonomy-best-practices
  • Mixpanel naming: docs.mixpanel.com/docs/data-structure/events-and-properties/best-practices
  • Object-Action framework: industry standard, derived from Segment + Mixpanel docs
  • Internal ~/.claude/rules/lessons.md 2026-04-26 entries on notifications + internal-account filtering