Servidor a servidor - Guía de implantación de SKAdNetwork 4

Documento

Guía de implementación de SKAdNetwork 4.0

Implemente el marco SKAdNetwork 4.0 de Apple para la atribución de iOS centrada en la privacidad mediante la integración de servidor a servidor, lo que permite la medición de campañas de instalación de aplicaciones con ventanas de postback mejoradas y valores de conversión gruesos a la vez que se protege la privacidad del usuario.


Visión general

¿Qué es SKAdNetwork 4.0?

SKAdNetwork (SKAN) es el marco de atribución de Apple centrado en la privacidad que permite medir las campañas publicitarias de instalación de aplicaciones de iOS protegiendo la privacidad del usuario mediante la implementación de servidor a servidor para una validación y un seguimiento sólidos.

El marco gestiona todos los aspectos críticos de la atribución al tiempo que mantiene la privacidad del usuario a través de los métodos prescritos por Apple, por lo que es esencial para los profesionales del marketing móvil que operan en el panorama posterior a iOS 14.5.


Características principales

SKAdNetwork 4.0 introduce funciones de medición mejoradas y flexibilidad para la optimización de campañas.

  • Validación robusta: Agregación de postbacks de todas las redes con protección antifraude integrada.
  • Gestión dinámica de conversiones: Configuración del panel de control para la codificación del valor de conversión
  • Informes mejorados: Parámetros de marketing enriquecidos y perspectivas de datos granulares
  • Devoluciones seguras de socios: Valores de conversión descodificados y seguimiento de ingresos
  • Validación exhaustiva: Seguimiento enriquecido de eventos y sesiones para la verificación de la implementación
  • Múltiples ventanas de devolución: Gestión automatizada de marcas de tiempo de 0 a 2 días, de 3 a 7 días y de 8 a 35 días
  • Seguimiento de ingresos: Soporte para la monetización de anuncios e ingresos regulares con especificaciones de divisas

Requisitos previos

Familiarícese con los conceptos de SKAdNetwork y las versiones anteriores antes de implementar SKAN 4.0.


Solución SKAdNetwork de Singular

La solución SKAdNetwork de Singular proporciona una gestión de la atribución de extremo a extremo, desde la implementación en el lado del cliente hasta el procesamiento postback y la optimización de la campaña.

Componentes de la solución

Características de la plataforma

Soporte integral de SKAdNetwork en todo el flujo de trabajo de atribución y análisis.

Componente Funcionalidad
Código cliente Ejemplos de código nativo de iOS para el registro del marco SKAdNetwork y la gestión del valor de conversión. Enfoque alternativo del lado del servidor disponible mediante el punto final de la API de valor de conversión.
Procesamiento de devoluciones Validación y agregación de postbacks de todas las redes publicitarias con informes unificados
Protección contra el fraude Validación de firmas criptográficas, deduplicación de ID de transacciones y verificación segura de parámetros para datos no firmados.
Gestión de la conversión Configuración dinámica del panel de control para codificar las actividades posteriores a la instalación en valores de conversión
Informes Traducción de ID de campaña y enriquecimiento con parámetros de marketing para análisis granulares
Devoluciones de socios Valores de conversión descodificados enviados como eventos e ingresos para la optimización de socios

Arquitectura de implantación

Implementación en dos partes

La implementación de SKAdNetwork del lado del cliente consta de dos componentes principales.

1.1. Implementación del lado del cliente (obligatoria):

  • Registro del marco SKAdNetwork en el lanzamiento de la aplicación
  • Gestión inteligente del valor de conversión basada en la actividad posterior a la instalación
  • Esencial para la optimización de campañas utilizando la atribución SKAdNetwork
  • Permite el seguimiento de las actividades asociadas posteriores a la instalación

2. Actualización de la integración del lado del servidor (recomendada):

  • Valida y soluciona problemas de implementación del lado del cliente
  • Enriquece los eventos y sesiones enviados a través de los puntos finales Sesióny Evento
  • Permite la validación de metadatos de SKAdNetwork.
  • Confirma la correcta implementación del lado de la aplicación

Implementación del lado del cliente

Implementa el registro del marco SKAdNetwork y la gestión del valor de conversión utilizando las muestras de código nativo iOS de Singular para una medición óptima de la campaña con las características de SKAN 4.0.

Responsabilidades de implementación

Funcionalidad básica

Los ejemplos de código soportan el registro de SKAdNetwork y la gestión inteligente del valor de conversión.

  1. Registro SKAdNetwork: Registra la aplicación en el framework inmediatamente después del lanzamiento para permitir la atribución.
  2. Gestión del valor de conversión:
    • Se comunica de forma sincrónica con el punto final de Singular para recibir el siguiente valor de conversión.
    • Informa de sesiones, eventos e ingresos a Singular.
    • Recibe un valor de conversión codificado que representa la actividad posterior a la instalación configurada.
    • Recoge metadatos de SKAdNetwork por periodo de medición para su validación y cálculo.

Recogida de metadatos

El código recopila los metadatos esenciales de SKAdNetwork tanto para la validación como para el cálculo del valor de conversión.

  • Fecha y hora de la primera llamada al marco SKAdNetwork subyacente
  • Fecha y hora de la última llamada al marco SKAdNetwork subyacente
  • Últimos valores de postback actualizados (tanto gruesos como finos)
  • Ingresos totales e ingresos totales de monetización de anuncios generados por dispositivo

Flujo de integración

Proceso integral

Flujo completo de SKAdNetwork para clientes de S2S, desde la gestión de la conversión en el lado del cliente hasta el procesamiento del postback.

