Server-to-Server - Guia de implementação da SKAdNetwork 4

Documento

Guia de implementação de SKAdNetwork 4.0

Implemente a estrutura SKAdNetwork 4.0 da Apple para atribuição de iOS focada na privacidade utilizando a integração servidor a servidor, permitindo a medição de campanhas de instalação de aplicações com janelas de postback melhoradas e valores de conversão grosseiros, ao mesmo tempo que protege a privacidade do utilizador.


Visão geral

O que é SKAdNetwork 4.0?

SKAdNetwork (SKAN) é a estrutura de atribuição focada na privacidade da Apple que permite a mensuração de campanhas publicitárias de instalação de aplicativos iOS, protegendo a privacidade do usuário através da implementação de servidor para servidor para validação e rastreamento robustos.

A estrutura lida com todos os aspectos críticos da atribuição, mantendo a privacidade do usuário através dos métodos prescritos pela Apple, tornando-a essencial para os profissionais de marketing mobile que operam no cenário pós-iOS 14.5.


Caraterísticas principais

SKAdNetwork 4.0 introduz capacidades de medição melhoradas e flexibilidade para a otimização de campanhas.

  • Validação robusta: Agregação de postbacks de todas as redes com proteção integrada contra fraudes
  • Gerenciamento dinâmico de conversões: Configuração do painel para codificação do valor de conversão
  • Relatórios melhorados: Parâmetros de marketing enriquecidos e informações de dados granulares
  • Postbacks seguros de parceiros: Valores de conversão descodificados e controlo de receitas
  • Validação abrangente: Acompanhamento enriquecido de eventos e sessões para verificação da implementação
  • Múltiplas janelas de postback: Gestão automatizada de carimbos de data/hora em 0-2 dias, 3-7 dias, 8-35 dias
  • Controlo de receitas: Suporte para monetização de anúncios e receita regular com especificações de moeda

Pré-requisitos

Familiarize-se com os conceitos da SKAdNetwork e com as versões anteriores antes de implementar o SKAN 4.0.


Solução Singular SKAdNetwork

A solução SKAdNetwork da Singular fornece gerenciamento de atribuição de ponta a ponta, desde a implementação no lado do cliente até o processamento de postback e otimização da campanha.

Componentes da solução

Caraterísticas da plataforma

Suporte abrangente de SKAdNetwork em todo o fluxo de trabalho de atribuição e análise.

Componente Funcionalidade
Código do lado do cliente Amostras de código iOS nativo para registro de estrutura SKAdNetwork e gerenciamento de valor de conversão. Abordagem alternativa do lado do servidor disponível usando o endpoint da API de valor de conversão
Processamento de postbacks Validação e agregação de postbacks de todas as redes de anúncios com relatórios unificados
Proteção contra fraudes Validação de assinatura criptográfica, deduplicação de ID de transação e verificação segura de parâmetros para dados não assinados
Gestão de conversões Configuração dinâmica do painel de controlo para codificar actividades pós-instalação em valores de conversão
Relatórios Tradução de ID de campanha e enriquecimento com parâmetros de marketing para análise granular
Postbacks de parceiros Valores de conversão descodificados enviados como eventos e receitas para otimização do parceiro

Arquitetura de implementação

Implementação em duas partes

A implementação de SKAdNetwork no lado do cliente consiste em dois componentes principais.

1. Implementação do lado do cliente (obrigatória):

  • Registo da estrutura SKAdNetwork no lançamento da aplicação
  • Gestão inteligente do valor de conversão com base na atividade pós-instalação
  • Essencial para a otimização de campanhas usando a atribuição de SKAdNetwork
  • Permite o acompanhamento das actividades pós-instalação associadas

2. Atualização da integração do lado do servidor (recomendada):

  • Valida e soluciona problemas de implementação no lado do cliente
  • Enriquece os eventos e sessões enviados através dos pontos finais Sessione Event
  • Permite a validação dos metadados da SKAdNetwork
  • Confirma a implementação correta do lado da aplicação

Implementação do lado do cliente

Implementar o registro da estrutura SKAdNetwork e o gerenciamento do valor de conversão usando exemplos de código iOS nativos da Singular para otimizar a mensuração de campanhas com os recursos do SKAN 4.0.

Responsabilidades de implementação

Funcionalidade principal

As amostras de código suportam o registo de SKAdNetwork e a gestão inteligente do valor de conversão.

  1. Registro de SKAdNetwork: Regista a aplicação na estrutura imediatamente após o lançamento para permitir a atribuição
  2. Gerenciamento de valor de conversão:
    • Comunica-se de forma síncrona com o endpoint Singular para receber o próximo valor de conversão
    • Relata sessões, eventos e receita para a Singular
    • Recebe o valor de conversão codificado que representa a atividade pós-instalação configurada
    • Coleta metadados de SKAdNetwork por período de medição para validação e cálculo

