Server-to-Server - 移动应用 S2S 指南

概述

本指南介绍在共享 S2S 机制之上叠加的移动端专属归因信号:Apple Search Ads、Google Play 和 Meta 安装引荐来源、卸载追踪以及 iOS 安装收据。它适用于以 server-to-server 方式上报会话和事件的 iOS 和 Android 应用集成。

这些功能假设您已经建立了共享的 S2S 请求流程。有关它们所依赖的端点、身份验证和通用参数,请从基础知识中心和两个端点参考开始。

开始之前: 这些功能构建于共享的 S2S 请求流程之上。有关端点和通用参数,请参阅 S2S 基础知识中心 ;有关 SESSION 端点参考 ;以及 EVENT 端点参考


Apple Search Ads 归因

iOS Search Ads 集成

Apple Search Ads 是一种自归因网络 (SAN),需要通过 Apple 框架进行平台专属的归因实现。

框架支持:

双重实现: 在 iAd 被弃用之前,请同时实现 iAd 和 AdServices 框架。Singular 在归因和报告中会优先使用 AdServices 信号而非 iAd 信号。

完整指南: Apple Search Ads 集成文档


iAd 框架实现

针对 iOS 14.2 及以下版本,通过 iAd 框架检索并发送 Apple Search Ads 归因数据。

第 1 步:检索归因数据

Objective-C
#import <iAd/iAd.h>

Class ADClientClass = NSClassFromString(@"ADClient");

if (ADClientClass) {
    id sharedClient = [ADClientClass performSelector:@selector(sharedClient)];

    if ([sharedClient respondsToSelector:@selector(requestAttributionDetailsWithBlock:)]) {
        [sharedClient requestAttributionDetailsWithBlock:^(NSDictionary *attributionDetails, NSError *error) {
            if (attributionDetails && attributionDetails.count > 0) {
                // REPORT attributionDetails FROM YOUR APP TO YOUR SERVER
            }
        }];
    }
}

延迟处理: 点击 Search Ads 的用户可能会立即下载并打开应用。请通过以下方式防止归因时序问题:

  1. 在检索归因数据之前设置几秒钟的延迟
  2. 如果响应为 False 或返回错误码 (0、2、3),则实现重试逻辑。在 2 秒后重试

第 2 步:发送至 Singular

通过 EVENT 端点 上报保留名称为 __iAd_Attribution__ 的事件, 并将归因 JSON 作为 e 参数传递。

时序要求:

  • iOS 13+:在安装/重装后的首个会话之后立即发送 __iAd_Attribution__ 事件。否则 Apple Search Ads 数据将不会用于归因
  • iOS 14+:归因响应仅在 特定条件下 可用,如果 ATT 状态为 ATTrackingManager.AuthorizationStatus.denied 则不可用

AdServices 框架实现

针对 iOS 14.3 及以上版本,通过 AdServices 框架检索并发送归因令牌。

第 1 步:检索归因令牌

Objective-C
#import <AdServices/AdServices.h>

NSError *error = nil;
Class AAAttributionClass = NSClassFromString(@"AAAttribution");
if (AAAttributionClass) {
    NSString *attributionToken = [AAAttributionClass attributionTokenWithError:&error];
    if (!error && attributionToken) {
        // Handle attributionToken
    }
}

令牌特性:

  • 在设备上生成
  • 缓存 5 分钟——如果调用 attributionToken() ,则会在过期后生成新令牌
  • 有效期为 24 小时

第 2 步:发送至 Singular

对令牌进行 URL 编码,并在每次安装和重装后的首个会话上,将其作为 attribution_token 参数附加到 SESSION 端点


Google Play 安装引荐来源

Android 安装归因

Google Play 安装引荐来源提供最准确的 Android 安装归因,其中包含用户在到达 Play Store 之前的来源信息。

在以下情况下为必需项:

更多信息: Google 安装引荐来源文档

实现步骤:

  1. 在首次打开应用时,使用 Play Install Referrer API 检索安装引荐来源
  2. 通过 SESSION 端点 上报会话,其中包含带有必需属性的 install_ref JSON 参数

install_ref 属性:

参数 详情
referrer

来自 Play Install Referrer API 的引荐来源值(JSON 对象——编码为字符串)

referrer_source

指定为 "service"

clickTimestampSeconds

来自 API 的点击时间戳(例如 "1550420123")

installBeginTimestampSeconds

来自 API 的安装开始时间

current_device_time

当前设备时间(毫秒,例如 "1550420454906")


Meta 安装引荐来源

Facebook 归因增强

自 2025 年 6 月 18 日起: Meta 的 高级移动测量 (AMM) 消除了实现 Meta 安装引荐来源的需求。如果已启用 AMM,则不推荐使用。

Meta Referrer 是 Android 专属的测量解决方案,结合了 Google Play 安装引荐来源和 Meta 安装引荐来源技术,为应用安装提供精细的用户级归因数据。

了解更多: Meta Referrer 常见问题

实现步骤:

  1. 在首次打开应用时,根据 Meta 的文档 检索 Meta 安装引荐来源
  2. 通过 SESSION 端点 上报会话,其中包含带有必需属性的 meta_ref JSON 参数

meta_ref 属性:

参数 详情
is_ct

来自 Meta 安装引荐来源的 is_ct(0 或 1)

install_referrer

来自 Meta 安装引荐来源的 install_referrer

actual_timestamp

来自 Meta 安装引荐来源的 actual_timestamp(例如 1693978124)


卸载追踪

静默推送通知设置

通过在每次会话通知中发送推送令牌,使用设备静默推送通知来追踪卸载。

设置要求:

  1. 遵循平台专属的卸载追踪设置:
  2. 将平台专属的令牌附加到 SESSION 请求:

iOS 安装收据

收据验证

在上报 iOS 会话时,将 iOS 安装收据传入 install_receipt 参数。

Swift
import Foundation
import StoreKit

class ReceiptManager {
    static func getInstallReceipt() -> String? {
        if #available(iOS 18.0, *) {
            let semaphore = DispatchSemaphore(value: 0)
            var result: String?

            Task {
                do {
                    let transaction = try await AppTransaction.shared
                    result = transaction.jwsRepresentation
                    semaphore.signal()
                } catch {
                    debugPrint("Failed to get app transaction: \(error.localizedDescription)")
                    semaphore.signal()
                }
            }

            semaphore.wait()
            return result

        } else {
            guard let receiptURL = Bundle.main.appStoreReceiptURL else {
                debugPrint("Receipt URL not found")
                return nil
            }

            do {
                let receiptData = try Data(contentsOf: receiptURL, options: .uncached)
                return receiptData.base64EncodedString(options: [])
            } catch {
                debugPrint("Failed to read receipt: \(error.localizedDescription)")
                return nil
            }
        }
    }
}