Event tracking spec

Onboarding v2: Mixpanel events

Audience Product & Engineering
Surface Mobile · React Native
Source onboarding-v2/ANALYTICS.md
Distribution Internal
TL;DR
  • ~37 Mixpanel events under the cur8:onboarding… namespace, re-derived from the registered onboarding-v2 routes.
  • One _pageViewed per screen for the funnel, one _completed per module carrying that module's outputs.
  • Events go through the shared @ifgengineering/client-analytics package. Screens never call mixpanel.track directly.
  • User state lives on the Mixpanel people-profile, set inside the event handler. No super-properties.
  • Quiz failure and the restricted-investor branch sit outside the headline funnel. Track them as separate metrics.

The Mixpanel event tracking spec for the onboarding-v2 flow. Product calls this flow "Onboarding v1.5"; the code lives at cur8-mob/src/screens/onboarding-v2/. They are the same thing.

This doc covers Mixpanel events and people-profile traits only. Customer.io, Avo, Google Ads, session replay and compliance are out of scope here. The older cur8-mob/ANALYTICS_ARCHITECTURE.md (v3.2) is kept as a reference for the wider design, including the deferred navigation-listener and abandonment-tracking work.

onboarding-v2 currently ships with no analytics calls. This doc is the spec for what to add, screen by screen. The screen inventory is taken from cur8-mob/src/navigation/onboardingV2.tsx (the OnboardingV2StackParamList), which is the source of truth.

1. What the events answer

Question Events
Where in onboarding do users drop? every _pageViewed, each _completed, cur8:onboarding_completed
Quiz pass rate cur8:onboardingQuiz_passed vs cur8:onboardingQuiz_failed
Accreditation type mix cur8:onboardingAccreditation_completed.certification
Ineligible / restricted-investor rate cur8:onboardingAccreditationIneligible_pageViewed, _certificationSelected.certification
KYC upload success rate cur8:onboardingVerification_documentUploaded vs _uploadFailed
Attribution source mix profile attribution_source, filtered on cur8:onboarding_completed
Personalisation inputs cur8:onboardingMandatoryQuestion_answered (goals, portfolio size, timeframe)
Quiz failure stage (first-time vs locked-out) cur8:onboardingQuiz_failed.attempt_band
KYC completion by identity branch cur8:onboardingVerification_completed.identity_status
Address-entry friction cur8:onboardingBasicInfo_completed.is_address_autocompleted

Note: profile traits (section 5) attach to every event automatically as Mixpanel "User properties". Any funnel or report can be sliced by them without the event carrying the value itself.

2. How events are wired

Every event goes through the shared @ifgengineering/client-analytics package, the same path every other cur8-mob event already uses. Screens never call mixpanel.track directly.

A screen gets a fire function from the useAnalytics hook and invokes it. The hook returns a promise, so callers await it. Wrap the call so a failure never blocks the user, matching existing screens such as home.tsx.

There are two shapes.

2.1 Page views

Page views reuse the generic pageView event key. Add the event-name string to the cur8PageViewEvents union in packages/client-analytics/src/types.ts, then fire it. The pageView handler attaches app and channel for you, so the call site only passes eventName.

const firePageView = useAnalytics("pageView");
// fire on screen focus:
(await firePageView)({ eventName: "cur8:onboardingPersonalInfo_pageViewed" });

For the three paginated routes (MandatoryQuestion, AccreditationQuestionnaire, QuizQuestion) one route instance serves every question, so the screen does not re-focus per question. Fire those _pageViewed events from a useEffect([questionIndex]), not from focus.

2.2 Action and outcome events

Action and outcome events get their own entry:

  1. In types.ts, add the key to the Event union and a <Name>Props interface.
  2. In packages/client-analytics/src/index.ts, add a case that calls track("cur8:onboarding…_…", { ...args, app: APP_NAME }).
  3. Fire it from the screen:
const fireCertificationSelected = useAnalytics("onboardingCertificationSelected");
(await fireCertificationSelected)({ certification: "HIGH_NET_WORTH" });

