Users who arrive via deep links have context: they clicked a specific link, from a specific source, expecting specific content. Their onboarding should reflect that context. A/B testing onboarding flows for deep-linked users is high-leverage because these users have demonstrated intent, and the gap between "interested" and "activated" is where most of them are lost.
For onboarding fundamentals, see User Onboarding with Deep Links: Complete Guide. For onboarding A/B testing patterns, see A/B Testing Onboarding Flows.
The A/B test creation form with test type, goal metric, route picker, and variant builder.
What to Test
High-Impact Onboarding Variables
| Variable | Expected Impact | Test Difficulty |
|---|---|---|
| Number of steps (2 vs. 3 vs. 4) | 10-20% completion lift | Easy |
| Signup method order (social vs. email) | 5-15% signup lift | Easy |
| Personalized vs. generic welcome | 10-25% completion lift | Medium |
| Content-first vs. auth-first | 15-35% activation lift | Medium |
| Permission request timing | 15-30% grant rate lift | Easy |
| Skip option availability | 5-15% activation lift | Easy |
| First action type | 10-20% D7 retention lift | Medium |
Deep Link-Specific Variables
These tests only apply to users arriving via deep links:
| Variable | What You're Testing |
|---|---|
| Show deferred content immediately vs. after signup | Does seeing the content increase signup rate? |
| Personalized welcome vs. standard | Does referrer context improve completion? |
| Skip onboarding entirely vs. minimal flow | For returning users with the app reinstalled |
| Pre-filled data vs. empty forms | Does pre-filling from deep link params reduce friction? |
Implementation
Experiment Configuration
const onboardingExperiments = {
step_count_v3: {
id: 'step_count_v3',
variants: [
{
id: 'two_steps',
weight: 50,
config: {
steps: ['signup', 'first_action'],
skipProfileSetup: true,
skipInterests: true,
},
},
{
id: 'three_steps',
weight: 50,
config: {
steps: ['signup', 'profile', 'first_action'],
skipProfileSetup: false,
skipInterests: true,
},
},
],
primaryMetric: 'activation', // Core action completed
secondaryMetrics: ['d1_retention', 'd7_retention', 'onboarding_completion'],
segmentBy: ['source', 'has_deferred_link'],
},
auth_method_v2: {
id: 'auth_method_v2',
variants: [
{
id: 'social_first',
weight: 50,
config: {
primaryAuth: ['apple', 'google'],
secondaryAuth: ['email'],
showPasskey: true,
},
},
{
id: 'email_first',
weight: 50,
config: {
primaryAuth: ['email'],
secondaryAuth: ['apple', 'google'],
showPasskey: true,
},
},
],
primaryMetric: 'signup_completed',
secondaryMetrics: ['time_to_signup', 'drop_off_step'],
},
};
Deep Link-Aware Onboarding Router
async function routeOnboarding(userId) {
const deferredLink = await checkDeferredDeepLink();
const experiments = getActiveOnboardingExperiments();
// Assign variants for all active experiments
const assignments = {};
for (const exp of experiments) {
assignments[exp.id] = assignVariant(userId, exp.id);
}
// Build onboarding config based on all assignments
const config = mergeExperimentConfigs(assignments);
// Add deep link context
if (deferredLink) {
config.deferredContent = deferredLink.params;
config.source = deferredLink.params.utm_source || 'deep_link';
config.welcomeMessage = getContextualWelcome(deferredLink.params);
}
return config;
}
function getContextualWelcome(params) {
if (params.referrer_name) {
return `${params.referrer_name} invited you to join.`;
}
if (params.product_id) {
return 'Sign up to see what you were looking at.';
}
if (params.promo_code) {
return 'Create an account to claim your offer.';
}
return 'Welcome! Let\'s get you started.';
}
Content-First vs. Auth-First
const contentFirstExperiment = {
id: 'content_first_v2',
// Only applies to users with deferred deep link content
eligibility: (user) => user.hasDeferredContent,
variants: [
{
id: 'content_first',
weight: 50,
config: {
flow: [
'show_deferred_content', // Show the product/article/content they clicked on
'signup_prompt', // "Sign up to save this / continue"
'first_action', // Core action
],
},
},
{
id: 'auth_first',
weight: 50,
config: {
flow: [
'signup', // Sign up first
'show_deferred_content', // Then show the content
'first_action',
],
},
},
],
primaryMetric: 'signup_completed',
secondaryMetrics: ['d7_retention', 'content_engagement'],
};
Typical result: Content-first wins by 20-35% for signup completion. Users who see value before signing up are more willing to create an account. Auth-first wins only when the content is time-sensitive (limited offer, expiring invite).
Permission Request Timing
const permissionTimingExperiment = {
id: 'permission_timing_v1',
variants: [
{
id: 'during_onboarding',
weight: 50,
config: {
requestPushPermission: 'onboarding_step_3',
requestLocationPermission: 'onboarding_step_4',
},
},
{
id: 'contextual',
weight: 50,
config: {
requestPushPermission: 'after_first_save', // "Want to know when this is back in stock?"
requestLocationPermission: 'on_store_locator', // "Allow location to find stores near you"
},
},
],
primaryMetric: 'push_permission_granted',
secondaryMetrics: ['location_permission_granted', 'onboarding_completion'],
};
Contextual permission requests grant rates are typically 15-30% higher than during-onboarding requests.
Tracking Onboarding Experiments
Event Schema
// Step started
analytics.track('onboarding_step_started', {
experimentId,
variantId,
step: 'signup',
stepIndex: 0,
totalSteps: 3,
source: deferredLink ? 'deep_link' : 'organic',
hasDeferredContent: !!deferredLink,
});
// Step completed
analytics.track('onboarding_step_completed', {
experimentId,
variantId,
step: 'signup',
stepIndex: 0,
duration: stepDurationMs,
method: 'google_sign_in', // or 'email', 'apple', 'passkey'
});
// Onboarding completed
analytics.track('onboarding_completed', {
experimentId,
variantId,
totalDuration: totalDurationMs,
stepsCompleted: 3,
stepsSkipped: 0,
source: deferredLink ? 'deep_link' : 'organic',
});
// Activation (the metric that actually matters)
analytics.track('user_activated', {
experimentId,
variantId,
activationAction: 'created_first_item',
timeFromSignup: activationDelayMs,
source: deferredLink ? 'deep_link' : 'organic',
});
Drop-Off Analysis
async function analyzeDropOff(experimentId) {
const experiment = getExperiment(experimentId);
for (const variant of experiment.variants) {
const steps = variant.config.steps || variant.config.flow;
const funnel = [];
for (let i = 0; i < steps.length; i++) {
const started = await countEvents('onboarding_step_started', {
experimentId,
variantId: variant.id,
stepIndex: i,
});
const completed = await countEvents('onboarding_step_completed', {
experimentId,
variantId: variant.id,
stepIndex: i,
});
funnel.push({
step: steps[i],
started,
completed,
completionRate: (completed / started * 100).toFixed(1) + '%',
dropOff: started - completed,
});
}
console.log(`\n${variant.id}:`);
funnel.forEach(f => {
console.log(` ${f.step}: ${f.completionRate} (${f.dropOff} dropped)`);
});
}
}
Common Test Patterns
Pattern 1: Referral Onboarding
Users arriving via referral links need a different flow:
const referralOnboardingTest = {
id: 'referral_onboarding_v1',
eligibility: (user) => user.deferredParams?.ref,
variants: [
{
id: 'referral_context',
weight: 50,
config: {
showReferrerInfo: true, // "Sarah invited you"
showReferralReward: true, // "You both get $10"
prefillReferralCode: true,
steps: ['signup', 'claim_reward'],
},
},
{
id: 'standard',
weight: 50,
config: {
showReferrerInfo: false,
showReferralReward: false,
prefillReferralCode: true, // Still apply the code silently
steps: ['signup', 'first_action'],
},
},
],
primaryMetric: 'signup_completed',
secondaryMetrics: ['referral_reward_claimed', 'd7_retention'],
};
Pattern 2: Campaign-Specific Onboarding
Users from ad campaigns see campaign-aligned onboarding:
const campaignOnboardingTest = {
id: 'campaign_onboarding_v1',
eligibility: (user) => user.deferredParams?.utm_campaign,
variants: [
{
id: 'campaign_aligned',
weight: 50,
config: {
welcomeHeadline: 'dynamicFromCampaign', // Matches the ad they clicked
firstScreen: 'campaign_content', // Show what the ad promised
skipGenericOnboarding: true,
},
},
{
id: 'standard',
weight: 50,
config: {
welcomeHeadline: 'Welcome to [App]',
firstScreen: 'standard_onboarding',
skipGenericOnboarding: false,
},
},
],
primaryMetric: 'activation',
};
Pattern 3: Progressive Profiling
Collect user data over multiple sessions instead of all at once:
const profilingTest = {
id: 'progressive_profiling_v1',
variants: [
{
id: 'upfront',
weight: 50,
config: {
collectDuringOnboarding: ['name', 'interests', 'notifications'],
},
},
{
id: 'progressive',
weight: 50,
config: {
collectDuringOnboarding: ['name'],
collectOnSession2: ['interests'],
collectOnSession3: ['notifications'],
},
},
],
primaryMetric: 'd7_retention',
secondaryMetrics: ['profile_completion_d30', 'notification_opt_in_d7'],
};
Progressive profiling typically produces 10-15% higher D7 retention because users don't feel overwhelmed, but 20-30% lower notification opt-in rates at day 1 (which catches up by day 7-14 in most cases).
Metrics That Matter
Primary: Activation Rate
Don't optimize for onboarding completion. Optimize for activation (the action most correlated with retention):
| App Type | Activation Action | Typical Timeline |
|---|---|---|
| E-commerce | First purchase | Within 7 days |
| Social | First post or connection | Within 3 days |
| Productivity | First project created | Within 1 day |
| Content | 3+ articles read | Within 7 days |
| Gaming | Level 5 reached | Within 3 days |
Secondary: Retention Cohorts
Track D1, D7, and D30 retention for each variant. An onboarding variant that has 5% higher completion but 10% lower D7 retention is a worse outcome.
async function retentionByVariant(experimentId, day) {
for (const variant of getVariants(experimentId)) {
const signedUp = await getUsersInVariant(experimentId, variant.id);
const retained = await getUsersRetainedOnDay(signedUp, day);
console.log(variant.id, {
users: signedUp.length,
retained: retained.length,
rate: (retained.length / signedUp.length * 100).toFixed(1) + '%',
});
}
}
Best Practices
- Measure activation, not completion: Onboarding completion is a vanity metric. Activation and retention are what matter.
- Segment deep link users from organic: Deep-linked users have context and intent. Their optimal onboarding is different.
- Test content-first for deep link users: If a user clicked to see specific content, show it before asking them to sign up.
- Run for at least 30 days: Onboarding experiments need retention data, which takes weeks to materialize.
- Don't test too many things at once: Change one major flow element per test. Save multivariate testing for when you have high enough traffic.
- Pre-fill everything you can: Deep link parameters often contain data (referral codes, product IDs, promo codes) that can pre-fill forms and reduce friction.
For A/B testing features, see Tolinku A/B testing. For onboarding use cases, see the onboarding docs.
Get deep linking tips in your inbox
One email per week. No spam.