SKAdNetwork 4.0 S2S Integration Flow

  1. Solicitud de valor de conversión: El código de la aplicación se comunica con el punto final de Singular de forma sincrónica para obtener el último valor de conversión basado en sesiones, eventos e ingresos.
  2. Actualización del marco : la aplicación actualiza el marco SKAdNetwork con el valor de conversión recibido.
  3. Enriquecimiento de metadatos: La aplicación enriquece los eventos y sesiones S2S con metadatos SKAdNetwork para su validación.
  4. Expiración del temporizador: Una vez expirado el temporizador, SKAdNetwork envía un postback a la red publicitaria.
  5. Reenvío de postbacks: La red reenvía el postback a Singular (configuración segura o normal)
  6. Procesamiento de postbacks: Singular procesa el postback mediante:
    • Validación de la firma criptográfica
    • Decodificación del valor de conversión utilizando el modelo configurado
    • Enriquecimiento con información de red de integraciones de socios
    • Envío de los datos descodificados a BI y a los socios mediante postbacks

Separación de datos: Los datos de SKAdNetwork (instalaciones y eventos descodificados) accesibles a través de informes, API, tablas ETL y postbacks separados para evitar que se mezclen con los conjuntos de datos existentes durante las pruebas y la validación.


Interfaz SKAdNetwork

Definición completa de la interfaz para la integración de SKAdNetwork que proporciona métodos para el seguimiento de atribuciones, actualizaciones de valores de conversión y gestión de ingresos.

Definiciones de métodos

Registro de atribución

Inicializa el seguimiento de atribuciones de SKAN en el primer lanzamiento de la aplicación, estableciendo el valor de conversión inicial en 0 y estableciendo marcas de tiempo de referencia.

Objective-C
+ (void)registerAppForAdNetworkAttribution;

Gestión del valor de conversión

Los métodos actualizan los valores de conversión basándose en la actividad posterior a la instalación capturada por la aplicación y el modelo de conversión seleccionado configurado dinámicamente.

Actividades admitidas:

  • Sesiones: Críticas para la medición de la retención
  • Eventos de conversión: Crítico para la medición de eventos post-instalación
  • Eventos de ingresos: Crítico para medir los ingresos

Seguimiento de sesiones

Gestiona el seguimiento basado en sesiones para la retención y el análisis de cohortes con controlador de finalización opcional para acciones posteriores a la actualización.

Objective-C
+ (void)updateConversionValuesAsync:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler;

Seguimiento de eventos

Gestiona el seguimiento de eventos de conversión antes de enviar los datos a Singular, actualizando los valores de conversión en función del contexto del evento.

Objective-C
+ (void)updateConversionValuesAsync:(NSString *)eventName 
                withCompletionHandler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler;

Gestión de ingresos

Realiza un seguimiento de los eventos de ingresos, manteniendo totales separados para la monetización de anuncios y los ingresos regulares. Debe ejecutarse antes de actualizar los valores de conversión.

Objective-C
+ (void)updateRevenue:(double)amount 
          andCurrency:(NSString *)currency 
     isAdMonetization:(BOOL)admon;

Recuperación de datos

Devuelve un completo diccionario de datos SKAN que incluye valores de conversión, marcas de tiempo y seguimiento de ingresos.

El diccionario contiene:

  • Valores de conversión detallados actuales y anteriores
  • Valores gruesos a través de diferentes ventanas de postback
  • Marcas de tiempo de bloqueo de ventanas
  • Seguimiento de ingresos por divisa
  • Seguimiento separado de la monetización de anuncios y de los ingresos ordinarios
Objective-C
+ (NSDictionary *)getSkanDetails;

Notas de implementación

  • Los métodos utilizan patrones asíncronos para evitar el bloqueo del hilo principal
  • El seguimiento de los ingresos debe preceder a las actualizaciones de los valores de conversión
  • Admite valores de conversión de grano fino (0-63) y grueso (bajo/medio/alto).
  • Mantiene un seguimiento separado para las diferentes ventanas de postback
  • Implementa la gestión integral de errores mediante gestores de finalización

Código completo de la interfaz

SKANSnippet.h

Objective-C
//SKANSnippet.h

#import <Foundation/Foundation.h>

@interface SKANSnippet : NSObject

// Register for SKAdNetwork attribution.
// Call this method as soon as possible on first app launch.
// Sets conversion value to 0 and updates timestamp for additional processing.
+ (void)registerAppForAdNetworkAttribution;

// Track retention and cohorts by calling for each app open.
// Reports session details and updates conversion value if needed.
// Optional callback runs once conversion value is updated.
+ (void)updateConversionValuesAsync:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler;

// Track conversion events by calling after each event and before sending to Singular.
// Reports event details and updates conversion value if needed.
// Optional callback runs once conversion value is updated.
+ (void)updateConversionValuesAsync:(NSString *)eventName 
                withCompletionHandler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler;

// Track revenue by calling before every revenue event.
// Updates total revenue for next conversion value calculation.
// Note:
// 1. Call before 'updateConversionValuesAsync' to ensure revenue included
// 2. Avoid calling on event retries to prevent double-counting
+ (void)updateRevenue:(double)amount 
          andCurrency:(NSString *)currency 
     isAdMonetization:(BOOL)admon;

// Gets current fine, coarse, window locked values saved in dictionary.
// Contains all relevant SKAN values including:
// - skan_current_conversion_value
// - prev_fine_value  
// - skan_first_call_to_skadnetwork_timestamp
// - skan_last_call_to_skadnetwork_timestamp
// - skan_total_revenue_by_currency
// - skan_total_admon_revenue_by_currency
// - p0_coarse, p1_coarse, p2_coarse
// - p0_window_lock, p1_window_lock, p2_window_lock
// - Previous coarse values and revenue per window
+ (NSDictionary *)getSkanDetails;

@end

Implementación de SKAdNetwork