People-profile traits are set inside the same case via mixpanel.people?.set, the way the nps_set case already does it (note the optional chaining, the codebase uses it). The fire function is the only thing a screen imports.

track also forwards every event to Customer.io. That fan-out is unconditional in the current package code. It is out of scope for this doc, but be aware new onboarding events reach Customer.io too.

3. Naming

  • Events: cur8:onboarding<Screen>_<action>. cur8: is the team namespace, <Screen> is PascalCase, <action> is a past-tense lowerCamelCase verb (_pageViewed, _completed, _passed, _failed, _documentUploaded, _answered). Module-level events drop the screen: cur8:onboarding_completed.
  • This matches the existing onboarding events already in Mixpanel (cur8:onboardingQuiz_pageViewed, cur8:onboardingBasicInfoPersonal_pageViewed).
  • Event properties: snake_case. Booleans prefix is_ / has_. Counts suffix _count. Timestamps suffix _at (ISO 8601). Banded values suffix _band.

Note: cur8:onboardingQuiz_pageViewed already exists as a v1 event. This spec assumes v2 replaces v1, so it reuses the clean names. If v1 and v2 ever run side by side, reusing a name merges the two in Mixpanel. Namespace the v2 events in that case.

4. Event taxonomy

Re-derived from the registered routes in onboardingV2.tsx. Every screen fires one _pageViewed on focus. Each module fires one _completed on its final screen, carrying that module's captured outputs as properties. No raw PII goes on any event (age_band, not date of birth; source_of_funds option keys, not free text).

_pageViewed events tell you where a user stalled, not why. Within the lean event set there is no "saw the screen but did not act" signal except where an explicit action event exists (_answered, _certificationSelected).

4.1 Entry and Mandatory Questionnaire

Event Properties Trigger
cur8:onboardingWelcomeBridge_pageViewed none Welcome bridge screen appears. Funnel entry.
cur8:onboardingMandatoryQuestion_pageViewed question_index Each questionnaire question appears (paginated route, fires on index change).
cur8:onboardingMandatoryQuestion_answered question_index, question_id, answer_id User selects an option for a question. Captures the personalisation inputs.
cur8:onboardingEmailPreferences_pageViewed none Email preferences screen appears.
cur8:onboardingSocialAttribution_pageViewed none Social attribution screen appears.
cur8:onboardingMandatoryQuestionnaire_completed email_preference, attribution_source Social attribution submitted. The questionnaire batch is POSTed at email preferences. Headline funnel anchor.

email_preference: cur8_capital / cur8_and_ifg / none. attribution_source: google / instagram / tiktok / youtube / friend_family / podcast / ifg / other.

Note: the questionnaire is server-driven (GET /investor/mandatory-questionnaire), so question_id and answer_id are server identifiers. The three questions are investment goals, portfolio size and investment timeframe, but the mapping from question_id to which one is resolved analysis-side from the stable IDs. The question count is not guaranteed to be three.

4.2 Basic Info

Seven screens: one explainer, six single-purpose data screens.

Event Properties Trigger
cur8:onboardingBasicInfoExplainer_pageViewed none Explainer appears.
cur8:onboardingPersonalInfo_pageViewed none Personal info screen appears.
cur8:onboardingResidentialInfo_pageViewed none Residential info screen appears.
cur8:onboardingTaxNINumber_pageViewed none NI number screen appears.
cur8:onboardingTaxResidency_pageViewed none Tax residency screen appears.
cur8:onboardingTaxSourceOfFunds_pageViewed none Source of funds screen appears.
cur8:onboardingTaxOccupation_pageViewed none Occupation screen appears.
cur8:onboardingBasicInfo_completed age_band, country, nationality, is_uk_tax_resident, source_of_funds, occupation, is_address_autocompleted Occupation screen submitted. Headline funnel anchor.

age_band: 18-24 / 25-34 / 35-44 / 45-54 / 55-64 / 65+. Derived from the date of birth held in OnboardingV2Context (captured back on PersonalInfo); the raw DOB never leaves the client. source_of_funds is an array of selected option keys. occupation is the option key, not the display label. is_address_autocompleted is true when the user picked a Google Places suggestion on the address screen rather than typing the address by hand; the flag is carried through OnboardingV2Context from ResidentialInfo.

