Replace EventLogger with typed AnalyticsManager using PostHog
Complete analytics overhaul: delete EventLogger.swift, create Analytics.swift with typed event enum (~45 events), screen tracking, super properties (theme, icon pack, voting layout, etc.), session replay with kill switch, autocapture, and network telemetry. Replace all 99 call sites across 38 files with compiler-enforced typed events in object_action naming convention. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,7 @@ struct SettingsContentView: View {
|
||||
legalSectionHeader
|
||||
eulaButton
|
||||
privacyButton
|
||||
analyticsToggle
|
||||
|
||||
addTestDataButton
|
||||
|
||||
@@ -108,7 +109,7 @@ struct SettingsContentView: View {
|
||||
ReminderTimePickerView()
|
||||
}
|
||||
.onAppear(perform: {
|
||||
EventLogger.log(event: "show_settings_view")
|
||||
AnalyticsManager.shared.trackScreen(.settings)
|
||||
FeelsTipsManager.shared.onSettingsViewed()
|
||||
})
|
||||
}
|
||||
@@ -119,7 +120,7 @@ struct SettingsContentView: View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
EventLogger.log(event: "tap_reminder_time")
|
||||
AnalyticsManager.shared.track(.reminderTimeTapped)
|
||||
showReminderTimePicker = true
|
||||
}, label: {
|
||||
HStack(spacing: 12) {
|
||||
@@ -832,7 +833,7 @@ struct SettingsContentView: View {
|
||||
if newValue {
|
||||
let success = await authManager.enableLock()
|
||||
if !success {
|
||||
EventLogger.log(event: "privacy_lock_enable_failed")
|
||||
AnalyticsManager.shared.track(.privacyLockEnableFailed)
|
||||
}
|
||||
} else {
|
||||
authManager.disableLock()
|
||||
@@ -927,16 +928,16 @@ struct SettingsContentView: View {
|
||||
// Sync all existing moods to HealthKit
|
||||
await HealthKitManager.shared.syncAllMoods()
|
||||
} else {
|
||||
EventLogger.log(event: "healthkit_state_of_mind_not_authorized")
|
||||
AnalyticsManager.shared.track(.healthKitNotAuthorized)
|
||||
}
|
||||
} catch {
|
||||
print("HealthKit authorization failed: \(error)")
|
||||
EventLogger.log(event: "healthkit_enable_failed")
|
||||
AnalyticsManager.shared.track(.healthKitEnableFailed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
healthService.isEnabled = false
|
||||
EventLogger.log(event: "healthkit_disabled")
|
||||
AnalyticsManager.shared.track(.healthKitDisabled)
|
||||
}
|
||||
}
|
||||
))
|
||||
@@ -988,7 +989,7 @@ struct SettingsContentView: View {
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
.healthKitSyncTip()
|
||||
.sheet(isPresented: $showSubscriptionStore) {
|
||||
FeelsSubscriptionStoreView()
|
||||
FeelsSubscriptionStoreView(source: "settings")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -998,7 +999,7 @@ struct SettingsContentView: View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
EventLogger.log(event: "tap_export_data")
|
||||
AnalyticsManager.shared.track(.exportTapped)
|
||||
showExportView = true
|
||||
}, label: {
|
||||
HStack(spacing: 12) {
|
||||
@@ -1035,7 +1036,7 @@ struct SettingsContentView: View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
EventLogger.log(event: "tap_show_onboarding")
|
||||
AnalyticsManager.shared.track(.onboardingReshown)
|
||||
showOnboarding.toggle()
|
||||
}, label: {
|
||||
Text(String(localized: "settings_view_show_onboarding"))
|
||||
@@ -1055,7 +1056,7 @@ struct SettingsContentView: View {
|
||||
Toggle(String(localized: "settings_use_delete_enable"),
|
||||
isOn: $deleteEnabled)
|
||||
.onChange(of: deleteEnabled) { _, newValue in
|
||||
EventLogger.log(event: "toggle_can_delete", withData: ["value": newValue])
|
||||
AnalyticsManager.shared.track(.deleteToggleChanged(enabled: newValue))
|
||||
}
|
||||
.foregroundColor(textColor)
|
||||
.accessibilityHint(String(localized: "Allow deleting mood entries by swiping"))
|
||||
@@ -1070,7 +1071,7 @@ struct SettingsContentView: View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
EventLogger.log(event: "show_eula")
|
||||
AnalyticsManager.shared.track(.eulaViewed)
|
||||
if let url = URL(string: "https://feels.app/eula.html") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
@@ -1089,7 +1090,7 @@ struct SettingsContentView: View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
EventLogger.log(event: "show_privacy")
|
||||
AnalyticsManager.shared.track(.privacyPolicyViewed)
|
||||
if let url = URL(string: "https://feels.app/privacy.html") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
@@ -1104,6 +1105,48 @@ struct SettingsContentView: View {
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
// MARK: - Analytics Toggle
|
||||
|
||||
private var analyticsToggle: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "chart.bar.xaxis")
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
.frame(width: 32)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Share Analytics")
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text("Help improve Feels by sharing anonymous usage data")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Toggle("", isOn: Binding(
|
||||
get: { !AnalyticsManager.shared.isOptedOut },
|
||||
set: { enabled in
|
||||
if enabled {
|
||||
AnalyticsManager.shared.optIn()
|
||||
} else {
|
||||
AnalyticsManager.shared.optOut()
|
||||
}
|
||||
}
|
||||
))
|
||||
.labelsHidden()
|
||||
.accessibilityLabel("Share Analytics")
|
||||
.accessibilityHint("Toggle anonymous usage analytics")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
// MARK: - Helper Functions
|
||||
|
||||
private func openInFilesApp(_ url: URL) {
|
||||
@@ -1175,7 +1218,7 @@ struct ReminderTimePickerView: View {
|
||||
// This handles notification scheduling and Live Activity rescheduling
|
||||
OnboardingDataDataManager.shared.updateOnboardingData(onboardingData: onboardingData)
|
||||
|
||||
EventLogger.log(event: "reminder_time_updated")
|
||||
AnalyticsManager.shared.track(.reminderTimeUpdated)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1231,6 +1274,7 @@ struct SettingsView: View {
|
||||
legalSectionHeader
|
||||
eulaButton
|
||||
privacyButton
|
||||
analyticsToggle
|
||||
// specialThanksCell
|
||||
}
|
||||
|
||||
@@ -1268,7 +1312,7 @@ struct SettingsView: View {
|
||||
))
|
||||
}
|
||||
.onAppear(perform: {
|
||||
EventLogger.log(event: "show_settings_view")
|
||||
AnalyticsManager.shared.trackScreen(.settings)
|
||||
})
|
||||
.background(
|
||||
theme.currentTheme.bg
|
||||
@@ -1282,7 +1326,7 @@ struct SettingsView: View {
|
||||
onCompletion: { result in
|
||||
switch result {
|
||||
case .success(let url):
|
||||
EventLogger.log(event: "exported_file")
|
||||
AnalyticsManager.shared.track(.dataExported(format: "file", count: 0))
|
||||
print("Saved to \(url)")
|
||||
case .failure(let error):
|
||||
print(error.localizedDescription)
|
||||
@@ -1319,13 +1363,13 @@ struct SettingsView: View {
|
||||
DataController.shared.add(mood: mood, forDate: forDate, entryType: entryType)
|
||||
}
|
||||
DataController.shared.saveAndRunDataListeners()
|
||||
EventLogger.log(event: "import_file")
|
||||
AnalyticsManager.shared.track(.importSucceeded)
|
||||
} else {
|
||||
EventLogger.log(event: "error_import_file")
|
||||
AnalyticsManager.shared.track(.importFailed(error: nil))
|
||||
}
|
||||
} catch {
|
||||
// Handle failure.
|
||||
EventLogger.log(event: "error_import_file", withData: ["error": error.localizedDescription])
|
||||
AnalyticsManager.shared.track(.importFailed(error: error.localizedDescription))
|
||||
print("Unable to read file contents")
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
@@ -1402,7 +1446,7 @@ struct SettingsView: View {
|
||||
if newValue {
|
||||
let success = await authManager.enableLock()
|
||||
if !success {
|
||||
EventLogger.log(event: "privacy_lock_enable_failed")
|
||||
AnalyticsManager.shared.track(.privacyLockEnableFailed)
|
||||
}
|
||||
} else {
|
||||
authManager.disableLock()
|
||||
@@ -1465,16 +1509,16 @@ struct SettingsView: View {
|
||||
// Sync all existing moods to HealthKit
|
||||
await HealthKitManager.shared.syncAllMoods()
|
||||
} else {
|
||||
EventLogger.log(event: "healthkit_state_of_mind_not_authorized")
|
||||
AnalyticsManager.shared.track(.healthKitNotAuthorized)
|
||||
}
|
||||
} catch {
|
||||
print("HealthKit authorization failed: \(error)")
|
||||
EventLogger.log(event: "healthkit_enable_failed")
|
||||
AnalyticsManager.shared.track(.healthKitEnableFailed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
healthService.isEnabled = false
|
||||
EventLogger.log(event: "healthkit_disabled")
|
||||
AnalyticsManager.shared.track(.healthKitDisabled)
|
||||
}
|
||||
}
|
||||
))
|
||||
@@ -1518,7 +1562,7 @@ struct SettingsView: View {
|
||||
.background(theme.currentTheme.secondaryBGColor)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
.sheet(isPresented: $showSubscriptionStore) {
|
||||
FeelsSubscriptionStoreView()
|
||||
FeelsSubscriptionStoreView(source: "settings")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1528,7 +1572,7 @@ struct SettingsView: View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
EventLogger.log(event: "tap_export_data")
|
||||
AnalyticsManager.shared.track(.exportTapped)
|
||||
showExportView = true
|
||||
}, label: {
|
||||
HStack(spacing: 12) {
|
||||
@@ -1558,12 +1602,12 @@ struct SettingsView: View {
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
|
||||
private var closeButtonView: some View {
|
||||
HStack{
|
||||
Spacer()
|
||||
Button(action: {
|
||||
EventLogger.log(event: "tap_settings_close")
|
||||
AnalyticsManager.shared.track(.settingsClosed)
|
||||
dismiss()
|
||||
}, label: {
|
||||
Text(String(localized: "settings_view_exit"))
|
||||
@@ -1578,7 +1622,7 @@ struct SettingsView: View {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
VStack {
|
||||
Button(action: {
|
||||
EventLogger.log(event: "tap_show_special_thanks")
|
||||
AnalyticsManager.shared.track(.specialThanksViewed)
|
||||
withAnimation{
|
||||
showSpecialThanks.toggle()
|
||||
}
|
||||
@@ -1753,7 +1797,7 @@ struct SettingsView: View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
EventLogger.log(event: "tap_show_onboarding")
|
||||
AnalyticsManager.shared.track(.onboardingReshown)
|
||||
showOnboarding.toggle()
|
||||
}, label: {
|
||||
Text(String(localized: "settings_view_show_onboarding"))
|
||||
@@ -1769,7 +1813,7 @@ struct SettingsView: View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
EventLogger.log(event: "show_eula")
|
||||
AnalyticsManager.shared.track(.eulaViewed)
|
||||
openURL(URL(string: "https://feels.app/eula.html")!)
|
||||
}, label: {
|
||||
Text(String(localized: "settings_view_show_eula"))
|
||||
@@ -1785,7 +1829,7 @@ struct SettingsView: View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
EventLogger.log(event: "show_privacy")
|
||||
AnalyticsManager.shared.track(.privacyPolicyViewed)
|
||||
openURL(URL(string: "https://feels.app/privacy.html")!)
|
||||
}, label: {
|
||||
Text(String(localized: "settings_view_show_privacy"))
|
||||
@@ -1796,7 +1840,47 @@ struct SettingsView: View {
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
|
||||
private var analyticsToggle: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "chart.bar.xaxis")
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
.frame(width: 32)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Share Analytics")
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Text("Help improve Feels by sharing anonymous usage data")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Toggle("", isOn: Binding(
|
||||
get: { !AnalyticsManager.shared.isOptedOut },
|
||||
set: { enabled in
|
||||
if enabled {
|
||||
AnalyticsManager.shared.optIn()
|
||||
} else {
|
||||
AnalyticsManager.shared.optOut()
|
||||
}
|
||||
}
|
||||
))
|
||||
.labelsHidden()
|
||||
.accessibilityLabel("Share Analytics")
|
||||
.accessibilityHint("Toggle anonymous usage analytics")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
|
||||
private var canDelete: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
@@ -1804,7 +1888,7 @@ struct SettingsView: View {
|
||||
Toggle(String(localized: "settings_use_delete_enable"),
|
||||
isOn: $deleteEnabled)
|
||||
.onChange(of: deleteEnabled) { _, newValue in
|
||||
EventLogger.log(event: "toggle_can_delete", withData: ["value": newValue])
|
||||
AnalyticsManager.shared.track(.deleteToggleChanged(enabled: newValue))
|
||||
}
|
||||
.foregroundColor(textColor)
|
||||
.padding()
|
||||
@@ -1819,7 +1903,7 @@ struct SettingsView: View {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
showingExporter.toggle()
|
||||
EventLogger.log(event: "export_data", withData: ["title": "default"])
|
||||
AnalyticsManager.shared.track(.exportTapped)
|
||||
}, label: {
|
||||
Text("Export")
|
||||
.foregroundColor(textColor)
|
||||
@@ -1835,7 +1919,7 @@ struct SettingsView: View {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
Button(action: {
|
||||
showingImporter.toggle()
|
||||
EventLogger.log(event: "import_data", withData: ["title": "default"])
|
||||
AnalyticsManager.shared.track(.importTapped)
|
||||
}, label: {
|
||||
Text("Import")
|
||||
.foregroundColor(textColor)
|
||||
|
||||
Reference in New Issue
Block a user