From 9cadc1ad1854191087a67085a5c5a000c6adf83c Mon Sep 17 00:00:00 2001 From: alexandrev-tibco Date: Thu, 2 Jul 2026 13:26:45 +0200 Subject: [PATCH] 1.1.5: fix activation analytics blind spots + purchase environment tagging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dish_added now carries a `source` param; onboarding-created dishes fire it with source="onboarding" (previously untracked, undercounting activation) - onboarding_completed now reports dish_count - premium_purchased tagged with StoreKit environment (production/sandbox/xcode) so test purchases can be excluded from conversion metrics - add .gitignore to keep signing secrets (.p12/.cer/.mobileprovision) and build artifacts out of the repo - document branch↔MARKETING_VERSION naming convention Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01UtWT52pAE91D7mbDda1cAM --- .gitignore | 22 +++++++++++++++++++ CLAUDE.md | 10 ++++++++- MealMood/Services/AnalyticsService.swift | 15 ++++++++----- MealMood/Services/StoreManager.swift | 13 ++++++++++- MealMood/ViewModels/OnboardingViewModel.swift | 8 ++++++- 5 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c477b95 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Signing secrets — NEVER commit +*.p12 +*.p8 +*.cer +*.certSigningRequest +*.mobileprovision + +# Build artifacts +*.ipa +*.dSYM.zip +/build/ +DerivedData/ + +# Fastlane generated +fastlane/report.xml +fastlane/Preview.html +fastlane/test_output/ + +# macOS / Xcode +.DS_Store +*.xcuserstate +xcuserdata/ diff --git a/CLAUDE.md b/CLAUDE.md index 8c38e5f..72197a6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,9 +4,17 @@ - **Name**: MealMood: Family Meal Planner - **Bundle ID**: com.alexandrevazquez.mealmood - **Platform**: iOS/iPadOS (also runs as iOS app on Mac) -- **Current version**: 1.1.0 +- **Current version**: 1.1.5 - **Main branch**: `1.0.1` (production), feature branches named after versions +### Branch ↔ version convention (IMPORTANT) +The active dev branch is named after the **current** `MARKETING_VERSION`, not the one it was created with. Whenever the iOS app version changes, **rename the branch to match**: +```bash +# after bumping MARKETING_VERSION in the Xcode project to e.g. 1.1.6 +git branch -m 1.1.6 # e.g. git branch -m 1.1.5 1.1.6 +``` +Right now: `MARKETING_VERSION = 1.1.5` → active branch is `1.1.5`. Keep them in sync. (The build number bump within the same version does NOT rename the branch — only a version change does.) + ## Credentials — all managed via `pass` (GPG-encrypted, syncs to Gitea) Never hardcode credentials. Always read from `pass`: diff --git a/MealMood/Services/AnalyticsService.swift b/MealMood/Services/AnalyticsService.swift index aeb9f17..7aba1f5 100644 --- a/MealMood/Services/AnalyticsService.swift +++ b/MealMood/Services/AnalyticsService.swift @@ -58,8 +58,11 @@ enum AnalyticsService { // MARK: - Convenience methods - static func logDishAdded(tagCount: Int) { - logEvent(AnalyticsEvent.dishAdded, parameters: ["tag_count": tagCount]) + static func logDishAdded(tagCount: Int, source: String = "editor") { + logEvent(AnalyticsEvent.dishAdded, parameters: [ + "tag_count": tagCount, + "source": source + ]) } static func logDishDeleted() { @@ -86,16 +89,16 @@ enum AnalyticsService { logEvent(AnalyticsEvent.premiumUpgradeTapped, parameters: ["source": source]) } - static func logPremiumPurchased() { - logEvent(AnalyticsEvent.premiumPurchased) + static func logPremiumPurchased(environment: String = "unknown") { + logEvent(AnalyticsEvent.premiumPurchased, parameters: ["environment": environment]) } static func logPremiumRestored() { logEvent(AnalyticsEvent.premiumRestored) } - static func logOnboardingCompleted() { - logEvent(AnalyticsEvent.onboardingCompleted) + static func logOnboardingCompleted(dishCount: Int) { + logEvent(AnalyticsEvent.onboardingCompleted, parameters: ["dish_count": dishCount]) } static func logOnboardingStepViewed(step: Int) { diff --git a/MealMood/Services/StoreManager.swift b/MealMood/Services/StoreManager.swift index d4fff77..7b53dd6 100644 --- a/MealMood/Services/StoreManager.swift +++ b/MealMood/Services/StoreManager.swift @@ -81,7 +81,7 @@ final class StoreManager: ObservableObject { if case .verified(let transaction) = verification { await transaction.finish() isPremium = true - AnalyticsService.logPremiumPurchased() + AnalyticsService.logPremiumPurchased(environment: Self.environmentName(for: transaction)) return .success } return .failed @@ -181,6 +181,17 @@ final class StoreManager: ObservableObject { return false } + /// Maps a transaction's StoreKit environment to an analytics-friendly string + /// so sandbox/Xcode test purchases can be excluded from real conversion metrics. + static func environmentName(for transaction: StoreKit.Transaction) -> String { + switch transaction.environment { + case .production: return "production" + case .sandbox: return "sandbox" + case .xcode: return "xcode" + default: return "unknown" + } + } + private struct TimeoutError: Error {} private func loadProductsWithRetries(maxAttempts: Int = 3) async throws -> [Product] { diff --git a/MealMood/ViewModels/OnboardingViewModel.swift b/MealMood/ViewModels/OnboardingViewModel.swift index bfbabb5..eb6c24b 100644 --- a/MealMood/ViewModels/OnboardingViewModel.swift +++ b/MealMood/ViewModels/OnboardingViewModel.swift @@ -147,6 +147,7 @@ final class OnboardingViewModel: ObservableObject { context.insert(dish) } let createdDishes = !addedDishes.isEmpty + let onboardingDishCount = addedDishes.count // Create first week plan let weekStart = Date().startOfWeek() @@ -154,7 +155,12 @@ final class OnboardingViewModel: ObservableObject { do { try context.save() - AnalyticsService.logOnboardingCompleted() + // Dishes created inside onboarding didn't previously fire dish_added, + // so activation looked lower than it was. Emit one per dish, tagged. + for dishData in addedDishes { + AnalyticsService.logDishAdded(tagCount: dishData.tagIds.count, source: "onboarding") + } + AnalyticsService.logOnboardingCompleted(dishCount: onboardingDishCount) // If the user added dishes, auto-fill their first week on launch so they // land on a ready plan instead of an empty one. Otherwise fall back to the // home auto-assign prompt.