Recolha de metadados

O código recolhe os metadados essenciais da SKAdNetwork para validação e cálculo do valor de conversão.

  • Registo de data e hora da primeira chamada para a estrutura SKAdNetwork subjacente
  • Último registo de data e hora da chamada para a estrutura SKAdNetwork subjacente
  • Valores de postback actualizados pela última vez (tanto Coarse como Fine)
  • Total Revenue e Total Ad Monetization Revenue gerados por dispositivo

Fluxo de integração

Processo de ponta a ponta

Fluxo completo de SKAdNetwork para clientes S2S, desde o gerenciamento de conversões no lado do cliente até o processamento de postback.

SKAdNetwork 4.0 S2S Integration Flow

  1. Solicitação de valor de conversão: O código do aplicativo se comunica com o endpoint Singular de forma síncrona para obter o valor de conversão mais recente com base em sessões, eventos e receita
  2. Atualização da framework: a aplicação actualiza a framework SKAdNetwork com o valor de conversão recebido
  3. Enriquecimento de metadados: A aplicação enriquece os eventos e sessões S2S com metadados SKAdNetwork para validação
  4. Expiração do temporizador: Após a expiração do timer, a SKAdNetwork envia um postback para a rede de anúncios
  5. Encaminhamento de postback: A rede encaminha o postback para a Singular (configuração segura ou regular)
  6. Processamento do postback: A Singular processa o postback por
    • Validação da assinatura criptográfica
    • Decodificação do valor de conversão usando o modelo configurado
    • Enriquecimento com informações de rede de integrações de parceiros
    • Envio de dados descodificados para o BI e parceiros através de postbacks

Separação de dados: Dados de SKAdNetwork (instalações e eventos descodificados) acessíveis através de relatórios separados, APIs, tabelas ETL e postbacks para evitar a mistura com conjuntos de dados existentes durante o teste e a validação.


Interface SKAdNetwork

Definição completa da interface para a integração da SKAdNetwork, fornecendo métodos para o acompanhamento da atribuição, actualizações do valor de conversão e gestão de receitas.

Definições de métodos

Registo de atribuição

Inicializa o rastreamento de atribuição de SKAN no lançamento do primeiro aplicativo, definindo o valor de conversão inicial como 0 e estabelecendo carimbos de data/hora de linha de base.

Objective-C
+ (void)registerAppForAdNetworkAttribution;

Gestão de valores de conversão

Os métodos atualizam os valores de conversão com base na atividade pós-instalação capturada pelo aplicativo e no modelo de conversão selecionado configurado dinamicamente.

Atividades suportadas:

  • Sessões: Crítico para a medição de retenção
  • Eventos de conversão: Crítico para a medição de eventos pós-instalação
  • Eventos de receita: Crítico para a medição de receitas

Rastreamento de sessão

Gerencia o rastreamento baseado em sessão para retenção e análise de coorte com o manipulador de conclusão opcional para ações pós-atualização.

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

Rastreamento de eventos

Trata o rastreamento de eventos de conversão antes de enviar dados para Singular, atualizando os valores de conversão com base no contexto do evento.

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

Gestão de receitas

Rastreia eventos de receita, mantendo totais separados para monetização de anúncios e receita regular. Deve ser chamado antes das atualizações dos valores de conversão.

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

Recuperação de dados

Retorna o dicionário de dados SKAN abrangente, incluindo valores de conversão, carimbos de data/hora e rastreamento de receita.

O dicionário contém:

  • Valores de conversão refinados atuais e anteriores
  • Valores grosseiros em diferentes janelas de postback
  • Carimbos de data e hora de bloqueio de janela
  • Controlo de receitas por moeda
  • Acompanhamento separado para monetização de anúncios e receita regular
Objective-C
+ (NSDictionary *)getSkanDetails;

Notas de implementação

  • Os métodos utilizam padrões assíncronos para evitar o bloqueio da thread principal
  • O acompanhamento das receitas deve preceder as actualizações dos valores de conversão
  • Suporta valores de conversão de granularidade fina (0-63) e grosseira (Baixa/Média/Alta)
  • Mantém um acompanhamento separado para diferentes janelas de postback
  • Implementa tratamento de erros abrangente através de manipuladores de conclusão

Código completo da interface

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

Implementação da SKAdNetwork

Código de implementação completo para a interface SKAdNetwork 4.0 da Apple, gerindo o controlo de atribuição, os valores de conversão e os relatórios de receitas em várias janelas de postback.

Descrição geral da implementação

Constantes e configuração