Código de implementación completo para la interfaz SKAdNetwork 4.0 de Apple, que gestiona el seguimiento de atribuciones, los valores de conversión y los informes de ingresos a través de múltiples ventanas de postback.

Resumen de la implementación

Constantes y configuración

La implementación define tres ventanas de postback distintas para el seguimiento de la actividad del usuario y las conversiones.

Objective-C
static NSInteger firstSkan4WindowInSec = 3600 * 24 * 2;  // 48 hours
static NSInteger secondSkan4WindowInSec = 3600 * 24 * 7;  // 7 days
static NSInteger thirdSkan4WindowInSec = 3600 * 24 * 35; // 35 days

Características principales

  • Registro de atribución: Gestiona la configuración inicial de la atribución de la aplicación y el seguimiento del valor de conversión por primera vez
  • Gestión de conversiones: Actualiza y realiza el seguimiento de los valores de conversión en varias ventanas de postback.
  • Seguimiento de ingresos: Mantiene un seguimiento separado para la monetización de anuncios y los eventos de ingresos regulares
  • Persistencia de datos: Utiliza NSUserDefaults para almacenar los datos relacionados con SKAN en todas las sesiones de la aplicación.
  • Seguridad de hilos: Implementa NSLock para operaciones seguras durante las llamadas a la red.

Estructura de almacenamiento de datos

  • Valores de conversión de grano fino (0-63)
  • Valores gruesos (bajo/medio/alto asignados a 0-2)
  • Seguimiento de ingresos por divisa para cada ventana de postback
  • Gestión de marcas de tiempo para las ventanas de retrocesión y los estados de bloqueo
  • Seguimiento de valores anteriores para conversiones finas y gruesas

Consideraciones de privacidad

  • Implementa funciones específicas de iOS 15.4+ y iOS 16.1
  • Gestiona las actualizaciones de valores de conversión postback de acuerdo con las directrices de privacidad de Apple
  • Mantiene un seguimiento separado de los distintos tipos de ingresos para garantizar una atribución precisa

Notas técnicas

  • Utiliza operaciones asíncronas para las llamadas de red y las actualizaciones de valores.
  • Implementa la gestión de errores y la validación de los valores de conversión.
  • Admite el seguimiento de valores de conversión tanto tradicional como grueso.
  • Gestiona múltiples ventanas de postback con diferentes duraciones y requisitos

Código de implementación completo

SKANSnippet.m

Importante: Sustituya los valores de los marcadores de posición (SU CLAVE DE API, SU VERSIÓN DE APP, etc.) por los valores reales de su aplicación antes del uso en producción.

Objective-C
//  SKANSnippet.m

#import "SKANSnippet.h"
#import <StoreKit/SKAdNetwork.h>
#import <UIKit/UIKit.h>

#define SESSION_EVENT_NAME @"__SESSION__"
#define SINGULAR_API_URL @"https://sdk-api-v1.singular.net/api/v2/conversion_value"

// SKAN Keys for NSUserDefaults persistency and requests
#define CONVERSION_VALUE_KEY @"skan_current_conversion_value"
#define FIRST_SKAN_CALL_TIMESTAMP @"skan_first_call_to_skadnetwork_timestamp"
#define LAST_SKAN_CALL_TIMESTAMP @"skan_last_call_to_skadnetwork_timestamp"
#define TOTAL_REVENUE_BY_CURRENCY @"skan_total_revenue_by_currency"
#define TOTAL_ADMON_REVENUE_BY_CURRENCY @"skan_total_admon_revenue_by_currency"
#define SKAN_UPDATED_CONVERSION_VALUE @"conversion_value"
#define SKAN_UPDATED_COARSE_VALUE @"skan_updated_coarse_value"
#define SKAN_UPDATED_LOCK_WINDOW_VALUE @"skan_updated_lock_window_value"

#define P0_COARSE @"p0_coarse"
#define P1_COARSE @"p1_coarse"
#define P2_COARSE @"p2_coarse"
#define P0_WINDOW_LOCK_TS @"p0_window_lock"
#define P1_WINDOW_LOCK_TS @"p1_window_lock"
#define P2_WINDOW_LOCK_TS @"p2_window_lock"

#define P0_PREV_FINE_VALUE @"prev_fine_value"
#define P0_PREV_COARSE_VALUE @"p0_prev_coarse_value"
#define P1_PREV_COARSE_VALUE @"p1_prev_coarse_value"
#define P2_PREV_COARSE_VALUE @"p2_prev_coarse_value"

#define TOTAL_REVENUE_P0 @"p0_total_iap_revenue"
#define TOTAL_REVENUE_P1 @"p1_total_iap_revenue"
#define TOTAL_REVENUE_P2 @"p2_total_iap_revenue"
#define TOTAL_ADMON_REVENUE_P0 @"p0_total_admon_revenue"
#define TOTAL_ADMON_REVENUE_P1 @"p1_total_admon_revenue"
#define TOTAL_ADMON_REVENUE_P2 @"p2_total_admon_revenue"

@implementation SKANSnippet

static NSInteger firstSkan4WindowInSec = 3600 * 24 * 2; //48 hours in sec
static NSInteger secondSkan4WindowInSec = 3600 * 24 * 7;
static NSInteger thirdSkan4WindowInSec = 3600 * 24 * 35;

static NSLock *lockObject;

+ (void)registerAppForAdNetworkAttribution {
    if ([SKANSnippet getFirstSkanCallTimestamp] != 0) {
        return;
    }
    
    if (@available(iOS 15.4, *)) {
        [SKAdNetwork updatePostbackConversionValue:0 completionHandler:nil];
        [SKANSnippet setFirstSkanCallTimestamp];
        [SKANSnippet setLastSkanCallTimestamp];
        [SKANSnippet valuesHasBeenUpdated:@(0) coarseValue:nil lockWindow:NO];
    } 
}

