Visão geral da SKAdNetwork
A SKAdNetwork é uma estrutura fornecida pela Apple para permitir a medição de campanhas publicitárias de instalação de aplicativos no iOS sem comprometer a privacidade(saiba mais). A estrutura ajuda a medir as taxas de conversão das campanhas de instalação de aplicações sem comprometer os identificadores dos utilizadores.
Como funciona a SKAdNetwork?
Na SKAdNetwork, o processo de atribuição é conduzido pela App Store através dos servidores da Apple. A informação de atribuição é depois desligada dos identificadores dos utilizadores e da informação temporal e enviada para a rede.
Quando um anúncio é clicado e a loja é aberta, a aplicação de publicação e a rede fornecem algumas informações básicas, como a rede, o editor e o ID da campanha. Se a aplicação do anunciante tiver sido lançada e registada na SKAdNetwork, o dispositivo enviará uma notificação de conversão bem sucedida para a rede. O dispositivo comunicará os valores anexados juntamente com um valor de conversão que pode ser comunicado pela aplicação anunciada.
Essa notificação será enviada pelo menos 24 horas após o primeiro lançamento e não conterá quaisquer informações de identificação do dispositivo ou do utilizador.
Além disso, a App Store conduz o processo de modo a que a aplicação anunciada não tenha conhecimento do anúncio original e do editor. Desta forma, a rede é notificada de uma instalação sem saber nada sobre o utilizador que a instala.
O que esperar ao usar a SKAdNetwork?
A SKAdNetwork tem algumas vantagens importantes. Fornece-lhe todas as informações seguintes:
- Atribuição do último clique que funciona sem consentimento
- Detalhamento de fontes, campanhas e editores
- Valores de conversão pós-instalação (até 64 valores discretos)
- Assinaturas criptográficas para validar as instalações
No entanto, na sua forma atual, a SKAdNetwork é simples e requer uma implementação e coordenação cuidadosa entre várias entidades para garantir o seu funcionamento.
Aqui estão algumas das limitações actuais da SKAdNetwork:
- Sem dados ao nível do utilizador
- Sem atribuição de visualização
-
Gama limitada de valores de conversão:
- Um único evento de conversão por instalação/reinstalação
- Até 64 valores de conversão (6 bits)
-
Granularidade limitada:
- Até 100 valores de campanha
- Nenhuma representação para o grupo de anúncios e o nível criativo
- Sem LTV / coortes longas
-
Exposição a fraudes:
- O valor de conversão em si não é assinado (pode ser manipulado)
- Os postbacks podem ser duplicados.
Para ultrapassar alguns destes problemas, a Singular lançou uma norma pública para a implementação da SKAdNetwork, bem como várias publicações no blogue que o podem ajudar a navegar na SKAdNEtwork:
- SKAN: Um padrão prático para a implementação da SKAdNetwork
- SKAdNetwork 101 - O que é? O que significa para si?
- Código SKAdNetwork: A Singular lança um repositório no Github com código para redes de publicidade, editores e anunciantes
- Como testar a SKAdNetwork: instruções passo-a-passo
- Singular anuncia o primeiro suporte ao mercado para substituir o IDFA
- Medição avançada usando SKAdNetwork: Desbloqueando ROAS
- SKAdNetwork 2.0 segura: Uma configuração perfeita para estabelecer confiança
Implementação da SKAdNetwork S2S
A solução Singular SKAdNetwork consiste nos seguintes componentes:
- Código do lado do cliente para implementar a SKAdNetwork
- Validação e agregação de postback de todas as redes
-
Proteção contra fraudes:
- Validação de assinaturas e redução do ID da transação.
- Configuração segura com as redes para verificar os parâmetros que não são assinados (valor de conversão e geodados).
- Gestão do valor de conversão: permite configurar dinamicamente no dashboard da Singular qual a atividade pós-instalação que deve ser codificada no valor de conversão SKAdNetwork.
- Relatórios: Traduzir o ID limitado da campanha SKAdNetwork e enriquecer os dados com mais parâmetros e granularidades de marketing
- Postbacks de parceiros: Fornece o envio de postbacks SKAdNetwork com um valor de conversão descodificado em eventos e receitas, o que é fundamental para a sua otimização.
Para os clientes que utilizam uma integração Singular de servidor para servidor, a implementação de SKAdNetwork consiste em duas partes principais:
- Implementação do lado do cliente de SKAdNetwork: Esta parte é fundamental para registar as suas aplicações na SKAdNetwork e gerir o valor de conversão da SKAdNetwork de forma inteligente. Isto significa que, ao implementá-la, será capaz de otimizar as suas campanhas com base nas atribuições de SKAdNetwork e nas actividades pós-instalação associadas.
- Atualização da integração S2S (opcional): Esta parte é importante para a validação e resolução de problemas da implementação do lado do cliente. Ao enriquecer os eventos e sessões enviados para o SIngular hoje através dos endpoints de sessão e evento S2S do Singular com dados da SKAdNetwork (valores de conversão e carimbos de data/hora de atualização), o Singular pode validar se a implementação foi feita corretamente no lado do aplicativo.
Implementação do lado do cliente da SKAdNetwork
A Singular fornece exemplos de código que suportam o registo na SKAdNetwork e a gestão do valor de conversão. Esses exemplos de código são responsáveis pelas seguintes partes:
- Suporte e registo de SKAdnetwork
- Gestão do valor de conversão:
- O código comunica de forma síncrona com um endpoint da Singular para receber o próximo valor de conversão com base no modelo de conversão configurado. Comunica eventos/sessões/receitas e, em resposta, obtém o próximo valor de conversão, que é um número codificado que representa a atividade pós-instalação que foi configurada para medição no painel de controlo do Singular.
- O código também recolhe metadados SKAdnetwork que são utilizados para validação e cálculo do valor da próxima conversão:
- O carimbo de data/hora da primeira chamada para a estrutura SKAdNetwork subjacente
- O carimbo de data/hora da última chamada à estrutura SKAdNetwork subjacente
- O último valor de conversão atualizado
Atualização da integração S2S
Obrigatório
Assim que tiver um modelo de conversão ativo, os nossos pontos finais S2S começarão a devolver um novo campo int chamado "conversion_value", que conterá o próximo valor a atualizar no código do lado do cliente.
Opcional
Para validar a integração e solucionar possíveis problemas de implementação, recomendamos o uso de nossos exemplos de código para estender sua integração S2S atual. Os endpoints de sessão e evento S2S da Singular já suportam a obtenção de parâmetros SKAdNetwork adicionais, tais como:
- Último valor de conversão
- Último registro de data e hora de uma chamada para a estrutura SKAdNetwork subjacente
- Primeiro registo de data e hora de uma chamada para a estrutura SKAdNetwork subjacente
Fluxo de integração
O diagrama acima ilustra o fluxo de SKAdNetwork para um cliente S2S:
- Primeiro, o código na aplicação comunica com um servidor Singular (através de um endpoint dedicado para SKAdNetwork) para obter o valor de conversão mais recente de forma síncrona com base em eventos/sessões/receitas que ocorrem na aplicação e atualizar a estrutura SKAdNetwork com este valor.
- Em segundo lugar, a aplicação enriquece os eventos e sessões existentes com dados SKAdNetwork, que serão posteriormente utilizados para validações.
- Quando a aplicação terminar de atualizar a estrutura SKAdNetwork com novos valores de conversão e o temporizador SKAdNetwork expirar, o postback SKAdNetwork será enviado para a rede.
- A rede encaminhará o postback para a Singular (através da configuração segura ou da configuração normal).
- A Singular processará o postback da seguinte forma
- Validando a sua assinatura
- Decodificando o valor da conversão com base no modelo de conversão configurado
- Enriquecer o postback com parâmetros de marketing mais voltados para o exterior, com base na integração com parceiros e no ID de campanha interno da SKAdNetwork
- Envio dos postbacks descodificados para o BI e para os parceiros
Uma nota importante é que a informação da SKAdNetwork, incluindo instalações e eventos descodificados, estará acessível através de um conjunto diferente de relatórios/APIs/tabelas ETL e postbacks para evitar misturá-la com os conjuntos de dados existentes. Isso é especialmente importante durante as próximas semanas para permitir que você meça e teste SKAdNetwork lado a lado com o seu esforço de campanha existente.
Amostras de código iOS nativo de SKAdNetwork
Interface de SKAdNetwork
Essa interface inclui os seguintes componentes de SKAdNetwork:
Registo em SKAdNetwork:
- Este método é responsável por registar a sua aplicação em SKAdNetwork. O método subjacente da API da Apple gera uma notificação se o dispositivo tiver dados de atribuição para essa aplicação e inicia um temporizador de 24 horas.
- O dispositivo envia a notificação de instalação para o ponto de extremidade de postback da rede de anúncios dentro de 0-24 horas após a expiração do temporizador.
- Observe que, se você atualizar o valor de conversão com um valor maior do que o anterior, ele redefinirá o primeiro timer para um novo intervalo de 24 horas(leia mais aqui).
Gestão e actualizações do valor de conversão:
- O valor de conversão é calculado com base na atividade pós-instalação de um dispositivo que é capturada pelos métodos abaixo e num modelo de conversão selecionado que pode ser configurado dinamicamente pelo utilizador.
- Os métodos nesta secção são responsáveis por extrair o próximo valor de conversão do ponto de extremidade da Singular de acordo com o modelo de conversão selecionado e a atividade pós-instalação comunicada (ver documentação acima).
- Os métodos abaixo atualizam o valor de conversão com base nas seguintes atividades pós-instalação:
- Sessão: essencial para a medição da retenção de um utilizador com SKAdNetwork
- Conversion Event: essencial para a medição de eventos de conversão pós-instalação com SKAdNetwork
- Evento de receita: Crítico para a medição de receita com SKAdNetwork
// SKANSnippet.h
#import <Foundation/Foundation.h>
@interface
SKANSnippet : NSObject
// Register for SKAdNetwork attribution.
// You should call this method as soon as possible once app is launched
+ (void)registerAppForAdNetworkAttribution;
// To track retention and cohorts you need to call this method after
// each session. It reports the session details and updates the conversion
// value due to this session if needed. The conversion value will be
// updated only if the new value is greater than the previous value.
// The callback passed to the method is optional, you can use it to
// run code once the conversion value is updated.
+ (void)updateConversionValueAsync:(void(^)(int, NSError*))handler;
// To track conversion events with SKAdNetwork you need to call this
// method after each event. It reports the event details and updates the
// conversion value due to this event if needed.
// The callback passed to the method is optional, you can use it to
// run code once the conversion value is updated.
+ (void)updateConversionValueAsync:(NSString*)eventName
withCompletionHandler:(void(^)(int, NSError*))handler;
// To track revenue with SKAdNetwork you need to call this
// method before each revenue event .
// It will update the total revenue, so when you call 'updateConversionValueAsync',
// the new conversion value will be determined according to the total amount of revenue.
// Note:
// 1. Call this method before calling 'updateConversionValueAsync' to
// make sure that revenue is updated.
// 2. In case of retrying an event, avoid calling this method
// so the same revenue will not count twice.
+ (void)updateRevenue:(double)amount andCurrency:(NSString*)currency;
// Gets the current conversion value (nil if none)
+ (NSNumber *)getConversionValue;
@end
Implementação da interface SKAdNetwork
// SKANSnippet.m
#import "SKANSnippet.h"
#import <StoreKit/SKAdNetwork.h>
#define SESSION_EVENT_NAME @”__SESSION__”
#define SINGULAR_API_URL @"https://sdk-api-v1.singular.net/api/v1/conversion_value"
// Keys for UserDefaults storage
#define CONVERSION_VALUE_KEY @"skan_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_KEY @"skan_total_revenue_by_currency"
#define SECONDS_PER_DAY 86400
@implementation
SKANSnippet
+ (void)registerAppForAdNetworkAttribution {
if ([SKANSnippet getFirstSkanCallTimestamp] != 0) {
return;
}
if (@available(iOS 11.3, *) ) {
[SKAdNetwork registerAppForAdNetworkAttribution];
[SKANSnippet setFirstSkanCallTimestamp];
[SKANSnippet setLastSkanCallTimestamp];
}
}
+ (void)updateConversionValueAsync:(void(^)(int, NSError*))handler {
[SKANSnippet updateConversionValueAsync:SESSION_EVENT_NAME withCompletionHandler:handler];
}
+ (void)updateConversionValueAsync:(NSString*)eventName withCompletionHandler:(void(^)(int, NSError*))handler {
if (@available(iOS 14, *)) {
if ([SKANSnippet isSkanUpdateWindowOver]) {
return;
}
[SKANSnippet getConversionValueFromServer:eventName withCompletionHandler:handler];
}
}
+ (void)updateRevenue:(double)amount andCurrency:(NSString*)currency {
if (@available(iOS 14, *)) {
// Update total revenues
if (amount != 0 && currency) {
NSMutableDictionary* revenues = [[SKANSnippet getTotalRevenue] mutableCopy];
NSNumber* currentRevenue = 0;
if ([revenues objectForKey:currency]) {
currentRevenue = [revenues objectForKey:currency];
}
currentRevenue = @([currentRevenue floatValue] + amount);
[revenues setObject:currentRevenue forKey:currency];
[SKANSnippet setTotalRevenue:revenues];
}
}
}
+ (NSNumber *)getConversionValue {
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
if (![userDefaults objectForKey:CONVERSION_VALUE_KEY]) {
return nil;
}
return @([userDefaults integerForKey:CONVERSION_VALUE_KEY]);
}
+ (void)getConversionValueFromServer:(NSString*)eventName withCompletionHandler:(void(^)(int, NSError*))handler {
if (!lockObject) {
lockObject = [NSLock new];
}
// 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];
NSURLComponents *components = [NSURLComponents componentsWithString:SINGULAR_API_URL];
NSString* bundleIdentifier = @"";
components.queryItems = @[
[NSURLQueryItem queryItemWithName:@"a" value:@""],
[NSURLQueryItem queryItemWithName:@"i" value:bundleIdentifier],
[NSURLQueryItem queryItemWithName:@"app_v" value:@""],
[NSURLQueryItem queryItemWithName:@"n" value:eventName],
[NSURLQueryItem queryItemWithName:@"p" value:@"iOS"],
[NSURLQueryItem queryItemWithName:@"idfv" value:@""],
[NSURLQueryItem queryItemWithName:@"idfa" value:@""],
[NSURLQueryItem queryItemWithName:@"conversion_value"
value:[[SKANSnippet getConversionValue] stringValue]],
[NSURLQueryItem queryItemWithName:@"total_revenue_by_currency"
value:[SKANSnippet dictionaryToJsonString:[SKANSnippet getTotalRevenue]]],
[NSURLQueryItem queryItemWithName:@"first_call_to_skadnetwork_timestamp"
value:[NSString stringWithFormat:@"%ld", [SKANSnippet getFirstSkanCallTimestamp]]],
[NSURLQueryItem queryItemWithName:@"last_call_to_skadnetwork_timestamp"
value:[NSString stringWithFormat:@"%ld", [SKANSnippet getLastSkanCallTimestamp]]],
];
[[[NSURLSession sharedSession] dataTaskWithURL:components.URL
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
[lockObject unlock];
if (handler) {
handler(-1, error);
}
return;
}
NSDictionary* parsedResponse = [SKANSnippet jsonDataToDictionary:data];
if (!parsedResponse) {
[lockObject unlock];
if (handler) {
handler(-1, [NSError errorWithDomain:bundleIdentifier
code:0
userInfo:@{NSLocalizedDescriptionKey:@"Failed parsing server response"}]);
}
return;
}
NSNumber *conversionValue = [parsedResponse objectForKey:@"conversion_value"];
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(-1, [NSError errorWithDomain:bundleIdentifier
code:0
userInfo:@{NSLocalizedDescriptionKey:reason}]);
}
}
return;
}
NSNumber* currentValue = [SKANSnippet getConversionValue];
if ([conversionValue intValue] <= [currentValue intValue]) {
[lockObject unlock];
return;
}
[SKANSnippet setConversionValue:[conversionValue intValue]];
[SKANSnippet setLastSkanCallTimestamp];
if (![SKANSnippet getFirstSkanCallTimestamp]) {
[SKANSnippet setFirstSkanCallTimestamp];
}
[lockObject unlock];
if (handler) {
handler([conversionValue intValue], error);
}
}] resume];
});
}
+ (BOOL)isSkanUpdateWindowOver {
NSInteger timeDiff = [SKANSnippet getCurrentUnixTimestamp] -
[SKANSnippet getLastSkanCallTimestamp];
return SECONDS_PER_DAY <= timeDiff;
}
Metadados e métodos utilitários de SKAdNetwork
Esta secção é responsável pela implementação de utilitários que estão a ser utilizados na implementação acima. Note-se que estes utilitários são essenciais para manter e armazenar os metadados necessários para calcular os valores de conversão seguintes.
São implementados os seguintes utilitários:
Guardar valores em UserDefaults (e recuperá-los):
- Registo de data e hora da primeira chamada para a API SKAdNetwork subjacente
- Registo de data e hora da última chamada para a API SKAdNetwork subjacente
- Receita total por moeda
Conversão de valores entre diferentes representações:
- Dicionário para string JSON
- JSON para dicionário
+ (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];
}
+ (NSDictionary*)getTotalRevenue {
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
if (![userDefaults objectForKey:TOTAL_REVENUE_BY_CURRENCY_KEY]) {
return [NSDictionary new];
}
return [userDefaults objectForKey:TOTAL_REVENUE_BY_CURRENCY_KEY];
}
+ (NSInteger)getCurrentUnixTimestamp {
return [[NSDate date]timeIntervalSince1970];
}
+ (void)setConversionValue:(int)value {
if (@available(iOS 14.0, *)) {
if (value <= [[SKANSnippet getConversionValue] intValue]) {
return;
}
[SKAdNetwork updateConversionValue:value];
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setInteger:value forKey:CONVERSION_VALUE_KEY];
[userDefaults synchronize];
}
}
+ (void)setFirstSkanCallTimestamp {
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setInteger:[SKANSnippet getCurrentUnixTimestamp]
forKey:FIRST_SKAN_CALL_TIMESTAMP];
[userDefaults synchronize];
}
+ (void)setLastSkanCallTimestamp {
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setInteger:[SKANSnippet getCurrentUnixTimestamp]
forKey:LAST_SKAN_CALL_TIMESTAMP];
[userDefaults synchronize];
}
+ (void)setTotalRevenue:(NSDictionary *)values {
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:values forKey:TOTAL_REVENUE_BY_CURRENCY_KEY];
[userDefaults synchronize];
}
+ (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];
}
+ (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;
}
@end
Atualização da integração S2S - Implementação (opcional)
Atualize sua integração S2S com os seguintes metadados da SKAdNetwork (esses metadados devem ser encaminhados em todas as sessões e eventos reportados à Singular para validação da implementação da SKAdNetwork):
- skan_conversion_value - O valor de conversão mais recente
- skan_first_call_timestamp - Carimbo de data/hora Unix da primeira chamada à API SKAdNetwork subjacente
- skan_last_call_timestamp - Carimbo de data/hora Unix da última chamada à API SKAdNetwork subjacente
O seguinte trecho de código mostra como extrair esses valores:
NSDictionary *values = @{
@"skan_conversion_value":
[[SKANSnippet getConversionValue] stringValue]],
@"skan_first_call_timestamp":
[NSString stringWithFormat:@"%ld", [SKANSnippet getFirstSkanCallTimestamp]],
@"skan_last_call_timestamp":
[NSString stringWithFormat:@"%ld", [SKANSnippet getLastSkanCallTimestamp]]
};
Assim que enviar estes parâmetros para o seu servidor, pode reencaminhá-los através dos nossos endpoints API de servidor para servidor. Para saber mais, pesquise estes parâmetros na nossa referência da API server-to-server.
Exemplo de fluxo do ciclo de vida da aplicação
// On app launch
[SKANSnippet registerAppForAdNetworkAttribution];
// After each session is handled
[SKANSnippet updateConversionValueAsync:handler];
// After handling non-revenue events
[SKANSnippet updateConversionValueAsync:@"event_name"
withCompletionHandler:handler];
// After handling revenue events
[SKANSnippet updateRevenue:15.53 andCurrency:@"KRW"];
[SKANSnippet updateConversionValueAsync:@"revenue_event_name" withCompletionHandler:handler];
Registo de alterações de código
- 1 de outubro de 2020
- Correção [IMPORTANTE]: o mecanismo de bloqueio em `getConversionValueFromServer`não foi iniciado corretamente
- Melhorado: retorno do motivo de falha da resposta do nosso servidor
- 2020/09/23
- Correção [IMPORTANTE]: `setConversionValue` chama agora `updateConversionValue`
- Melhorado: tratamento de erros ao recuperar o valor de conversão mais recente do servidor
- 2020/09/15
- Alterado [IMPORTANTE]: Se o valor de conversão não for definido, o valor predefinido devolvido por `getConversionValue` é 0 em vez de nulo
- Melhorado: Relatório e tratamento de receitas
- Melhorado: Recuperação do próximo valor de conversão de forma assíncrona
Testar a sua implementação
Gerenciamento de conversões
Para simular o fluxo da aplicação e testar a implementação, substitua o endpoint de produção do gerenciamento de conversões(SINGULAR_API_URL) pelo seguinte endpoint de teste -
- https://skadnetwork-testing.singular.net/api/v1/conversion_value
- Não é necessária uma chave de API
Um fluxo de teste sugerido:
- Gerar 3 eventos diferentes na sua aplicação.
- Se tudo estiver implementado corretamente -
- Após cada evento, updateConversionValueAsync deve ser chamado (certifique-se de que o valor de conversão padrão seja inicializado como 0).
- O ponto de extremidade de teste recebe o valor de conversão atual do aplicativo e retorna o próximo valor.
- Se tudo estiver implementado corretamente -
- Registre o valor de conversão retornado para ver se tudo está funcionando conforme o esperado.
- O ponto de extremidade de teste é implementado para retornar valores entre 0-2.
- Por conseguinte, após a terceira chamada, devolverá uma cadeia vazia e o último valor será utilizado como o valor final de conversão SKAdNetwork.