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.
- Saiba mais sobre a SKAdNetwork 4.0 nas Perguntas freqüentes sobre a SKAN 4.0
- Para obter informações sobre a SKAdNetwork, consulte o Guia de implementação S2S da SKAdNetwork 3.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):
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.
- Registro de SKAdNetwork: Regista a aplicação na estrutura imediatamente após o lançamento para permitir a atribuição
-
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.
- 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
- Atualização da framework: a aplicação actualiza a framework SKAdNetwork com o valor de conversão recebido
- Enriquecimento de metadados: A aplicação enriquece os eventos e sessões S2S com metadados SKAdNetwork para validação
- Expiração do temporizador: Após a expiração do timer, a SKAdNetwork envia um postback para a rede de anúncios
- Encaminhamento de postback: A rede encaminha o postback para a Singular (configuração segura ou regular)
-
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.
+ (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.
+ (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.
+ (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.
+ (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
+ (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
//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.
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.
// 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.
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.
[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.
[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.
[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.
[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.
- Interface SKAdNetwork direta: Implementação do lado do cliente (ver acima)
- 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_valueiOS 15.4+ |
Valor de conversão SKAdNetwork mais recente no momento da sessão/evento anterior (0-63). Exemplo: 7
|
p1_coarseiOS 16.1+ |
Último valor de conversão grosseiro de SKAdNetwork para postback_sequence 1 (0-2). Exemplo: 0
|
p2_coarseiOS 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_currencyiOS 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_currencyiOS 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_timestampiOS 15.4+ |
Carimbo de data/hora Unix da primeira chamada à API SKAdNetwork subjacente. Exemplo: 1483228800
|
skan_last_call_to_skadnetwork_timestampiOS 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.
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())
curl -G 'https://sdk-api-v1.singular.net/api/v2/conversion_value' \
--data-urlencode 'a=sdk_key_here' \
--data-urlencode 'p=iOS' \
--data-urlencode 'i=com.singular.app' \
--data-urlencode 'v=16.1' \
--data-urlencode 'idfa=DFC5A647-9043-4699-B2A5-76F03A97064B' \
--data-urlencode 'idfv=21DB6612-09B3-4ECC-84AC-B353B0AF1334' \
--data-urlencode 'n=__SESSION__' \
--data-urlencode 'app_v=1.2.3' \
--data-urlencode 'skan_current_conversion_value=7' \
--data-urlencode 'p1_coarse=0' \
--data-urlencode 'p2_coarse=1' \
--data-urlencode 'skan_total_revenue_by_currency={"USD":9.99}' \
--data-urlencode 'skan_total_admon_revenue_by_currency={"USD":1.2}' \
--data-urlencode 'skan_first_call_to_skadnetwork_timestamp=1510040127' \
--data-urlencode 'skan_last_call_to_skadnetwork_timestamp=1510090877'
GET /api/v2/conversion_value
?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=%7B%22USD%22%3A9.99%7D
&skan_total_admon_revenue_by_currency=%7B%22USD%22%3A1.2%7D
&skan_first_call_to_skadnetwork_timestamp=1510090877
&skan_last_call_to_skadnetwork_timestamp=1510090877 HTTP/1.1
Host: sdk-api-v1.singular.net
Accept: application/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