Event Schema Design
Onboarding
| Event | When it fires | Key properties |
|---|---|---|
| user_signed_up | Email or OAuth form submitted | signup_method, referral_source, plan_intent |
| email_verified | User clicks verification link | time_to_verify_seconds |
| onboarding_step_completed | Each onboarding step finished | step_name, step_index, time_on_step_seconds |
| first_claim_started | User initiates their first claim | time_since_signup_seconds |
Core Usage
| Event | When it fires | Key properties |
|---|---|---|
| claim_created | Claim form submitted successfully | claim_type, is_first_claim |
| document_uploaded | File upload completed | file_type, file_size_kb, claim_id, upload_position |
| extraction_started | System begins processing | claim_id, document_count |
| extraction_completed | Data extracted successfully | claim_id, fields_extracted_count, processing_time_seconds |
| extraction_failed | Processing failed | claim_id, failure_reason |
| claim_verified | User accepts extracted data | claim_id, fields_edited_count |
| claim_exported | User exports or downloads results | claim_id, export_format |
Monetization
| Event | When it fires | Key properties |
|---|---|---|
| usage_limit_reached | Claim count hits free tier cap | claims_used, claims_limit, days_since_signup |
| upgrade_prompt_shown | Paywall or upsell modal appears | trigger_point, user_claims_count, prompt_variant |
| pricing_page_viewed | User lands on pricing page | source, prompt_variant |
| checkout_initiated | Stripe checkout session opened | plan_selected, trigger_source |
| upgrade_completed | Stripe webhook confirms payment | plan, mrr, days_on_free_tier |
Design principles
All events fire server-side after write confirmation
WhyPrevents null claim_id on race conditions where frontend fires before server response returns.
failure_reason is a structured enum
Valuesunsupported_format · file_too_large · parse_timeout · low_quality_scan
prompt_variant on upgrade_prompt_shown enables A/B analysis without separate event streams
BenefitSegment conversion rate by variant directly in PostHog funnel without joining to experiment tables.
Funnel Design
Three funnels. The standard activation and monetization paths miss the most interesting drop-offs.
A. Document Quality Funnel
Goal: measure whether the product actually delivers value, not just whether the user completed a step.
1
claim_created
Intent confirmed. User has a real job to do.
2
document_uploaded
Core product action. Drop here means UX friction or unclear next step.
⚠ Drop-off: format confusion, unclear upload area
3
extraction_completed
System delivered a result — but not necessarily a good one.
⚠ Drop-off: unsupported formats, poor scan quality
4
claim_verified
User accepted the extracted data. This is the real value delivery moment. If verify rate is below 70% of completions, extraction quality is the product problem — not funnel friction.
B. Upgrade Intent Funnel
Goal: understand where in the upgrade decision users drop — not just whether they converted.
1
usage_limit_reached
User has demonstrated enough value to hit the cap.
2
pricing_page_viewed
User is evaluating options. Drop here means the prompt doesn't link clearly to pricing.
⚠ Drop-off: prompt copy doesn't drive to pricing page
3
plan_selected
User chose a plan. Drop between view and select means too many options or no recommendation.
⚠ Drop-off: no recommended plan, price anchoring missing
4
checkout_initiated
Stripe checkout opened. Drop here means second-guessing at plan selection.
5
upgrade_completed
Payment confirmed via Stripe webhook. Drop here is checkout friction (card entry, unexpected tax lines).
C. Recovery Funnel
Goal: measure how well the product rescues users after an extraction failure.
1
extraction_failed
Without recovery, this is a silent churn trigger. User leaves, never comes back.
2
failure_message_shown
In-app guidance fires with specific failure_reason and fix tip. If below 80% of failures, error handling is broken.
⚠ Drop-off: generic error message, no actionable guidance
3
retry_attempted
User tries again. Drop here means the guidance was unclear or the fix too hard.
⚠ Drop-off: fix requires too many steps outside the product
4
extraction_completed
Recovery successful. Users who complete recovery retain at the same rate as users who never failed — this funnel tells you whether recovery is working.
Dashboard Design
Weekly team dashboard in PostHog, mock data. Covers activation, monetization, retention, and health.
Activation Rate
34%
↑ 2.1pp WoW · 7-day cohort
Free → Paid Conversion
8.2%
↑ 0.4pp WoW · 30-day cohort
Extraction Success Rate
91.4%
→ stable · alert threshold 90%
Activation Funnel
signup → first extraction · 7-day cohort
Upgrade Funnel
limit reached → upgrade completed
Extraction Success Rate
extraction_completed / extraction_started · 12 weeks
MRR Added (weekly)
sum of mrr on upgrade_completed events
D7 / D30 Retention by Cohort
% of signup cohort returning to fire any event on day 7 and day 30.
| Cohort | Signups | D7 | D30 |
|---|---|---|---|
| Mar 24 | 312 | 48% | 29% |
| Mar 31 | 287 | 51% | 31% |
| Apr 7 | 341 | 46% | 28% |
| Apr 14 | 298 | 44% | — |
| Apr 21 | 356 | 42% | — |
Instrumentation QA
Five risks, all based on patterns I've seen in production systems.
1. Duplicate extraction events
What happensSystem retries on failure. extraction_completed fires twice for the same claim_id.
ImpactExtraction success rate appears artificially high. Funnel counts inflate.
FixAdd idempotency_key property (hash of claim_id + attempt_number). Deduplicate on this key in all analysis queries.
2. Missing claim_id on document_uploaded
What happensFrontend fires document_uploaded immediately after file pick, before claim is saved server-side. claim_id is null.
ImpactCannot join uploads to claims. Same pattern seen at scale: in a high-volume web analytics stack, spot_id was missing on the listing view event — blocked all listing-level funnel analysis for months.
FixAlways emit events server-side after write transaction completes. Never trust the frontend to have the entity ID at fire time.
3. Upgrade prompt fires but event not tracked
What happensPaywall component added without wiring a PostHog call. upgrade_prompt_shown never fires.
ImpactFunnel shows usage_limit_reached → checkout_initiated with no middle step. Biggest lever in the funnel is unmeasured.
FixPR checklist: every new modal, banner, or in-app prompt must include an analytics event. Make it a required review item.
4. failure_reason is too generic
What happensextraction_failed fires with failure_reason: "error" or not set at all.
ImpactCannot prioritize which failure type to fix. Engineers fix the easiest one, not the most impactful one.
FixEnforce a structured enum: unsupported_format · file_too_large · parse_timeout · low_quality_scan. Any value outside this enum triggers a Slack alert.
5. Signup attribution not captured
What happensUTM params in the URL at signup are not passed to user_signed_up. referral_source is null for a large share of users.
ImpactCannot measure which channels produce users who activate and upgrade. Real pattern: in a subscription product with 50k+ monthly trials, 51% had empty attribution.
FixCapture UTM params at page load, store in session storage, attach server-side when account is created.
Audit approach
Weekly checks in PostHog
VolumeEvent count by day — spikes = duplicates, gaps = instrumentation broke on a deploy.
Integrityextraction_started count must always be ≥ extraction_completed count. If inverted, there is a bug.
CompletenessQuery claim_id IS NULL on upload and extraction events weekly. Any nulls need investigation.
Optimization Opportunities
Five things I'd actually prioritize — where the data points and the fix is concrete.
1. Upgrade prompt fires at the wrong moment
Issue
Prompt shows only at 0 claims remaining. User is blocked and frustrated — abandons rather than upgrades.
Fix
Soft non-blocking banner at 80% usage. Hard prompt at 100%. They still have claims left, so they're thinking, not reacting.
↑ checkout_initiated / upgrade_prompt_shown
2. Extraction failure is a silent churn trigger
Issue
User uploads doc, extraction fails, sees a generic error. No retry nudge, no explanation. They leave.
Fix
On extraction_failed, show in-app message with specific failure_reason + fix tip. Track extraction_retried.
↑ D7 retention for extraction_failed cohort
3. Post-signup blank state kills activation
Issue
After signup, user lands on empty dashboard with no guided action. A lot of them never start a claim.
Fix
Redirect new users directly to claim creation wizard. Consider pre-loading a demo claim for instant extraction preview.
↑ Activation rate (7-day)
4. Upgrade page lacks personalization
Issue
Generic feature comparison table. No reference to what the user has done or what they are about to lose.
Fix
Personalize with user's actual usage: "You've processed X claims — upgrade to keep going." Single recommended plan with clear highlight.
↑ checkout_initiated / prompt_shown
5. Dormant free users get no re-engagement
Issue
Users who signed up, ran 1-2 claims, and went quiet receive nothing. No email trigger, no in-app notification.
Fix
Trigger email at D7 inactivity with a hook specific to where they left off. Use a Flagsmith flag to A/B test email vs in-app nudge.
↑ D30 retention for re-engaged cohort vs control
Experimentation Plan
Two experiments. Both hit the monetization funnel where the biggest drop-offs are.
Experiment 1: Upgrade Prompt Timing
Hypothesis
Showing the upgrade prompt at 80% of the claim limit will increase upgrade conversion. A user with 1-2 claims left still has time to think. A user who just hit zero is frustrated and more likely to close the tab than enter a card.
Setup — Flagsmith + PostHog
flag: upgrade_prompt_timing
variant A (control): at_limit — prompt fires at 100% (current behavior)
variant B (treatment): at_80_percent — soft banner at 80%, hard prompt at 100%
PostHog: upgrade_prompt_shown { prompt_variant } → checkout_initiated → upgrade_completed
Rollout: 50/50 split on new free users only
variant A (control): at_limit — prompt fires at 100% (current behavior)
variant B (treatment): at_80_percent — soft banner at 80%, hard prompt at 100%
PostHog: upgrade_prompt_shown { prompt_variant } → checkout_initiated → upgrade_completed
Rollout: 50/50 split on new free users only
Success metric
Primary: upgrade_completed rate per user who saw upgrade_prompt_shown (treatment vs control, 30-day window).
Secondary: upgrade_prompt_dismissed rate must not increase significantly — would signal early prompt is annoying.
Secondary: upgrade_prompt_dismissed rate must not increase significantly — would signal early prompt is annoying.
Expected: 15-25% lift in upgrade conversion
Experiment 2: Soft Limit with 7-Day Auto-Trial
Hypothesis
When a free user hits the claim limit, giving them 7 days of the paid tier rather than a hard block will increase paid conversion. They get to use what they'd be paying for before deciding. That changes the ask from "trust us" to "you've already seen it."
Setup — Flagsmith + PostHog
flag: limit_behavior
variant A (control): hard_block — user cannot create new claims until upgrade
variant B (treatment): soft_trial_7d — auto-activate paid tier for 7 days on limit_reached
New events: trial_activated · trial_day_3_active · trial_expired
Measure: upgrade_completed within 14d of usage_limit_reached
Anti-gaming: trial tied to email, no second trial on re-signup
variant A (control): hard_block — user cannot create new claims until upgrade
variant B (treatment): soft_trial_7d — auto-activate paid tier for 7 days on limit_reached
New events: trial_activated · trial_day_3_active · trial_expired
Measure: upgrade_completed within 14d of usage_limit_reached
Anti-gaming: trial tied to email, no second trial on re-signup
Success metric
Primary: upgrade_completed within 14 days of limit_reached (treatment vs control).
Secondary: trial_day_3_active rate in treatment — if users don't engage during trial, conversion won't follow.
Secondary: trial_day_3_active rate in treatment — if users don't engage during trial, conversion won't follow.
Expected: 30-40% lift in conversion · Monitor gaming rate (<5% threshold)