The Movement Athlete · Native App Analytics — Companion Reference

Event Catalog & Wiring Spec

The good news up front: TMA's product-event taxonomy is already fully designed — 76 custom events named and structured in Amplitude. The native app just isn't firing most of them. So your job is not to design events; it's to make these specific, already-named events fire at the right moments. This is the exact target list, pulled live from Amplitude, grouped by area, with a "fire when" for each.

✅ Web conversion wiring shipped 21 Jul 2026 (Nic): Google Ads Lead (generate_lead GTM tag) + Purchase with value/currency/transaction_id (both new & legacy checkouts, via Custom-JS), and a server-side Stripe → GA4 + Meta CAPI webhook (/payments/analytics/stripe-purchase/). This catalog is the native-app contract; the web/checkout conversion contract + full end-to-end architecture now live in docs/company/ANALYTICS/TRACKING-SYSTEM/.

Amplitude API snapshot 7 Jul 2026 (event NAMES are the locked contract — the fire-count columns are that day's 30-day totals, historical) · auto-synced 12 Aug 2026 · companion to the Native App Analytics Dev Brief
76
Custom events defined
23
Currently firing
53
Defined but DORMANT
100% web
Current source (no native)

0How to use this catalog

Source-prefix naming convention (Nic · 9 Jul 2026)

Every event carries its source in its name: rc_* = RevenueCat (e.g. rc_initial_purchase) · af_* = AppsFlyer (e.g. af_purchase) · unprefixed = web/GTM (e.g. purchase). Amplitude's AI-merge recommendation then unifies same-meaning events across sources (e.g. af_purchase + purchase + rc_initial_purchase → merged Purchase) — analysis uses the merged event; the raw prefixed events stay unmerged for audit. Status at the 9 Jul API check: af_* confirmed flowing (4 events); rc_* not yet visible in the project taxonomy — the RC→Amplitude wire counts as done when rc_* events appear and reconcile within 5% of the RevenueCat dashboard.

⚠️ Scope note

Some events (begin_checkout, add_to_cart, generate_lead, quiz_*, subscription_checkout_*) belong to the web checkout/quiz surface and are the internal team's job — they're marked WEB and listed for context only. Your focus is the native rows: auth, onboarding/assessment, workout, today-screen, profile, and the AppsFlyer conversion events.

1💰 Conversion & money — P0 (Task T5)

⭐ THE GATE METRIC = Lead → Paid (not "trial → paid") — read before building the money events

We do not have a normal trial→paid funnel. A lead becomes a paying customer at the paywall, on one of two paths, and the meter must count both as "a new paying customer":

  1. Carded free trial — card captured up front, 7 days, auto-charged unless cancelled. Trial-start already IS the purchase (money captured; ~50% simply don't cancel — a retention signal, not the acquisition rate).
  2. Direct discounted purchase — a large share of leads take a ~50%-off offer and pay immediately, no trial. These buyers never appear as "trials" — so a trial-only meter is blind to them.

Build ONE event for both pathsnew_paying_customer (or reuse purchase) fired on BOTH the carded trial-start AND the direct discounted purchase, with:

PropertyValues / note
pathtrial_start | direct_purchase
source / UTMchannel that drove them — makes it attributable
planmonthly | quarterly | annual
valueamount charged
transaction_idStripe / RevenueCat id — dedupe key (kills the 1.52× double-fire for the GA4→Ads import, Task 3)

Why it matters for paid (Task 3): import THIS event (trial-start OR direct purchase), deduped on transaction_id. If we import only trial_started, the ad algorithm optimises toward trials and goes blind to the direct-purchase buyers — a big share of real revenue. Order: behind Task 19 (can't fire a clean purchase event through a checkout that isn't working yet). Source: MESSAGE-TO-NIC-FUNNEL-CORRECTION-2026-07-16.md · §⑮/§⑰ of the $15K doc.

These make paid acquisition measurable. Drive them off the RevenueCat purchase callback so they can't drift from the source of truth. Include revenue + currency on every one.

Event30dStatusFire whenKey props
af_start_trial0DORMANTRevenueCat reports a trial startedcurrency, product_id
af_purchase0DORMANTFirst paid conversion (trial→paid or direct)revenue, currency, product_id
af_subscribe0DORMANTSubscription renewal / active subrevenue, currency, product_id
payment_initiated4PARTIALUser taps buy / enters paymentproduct_id, price
purchase10PARTIALGeneric purchase confirmedrevenue, currency
Web checkout (internal team — context only)
subscription_checkout_screen_opened0WEBPaywall/checkout screen shown
subscription_checkout_confirmed0WEBCheckout confirmed
subscription_billing_portal_failed0WEBBilling portal error
Recommended pattern — one source, three sinks
// On the RevenueCat purchase/trial callback, fan out to all trackers.
// This guarantees AppsFlyer + Amplitude never disagree with RevenueCat.
function onRcEntitlementChange(info) {
  if (info.isTrial)   { appsFlyer.logEvent('af_start_trial', { af_currency }); }
  if (info.isNewPaid) { appsFlyer.logEvent('af_purchase', { af_revenue, af_currency });
                        amplitude.track('purchase', { revenue, currency, product_id }); }
  if (info.isRenewal) { appsFlyer.logEvent('af_subscribe', { af_revenue, af_currency }); }
}

2🔑 Auth & identity — P0 (anchors Task T4)

These are where you set the canonical user_id (§6). auth_register_succeeded and auth_login_succeeded are the moments identity gets established — wire identity here first, then everything downstream attributes.

Event30dStatusFire when
auth_register_succeeded0DORMANTNew account created → SET user_id on all SDKs here
auth_login_succeeded3PARTIALLogin success → SET user_id on all SDKs here
auth_session_started3PARTIALApp session begins for a logged-in user
auth_register_failed0DORMANTRegistration error (capture reason prop)
auth_login_failed0DORMANTLogin error (capture reason prop)
auth_logout0DORMANTLogout → clear identity on all SDKs
auth_forgot_password_requested0DORMANTPassword reset requested
auth_mode_switched0DORMANTToggled login ↔ register

3🏋️ Workout lifecycle — P1 (Task T7 · retention core)

This is the behaviour that drives retention (the #1 revenue leak). The full workout journey is defined but silent — wiring it unlocks D1/D7/D30 retention and activation analysis.

Event30dStatusFire whenKey props
workout_started1PARTIALUser starts a workoutworkout_id, level
workout_session_started1PARTIALSession (day) beginssession_id
workout_completed0DORMANTWorkout finished — key activation eventworkout_id, duration
workout_session_completed0DORMANTFull session completedsession_id, exercises_done
rpe_submitted1PARTIALUser rates exertion (RPE)value, exercise_id
workout_session_feedback1PARTIALPost-session feedback givenrating
workout_swap_confirmed0DORMANTUser swaps an exercisefrom_id, to_id
workout_prep_opened / _closed0DORMANTPre-workout review screen open/close
workout_player_leave_to_plan0DORMANTLeft the workout player (drop-off signal)at_exercise
workout_recap_share0DORMANTShared a workout recap
workout_cantdo_pain_chain0DORMANT"Can't do / pain" flow triggeredexercise_id
workout_start_l2_gated0DORMANTWorkout start blocked by L2 gate

4🧭 Onboarding & assessment — P1 (Task T7 · activation)

Event30dStatusFire when
mini_onboard_step2PARTIALEach mini-onboarding step viewed
mini_onboard_pick0DORMANTUser makes an onboarding choice
mini_onboard_assessment0DORMANTOnboarding assessment step
mini_onboard_completed1PARTIALOnboarding finished — key activation event
advanced_assessment_started0DORMANTAdvanced (L2) assessment begins
advanced_assessment_completed1PARTIALAdvanced assessment finished
advanced_assessment_exited0DORMANTAbandoned mid-assessment (drop-off)
advanced_assessment_saved1PARTIALAssessment results saved
advanced_assessment_goal_recommended1PARTIALEngine recommends a goal
placement_feedback_submitted0DORMANTUser confirms/adjusts placement
profile_leverage_submitted0DORMANTLeverage/profile data submitted
engine_decision1PARTIALPersonalisation engine makes a decision
training_disclaimer_acknowledged0DORMANTSafety disclaimer accepted

5🏠 Today screen, profile & settings — P2 (depth)

Engagement depth on the home screen and profile. Wire after the P0/P1 sets are proven. The today_* family is a rich home-screen interaction map — useful for engagement analysis but not blocking the core funnel.

Event groupEvents (all currently DORMANT unless noted)Fire when
Today — starttoday_start_workout_clicked, today_choose_workout_clicked, today_quick_ten_clicked, today_lets_go_clicked, today_open_workout_tab, today_start_workout_l2_gatedUser taps a start-workout entry point on the home screen
Today — exploretoday_skill_row_clicked, today_build_goal_clicked, today_plan_section_toggled, today_see_all_achievements, today_continue_assessment_in_app_onboarding 2User explores plan/skills/achievements
Today — sharetoday_share_levels_open, today_share_trophy_open, today_share_start_point_open, today_l2_nudge_dismissedUser opens a share sheet or dismisses a nudge
Profile / bodybody_status_saved, body_metrics_unit_toggled, injury_profile_saved, profile_save_succeeded 1User edits body metrics / injury profile
Settingssettings_save_succeeded, settings_save_failed, settings_achievements_view, settings_subpage_opened 2Settings interactions
Nav / misctab_switched 9, tab_viewed 22, notifications_bell_clicked, referral_attribution_touchpoint_capturedTab navigation; referral touchpoint captured

6🔑 The canonical user_id — the keystone (Task T4)

Everything above is only attributable if it carries one consistent identity. This is the single most important decision in the project — agree it with TMA before wiring, so the web side threads the same value.

The identity rule

Pick ONE canonical id and set it on all three native SDKs at every auth point (register + login), before any purchase or event fires. Recommended: the authenticated app user id; pre-auth, fall back to a lowercased-email hash so a quiz lead can later resolve to the subscriber.

// Set at auth_register_succeeded AND auth_login_succeeded:
await Purchases.logIn(APP_USER_ID);            // RevenueCat appUserID
amplitude.setUserId(APP_USER_ID);              // Amplitude
appsFlyer.setCustomerUserId(APP_USER_ID);      // AppsFlyer

// On auth_logout, reset all three:
await Purchases.logOut();
amplitude.reset();
appsFlyer.setCustomerUserId(null);

// Decisions to confirm with TMA before you start:
//  1. What IS the canonical id?  (app user id recommended)
//  2. Same id used by the WEB checkout so web lead == app subscriber?
//  3. Pre-auth fallback (email hash) — needed to attribute quiz leads?

7Definition of done (per group)

GroupDone when Claude confirms via API…
Conversion (P0)af_start_trial + af_purchase + af_subscribe fire at volumes matching RevenueCat's real trials/purchases
Auth/identity (P0)a test user resolves to ONE profile across RevenueCat + Amplitude + AppsFlyer with purchase attached
Workout (P1)workout_started → workout_completed fire from real devices with props; a retention curve can be read
Onboarding (P1)mini_onboard_completed + advanced_assessment_completed fire; activation funnel is visible
Today/profile (P2)the today_* and profile events fire on interaction (engagement depth available)