订阅事件追踪实施指南

订阅事件追踪实施指南

通过 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 中设置应用内商品和订阅项目。

  1. 创建订阅商品: 在 Play Console 中定义订阅层级、结算周期和定价
  2. 配置优惠: 设置基础套餐、优惠令牌和促销定价
  3. 测试设置: 创建测试账户并验证商品可用性

添加 Billing Library 依赖项

更新 build.gradle

将 Google Play Billing Library 8.0.0 添加到应用的依赖项中,并附带用于协程支持的 Kotlin 扩展。

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 以及增强的待处理交易处理功能。


初始化 BillingClient

创建结算管理器

设置一个结算管理器类来处理所有订阅操作,并与 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}")
        }
    }
}

关键提示: 在追踪订阅事件时,请勿将 Google Play 收据发送给 Singular。使用 customRevenue() 方法且不进行收据验证。请在 3 天内确认购买,否则购买将被自动退款。


iOS:StoreKit 集成

集成 Apple 的 StoreKit 框架,以管理订阅并向 Singular 发送 iOS 应用的事件。

前提条件

配置 App Store Connect

在应用中实施 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 方法来追踪订阅事件。

Android 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 Native 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 的 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
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 集成


Superwall 集成

连接 Singular 以接收 Superwall 的订阅生命周期和收入事件,并借助 SDID 支持实现更强的归因。

设置步骤:

  1. 集成 Singular SDK: 将 Singular SDK 添加到您的应用,以进行会话和事件追踪
  2. 设置 Singular SDID(可选): 设置 Singular SDID,并在应用中使用该 SDID 更新 Superwall 用户属性,以实现更强的归因
  3. 在 Superwall 中添加 Singular 集成: 在您的 Superwall 控制面板中启用并配置 Singular 集成

文档: Superwall Singular 集成


Xsolla 集成

Xsolla 将 Web Shop 中的购买信息作为移动应用内事件发送给 Singular,随后 Singular 将这些事件归因到移动应用安装、用户获取和再互动广告系列。

设置步骤:

  1. 在您的移动应用中集成 Singular: 将 Singular SDK 添加到您的移动应用中
  2. 配置应用内事件: 向 Singular 发送应用内事件,例如登录,或任何其他包含 CUID 的事件
  3. 配置 Singular 合作伙伴配置: 为 Xsolla 设置 Singular 合作伙伴配置
  4. 在您的 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 的事件。已恢复的购买代表现有订阅,而非新收入。


事件实施示例

新订阅

追踪用户首次购买新订阅的时间。

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

测试与验证

测试检查清单

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

常见问题

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

支持资源: 如需故障排查协助,请联系 Singular Support,并提供 SDK 版本、平台详情、示例事件负载以及显示问题的 Singular 控制面板截图。