← Volver al listado de tecnologías
Capítulo 3: Tracking de Instalaciones y Atribución
Capítulo 3: Tracking de Instalaciones y Atribución
En este capítulo aprenderás cómo AppsFlyer rastrea y atribuye instalaciones, cómo interpretar los datos de conversión y cómo validar que todo funciona correctamente.
🎯 Objetivos del Capítulo
Al finalizar este capítulo serás capaz de:
- ✅ Entender el flujo de atribución de instalaciones
- ✅ Diferenciar entre usuarios orgánicos y no-orgánicos
- ✅ Interpretar los datos de conversión
- ✅ Implementar re-atribución y re-engagement
- ✅ Validar y testear la atribución
📊 Flujo de Atribución
¿Cómo Funciona la Atribución?
graph TD
A[Usuario ve anuncio] --> B[Click en anuncio]
B --> C[AppsFlyer registra click]
C --> D[Redirección a App Store/Play Store]
D --> E[Usuario instala app]
E --> F[App se abre por primera vez]
F --> G[SDK envía evento de instalación]
G --> H[AppsFlyer atribuye instalación]
H --> I[Datos disponibles en dashboard]
Ventana de Atribución
AppsFlyer usa ventanas de tiempo para atribuir instalaciones:
- Click Lookback Window: 7 días (por defecto)
- View-Through Window: 24 horas (para impresiones)
- Re-engagement Window: Configurable
🔍 Implementación de Tracking de Instalaciones
Listener de Datos de Conversión
// ConversionDataManager.js
import appsFlyer from 'react-native-appsflyer';
import AsyncStorage from '@react-native-async-storage/async-storage';
class ConversionDataManager {
constructor() {
this.conversionData = null;
this.isFirstLaunch = null;
this.attributionType = null;
}
init() {
return new Promise((resolve) => {
// Listener principal de conversión
appsFlyer.onInstallConversionData((data) => {
console.log('📊 Datos de conversión recibidos:', JSON.stringify(data, null, 2));
this.conversionData = data;
this.processConversionData(data);
resolve(data);
});
// Listener de errores
appsFlyer.onInstallConversionFailure((data) => {
console.error('❌ Error en conversión:', data);
resolve(null);
});
});
}
processConversionData(data) {
const {
af_status, // 'Organic' o 'Non-organic'
media_source, // Fuente de medios (ej: 'facebook')
campaign, // Nombre de campaña
af_channel, // Canal de marketing
af_ad, // Nombre del anuncio
af_adset, // Conjunto de anuncios
af_keywords, // Palabras clave
is_first_launch, // Boolean: primera instalación
install_time, // Timestamp de instalación
af_click_lookback, // Ventana de atribución usada
retargeting_conversion_type, // Para re-engagement
is_universal_link, // Si vino de Universal Link
af_dp, // Deep link value
af_sub1, af_sub2, af_sub3, af_sub4, af_sub5 // Parámetros custom
} = data;
// Guardar tipo de atribución
this.attributionType = af_status;
this.isFirstLaunch = is_first_launch;
// Procesar según el tipo
if (af_status === 'Non-organic') {
this.handleNonOrganicInstall(data);
} else {
this.handleOrganicInstall(data);
}
// Guardar datos para análisis posterior
this.saveConversionData(data);
}
handleNonOrganicInstall(data) {
console.log('📱 Instalación NO ORGÁNICA detectada');
console.log('Media Source:', data.media_source);
console.log('Campaña:', data.campaign);
// Categorizar por fuente
const source = this.categorizeSource(data.media_source);
// Log evento para analytics interno
appsFlyer.logEvent('non_organic_install', {
media_source: data.media_source,
campaign: data.campaign || 'unknown',
ad_set: data.af_adset || 'unknown',
ad_name: data.af_ad || 'unknown',
channel: data.af_channel || 'unknown',
source_category: source.category,
source_type: source.type,
install_time: data.install_time,
attribution_window: data.af_click_lookback
});
// Personalizar experiencia según campaña
this.personalizeUserExperience(data);
}
handleOrganicInstall(data) {
console.log('🌱 Instalación ORGÁNICA detectada');
appsFlyer.logEvent('organic_install', {
install_time: data.install_time,
has_deep_link: !!data.af_dp
});
// Experiencia estándar para usuarios orgánicos
this.setDefaultUserExperience();
}
categorizeSource(mediaSource) {
const sourceMap = {
// Redes sociales
'facebook': { category: 'social', type: 'paid_social' },
'instagram': { category: 'social', type: 'paid_social' },
'twitter': { category: 'social', type: 'paid_social' },
'snapchat': { category: 'social', type: 'paid_social' },
'tiktok': { category: 'social', type: 'paid_social' },
// Search
'google': { category: 'search', type: 'paid_search' },
'bing': { category: 'search', type: 'paid_search' },
// Redes de display
'google_display': { category: 'display', type: 'display_network' },
'admob': { category: 'display', type: 'mobile_display' },
// Email
'email': { category: 'email', type: 'email_marketing' },
// Orgánico
'organic': { category: 'organic', type: 'natural' }
};
const normalized = mediaSource?.toLowerCase() || 'unknown';
// Buscar coincidencia exacta o parcial
for (const [key, value] of Object.entries(sourceMap)) {
if (normalized.includes(key)) {
return value;
}
}
return { category: 'other', type: 'unknown' };
}
personalizeUserExperience(conversionData) {
const { campaign, media_source, af_adset } = conversionData;
// Ejemplos de personalización basada en campaña
if (campaign?.includes('discount')) {
// Usuario vino de campaña de descuento
this.showDiscountOnboarding();
} else if (campaign?.includes('feature_x')) {
// Usuario interesado en feature específica
this.highlightFeatureX();
} else if (media_source === 'facebook' && af_adset?.includes('lookalike')) {
// Usuario de audiencia lookalike
this.showPremiumFeatures();
}
}
showDiscountOnboarding() {
// Implementar onboarding con descuento
console.log('Mostrando onboarding con descuento');
// NavigationService.navigate('DiscountOnboarding');
}
highlightFeatureX() {
console.log('Destacando Feature X');
// NavigationService.navigate('FeatureXTutorial');
}
showPremiumFeatures() {
console.log('Mostrando características premium');
// NavigationService.navigate('PremiumOnboarding');
}
setDefaultUserExperience() {
console.log('Configurando experiencia estándar');
// NavigationService.navigate('StandardOnboarding');
}
async saveConversionData(data) {
try {
await AsyncStorage.setItem('af_conversion_data', JSON.stringify({
data,
timestamp: Date.now()
}));
console.log('Datos de conversión guardados');
} catch (error) {
console.error('Error guardando datos de conversión:', error);
}
}
async getConversionData() {
try {
const stored = await AsyncStorage.getItem('af_conversion_data');
return stored ? JSON.parse(stored) : null;
} catch (error) {
console.error('Error obteniendo datos de conversión:', error);
return null;
}
}
// Métodos de utilidad
isOrganicUser() {
return this.attributionType === 'Organic';
}
isNonOrganicUser() {
return this.attributionType === 'Non-organic';
}
getCampaignName() {
return this.conversionData?.campaign || null;
}
getMediaSource() {
return this.conversionData?.media_source || null;
}
}
export default new ConversionDataManager();
🔄 Re-atribución y Re-engagement
Tracking de Re-instalaciones
// ReAttributionManager.js
import appsFlyer from 'react-native-appsflyer';
class ReAttributionManager {
handleReAttribution(conversionData) {
const {
is_first_launch,
retargeting_conversion_type,
af_reengagement_window,
orig_cost,
is_retargeting
} = conversionData;
if (!is_first_launch && is_retargeting) {
console.log('🔄 Re-engagement detectado');
const reEngagementType = this.classifyReEngagement(retargeting_conversion_type);
appsFlyer.logEvent('re_engagement', {
type: reEngagementType,
conversion_type: retargeting_conversion_type,
window: af_reengagement_window,
original_cost: orig_cost
});
this.handleReEngagementFlow(reEngagementType);
}
}
classifyReEngagement(conversionType) {
const types = {
're-engagement': 'standard',
're-attribution': 'reinstall',
'loyal_user': 'loyal',
'existing_user': 'existing'
};
return types[conversionType] || 'unknown';
}
handleReEngagementFlow(type) {
switch(type) {
case 'reinstall':
this.welcomeBackUser();
break;
case 'loyal':
this.rewardLoyalUser();
break;
case 'standard':
this.showReEngagementOffer();
break;
default:
this.standardFlow();
}
}
welcomeBackUser() {
console.log('👋 Bienvenido de vuelta!');
appsFlyer.logEvent('welcome_back', {
timestamp: Date.now()
});
// Mostrar pantalla de welcome back
}
rewardLoyalUser() {
console.log('🎁 Usuario leal detectado');
appsFlyer.logEvent('loyal_user_returned', {
timestamp: Date.now()
});
// Ofrecer recompensa especial
}
showReEngagementOffer() {
console.log('💰 Mostrando oferta de re-engagement');
appsFlyer.logEvent('reengagement_offer_shown', {
timestamp: Date.now()
});
// Mostrar oferta especial
}
standardFlow() {
console.log('Flujo estándar de re-engagement');
}
}
export default new ReAttributionManager();
📈 Análisis Avanzado de Atribución
Tracking de Fuentes y Campañas
// AttributionAnalytics.js
import appsFlyer from 'react-native-appsflyer';
class AttributionAnalytics {
constructor() {
this.sessionData = {
source: null,
campaign: null,
adSet: null,
startTime: null
};
}
analyzeUserJourney(conversionData) {
const journey = {
// Punto de entrada
entryPoint: this.determineEntryPoint(conversionData),
// Fuente de tráfico
trafficSource: this.analyzeTrafficSource(conversionData),
// Calidad del usuario
userQuality: this.assessUserQuality(conversionData),
// Costo de adquisición
acquisitionCost: this.calculateAcquisitionCost(conversionData),
// Predicción de LTV
predictedLTV: this.predictLifetimeValue(conversionData)
};
this.logJourneyAnalysis(journey);
return journey;
}
determineEntryPoint(data) {
if (data.af_dp) {
return {
type: 'deep_link',
value: data.af_dp,
hasParameters: !!(data.af_sub1 || data.af_sub2)
};
} else if (data.is_universal_link) {
return {
type: 'universal_link',
value: data.link
};
} else {
return {
type: 'standard',
value: 'home'
};
}
}
analyzeTrafficSource(data) {
const { media_source, af_channel, campaign } = data;
return {
primary: media_source,
channel: af_channel,
campaign: campaign,
quality: this.rateSourceQuality(media_source),
tier: this.getSourceTier(media_source)
};
}
rateSourceQuality(source) {
// Basado en datos históricos de tu app
const qualityScores = {
'google': 85,
'facebook': 80,
'organic': 90,
'instagram': 75,
'tiktok': 70,
'email': 85
};
return qualityScores[source?.toLowerCase()] || 50;
}
getSourceTier(source) {
const tierMap = {
'google': 'tier1',
'facebook': 'tier1',
'organic': 'premium',
'instagram': 'tier2',
'tiktok': 'tier2',
'email': 'tier1'
};
return tierMap[source?.toLowerCase()] || 'tier3';
}
assessUserQuality(data) {
let score = 50; // Base score
// Factores que aumentan la calidad
if (data.af_status === 'Organic') score += 20;
if (data.af_keywords?.includes('premium')) score += 10;
if (data.campaign?.includes('lookalike')) score += 15;
if (data.af_ad?.includes('video')) score += 5;
// Factores que disminuyen la calidad
if (data.campaign?.includes('broad')) score -= 10;
if (data.media_source?.includes('incentive')) score -= 20;
return {
score: Math.min(100, Math.max(0, score)),
tier: score >= 70 ? 'high' : score >= 40 ? 'medium' : 'low'
};
}
calculateAcquisitionCost(data) {
// Esto normalmente vendría de tu backend o data warehouse
const { af_cost_value, af_cost_currency } = data;
if (af_cost_value) {
return {
value: parseFloat(af_cost_value),
currency: af_cost_currency || 'USD',
type: 'actual'
};
}
// Estimación basada en promedios
const estimatedCosts = {
'google': 2.50,
'facebook': 1.80,
'instagram': 1.60,
'tiktok': 1.20,
'organic': 0
};
return {
value: estimatedCosts[data.media_source?.toLowerCase()] || 1.00,
currency: 'USD',
type: 'estimated'
};
}
predictLifetimeValue(data) {
// Modelo simplificado de predicción LTV
const { media_source, campaign } = data;
let baseLTV = 10; // LTV base
// Ajustes basados en la fuente
const sourceMultipliers = {
'organic': 1.5,
'google': 1.3,
'facebook': 1.2,
'instagram': 1.1,
'tiktok': 0.9
};
const multiplier = sourceMultipliers[media_source?.toLowerCase()] || 1.0;
// Ajustes basados en la campaña
if (campaign?.includes('premium')) baseLTV *= 1.5;
if (campaign?.includes('discount')) baseLTV *= 0.8;
return {
value: baseLTV * multiplier,
confidence: 'medium',
currency: 'USD'
};
}
logJourneyAnalysis(journey) {
appsFlyer.logEvent('user_journey_analyzed', {
entry_type: journey.entryPoint.type,
traffic_source: journey.trafficSource.primary,
source_quality: journey.trafficSource.quality,
user_quality_score: journey.userQuality.score,
user_tier: journey.userQuality.tier,
acquisition_cost: journey.acquisitionCost.value,
predicted_ltv: journey.predictedLTV.value,
roi_prediction: journey.predictedLTV.value / journey.acquisitionCost.value
});
console.log('📊 Análisis de Journey:', journey);
}
}
export default new AttributionAnalytics();
🧪 Testing y Validación
Herramientas de Testing
// AttributionTester.js
import appsFlyer from 'react-native-appsflyer';
import { Linking, Alert } from 'react-native';
class AttributionTester {
constructor() {
this.testResults = [];
}
// Test de instalación orgánica
async testOrganicInstall() {
console.log('🧪 Testing instalación orgánica...');
// Simular primera apertura sin parámetros
const result = await this.simulateFirstOpen({});
this.validateOrganicAttribution(result);
return result;
}
// Test de instalación non-organic
async testNonOrganicInstall(testLink) {
console.log('🧪 Testing instalación non-organic...');
// Ejemplo de link de test:
// https://app.appsflyer.com/id123456789?pid=test_source&c=test_campaign
const params = this.parseTestLink(testLink);
const result = await this.simulateFirstOpen(params);
this.validateNonOrganicAttribution(result);
return result;
}
parseTestLink(link) {
const url = new URL(link);
const params = {};
url.searchParams.forEach((value, key) => {
params[key] = value;
});
return {
media_source: params.pid || 'test_source',
campaign: params.c || 'test_campaign',
af_channel: params.af_channel || 'test',
af_ad: params.af_ad || 'test_ad',
af_adset: params.af_adset || 'test_adset'
};
}
async simulateFirstOpen(params) {
return new Promise((resolve) => {
// Configurar listener temporal
const unsubscribe = appsFlyer.onInstallConversionData((data) => {
console.log('📱 Datos recibidos en test:', data);
unsubscribe();
resolve(data);
});
// Trigger del SDK
if (Object.keys(params).length > 0) {
// Simular apertura con parámetros
this.triggerWithParams(params);
} else {
// Simular apertura orgánica
this.triggerOrganic();
}
});
}
triggerWithParams(params) {
// Crear deep link de test
const deepLink = `tuapp://test?${new URLSearchParams(params).toString()}`;
Linking.openURL(deepLink);
}
triggerOrganic() {
// Reiniciar SDK para simular primera apertura
appsFlyer.stop(false);
setTimeout(() => {
appsFlyer.start();
}, 1000);
}
validateOrganicAttribution(data) {
const tests = [
{
name: 'Status es Organic',
pass: data.af_status === 'Organic',
actual: data.af_status,
expected: 'Organic'
},
{
name: 'No tiene media_source',
pass: !data.media_source || data.media_source === 'organic',
actual: data.media_source,
expected: 'organic o undefined'
},
{
name: 'is_first_launch es true',
pass: data.is_first_launch === true,
actual: data.is_first_launch,
expected: true
}
];
this.logTestResults('Organic Install', tests);
}
validateNonOrganicAttribution(data) {
const tests = [
{
name: 'Status es Non-organic',
pass: data.af_status === 'Non-organic',
actual: data.af_status,
expected: 'Non-organic'
},
{
name: 'Media source presente',
pass: !!data.media_source && data.media_source !== 'organic',
actual: data.media_source,
expected: 'cualquier fuente no-orgánica'
},
{
name: 'Campaign presente',
pass: !!data.campaign,
actual: data.campaign,
expected: 'valor de campaña'
},
{
name: 'is_first_launch es true',
pass: data.is_first_launch === true,
actual: data.is_first_launch,
expected: true
}
];
this.logTestResults('Non-Organic Install', tests);
}
logTestResults(testName, tests) {
console.log(`\n📋 Resultados de Test: ${testName}`);
console.log('================================');
let passed = 0;
let failed = 0;
tests.forEach(test => {
const icon = test.pass ? '✅' : '❌';
console.log(`${icon} ${test.name}`);
if (!test.pass) {
console.log(` Esperado: ${test.expected}`);
console.log(` Recibido: ${test.actual}`);
failed++;
} else {
passed++;
}
});
console.log('================================');
console.log(`Pasados: ${passed}/${tests.length}`);
if (failed > 0) {
Alert.alert('Test Fallido', `${failed} pruebas fallaron. Ver consola para detalles.`);
} else {
Alert.alert('Test Exitoso', 'Todas las pruebas pasaron correctamente.');
}
this.testResults.push({
name: testName,
tests,
passed,
failed,
timestamp: Date.now()
});
}
// Test de deep link con atribución
async testDeepLinkAttribution() {
const testLink = 'tuapp://product/123?af_dp=product%2F123&pid=email&c=newsletter';
console.log('🔗 Testing deep link attribution...');
console.log('Link:', testLink);
return new Promise((resolve) => {
appsFlyer.onDeepLink((deepLinkData) => {
console.log('Deep link data:', deepLinkData);
const tests = [
{
name: 'Deep link value presente',
pass: !!deepLinkData.deep_link_value,
actual: deepLinkData.deep_link_value,
expected: 'product/123'
},
{
name: 'Media source correcta',
pass: deepLinkData.media_source === 'email',
actual: deepLinkData.media_source,
expected: 'email'
},
{
name: 'Campaign correcta',
pass: deepLinkData.campaign === 'newsletter',
actual: deepLinkData.campaign,
expected: 'newsletter'
}
];
this.logTestResults('Deep Link Attribution', tests);
resolve(deepLinkData);
});
// Abrir el deep link
Linking.openURL(testLink);
});
}
// Generar reporte de tests
generateReport() {
const report = {
timestamp: Date.now(),
totalTests: this.testResults.length,
results: this.testResults,
summary: {
totalPassed: this.testResults.reduce((acc, r) => acc + r.passed, 0),
totalFailed: this.testResults.reduce((acc, r) => acc + r.failed, 0)
}
};
console.log('\n📊 REPORTE FINAL DE TESTS');
console.log('========================');
console.log(`Total de test suites: ${report.totalTests}`);
console.log(`Pruebas pasadas: ${report.summary.totalPassed}`);
console.log(`Pruebas fallidas: ${report.summary.totalFailed}`);
return report;
}
}
export default new AttributionTester();
Testing en el Dashboard
// DashboardValidator.js
class DashboardValidator {
getValidationSteps() {
return [
{
step: 1,
title: 'Verificar SDK Integration',
description: 'Ve a App Settings > SDK Integration',
checks: [
'SDK Version correcto',
'Last SDK Signal reciente (< 1 hora)',
'Development/Production mode correcto'
]
},
{
step: 2,
title: 'Verificar Real-Time Data',
description: 'Ve a Real-Time > Install & In-App Events',
checks: [
'Instalaciones aparecen en < 30 segundos',
'Media source correcta',
'Campaign parameters visibles'
]
},
{
step: 3,
title: 'Verificar Attribution',
description: 'Ve a Overview > Attribution',
checks: [
'Instalaciones orgánicas vs non-orgánicas',
'Sources breakdown correcto',
'Conversion paths visibles'
]
},
{
step: 4,
title: 'Verificar Raw Data',
description: 'Ve a Export Data > Raw Data Reports',
checks: [
'Todos los campos requeridos presentes',
'Customer User ID visible',
'Attribution correcta en CSV'
]
}
];
}
async validateInstallation(installId) {
// Validar una instalación específica
console.log(`Validando instalación: ${installId}`);
// Aquí podrías hacer una llamada a la API de AppsFlyer
// para verificar los datos de una instalación específica
return {
found: true,
data: {
install_time: Date.now(),
attributed_to: 'test_campaign',
status: 'valid'
}
};
}
}
export default new DashboardValidator();
📋 Checklist de Validación
- Conversion listener configurado correctamente
- Datos de primera instalación recibidos
- Diferenciación organic/non-organic funcionando
- Parámetros de campaña visibles
- Deep links con atribución funcionando
- Re-attribution detectada correctamente
- Datos visibles en dashboard en tiempo real
- Customer User ID asociado correctamente
- Tests de atribución pasando
- Raw data exports contienen todos los campos
🎯 Ejercicios Prácticos
Ejercicio 1: Test Completo de Atribución
- Desinstala la app
- Crea un link de test con parámetros
- Instala la app a través del link
- Verifica los datos de conversión
- Confirma en el dashboard
Ejercicio 2: Personalización por Campaña
Implementa diferentes flujos según la campaña:
- Si viene de “discount_50”: mostrar pantalla de descuento
- Si viene de “premium_features”: destacar features premium
- Si es orgánico: flujo estándar
Ejercicio 3: Análisis de Cohortes
- Implementa tracking de cohortes por fuente
- Compara retención entre orgánicos y pagados
- Calcula LTV por media source
📚 Resumen
En este capítulo aprendiste:
- ✅ Cómo funciona la atribución de instalaciones
- ✅ Interpretar datos de conversión
- ✅ Diferenciar usuarios orgánicos y no-orgánicos
- ✅ Implementar re-atribución y re-engagement
- ✅ Validar y testear la atribución
- ✅ Analizar el journey del usuario
🚀 Próximo Capítulo
En el siguiente capítulo exploraremos los Eventos In-App, incluyendo:
- Tipos de eventos predefinidos
- Eventos personalizados
- Tracking de revenue
- Optimización de eventos