订阅事件追踪实施指南
通过 Singular 的 SDK、服务器到服务器(S2S)集成或第三方合作伙伴,实施全面的订阅追踪,以衡量所有平台上的订阅收入、用户留存和广告系列效果。
Singular 能够将订阅事件(包括新订阅、续订、试用、取消和退款)准确归因到带来用户的营销广告系列,从而全面掌握订阅生命周期和收入产生情况。
实施方案
请选择最适合您应用架构和业务需求的实施方法。
| 考量因素 | SDK 集成 | 服务器到服务器(S2S) | 第三方集成 |
|---|---|---|---|
| 实施方式 | 通过 Singular SDK 中的自定义收入方法发送订阅事件 | 从您的后端向 Singular 的 REST API 发送事件,并附带所需的移动设备属性 | 配置与 RevenueCat、Adapty、Superwall、Xsolla 或其他订阅平台的集成 |
| 对应用活动的依赖 | 应用必须启动,SDK 才能发送事件 | 事件实时发送,不依赖应用状态 | 事件从合作伙伴平台实时发送 |
| 事件时间 | 事件时间可能因应用启动时机而与实际订阅时间不同 | 在后端处理时进行实时事件追踪 | 在合作伙伴处理后进行近乎实时的追踪 |
| 复杂度 | 简单的客户端实施 | 需要安全的后端基础设施 | 需要合作伙伴账户和配置 |
重要提示: 大多数使用 S2S 或第三方集成处理订阅事件的客户仍应集成 Singular SDK,以管理会话和非订阅事件,从而实现完整的归因追踪。
Android:Google Play Billing 集成
集成 Google Play Billing Library 8.0.0,以查询订阅信息并直接在设备上管理订阅状态,用于 Singular SDK 集成。
前提条件
配置 Google Play Console
在应用中实施结算功能之前,先在 Google Play Console 中设置应用内商品和订阅项目。
- 创建订阅商品: 在 Play Console 中定义订阅层级、结算周期和定价
- 配置优惠: 设置基础套餐、优惠令牌和促销定价
- 测试设置: 创建测试账户并验证商品可用性
添加 Billing Library 依赖项
更新 build.gradle
将 Google Play Billing Library 8.0.0 添加到应用的依赖项中,并附带用于协程支持的 Kotlin 扩展。
dependencies {
val billingVersion = "8.0.0"
// Google Play Billing Library
implementation("com.android.billingclient:billing:$billingVersion")
// Kotlin extensions and coroutines support
implementation("com.android.billingclient:billing-ktx:$billingVersion")
}
8.0.0 版本功能:
此版本引入了通过
enableAutoServiceReconnection()
实现的服务自动重连、改进的购买查询 API 以及增强的待处理交易处理功能。
初始化 BillingClient
创建结算管理器
设置一个结算管理器类来处理所有订阅操作,并与 Singular SDK 集成以进行事件追踪。
class SubscriptionManager(private val context: Context) {
private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases ->
when (billingResult.responseCode) {
BillingResponseCode.OK -> {
purchases?.forEach { purchase ->
handlePurchaseUpdate(purchase)
}
}
BillingResponseCode.USER_CANCELED -> {
Log.d(TAG, "User canceled the purchase")
}
else -> {
Log.e(TAG, "Purchase error: ${billingResult.debugMessage}")
}
}
}
private val billingClient = BillingClient.newBuilder(context)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases() // Required for all purchases
.enableAutoServiceReconnection() // NEW in 8.0.0 - handles reconnection automatically
.build()
fun initialize() {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingResponseCode.OK) {
Log.d(TAG, "Billing client ready")
// Query existing subscriptions
queryExistingSubscriptions()
} else {
Log.e(TAG, "Billing setup failed: ${billingResult.debugMessage}")
}
}
override fun onBillingServiceDisconnected() {
// With enableAutoServiceReconnection(), SDK handles reconnection
// Manual retry is no longer required
Log.d(TAG, "Billing service disconnected - auto-reconnection enabled")
}
})
}
companion object {
private const val TAG = "SubscriptionManager"
}
}
主要功能:
-
自动重连:
8.0.0 版本的
enableAutoServiceReconnection()会在需要时自动重新建立连接 - 购买监听器: 接收所有购买更新的回调,包括续订和待处理交易
- 错误处理: 对用户取消和结算错误进行全面处理
查询订阅信息
检索有效订阅
使用
queryPurchasesAsync()
查询现有订阅,以处理续订以及在应用之外购买的订阅。
private suspend fun queryExistingSubscriptions() {
val params = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.build()
withContext(Dispatchers.IO) {
val result = billingClient.queryPurchasesAsync(params)
if (result.billingResult.responseCode == BillingResponseCode.OK) {
result.purchasesList.forEach { purchase ->
when (purchase.purchaseState) {
Purchase.PurchaseState.PURCHASED -> {
// Active subscription - process it
handleSubscriptionPurchase(purchase)
}
Purchase.PurchaseState.PENDING -> {
// Pending payment - notify user
Log.d(TAG, "Subscription pending: ${purchase.products}")
}
}
}
} else {
Log.e(TAG, "Query failed: ${result.billingResult.debugMessage}")
}
}
}
最佳实践:
在
onResume()
中调用
queryPurchasesAsync()
,以捕获在应用处于后台或已关闭期间发生的订阅续订。
查询商品详情
在向用户展示之前,检索订阅商品详情,包括定价、结算周期和优惠令牌。
private suspend fun querySubscriptionProducts() {
val productList = listOf(
QueryProductDetailsParams.Product.newBuilder()
.setProductId("premium_monthly")
.setProductType(BillingClient.ProductType.SUBS)
.build(),
QueryProductDetailsParams.Product.newBuilder()
.setProductId("premium_yearly")
.setProductType(BillingClient.ProductType.SUBS)
.build()
)
val params = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build()
withContext(Dispatchers.IO) {
val productDetailsResult = billingClient.queryProductDetails(params)
if (productDetailsResult.billingResult.responseCode == BillingResponseCode.OK) {
// Process successfully retrieved product details
productDetailsResult.productDetailsList?.forEach { productDetails ->
// Extract subscription offers
productDetails.subscriptionOfferDetails?.forEach { offer ->
val price = offer.pricingPhases.pricingPhaseList.firstOrNull()
Log.d(TAG, "Product: ${productDetails.productId}, Price: ${price?.formattedPrice}")
}
}
// Handle unfetched products
productDetailsResult.unfetchedProductList?.forEach { unfetchedProduct ->
Log.w(TAG, "Unfetched: ${unfetchedProduct.productId}")
}
}
}
}
处理购买更新
处理订阅事件
根据订阅生命周期,处理购买状态变更并向 Singular 发送相应的事件。
private fun handlePurchaseUpdate(purchase: Purchase) {
when (purchase.purchaseState) {
Purchase.PurchaseState.PURCHASED -> {
if (!purchase.isAcknowledged) {
// Verify purchase on your backend first
verifyPurchaseOnBackend(purchase) { isValid ->
if (isValid) {
// Send subscription event to Singular
trackSubscriptionToSingular(purchase)
// Grant entitlement to user
grantSubscriptionAccess(purchase)
// Acknowledge purchase to Google
acknowledgePurchase(purchase)
}
}
}
}
Purchase.PurchaseState.PENDING -> {
// Notify user that payment is pending
Log.d(TAG, "Purchase pending approval: ${purchase.products}")
notifyUserPendingPayment(purchase)
}
}
}
private fun trackSubscriptionToSingular(purchase: Purchase) {
// Get cached product details for pricing information
val productDetails = getCachedProductDetails(purchase.products.first())
productDetails?.let { details ->
val subscriptionOffer = details.subscriptionOfferDetails?.firstOrNull()
val pricingPhase = subscriptionOffer?.pricingPhases?.pricingPhaseList?.firstOrNull()
val price = pricingPhase?.priceAmountMicros?.div(1_000_000.0) ?: 0.0
val currency = pricingPhase?.priceCurrencyCode ?: "USD"
// Determine event name based on purchase type
val eventName = if (isNewSubscription(purchase)) {
"sng_subscribe"
} else {
"subscription_renewed"
}
// Send to Singular WITHOUT receipt
Singular.customRevenue(
eventName,
currency,
price,
mapOf(
"subscription_id" to purchase.products.first(),
"order_id" to (purchase.orderId ?: ""),
"purchase_time" to purchase.purchaseTime.toString()
)
)
Log.d(TAG, "Tracked $eventName to Singular: $price $currency")
}
}
private fun acknowledgePurchase(purchase: Purchase) {
val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
lifecycleScope.launch {
val ackResult = withContext(Dispatchers.IO) {
billingClient.acknowledgePurchase(acknowledgePurchaseParams)
}
if (ackResult.responseCode == BillingResponseCode.OK) {
Log.d(TAG, "Purchase acknowledged successfully")
} else {
Log.e(TAG, "Acknowledgement failed: ${ackResult.debugMessage}")
}
}
}
关键提示:
在追踪订阅事件时,请勿将 Google Play 收据发送给 Singular。使用
customRevenue()
方法且不进行收据验证。请在 3 天内确认购买,否则购买将被自动退款。
iOS:StoreKit 集成
集成 Apple 的 StoreKit 框架,以管理订阅并向 Singular 发送 iOS 应用的事件。
前提条件
配置 App Store Connect
在应用中实施 StoreKit 之前,先在 App Store Connect 中设置订阅商品和订阅群组。
- 创建订阅群组: 根据访问级别将订阅组织成群组
- 定义订阅商品: 设置订阅层级、时长和定价
- 配置优惠: 创建介绍性优惠、促销优惠和订阅代码
- 测试环境: 为开发设置沙盒测试账户
实施 StoreKit
设置交易观察器
实施
SKPaymentTransactionObserver
以接收订阅购买通知和续订。
import StoreKit
class SubscriptionManager: NSObject, SKPaymentTransactionObserver {
static let shared = SubscriptionManager()
private override init() {
super.init()
// Add transaction observer
SKPaymentQueue.default().add(self)
}
deinit {
SKPaymentQueue.default().remove(self)
}
// MARK: - SKPaymentTransactionObserver
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
// New subscription or renewal
handlePurchasedTransaction(transaction)
case .restored:
// Subscription restored from another device
handleRestoredTransaction(transaction)
case .failed:
// Purchase failed
handleFailedTransaction(transaction)
case .deferred:
// Purchase awaiting approval (family sharing)
print("Transaction deferred: \(transaction.payment.productIdentifier)")
case .purchasing:
// Transaction in progress
break
@unknown default:
break
}
}
}
private func handlePurchasedTransaction(_ transaction: SKPaymentTransaction) {
// Verify receipt with your backend
verifyReceipt { isValid in
if isValid {
// Send to Singular
self.trackSubscriptionToSingular(transaction)
// Grant entitlement
self.grantSubscriptionAccess(transaction)
// Finish transaction
SKPaymentQueue.default().finishTransaction(transaction)
}
}
}
private func trackSubscriptionToSingular(_ transaction: SKPaymentTransaction) {
// Get product details for pricing
let productId = transaction.payment.productIdentifier
// You should cache product details from SKProductsRequest
if let product = getCachedProduct(productId) {
let price = product.price.doubleValue
let currency = product.priceLocale.currencyCode ?? "USD"
// Determine event name
let eventName = isNewSubscription(transaction) ? "sng_subscribe" : "subscription_renewed"
// Send to Singular WITHOUT receipt
Singular.customRevenue(
eventName,
currency: currency,
amount: price,
withAttributes: [
"subscription_id": productId,
"transaction_id": transaction.transactionIdentifier ?? "",
"transaction_date": transaction.transactionDate?.timeIntervalSince1970 ?? 0
]
)
}
}
}
查询订阅状态
使用收据验证来检查当前订阅状态并处理续订。
func refreshSubscriptionStatus() {
let request = SKReceiptRefreshRequest()
request.delegate = self
request.start()
}
extension SubscriptionManager: SKRequestDelegate {
func requestDidFinish(_ request: SKRequest) {
// Receipt refreshed successfully
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
// Send receipt to your backend for validation
validateReceiptOnServer()
}
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("Receipt refresh failed: \(error.localizedDescription)")
}
}
SDK 集成方法
使用 Singular SDK 的自定义收入方法从您的应用发送订阅事件,无需进行收据验证。
各平台专用的 SDK 方法
请使用适用于您平台的 SDK 方法来追踪订阅事件。
Android SDK
// Track subscription without receipt
Singular.customRevenue(
"sng_subscribe", // Event name
"USD", // Currency
9.99, // Amount
mapOf( // Additional attributes
"subscription_id" to "premium_monthly",
"billing_period" to "monthly"
)
)
文档: Android SDK 收入追踪
iOS SDK
// Track subscription without receipt
Singular.customRevenue(
"sng_subscribe", // Event name
currency: "USD", // Currency
amount: 9.99, // Amount
withAttributes: [ // Additional attributes
"subscription_id": "premium_monthly",
"billing_period": "monthly"
]
)
文档: iOS SDK 收入追踪
React Native SDK
// Track subscription without receipt
Singular.customRevenueWithArgs(
"sng_subscribe", // Event name
"USD", // Currency
9.99, // Amount
{ // Additional attributes
subscription_id: "premium_monthly",
billing_period: "monthly"
}
);
Unity SDK
// Track subscription without receipt
SingularSDK.CustomRevenue(
"sng_subscribe", // Event name
"USD", // Currency
9.99, // Amount
new Dictionary<string, object> { // Additional attributes
{ "subscription_id", "premium_monthly" },
{ "billing_period", "monthly" }
}
);
文档: Unity SDK 收入追踪
Flutter SDK
// Track subscription without receipt
Singular.customRevenueWithAttributes(
"sng_subscribe", // Event name
"USD", // Currency
9.99, // Amount
{ // Additional attributes
"subscription_id": "premium_monthly",
"billing_period": "monthly"
}
);
文档: Flutter SDK 收入追踪
Cordova SDK
// Track subscription without receipt
cordova.plugins.SingularCordovaSdk.customRevenueWithArgs(
"sng_subscribe", // Event name
"USD", // Currency
9.99, // Amount
{ // Additional attributes
subscription_id: "premium_monthly",
billing_period: "monthly"
}
);
文档: Cordova SDK 收入追踪
关键提示:
追踪订阅时,请勿使用 IAP(应用内购买)方法或向 Singular 发送收据值。只需使用不带收据的
customRevenue()
方法。
服务器到服务器集成
从您的后端向 Singular 的 REST API 实时发送订阅事件,以便进行即时追踪,且不依赖应用状态。
实施要求
使用 Singular 的 Event Endpoint,从您的安全后端发送带有收入参数的订阅事件。
Event Endpoint
向 Singular 的 Event API 发送 POST 请求,附带订阅事件数据和所需的移动设备属性。
端点:
POST https://api.singular.net/api/v1/evt
必需参数:
-
SDK Key:
您的 Singular SDK 密钥(
a参数) -
事件名称:
订阅事件名称(
n参数) -
收入:
订阅金额(
amt参数) -
货币:
ISO 4217 货币代码(
cur参数) - 设备标识符: 用于归因的 IDFA(iOS)或 GAID(Android)
-
平台:
操作系统(
p参数)
文档: Event Endpoint 参考 以及 收入参数
示例请求
curl -X POST 'https://api.singular.net/api/v1/evt' \
-H 'Content-Type: application/json' \
-d '{
"a": "YOUR_SDK_KEY",
"n": "sng_subscribe",
"r": 9.99,
"pcc": "USD",
"idfa": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"p": "iOS",
"install_time": 1697000000000,
"extra": {
"subscription_id": "premium_monthly",
"order_id": "GPA.1234.5678.9012"
}
}'
无需 SDK: 如果使用 S2S 处理订阅事件,则无需从 SDK 发送这些事件。但是,大多数应用仍应集成 SDK 以进行会话追踪和非订阅事件追踪。
第三方集成
通过管理订阅基础设施并直接向 Singular 发送事件的第三方平台,配置订阅追踪。
支持的合作伙伴
与内置 Singular 支持的订阅管理平台集成。
RevenueCat 集成
RevenueCat 负责处理订阅基础设施,并自动向 Singular 的 REST API 发送订阅事件。
设置步骤:
- 创建 RevenueCat 账户: 在 RevenueCat 控制面板中设置您的应用
- 配置 Singular 集成: 在 RevenueCat 集成设置中添加 Singular SDK 密钥
- 映射事件: 配置哪些 RevenueCat 事件应发送到 Singular
- 测试集成: 验证事件是否出现在 Singular 控制面板中
Adapty 集成
Adapty 提供订阅分析,并通过其集成向 Singular 发送事件。
设置步骤:
- 创建 Adapty 账户: 在 Adapty 中注册并配置您的应用
- 启用 Singular 集成: 导航至集成设置并添加 Singular 凭据
- 配置事件映射: 选择要转发的订阅事件
- 验证数据流: 确认事件成功到达 Singular
Superwall 集成
连接 Singular 以接收 Superwall 的订阅生命周期和收入事件,并借助 SDID 支持实现更强的归因。
设置步骤:
- 集成 Singular SDK: 将 Singular SDK 添加到您的应用,以进行会话和事件追踪
- 设置 Singular SDID(可选): 设置 Singular SDID,并在应用中使用该 SDID 更新 Superwall 用户属性,以实现更强的归因
- 在 Superwall 中添加 Singular 集成: 在您的 Superwall 控制面板中启用并配置 Singular 集成
Xsolla 集成
Xsolla 将 Web Shop 中的购买信息作为移动应用内事件发送给 Singular,随后 Singular 将这些事件归因到移动应用安装、用户获取和再互动广告系列。
设置步骤:
- 在您的移动应用中集成 Singular: 将 Singular SDK 添加到您的移动应用中
- 配置应用内事件: 向 Singular 发送应用内事件,例如登录,或任何其他包含 CUID 的事件
- 配置 Singular 合作伙伴配置: 为 Xsolla 设置 Singular 合作伙伴配置
- 在您的 Xsolla 账户中添加 Singular: 在您的 Xsolla 账户中启用 Singular
注意: 第三方集成由合作伙伴平台管理。配置必须在其各自的控制面板中完成。仍应集成 Singular SDK 以进行会话和非订阅事件追踪。
订阅事件类型
追踪各种订阅生命周期事件,以衡量整个订阅历程中的用户互动、留存和收入。
标准事件名称
使用 Singular 的标准事件名称,以实现跨平台的一致报告,或根据您的追踪需求定义自定义事件名称。
| 订阅状态 | 事件名称 | SDK 方法 | S2S 集成 |
|---|---|---|---|
| 新订阅 |
sng_subscribe
|
带收入金额的 customRevenue()(无收据) | 带收入参数的 Event endpoint |
| 试用开始 |
sng_start_trial
|
不带收入的 event() 方法 | 不带收入的标准 Event endpoint |
| 试用结束 |
sng_end_trial
|
不带收入的 event() 方法 | 不带收入的标准 Event endpoint |
| 订阅续订 |
subscription_renewed
|
带收入金额的 customRevenue()(无收据) | 带收入参数的 Event endpoint |
| 取消 |
subscription_cancelled
|
不带收入的 event() 方法 | 不带收入的标准 Event endpoint |
| 退款 |
subscription_refunded
|
带负值金额的 customRevenue()(可选)或不带收入的 event() | 带负收入的 Event endpoint(可选)或标准事件 |
重要提示:
通过 customRevenue() 发送订阅事件时,只发送
isRestored
为
false
的事件。已恢复的购买代表现有订阅,而非新收入。
事件实施示例
新订阅
追踪用户首次购买新订阅的时间。
// New subscription purchase
Singular.customRevenue(
"sng_subscribe",
"USD",
9.99,
mapOf(
"subscription_tier" to "premium",
"billing_period" to "monthly",
"product_id" to "premium_monthly"
)
)
试用开始
追踪用户开始免费试用期的时间,不含收入。
// Trial start (no revenue)
Singular.event(
"sng_start_trial",
mapOf(
"trial_duration" to "7_days",
"subscription_tier" to "premium"
)
)
订阅续订
追踪带收入金额的自动订阅续订。
// Subscription renewal
Singular.customRevenue(
"subscription_renewed",
"USD",
9.99,
mapOf(
"subscription_tier" to "premium",
"renewal_count" to 3,
"product_id" to "premium_monthly"
)
)
取消
追踪用户取消订阅的时间(无收入事件)。
// Subscription cancelled (no revenue)
Singular.event(
"subscription_cancelled",
mapOf(
"cancellation_reason" to "too_expensive",
"days_active" to 45,
"subscription_tier" to "premium"
)
)
SKAdNetwork 衡量
通过 Apple 的 SKAdNetwork 衡量订阅事件,以实现符合 iOS 隐私要求的归因。
SKAN 订阅支持
Singular 可以通过所有集成方法(SDK、S2S 和第三方)衡量订阅事件数据。事件衡量可能会受到 SKAN postback 时间窗口的限制,具体取决于 SKAN 版本。
混合 SKAN 实施
对于使用 Singular SDK 处理生命周期事件、使用 S2S/第三方集成处理订阅的应用,请启用混合 SKAN 以合并这两种数据源。
联系支持团队: 请联系您的客户成功经理(CSM)为您的应用启用混合 SKAN。这可确保来自后端来源的订阅事件在 SKAN 转化值中得到正确归因。
SKAN Postback 窗口:
- SKAN 4.0: 三个 postback 窗口(0-2 天、3-7 天、8-35 天)可衡量早期订阅事件和短期续订
- SKAN 3.0: 单个 24 小时 postback 窗口将衡量限制为仅即时订阅
- 转化值: 在您的 SKAN 转化值架构中配置订阅事件,以优先处理高价值事件
最佳实践与验证
在生产环境部署之前,请遵循实施最佳实践并验证订阅追踪是否正常工作。
实施最佳实践
关键准则
- 无收据验证: 追踪订阅时,切勿向 Singular 发送平台收据(Google Play、App Store)——请使用不带收据的 customRevenue()
- 过滤已恢复的购买: 只发送 isRestored 为 false 的事件,以避免重复的收入报告
- 后端验证: 在向 Singular 发送事件之前,务必在您的安全后端上验证购买
- 及时确认: 在 3 天内确认 Google Play 购买,以防止自动退款
- 在 Resume 时查询: 在 onResume() 中调用 queryPurchasesAsync(),以捕获在应用关闭期间发生的续订
- 一致的事件名称: 使用标准 Singular 事件名称(sng_subscribe、subscription_renewed)以实现统一报告
- 包含元数据: 添加 subscription_tier、billing_period 和 product_id 等属性,以进行详细分析
测试与验证
测试检查清单
- 测试环境设置: 为 Google Play 和 App Store 均使用沙盒/测试账户
- 新订阅: 验证 sng_subscribe 事件是否以正确的金额和货币正常发送
- 试用流程: 测试不带收入的试用开始和试用结束事件
- 续订处理: 确认续订触发带正确收入的 subscription_renewed 事件
- 取消追踪: 验证用户取消订阅时是否触发取消事件
- 恢复过滤: 确保已恢复的购买不会发送重复事件
- Singular 控制面板: 检查事件是否以正确的归因出现在 Singular 中
- 跨设备: 测试订阅在多个设备之间的同步
常见问题
- 事件缺失: 验证结算客户端连接处于活动状态,且已在应用 resume 时调用 queryPurchasesAsync()
- 重复事件: 检查是否通过 isRestored 检查正确过滤了已恢复的购买
- 收入错误: 确保从 ProductDetails 中提取价格时正确地将 micros 除以 1,000,000
- 归因问题: 确认设备标识符(IDFA/GAID)已正确包含在 S2S 请求中
- 确认失败: 验证后端收据验证在确认之前完成
支持资源: 如需故障排查协助,请联系 Singular Support,并提供 SDK 版本、平台详情、示例事件负载以及显示问题的 Singular 控制面板截图。