2.0.2: evento de fallo de compra + analytics off en UI tests
premium_purchase_failed con reason (cancelled/pending/unverified/error) en todas las salidas no exitosas de StoreManager.purchase. Hasta ahora un usuario que se echaba atras y uno al que le fallaba la compra eran indistinguibles: los dos dejaban un premium_upgrade_tapped huerfano. El caso de Aleman (6 taps en 2 dias, cero compras) no se puede diagnosticar sin esto. Los UI tests dejan de mandar eventos a GA4 de produccion: launch argument UITEST_DISABLE_ANALYTICS que hace no-op logEvent y apaga la recoleccion de Firebase, para cortar tambien los automaticos (first_open, session_start, screen_view). Ya habian contaminado los datos: los eventos de pasos 5/6 bajo la version 2.0 eran ejecuciones de test, no usuarios. Detalle que costo encontrar: setAnalyticsCollectionEnabled persiste entre lanzamientos, asi que se escribe siempre (!isDisabled) y no solo al apagar. Si no, un unico test dejaba analytics muerto para siempre en esa instalacion. Verificado por pares de lanzamientos: con flag acaba en disabled, sin flag vuelve a enabled. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Ks8uUcMA9mjypVK7F2Pkt
This commit is contained in:
@@ -81,6 +81,7 @@ struct MealMoodApp: App {
|
||||
|
||||
init() {
|
||||
FirebaseApp.configure()
|
||||
AnalyticsService.applyCollectionPolicy()
|
||||
#if canImport(GoogleMobileAds)
|
||||
GADMobileAds.sharedInstance().start(completionHandler: nil)
|
||||
#endif
|
||||
|
||||
@@ -17,6 +17,7 @@ enum AnalyticsEvent {
|
||||
// Premium
|
||||
static let premiumUpgradeTapped = "premium_upgrade_tapped"
|
||||
static let premiumPurchased = "premium_purchased"
|
||||
static let premiumPurchaseFailed = "premium_purchase_failed"
|
||||
static let premiumRestored = "premium_restored"
|
||||
|
||||
// Planner
|
||||
@@ -51,11 +52,33 @@ enum AnalyticsEvent {
|
||||
}
|
||||
|
||||
enum AnalyticsService {
|
||||
/// UI tests drive the app against the production Firebase project, so every
|
||||
/// run would otherwise land in GA4 as a real user: a full onboarding funnel,
|
||||
/// a paywall view, the lot. It already happened — the step 5/6 events
|
||||
/// recorded under 2.0 were test runs, not users.
|
||||
static let launchArgumentDisablingAnalytics = "UITEST_DISABLE_ANALYTICS"
|
||||
|
||||
static let isDisabled: Bool =
|
||||
ProcessInfo.processInfo.arguments.contains(launchArgumentDisablingAnalytics)
|
||||
|
||||
/// Silences Firebase's own automatic events (first_open, session_start,
|
||||
/// screen_view), which no-oping `logEvent` alone would not catch.
|
||||
/// Call once, right after `FirebaseApp.configure()`.
|
||||
///
|
||||
/// Always sets the flag, never just when disabling: Firebase persists this
|
||||
/// across launches, so a single test run would otherwise leave analytics off
|
||||
/// for good on that install.
|
||||
static func applyCollectionPolicy() {
|
||||
Analytics.setAnalyticsCollectionEnabled(!isDisabled)
|
||||
}
|
||||
|
||||
static func logEvent(_ name: String, parameters: [String: Any]? = nil) {
|
||||
guard !isDisabled else { return }
|
||||
Analytics.logEvent(name, parameters: parameters)
|
||||
}
|
||||
|
||||
static func logScreenView(_ screenName: String) {
|
||||
guard !isDisabled else { return }
|
||||
Analytics.logEvent(AnalyticsEventScreenView, parameters: [
|
||||
AnalyticsParameterScreenName: screenName,
|
||||
AnalyticsParameterScreenClass: screenName
|
||||
@@ -99,6 +122,20 @@ enum AnalyticsService {
|
||||
logEvent(AnalyticsEvent.premiumPurchased, parameters: ["environment": environment])
|
||||
}
|
||||
|
||||
/// Why a tapped upgrade never became a purchase. Without this a user who
|
||||
/// changed their mind and a user whose purchase broke look identical — both
|
||||
/// are just a `premium_upgrade_tapped` with nothing after it.
|
||||
///
|
||||
/// `reason` is one of: `cancelled`, `pending`, `unverified`, `error`.
|
||||
static func logPremiumPurchaseFailed(reason: String, productId: String, detail: String? = nil) {
|
||||
var parameters: [String: Any] = ["reason": reason, "product_id": productId]
|
||||
if let detail {
|
||||
// GA4 drops string params over 100 chars.
|
||||
parameters["detail"] = String(detail.prefix(100))
|
||||
}
|
||||
logEvent(AnalyticsEvent.premiumPurchaseFailed, parameters: parameters)
|
||||
}
|
||||
|
||||
static func logPremiumRestored() {
|
||||
logEvent(AnalyticsEvent.premiumRestored)
|
||||
}
|
||||
|
||||
@@ -85,16 +85,25 @@ final class StoreManager: ObservableObject {
|
||||
AnalyticsService.logPremiumPurchased(environment: Self.environmentName(for: transaction))
|
||||
return .success
|
||||
}
|
||||
AnalyticsService.logPremiumPurchaseFailed(reason: "unverified", productId: product.id)
|
||||
return .failed
|
||||
case .userCancelled:
|
||||
AnalyticsService.logPremiumPurchaseFailed(reason: "cancelled", productId: product.id)
|
||||
return .cancelled
|
||||
case .pending:
|
||||
AnalyticsService.logPremiumPurchaseFailed(reason: "pending", productId: product.id)
|
||||
return .pending
|
||||
@unknown default:
|
||||
AnalyticsService.logPremiumPurchaseFailed(reason: "unknown", productId: product.id)
|
||||
return .failed
|
||||
}
|
||||
} catch {
|
||||
CrashlyticsService.record(error, context: "purchase")
|
||||
AnalyticsService.logPremiumPurchaseFailed(
|
||||
reason: "error",
|
||||
productId: product.id,
|
||||
detail: error.localizedDescription
|
||||
)
|
||||
print("Purchase failed: \(error)")
|
||||
return .failed
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@ final class OnboardingFlowUITests: XCTestCase {
|
||||
app = XCUIApplication()
|
||||
// Pin the language so the run doesn't depend on the simulator's locale.
|
||||
app.launchArguments += ["-AppleLanguages", "(en)", "-AppleLocale", "en_US"]
|
||||
// Keep synthetic runs out of the production GA4 property. Literal on
|
||||
// purpose: a UI test bundle doesn't link the app target, so it can't
|
||||
// reference AnalyticsService.launchArgumentDisablingAnalytics — keep the
|
||||
// two in sync by hand.
|
||||
app.launchArguments.append("UITEST_DISABLE_ANALYTICS")
|
||||
}
|
||||
|
||||
func testOnboardingReachesWeekReadyAndPaywallBeforeTheHome() {
|
||||
|
||||
Reference in New Issue
Block a user