订阅事件跟踪实施指南
使用 Singular 的 SDK、服务器到服务器集成或第三方合作伙伴,在所有平台上实施全面的订阅跟踪,以衡量订阅收入、用户保留率和营销活动绩效。
Singular 能够将订阅事件(包括新订阅、续订、试用、取消和退款)准确地归因于产生用户的营销活动,从而提供订阅生命周期和创收的完整可见性。
实施选项
选择最适合您的应用程序架构和业务需求的实施方法。
| 考虑因素 | SDK 集成 | 服务器到服务器 (S2S) | 第三方集成 |
|---|---|---|---|
| 实施 | 通过Singular SDK中的自定义收入方法发送订阅事件 | 将事件从您的后台发送到Singular的REST API,并提供所需的移动设备属性 | 配置与 RevenueCat、Adapty 或其他订阅平台的集成 |
| 应用程序活动依赖性 | 必须启动应用程序,SDK 才能发送事件 | 实时发送事件,与应用程序状态无关 | 从合作伙伴平台实时发送事件 |
| 事件时间 | 根据应用程序的启动情况,事件时间可能与实际订阅时间不同 | 后台处理后的实时事件跟踪 | 合作伙伴处理后的近实时跟踪 |
| 复杂性 | 客户端实施简单 | 需要安全的后台基础设施 | 需要合作伙伴账户和配置 |
重要提示:大多数使用 S2S 或第三方集成来处理订阅事件的客户仍应集成 Singular SDK 来管理会话和非订阅事件,以实现完整的归因跟踪。
安卓:谷歌播放计费集成
集成 Google Play Billing Library 8.0.0,直接在设备上查询订阅信息和管理订阅状态,以实现 Singular SDK 集成。
前提条件
配置 Google Play 控制台
在应用中实施计费之前,在Google Play 控制台中设置应用内产品和订阅项目。
- 创建订阅产品:在 Play 控制台中定义订阅层级、计费期和定价
- 配置优惠:设置基本计划、优惠代币和促销价格
- 测试设置:创建测试账户并验证产品可用性
添加计费库依赖关系
更新 build.gradle
将 Google Play Billing Library 8.0.0 添加到应用程序的依赖关系中,并使用 Kotlin 扩展来支持 coroutine。
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 和增强的待处理事务处理。
初始化账单客户端
创建账单管理器
建立一个计费管理器类来处理所有订阅操作,并与 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}")
}
}
}
关键:跟踪订阅事件时,不要向 Singular 发送 Google Play 收据。使用customRevenue() 方法,无需验证收据。在 3 天内确认购买,否则将自动退款。
iOS:StoreKit 集成
为 iOS 应用程序集成 Apple StoreKit 框架,以管理订阅并向 Singular 发送事件。
前提条件
配置应用商店连接
在应用程序中实施 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 方法来跟踪订阅事件。
安卓 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"
)
)
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 原生 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" }
}
);
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"
}
);
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"
}
);
关键:切勿使用 IAP(应用内购买)方法或向 Singular 发送收据值以进行订阅跟踪。只使用customRevenue() 方法,不使用收据。
服务器到服务器集成
从您的后台向 Singular 的 REST API 实时发送订阅事件,以实现独立于应用程序状态的即时跟踪。
实施要求
使用 Singular 的事件端点从安全的后台发送带有收入参数的订阅事件。
事件端点
向 Singular 的事件 API 发送带有订阅事件数据和所需移动设备属性的 POST 请求。
端点:
POST https://api.singular.net/api/v1/evt
所需参数:
-
SDK 密钥:您的Singular SDK密钥(
a参数) -
事件名称:订阅事件名称 (
n参数) -
收入:订阅金额 (
amt参数) -
货币:ISO 4217 货币代码 (
cur参数) - 设备标识符:用于归属的 IDFA(iOS)或 GAID(Android)
-
平台:操作系统 (
p参数)
请求示例
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
注:第三方集成由合作伙伴平台管理。仍应集成 Singular SDK 以跟踪会话和非订阅事件。
订阅事件类型
跟踪各种订阅生命周期事件,以衡量整个订阅过程中的用户参与度、保留率和收入。
标准事件名称
使用 Singular 的标准事件名称实现跨平台的一致报告,或根据您的跟踪要求定义自定义事件名称。
| 订阅状态 | 事件名称 | SDK 方法 | S2S 集成 |
|---|---|---|---|
| 新订阅 |
sng_subscribe
|
带有收入金额(无收据)的 customRevenue() | 带收入参数的事件端点 |
| 试用开始 |
sng_start_trial
|
不含收入的 event() 方法 | 不含收入的标准事件终点 |
| 试用结束 |
sng_end_trial
|
事件()方法,不含收益 | 标准事件终点,无收入 |
| 续订 |
subscription_renewed
|
自定义收入(),有收入金额(无收据) | 带收入参数的事件终端 |
| 取消 |
subscription_cancelled
|
无收入的 event() 方法 | 无收入的标准事件终点 |
| 退款 |
subscription_refunded
|
自定义收入(),带负值(可选)或不带收入的事件() | 带负收入的事件终端(可选)或标准事件 |
重要:通过 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 版本的不同,事件测量可能会受到 SKAN 回传定时窗口的限制。
混合 SKAN 实施
对于使用 Singular SDK 获取生命周期事件和使用 S2S/第三方集成获取订阅的应用程序,请启用混合 SKAN 以结合这两种数据源。
联系支持:联系您的客户成功经理 (CSM),为您的应用程序启用混合 SKAN。这可确保来自后台源的订阅事件在 SKAN 转换值中得到正确归属。
SKAN 回传窗口:
- SKAN 4.0:三个回溯窗口(0-2 天、3-7 天、8-35 天)可测量早期订阅事件和短期续订。
- SKAN 3.0:单个 24 小时回传窗口将测量限制在即时订购范围内
- 转换值:在 SKAN 转换值模式中配置订阅事件,以优先处理高价值事件
最佳实践和验证
在生产部署之前,请遵循实施最佳实践并验证订阅跟踪是否正常工作。
实施最佳实践
关键准则
- 无收据验证:在跟踪订阅时,切勿向 Singular 发送平台收据(Google Play、App Store)--在不发送收据的情况下使用 customRevenue()
- 过滤已恢复的购买:只发送 isRestored 为 false 的事件,以避免重复收入报告
- 后台验证:在向 Singular 发送事件之前,始终在安全的后台验证购买行为
- 及时确认:在 3 天内确认 Google Play 购买,防止自动退款
- 恢复查询:在 onResume() 中调用 queryPurchasesAsync() 来捕捉应用程序关闭时发生的续订。
- 一致的事件名称:使用标准的奇异事件名称(sng_subscribe、subscription_renewed)进行统一报告
- 包含元数据:添加订阅层级、计费期和产品 ID 等属性,以便进行详细分析
测试和验证
测试清单
- 测试环境设置:使用 Google Play 和 App Store 的沙盒/测试账户
- 新订阅:验证 sng_subscribe 事件是否以正确的金额和货币正确发送
- 试用流程:测试无收入的试用开始和试用结束事件
- 续订处理:确认续订会触发具有正确收入的 subscription_renewed 事件
- 取消跟踪:验证用户取消订阅时触发的取消事件
- 恢复过滤:确保恢复购买不会发送重复事件
- Singular 控制面板:检查事件是否以正确的归属出现在 Singular 中
- 跨设备:测试跨多个设备的订阅同步
常见问题
- 缺失事件:验证计费客户端连接是否处于活动状态,应用程序恢复时是否调用了 queryPurchasesAsync()
- 重复事件:检查恢复的购买是否通过 isRestored 检查正确过滤
- 错误收入:确保从 ProductDetails 提取的价格正确除以 1,000,000 微米
- 归属问题:确认设备标识符(IDFA/GAID)正确包含在 S2S 请求中
- 确认失败:确认后端收据验证在确认前完成
支持资源:如需故障排除帮助,请联系 Singular 支持,并提供 SDK 版本、平台详情、示例事件有效载荷和显示问题的 Singular 面板截图。