From 12308a7e428c3ae239b15a1f2c25971749726d02 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Fri, 29 Nov 2024 12:08:25 +0100 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20(Analyt?= =?UTF-8?q?ics):=20Check=20events,=20parameters=20lenght,=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Analytics/AnalyticsManager.swift | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/Modules/AnalyticsKit/Sources/Analytics/AnalyticsManager.swift b/Modules/AnalyticsKit/Sources/Analytics/AnalyticsManager.swift index fbac48f79..2cfafe65a 100644 --- a/Modules/AnalyticsKit/Sources/Analytics/AnalyticsManager.swift +++ b/Modules/AnalyticsKit/Sources/Analytics/AnalyticsManager.swift @@ -4,6 +4,8 @@ import Combine import FirebaseAnalytics +import Foundation +import Logging public class AnalyticsManager { // MARK: Lifecycle @@ -89,11 +91,99 @@ public class AnalyticsManager { } } + struct AnalyticsError { + // MARK: Lifecycle + + init(domain: Domain, code: Int, message: String) { + self.domain = domain + self.code = code + self.message = "\(message)" + } + + // MARK: Internal + + enum Domain: String { + case event + case userProperty + } + + let message: Logger.Message + let domain: Domain + let code: Int + + var error: NSError { + NSError( + domain: "app.leka.error.analytics.\(self.domain)", + code: self.code, + userInfo: [NSLocalizedDescriptionKey: self.message.description] + ) + } + } + static func logEvent(_ event: Event, parameters: [String: Any] = [:]) { + if event.name.isEmpty { + let error = AnalyticsError(domain: .event, code: 0, message: "Event name is empty: \(event)") + log.error(error.message) + CrashlyticsManager.recordError(error.error) + } + + if event.name.count > 40 { + let error = AnalyticsError(domain: .event, code: 1, message: "Event name is too long: \(event) - (\(event.name.count) characters)") + log.error(error.message) + CrashlyticsManager.recordError(error.error) + } + + let kAuthorizedKeys: [String] = ["screen_class", "screen_name", "content_type", "item_id"] + + for (key, value) in parameters { + if key.isEmpty { + let error = AnalyticsError(domain: .event, code: 2, message: "Event parameter key is empty: \(event)") + log.error(error.message) + CrashlyticsManager.recordError(error.error) + } + + if key.count > 40 { + let error = AnalyticsError(domain: .event, code: 3, message: "Event parameter key too long: \(event) - \(key) - (\(key.count) characters)") + log.error(error.message) + CrashlyticsManager.recordError(error.error) + } + + if "\(value)".count > 100 { + let error = AnalyticsError(domain: .event, code: 4, message: "Event parameter value too long: \(event) - \(key) - \(value) - (\("\(value)".count) characters)") + log.error(error.message) + CrashlyticsManager.recordError(error.error) + } + + if !key.starts(with: "lk_"), !kAuthorizedKeys.contains(key) { + let error = AnalyticsError(domain: .event, code: 5, message: "Event parameter key missing prefix 'lk_': \(event) - \(key)") + log.error(error.message) + CrashlyticsManager.recordError(error.error) + CrashlyticsManager.recordError(error.error) + } + } + Analytics.logEvent(event.name, parameters: parameters) } static func setUserProperty(value: String, forName name: String) { + if name.isEmpty { + let error = AnalyticsError(domain: .userProperty, code: 0, message: "User property name is empty") + log.error(error.message) + CrashlyticsManager.recordError(error.error) + } + + if name.count > 24 { + let error = AnalyticsError(domain: .userProperty, code: 1, message: "User property name is too long: \(name) - (\(name.count) characters)") + log.error(error.message) + CrashlyticsManager.recordError(error.error) + } + + if value.count > 36 { + let error = AnalyticsError(domain: .userProperty, code: 2, message: "User property value is too long: \(name) - \(value) - (\(value.count) characters)") + log.error(error.message) + CrashlyticsManager.recordError(error.error) + } + Analytics.setUserProperty(value, forName: name) } From 814a7161dfd9907329b6ae4f7a1b9c99f051e96e Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Fri, 29 Nov 2024 13:13:24 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=93=88=20(Analytics):=20Set=20crashly?= =?UTF-8?q?tics=20user=20id=20as=20well?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Analytics/AnalyticsManager+UserProperties.swift | 1 + .../AnalyticsKit/Sources/Crashlytics/CrashlyticsManager.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/AnalyticsKit/Sources/Analytics/AnalyticsManager+UserProperties.swift b/Modules/AnalyticsKit/Sources/Analytics/AnalyticsManager+UserProperties.swift index 368d2de37..5c518b306 100644 --- a/Modules/AnalyticsKit/Sources/Analytics/AnalyticsManager+UserProperties.swift +++ b/Modules/AnalyticsKit/Sources/Analytics/AnalyticsManager+UserProperties.swift @@ -7,6 +7,7 @@ import FirebaseAnalytics public extension AnalyticsManager { static func setUserID(_ userID: String?) { Analytics.setUserID(userID) + CrashlyticsManager.setUserID(userID) } static func setUserPropertyUserIsLoggedIn(value: Bool) { diff --git a/Modules/AnalyticsKit/Sources/Crashlytics/CrashlyticsManager.swift b/Modules/AnalyticsKit/Sources/Crashlytics/CrashlyticsManager.swift index 76d167032..e0a71e2b9 100644 --- a/Modules/AnalyticsKit/Sources/Crashlytics/CrashlyticsManager.swift +++ b/Modules/AnalyticsKit/Sources/Crashlytics/CrashlyticsManager.swift @@ -13,7 +13,7 @@ public class CrashlyticsManager { Crashlytics.crashlytics().setCustomValue(value, forKey: key) } - public static func setUserID(_ userID: String) { + public static func setUserID(_ userID: String?) { Crashlytics.crashlytics().setUserID(userID) } From b135c35d726e5f6c2197c8b1a8106a4281a59855 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Wed, 27 Nov 2024 12:04:12 +0100 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=93=88=20(Analytics):=20Improve=20log?= =?UTF-8?q?=20event=20screen=5Fview=20with=20context?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AnalyticsManager+ScreenView.swift | 88 +++++++++++++++++-- 1 file changed, 83 insertions(+), 5 deletions(-) diff --git a/Modules/AnalyticsKit/Sources/Analytics/AnalyticsManager+ScreenView.swift b/Modules/AnalyticsKit/Sources/Analytics/AnalyticsManager+ScreenView.swift index 288328614..73459c987 100644 --- a/Modules/AnalyticsKit/Sources/Analytics/AnalyticsManager+ScreenView.swift +++ b/Modules/AnalyticsKit/Sources/Analytics/AnalyticsManager+ScreenView.swift @@ -17,14 +17,92 @@ public extension AnalyticsManager { logEvent(.screenView, parameters: params) } + + enum ScreenViewContext { + case splitView + case sheet + case fullScreenCover + case context(String) + case none + case additionalInfo(String) + + // MARK: Internal + + var description: String { + switch self { + case .splitView: + "main_splitview" + case .sheet: + "main_sheet" + case .fullScreenCover: + "main_fullscreen" + case let .context(value): + "\(value)" + case .none: + "none" + case let .additionalInfo(value): + "\(value)" + } + } + + static func + (lhs: Self, rhs: Self) -> ScreenViewContext { + .additionalInfo("\(lhs.description)-\(rhs.description)") + } + } +} + +// MARK: - AnalyticsLogScreenViewViewModifier + +struct AnalyticsLogScreenViewViewModifier: ViewModifier { + // MARK: Lifecycle + + init( + screenName: String, + screenClass: String, + context: AnalyticsManager.ScreenViewContext? = nil, + parameters: [String: Any] = [:] + ) { + self.screenName = screenName + self.screenClass = screenClass + self.context = context + self.parameters = parameters + } + + // MARK: Internal + + let screenName: String + let screenClass: String + let context: AnalyticsManager.ScreenViewContext? + let parameters: [String: Any] + + func body(content: Content) -> some View { + content + .onAppear { + let screenName = "\(screenName)" + let params: [String: Any] = [ + "lk_context": context?.description ?? NSNull(), + ].merging(self.parameters) { _, new in new } + + AnalyticsManager.logEventScreenView(screenName: screenName, screenClass: self.screenClass, parameters: self.parameters) + } + } } public extension View { - func logEventScreenView(screenName: String, screenClass: String? = nil, parameters: [String: Any] = [:]) -> some View { - let screenClass = screenClass ?? String(describing: type(of: self)) - return self.onAppear { - AnalyticsManager.logEventScreenView(screenName: screenName, screenClass: screenClass, parameters: parameters) - } + func logEventScreenView( + screenName: String, + context: AnalyticsManager.ScreenViewContext?, + screenClass: String? = nil, + parameters: [String: Any] = [:] + ) -> some View { + self.modifier( + AnalyticsLogScreenViewViewModifier( + screenName: screenName, + screenClass: screenClass ?? String(describing: type(of: self)), + context: context, + parameters: parameters + ) + ) } } From 968d9211b83dc39375774801dc22821a142794a8 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Wed, 27 Nov 2024 17:34:25 +0100 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=93=88=20(LekaApp):=20Apply=20logEven?= =?UTF-8?q?tScreenView,=20refactor=20selectContent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AccountCreationProcess+Step2.swift | 1 + .../Sources/Views/MainView/MainView.swift | 101 +++++------------- .../Users/CareReceiver/CarereceiverList.swift | 1 + .../Users/CareReceiver/CarereceiverView.swift | 1 + .../CareReceiver/CreateCarereceiverView.swift | 3 - .../Views/Users/Caregiver/CaregiverList.swift | 1 + .../Users/Caregiver/CaregiverPicker.swift | 2 +- .../Authentication/AuthManagerViewModel.swift | 1 - .../RootAccounts/RootAccountManager.swift | 1 - .../AnalyticsManager+ScreenView.swift | 31 ++---- .../Sources/CurriculumListView.swift | 13 ++- .../Sources/Views/ActivityDetailsView.swift | 4 - .../Sources/Views/ActivityGridView.swift | 23 ++-- .../Views/ActivityHorizontalListView.swift | 16 +++ .../Sources/Views/ActivityListView.swift | 24 +++-- .../Sources/Views/CurriculumDetailsView.swift | 4 - .../Sources/Views/CurriculumGridView.swift | 29 ++--- .../Views/CurriculumHorizontalListView.swift | 29 ++--- .../Sources/Views/GamepadGridView.swift | 25 +++-- .../Views/LibraryActivityListView.swift | 23 ++-- .../Sources/Views/StoryDetailsView.swift | 4 - .../Sources/Views/StoryGridView.swift | 23 ++-- .../Sources/Views/StoryListView.swift | 23 ++-- .../Sources/OldSystem/Stories/StoryView.swift | 7 ++ .../Views/Activity/ActivityView.swift | 7 ++ 25 files changed, 210 insertions(+), 187 deletions(-) diff --git a/Apps/LekaApp/Sources/Views/AccountCreation/Process/AccountCreationProcess+Step2.swift b/Apps/LekaApp/Sources/Views/AccountCreation/Process/AccountCreationProcess+Step2.swift index e9465bd52..132f0bdaa 100644 --- a/Apps/LekaApp/Sources/Views/AccountCreation/Process/AccountCreationProcess+Step2.swift +++ b/Apps/LekaApp/Sources/Views/AccountCreation/Process/AccountCreationProcess+Step2.swift @@ -46,6 +46,7 @@ extension AccountCreationProcess { self.selectedTab = .carereceiverCreation } }) + .logEventScreenView(screenName: "caregiver_create", context: .context("account_creation_sheet")) .navigationBarTitleDisplayMode(.inline) .interactiveDismissDisabled() } diff --git a/Apps/LekaApp/Sources/Views/MainView/MainView.swift b/Apps/LekaApp/Sources/Views/MainView/MainView.swift index 67d42dc0b..341c85542 100644 --- a/Apps/LekaApp/Sources/Views/MainView/MainView.swift +++ b/Apps/LekaApp/Sources/Views/MainView/MainView.swift @@ -157,69 +157,47 @@ struct MainView: View { switch self.navigation.selectedCategory { case .home: CategoryHome() - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_category_home") - } + .logEventScreenView(screenName: "home", context: .splitView) case .search: CategorySearchView() - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_category_search") - } + .logEventScreenView(screenName: "search", context: .splitView) case .resourcesFirstSteps: CategoryResourcesFirstStepsView() - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_category_resources_first_steps") - } + .logEventScreenView(screenName: "resources_first_steps", context: .splitView) case .resourcesVideo: CategoryResourcesVideosView() - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_category_resources_video") - } + .logEventScreenView(screenName: "resources_video", context: .splitView) case .resourcesDeepDive: CategoryResourcesDeepDiveView() - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_category_resources_deep_dive") - } + .logEventScreenView(screenName: "resources_deep_dive", context: .splitView) case .curriculums: CategoryCurriculumsView() - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_category_curriculums") - } + .logEventScreenView(screenName: "curriculums", context: .splitView) case .educationalGames: CategoryEducationalGamesView() - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_category_educational_games") - } + .logEventScreenView(screenName: "educational_games", context: .splitView) case .stories: CategoryStoriesView() - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_category_stories") - } + .logEventScreenView(screenName: "stories", context: .splitView) case .gamepads: CategoryGamepadsView() - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_category_gamepads") - } + .logEventScreenView(screenName: "gamepads", context: .splitView) case .caregivers: CaregiverList() - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_category_caregivers") - } + .logEventScreenView(screenName: "caregivers", context: .splitView) case .carereceivers: CarereceiverList() - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_category_carereceivers") - } + .logEventScreenView(screenName: "carereceivers", context: .splitView) // ? DEVELOPER_MODE + TESTFLIGHT_BUILD case .allPublishedActivities: @@ -247,21 +225,15 @@ struct MainView: View { case .libraryCurriculums: CategoryLibraryView(category: .libraryCurriculums) - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_category_library_curriculums") - } + .logEventScreenView(screenName: "library_curriculums", context: .splitView) case .libraryActivities: CategoryLibraryView(category: .libraryActivities) - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_category_library_activities") - } + .logEventScreenView(screenName: "library_activities", context: .splitView) case .libraryStories: CategoryLibraryView(category: .libraryStories) - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_category_library_stories") - } + .logEventScreenView(screenName: "library_stories", context: .splitView) case .none: Text(l10n.MainView.Sidebar.CategoryLabel.home) @@ -279,19 +251,15 @@ struct MainView: View { switch content { case .welcomeView: WelcomeView() - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_welcome") - } + .logEventScreenView(screenName: "welcome", context: .fullScreenCover) + case let .activityView(carereceivers): ActivityView(activity: self.navigation.currentActivity!, reinforcer: carereceivers.first?.reinforcer ?? .rainbow) - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_activity") - } + .logEventScreenView(screenName: "activity", context: .fullScreenCover) + case .storyView: StoryView(story: self.navigation.currentStory!) - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_story") - } + .logEventScreenView(screenName: "story", context: .fullScreenCover) } } } @@ -302,34 +270,28 @@ struct MainView: View { switch content { case .robotConnection: RobotConnectionView(viewModel: RobotConnectionViewModel()) + .logEventScreenView(screenName: "robot_connection", context: .sheet) .navigationBarTitleDisplayMode(.inline) - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_robot_connection") - } + case .settings: SettingsView() + .logEventScreenView(screenName: "settings", context: .sheet) .navigationBarTitleDisplayMode(.inline) - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_settings") - } + case .editCaregiver: EditCaregiverView(caregiver: self.caregiverManagerViewModel.currentCaregiver!) + .logEventScreenView(screenName: "caregiver_edit", context: .sheet) .navigationBarTitleDisplayMode(.inline) - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_edit_caregiver") - } + case .createCaregiver: CreateCaregiverView() + .logEventScreenView(screenName: "caregiver_create", context: .sheet) .navigationBarTitleDisplayMode(.inline) - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_create_caregiver") - } + case .caregiverPicker: CaregiverPicker() + .logEventScreenView(screenName: "caregiver_picker", context: .sheet) .navigationBarTitleDisplayMode(.inline) - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_caregiver_picker") - } .onDisappear { DispatchQueue.main.asyncAfter(deadline: .now() + 2) { if case .appUpdateAvailable = UpdateManager.shared.appUpdateStatus { @@ -361,10 +323,8 @@ struct MainView: View { self.navigation.fullScreenCoverContent = .storyView(carereceivers: []) } }) + .logEventScreenView(screenName: "carereceiver_picker", context: .sheet) .navigationBarTitleDisplayMode(.inline) - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_carereceiver_picker") - } } } } @@ -374,9 +334,6 @@ struct MainView: View { } self.persistentDataManager.checkInactivity() } - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_main_navigation_split_view") - } .onChange(of: self.scenePhase) { newPhase in guard self.authManagerViewModel.userAuthenticationState == .loggedIn else { return diff --git a/Apps/LekaApp/Sources/Views/Users/CareReceiver/CarereceiverList.swift b/Apps/LekaApp/Sources/Views/Users/CareReceiver/CarereceiverList.swift index 5683915a3..f094a811c 100644 --- a/Apps/LekaApp/Sources/Views/Users/CareReceiver/CarereceiverList.swift +++ b/Apps/LekaApp/Sources/Views/Users/CareReceiver/CarereceiverList.swift @@ -79,6 +79,7 @@ struct CarereceiverList: View { .sheet(isPresented: self.$isCarereceiverCreationPresented) { NavigationStack { CreateCarereceiverView() + .logEventScreenView(screenName: "carereceiver_create", context: .sheet) .navigationBarTitleDisplayMode(.inline) } } diff --git a/Apps/LekaApp/Sources/Views/Users/CareReceiver/CarereceiverView.swift b/Apps/LekaApp/Sources/Views/Users/CareReceiver/CarereceiverView.swift index 2c2f4863f..31501aa2f 100644 --- a/Apps/LekaApp/Sources/Views/Users/CareReceiver/CarereceiverView.swift +++ b/Apps/LekaApp/Sources/Views/Users/CareReceiver/CarereceiverView.swift @@ -59,6 +59,7 @@ struct CarereceiverView: View { .sheet(isPresented: self.$isEditCarereceiverViewPresented) { NavigationStack { EditCarereceiverView(modifiedCarereceiver: self.$carereceiver) + .logEventScreenView(screenName: "carereceiver_edit", context: .sheet) .navigationBarTitleDisplayMode(.inline) } } diff --git a/Apps/LekaApp/Sources/Views/Users/CareReceiver/CreateCarereceiverView.swift b/Apps/LekaApp/Sources/Views/Users/CareReceiver/CreateCarereceiverView.swift index 9d5dc1fa0..9402d6b30 100644 --- a/Apps/LekaApp/Sources/Views/Users/CareReceiver/CreateCarereceiverView.swift +++ b/Apps/LekaApp/Sources/Views/Users/CareReceiver/CreateCarereceiverView.swift @@ -189,9 +189,6 @@ extension l10n { CreateCarereceiverView(onClose: { print("Care receiver creation canceled") }) -// , onCreated: { -// print("Carereceiver \($0.username) created") -// }) } } } diff --git a/Apps/LekaApp/Sources/Views/Users/Caregiver/CaregiverList.swift b/Apps/LekaApp/Sources/Views/Users/Caregiver/CaregiverList.swift index a7675cd81..891676022 100644 --- a/Apps/LekaApp/Sources/Views/Users/Caregiver/CaregiverList.swift +++ b/Apps/LekaApp/Sources/Views/Users/Caregiver/CaregiverList.swift @@ -79,6 +79,7 @@ struct CaregiverList: View { .sheet(isPresented: self.$isCaregiverCreationPresented) { NavigationStack { CreateCaregiverView() + .logEventScreenView(screenName: "caregiver_create", context: .sheet) .navigationBarTitleDisplayMode(.inline) } } diff --git a/Apps/LekaApp/Sources/Views/Users/Caregiver/CaregiverPicker.swift b/Apps/LekaApp/Sources/Views/Users/Caregiver/CaregiverPicker.swift index 0784460a2..cbe75d316 100644 --- a/Apps/LekaApp/Sources/Views/Users/Caregiver/CaregiverPicker.swift +++ b/Apps/LekaApp/Sources/Views/Users/Caregiver/CaregiverPicker.swift @@ -3,7 +3,6 @@ // SPDX-License-Identifier: Apache-2.0 import AccountKit -import AnalyticsKit import DesignKit import LocalizationKit import SwiftUI @@ -35,6 +34,7 @@ struct CaregiverPicker: View { .sheet(isPresented: self.$isCaregiverCreationPresented) { NavigationStack { CreateCaregiverView() + .logEventScreenView(screenName: "caregiver_create", context: .sheet) .navigationBarTitleDisplayMode(.inline) } } diff --git a/Modules/AccountKit/Sources/Authentication/AuthManagerViewModel.swift b/Modules/AccountKit/Sources/Authentication/AuthManagerViewModel.swift index b83e8bac0..452c2bb98 100644 --- a/Modules/AccountKit/Sources/Authentication/AuthManagerViewModel.swift +++ b/Modules/AccountKit/Sources/Authentication/AuthManagerViewModel.swift @@ -2,7 +2,6 @@ // Copyright APF France handicap // SPDX-License-Identifier: Apache-2.0 -import AnalyticsKit import Combine import Foundation import LocalizationKit diff --git a/Modules/AccountKit/Sources/Managers/RootAccounts/RootAccountManager.swift b/Modules/AccountKit/Sources/Managers/RootAccounts/RootAccountManager.swift index 40d349b78..4d1a460e0 100644 --- a/Modules/AccountKit/Sources/Managers/RootAccounts/RootAccountManager.swift +++ b/Modules/AccountKit/Sources/Managers/RootAccounts/RootAccountManager.swift @@ -2,7 +2,6 @@ // Copyright APF France handicap // SPDX-License-Identifier: Apache-2.0 -import AnalyticsKit import Combine public class RootAccountManager { diff --git a/Modules/AnalyticsKit/Sources/Analytics/AnalyticsManager+ScreenView.swift b/Modules/AnalyticsKit/Sources/Analytics/AnalyticsManager+ScreenView.swift index 73459c987..54cbabf26 100644 --- a/Modules/AnalyticsKit/Sources/Analytics/AnalyticsManager+ScreenView.swift +++ b/Modules/AnalyticsKit/Sources/Analytics/AnalyticsManager+ScreenView.swift @@ -23,31 +23,21 @@ public extension AnalyticsManager { case sheet case fullScreenCover case context(String) - case none - case additionalInfo(String) // MARK: Internal var description: String { switch self { case .splitView: - "main_splitview" + "splitview" case .sheet: - "main_sheet" + "sheet" case .fullScreenCover: - "main_fullscreen" + "fullscreen" case let .context(value): "\(value)" - case .none: - "none" - case let .additionalInfo(value): - "\(value)" } } - - static func + (lhs: Self, rhs: Self) -> ScreenViewContext { - .additionalInfo("\(lhs.description)-\(rhs.description)") - } } } @@ -57,8 +47,8 @@ struct AnalyticsLogScreenViewViewModifier: ViewModifier { // MARK: Lifecycle init( - screenName: String, screenClass: String, + screenName: String, context: AnalyticsManager.ScreenViewContext? = nil, parameters: [String: Any] = [:] ) { @@ -70,35 +60,34 @@ struct AnalyticsLogScreenViewViewModifier: ViewModifier { // MARK: Internal - let screenName: String let screenClass: String + let screenName: String let context: AnalyticsManager.ScreenViewContext? let parameters: [String: Any] func body(content: Content) -> some View { content .onAppear { - let screenName = "\(screenName)" let params: [String: Any] = [ "lk_context": context?.description ?? NSNull(), ].merging(self.parameters) { _, new in new } - AnalyticsManager.logEventScreenView(screenName: screenName, screenClass: self.screenClass, parameters: self.parameters) + AnalyticsManager.logEventScreenView(screenName: self.screenName, screenClass: self.screenClass, parameters: params) } } } public extension View { func logEventScreenView( - screenName: String, - context: AnalyticsManager.ScreenViewContext?, screenClass: String? = nil, + screenName: String, + context: AnalyticsManager.ScreenViewContext? = nil, parameters: [String: Any] = [:] ) -> some View { self.modifier( AnalyticsLogScreenViewViewModifier( - screenName: screenName, screenClass: screenClass ?? String(describing: type(of: self)), + screenName: screenName, context: context, parameters: parameters ) @@ -106,8 +95,6 @@ public extension View { } } -// MARK: - MyCustomView - #Preview { struct MyCustomView: View { @State private var isPresented = true diff --git a/Modules/ContentKit/Examples/ContentKitExample/Sources/CurriculumListView.swift b/Modules/ContentKit/Examples/ContentKitExample/Sources/CurriculumListView.swift index 975b7222a..923d61eec 100644 --- a/Modules/ContentKit/Examples/ContentKitExample/Sources/CurriculumListView.swift +++ b/Modules/ContentKit/Examples/ContentKitExample/Sources/CurriculumListView.swift @@ -2,6 +2,7 @@ // Copyright APF France handicap // SPDX-License-Identifier: Apache-2.0 +import AnalyticsKit import ContentKit import MarkdownUI import SwiftUI @@ -14,13 +15,23 @@ struct CurriculumListView: View { var body: some View { List { ForEach(self.activities) { curriculum in - NavigationLink(destination: CurriculumDetailsView(curriculum: curriculum)) { + NavigationLink(destination: + CurriculumDetailsView(curriculum: curriculum) + ) { Image(uiImage: curriculum.details.iconImage) .resizable() .scaledToFit() .frame(width: 44, height: 44) Text(curriculum.details.title) } + .simultaneousGesture(TapGesture().onEnded { + AnalyticsManager.logEventSelectContent( + type: .curriculum, + id: curriculum.id, + name: curriculum.name, + origin: .generalLibrary + ) + }) } } .navigationTitle("Curriculums") diff --git a/Modules/ContentKit/Sources/Views/ActivityDetailsView.swift b/Modules/ContentKit/Sources/Views/ActivityDetailsView.swift index bbc84b81d..bb305689d 100644 --- a/Modules/ContentKit/Sources/Views/ActivityDetailsView.swift +++ b/Modules/ContentKit/Sources/Views/ActivityDetailsView.swift @@ -3,7 +3,6 @@ // SPDX-License-Identifier: Apache-2.0 import AccountKit -import AnalyticsKit import DesignKit import Fit import LocalizationKit @@ -117,9 +116,6 @@ public struct ActivityDetailsView: View { .markdownTheme(.gitHub) } } - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_activity_details_view") - } .toolbar { #if DEVELOPER_MODE || TESTFLIGHT_BUILD if let currentCaregiverID = self.caregiverManagerViewModel.currentCaregiver?.id { diff --git a/Modules/ContentKit/Sources/Views/ActivityGridView.swift b/Modules/ContentKit/Sources/Views/ActivityGridView.swift index 0f9fe5589..e4e30c86f 100644 --- a/Modules/ContentKit/Sources/Views/ActivityGridView.swift +++ b/Modules/ContentKit/Sources/Views/ActivityGridView.swift @@ -24,14 +24,13 @@ public struct ActivityGridView: View { ForEach(self.activities) { activity in NavigationLink(destination: ActivityDetailsView(activity: activity, onStartActivity: self.onStartActivity) - .onAppear { - AnalyticsManager.logEventSelectContent( - type: .educationalGame, - id: activity.id, - name: activity.name, - origin: .generalLibrary - ) - } + .logEventScreenView( + screenName: "activity_details", + context: .splitView, + parameters: [ + "lk_activity_id": "\(activity.name)-\(activity.id)", + ] + ) ) { VStack { Image(uiImage: activity.details.iconImage) @@ -56,6 +55,14 @@ public struct ActivityGridView: View { } .padding(.vertical) } + .simultaneousGesture(TapGesture().onEnded { + AnalyticsManager.logEventSelectContent( + type: .educationalGame, + id: activity.id, + name: activity.name, + origin: .generalLibrary + ) + }) } } .padding() diff --git a/Modules/ContentKit/Sources/Views/ActivityHorizontalListView.swift b/Modules/ContentKit/Sources/Views/ActivityHorizontalListView.swift index 96ba6df2e..9f5a7d5ff 100644 --- a/Modules/ContentKit/Sources/Views/ActivityHorizontalListView.swift +++ b/Modules/ContentKit/Sources/Views/ActivityHorizontalListView.swift @@ -2,6 +2,7 @@ // Copyright APF France handicap // SPDX-License-Identifier: Apache-2.0 +import AnalyticsKit import DesignKit import SwiftUI @@ -23,6 +24,13 @@ public struct ActivityHorizontalListView: View { ForEach(self.activities) { activity in NavigationLink(destination: ActivityDetailsView(activity: activity, onStartActivity: self.onActivitySelected) + .logEventScreenView( + screenName: "activity_details", + context: .splitView, + parameters: [ + "lk_activity_id": "\(activity.name)-\(activity.id)", + ] + ) ) { VStack(spacing: 10) { Image(uiImage: activity.details.iconImage) @@ -51,6 +59,14 @@ public struct ActivityHorizontalListView: View { } .frame(width: 280) } + .simultaneousGesture(TapGesture().onEnded { + AnalyticsManager.logEventSelectContent( + type: .activity, + id: activity.id, + name: activity.name, + origin: .generalLibrary + ) + }) } } } diff --git a/Modules/ContentKit/Sources/Views/ActivityListView.swift b/Modules/ContentKit/Sources/Views/ActivityListView.swift index 0b6765fa4..efe638c4e 100644 --- a/Modules/ContentKit/Sources/Views/ActivityListView.swift +++ b/Modules/ContentKit/Sources/Views/ActivityListView.swift @@ -25,14 +25,14 @@ public struct ActivityListView: View { ForEach(self.activities) { activity in NavigationLink(destination: ActivityDetailsView(activity: activity, onStartActivity: self.onStartActivity) - .onAppear { - AnalyticsManager.logEventSelectContent( - type: .activity, - id: activity.id, - name: activity.name, - origin: .generalLibrary - ) - } + .logEventScreenView( + screenName: "activity_details", + context: .splitView, + parameters: [ + "lk_activity_id": "\(activity.name)-\(activity.id)", + ] + ) + ) { HStack(alignment: .center) { Image(uiImage: activity.details.iconImage) @@ -120,6 +120,14 @@ public struct ActivityListView: View { .frame(maxWidth: .infinity, maxHeight: 120) .contentShape(Rectangle()) } + .simultaneousGesture(TapGesture().onEnded { + AnalyticsManager.logEventSelectContent( + type: .activity, + id: activity.id, + name: activity.name, + origin: .generalLibrary + ) + }) .buttonStyle(.plain) } } diff --git a/Modules/ContentKit/Sources/Views/CurriculumDetailsView.swift b/Modules/ContentKit/Sources/Views/CurriculumDetailsView.swift index bd106cf58..be96c70a8 100644 --- a/Modules/ContentKit/Sources/Views/CurriculumDetailsView.swift +++ b/Modules/ContentKit/Sources/Views/CurriculumDetailsView.swift @@ -3,7 +3,6 @@ // SPDX-License-Identifier: Apache-2.0 import AccountKit -import AnalyticsKit import DesignKit import Fit import LocalizationKit @@ -142,9 +141,6 @@ public struct CurriculumDetailsView: View { } } } - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_curriculum_details_view") - } .toolbar { #if DEVELOPER_MODE || TESTFLIGHT_BUILD if let currentCaregiverID = self.caregiverManagerViewModel.currentCaregiver?.id { diff --git a/Modules/ContentKit/Sources/Views/CurriculumGridView.swift b/Modules/ContentKit/Sources/Views/CurriculumGridView.swift index 1f6e2f913..0b5d074eb 100644 --- a/Modules/ContentKit/Sources/Views/CurriculumGridView.swift +++ b/Modules/ContentKit/Sources/Views/CurriculumGridView.swift @@ -23,23 +23,26 @@ public struct CurriculumGridView: View { public var body: some View { LazyVGrid(columns: self.columns) { ForEach(self.curriculums) { curriculum in - NavigationLink( - destination: - CurriculumDetailsView( - curriculum: curriculum, - onActivitySelected: self.onActivitySelected - ) - .onAppear { - AnalyticsManager.logEventSelectContent( - type: .curriculum, - id: curriculum.id, - name: curriculum.name, - origin: .personalLibrary + NavigationLink(destination: + CurriculumDetailsView(curriculum: curriculum, onActivitySelected: self.onActivitySelected) + .logEventScreenView( + screenName: "curriculum_details", + context: .splitView, + parameters: [ + "lk_curriculum_id": "\(curriculum.name)-\(curriculum.id)", + ] ) - } ) { CurriculumGroupboxView(curriculum: curriculum) } + .simultaneousGesture(TapGesture().onEnded { + AnalyticsManager.logEventSelectContent( + type: .curriculum, + id: curriculum.id, + name: curriculum.name, + origin: .personalLibrary + ) + }) } } .padding() diff --git a/Modules/ContentKit/Sources/Views/CurriculumHorizontalListView.swift b/Modules/ContentKit/Sources/Views/CurriculumHorizontalListView.swift index 7fcf2b6ce..ebe6a2194 100644 --- a/Modules/ContentKit/Sources/Views/CurriculumHorizontalListView.swift +++ b/Modules/ContentKit/Sources/Views/CurriculumHorizontalListView.swift @@ -23,23 +23,26 @@ public struct CurriculumHorizontalListView: View { ScrollView(.horizontal) { HStack(alignment: .firstTextBaseline) { ForEach(self.curriculums) { curriculum in - NavigationLink( - destination: - CurriculumDetailsView( - curriculum: curriculum, - onActivitySelected: self.onActivitySelected - ) - .onAppear { - AnalyticsManager.logEventSelectContent( - type: .curriculum, - id: curriculum.id, - name: curriculum.name, - origin: .generalLibrary + NavigationLink(destination: + CurriculumDetailsView(curriculum: curriculum, onActivitySelected: self.onActivitySelected) + .logEventScreenView( + screenName: "curriculum_details", + context: .splitView, + parameters: [ + "lk_curriculum_id": "\(curriculum.name)-\(curriculum.id)", + ] ) - } ) { CurriculumGroupboxView(curriculum: curriculum) } + .simultaneousGesture(TapGesture().onEnded { + AnalyticsManager.logEventSelectContent( + type: .curriculum, + id: curriculum.id, + name: curriculum.name, + origin: .generalLibrary + ) + }) } } } diff --git a/Modules/ContentKit/Sources/Views/GamepadGridView.swift b/Modules/ContentKit/Sources/Views/GamepadGridView.swift index 830d04529..92d7db683 100644 --- a/Modules/ContentKit/Sources/Views/GamepadGridView.swift +++ b/Modules/ContentKit/Sources/Views/GamepadGridView.swift @@ -23,14 +23,14 @@ public struct GamepadGridView: View { ForEach(self.gamepads) { activity in NavigationLink(destination: ActivityDetailsView(activity: activity, onStartActivity: self.onStartGamepad) - .onAppear { - AnalyticsManager.logEventSelectContent( - type: .gamepad, - id: activity.id, - name: activity.name, - origin: .generalLibrary - ) - } + .logEventScreenView( + screenName: "activity_details", + context: .splitView, + parameters: [ + "lk_activity_id": "\(activity.name)-\(activity.id)", + ] + ) + ) { VStack { Image(uiImage: activity.details.iconImage) @@ -46,6 +46,15 @@ public struct GamepadGridView: View { Spacer() } } + .simultaneousGesture(TapGesture().onEnded { + log.debug("Gamepad selected: \(activity.name)") + AnalyticsManager.logEventSelectContent( + type: .gamepad, + id: activity.id, + name: activity.name, + origin: .generalLibrary + ) + }) } } } diff --git a/Modules/ContentKit/Sources/Views/LibraryActivityListView.swift b/Modules/ContentKit/Sources/Views/LibraryActivityListView.swift index a6957a51c..5aea2aa72 100644 --- a/Modules/ContentKit/Sources/Views/LibraryActivityListView.swift +++ b/Modules/ContentKit/Sources/Views/LibraryActivityListView.swift @@ -25,14 +25,13 @@ public struct LibraryActivityListView: View { ForEach(self.activities) { activity in NavigationLink(destination: ActivityDetailsView(activity: activity, onStartActivity: self.onStartActivity) - .onAppear { - AnalyticsManager.logEventSelectContent( - type: .activity, - id: activity.id, - name: activity.name, - origin: .personalLibrary - ) - } + .logEventScreenView( + screenName: "activity_details", + context: .splitView, + parameters: [ + "lk_activity_id": "\(activity.name)-\(activity.id)", + ] + ) ) { HStack(alignment: .center) { Image(uiImage: activity.details.iconImage) @@ -121,6 +120,14 @@ public struct LibraryActivityListView: View { .contentShape(Rectangle()) } .buttonStyle(.plain) + .simultaneousGesture(TapGesture().onEnded { + AnalyticsManager.logEventSelectContent( + type: .activity, + id: activity.id, + name: activity.name, + origin: .personalLibrary + ) + }) } } .padding() diff --git a/Modules/ContentKit/Sources/Views/StoryDetailsView.swift b/Modules/ContentKit/Sources/Views/StoryDetailsView.swift index 0ebf2ef11..ae54a769a 100644 --- a/Modules/ContentKit/Sources/Views/StoryDetailsView.swift +++ b/Modules/ContentKit/Sources/Views/StoryDetailsView.swift @@ -3,7 +3,6 @@ // SPDX-License-Identifier: Apache-2.0 import AccountKit -import AnalyticsKit import DesignKit import Fit import LocalizationKit @@ -108,9 +107,6 @@ public struct StoryDetailsView: View { .markdownTheme(.gitHub) } } - .onAppear { - AnalyticsManager.logEventScreenView(screenName: "view_story_details_view") - } .toolbar { #if DEVELOPER_MODE || TESTFLIGHT_BUILD if let currentCaregiverID = self.caregiverManagerViewModel.currentCaregiver?.id { diff --git a/Modules/ContentKit/Sources/Views/StoryGridView.swift b/Modules/ContentKit/Sources/Views/StoryGridView.swift index 7a430e518..039c7dd45 100644 --- a/Modules/ContentKit/Sources/Views/StoryGridView.swift +++ b/Modules/ContentKit/Sources/Views/StoryGridView.swift @@ -23,14 +23,13 @@ public struct StoryGridView: View { ForEach(self.stories) { story in NavigationLink(destination: StoryDetailsView(story: story, onStartStory: self.onStartStory) - .onAppear { - AnalyticsManager.logEventSelectContent( - type: .story, - id: story.id, - name: story.name, - origin: .generalLibrary - ) - } + .logEventScreenView( + screenName: "story_details", + context: .splitView, + parameters: [ + "lk_story_id": "\(story.name)-\(story.id)", + ] + ) ) { VStack(spacing: 0) { Image(uiImage: story.details.iconImage) @@ -52,6 +51,14 @@ public struct StoryGridView: View { } .padding(.vertical) } + .simultaneousGesture(TapGesture().onEnded { + AnalyticsManager.logEventSelectContent( + type: .story, + id: story.id, + name: story.name, + origin: .generalLibrary + ) + }) } } } diff --git a/Modules/ContentKit/Sources/Views/StoryListView.swift b/Modules/ContentKit/Sources/Views/StoryListView.swift index 33cb3e726..fbb8f3a32 100644 --- a/Modules/ContentKit/Sources/Views/StoryListView.swift +++ b/Modules/ContentKit/Sources/Views/StoryListView.swift @@ -25,14 +25,13 @@ public struct StoryListView: View { ForEach(self.stories) { story in NavigationLink(destination: StoryDetailsView(story: story, onStartStory: self.onStartStory) - .onAppear { - AnalyticsManager.logEventSelectContent( - type: .story, - id: story.id, - name: story.name, - origin: .personalLibrary - ) - } + .logEventScreenView( + screenName: "story_details", + context: .splitView, + parameters: [ + "lk_story_id": "\(story.name)-\(story.id)", + ] + ) ) { HStack(alignment: .center, spacing: 30) { Image(uiImage: story.details.iconImage) @@ -103,6 +102,14 @@ public struct StoryListView: View { } .frame(maxWidth: .infinity, maxHeight: 120) .contentShape(Rectangle()) + .simultaneousGesture(TapGesture().onEnded { + AnalyticsManager.logEventSelectContent( + type: .story, + id: story.id, + name: story.name, + origin: .personalLibrary + ) + }) } .buttonStyle(.plain) } diff --git a/Modules/GameEngineKit/Sources/OldSystem/Stories/StoryView.swift b/Modules/GameEngineKit/Sources/OldSystem/Stories/StoryView.swift index 0d7561afc..aa83bab50 100644 --- a/Modules/GameEngineKit/Sources/OldSystem/Stories/StoryView.swift +++ b/Modules/GameEngineKit/Sources/OldSystem/Stories/StoryView.swift @@ -56,6 +56,13 @@ public struct StoryView: View { } .sheet(isPresented: self.$isInfoSheetPresented) { StoryDetailsView(story: self.viewModel.currentStory) + .logEventScreenView( + screenName: "story_details", + context: .sheet, + parameters: [ + "lk_story_id": "\(self.viewModel.currentStory.name)-\(self.viewModel.currentStory.id)", + ] + ) } .onAppear { Robot.shared.stop() diff --git a/Modules/GameEngineKit/Sources/OldSystem/Views/Activity/ActivityView.swift b/Modules/GameEngineKit/Sources/OldSystem/Views/Activity/ActivityView.swift index 435debf86..df7d19fd7 100644 --- a/Modules/GameEngineKit/Sources/OldSystem/Views/Activity/ActivityView.swift +++ b/Modules/GameEngineKit/Sources/OldSystem/Views/Activity/ActivityView.swift @@ -194,6 +194,13 @@ public struct ActivityView: View { } .sheet(isPresented: self.$isInfoSheetPresented) { ActivityDetailsView(activity: self.viewModel.currentActivity) + .logEventScreenView( + screenName: "activity_details", + context: .sheet, + parameters: [ + "lk_activity_id": "\(self.viewModel.currentActivity.name)-\(self.viewModel.currentActivity.id)", + ] + ) } .fullScreenCover(isPresented: self.$viewModel.isCurrentActivityCompleted) { self.endOfActivityScoreView