订阅事件技术实施指南

文档

订阅事件跟踪实施指南

使用 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 控制台中设置应用内产品和订阅项目。

  1. 创建订阅产品:在 Play 控制台中定义订阅层级、计费期和定价
  2. 配置优惠:设置基本计划、优惠代币和促销价格
  3. 测试设置:创建测试账户并验证产品可用性

添加计费库依赖关系

更新 build.gradle

将 Google Play Billing Library 8.0.0 添加到应用程序的依赖关系中,并使用 Kotlin 扩展来支持 coroutine。

Kotlin DSL
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 集成以进行事件跟踪。

Kotlin
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()查询现有订阅,以处理续订和在应用程序外购买的订阅。

Kotlin
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()来捕捉应用程序后台或关闭时发生的订阅续订。


查询产品详细信息

在向用户显示之前,检索订阅产品的详细信息,包括定价、账单期和报价令牌。

Kotlin
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 发送适当的事件。

Kotlin
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中设置订阅产品和订阅组。

  1. 创建订阅组:根据访问级别将订阅组织到组中
  2. 定义订阅产品:设置订阅层级、期限和定价
  3. 配置优惠:创建介绍性优惠、促销优惠和订阅代码
  4. 测试环境:为开发设置沙盒测试账户

实施 StoreKit

设置交易观察器

实施SKPaymentTransactionObserver 以接收订阅购买和续订通知。

Swift
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
                ]
            )
        }
    }
}

查询订阅状态

使用收据验证检查当前订阅状态并处理续订。

Swift
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

Kotlin
// 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

Swift
// 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

JavaScript
// Track subscription without receipt
Singular.customRevenueWithArgs(
    "sng_subscribe",  // Event name
    "USD",            // Currency
    9.99,             // Amount
    {                 // Additional attributes
        subscription_id: "premium_monthly",
        billing_period: "monthly"
    }
);

文档:React Native SDK 收入跟踪


Unity SDK

C#
// 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

Dart
// 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

JavaScript
// 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 的事件端点从安全的后台发送带有收入参数的订阅事件。

事件端点

向 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
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 发送订阅事件。

设置步骤

  1. 创建 RevenueCat 账户:在 RevenueCat 控制面板中设置应用程序
  2. 配置 Singular 集成:在 RevenueCat 集成设置中添加 Singular SDK 密钥
  3. 映射事件:配置哪些 RevenueCat 事件应发送至 Singular
  4. 测试集成:验证事件是否出现在 Singular 面板中

文档:RevenueCat Singular 集成


Adapty 集成

Adapty 提供订阅分析,并通过集成向 Singular 发送事件。

设置步骤

  1. 创建 Adapty 账户:在 Adapty 中注册并配置您的应用程序
  2. 启用 Singular 集成:导航至集成并添加 Singular 凭据
  3. 配置事件映射:选择要转发的订阅事件
  4. 验证数据流:确认事件成功到达 Singular

文档:Adapty Singular 集成

注:第三方集成由合作伙伴平台管理。仍应集成 Singular SDK 以跟踪会话和非订阅事件。


订阅事件类型

跟踪各种订阅生命周期事件,以衡量整个订阅过程中的用户参与度、保留率和收入。

标准事件名称

使用 Singular 的标准事件名称实现跨平台的一致报告,或根据您的跟踪要求定义自定义事件名称。

订阅状态 事件名称 SDK 方法 S2S 集成
新订阅 sng_subscribe 带有收入金额(无收据)的 customRevenue() 带收入参数的事件端点
试用开始 sng_start_trial 不含收入的 event() 方法 不含收入的标准事件终点
试用结束 sng_end_trial 事件()方法,不含收益 标准事件终点,无收入
续订 subscription_renewed 自定义收入(),有收入金额(无收据) 带收入参数的事件终端
取消 subscription_cancelled 无收入的 event() 方法 无收入的标准事件终点
退款 subscription_refunded 自定义收入(),带负值(可选)或不带收入的事件() 带负收入的事件终端(可选)或标准事件

重要:通过 customRevenue() 发送订阅事件时,只发送isRestoredfalse 的事件。恢复购买代表现有订阅,而不是新收入。


事件实现示例

新订阅

跟踪用户首次购买新订阅的情况。

Kotlin
// New subscription purchase
Singular.customRevenue(
    "sng_subscribe",
    "USD",
    9.99,
    mapOf(
        "subscription_tier" to "premium",
        "billing_period" to "monthly",
        "product_id" to "premium_monthly"
    )
)

试用开始

跟踪用户开始无收入免费试用期的时间。

Kotlin
// Trial start (no revenue)
Singular.event(
    "sng_start_trial",
    mapOf(
        "trial_duration" to "7_days",
        "subscription_tier" to "premium"
    )
)

续订

跟踪自动续订情况及收入金额。

Kotlin
// Subscription renewal
Singular.customRevenue(
    "subscription_renewed",
    "USD",
    9.99,
    mapOf(
        "subscription_tier" to "premium",
        "renewal_count" to 3,
        "product_id" to "premium_monthly"
    )
)

取消订阅

跟踪用户何时取消订阅(无收入事件)。

Kotlin
// 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 等属性,以便进行详细分析

测试和验证

测试清单

  1. 测试环境设置:使用 Google Play 和 App Store 的沙盒/测试账户
  2. 新订阅:验证 sng_subscribe 事件是否以正确的金额和货币正确发送
  3. 试用流程:测试无收入的试用开始和试用结束事件
  4. 续订处理:确认续订会触发具有正确收入的 subscription_renewed 事件
  5. 取消跟踪:验证用户取消订阅时触发的取消事件
  6. 恢复过滤:确保恢复购买不会发送重复事件
  7. Singular 控制面板:检查事件是否以正确的归属出现在 Singular 中
  8. 跨设备:测试跨多个设备的订阅同步

常见问题

  • 缺失事件:验证计费客户端连接是否处于活动状态,应用程序恢复时是否调用了 queryPurchasesAsync()
  • 重复事件:检查恢复的购买是否通过 isRestored 检查正确过滤
  • 错误收入:确保从 ProductDetails 提取的价格正确除以 1,000,000 微米
  • 归属问题:确认设备标识符(IDFA/GAID)正确包含在 S2S 请求中
  • 确认失败:确认后端收据验证在确认前完成

支持资源:如需故障排除帮助,请联系 Singular 支持,并提供 SDK 版本、平台详情、示例事件有效载荷和显示问题的 Singular 面板截图。