+ (void)updateConversionValuesAsync:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler {
    [SKANSnippet updateConversionValuesAsync:SESSION_EVENT_NAME withCompletionHandler:handler];
}

+ (void)updateConversionValuesAsync:(NSString *)eventName withCompletionHandler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError *))handler {
        if ([SKANSnippet isSkanWindowOver]) {
            return;
        }
        
        [SKANSnippet getConversionValueFromServer:eventName withCompletionHandler:handler];
}

+ (void)updateRevenue:(double)amount andCurrency:(NSString *)currency isAdMonetization:(BOOL)admon {
    // Update total revenues
    if (amount == 0 || !currency ) {
        return;
    }
    
    [SKANSnippet addToTotalRevenue:@(amount) withCurrency:currency isAdmon:admon];
}

+ (NSDictionary *)getSkanDetails {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    NSMutableDictionary *res = [NSMutableDictionary dictionary];
    //current fine
    [res setValue:[[userDefaults valueForKey:CONVERSION_VALUE_KEY] stringValue] forKey:CONVERSION_VALUE_KEY];
    //prev fine
    [res setValue:[[userDefaults valueForKey:P0_PREV_FINE_VALUE] stringValue] forKey:P0_PREV_FINE_VALUE];
    //current coarse
    [res setValue:[[userDefaults valueForKey:P0_COARSE] stringValue] forKey:P0_COARSE];
    [res setValue:[[userDefaults valueForKey:P1_COARSE] stringValue] forKey:P1_COARSE];
    [res setValue:[[userDefaults valueForKey:P2_COARSE] stringValue] forKey:P2_COARSE];
    //prev coarse
    [res setValue:[[userDefaults valueForKey:P0_PREV_COARSE_VALUE] stringValue] forKey:P0_PREV_COARSE_VALUE];
    [res setValue:[[userDefaults valueForKey:P1_PREV_COARSE_VALUE] stringValue] forKey:P1_PREV_COARSE_VALUE];
    [res setValue:[[userDefaults valueForKey:P2_PREV_COARSE_VALUE] stringValue] forKey:P2_PREV_COARSE_VALUE];
    //lock windows ts
    [res setValue:[[userDefaults valueForKey:P0_WINDOW_LOCK_TS] stringValue] forKey:P0_WINDOW_LOCK_TS];
    [res setValue:[[userDefaults valueForKey:P1_WINDOW_LOCK_TS] stringValue] forKey:P1_WINDOW_LOCK_TS];
    [res setValue:[[userDefaults valueForKey:P2_WINDOW_LOCK_TS] stringValue] forKey:P2_WINDOW_LOCK_TS];
    //total revenues
    [res setValue:[userDefaults valueForKey:TOTAL_REVENUE_BY_CURRENCY] forKey:TOTAL_REVENUE_BY_CURRENCY];
    [res setValue:[userDefaults valueForKey:TOTAL_ADMON_REVENUE_BY_CURRENCY] forKey:TOTAL_ADMON_REVENUE_BY_CURRENCY];
    //revenue per window
    [res setValue:[userDefaults valueForKey:TOTAL_REVENUE_P0] forKey:TOTAL_REVENUE_P0];
    [res setValue:[userDefaults valueForKey:TOTAL_REVENUE_P1] forKey:TOTAL_REVENUE_P1];
    [res setValue:[userDefaults valueForKey:TOTAL_REVENUE_P2] forKey:TOTAL_REVENUE_P2];
    [res setValue:[userDefaults valueForKey:TOTAL_ADMON_REVENUE_P0] forKey:TOTAL_ADMON_REVENUE_P0];
    [res setValue:[userDefaults valueForKey:TOTAL_ADMON_REVENUE_P1] forKey:TOTAL_ADMON_REVENUE_P1];
    [res setValue:[userDefaults valueForKey:TOTAL_ADMON_REVENUE_P2] forKey:TOTAL_ADMON_REVENUE_P2];
    //skan TS
    [res setValue:[[userDefaults valueForKey:LAST_SKAN_CALL_TIMESTAMP] stringValue] forKey:LAST_SKAN_CALL_TIMESTAMP];
    [res setValue:[[userDefaults valueForKey:FIRST_SKAN_CALL_TIMESTAMP] stringValue] forKey:FIRST_SKAN_CALL_TIMESTAMP];
    
    return res;
}


#pragma mark - internal
+ (BOOL)validateValues:(NSNumber *)conversionValue coarse:(NSNumber *)coarseValue{
    if ([conversionValue intValue] < 0 || 63 < [conversionValue intValue]) {
        return NO;
    }
    
    if (coarseValue) {
        if ([coarseValue intValue] > 2 || [coarseValue intValue] < 0) {
            return NO;
        }
    }
    
    return YES;
}

+ (NSURLComponents *)prepareQueryParams:(NSString *)bundleIdentifier eventName:(NSString *)eventName {
    NSURLComponents *components = [NSURLComponents componentsWithString:SINGULAR_API_URL];
    
    NSString *API_KEY = @"YOUR API KEY";
    NSString *APP_VERSION = @"YOUR APP VERSION";
    NSString *IDFV = @"IDFV";
    NSString *IDFA = @"IDFA";
    
    NSMutableArray *queryItems = [@[
        [NSURLQueryItem queryItemWithName:@"a" value:API_KEY],
        [NSURLQueryItem queryItemWithName:@"v" value:[[UIDevice currentDevice] systemVersion]],
        [NSURLQueryItem queryItemWithName:@"i" value:bundleIdentifier],
        [NSURLQueryItem queryItemWithName:@"app_v" value:APP_VERSION],
        [NSURLQueryItem queryItemWithName:@"n" value:eventName],
        [NSURLQueryItem queryItemWithName:@"p" value:@"iOS"],
        [NSURLQueryItem queryItemWithName:@"idfv" value:IDFV],
        [NSURLQueryItem queryItemWithName:@"idfa" value:IDFA]
    ] mutableCopy];
    
    NSDictionary *skanValues = [SKANSnippet getSkanDetails];
    [skanValues enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        if ([obj isKindOfClass:[NSDictionary class]]) {
            [queryItems addObject:[NSURLQueryItem queryItemWithName:key value:[SKANSnippet dictionaryToJsonString:obj]]];
        } else {
            [queryItems addObject:[NSURLQueryItem queryItemWithName:key value:obj]];
        }
    }];
    
    components.queryItems = queryItems;
    
    return components;
}