4.3 Accreditation

Code folder: eligibility/. The flow branches on the chosen certification: self-certified and high-net-worth users continue through the questionnaire and declaration. US residents are offered a US-accredited option instead. Users who pick "none apply" (NONE_APPLY) route to the not-eligible screen.

Event Properties Trigger
cur8:onboardingAccreditationExplainer_pageViewed none Explainer appears.
cur8:onboardingCertification_pageViewed none Certification screen appears.
cur8:onboardingAccreditation_certificationSelected certification User confirms a certification. Sets the investor_certification profile trait.
cur8:onboardingAccreditationQuestionnaire_pageViewed question_index, profile_type Each questionnaire question appears (paginated route, fires on index change). profile_type is the route param; self-certified and HNW users see different questions.
cur8:onboardingAccreditationIneligible_pageViewed profile_type Ineligible screen appears. A genuine drop-off point.
cur8:onboardingAccreditationNotEligible_pageViewed none Not-eligible screen appears.
cur8:onboardingUSAccreditationReason_pageViewed none US accreditation reason screen appears.
cur8:onboardingDeclaration_pageViewed none Declaration screen appears (four risk-acknowledgement checkboxes).
cur8:onboardingAccreditation_completed certification, yes_answer_count Declaration submitted. Headline funnel anchor.

certification: SELF_CERTIFIED / HIGH_NET_WORTH / US_ACCREDITED / NONE_APPLY. NONE_APPLY appears only on _certificationSelected; those users route to the not-eligible screen and never reach _completed. profile_type: SELF_CERTIFIED / HIGH_NET_WORTH / US_ACCREDITED. yes_answer_count is how many questionnaire questions were answered Yes (the questionnaire content itself is not captured).

4.4 Quiz

FCA appropriateness check. On submit the quiz navigates to a dedicated QuizResult screen, which receives failed and an optional blockedUntil and shows a live countdown when the user is rate-limited.

Event Properties Trigger
cur8:onboardingQuizExplainer_pageViewed none Explainer appears.
cur8:onboardingQuiz_pageViewed question_index Each quiz question appears (paginated route, fires on index change).
cur8:onboardingQuiz_passed none QuizResult opens with failed=false. Headline funnel anchor. Sets quiz_passed_at.
cur8:onboardingQuiz_failed blocked_until, attempt_band QuizResult opens with failed=true. Fires on any non-passing result.

blocked_until is the ISO 8601 rate-limit timestamp, or omitted when the user is not rate-limited (the quiz API returns { failed, blockedUntil? }). attempt_band is first_fail / second_or_third_fail / fourth_plus_fail, derived client-side from blocked_until against the server's fixed block ladder (fail 1 = no block, fails 2 to 3 = 1-day block, fails 4+ = 7-day block). It separates first-time failures from hard-stuck repeat users.

Note: the quiz API returns only pass/fail and the rate-limit window, not a score, and the client cannot grade the answers. attempt_band is the closest available proxy. The clean fix is for the backend to return attemptCount on the quiz response (it already computes it); track that as a follow-up.

4.5 Verification (KYC)

The verification flow branches on the user's identity status: most users go straight to document upload, some are routed to an identity scan, blacklisted users hit the blocked screen.

Event Properties Trigger
cur8:onboardingVerificationExplainer_pageViewed none Verification explainer appears.
cur8:onboardingVerificationIntro_pageViewed none Requirements intro screen appears.
cur8:onboardingIdentityScan_pageViewed none Identity scan screen appears (users who need an ID scan).
cur8:onboardingVerificationBlocked_pageViewed none Blocked screen appears (blacklisted users).
cur8:onboardingProofOfAddress_pageViewed none Proof of address screen appears.
cur8:onboardingSourceOfFunds_pageViewed none Source of funds screen appears.
cur8:onboardingVerificationStatus_pageViewed none Verification status screen appears.
cur8:onboardingVerification_documentUploaded doc_type, file_count The document set for one doc_type uploads successfully. Uploads are batched at the CTA, not per file. No filenames.
cur8:onboardingVerification_uploadFailed doc_type, failure_reason A document upload batch errors.
cur8:onboardingVerification_completed document_count, identity_status Verification status submitted (descriptions PUT, KYC marked submitted). Headline funnel anchor.

