You can't improve onboarding without measuring it. Most teams track whether users "completed onboarding" as a single binary event, which tells you almost nothing about where users struggle or what makes successful users different from those who churn. Effective onboarding analytics break the process into stages, define activation clearly, and track the path from install to engaged user.
For improving completion rates, see Improving Onboarding Completion Rates. For click-through optimization, see Click-Through Rate Optimization for Deep Links.
Defining Activation
Activation vs. Onboarding Completion
These are not the same thing:
| Concept | Definition | Example |
|---|---|---|
| Onboarding completion | User finished the setup flow | Saw all screens, created account |
| Activation | User experienced core value | Created first project, made first purchase |
A user can complete onboarding without being activated (they created an account but never used the product). An activated user is far more likely to retain.
Finding Your Activation Event
The activation event is the action most correlated with long-term retention. To find it:
async function findActivationEvent() {
const candidates = [
'first_project_created',
'first_item_saved',
'first_share',
'first_purchase',
'profile_completed',
'first_connection',
'first_content_viewed',
];
for (const event of candidates) {
const usersWhoDidIt = await getUsersWithEvent(event, { within: '7_days' });
const usersWhoDid_not = await getUsersWithoutEvent(event, { within: '7_days' });
const retentionWith = await getDay30Retention(usersWhoDidIt);
const retentionWithout = await getDay30Retention(usersWhoDid_not);
console.log(event, {
retention_with: retentionWith,
retention_without: retentionWithout,
lift: ((retentionWith - retentionWithout) / retentionWithout * 100).toFixed(1) + '%',
});
}
}
The event with the highest retention lift is your activation event. For most apps, it's some form of "user created or experienced the product's core value."
Activation Timing
Track not just whether activation happens, but when:
| Time to Activation | Day 30 Retention |
|---|---|
| Same session (< 10 min) | 45-55% |
| Day 1 | 35-45% |
| Day 2-3 | 25-35% |
| Day 4-7 | 15-25% |
| After Day 7 | 5-10% |
Users who activate in their first session retain at 3-5x the rate of those who take a week.
The Onboarding Funnel
Stage Definitions
const ONBOARDING_FUNNEL = [
{
stage: 'install',
event: 'app_installed',
description: 'App downloaded from store',
},
{
stage: 'first_open',
event: 'app_opened_first_time',
description: 'App opened for the first time',
},
{
stage: 'signup_started',
event: 'signup_form_viewed',
description: 'User saw the signup form',
},
{
stage: 'signup_completed',
event: 'account_created',
description: 'Account successfully created',
},
{
stage: 'setup_completed',
event: 'onboarding_completed',
description: 'Finished all onboarding steps',
},
{
stage: 'activated',
event: 'activation_event',
description: 'Performed the core value action',
},
{
stage: 'retained',
event: 'return_visit_day_7',
description: 'Came back on day 7',
},
];
Funnel Calculation
async function calculateOnboardingFunnel(dateRange, segment) {
const funnel = [];
for (let i = 0; i < ONBOARDING_FUNNEL.length; i++) {
const stage = ONBOARDING_FUNNEL[i];
const users = await countUniqueUsers(stage.event, { dateRange, segment });
const previousUsers = i > 0 ? funnel[i - 1].users : users;
const firstStageUsers = funnel.length > 0 ? funnel[0].users : users;
funnel.push({
stage: stage.stage,
users,
stepConversion: previousUsers > 0 ? (users / previousUsers * 100).toFixed(1) + '%' : '100%',
overallConversion: (users / firstStageUsers * 100).toFixed(1) + '%',
dropOff: previousUsers - users,
});
}
return funnel;
}
Example Funnel Output
| Stage | Users | Step Conversion | Overall | Drop-Off |
|---|---|---|---|---|
| Install | 10,000 | 100% | 100% | 0 |
| First open | 8,500 | 85% | 85% | 1,500 |
| Signup started | 6,200 | 73% | 62% | 2,300 |
| Signup completed | 4,100 | 66% | 41% | 2,100 |
| Setup completed | 3,000 | 73% | 30% | 1,100 |
| Activated | 1,800 | 60% | 18% | 1,200 |
| Retained (D7) | 900 | 50% | 9% | 900 |
This tells you that the biggest absolute drop-offs are between install and first open (1,500), and between first open and signup started (2,300). The biggest percentage drop is between signup started and completed (66%), indicating signup form friction. For strategies to address these drop-offs, see Reducing Onboarding Drop-Off.
Segmented Analysis
By Acquisition Source
async function funnelBySource(dateRange) {
const sources = ['organic', 'referral', 'paid_social', 'paid_search', 'email'];
for (const source of sources) {
const funnel = await calculateOnboardingFunnel(dateRange, { source });
console.log(`\n${source}:`);
for (const stage of funnel) {
console.log(` ${stage.stage}: ${stage.users} (${stage.overallConversion})`);
}
}
}
Expected pattern:
- Referral users: Highest completion and activation rates (social proof + trust)
- Organic search: High intent but moderate completion (no context)
- Paid social: Moderate intent, lower completion (impulse installs)
- Paid search: Higher intent than social, moderate completion
By Platform
iOS and Android users often have different onboarding behavior:
async function funnelByPlatform(dateRange) {
const iosFunnel = await calculateOnboardingFunnel(dateRange, { platform: 'ios' });
const androidFunnel = await calculateOnboardingFunnel(dateRange, { platform: 'android' });
console.log('Stage | iOS | Android');
for (let i = 0; i < iosFunnel.length; i++) {
console.log(
iosFunnel[i].stage,
iosFunnel[i].overallConversion,
androidFunnel[i].overallConversion
);
}
}
By Deep Link Context
Users who arrive via deep links carry context. Measure whether personalized onboarding (using that context) outperforms generic onboarding:
async function funnelByDeepLinkContext(dateRange) {
const withContext = await calculateOnboardingFunnel(dateRange, { hasDeepLinkContext: true });
const withoutContext = await calculateOnboardingFunnel(dateRange, { hasDeepLinkContext: false });
console.log('With deep link context vs. without:');
for (let i = 0; i < withContext.length; i++) {
console.log(
withContext[i].stage,
'With:', withContext[i].overallConversion,
'Without:', withoutContext[i].overallConversion
);
}
}
Time-Based Metrics
Time to Complete Each Step
async function getStepDurations(dateRange) {
const steps = ONBOARDING_FUNNEL.map(s => s.event);
for (let i = 1; i < steps.length; i++) {
const durations = await getTimeBetweenEvents(steps[i - 1], steps[i], dateRange);
console.log(`${steps[i - 1]} -> ${steps[i]}:`, {
median: durations.p50,
p75: durations.p75,
p90: durations.p90,
});
}
}
Long durations between steps indicate friction. If the median time between "signup form viewed" and "account created" is 3 minutes, the form has too many fields or unclear error messages.
Session Analysis
How many sessions does it take to complete onboarding?
| Sessions to Complete | % of Users | Implication |
|---|---|---|
| 1 session | 60% | Healthy (completed in first visit) |
| 2 sessions | 20% | Acceptable (came back to finish) |
| 3+ sessions | 10% | Friction (onboarding is too long) |
| Never | 10% | Lost (need re-engagement) |
Cohort Analysis
Weekly Cohorts
Track onboarding metrics by weekly cohorts to measure improvement over time:
async function onboardingCohorts() {
const weeks = getLastNWeeks(8);
for (const week of weeks) {
const cohortUsers = await getUsersInstalledDuring(week);
const completed = await countActivated(cohortUsers, { within: '7_days' });
const retained = await countRetained(cohortUsers, { day: 30 });
console.log(week.label, {
cohortSize: cohortUsers.length,
activationRate: (completed / cohortUsers.length * 100).toFixed(1) + '%',
day30Retention: (retained / cohortUsers.length * 100).toFixed(1) + '%',
});
}
}
This reveals whether your onboarding changes are actually improving metrics over time.
Event Taxonomy
Standard Events
Use consistent event names across your analytics:
// Onboarding events
analytics.track('onboarding_started', { source, variant });
analytics.track('onboarding_step_viewed', { step, stepIndex, totalSteps, variant });
analytics.track('onboarding_step_completed', { step, timeOnStep, variant });
analytics.track('onboarding_step_skipped', { step, variant });
analytics.track('onboarding_abandoned', { lastStep, totalTime, variant });
analytics.track('onboarding_completed', { totalTime, stepsCompleted, variant });
// Activation events
analytics.track('activation_event', { action, timeFromSignup, source });
// Re-engagement events
analytics.track('onboarding_resumed', { lastStep, daysSinceAbandonment, channel });
analytics.track('onboarding_reminder_sent', { nextStep, channel });
analytics.track('onboarding_reminder_opened', { nextStep, channel });
Properties to Include
Every onboarding event should include:
- userId: For user-level analysis
- variant: If running A/B tests
- source: Acquisition channel (organic, referral, paid, etc.)
- platform: iOS, Android, web
- appVersion: To catch version-specific issues
- timestamp: For time-based analysis
Dashboards
Daily Monitoring
Track these daily to catch problems early:
| Metric | Alert If |
|---|---|
| Signup conversion rate | Drops > 5% from baseline |
| Activation rate (7-day) | Drops > 3% from baseline |
| Median time to activate | Increases > 20% |
| Onboarding error rate | Exceeds 2% |
| Permission grant rate | Drops > 10% |
Weekly Review
| Metric | What to Look For |
|---|---|
| Funnel by source | Are any sources degrading? |
| Cohort activation trends | Is the latest cohort better or worse? |
| Step-level drop-off | Has a specific step gotten worse? |
| A/B test results | Ready to call a winner? (see A/B Testing Onboarding Flows) |
| Re-engagement recovery rate | Are reminders working? |
For analytics features, see Tolinku analytics. For onboarding use cases, see the onboarding documentation. For analytics setup, see the analytics docs.
Get deep linking tips in your inbox
One email per week. No spam.