Files
FamilyMealPlanner/MealMood/Services/AnalyticsService.swift
T
alexandrev-tibco 148a301d6c 2.0: analytics — log onboarding step as String + step_name for GA4 drop-off
GA4 text custom dimensions do not capture numeric params (step showed as
"(not set)"). Log step as String and add a readable step_name so the
onboarding drop-off is queryable. Registered source/environment/step_name
custom dimensions in the GA4 property to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Df9kZEUMXPLn4Kb9Zx1f5o
2026-07-21 09:19:50 +02:00

200 lines
6.3 KiB
Swift

import FirebaseAnalytics
enum AnalyticsEvent {
// Onboarding
static let onboardingCompleted = "onboarding_completed"
// Dishes
static let dishAdded = "dish_added"
static let dishDeleted = "dish_deleted"
// Planner
static let mealAssigned = "meal_assigned"
static let mealCleared = "meal_cleared"
static let weekReset = "week_reset"
static let weekPlanShared = "week_plan_shared"
// Premium
static let premiumUpgradeTapped = "premium_upgrade_tapped"
static let premiumPurchased = "premium_purchased"
static let premiumRestored = "premium_restored"
// Planner
static let autoAssignUsed = "auto_assign_used"
static let weekCopiedPrevious = "week_copied_previous"
// Shopping list (2.0)
static let shoppingListOpened = "shopping_list_opened"
static let shoppingItemAdded = "shopping_item_added"
static let ingredientsGenerated = "ingredients_generated"
// Onboarding
static let onboardingStepViewed = "onboarding_step_viewed"
// Settings
static let iCloudSyncToggled = "icloud_sync_toggled"
static let notificationsToggled = "notifications_toggled"
static let languageChanged = "language_changed"
// Stats
static let statsViewed = "stats_viewed"
// Feedback loop
static let weekRated = "week_rated"
// Conversion funnel (1.1.5)
static let paywallViewed = "paywall_viewed"
static let paywallDismissed = "paywall_dismissed"
static let dishLimitHit = "dish_limit_hit"
static let weekLimitHit = "week_limit_hit"
static let freeTrialStarted = "free_trial_started"
}
enum AnalyticsService {
static func logEvent(_ name: String, parameters: [String: Any]? = nil) {
Analytics.logEvent(name, parameters: parameters)
}
static func logScreenView(_ screenName: String) {
Analytics.logEvent(AnalyticsEventScreenView, parameters: [
AnalyticsParameterScreenName: screenName,
AnalyticsParameterScreenClass: screenName
])
}
// MARK: - Convenience methods
static func logDishAdded(tagCount: Int, source: String = "editor") {
logEvent(AnalyticsEvent.dishAdded, parameters: [
"tag_count": tagCount,
"source": source
])
}
static func logDishDeleted() {
logEvent(AnalyticsEvent.dishDeleted)
}
static func logMealAssigned() {
logEvent(AnalyticsEvent.mealAssigned)
}
static func logMealCleared() {
logEvent(AnalyticsEvent.mealCleared)
}
static func logWeekReset() {
logEvent(AnalyticsEvent.weekReset)
}
static func logWeekPlanShared(format: String) {
logEvent(AnalyticsEvent.weekPlanShared, parameters: ["format": format])
}
static func logPremiumUpgradeTapped(source: String) {
logEvent(AnalyticsEvent.premiumUpgradeTapped, parameters: ["source": source])
}
static func logPremiumPurchased(environment: String = "unknown") {
logEvent(AnalyticsEvent.premiumPurchased, parameters: ["environment": environment])
}
static func logPremiumRestored() {
logEvent(AnalyticsEvent.premiumRestored)
}
static func logOnboardingCompleted(dishCount: Int) {
logEvent(AnalyticsEvent.onboardingCompleted, parameters: ["dish_count": dishCount])
}
// Human-readable names for each onboarding step, indexed by step number.
// Keep in sync with the OnboardingView step order.
private static let onboardingStepNames = [
"0_welcome", "1_meal_windows", "2_weekends",
"3_calendar", "4_first_dishes", "5_paywall"
]
static func logOnboardingStepViewed(step: Int) {
// GA4 text custom dimensions do not capture numeric params (they show as
// "(not set)"). Log the step as a String so the drop-off is queryable.
let name = step >= 0 && step < onboardingStepNames.count ? onboardingStepNames[step] : "\(step)"
logEvent(AnalyticsEvent.onboardingStepViewed, parameters: [
"step": "\(step)",
"step_name": name
])
}
static func logAutoAssignUsed(filledSlots: Int, unfilledSlots: Int) {
logEvent(AnalyticsEvent.autoAssignUsed, parameters: [
"filled_slots": filledSlots,
"unfilled_slots": unfilledSlots
])
}
static func logWeekCopiedPrevious(copiedSlots: Int, source: String) {
logEvent(AnalyticsEvent.weekCopiedPrevious, parameters: [
"copied_slots": copiedSlots,
"source": source
])
}
static func logShoppingListOpened(itemCount: Int, missingDishes: Int) {
logEvent(AnalyticsEvent.shoppingListOpened, parameters: [
"item_count": itemCount,
"missing_dishes": missingDishes
])
}
static func logShoppingItemAdded() {
logEvent(AnalyticsEvent.shoppingItemAdded)
}
static func logIngredientsGenerated(lineCount: Int, source: String) {
logEvent(AnalyticsEvent.ingredientsGenerated, parameters: [
"line_count": lineCount,
"source": source
])
}
static func logICloudSyncToggled(enabled: Bool) {
logEvent(AnalyticsEvent.iCloudSyncToggled, parameters: ["enabled": enabled])
}
static func logNotificationsToggled(enabled: Bool) {
logEvent(AnalyticsEvent.notificationsToggled, parameters: ["enabled": enabled])
}
static func logStatsViewed() {
logEvent(AnalyticsEvent.statsViewed)
}
static func logWeekRated(rating: Int) {
let value = rating == 1 ? "liked" : rating == -1 ? "disliked" : "cleared"
logEvent(AnalyticsEvent.weekRated, parameters: ["rating": value])
}
static func logPaywallViewed(source: String) {
logEvent(AnalyticsEvent.paywallViewed, parameters: ["source": source])
}
static func logPaywallDismissed(source: String, secondsOnScreen: Int, didConvert: Bool) {
logEvent(AnalyticsEvent.paywallDismissed, parameters: [
"source": source,
"seconds_on_screen": secondsOnScreen,
"did_convert": didConvert
])
}
static func logDishLimitHit() {
logEvent(AnalyticsEvent.dishLimitHit)
}
static func logWeekLimitHit() {
logEvent(AnalyticsEvent.weekLimitHit)
}
static func logFreeTrialStarted(plan: String) {
logEvent(AnalyticsEvent.freeTrialStarted, parameters: ["plan": plan])
}
}