+ (void)getConversionValueFromServer:(NSString*)eventName withCompletionHandler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError*))handler {
    if (!lockObject) {
        lockObject = [NSLock new];
    }
    
    @try {
        // Making the lock async so it will not freeze the calling thread
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
            
            [lockObject lock];
            NSString *bundleIdentifier = @"YOUR BUNDLE IDENTIFIER";
            NSURLComponents *components = [SKANSnippet prepareQueryParams:bundleIdentifier eventName:eventName];
            
            [[[NSURLSession sharedSession] dataTaskWithURL:components.URL
                                         completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
                if (error) {
                    [lockObject unlock];
                    if (handler) {
                        handler(nil, nil, NO, error);
                    }
                    
                    return;
                }
                
                NSDictionary *parsedResponse = [SKANSnippet jsonDataToDictionary:data];
                
                if (!parsedResponse) {
                    [lockObject unlock];
                    if (handler) {
                        handler(nil,nil, NO, [NSError errorWithDomain:bundleIdentifier
                                                                 code:0
                                                             userInfo:@{NSLocalizedDescriptionKey:@"Failed parsing server response"}]);
                    }
                    
                    return;
                }
                
                NSNumber *conversionValue = [parsedResponse objectForKey:SKAN_UPDATED_CONVERSION_VALUE];
                NSNumber *coarseValue = [parsedResponse objectForKey:SKAN_UPDATED_COARSE_VALUE];
                BOOL lockWindow = [[parsedResponse objectForKey:SKAN_UPDATED_LOCK_WINDOW_VALUE] boolValue];
                
                
                if (!conversionValue) {
                    [lockObject unlock];
                    NSString *status = [parsedResponse objectForKey:@"status"];
                    
                    if (!status || ![status isEqualToString:@"ok"]) {
                        if (handler) {
                            NSString *reason = [parsedResponse objectForKey:@"reason"];
                            if (!reason) {
                                reason = @"Got error from server";
                            }
                            
                            handler(nil, nil, NO, [NSError errorWithDomain:bundleIdentifier
                                                                      code:0
                                                                  userInfo:@{NSLocalizedDescriptionKey:reason}]);
                        }
                    }
                    
                    return;
                }
                
                
                if(![SKANSnippet validateValues:conversionValue coarse:coarseValue]) {
                    [lockObject unlock];
                    if (handler) {
                        handler(nil,nil, NO, [NSError errorWithDomain:bundleIdentifier
                                                                 code:0
                                                             userInfo:@{NSLocalizedDescriptionKey:@"Illegal values received"}]);
                    }
                    
                    return;
                }
                
                if (![SKANSnippet getFirstSkanCallTimestamp]) {
                    [SKANSnippet setFirstSkanCallTimestamp];
                }
                
                [SKANSnippet setConversionValues:conversionValue coarseValue:coarseValue lockWindow:lockWindow handler:handler];
                
                [lockObject unlock];
            }] resume];
        });
    } @catch (id exception) {
        NSLog(@"%@", exception);
    }
}

+ (void)setFirstSkanCallTimestamp {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    [userDefaults setInteger:[SKANSnippet getCurrentUnixTimestamp] forKey:FIRST_SKAN_CALL_TIMESTAMP];
    
}

+ (void)setLastSkanCallTimestamp {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    [userDefaults setInteger:[SKANSnippet getCurrentUnixTimestamp] forKey:LAST_SKAN_CALL_TIMESTAMP];
    
}

+ (NSString*)dictionaryToJsonString:(NSDictionary*)dictionary {
    if (!dictionary || [dictionary count] == 0){
        return @"{}";
    }
    
    NSError *error;
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary
                                                       options:0
                                                         error:&error];
    
    if (error || !jsonData) {
        return @"{}";
    }
    
    return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}

+ (NSInteger)getFirstSkanCallTimestamp {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    return [userDefaults integerForKey:FIRST_SKAN_CALL_TIMESTAMP];
}

+ (NSInteger)getLastSkanCallTimestamp {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    return [userDefaults integerForKey:LAST_SKAN_CALL_TIMESTAMP];
}

+ (NSInteger)getCurrentUnixTimestamp {
    return [[NSDate date] timeIntervalSince1970];
}

+ (void)setConversionValues:(NSNumber *)conversionValue coarseValue:(NSNumber *)coarse lockWindow:(BOOL)lockWindow handler:(void(^)(NSNumber *, NSNumber *, BOOL, NSError*))handler {
    @try {
        __block void(^skanResultHandler)(NSError * _Nullable error) = ^(NSError * _Nullable error) {
            if (handler) {
                if (error) {
                    handler(nil, nil, NO, error);
                } else {
                    handler(conversionValue, coarse, lockWindow, nil);
                }
            }
            
            [SKANSnippet valuesHasBeenUpdated:conversionValue coarseValue:coarse lockWindow:lockWindow];
        };
        
        if (@available(iOS 16.1, *)) {
            [SKAdNetwork updatePostbackConversionValue:[conversionValue integerValue] coarseValue:[SKANSnippet resolveCoarseValueFrom:coarse] lockWindow:lockWindow completionHandler:^(NSError * _Nullable error) {
                skanResultHandler(error);
            }];
        } else {
            if (@available(iOS 15.4, *)) {
                [SKAdNetwork updatePostbackConversionValue:[conversionValue integerValue] completionHandler:^(NSError * _Nullable error) {
                    skanResultHandler(error);
                }];
            }
        }
    } @catch (id exception) {
        NSLog(@"%@", exception);
    }
}

