← Volver al listado de tecnologías

Capítulo 3: Tracking de Instalaciones y Atribución

Por: Artiko
appsflyerreact-nativeatribucióninstalacionesconversiónanalytics

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:

📊 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:

🔍 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

🎯 Ejercicios Prácticos

Ejercicio 1: Test Completo de Atribución

  1. Desinstala la app
  2. Crea un link de test con parámetros
  3. Instala la app a través del link
  4. Verifica los datos de conversión
  5. Confirma en el dashboard

Ejercicio 2: Personalización por Campaña

Implementa diferentes flujos según la campaña:

  1. Si viene de “discount_50”: mostrar pantalla de descuento
  2. Si viene de “premium_features”: destacar features premium
  3. Si es orgánico: flujo estándar

Ejercicio 3: Análisis de Cohortes

  1. Implementa tracking de cohortes por fuente
  2. Compara retención entre orgánicos y pagados
  3. Calcula LTV por media source

📚 Resumen

En este capítulo aprendiste:

🚀 Próximo Capítulo

En el siguiente capítulo exploraremos los Eventos In-App, incluyendo:


← Capítulo 2: Inicialización | Siguiente: Eventos In-App →