旧版 — SKAdNetwork 3。 本指南介绍旧版 SKAdNetwork 3.0 实现方式,仅为现有集成保留。对于所有新的和当前的实现,请使用 SKAdNetwork 4 实现指南 。
SKAdNetwork 3.0 S2S 实现指南(旧版)
使用服务器到服务器集成实现 Apple 的 SKAdNetwork 3.0 框架,以进行隐私合规的 iOS 归因,从而在不泄露用户标识符的情况下衡量应用安装广告系列的表现。
概述
什么是 SKAdNetwork?
SKAdNetwork 是 Apple 的隐私保护型归因框架,能够在 iOS 上衡量应用安装广告 系列的转化率,而无需用户级别的标识符。
该框架通过 App Store 服务器处理归因,在将归因信息发送给广告网络之前, 将其与用户标识符和时间数据断开关联。
Apple 文档: SKAdNetwork Framework Reference
SKAdNetwork 的工作原理
归因过程完全通过 Apple 的基础设施进行,在实现广告系列表现衡量的同时确保 用户隐私。
归因流程:
- 广告点击: 用户点击包含 SKAdNetwork 签名(带有网络、发布商和广告系列 ID)的广告
- 打开 App Store: 设备存储归因数据并 打开 App Store 进行安装
- 应用启动: 用户首次安装并启动应用
- 注册: 应用向 SKAdNetwork 框架注册 以发出安装成功的信号
- Conversion Value: 应用可选地更新 conversion value(0-63),代表安装后的活动
- 计时器窗口: 设备在首次启动或最后一次 conversion value 更新后等待 24 小时以上
- Postback: App Store 向广告网络发送归因 postback, 其中包含广告系列 ID、conversion value 和加密 签名
隐私优先设计:
- Postback 不包含任何设备或用户标识符
- 至少 24 小时的延迟可防止时间关联
- 应用永远不知道用户点击了哪个广告
- 网络永远不知道具体是哪个用户进行了安装
功能与限制
SKAdNetwork 提供:
- 最后点击归因: 无需用户同意 或 ATT 选择加入即可运作
- 广告系列细分: 来源、广告系列和发布商 粒度
- Conversion Value: 64 个离散值(0-63),用于 安装后衡量
- 欺诈防护: 加密签名验证 归因的真实性
当前限制:
- 无用户级别数据: 无法追踪单个用户 的旅程
- 无浏览归因: 仅支持基于点击的归因
- Conversion Value 有限: 每次安装仅有单个 6 位值(0-63)
- 粒度有限: 最多 100 个广告系列 ID,无 广告组或素材细分
- 无长期 cohort: 转化衡量的时间范围有限
- 欺诈风险: conversion value 本身未签名, postback 可能被重复
Singular 资源
Singular 为 SKAdNetwork 的实现和优化提供全面的资源。
- SKAN: Practical Standard for SKAdNetwork Implementation
- SKAdNetwork 101: What It Means for You
- SKAdNetwork Code: GitHub Repository for Networks, Publishers, Advertisers
- How to Test SKAdNetwork: Step-by-Step Instructions
- Singular Announces First-to-Market IDFA Replacement Support
- Advanced Measurement Using SKAdNetwork: Unlocking ROAS
- Secure SKAdNetwork 2.0: Seamless Trust Establishment Setup
Singular SKAdNetwork 解决方案
Singular 的 SKAdNetwork 解决方案提供端到端的归因管理, 从客户端实现到 postback 处理和广告系列优化。
解决方案组件
平台功能
在整个归因和分析工作流程中提供全面的 SKAdNetwork 支持。
| 组件 | 功能 |
|---|---|
| 客户端代码 | 用于 SKAdNetwork 框架注册和 conversion value 管理的 原生 iOS 代码示例 |
| Postback 处理 | 对来自所有广告网络的 postback 进行验证和聚合, 并提供统一报告 |
| 欺诈防护 | 加密签名验证、事务 ID 去重 以及安全参数校验 |
| 转化管理 | 动态仪表盘配置,用于将安装后 活动编码为 conversion value |
| 报告 | 广告系列 ID 转换及使用营销参数进行丰富, 以便进行精细化分析 |
| 合作伙伴 Postback | 将解码后的 conversion value 作为事件和收入发送, 用于合作伙伴优化 |
S2S 集成架构
实现组件
服务器到服务器 SKAdNetwork 集成由两个主要实现 领域组成。
1. 客户端实现(必需):
- 应用启动时进行 SKAdNetwork 框架注册
- 基于安装后活动的智能 conversion value 管理
- 对于使用 SKAdNetwork 归因进行广告系列优化至关重要
2. S2S 集成更新(可选):
- 使用 SKAdNetwork 元数据丰富 Singular S2S 事件和会话
- 支持实现验证和问题排查
- 提供 conversion value 时间戳以便调试
客户端实现
使用 Singular 的原生 iOS 代码示例实现 SKAdNetwork 框架注册和 conversion value 管理, 以实现最佳的广告系列衡量。
实现职责
核心功能
客户端代码为 SKAdNetwork 衡量处理两个关键功能。
- SKAdNetwork 注册: 在应用启动后立即向框架注册, 以启用归因
-
Conversion Value 管理:
- 与 Singular endpoint 同步通信以接收 下一个 conversion value
- 向 Singular 报告会话、事件和收入
- 接收表示已配置的安装后活动的 编码 conversion value
- 收集 SKAdNetwork 元数据(首次调用时间戳、最后一次 调用时间戳、当前值)
SKAdNetwork 接口
方法定义
接口包含用于框架注册、会话追踪、 事件追踪和收入衡量的方法。
// SKANSnippet.h
#import <Foundation/Foundation.h>
@interface SKANSnippet : NSObject
// Register for SKAdNetwork attribution.
// Call this method as soon as possible once app is launched
+ (void)registerAppForAdNetworkAttribution;
// Track retention and cohorts by calling after each session.
// Reports session details and updates conversion value if needed.
// Conversion value updates only if new value greater than previous.
// Optional callback runs once conversion value updated.
+ (void)updateConversionValueAsync:(void(^)(int, NSError*))handler;
// Track conversion events by calling after each event.
// Reports event details and updates conversion value if needed.
// Optional callback runs once conversion value updated.
+ (void)updateConversionValueAsync:(NSString*)eventName
withCompletionHandler:(void(^)(int, NSError*))handler;
// Track revenue by calling before each revenue event.
// Updates total revenue for next conversion value calculation.
// Note:
// 1. Call before 'updateConversionValueAsync' to ensure revenue included
// 2. Avoid calling on event retries to prevent double-counting
+ (void)updateRevenue:(double)amount andCurrency:(NSString*)currency;
// Gets current conversion value (nil if none set)
+ (NSNumber *)getConversionValue;
@end
注册方法
registerAppForAdNetworkAttribution
向 SKAdNetwork
框架注册应用,启动 24 小时归因计时器。
计时器行为:
- 设备在计时器到期后的 0-24 小时内发送安装通知
- 使用更高的值更新 conversion value 会重置计时器, 开始新的 24 小时间隔
- Apple 参考: updateConversionValue Documentation
Conversion Value 方法
这些方法根据安装后活动和已配置的转化模型 计算并更新 conversion value。
支持的活动:
- 会话: 对留存衡量至关重要
- 转化事件: 对安装后事件 衡量至关重要
- 收入事件: 对收入衡量至关重要
接口实现
完整实现代码
完整实现处理注册、转化管理以及 Singular API 通信。
// 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
static NSLock *lockObject;
@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 = [[NSBundle mainBundle] bundleIdentifier];
components.queryItems = @[
[NSURLQueryItem queryItemWithName:@"a" value:@"YOUR_SDK_KEY"],
[NSURLQueryItem queryItemWithName:@"i" value:bundleIdentifier],
[NSURLQueryItem queryItemWithName:@"app_v" value:@"YOUR_APP_VERSION"],
[NSURLQueryItem queryItemWithName:@"n" value:eventName],
[NSURLQueryItem queryItemWithName:@"p" value:@"iOS"],
[NSURLQueryItem queryItemWithName:@"idfv" value:@"YOUR_IDFV"],
[NSURLQueryItem queryItemWithName:@"idfa" value:@"YOUR_IDFA"],
[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;
}
@end
实用方法
元数据管理
实用方法管理 SKAdNetwork 元数据在 UserDefaults 中的存储, 对 conversion value 计算至关重要。
关键实现: 这些实用方法维护 准确计算 conversion value 所需的元数据。除非必要,否则请勿 修改。
+ (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
S2S 集成更新
可选择使用 SKAdNetwork 元数据增强服务器到服务器集成, 以进行实现验证和问题排查。
必需的更新
Conversion Value 响应
一旦转化模型激活,S2S endpoint 会返回
conversion_value
整数字段,其中包含用于
客户端更新的下一个值。
响应处理:
从 S2S endpoint 响应中解析
conversion_value
,并通过客户端代码将其应用于 SKAdNetwork 框架。
可选增强功能
元数据参数
使用 SKAdNetwork 元数据丰富 S2S 会话和事件请求,以验证 集成并排查实现问题。
| 参数 | 描述 |
|---|---|
skan_conversion_value
|
请求时的最新 conversion value |
skan_first_call_timestamp
|
首次调用 SKAdNetwork API 的 Unix 时间戳 |
skan_last_call_timestamp
|
最近一次调用 SKAdNetwork API 的 Unix 时间戳 |
元数据提取代码
使用实用方法提取 SKAdNetwork 元数据值以进行 S2S 传输。
NSDictionary *skanMetadata = @{
@"skan_conversion_value":
[[SKANSnippet getConversionValue] stringValue],
@"skan_first_call_timestamp":
[NSString stringWithFormat:@"%ld", [SKANSnippet getFirstSkanCallTimestamp]],
@"skan_last_call_timestamp":
[NSString stringWithFormat:@"%ld", [SKANSnippet getLastSkanCallTimestamp]]
};
// Forward skanMetadata to your server for S2S API enrichment
将参数转发到服务器端,并附加到 Singular S2S API 请求中。 完整参数文档: S2S API Reference
集成流程
面向 S2S 客户的完整 SKAdNetwork 流程,从客户端转化 管理到 postback 处理。
流程阶段
端到端流程
- Conversion Value 请求: 应用代码 与 Singular endpoint 同步通信,根据会话、事件和收入 获取最新的 conversion value
- 框架更新: 应用使用接收到的 conversion value 更新 SKAdNetwork 框架
- 元数据丰富: 应用使用 SKAdNetwork 元数据丰富 S2S 事件和会话以进行验证
- 计时器到期: 从最后一次更新起 24 小时以上后, SKAdNetwork 向广告网络发送 postback
- Postback 转发: 网络将 postback 转发到 Singular(安全设置或常规)
-
Postback 处理:
Singular 通过以下方式处理
postback:
- 验证加密签名
- 使用已配置的模型解码 conversion value
- 根据合作伙伴集成使用营销参数进行丰富
- 通过 postback 将解码后的数据发送到 BI 和合作伙伴
数据分离: SKAdNetwork 数据(安装和 解码后的事件)可通过单独的报告、API、ETL 表 和 postback 访问,以防止在测试和验证期间 与现有数据集混淆。
应用生命周期实现
在适当的应用生命周期节点集成 SKAdNetwork 方法,以实现 完整的归因覆盖。
实现示例
方法放置位置
// On app launch (in applicationDidFinishLaunching)
[SKANSnippet registerAppForAdNetworkAttribution];
// After each session handled
[SKANSnippet updateConversionValueAsync:^(int value, NSError *error) {
if (error) {
NSLog(@"Conversion value update failed: %@", error);
} else {
NSLog(@"Conversion value updated to: %d", value);
}
}];
// After handling non-revenue events
[SKANSnippet updateConversionValueAsync:@"event_name"
withCompletionHandler:^(int value, NSError *error) {
if (error) {
NSLog(@"Event conversion value update failed: %@", error);
} else {
NSLog(@"Event conversion value updated to: %d", value);
}
}];
// After handling revenue events
[SKANSnippet updateRevenue:15.53 andCurrency:@"USD"];
[SKANSnippet updateConversionValueAsync:@"revenue_event_name"
withCompletionHandler:^(int value, NSError *error) {
if (error) {
NSLog(@"Revenue conversion value update failed: %@", error);
} else {
NSLog(@"Revenue conversion value updated to: %d", value);
}
}];
测试实现
在生产部署之前,使用 Singular 的测试 endpoint 验证 SKAdNetwork 实现。
测试环境
测试 Endpoint 配置
使用测试 endpoint 替换生产环境的转化管理 endpoint, 以模拟应用流程。
测试 Endpoint URL:
https://skadnetwork-testing.singular.net/api/v1/conversion_value
- 测试 endpoint 无需 API 密钥
- 依次返回 conversion value 0-2
- 第三次调用后返回空字符串
测试步骤
验证步骤
-
更新 Endpoint:
将
SINGULAR_API_URL常量替换为测试 endpoint URL - 初始化 Conversion Value: 确保默认 conversion value 初始化为 0
- 生成事件: 在应用内触发 3 个不同的事件
-
验证更新:
确认每个事件后都调用了
updateConversionValueAsync - 记录值: 记录返回的 conversion value,以验证 正确的进程(0 → 1 → 2)
- 确认完成: 第三次调用后,验证是否返回空 响应并保留最终值
预期行为:
- 第一个事件:接收 conversion value 0
- 第二个事件:接收 conversion value 1
- 第三个事件:接收 conversion value 2
- 第四个及以后的事件:空响应,值 2 保持不变
代码变更日志
追踪代码示例的更新和随时间应用的关键修复。
版本历史
| 日期 | 变更 |
|---|---|
| 2020 年 10 月 1 日 |
|
| 2020 年 9 月 23 日 |
|
| 2020 年 9 月 15 日 |
|