+ (NSNumber *)getConversionValue {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    
    if (![userDefaults objectForKey:CONVERSION_VALUE_KEY]) {
        return @(0);
    }
    
    return @([userDefaults integerForKey:CONVERSION_VALUE_KEY]);
}

+ (NSDictionary*)jsonDataToDictionary:(NSData*)jsonData {
    if (!jsonData) {
        return nil;
    }
    
    NSError *error;
    NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:jsonData
                                                                options:kNilOptions error:&error];
    
    if (error || !parsedData) {
        return nil;
    }
    
    return parsedData;
}

+ (NSInteger)getCurrentSkanWindow {
    NSInteger timeDiff = [SKANSnippet getCurrentUnixTimestamp] - [SKANSnippet getFirstSkanCallTimestamp];
    if (timeDiff < firstSkan4WindowInSec) { return 0; }
    if (timeDiff < secondSkan4WindowInSec) { return 1; }
    if (timeDiff < thirdSkan4WindowInSec) { return 2; }
    
    return -1;
}

// persist updated conversion values based on the active skan window.
+ (void)valuesHasBeenUpdated:(NSNumber *)fineValue coarseValue:(NSNumber *)coarseValue lockWindow:(BOOL)lockWindow {
    NSNumber *currentPersistedFineValue;
    NSNumber *currentPersistedCoarseValue;
    NSInteger window = [SKANSnippet getCurrentSkanWindow];
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
        
    switch (window) {
    case 0:
        currentPersistedFineValue = [userDefaults objectForKey:CONVERSION_VALUE_KEY];
        currentPersistedCoarseValue = [userDefaults objectForKey:P0_COARSE];
        [userDefaults setValue:fineValue forKey:CONVERSION_VALUE_KEY];
        [userDefaults setValue:currentPersistedFineValue forKey:P0_PREV_FINE_VALUE];
        [userDefaults setValue:coarseValue forKey:P0_COARSE];
        [userDefaults setValue:currentPersistedCoarseValue forKey:P0_PREV_COARSE_VALUE];
            
        if (lockWindow) {
            [userDefaults setObject:@([SKANSnippet getCurrentUnixTimestamp]) forKey:P0_WINDOW_LOCK_TS];
        }
            
        break;
    
    case 1:
        currentPersistedCoarseValue = [userDefaults objectForKey:P1_COARSE];
        [userDefaults setValue:coarseValue forKey:P1_COARSE];
        [userDefaults setValue:currentPersistedCoarseValue forKey:P1_PREV_COARSE_VALUE];
            
        if (lockWindow) {
            [userDefaults setObject:@([SKANSnippet getCurrentUnixTimestamp]) forKey:P1_WINDOW_LOCK_TS];
        }
            
        break;
    
    case 2:
        currentPersistedCoarseValue = [userDefaults objectForKey:P2_COARSE];
        [userDefaults setValue:coarseValue forKey:P2_COARSE];
        [userDefaults setValue:currentPersistedCoarseValue forKey:P2_PREV_COARSE_VALUE];
            
        if (lockWindow) {
            [userDefaults setValue:@([SKANSnippet getCurrentUnixTimestamp]) forKey:P2_WINDOW_LOCK_TS];
        }
            
        break;
    }
    
    [SKANSnippet setLastSkanCallTimestamp];
}

+ (BOOL)isSkanWindowOver {
    NSInteger timeDiff = [SKANSnippet getCurrentUnixTimestamp] - [SKANSnippet getFirstSkanCallTimestamp];
    return thirdSkan4WindowInSec <= timeDiff;
}

// Revenues are being accumulated and saved by Ad monetization and non ad monetization events, total sum and break down by skan windows.
+ (void)addToTotalRevenue:(NSNumber *)newRevenue withCurrency:(NSString *)currency isAdmon:(BOOL)isAdmon  {
    
    NSString *key = isAdmon ? TOTAL_ADMON_REVENUE_BY_CURRENCY : TOTAL_REVENUE_BY_CURRENCY;
    [SKANSnippet addToTotalRevenue:newRevenue withCurrency:currency forKey:key];
        
    NSInteger window = [SKANSnippet getCurrentSkanWindow];
    switch (window) {
        case 0:
            key = isAdmon ? TOTAL_ADMON_REVENUE_P0 : TOTAL_REVENUE_P0 ;
            break;
        case 1:
            key = isAdmon ? TOTAL_ADMON_REVENUE_P1 : TOTAL_REVENUE_P1 ;
            break;
            
        case 2:
            key = isAdmon ? TOTAL_ADMON_REVENUE_P2 : TOTAL_REVENUE_P2 ;
            break;
        case -1:
            key = nil;
            return;
    }
    
    [SKANSnippet addToTotalRevenue:newRevenue withCurrency:currency forKey:key];
}

// Coarse value is being sent on requests and responses as an Int and being translated into the system defined coarse value upon API execution.
+ (NSString *)resolveCoarseValueFrom:(NSNumber *)value {
    if(@available(iOS 16.1, *)) {
        if (!value) {
            return nil;
        }
        
        switch ([value integerValue]) {
            case 0:
                return SKAdNetworkCoarseConversionValueLow;
            case 1:
                return SKAdNetworkCoarseConversionValueMedium;
            case 2:
                return SKAdNetworkCoarseConversionValueHigh;
            default:
                return nil;
        }
    }
    
    return nil;
}

