feat: add canonical onboarding funnel events for Mixpanel#2000
feat: add canonical onboarding funnel events for Mixpanel#2000
Conversation
Introduces a thin canonical event layer on top of the existing diagnostic events so the Mixpanel onboarding funnel can measure drop-off with statistical confidence. The ~200 pre-existing events remain untouched. Canonical events fire at most once per attempt, guarded by a shared helper in mobile-sdk-alpha rather than component lifecycle — back-nav no longer re-fires step events. Every canonical event carries a `branch` property (biometric_passport | biometric_id | kyc | aadhaar) so a single macro funnel and per-branch diagnostic funnels can both be built from one stream. Terminal invariant: `onboarding_completed` fires only when the proving machine reaches `completed` via a new registration proof this session (tracked via `didNewRegistrationProof`). The `ALREADY_REGISTERED` shortcut and any `disclose` flow do not trigger it — protects the funnel's terminal event from disclosure-later pollution. Also fixes the AbstractButton `Click:` prefix for canonical events via a new `trackEventRaw` prop (existing `trackEvent` consumers unchanged), and instruments the four dead-zone screens (LogoConfirmation, CountryPicker, IDSelection, registration fallbacks). Spec: specs/projects/sdk/workstreams/analytics/SPEC.md Plan: specs/projects/sdk/workstreams/analytics/plans/ANA-01-canonical-onboarding-funnel.md Deferred to follow-up issues (ANA-02/03/04): - TestFlight / internal-build contamination filtering - Raw-string event cleanup (REGISTRATION_FALLBACK_*, DEVICE_TOKEN_REG_*) - Mixpanel NFC native channel vs Segment consolidation Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR introduces a comprehensive onboarding funnel analytics system across the mobile SDK and application. It adds a new state-machine-based onboarding attempt tracker that records canonical funnel events (started, country/document selection, scan progression, proof generation, completion), integrates this tracking into existing screens and flows, and extends the SDK's public API with onboarding types and control functions. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/mobile-sdk-alpha/src/proving/provingMachine.ts (1)
532-555:⚠️ Potential issue | 🟠 MajorTerminalize DSC registration proof failures in the onboarding funnel.
The DSC leg is handled as a registration-flow error elsewhere (
circuitType !== 'disclose'), but these failure branches only callfailOnboardingAttempt()forregister. If the DSC proof fails before switching to register, the onboarding attempt remains active with no failed terminal event.Proposed fix
- } else if (get().circuitType === 'register') { + } else if (get().circuitType === 'register' || get().circuitType === 'dsc') { failOnboardingAttempt(selfClient, 'proof_generation_started', reason ?? error_code ?? 'proof_failure', { recoverable: false, }); } @@ - } else if (get().circuitType === 'register') { + } else if (get().circuitType === 'register' || get().circuitType === 'dsc') { failOnboardingAttempt(selfClient, 'proof_generation_started', get().reason ?? get().error_code ?? 'error', { recoverable: true, }); }
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 80621fbc-d783-41b9-b5b2-52901e031aeb
📒 Files selected for processing (19)
app/src/screens/documents/aadhaar/AadhaarUploadScreen.tsxapp/src/screens/documents/management/ManageDocumentsScreen.tsxapp/src/screens/documents/scanning/DocumentCameraScreen.tsxapp/src/screens/documents/scanning/DocumentNFCScanScreen.tsxapp/src/screens/documents/scanning/RegistrationFallbackMRZScreen.tsxapp/src/screens/documents/scanning/RegistrationFallbackNFCScreen.tsxapp/src/screens/documents/selection/LogoConfirmationScreen.tsxapp/src/screens/onboarding/DisclaimerScreen.tsxpackages/mobile-sdk-alpha/src/analytics/onboardingFunnel.tspackages/mobile-sdk-alpha/src/components/buttons/AbstractButton.tsxpackages/mobile-sdk-alpha/src/constants/analytics.tspackages/mobile-sdk-alpha/src/flows/onboarding/country-picker-screen.tsxpackages/mobile-sdk-alpha/src/flows/onboarding/id-selection-screen.tsxpackages/mobile-sdk-alpha/src/index.tspackages/mobile-sdk-alpha/src/proving/provingMachine.tspackages/mobile-sdk-alpha/tests/analytics/onboardingFunnel.test.tsspecs/projects/sdk/INDEX.mdspecs/projects/sdk/workstreams/analytics/SPEC.mdspecs/projects/sdk/workstreams/analytics/plans/ANA-01-canonical-onboarding-funnel.md
| trackOnboardingStep(selfClient, OnboardingEvents.SCAN_SUCCEEDED, { | ||
| branch: resolveOnboardingBranch(documentType ?? 'p'), | ||
| duration_seconds: parseFloat(scanDurationSeconds), | ||
| }); |
There was a problem hiding this comment.
Move the canonical scan success event after the scan is committed.
document_scan_succeeded fires before parseScanResponse(scanResponse) and storePassportData(passportData). If parsing or storage fails, this attempt is still counted as a successful scan and the fire-once guard can block the real success on retry. Emit this only after the passport data has been parsed and stored successfully.
🐛 Proposed fix
- trackOnboardingStep(selfClient, OnboardingEvents.SCAN_SUCCEEDED, {
- branch: resolveOnboardingBranch(documentType ?? 'p'),
- duration_seconds: parseFloat(scanDurationSeconds),
- });
logNFCEvent(
'info',
'scan_success',
@@
if (passportData) {
console.log('Storing passport data from NFC scan...');
await storePassportData(passportData);
console.log('Passport data stored successfully');
+ trackOnboardingStep(selfClient, OnboardingEvents.SCAN_SUCCEEDED, {
+ branch: resolveOnboardingBranch(documentType ?? 'p'),
+ duration_seconds: parseFloat(scanDurationSeconds),
+ });
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| trackOnboardingStep(selfClient, OnboardingEvents.SCAN_SUCCEEDED, { | |
| branch: resolveOnboardingBranch(documentType ?? 'p'), | |
| duration_seconds: parseFloat(scanDurationSeconds), | |
| }); | |
| logNFCEvent( | |
| 'info', | |
| 'scan_success', | |
| ); | |
| const passportData = parseScanResponse(scanResponse); | |
| if (passportData) { | |
| console.log('Storing passport data from NFC scan...'); | |
| await storePassportData(passportData); | |
| console.log('Passport data stored successfully'); | |
| trackOnboardingStep(selfClient, OnboardingEvents.SCAN_SUCCEEDED, { | |
| branch: resolveOnboardingBranch(documentType ?? 'p'), | |
| duration_seconds: parseFloat(scanDurationSeconds), | |
| }); | |
| } |
| function ensureAttempt(): OnboardingAttempt { | ||
| if (!currentAttempt) { | ||
| currentAttempt = { | ||
| id: uuid(), | ||
| branch: 'pending', | ||
| startedAt: Date.now(), | ||
| firedSteps: new Set(), | ||
| retryCounts: {}, | ||
| }; | ||
| } | ||
| return currentAttempt; | ||
| } |
There was a problem hiding this comment.
Emit STARTED when bootstrapping a deep-link attempt.
ensureAttempt() creates an attempt silently, so a deep-link entry can emit COUNTRY_SELECTED, STEP_RETRIED, or later proof events with an attempt_id but no matching Onboarding: Started. That undercounts top-of-funnel starts and leaves the attempt lifecycle inconsistent.
Proposed fix
-function ensureAttempt(): OnboardingAttempt {
+function ensureAttempt(selfClient: Pick<SelfClient, 'trackEvent'>): OnboardingAttempt {
if (!currentAttempt) {
currentAttempt = {
id: uuid(),
branch: 'pending',
startedAt: Date.now(),
firedSteps: new Set(),
retryCounts: {},
};
+ currentAttempt.firedSteps.add(OnboardingEvents.STARTED);
+ selfClient.trackEvent(OnboardingEvents.STARTED, baseProperties(currentAttempt));
}
return currentAttempt;
}
@@
- const attempt = ensureAttempt();
+ const attempt = ensureAttempt(selfClient);
@@
- const attempt = ensureAttempt();
+ const attempt = ensureAttempt(selfClient);Also applies to: 205-219, 230-254
| if (state.value === 'proving') { | ||
| // Canonical funnel: fire-once per attempt. `trackOnboardingStep` | ||
| // dedupes, so transient re-entry into `proving` (e.g. via reconnect) | ||
| // does not re-emit. | ||
| trackOnboardingStep(selfClient, OnboardingEvents.PROOF_STARTED); | ||
| } |
There was a problem hiding this comment.
Gate canonical onboarding proof-start events away from disclosure flows.
Line 463 runs for disclose too. Since trackOnboardingStep() bootstraps an onboarding attempt and the disclosure completion path only sends DISCLOSURE_COMPLETED, disclosure proofs can leave a stale onboarding attempt open and pollute the next funnel.
Proposed fix
- if (state.value === 'proving') {
+ if (state.value === 'proving' && get().circuitType !== 'disclose') {
// Canonical funnel: fire-once per attempt. `trackOnboardingStep`
// dedupes, so transient re-entry into `proving` (e.g. via reconnect)
// does not re-emit.
trackOnboardingStep(selfClient, OnboardingEvents.PROOF_STARTED);
}
Greptile SummaryThis PR introduces a thin canonical onboarding funnel layer on top of the existing ~200 diagnostic events, enabling Mixpanel to measure drop-off with statistical confidence. The new Key changes:
Confidence Score: 4/5Safe to merge; core funnel invariants and deduplication are correct. One concrete data-quality fix remains for the biometric-failure → KYC recovery cohort. The architecture is sound — the lazy-bootstrap pattern resolves the double-STARTED issue from the previous review thread, the terminal invariant is correctly guarded by didNewRegistrationProof, and the unit test suite is comprehensive. The single remaining issue (missing SCAN_SUCCEEDED in both RegistrationFallback screens' KYC paths) is a real data gap that will produce misleading drop-off metrics for that cohort, but does not break user flows or cause data loss. app/src/screens/documents/scanning/RegistrationFallbackMRZScreen.tsx and RegistrationFallbackNFCScreen.tsx — handleTryAlternative needs SCAN_SUCCEEDED tracking after successful KYC. Important Files Changed
|
| // SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc. | ||
| // SPDX-License-Identifier: BUSL-1.1 | ||
| // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. | ||
|
|
||
| import { beforeEach, describe, expect, it, vi } from 'vitest'; |
There was a problem hiding this comment.
Missing terminal invariant tests for the proving machine
The spec (SPEC.md / ANA-01-canonical-onboarding-funnel.md) explicitly requires:
Integration test for the proving-machine terminal invariant (fires on new registration, does not fire on
ALREADY_REGISTERED, does not fire ondisclose).
The existing test file covers onboardingFunnel.ts unit behaviour thoroughly, but there are no tests verifying the didNewRegistrationProof gate in provingMachine.ts. The spec mentions these should live at packages/mobile-sdk-alpha/src/proving/__tests__/provingMachine.analytics.test.ts.
The invariant is the most critical correctness guarantee of the PR — if completeOnboardingAttempt fires on the ALREADY_REGISTERED shortcut or on a disclose flow, the onboarding_completed event would be permanently polluted with non-registration completions. Without automated regression coverage this invariant is easy to accidentally break in a future proving-machine refactor.
…backs Splits the single `branch` property into `initial_branch` (user's original intent, immutable for the attempt) and `current_branch` (updated when the user falls back from biometric to KYC mid-flow via setOnboardingBranch). Terminal events additionally stamp `used_fallback: boolean`. Without this split, a user who starts biometric and falls back to KYC gets later events stamped with branch=kyc, breaking both the biometric funnel (appears as a drop-off at scan) and the KYC funnel (appears as an entry without document_type_selected). With the split: initial_branch = biometric_passport → "what did the user intend" current_branch = kyc → "what did they actually complete" used_fallback = true → easy cohort for fallback analysis Also documents the three-layer event model (canonical funnel, canonical decision events, diagnostic) and adds six new backlog issues covering the gap between "measurable funnel" and "Revolut-grade funnel": ANA-05 fallback decision events (Layer 2) — next priority after ANA-01 ANA-06 super-property enrichment (device, OS, version, channel) ANA-07 step-view vs step-commit mini-funnels ANA-08 abandonment events on app-background ANA-09 A/B test tagging at the super-property layer ANA-10 PM dashboard roll-ups (D1/D7/D30 conversion, TTV, top drops) Spec: SPEC.md § Cross-branch flows, § What This Doesn't Measure Yet Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…boarding-funnel # Conflicts: # app/src/screens/documents/scanning/DocumentCameraScreen.tsx # packages/mobile-sdk-alpha/src/proving/provingMachine.ts
…er-entry Moves the STARTED emission into the funnel helper's `ensureAttempt` bootstrap so it fires exactly once per attempt at the first canonical step event (typically `country_selected`), regardless of which entry path the user took. Deletes the two explicit `startOnboardingAttempt` call sites. Closes the P1 Greptile finding on PR #2000. Before this change: First-time via Home EmptyIdCard 1 STARTED ✓ First-time via ManageDocuments 2 STARTED ✗ (Greptile bug) Returning via HomeNavBar + 0 STARTED ✗ (silent bootstrap) Returning via ManageDocuments 1 STARTED ✓ Recovery / RecoverWithPhrase 0 STARTED ✗ CloudBackupScreen "Register now" 0 STARTED ✗ After: Every entry path 1 STARTED ✓ (by construction) Removes the public `startOnboardingAttempt` API entirely — no callers needed it once the bootstrap carries the emission. The dual-creation- paths footgun is gone. Trade-off accepted: `duration_seconds` on `onboarding_completed` now measures from the first canonical step (typically country_selected) rather than from privacy-disclaimer dismiss. The disclaimer is a one-time legal gate unrelated to registration effort, and a "dismissed-but-didn't-start" signal belongs in an app-engagement funnel, not the registration funnel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces a thin canonical event layer on top of the existing diagnostic events so the Mixpanel onboarding funnel can measure drop-off with statistical confidence. The ~200 pre-existing events remain untouched.
Canonical events fire at most once per attempt, guarded by a shared helper in mobile-sdk-alpha rather than component lifecycle — back-nav no longer re-fires step events. Every canonical event carries a
branchproperty (biometric_passport | biometric_id | kyc | aadhaar) so a single macro funnel and per-branch diagnostic funnels can both be built from one stream.Terminal invariant:
onboarding_completedfires only when the proving machine reachescompletedvia a new registration proof this session (tracked viadidNewRegistrationProof). TheALREADY_REGISTEREDshortcut and anydiscloseflow do not trigger it — protects the funnel's terminal event from disclosure-later pollution.Also fixes the AbstractButton
Click:prefix for canonical events via a newtrackEventRawprop (existingtrackEventconsumers unchanged), and instruments the four dead-zone screens (LogoConfirmation, CountryPicker, IDSelection, registration fallbacks).Spec: specs/projects/sdk/workstreams/analytics/SPEC.md
Plan: specs/projects/sdk/workstreams/analytics/plans/ANA-01-canonical-onboarding-funnel.md
Deferred to follow-up issues (ANA-02/03/04):
Summary
Test plan
Native Consolidation Checklist
cd app && yarn jest:run/yarn workspace @selfxyz/rn-sdk-test-app test)Summary by CodeRabbit
Chores
Tests
Documentation