A implementação define três janelas de postback distintas para acompanhar a atividade do utilizador e as conversões.

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

Caraterísticas principais

  • Registo de Atribuição: Trata da configuração inicial de atribuição de aplicativos e do rastreamento do valor de conversão pela primeira vez
  • Gerenciamento de conversões: Atualiza e rastreia os valores de conversão em várias janelas de postback
  • Rastreamento de receita: Mantém o rastreamento separado para monetização de anúncios e eventos regulares de receita
  • Persistência de dados: Usa NSUserDefaults para armazenar dados relacionados ao SKAN em sessões de aplicativos
  • Segurança de thread: Implementa NSLock para operações thread-safe durante chamadas de rede

Estrutura de armazenamento de dados

  • Valores de conversão de granulação fina (0-63)
  • Valores grosseiros (Baixo/Médio/Alto mapeados para 0-2)
  • Controlo de receitas por moeda para cada janela de postback
  • Gestão de carimbos temporais para janelas de postback e estados de bloqueio
  • Controlo de valores anteriores para conversões finas e grosseiras

Considerações sobre privacidade

  • Implementa funcionalidades específicas do iOS 15.4+ e do iOS 16.1
  • Trata as actualizações de valores de conversão de postback de acordo com as diretrizes de privacidade da Apple
  • Mantém o rastreamento separado para diferentes tipos de receita para garantir uma atribuição precisa

Notas técnicas

  • Utiliza operações assíncronas para chamadas de rede e actualizações de valores
  • Implementa tratamento de erros e validação para valores de conversão
  • Suporta o controlo de valores de conversão tradicional e grosseiro
  • Gerencia várias janelas de postback com diferentes durações e requisitos

Código de implementação completo

SKANSnippet.m

Importante: Substitua os valores do marcador de posição (SUA CHAVE DE API, SUA VERSÃO DE APLICAÇÃO, etc.) por valores reais da sua aplicação antes da utilização na produção.

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

Atualização da integração S2S

Melhoria da integração servidor a servidor com metadados SKAdNetwork para validação da implementação e resolução de problemas (recomendado para todas as implementações).

Estrutura de metadados

Recuperação de dados

Utilizar o método getSkanDetails para extrair o dicionário de metadados e reencaminhar para o servidor para anexar como parâmetros de consulta nos pedidos de API de ponto final Session e Event.

Crítico: Os metadados devem ser encaminhados em todas as sessões e em todos os eventos reportados à Singular através do Session Notification Endpointe do Event Notification Endpoint.

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

// Forward skanMetadata to your server for S2S API enrichment

Implementação do ciclo de vida da aplicação

Integre os métodos SKAdNetwork nos pontos apropriados do ciclo de vida do aplicativo para obter uma cobertura de atribuição completa com os recursos do SKAN 4.0.

Exemplos de implementação

Notas de implementação:

  • Utiliza métodos assíncronos para actualizações de valores de conversão para evitar o bloqueio da thread principal
  • Todos os dados relacionados com o SKAN são recolhidos em formato de dicionário antes da transmissão para o servidor
  • Segue a abordagem de privacidade da Apple em primeiro lugar, ao mesmo tempo que permite o rastreio de atribuição essencial
  • O controlo de receitas inclui o valor monetário e a especificação da moeda para relatórios precisos

Primeiro lançamento do aplicativo

Regista a aplicação na SKAdNetwork para rastreio de atribuição e envia os dados iniciais da sessão para o endpoint da Singular. Só é executado no primeiro lançamento do aplicativo para estabelecer o rastreamento de atribuição.

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

Gerenciamento de sessão

Actualiza os valores de conversão após cada sessão e envia detalhes SKAN actualizados para acompanhar o envolvimento do utilizador.

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];

Rastreamento de eventos

Lida com eventos que não geram receita, atualizando os valores de conversão e enviando os dados do evento para o endpoint de eventos da 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"];

Rastreamento de receita

Gerencia eventos de receita atualizando o valor da receita com a moeda e os valores de conversão associados e, em seguida, enviando para o endpoint de evento da Singular para atividades relacionadas à compra.

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 conversão

Abordagem alternativa do lado do servidor para reportar os valores de conversão da SKAdNetwork utilizando o endpoint REST API em vez da implementação da interface do lado do cliente.

Visão geral da API

Métodos de implementação

Os valores de conversão da SKAdNetwork podem ser comunicados através de dois métodos com fluxo de dados e integridade de comunicação idênticos.

  1. Interface SKAdNetwork direta: Implementação do lado do cliente (ver acima)
  2. Integração do lado do servidor: Utilizando o ponto final da API de valores de conversão