+ (NSDictionary*)getTotalRevenue:(NSString *)revenueKey {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    
    if (![userDefaults objectForKey:revenueKey]){
        [userDefaults setObject:[NSDictionary dictionary] forKey:revenueKey];
    }
    
    return [userDefaults objectForKey:revenueKey];
}

+ (void)addToTotalRevenue:(NSNumber *)newRevenue withCurrency:(NSString *)currency forKey:(NSString *)revenueKey {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    NSMutableDictionary *revenues = [[SKANSnippet getTotalRevenue:revenueKey] mutableCopy];
    
    NSNumber *currentRevenue = @(0);

    if ([revenues objectForKey:currency]) {
        currentRevenue = [revenues objectForKey:currency];
    }

    currentRevenue = @([currentRevenue floatValue] + [newRevenue floatValue]);
    [revenues setObject:currentRevenue forKey:currency];
    [userDefaults setObject:revenues forKey:revenueKey];
    
}

@end

Actualización de la integración S2S

Mejora la integración de servidor a servidor con los metadatos de SKAdNetwork para la validación de la implementación y la resolución de problemas (recomendado para todas las implementaciones).

Estructura de metadatos

Recuperación de datos

Utilice el método getSkanDetails para extraer el diccionario de metadatos y enviarlo al servidor para añadirlo como parámetro de consulta en las solicitudes de API de punto final de sesión y evento.

Crítico: Los metadatos deben enviarse en cada sesión y cada evento notificado a Singular a través del punto final de notificación de sesionesy el punto final de notificación de eventos.

Objective-C
NSDictionary *skanMetadata = [SKANSnippet getSkanDetails];

// Forward skanMetadata to your server for S2S API enrichment

Implementación del ciclo de vida de la aplicación

Integre los métodos de SKAdNetwork en los puntos adecuados del ciclo de vida de la aplicación para una cobertura completa de la atribución con las funciones de SKAN 4.0.

Ejemplos de implementación

Notas de implementación:

  • Utiliza métodos asíncronos para las actualizaciones de los valores de conversión a fin de evitar el bloqueo del subproceso principal.
  • Todos los datos relacionados con SKAN se recopilan en formato de diccionario antes de la transmisión al servidor
  • Sigue el enfoque de Apple de dar prioridad a la privacidad al tiempo que permite un seguimiento esencial de las atribuciones
  • El seguimiento de los ingresos incluye el valor monetario y la especificación de la moneda para obtener informes precisos

Primer lanzamiento de la aplicación

Registra la aplicación en SKAdNetwork para el seguimiento de la atribución y envía los datos de la sesión inicial al punto final de Singular. Sólo se ejecuta en el primer lanzamiento de la aplicación para establecer el seguimiento de la atribución.

Objective-C
[SKANSnippet registerAppForAdNetworkAttribution];
NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendSessionToServer:skanValues]; // to Singular launch EP

Gestión de sesiones

Actualiza los valores de conversión después de cada sesión y envía detalles actualizados de SKAN para realizar un seguimiento de la participación del usuario.

Objective-C
[SKANSnippet updateConversionValuesAsync:^(NSNumber *fine, NSNumber *coarse, BOOL lock, NSError *error) {
    if (error) {
        NSLog(@"Conversion value update failed: %@", error);
    } else {
        NSLog(@"Values updated - Fine: %@, Coarse: %@, Lock: %d", fine, coarse, lock);
    }
}];

NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendSessionToServer:skanValues];

Seguimiento de eventos

Gestiona los eventos que no generan ingresos actualizando los valores de conversión y enviando los datos del evento al punto final de eventos de Singular.

Objective-C
[SKANSnippet updateConversionValuesAsync:@"event_name" 
                   withCompletionHandler:^(NSNumber *fine, NSNumber *coarse, BOOL lock, NSError *error) {
    if (error) {
        NSLog(@"Event conversion update failed: %@", error);
    } else {
        NSLog(@"Event values updated - Fine: %@, Coarse: %@", fine, coarse);
    }
}];

NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendEventToServer:skanValues eventName:@"event_name"];

Seguimiento de ingresos

Gestiona los eventos de ingresos actualizando el importe de los ingresos con la moneda y los valores de conversión asociados, y enviándolos al punto final de eventos de Singular para las actividades relacionadas con las compras.

Objective-C
[SKANSnippet updateRevenue:15.53 andCurrency:@"USD" isAdMonetization:NO];
[SKANSnippet updateConversionValuesAsync:@"revenue_event_name" 
                   withCompletionHandler:^(NSNumber *fine, NSNumber *coarse, BOOL lock, NSError *error) {
    if (error) {
        NSLog(@"Revenue conversion update failed: %@", error);
    } else {
        NSLog(@"Revenue values updated - Fine: %@, Coarse: %@", fine, coarse);
    }
}];

NSDictionary *skanValues = [SKANSnippet getSkanDetails];
[self sendEventToServer:skanValues eventName:@"revenue_event_name"];

API de valor de conversión

Enfoque alternativo del lado del servidor para informar de los valores de conversión de SKAdNetwork utilizando el punto final de la API REST en lugar de la implementación de la interfaz del lado del cliente.

Descripción general de la API

Métodos de implementación

Los valores de conversión de SKAdNetwork pueden notificarse a través de dos métodos con idéntico flujo de datos e integridad de notificación.

  1. Interfaz directa SKAdNetwork: Implementación del lado del cliente (véase más arriba)
  2. Integración del lado del servidor: Uso del punto final de la API de valores de conversión

El punto final de la API de valor de conversión acepta parámetros idénticos a los de la interfaz del lado del cliente, lo que garantiza un seguimiento coherente de la atribución con flexibilidad para elegir la implementación que mejor se adapte a la arquitectura técnica.


