adattributionkit
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAdAttributionKit
AdAttributionKit
Privacy-preserving ad attribution for iOS 17.4+ / Swift 6.3. AdAttributionKit
lets ad networks measure conversions (installs and re-engagements) without
exposing user-level data. It supports the App Store and alternative
marketplaces, and interoperates with SKAdNetwork.
Three roles exist in the attribution flow: the ad network (signs
impressions, receives postbacks), the publisher app (displays ads), and the
advertised app (the app being promoted).
适用于iOS 17.4+ / Swift 6.3的隐私保护型广告归因工具。AdAttributionKit可让广告网络在不暴露用户层级数据的前提下衡量转化(安装和再互动),支持App Store及其他应用市场,可与SKAdNetwork互通。
归因流程包含三个角色:广告网络(对曝光进行签名,接收回传)、发布方应用(展示广告)、被推广应用(即被宣传的应用)。
Contents
目录
Overview and Privacy Model
概述与隐私模型
AdAttributionKit preserves user privacy through several mechanisms:
- Crowd anonymity tiers -- the device limits postback data granularity based on the crowd size associated with the ad, ranging from Tier 0 (minimal data) to Tier 3 (most data including publisher ID and country code).
- Time-delayed postbacks -- postbacks are sent 24-48 hours after conversion window close (first window) or 24-144 hours (second/third windows).
- No user-level identifiers -- postbacks contain aggregate source identifiers and conversion values, not device or user IDs.
- Hierarchical source identifiers -- 2, 3, or 4-digit source IDs where the number of digits returned depends on the crowd anonymity tier.
The system evaluates impressions from both AdAttributionKit and SKAdNetwork
together when determining attribution winners. Only one impression wins per
conversion. Click-through ads always take precedence over view-through ads,
with recency as the tiebreaker within each group.
AdAttributionKit通过多种机制保护用户隐私:
- 人群匿名层级:设备会根据广告对应的人群规模限制回传数据的粒度,从0级(最少数据)到3级(最多数据,包含发布商ID和国家代码)不等。
- 时间延迟回传:回传会在转化窗口关闭后24-48小时(第一个窗口)或24-144小时(第二/第三个窗口)发送。
- 无用户层级标识符:回传仅包含聚合来源标识符和转化值,不包含设备或用户ID。
- 层级化来源标识符:2、3或4位的来源ID,返回的位数取决于人群匿名层级。
系统在判定归因胜出者时会同时评估来自AdAttributionKit和SKAdNetwork的曝光,每次转化仅会有一个曝光胜出。点击类广告的优先级始终高于曝光类广告,同类别内按时间最近原则判定。
Publisher App Setup
发布方应用配置
A publisher app displays ads from registered ad networks. Add each ad network's
ID to the app's Info.plist so its impressions qualify for install validation.
发布方应用展示已注册广告网络的广告。需将每个广告网络的ID添加到应用的Info.plist中,其曝光才能通过安装验证。
Add ad network identifiers
添加广告网络标识符
xml
<key>AdNetworkIdentifiers</key>
<array>
<string>example123.adattributionkit</string>
<string>another456.adattributionkit</string>
</array>Ad network IDs must be lowercase. SKAdNetwork IDs (ending in )
are also accepted -- the frameworks share IDs.
.skadnetworkxml
<key>AdNetworkIdentifiers</key>
<array>
<string>example123.adattributionkit</string>
<string>another456.adattributionkit</string>
</array>广告网络ID必须为小写。也支持SKAdNetwork ID(以结尾),两个框架的ID是互通的。
.skadnetworkDisplay a UIEventAttributionView
展示UIEventAttributionView
For click-through custom-rendered ads, overlay a on
the ad content. The system requires a tap on this view before
succeeds.
UIEventAttributionViewhandleTap()swift
import UIKit
let attributionView = UIEventAttributionView()
attributionView.frame = adContentView.bounds
attributionView.isUserInteractionEnabled = true
adContentView.addSubview(attributionView)对于点击类自定义渲染广告,需在广告内容上叠加,系统要求用户点击该视图后才能调用成功。
UIEventAttributionViewhandleTap()swift
import UIKit
let attributionView = UIEventAttributionView()
attributionView.frame = adContentView.bounds
attributionView.isUserInteractionEnabled = true
adContentView.addSubview(attributionView)Advertiser App Setup
广告主应用配置
The advertised app is the app someone installs or re-engages with after seeing
an ad. It must call a conversion value update at least once to begin the
postback conversion window.
被推广应用是用户看到广告后安装或再次互动的应用,必须至少调用一次转化值更新才能启动回传转化窗口。
Opt in to receive winning postback copies
开启接收胜出回传副本
Add the key to Info.plist so the device sends a copy
of the winning postback to your server:
AttributionCopyEndpointxml
<key>AdAttributionKit</key>
<dict>
<key>AttributionCopyEndpoint</key>
<string>https://example.com</string>
</dict>The system generates a well-known path from the domain:
https://example.com/.well-known/appattribution/report-attribution/Configure your server to accept HTTPS POST requests at that path. The domain
must have a valid SSL certificate.
在Info.plist中添加键,设备会将胜出回传的副本发送到你的服务器:
AttributionCopyEndpointxml
<key>AdAttributionKit</key>
<dict>
<key>AttributionCopyEndpoint</key>
<string>https://example.com</string>
</dict>系统会基于域名生成固定路径:
https://example.com/.well-known/appattribution/report-attribution/请配置你的服务器接受该路径下的HTTPS POST请求,域名必须持有有效的SSL证书。
Opt in for re-engagement postback copies
开启接收再互动回传副本
Add a second key to also receive copies of winning re-engagement postbacks:
xml
<key>AdAttributionKit</key>
<dict>
<key>AttributionCopyEndpoint</key>
<string>https://example.com</string>
<key>OptInForReengagementPostbackCopies</key>
<true/>
</dict>添加第二个键即可同时接收胜出的再互动回传副本:
xml
<key>AdAttributionKit</key>
<dict>
<key>AttributionCopyEndpoint</key>
<string>https://example.com</string>
<key>OptInForReengagementPostbackCopies</key>
<true/>
</dict>Update conversion value on first launch
首次启动时更新转化值
Call a conversion value update as early as possible after first launch to begin
the conversion window:
swift
import AdAttributionKit
func applicationDidFinishLaunching() async {
do {
try await Postback.updateConversionValue(0, lockPostback: false)
} catch {
print("Failed to set initial conversion value: \(error)")
}
}首次启动后请尽早调用转化值更新,以启动转化窗口:
swift
import AdAttributionKit
func applicationDidFinishLaunching() async {
do {
try await Postback.updateConversionValue(0, lockPostback: false)
} catch {
print("设置初始转化值失败:\(error)")
}
}Impressions
广告曝光
Ad networks create signed impressions using JWS (JSON Web Signature). The
publisher app uses to register and handle those impressions.
AppImpression广告网络使用JWS(JSON Web Signature)生成已签名的曝光,发布方应用使用注册和处理这些曝光。
AppImpressionCreate an impression from a JWS
从JWS创建曝光实例
swift
import AdAttributionKit
let impression = try await AppImpression(compactJWS: signedJWSString)The JWS contains the ad network ID, advertised item ID, publisher item ID,
source identifier, timestamp, and optional re-engagement eligibility flag. See
references/adattributionkit-patterns.md
for JWS generation details.
swift
import AdAttributionKit
let impression = try await AppImpression(compactJWS: signedJWSString)JWS包含广告网络ID、被推广项目ID、发布方项目ID、来源标识符、时间戳,以及可选的再互动 eligibility 标记。JWS生成细节请参考references/adattributionkit-patterns.md。
Check device support
检查设备支持情况
swift
guard AppImpression.isSupported else {
// Fall back to alternative ad display
return
}swift
guard AppImpression.isSupported else {
// 降级使用其他广告展示方案
return
}View-through impressions
曝光类广告记录
Record a view impression when the ad content has been displayed and dismissed:
swift
func handleAdViewed(impression: AppImpression) async {
do {
try await impression.handleView()
} catch {
print("Failed to record view-through impression: \(error)")
}
}For long-lived ad views, use and to track view
duration:
beginView()endView()swift
try await impression.beginView()
// ... ad remains visible ...
try await impression.endView()当广告内容完成展示并关闭时记录曝光:
swift
func handleAdViewed(impression: AppImpression) async {
do {
try await impression.handleView()
} catch {
print("记录曝光类广告失败:\(error)")
}
}对于长时间展示的广告,可使用和追踪展示时长:
beginView()endView()swift
try await impression.beginView()
// ... 广告保持展示状态 ...
try await impression.endView()Click-through impressions
点击类广告记录
Respond to ad taps by calling . If the advertised app is not
installed, the system opens its App Store or marketplace page. If installed,
the system launches it directly.
handleTap()swift
func handleAdTapped(impression: AppImpression) async {
do {
try await impression.handleTap()
} catch {
print("Failed to record click-through impression: \(error)")
}
}A must overlay the ad for to succeed.
UIEventAttributionViewhandleTap()用户点击广告时调用响应。如果被推广应用未安装,系统会打开其App Store或应用市场页面;如果已安装,系统会直接启动应用。
handleTap()swift
func handleAdTapped(impression: AppImpression) async {
do {
try await impression.handleTap()
} catch {
print("记录点击类广告失败:\(error)")
}
}必须在广告上叠加,才能调用成功。
UIEventAttributionViewhandleTap()StoreKit-rendered ads
StoreKit渲染的广告
Pass the impression to StoreKit overlay or product view controller APIs. StoreKit
automatically records view-through impressions after 2 seconds of display and
click-through impressions on tap.
swift
import StoreKit
let config = SKOverlay.AppConfiguration(appIdentifier: "1234567890",
position: .bottom)
config.appImpression = impression将曝光实例传递给StoreKit浮层或产品视图控制器API,StoreKit会在广告展示2秒后自动记录曝光,点击时自动记录点击事件。
swift
import StoreKit
let config = SKOverlay.AppConfiguration(appIdentifier: "1234567890",
position: .bottom)
config.appImpression = impressionPostbacks
回传数据
Postbacks are attribution reports the device sends to ad networks (and
optionally to the advertised app developer) after a conversion event.
回传是设备在转化事件发生后发送给广告网络(可选发送给被推广应用开发者)的归因报告。
Conversion windows
转化窗口
Three windows produce up to three postbacks for winning attributions:
| Window | Duration | Postback delay |
|---|---|---|
| 1st | Days 0-2 | 24-48 hours |
| 2nd | Days 3-7 | 24-144 hours |
| 3rd | Days 8-35 | 24-144 hours |
Tier 0 postbacks only produce the first postback. Nonwinning attributions
produce only one postback.
胜出的归因最多会在三个窗口生成三次回传:
| 窗口 | 时长 | 回传延迟 |
|---|---|---|
| 第一窗口 | 第0-2天 | 24-48小时 |
| 第二窗口 | 第3-7天 | 24-144小时 |
| 第三窗口 | 第8-35天 | 24-144小时 |
0级回传仅会生成第一次回传,未胜出的归因仅会生成一次回传。
Time windows for events
事件时间限制
| Event | Time limit |
|---|---|
| View-through to install | 24 hours (configurable up to 7 days) |
| Click-through to install | 30 days (configurable down to 1 day) |
| Install to first update | 60 days |
| Re-engagement to first update | 2 days |
| 事件 | 时间限制 |
|---|---|
| 曝光到安装转化 | 24小时(最高可配置为7天) |
| 点击到安装转化 | 30天(最低可配置为1天) |
| 安装到首次更新转化值 | 60天 |
| 再互动到首次更新转化值 | 2天 |
Lock conversion values early
提前锁定转化值
Lock the postback to finalize a conversion value before the window ends and
receive the postback sooner:
swift
try await Postback.updateConversionValue(
42,
coarseConversionValue: .high,
lockPostback: true
)After locking, the system ignores further updates in that conversion window.
可在窗口结束前锁定回传以最终确定转化值,更早收到回传:
swift
try await Postback.updateConversionValue(
42,
coarseConversionValue: .high,
lockPostback: true
)锁定后,系统会忽略该转化窗口内的后续更新。
Postback data by tier
不同层级的回传数据
| Field | Tier 0 | Tier 1 | Tier 2 | Tier 3 |
|---|---|---|---|---|
| 2 | 2 | 2-4 | 2-4 |
| -- | -- | 1st only | 1st only |
| -- | 1st only | 2nd/3rd | 2nd/3rd |
| -- | -- | -- | Yes |
| -- | -- | -- | Conditional |
| 字段 | 0级 | 1级 | 2级 | 3级 |
|---|---|---|---|---|
| 2 | 2 | 2-4 | 2-4 |
精细 | -- | -- | 仅第一窗口 | 仅第一窗口 |
| -- | 仅第一窗口 | 第二/第三窗口 | 第二/第三窗口 |
| -- | -- | -- | 是 |
| -- | -- | -- | 条件触发 |
Conversion Values
转化值
Fine-grained values
精细值
An integer from 0-63 (6 bits). Available only in the first postback and only at
Tier 2 or higher:
swift
try await Postback.updateConversionValue(
35,
coarseConversionValue: .medium,
lockPostback: false
)0-63的整数(6位),仅在第一回传且层级为2级及以上时可用:
swift
try await Postback.updateConversionValue(
35,
coarseConversionValue: .medium,
lockPostback: false
)Coarse values
粗糙值
Three levels for lower tiers and second/third postbacks:
swift
// CoarseConversionValue cases: .low, .medium, .high
try await Postback.updateConversionValue(
10,
coarseConversionValue: .high,
lockPostback: false
)针对低层级和第二/第三回传的三个等级:
swift
// CoarseConversionValue 可选值:.low, .medium, .high
try await Postback.updateConversionValue(
10,
coarseConversionValue: .high,
lockPostback: false
)Update by conversion type (iOS 18+)
按转化类型更新(iOS 18+)
Separate conversion values for install vs. re-engagement postbacks:
swift
let installUpdate = PostbackUpdate(
fineConversionValue: 20,
lockPostback: false,
conversionTypes: [.install]
)
try await Postback.updateConversionValue(installUpdate)
let reengagementUpdate = PostbackUpdate(
fineConversionValue: 12,
lockPostback: false,
conversionTypes: [.reengagement]
)
try await Postback.updateConversionValue(reengagementUpdate)可为安装和再互动回传设置单独的转化值:
swift
let installUpdate = PostbackUpdate(
fineConversionValue: 20,
lockPostback: false,
conversionTypes: [.install]
)
try await Postback.updateConversionValue(installUpdate)
let reengagementUpdate = PostbackUpdate(
fineConversionValue: 12,
lockPostback: false,
conversionTypes: [.reengagement]
)
try await Postback.updateConversionValue(reengagementUpdate)Conversion tags (iOS 18.4+)
转化标签(iOS 18.4+)
Use conversion tags to selectively update specific postbacks when overlapping
conversion windows exist:
swift
let update = PostbackUpdate(
fineConversionValue: 15,
lockPostback: false,
conversionTag: savedConversionTag,
conversionTypes: [.reengagement]
)
try await Postback.updateConversionValue(update)The system delivers the conversion tag through the re-engagement URL's
query parameter.
AdAttributionKitReengagementOpen当存在重叠转化窗口时,可使用转化标签选择性更新特定回传:
swift
let update = PostbackUpdate(
fineConversionValue: 15,
lockPostback: false,
conversionTag: savedConversionTag,
conversionTypes: [.reengagement]
)
try await Postback.updateConversionValue(update)系统会通过再互动URL的查询参数传递转化标签。
AdAttributionKitReengagementOpenRe-engagement
再互动
Re-engagement tracks users who already have the advertised app installed and
interact with an ad to return to it.
再互动用于追踪已安装被推广应用的用户,通过点击广告返回应用的行为。
Mark impressions as re-engagement eligible
标记曝光支持再互动
Set to in the JWS payload when generating
the impression.
eligible-for-re-engagementtrue生成曝光时,在JWS payload中设置为。
eligible-for-re-engagementtrueHandle re-engagement taps with a URL
使用URL处理再互动点击
Pass a universal link that the system opens in the advertised app:
swift
let reengagementURL = URL(string: "https://example.com/promo/summer")!
try await impression.handleTap(reengagementURL: reengagementURL)The system appends as a query parameter. The
advertised app checks for this parameter to detect AdAttributionKit-driven
opens:
AdAttributionKitReengagementOpenswift
func handleUniversalLink(_ url: URL) {
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
let isReengagement = components?.queryItems?.contains(where: {
$0.name == Postback.reengagementOpenURLParameter
}) ?? false
if isReengagement {
// AdAttributionKit opened this app via a re-engagement ad
}
}传递一个通用链接,系统会在被推广应用中打开该链接:
swift
let reengagementURL = URL(string: "https://example.com/promo/summer")!
try await impression.handleTap(reengagementURL: reengagementURL)系统会添加作为查询参数,被推广应用可检测该参数判断是否为AdAttributionKit触发的打开:
AdAttributionKitReengagementOpenswift
func handleUniversalLink(_ url: URL) {
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
let isReengagement = components?.queryItems?.contains(where: {
$0.name == Postback.reengagementOpenURLParameter
}) ?? false
if isReengagement {
// 本次应用打开由AdAttributionKit再互动广告触发
}
}Re-engagement limits
再互动限制
- Only click-through interactions create re-engagement postbacks (not view-through).
- The device enforces monthly per-app and yearly per-device re-engagement limits.
- The parameter is always present on the URL, even when the system does not create a postback.
AdAttributionKitReengagementOpen
- 仅点击互动会生成再互动回传(曝光类不会)。
- 设备会强制执行单应用每月、单设备每年的再互动上限。
- 即使系统未生成回传,URL上也始终会带有参数。
AdAttributionKitReengagementOpen
Common Mistakes
常见错误
Forgetting to update conversion value on launch
忘记在启动时更新转化值
swift
// DON'T -- never updating the conversion value
func appDidLaunch() {
// No conversion value update; postback window never starts
}
// DO -- update conversion value on first launch
func appDidLaunch() async {
try? await Postback.updateConversionValue(0, lockPostback: false)
}swift
// 错误做法:从不更新转化值
func appDidLaunch() {
// 无转化值更新,回传窗口永远不会启动
}
// 正确做法:首次启动时更新转化值
func appDidLaunch() async {
try? await Postback.updateConversionValue(0, lockPostback: false)
}Using uppercase ad network IDs
使用大写的广告网络ID
xml
<!-- DON'T -->
<string>Example123.AdAttributionKit</string>
<!-- DO -->
<string>example123.adattributionkit</string>xml
<!-- 错误 -->
<string>Example123.AdAttributionKit</string>
<!-- 正确 -->
<string>example123.adattributionkit</string>Calling handleTap without UIEventAttributionView
未添加UIEventAttributionView就调用handleTap
swift
// DON'T -- tap without attribution view overlay
try await impression.handleTap()
// Throws AdAttributionKitError.missingAttributionView
// DO -- ensure UIEventAttributionView covers the ad
let attributionView = UIEventAttributionView()
attributionView.frame = adView.bounds
adView.addSubview(attributionView)
// Then handle the tap after the user taps the attribution view
try await impression.handleTap()swift
// 错误做法:没有叠加归因视图就调用点击方法
try await impression.handleTap()
// 会抛出AdAttributionKitError.missingAttributionView
// 正确做法:确保UIEventAttributionView覆盖广告区域
let attributionView = UIEventAttributionView()
attributionView.frame = adView.bounds
adView.addSubview(attributionView)
// 用户点击归因视图后再处理点击事件
try await impression.handleTap()Ignoring handleTap errors
忽略handleTap的错误
swift
// DON'T
try? await impression.handleTap()
// DO -- handle specific errors
do {
try await impression.handleTap()
} catch let error as AdAttributionKitError {
switch error {
case .impressionExpired:
// Impression older than 30 days
refreshAdImpression()
case .missingAttributionView:
// UIEventAttributionView not present
break
default:
print("Attribution error: \(error)")
}
}swift
// 错误做法
try? await impression.handleTap()
// 正确做法:处理特定错误
do {
try await impression.handleTap()
} catch let error as AdAttributionKitError {
switch error {
case .impressionExpired:
// 曝光已超过30天有效期
refreshAdImpression()
case .missingAttributionView:
// 未添加UIEventAttributionView
break
default:
print("归因错误:\(error)")
}
}Not responding to postback requests
不响应回传请求
swift
// DON'T -- silently dropping the request
// The device retries up to 9 times over 9 days on HTTP 500
// DO -- respond with 200 OK immediately
// Server handler:
func handlePostback(request: Request) -> Response {
// Process asynchronously, respond immediately
Task { await processPostback(request.body) }
return Response(status: .ok)
}swift
// 错误做法:静默丢弃请求
// 遇到HTTP 500时设备会在9天内最多重试9次
// 正确做法:立即返回200 OK
// 服务器处理逻辑:
func handlePostback(request: Request) -> Response {
// 异步处理请求,立即响应
Task { await processPostback(request.body) }
return Response(status: .ok)
}Review Checklist
检查清单
- Publisher app includes all ad network IDs in (lowercase)
AdNetworkIdentifiers - Ad network IDs match between publisher app's Info.plist and JWS
kid - overlays ad content for click-through ads
UIEventAttributionView - Advertised app calls on first launch
updateConversionValue - Server endpoint at well-known path accepts HTTPS POST with valid SSL
- Postback verification uses correct Apple public key for environment
- Duplicate postbacks filtered by
postback-identifier - Server responds with HTTP 200 to postback requests
- Re-engagement URL is a registered universal link for the advertised app
- Conversion value strategy accounts for all three conversion windows
- checked before attempting impression APIs
AppImpression.isSupported
- 发布方应用在中包含所有广告网络ID(小写)
AdNetworkIdentifiers - 发布方应用Info.plist中的广告网络ID与JWS的一致
kid - 点击类广告的内容上叠加了
UIEventAttributionView - 被推广应用在首次启动时调用了
updateConversionValue - 固定路径的服务器端点接受HTTPS POST请求,SSL证书有效
- 回传验证使用了对应环境的正确Apple公钥
- 已通过过滤重复回传
postback-identifier - 服务器对回传请求返回HTTP 200响应
- 再互动URL是被推广应用已注册的通用链接
- 转化值策略覆盖了全部三个转化窗口
- 调用曝光API前已检查
AppImpression.isSupported
References
参考资料
- references/adattributionkit-patterns.md -- postback verification, server handling, testing, SKAdNetwork migration, alternative marketplaces, attribution rules configuration
- Apple: AdAttributionKit
- Apple: Presenting ads in your app
- Apple: Receiving ad attributions and postbacks
- Apple: Verifying a postback
- Apple: SKAdNetwork interoperability
- references/adattributionkit-patterns.md -- 回传验证、服务端处理、测试、SKAdNetwork迁移、其他应用市场适配、归因规则配置
- Apple: AdAttributionKit
- Apple: 在应用中展示广告
- Apple: 接收广告归因与回传
- Apple: 验证回传
- Apple: SKAdNetwork互通