O Conversion Value API Endpoint aceita parâmetros idênticos aos da interface do lado do cliente, garantindo um acompanhamento consistente da atribuição com flexibilidade para escolher a implementação que melhor se adapta à arquitetura técnica.


Ponto de extremidade da API

Método HTTP e URL

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

Parâmetros necessários

Chave da API

Parâmetro Descrição
a Chave SDK única das ferramentas de desenvolvimento. Não utilizar a chave API de comunicação.

Exemplo: sdkKey_afdadsf7asf56

Identificadores de dispositivos

Parâmetro Descrição
idfa Identificador para anunciantes (IDFA) para rastreio e atribuição de anúncios. A partir do iOS 14.5, requer a opção de adesão à estrutura da ATT. Omitir se não estiver disponível (não passar NULL ou uma cadeia de caracteres vazia).

Exemplo:DFC5A647-9043-4699-B2A5-76F03A97064B
idfv Identificador para fornecedores (IDFV) - necessário em todos os pedidos, independentemente do estado da ATT. Único por fornecedor/desenvolvedor em todo o seu ecossistema de aplicações.

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

Parâmetros do dispositivo

Parâmetro Descrição
p Plataforma da aplicação (tem de ser "iOS" para esta API).

Exemplo: iOS
v Versão do SO do dispositivo no momento da sessão.

Exemplo: 16.1

Parâmetros da aplicação

Parâmetro Descrição
i Identificador da aplicação (ID do pacote para a aplicação iOS, sensível a maiúsculas e minúsculas).

Exemplo: com.singular.app
app_v Versão da aplicação.

Exemplo: Versão da aplicação: 1.2.3

Parâmetros de eventos

Parâmetro Descrição
n Nome do evento que está a ser controlado (máximo de 32 caracteres ASCII). Para sessões, utilizar __SESSION__. Para eventos, use o mesmo nome e caixa enviados ao Singular via API de eventos.

Exemplo: sng_add_to_cart

Parâmetros de valor de conversão

Parâmetro Descrição
skan_current_conversion_value
iOS 15.4+
Valor de conversão SKAdNetwork mais recente no momento da sessão/evento anterior (0-63).

Exemplo: 7
p1_coarse
iOS 16.1+
Último valor de conversão grosseiro de SKAdNetwork para postback_sequence 1 (0-2).

Exemplo: 0
p2_coarse
iOS 16.1+
Valor mais recente de conversão grosseira de SKAdNetwork para postback_sequence 2 (0-2).

Exemplo: 1

Parâmetros de rastreamento de receita

Parâmetro Descrição
skan_total_revenue_by_currency
iOS 15.4+
Necessário para os modelos IAP ou Todas as receitas. Total agregado atual das receitas de IAP (excluindo a monetização de anúncios), cadeia de caracteres JSON codificada por URL.

Exemplo: %7B%22USD%22%3A9.99%7D
skan_total_admon_revenue_by_currency
iOS 15.4+
Necessário para os modelos Admon ou Todas as receitas. Total agregado atual da receita de monetização de anúncios, cadeia de caracteres JSON codificada por URL.

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

Parâmetros de carimbo de data/hora

Parâmetro Descrição
skan_first_call_to_skadnetwork_timestamp
iOS 15.4+
Carimbo de data/hora Unix da primeira chamada à API SKAdNetwork subjacente.

Exemplo: 1483228800
skan_last_call_to_skadnetwork_timestamp
iOS 15.4+
Carimbo de data/hora Unix da última chamada à API SKAdNetwork subjacente no momento da notificação desta sessão.

Exemplo: 1483228800

Exemplos de pedidos

Implementações de amostra

As amostras de código demonstram os principais parâmetros necessários. Ao implementar, inclua todos os parâmetros necessários e valide os valores corretos antes da utilização na produção.

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 da resposta

Resposta bem-sucedida

HTTP 200 - resposta ok sem erro ou motivo indica que o pedido foi enviado para a fila de espera para processamento.

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

Parâmetros de resposta

Chave Descrição Exemplo
conversion_value Novo valor de conversão fina 0-63
skan_updated_coarse_value Novo valor de conversão grosseira 0-2
postback_sequence_index Período de medição do postback SKAN (0=postback 1, 1=postback 2, 2=postback 3). Indica qual a chave de valor grosseiro a atualizar 0-2
status Estado de processamento ok

Erros possíveis

  • Passaram mais de 24 horas desde a última atualização de conversão (28032 horas), janela de atualização fechada
  • Erro de plataforma desconhecida - plataforma não iOS
  • Gestão de conversões: Parâmetro inválido fornecido
  • Gestão de conversões: Modelo de conversão não encontrado para a aplicação
  • Período de medição inválido
  • Gestão de Conversões: Não é possível encontrar a moeda do proprietário