Punto final de API

Método HTTP y URL

GET https://sdk-api-v1.singular.net/api/v2/conversion_value

Parámetros requeridos

Clave API

Parámetro Descripción
a Clave SDK singular de las Herramientas para Desarrolladores. No utilice la Clave API de informes.

Ejemplo: sdkKey_afdadsf7asf56

Identificadores de dispositivos

Parámetro Descripción
idfa Identificador para anunciantes (IDFA) para el seguimiento y la atribución de anuncios. A partir de iOS 14.5, requiere la aceptación de ATT framework. Omítalo si no está disponible (no pase NULL ni una cadena vacía).

Ejemplo:DFC5A647-9043-4699-B2A5-76F03A97064B
idfv Identificador para proveedores (IDFV): obligatorio en todas las solicitudes, independientemente del estado de la ATT. Único por proveedor/desarrollador en todo su ecosistema de aplicaciones.

Ejemplo:21DB6612-09B3-4ECC-84AC-B353B0AF1334

Parámetros del dispositivo

Parámetro Descripción
p Plataforma de la aplicación (debe ser "iOS" para esta API).

Ejemplo iOS
v Versión del sistema operativo del dispositivo en el momento de la sesión.

Ejemplo: 16.1

Parámetros de aplicación

Parámetro Descripción
i App Identifier (ID de paquete para aplicación iOS, distingue mayúsculas de minúsculas).

Ejemplo com.singular.app
app_v Versión de la aplicación.

Ejemplo: Versión de la aplicación: 1.2.3

Parámetros de Evento

Parámetro Descripción
n Nombre del evento rastreado (máximo 32 caracteres ASCII). Para sesiones, utilice __SESSION__. Para los eventos, utilice el mismo nombre y el mismo tipo de letra que se envían a Singular a través de la API de eventos.

Ejemplo sng_add_to_cart

Parámetros de valor de conversión

Parámetro Descripción
skan_current_conversion_value
iOS 15.4+
Último valor de conversión de SKAdNetwork en el momento de la sesión/evento anterior (0-63).

Ejemplo: 7
p1_coarse
iOS 16.1+
Último valor de conversión grueso de SKAdNetwork para postback_sequence 1 (0-2).

Ejemplo: 0
p2_coarse
iOS 16.1+
Último valor de conversión grueso de SKAdNetwork para postback_sequence 2 (0-2).

Ejemplo: 1

Parámetros de seguimiento de ingresos

Parámetro Descripción
skan_total_revenue_by_currency
iOS 15.4+
Obligatorio para los modelos IAP o Todos los ingresos. Total agregado actual de ingresos de IAP (excluida la monetización de anuncios), cadena codificada con URL JSON.

Ejemplo: %7B%22USD%22%3A9.99%7D
skan_total_admon_revenue_by_currency
iOS 15.4+
Necesario para los modelos Admon o Todos los ingresos. Total agregado actual de ingresos por monetización de anuncios, cadena codificada con URL JSON.

Ejemplo: %7B%22USD%22%3A1.2%7D

Parámetros de marca de tiempo

Parámetro Descripción
skan_first_call_to_skadnetwork_timestamp
iOS 15.4+
Marca de tiempo Unix de la primera llamada a la API SKAdNetwork subyacente.

Ejemplo: 1483228800
skan_last_call_to_skadnetwork_timestamp
iOS 15.4+
Unix timestamp de la última llamada a la API SKAdNetwork subyacente en el momento de la notificación de esta sesión.

Ejemplo: 1483228800

Ejemplos de solicitud

Ejemplos de implementación

Los ejemplos de código muestran los parámetros básicos necesarios. Cuando los implemente, incluya todos los parámetros requeridos y valide los valores correctos antes de su uso en producción.

PythoncURLHTTP
import requests

params = {
    'a': 'sdk_key_here',
    'p': 'iOS',
    'i': 'com.singular.app',
    'v': '16.1',
    'idfa': 'DFC5A647-9043-4699-B2A5-76F03A97064B',
    'idfv': '21DB6612-09B3-4ECC-84AC-B353B0AF1334',
    'n': '__SESSION__',
    'app_v': '1.2.3',
    'skan_current_conversion_value': 7,
    'p1_coarse': 0,
    'p2_coarse': 1,
    'skan_total_revenue_by_currency': {"USD":9.99},
    'skan_total_admon_revenue_by_currency': {"USD":1.2},
    'skan_first_call_to_skadnetwork_timestamp': 1510090877,
    'skan_last_call_to_skadnetwork_timestamp': 1510090877
}

response = requests.get('https://sdk-api-v1.singular.net/api/v2/conversion_value', params=params)
print(response.json())

Formato de respuesta

Respuesta correcta

HTTP 200 - respuesta ok sin error o razón indica que la solicitud se ha enviado a la cola para su procesamiento.

{
   "conversion_value":1,
   "skan_updated_coarse_value":0,
   "postback_sequence_index":0,
   "status":"ok"
}

Parámetros de respuesta

Clave Descripción Ejemplo
conversion_value Nuevo valor de conversión fina 0-63
skan_updated_coarse_value Nuevo valor de conversión gruesa 0-2
postback_sequence_index Periodo de medición SKAN postback (0=postback 1, 1=postback 2, 2=postback 3). Indica qué clave de valor grueso actualizar 0-2
status Estado de procesamiento ok

Posibles errores

  • Han pasado más de 24 horas desde la última actualización de la conversión (28032 horas), ventana de actualización cerrada
  • Error de plataforma desconocida - Plataforma no iOS
  • Gestión de conversión: Parámetro no válido introducido
  • Gestión de conversiones: Modelo de conversión no encontrado para la aplicación
  • Periodo de medición no válido
  • Gestión de conversión: No se encuentra la moneda del propietario