doc_type: proof_of_address / source_of_funds. identity_status: needs_scan / grey_list. The client-side KYC branch hint; it is never sent to the backend, so this event is the only place the grey-list versus standard-KYC split is observable. failure_reason: file_too_large / unsupported_format / network_error / server_error / other. Map the upload pipeline's real error to this closed set at emission; never pass a raw exception string.

4.6 Completion and cross-flow

Event Properties Trigger
cur8:onboardingCompletion_pageViewed none Completion splash appears.
cur8:onboarding_completed none POST /onboarding/completion succeeds. Terminal funnel event.
cur8:onboarding_backPressed from_screen, to_screen Back button pressed inside onboarding.
cur8:onboarding_helpTapped screen, help_target A help, info or support affordance is tapped.

5. People-profile traits

User state lives on the Mixpanel people-profile, not on events. Each trait is set with mixpanel.people?.set inside the event handler named below, alongside the track call.

Trait Set by
email_preference cur8:onboardingMandatoryQuestionnaire_completed
attribution_source cur8:onboardingMandatoryQuestionnaire_completed
age_band cur8:onboardingBasicInfo_completed
country cur8:onboardingBasicInfo_completed
nationality cur8:onboardingBasicInfo_completed
is_uk_tax_resident cur8:onboardingBasicInfo_completed
occupation cur8:onboardingBasicInfo_completed
investor_certification cur8:onboardingAccreditation_certificationSelected
quiz_passed_at cur8:onboardingQuiz_passed
kyc_status cur8:onboardingVerification_completed
onboarding_completed_at cur8:onboarding_completed

Note: onboarding-v2 runs post-signup. The user is already identified at signup (email-keyed, via safeMixpanelIdentify in the analytics package). onboarding-v2 fires no identify, alias or reset calls. Mixpanel's mobile SDK auto-properties ($os, $app_version_string, $device, and so on) are free; do not duplicate them.

6. Headline funnel

Step Event
1 cur8:onboardingWelcomeBridge_pageViewed
2 cur8:onboardingMandatoryQuestionnaire_completed
3 cur8:onboardingBasicInfo_completed
4 cur8:onboardingAccreditation_completed
5 cur8:onboardingQuiz_passed
6 cur8:onboardingVerification_completed
7 cur8:onboarding_completed

Slice by profile properties country, attribution_source, investor_certification, email_preference. Per-screen _pageViewed events give the within-module drop-off.

Caution: the funnel hides quiz failure. A user who fails the quiz and never passes simply does not reach step 5, with no signal distinguishing a failed quiz from an abandoned explainer. Track quiz fail rate separately as cur8:onboardingQuiz_failed count against cur8:onboardingQuiz_passed. The restricted-investor branch (step 4) is similar: UNACCREDITED users who stop at the ineligible screen need cur8:onboardingAccreditationIneligible_pageViewed, not the funnel, to be counted.

7. Not covered here

Deliberately cut from the v3.2 architecture doc for this simplified spec:

  • Abandonment events (_screenAbandoned, _abandoned, _attemptAbandoned, _docAbandoned) and the navigation-state listener / onboarding_session_id subsystem they need. Drop-off is read from where users stop in the _pageViewed funnel.
  • active_*_ms timing properties. Mixpanel derives time-between-steps from event timestamps; foreground-pause timing has no precedent in the codebase.
  • Per-event metadata ($insert_id, experiment_variant, flag_resolution) and the experiment_exposed event.
  • Customer.io, Avo, Google Ads, session replay, PII rules and compliance.

See cur8-mob/ANALYTICS_ARCHITECTURE.md for the fuller v3.2 design if any of the above is revived.