mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Set up Analytics to track data per session (#780)
This commit is contained in:
parent
d01349a60e
commit
5b7ec6c9e2
@ -52,7 +52,8 @@ let allowList = ["stefanceriu",
|
||||
"aringenbach",
|
||||
"flescio",
|
||||
"Velin92",
|
||||
"alfogrillo"]
|
||||
"alfogrillo",
|
||||
"nimau"]
|
||||
|
||||
let requiresSignOff = !allowList.contains(where: {
|
||||
$0.caseInsensitiveCompare(danger.github.pullRequest.user.login) == .orderedSame
|
||||
|
@ -53,6 +53,7 @@
|
||||
"action_view_source" = "View Source";
|
||||
"action_yes" = "Yes";
|
||||
"common_about" = "About";
|
||||
"common_analytics" = "Analytics";
|
||||
"common_audio" = "Audio";
|
||||
"common_bubbles" = "Bubbles";
|
||||
"common_creating_room" = "Creating room…";
|
||||
@ -161,6 +162,17 @@
|
||||
"room_timeline_beginning_of_room" = "This is the beginning of %1$@.";
|
||||
"room_timeline_beginning_of_room_no_name" = "This is the beginning of this conversation.";
|
||||
"room_timeline_read_marker_title" = "New";
|
||||
"screen_analytics_help_us_improve" = "Help us identify issues and improve %1$@ by sharing anonymous usage data.";
|
||||
"screen_analytics_prompt_data_usage" = "We <b>don't</b> record or profile any account data";
|
||||
"screen_analytics_prompt_help_us_improve" = "Help us identify issues and improve %1$@ by sharing anonymous usage data.";
|
||||
"screen_analytics_prompt_read_terms" = "You can read all our terms %1$@.";
|
||||
"screen_analytics_prompt_read_terms_content_link" = "here";
|
||||
"screen_analytics_prompt_settings" = "You can turn this off anytime in settings";
|
||||
"screen_analytics_prompt_third_party_sharing" = "We <b>don't</b> share information with third parties";
|
||||
"screen_analytics_prompt_title" = "Help improve %1$@";
|
||||
"screen_analytics_read_terms" = "You can read all our terms %1$@.";
|
||||
"screen_analytics_read_terms_content_link" = "here";
|
||||
"screen_analytics_share_data" = "Share analytics data";
|
||||
"screen_bug_report_attach_screenshot" = "Attach screenshot";
|
||||
"screen_bug_report_contact_me" = "You may contact me if you have any follow up questions";
|
||||
"screen_bug_report_edit_screenshot" = "Edit screenshot";
|
||||
@ -198,6 +210,10 @@
|
||||
"screen_dm_details_unblock_alert_action" = "Unblock";
|
||||
"screen_dm_details_unblock_alert_description" = "On unblocking the user, you will be able to see all messages by them again.";
|
||||
"screen_dm_details_unblock_user" = "Unblock user";
|
||||
"screen_invites_decline_chat_message" = "Are you sure you want to decline joining %1$@?";
|
||||
"screen_invites_decline_chat_title" = "Decline invite";
|
||||
"screen_invites_decline_direct_chat_message" = "Are you sure you want to decline to chat with %1$@?";
|
||||
"screen_invites_decline_direct_chat_title" = "Decline chat";
|
||||
"screen_invites_empty_list" = "No Invites";
|
||||
"screen_invites_invited_you" = "%1$@ invited you";
|
||||
"screen_login_error_deactivated_account" = "This account has been deactivated.";
|
||||
|
@ -4,15 +4,6 @@
|
||||
/* Used for testing */
|
||||
"untranslated" = "Untranslated";
|
||||
|
||||
// MARK: - Analytics
|
||||
|
||||
"analytics_opt_in_title" = "Help improve %@";
|
||||
"analytics_opt_in_content" = "Help us identify issues and improve %@ by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices.\n\nYou can read all our terms %@.";
|
||||
"analytics_opt_in_content_link" = "here";
|
||||
"analytics_opt_in_list_item_1" = "We <b>don\'t</b> record or profile any account data";
|
||||
"analytics_opt_in_list_item_2" = "We <b>don\'t</b> share information with third parties";
|
||||
"analytics_opt_in_list_item_3" = "You can turn this off anytime in settings";
|
||||
|
||||
// MARK: - Soft logout
|
||||
|
||||
"soft_logout_forgot_password" = "Forgot password";
|
||||
|
@ -42,7 +42,6 @@ class AppCoordinator: AppCoordinatorProtocol {
|
||||
private var userSessionFlowCoordinator: UserSessionFlowCoordinator?
|
||||
private var authenticationCoordinator: AuthenticationCoordinator?
|
||||
|
||||
private let bugReportService: BugReportServiceProtocol
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
|
||||
private var userSessionCancellables = Set<AnyCancellable>()
|
||||
@ -57,9 +56,9 @@ class AppCoordinator: AppCoordinatorProtocol {
|
||||
Self.setupServiceLocator(navigationRootCoordinator: navigationRootCoordinator)
|
||||
Self.setupLogging()
|
||||
|
||||
stateMachine = AppCoordinatorStateMachine()
|
||||
ServiceLocator.shared.analytics.startIfEnabled()
|
||||
|
||||
bugReportService = BugReportService(withBaseURL: ServiceLocator.shared.settings.bugReportServiceBaseURL, sentryURL: ServiceLocator.shared.settings.bugReportSentryURL)
|
||||
stateMachine = AppCoordinatorStateMachine()
|
||||
|
||||
navigationRootCoordinator.setRootCoordinator(SplashScreenCoordinator())
|
||||
|
||||
@ -114,6 +113,9 @@ class AppCoordinator: AppCoordinatorProtocol {
|
||||
ServiceLocator.shared.register(userIndicatorController: UserIndicatorController(rootCoordinator: navigationRootCoordinator))
|
||||
ServiceLocator.shared.register(appSettings: AppSettings())
|
||||
ServiceLocator.shared.register(networkMonitor: NetworkMonitor())
|
||||
ServiceLocator.shared.register(bugReportService: BugReportService(withBaseURL: ServiceLocator.shared.settings.bugReportServiceBaseURL,
|
||||
sentryURL: ServiceLocator.shared.settings.bugReportSentryURL))
|
||||
ServiceLocator.shared.register(analytics: Analytics(client: PostHogAnalyticsClient()))
|
||||
}
|
||||
|
||||
private static func setupLogging() {
|
||||
@ -248,7 +250,7 @@ class AppCoordinator: AppCoordinatorProtocol {
|
||||
let navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: SplashScreenCoordinator())
|
||||
let userSessionFlowCoordinator = UserSessionFlowCoordinator(userSession: userSession,
|
||||
navigationSplitCoordinator: navigationSplitCoordinator,
|
||||
bugReportService: bugReportService,
|
||||
bugReportService: ServiceLocator.shared.bugReportService,
|
||||
roomTimelineControllerFactory: RoomTimelineControllerFactory())
|
||||
|
||||
userSessionFlowCoordinator.callback = { [weak self] action in
|
||||
@ -292,6 +294,9 @@ class AppCoordinator: AppCoordinatorProtocol {
|
||||
userSessionStore.logout(userSession: userSession)
|
||||
tearDownUserSession()
|
||||
|
||||
// reset analytics
|
||||
ServiceLocator.shared.analytics.reset()
|
||||
|
||||
stateMachine.processEvent(.completedSigningOut(isSoft: isSoft))
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +68,6 @@ class AppCoordinatorStateMachine {
|
||||
private func configure() {
|
||||
stateMachine.addRoutes(event: .startWithAuthentication, transitions: [.initial => .signedOut])
|
||||
stateMachine.addRoutes(event: .createdUserSession, transitions: [.signedOut => .signedIn])
|
||||
|
||||
stateMachine.addRoutes(event: .startWithExistingSession, transitions: [.initial => .restoringSession])
|
||||
stateMachine.addRoutes(event: .createdUserSession, transitions: [.restoringSession => .signedIn])
|
||||
stateMachine.addRoutes(event: .failedRestoringSession, transitions: [.restoringSession => .signedOut])
|
||||
|
@ -23,7 +23,6 @@ final class AppSettings: ObservableObject {
|
||||
case lastVersionLaunched
|
||||
case timelineStyle
|
||||
case enableAnalytics
|
||||
case isIdentifiedForAnalytics
|
||||
case enableInAppNotifications
|
||||
case pusherProfileTag
|
||||
case shouldCollapseRoomStateEvents
|
||||
@ -130,12 +129,6 @@ final class AppSettings: ObservableObject {
|
||||
@UserSetting(key: UserDefaultsKeys.enableAnalytics.rawValue, defaultValue: false, persistIn: store)
|
||||
var enableAnalytics
|
||||
|
||||
/// Indicates if the device has already called identify for this session to PostHog.
|
||||
/// This is separate to `enableAnalytics` as logging out leaves analytics
|
||||
/// enabled, but requires the next account to be identified separately.
|
||||
@UserSetting(key: UserDefaultsKeys.isIdentifiedForAnalytics.rawValue, defaultValue: false, persistIn: store)
|
||||
var isIdentifiedForAnalytics
|
||||
|
||||
// MARK: - Room Screen
|
||||
|
||||
@UserSettingRawRepresentable(key: UserDefaultsKeys.timelineStyle.rawValue, defaultValue: TimelineStyle.bubbles, persistIn: store)
|
||||
|
@ -38,4 +38,16 @@ class ServiceLocator {
|
||||
func register(networkMonitor: NetworkMonitor) {
|
||||
self.networkMonitor = networkMonitor
|
||||
}
|
||||
|
||||
private(set) var analytics: Analytics!
|
||||
|
||||
func register(analytics: Analytics) {
|
||||
self.analytics = analytics
|
||||
}
|
||||
|
||||
private(set) var bugReportService: BugReportServiceProtocol!
|
||||
|
||||
func register(bugReportService: BugReportServiceProtocol) {
|
||||
self.bugReportService = bugReportService
|
||||
}
|
||||
}
|
||||
|
@ -10,24 +10,6 @@ import Foundation
|
||||
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
|
||||
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||
public enum UntranslatedL10n {
|
||||
/// Help us identify issues and improve %@ by sharing anonymous usage data. To understand how people use multiple devices, we’ll generate a random identifier, shared by your devices.
|
||||
///
|
||||
/// You can read all our terms %@.
|
||||
public static func analyticsOptInContent(_ p1: Any, _ p2: Any) -> String {
|
||||
return UntranslatedL10n.tr("Untranslated", "analytics_opt_in_content", String(describing: p1), String(describing: p2))
|
||||
}
|
||||
/// here
|
||||
public static var analyticsOptInContentLink: String { return UntranslatedL10n.tr("Untranslated", "analytics_opt_in_content_link") }
|
||||
/// We <b>don't</b> record or profile any account data
|
||||
public static var analyticsOptInListItem1: String { return UntranslatedL10n.tr("Untranslated", "analytics_opt_in_list_item_1") }
|
||||
/// We <b>don't</b> share information with third parties
|
||||
public static var analyticsOptInListItem2: String { return UntranslatedL10n.tr("Untranslated", "analytics_opt_in_list_item_2") }
|
||||
/// You can turn this off anytime in settings
|
||||
public static var analyticsOptInListItem3: String { return UntranslatedL10n.tr("Untranslated", "analytics_opt_in_list_item_3") }
|
||||
/// Help improve %@
|
||||
public static func analyticsOptInTitle(_ p1: Any) -> String {
|
||||
return UntranslatedL10n.tr("Untranslated", "analytics_opt_in_title", String(describing: p1))
|
||||
}
|
||||
/// Camera
|
||||
public static var mediaUploadCameraPicker: String { return UntranslatedL10n.tr("Untranslated", "media_upload_camera_picker") }
|
||||
/// Document
|
||||
|
@ -120,6 +120,8 @@ public enum L10n {
|
||||
public static var actionYes: String { return L10n.tr("Localizable", "action_yes") }
|
||||
/// About
|
||||
public static var commonAbout: String { return L10n.tr("Localizable", "common_about") }
|
||||
/// Analytics
|
||||
public static var commonAnalytics: String { return L10n.tr("Localizable", "common_analytics") }
|
||||
/// Audio
|
||||
public static var commonAudio: String { return L10n.tr("Localizable", "common_audio") }
|
||||
/// Bubbles
|
||||
@ -390,6 +392,38 @@ public enum L10n {
|
||||
public static func roomTimelineStateChanges(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "room_timeline_state_changes", p1)
|
||||
}
|
||||
/// Help us identify issues and improve %1$@ by sharing anonymous usage data.
|
||||
public static func screenAnalyticsHelpUsImprove(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_analytics_help_us_improve", String(describing: p1))
|
||||
}
|
||||
/// We <b>don't</b> record or profile any account data
|
||||
public static var screenAnalyticsPromptDataUsage: String { return L10n.tr("Localizable", "screen_analytics_prompt_data_usage") }
|
||||
/// Help us identify issues and improve %1$@ by sharing anonymous usage data.
|
||||
public static func screenAnalyticsPromptHelpUsImprove(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_analytics_prompt_help_us_improve", String(describing: p1))
|
||||
}
|
||||
/// You can read all our terms %1$@.
|
||||
public static func screenAnalyticsPromptReadTerms(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_analytics_prompt_read_terms", String(describing: p1))
|
||||
}
|
||||
/// here
|
||||
public static var screenAnalyticsPromptReadTermsContentLink: String { return L10n.tr("Localizable", "screen_analytics_prompt_read_terms_content_link") }
|
||||
/// You can turn this off anytime in settings
|
||||
public static var screenAnalyticsPromptSettings: String { return L10n.tr("Localizable", "screen_analytics_prompt_settings") }
|
||||
/// We <b>don't</b> share information with third parties
|
||||
public static var screenAnalyticsPromptThirdPartySharing: String { return L10n.tr("Localizable", "screen_analytics_prompt_third_party_sharing") }
|
||||
/// Help improve %1$@
|
||||
public static func screenAnalyticsPromptTitle(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_analytics_prompt_title", String(describing: p1))
|
||||
}
|
||||
/// You can read all our terms %1$@.
|
||||
public static func screenAnalyticsReadTerms(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_analytics_read_terms", String(describing: p1))
|
||||
}
|
||||
/// here
|
||||
public static var screenAnalyticsReadTermsContentLink: String { return L10n.tr("Localizable", "screen_analytics_read_terms_content_link") }
|
||||
/// Share analytics data
|
||||
public static var screenAnalyticsShareData: String { return L10n.tr("Localizable", "screen_analytics_share_data") }
|
||||
/// Attach screenshot
|
||||
public static var screenBugReportAttachScreenshot: String { return L10n.tr("Localizable", "screen_bug_report_attach_screenshot") }
|
||||
/// You may contact me if you have any follow up questions
|
||||
@ -468,6 +502,18 @@ public enum L10n {
|
||||
public static var screenDmDetailsUnblockAlertDescription: String { return L10n.tr("Localizable", "screen_dm_details_unblock_alert_description") }
|
||||
/// Unblock user
|
||||
public static var screenDmDetailsUnblockUser: String { return L10n.tr("Localizable", "screen_dm_details_unblock_user") }
|
||||
/// Are you sure you want to decline joining %1$@?
|
||||
public static func screenInvitesDeclineChatMessage(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_invites_decline_chat_message", String(describing: p1))
|
||||
}
|
||||
/// Decline invite
|
||||
public static var screenInvitesDeclineChatTitle: String { return L10n.tr("Localizable", "screen_invites_decline_chat_title") }
|
||||
/// Are you sure you want to decline to chat with %1$@?
|
||||
public static func screenInvitesDeclineDirectChatMessage(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_invites_decline_direct_chat_message", String(describing: p1))
|
||||
}
|
||||
/// Decline chat
|
||||
public static var screenInvitesDeclineDirectChatTitle: String { return L10n.tr("Localizable", "screen_invites_decline_direct_chat_title") }
|
||||
/// No Invites
|
||||
public static var screenInvitesEmptyList: String { return L10n.tr("Localizable", "screen_invites_empty_list") }
|
||||
/// %1$@ invited you
|
||||
|
@ -5,13 +5,159 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import AnalyticsEvents
|
||||
class AnalyticsClientMock: AnalyticsClientProtocol {
|
||||
var isRunning: Bool {
|
||||
get { return underlyingIsRunning }
|
||||
set(value) { underlyingIsRunning = value }
|
||||
}
|
||||
var underlyingIsRunning: Bool!
|
||||
|
||||
//MARK: - start
|
||||
|
||||
var startCallsCount = 0
|
||||
var startCalled: Bool {
|
||||
return startCallsCount > 0
|
||||
}
|
||||
var startClosure: (() -> Void)?
|
||||
|
||||
func start() {
|
||||
startCallsCount += 1
|
||||
startClosure?()
|
||||
}
|
||||
//MARK: - reset
|
||||
|
||||
var resetCallsCount = 0
|
||||
var resetCalled: Bool {
|
||||
return resetCallsCount > 0
|
||||
}
|
||||
var resetClosure: (() -> Void)?
|
||||
|
||||
func reset() {
|
||||
resetCallsCount += 1
|
||||
resetClosure?()
|
||||
}
|
||||
//MARK: - stop
|
||||
|
||||
var stopCallsCount = 0
|
||||
var stopCalled: Bool {
|
||||
return stopCallsCount > 0
|
||||
}
|
||||
var stopClosure: (() -> Void)?
|
||||
|
||||
func stop() {
|
||||
stopCallsCount += 1
|
||||
stopClosure?()
|
||||
}
|
||||
//MARK: - flush
|
||||
|
||||
var flushCallsCount = 0
|
||||
var flushCalled: Bool {
|
||||
return flushCallsCount > 0
|
||||
}
|
||||
var flushClosure: (() -> Void)?
|
||||
|
||||
func flush() {
|
||||
flushCallsCount += 1
|
||||
flushClosure?()
|
||||
}
|
||||
//MARK: - capture
|
||||
|
||||
var captureCallsCount = 0
|
||||
var captureCalled: Bool {
|
||||
return captureCallsCount > 0
|
||||
}
|
||||
var captureReceivedEvent: AnalyticsEventProtocol?
|
||||
var captureReceivedInvocations: [AnalyticsEventProtocol] = []
|
||||
var captureClosure: ((AnalyticsEventProtocol) -> Void)?
|
||||
|
||||
func capture(_ event: AnalyticsEventProtocol) {
|
||||
captureCallsCount += 1
|
||||
captureReceivedEvent = event
|
||||
captureReceivedInvocations.append(event)
|
||||
captureClosure?(event)
|
||||
}
|
||||
//MARK: - screen
|
||||
|
||||
var screenCallsCount = 0
|
||||
var screenCalled: Bool {
|
||||
return screenCallsCount > 0
|
||||
}
|
||||
var screenReceivedEvent: AnalyticsScreenProtocol?
|
||||
var screenReceivedInvocations: [AnalyticsScreenProtocol] = []
|
||||
var screenClosure: ((AnalyticsScreenProtocol) -> Void)?
|
||||
|
||||
func screen(_ event: AnalyticsScreenProtocol) {
|
||||
screenCallsCount += 1
|
||||
screenReceivedEvent = event
|
||||
screenReceivedInvocations.append(event)
|
||||
screenClosure?(event)
|
||||
}
|
||||
//MARK: - updateUserProperties
|
||||
|
||||
var updateUserPropertiesCallsCount = 0
|
||||
var updateUserPropertiesCalled: Bool {
|
||||
return updateUserPropertiesCallsCount > 0
|
||||
}
|
||||
var updateUserPropertiesReceivedUserProperties: AnalyticsEvent.UserProperties?
|
||||
var updateUserPropertiesReceivedInvocations: [AnalyticsEvent.UserProperties] = []
|
||||
var updateUserPropertiesClosure: ((AnalyticsEvent.UserProperties) -> Void)?
|
||||
|
||||
func updateUserProperties(_ userProperties: AnalyticsEvent.UserProperties) {
|
||||
updateUserPropertiesCallsCount += 1
|
||||
updateUserPropertiesReceivedUserProperties = userProperties
|
||||
updateUserPropertiesReceivedInvocations.append(userProperties)
|
||||
updateUserPropertiesClosure?(userProperties)
|
||||
}
|
||||
}
|
||||
class BugReportServiceMock: BugReportServiceProtocol {
|
||||
var isRunning: Bool {
|
||||
get { return underlyingIsRunning }
|
||||
set(value) { underlyingIsRunning = value }
|
||||
}
|
||||
var underlyingIsRunning: Bool!
|
||||
var crashedLastRun: Bool {
|
||||
get { return underlyingCrashedLastRun }
|
||||
set(value) { underlyingCrashedLastRun = value }
|
||||
}
|
||||
var underlyingCrashedLastRun: Bool!
|
||||
|
||||
//MARK: - start
|
||||
|
||||
var startCallsCount = 0
|
||||
var startCalled: Bool {
|
||||
return startCallsCount > 0
|
||||
}
|
||||
var startClosure: (() -> Void)?
|
||||
|
||||
func start() {
|
||||
startCallsCount += 1
|
||||
startClosure?()
|
||||
}
|
||||
//MARK: - stop
|
||||
|
||||
var stopCallsCount = 0
|
||||
var stopCalled: Bool {
|
||||
return stopCallsCount > 0
|
||||
}
|
||||
var stopClosure: (() -> Void)?
|
||||
|
||||
func stop() {
|
||||
stopCallsCount += 1
|
||||
stopClosure?()
|
||||
}
|
||||
//MARK: - reset
|
||||
|
||||
var resetCallsCount = 0
|
||||
var resetCalled: Bool {
|
||||
return resetCallsCount > 0
|
||||
}
|
||||
var resetClosure: (() -> Void)?
|
||||
|
||||
func reset() {
|
||||
resetCallsCount += 1
|
||||
resetClosure?()
|
||||
}
|
||||
//MARK: - crash
|
||||
|
||||
var crashCallsCount = 0
|
||||
|
@ -0,0 +1,36 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// `ScreenTrackerViewModifier` is a helper class used to track PostHog screen from SwiftUI screens.
|
||||
struct ScreenTrackerViewModifier: ViewModifier {
|
||||
let screen: AnalyticsScreen
|
||||
|
||||
@ViewBuilder
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.onAppear {
|
||||
ServiceLocator.shared.analytics.track(screen: screen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func track(screen: AnalyticsScreen) -> some View {
|
||||
modifier(ScreenTrackerViewModifier(screen: screen))
|
||||
}
|
||||
}
|
@ -16,21 +16,13 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AnalyticsPromptCoordinatorParameters {
|
||||
/// The user session to use if analytics are enabled.
|
||||
let userSession: UserSessionProtocol
|
||||
}
|
||||
|
||||
final class AnalyticsPromptCoordinator: CoordinatorProtocol {
|
||||
private let parameters: AnalyticsPromptCoordinatorParameters
|
||||
private var viewModel: AnalyticsPromptViewModel
|
||||
|
||||
var callback: (@MainActor () -> Void)?
|
||||
|
||||
init(parameters: AnalyticsPromptCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = AnalyticsPromptViewModel(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
|
||||
init() {
|
||||
viewModel = AnalyticsPromptViewModel()
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@ -42,11 +34,11 @@ final class AnalyticsPromptCoordinator: CoordinatorProtocol {
|
||||
switch result {
|
||||
case .enable:
|
||||
MXLog.info("Enable Analytics")
|
||||
Analytics.shared.optIn(with: self.parameters.userSession)
|
||||
ServiceLocator.shared.analytics.optIn()
|
||||
self.callback?()
|
||||
case .disable:
|
||||
MXLog.info("Disable Analytics")
|
||||
Analytics.shared.optOut()
|
||||
ServiceLocator.shared.analytics.optOut()
|
||||
self.callback?()
|
||||
}
|
||||
}
|
||||
|
@ -32,22 +32,24 @@ enum AnalyticsPromptViewModelAction {
|
||||
|
||||
struct AnalyticsPromptViewState: BindableState {
|
||||
/// Attributed strings created from localized HTML.
|
||||
let strings = AnalyticsPromptStrings()
|
||||
let strings: AnalyticsPromptStrings
|
||||
}
|
||||
|
||||
/// A collection of strings for the UI that need to be parsed from HTML
|
||||
struct AnalyticsPromptStrings {
|
||||
let optInContent: AttributedString
|
||||
let point1 = AttributedStringBuilder().fromHTML(UntranslatedL10n.analyticsOptInListItem1) ?? AttributedString(UntranslatedL10n.analyticsOptInListItem1)
|
||||
let point2 = AttributedStringBuilder().fromHTML(UntranslatedL10n.analyticsOptInListItem2) ?? AttributedString(UntranslatedL10n.analyticsOptInListItem2)
|
||||
let point1 = AttributedStringBuilder().fromHTML(L10n.screenAnalyticsPromptDataUsage) ?? AttributedString(L10n.screenAnalyticsPromptDataUsage)
|
||||
let point2 = AttributedStringBuilder().fromHTML(L10n.screenAnalyticsPromptThirdPartySharing) ?? AttributedString(L10n.screenAnalyticsPromptThirdPartySharing)
|
||||
let point3 = L10n.screenAnalyticsPromptSettings
|
||||
|
||||
init() {
|
||||
init(termsURL: URL) {
|
||||
let content = AttributedString(L10n.screenAnalyticsPromptHelpUsImprove(InfoPlistReader.main.bundleDisplayName))
|
||||
// Create the opt in content with a placeholder.
|
||||
let linkPlaceholder = "{link}"
|
||||
var optInContent = AttributedString(UntranslatedL10n.analyticsOptInContent(InfoPlistReader.main.bundleDisplayName, linkPlaceholder))
|
||||
optInContent.replace(linkPlaceholder,
|
||||
with: UntranslatedL10n.analyticsOptInContentLink,
|
||||
asLinkTo: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
|
||||
self.optInContent = optInContent
|
||||
var readTerms = AttributedString(L10n.screenAnalyticsPromptReadTerms(linkPlaceholder))
|
||||
readTerms.replace(linkPlaceholder,
|
||||
with: L10n.screenAnalyticsPromptReadTermsContentLink,
|
||||
asLinkTo: termsURL)
|
||||
optInContent = content + "\n\n" + readTerms
|
||||
}
|
||||
}
|
||||
|
@ -20,14 +20,12 @@ import SwiftUI
|
||||
typealias AnalyticsPromptViewModelType = StateStoreViewModel<AnalyticsPromptViewState, AnalyticsPromptViewAction>
|
||||
|
||||
class AnalyticsPromptViewModel: AnalyticsPromptViewModelType, AnalyticsPromptViewModelProtocol {
|
||||
private let termsURL: URL
|
||||
|
||||
var callback: (@MainActor (AnalyticsPromptViewModelAction) -> Void)?
|
||||
|
||||
/// Initialize a view model with the specified prompt type and app display name.
|
||||
init(termsURL: URL) {
|
||||
self.termsURL = termsURL
|
||||
super.init(initialViewState: AnalyticsPromptViewState())
|
||||
init() {
|
||||
let promptStrings = AnalyticsPromptStrings(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
|
||||
super.init(initialViewState: AnalyticsPromptViewState(strings: promptStrings))
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
@ -57,9 +57,9 @@ struct AnalyticsPrompt: View {
|
||||
private var mainContent: some View {
|
||||
VStack {
|
||||
Image(uiImage: Asset.Images.analyticsLogo.image)
|
||||
.padding(.bottom, 25)
|
||||
.padding(.bottom, 24)
|
||||
|
||||
Text(UntranslatedL10n.analyticsOptInTitle(InfoPlistReader.main.bundleDisplayName))
|
||||
Text(L10n.screenAnalyticsPromptTitle(InfoPlistReader.main.bundleDisplayName))
|
||||
.font(.element.title2Bold)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
@ -73,7 +73,7 @@ struct AnalyticsPrompt: View {
|
||||
|
||||
Divider()
|
||||
.background(Color.element.quinaryContent)
|
||||
.padding(.vertical, 28)
|
||||
.padding(.vertical, 20)
|
||||
|
||||
checkmarkList
|
||||
}
|
||||
@ -81,10 +81,10 @@ struct AnalyticsPrompt: View {
|
||||
|
||||
/// The list of re-assurances about analytics.
|
||||
private var checkmarkList: some View {
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
AnalyticsPromptCheckmarkItem(attributedString: context.viewState.strings.point1)
|
||||
AnalyticsPromptCheckmarkItem(attributedString: context.viewState.strings.point1)
|
||||
AnalyticsPromptCheckmarkItem(string: UntranslatedL10n.analyticsOptInListItem3)
|
||||
AnalyticsPromptCheckmarkItem(attributedString: context.viewState.strings.point2)
|
||||
AnalyticsPromptCheckmarkItem(string: context.viewState.strings.point3)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.font(.element.body)
|
||||
@ -113,7 +113,7 @@ struct AnalyticsPrompt: View {
|
||||
// MARK: - Previews
|
||||
|
||||
struct AnalyticsPrompt_Previews: PreviewProvider {
|
||||
static let viewModel = AnalyticsPromptViewModel(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
|
||||
static let viewModel = AnalyticsPromptViewModel()
|
||||
static var previews: some View {
|
||||
AnalyticsPrompt(context: viewModel.context)
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ struct AnalyticsPromptCheckmarkItem: View {
|
||||
|
||||
var body: some View {
|
||||
Label { Text(attributedString) } icon: {
|
||||
Image(uiImage: Asset.Images.analyticsCheckmark.image)
|
||||
Image(systemName: "checkmark.circle")
|
||||
.foregroundColor(.element.accent)
|
||||
}
|
||||
}
|
||||
@ -38,7 +38,7 @@ struct AnalyticsPromptCheckmarkItem: View {
|
||||
// MARK: - Previews
|
||||
|
||||
struct AnalyticsPromptCheckmarkItem_Previews: PreviewProvider {
|
||||
static let strings = AnalyticsPromptStrings()
|
||||
static let strings = AnalyticsPromptStrings(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
|
||||
|
||||
static var previews: some View {
|
||||
VStack(alignment: .leading) {
|
||||
|
@ -0,0 +1,30 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
final class AnalyticsSettingsScreenCoordinator: CoordinatorProtocol {
|
||||
private let viewModel: AnalyticsSettingsScreenViewModel
|
||||
|
||||
init() {
|
||||
viewModel = AnalyticsSettingsScreenViewModel()
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(AnalyticsSettingsScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct AnalyticsSettingsScreenViewState: BindableState {
|
||||
/// Attributed strings created from localized HTML.
|
||||
let strings: AnalyticsSettingsScreenStrings
|
||||
var bindings: AnalyticsSettingsScreenViewStateBindings
|
||||
}
|
||||
|
||||
struct AnalyticsSettingsScreenViewStateBindings {
|
||||
var enableAnalytics: Bool
|
||||
}
|
||||
|
||||
enum AnalyticsSettingsScreenViewAction {
|
||||
case toggleAnalytics
|
||||
}
|
||||
|
||||
struct AnalyticsSettingsScreenStrings {
|
||||
let sectionFooter: AttributedString
|
||||
|
||||
init(termsURL: URL) {
|
||||
let content = AttributedString(L10n.screenAnalyticsHelpUsImprove(InfoPlistReader.main.bundleDisplayName))
|
||||
// Create the 'read terms' with a placeholder.
|
||||
let linkPlaceholder = "{link}"
|
||||
var readTerms = AttributedString(L10n.screenAnalyticsReadTerms(linkPlaceholder))
|
||||
readTerms.replace(linkPlaceholder,
|
||||
with: L10n.screenAnalyticsReadTermsContentLink,
|
||||
asLinkTo: termsURL)
|
||||
sectionFooter = content + "\n\n" + readTerms
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
typealias AnalyticsSettingsScreenViewModelType = StateStoreViewModel<AnalyticsSettingsScreenViewState, AnalyticsSettingsScreenViewAction>
|
||||
|
||||
class AnalyticsSettingsScreenViewModel: AnalyticsSettingsScreenViewModelType, AnalyticsSettingsScreenViewModelProtocol {
|
||||
init() {
|
||||
let strings = AnalyticsSettingsScreenStrings(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
|
||||
let bindings = AnalyticsSettingsScreenViewStateBindings(enableAnalytics: ServiceLocator.shared.settings.enableAnalytics)
|
||||
let state = AnalyticsSettingsScreenViewState(strings: strings, bindings: bindings)
|
||||
|
||||
super.init(initialViewState: state)
|
||||
|
||||
ServiceLocator.shared.settings.$enableAnalytics
|
||||
.weakAssign(to: \.state.bindings.enableAnalytics, on: self)
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
override func process(viewAction: AnalyticsSettingsScreenViewAction) {
|
||||
switch viewAction {
|
||||
case .toggleAnalytics:
|
||||
if ServiceLocator.shared.settings.enableAnalytics {
|
||||
ServiceLocator.shared.analytics.optOut()
|
||||
} else {
|
||||
ServiceLocator.shared.analytics.optIn()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
protocol AnalyticsSettingsScreenViewModelProtocol {
|
||||
var context: AnalyticsSettingsScreenViewModelType.Context { get }
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AnalyticsSettingsScreen: View {
|
||||
@ObservedObject var context: AnalyticsSettingsScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
analyticsSection
|
||||
}
|
||||
.compoundForm()
|
||||
.navigationTitle(L10n.commonAnalytics)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
var analyticsSection: some View {
|
||||
Section {
|
||||
Toggle(isOn: $context.enableAnalytics) {
|
||||
Label(L10n.screenAnalyticsShareData, systemImage: "chart.bar")
|
||||
}
|
||||
.toggleStyle(.compoundForm)
|
||||
.onChange(of: context.enableAnalytics) { _ in
|
||||
context.send(viewAction: .toggleAnalytics)
|
||||
}
|
||||
} footer: {
|
||||
Text(context.viewState.strings.sectionFooter)
|
||||
.compoundFormSectionFooter()
|
||||
.tint(.compound.textLinkExternal)
|
||||
}
|
||||
.compoundFormSection()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct AnalyticsSettingsScreen_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let viewModel = AnalyticsSettingsScreenViewModel()
|
||||
AnalyticsSettingsScreen(context: viewModel.context)
|
||||
}
|
||||
}
|
@ -101,23 +101,30 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
|
||||
switch action {
|
||||
case .signedIn(let userSession):
|
||||
self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
|
||||
self.userHasSignedIn(userSession: userSession)
|
||||
}
|
||||
}
|
||||
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
private func showAnalyticsPrompt(with userSession: UserSessionProtocol) {
|
||||
let parameters = AnalyticsPromptCoordinatorParameters(userSession: userSession)
|
||||
let coordinator = AnalyticsPromptCoordinator(parameters: parameters)
|
||||
|
||||
coordinator.callback = { [weak self] in
|
||||
private func userHasSignedIn(userSession: UserSessionProtocol) {
|
||||
showAnalyticsPromptIfNeeded { [weak self] in
|
||||
guard let self else { return }
|
||||
self.delegate?.authenticationCoordinator(self, didLoginWithSession: userSession)
|
||||
}
|
||||
}
|
||||
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
private func showAnalyticsPromptIfNeeded(completion: @escaping () -> Void) {
|
||||
guard ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
let coordinator = AnalyticsPromptCoordinator()
|
||||
coordinator.callback = {
|
||||
completion()
|
||||
}
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
static let loadingIndicatorIdentifier = "AuthenticationCoordinatorLoading"
|
||||
|
@ -62,11 +62,8 @@ struct HomeScreenViewState: BindableState {
|
||||
let userID: String
|
||||
var userDisplayName: String?
|
||||
var userAvatarURL: URL?
|
||||
|
||||
var showSessionVerificationBanner = false
|
||||
|
||||
var rooms: [HomeScreenRoom] = []
|
||||
|
||||
var roomListMode: HomeScreenRoomListMode = .skeletons
|
||||
|
||||
/// The URL that will be shared when inviting friends to use the app.
|
||||
|
@ -119,6 +119,7 @@ struct HomeScreen: View {
|
||||
}
|
||||
}
|
||||
.background(Color.element.background.ignoresSafeArea())
|
||||
.track(screen: .home)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
@ -40,6 +40,7 @@ struct RoomScreen: View {
|
||||
.overlay { loadingIndicator }
|
||||
.alert(item: $context.alertInfo) { $0.alert }
|
||||
.sheet(item: $context.debugInfo) { TimelineItemDebugView(info: $0) }
|
||||
.track(screen: .room)
|
||||
.task(id: context.viewState.roomId) {
|
||||
// Give a couple of seconds for items to load and to see them.
|
||||
try? await Task.sleep(for: .seconds(2))
|
||||
|
@ -46,8 +46,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
|
||||
switch action {
|
||||
case .close:
|
||||
self.callback?(.dismiss)
|
||||
case .toggleAnalytics:
|
||||
self.toggleAnalytics()
|
||||
case .analytics:
|
||||
self.presentAnalyticsScreen()
|
||||
case .reportBug:
|
||||
self.presentBugReportScreen()
|
||||
case .sessionVerification:
|
||||
@ -68,12 +68,9 @@ final class SettingsScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func toggleAnalytics() {
|
||||
if ServiceLocator.shared.settings.enableAnalytics {
|
||||
Analytics.shared.optOut()
|
||||
} else {
|
||||
Analytics.shared.optIn(with: parameters.userSession)
|
||||
}
|
||||
private func presentAnalyticsScreen() {
|
||||
let coordinator = AnalyticsSettingsScreenCoordinator()
|
||||
parameters.navigationStackCoordinator?.push(coordinator)
|
||||
}
|
||||
|
||||
private func presentBugReportScreen() {
|
||||
|
@ -19,7 +19,7 @@ import UIKit
|
||||
|
||||
enum SettingsScreenViewModelAction {
|
||||
case close
|
||||
case toggleAnalytics
|
||||
case analytics
|
||||
case reportBug
|
||||
case sessionVerification
|
||||
case developerOptions
|
||||
@ -42,7 +42,7 @@ struct SettingsScreenViewStateBindings {
|
||||
|
||||
enum SettingsScreenViewAction {
|
||||
case close
|
||||
case toggleAnalytics
|
||||
case analytics
|
||||
case reportBug
|
||||
case sessionVerification
|
||||
case logout
|
||||
|
@ -77,8 +77,8 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
|
||||
switch viewAction {
|
||||
case .close:
|
||||
callback?(.close)
|
||||
case .toggleAnalytics:
|
||||
callback?(.toggleAnalytics)
|
||||
case .analytics:
|
||||
callback?(.analytics)
|
||||
case .reportBug:
|
||||
callback?(.reportBug)
|
||||
case .logout:
|
||||
|
@ -92,7 +92,7 @@ struct SettingsScreen: View {
|
||||
Label(L10n.commonDeveloperOptions, systemImage: "hammer.circle")
|
||||
}
|
||||
.buttonStyle(.compoundForm(accessory: .navigationLink))
|
||||
.accessibilityIdentifier("sessionVerificationButton")
|
||||
.accessibilityIdentifier("developerOptionsButton")
|
||||
}
|
||||
.compoundFormSection()
|
||||
}
|
||||
@ -113,6 +113,14 @@ struct SettingsScreen: View {
|
||||
context.send(viewAction: .changedTimelineStyle)
|
||||
}
|
||||
|
||||
// Analytics
|
||||
Button { context.send(viewAction: .analytics) } label: {
|
||||
Label(L10n.commonAnalytics, systemImage: "chart.bar")
|
||||
}
|
||||
.buttonStyle(.compoundForm(accessory: .navigationLink))
|
||||
.accessibilityIdentifier("analyticsButton")
|
||||
|
||||
// Report Bug
|
||||
Button { context.send(viewAction: .reportBug) } label: {
|
||||
Label(L10n.actionReportBug, systemImage: "questionmark.circle")
|
||||
}
|
||||
|
@ -31,17 +31,12 @@ import PostHog
|
||||
/// into `main`, update the AnalyticsEvents Swift package in `project.yml`.
|
||||
///
|
||||
class Analytics {
|
||||
/// The singleton instance to be used within the Riot target.
|
||||
static let shared = Analytics()
|
||||
|
||||
/// The analytics client to send events with.
|
||||
private var client: AnalyticsClientProtocol = PostHogAnalyticsClient()
|
||||
private let client: AnalyticsClientProtocol
|
||||
|
||||
// /// The monitoring client to track crashes, issues and performance
|
||||
// private var monitoringClient = SentryMonitoringClient()
|
||||
|
||||
/// The service used to interact with account data settings.
|
||||
private var service: AnalyticsService?
|
||||
init(client: AnalyticsClientProtocol) {
|
||||
self.client = client
|
||||
}
|
||||
|
||||
/// Whether or not the object is enabled and sending events to the server.
|
||||
var isRunning: Bool { client.isRunning }
|
||||
@ -53,13 +48,9 @@ class Analytics {
|
||||
}
|
||||
|
||||
/// Opts in to analytics tracking with the supplied user session.
|
||||
/// - Parameter userSession: The user session to use to when reading/generating the analytics ID.
|
||||
/// The session will be ignored if not running.
|
||||
func optIn(with userSession: UserSessionProtocol) {
|
||||
func optIn() {
|
||||
ServiceLocator.shared.settings.enableAnalytics = true
|
||||
startIfEnabled()
|
||||
|
||||
Task { await useAnalyticsSettings(from: userSession) }
|
||||
}
|
||||
|
||||
/// Stops analytics tracking and calls `reset` to clear any IDs and event queues.
|
||||
@ -69,8 +60,7 @@ class Analytics {
|
||||
// The order is important here. PostHog ignores the reset if stopped.
|
||||
reset()
|
||||
client.stop()
|
||||
// monitoringClient.stop()
|
||||
|
||||
ServiceLocator.shared.bugReportService.stop()
|
||||
MXLog.info("Stopped.")
|
||||
}
|
||||
|
||||
@ -79,38 +69,12 @@ class Analytics {
|
||||
guard ServiceLocator.shared.settings.enableAnalytics, !isRunning else { return }
|
||||
|
||||
client.start()
|
||||
// monitoringClient.start()
|
||||
ServiceLocator.shared.bugReportService.start()
|
||||
|
||||
// Sanity check in case something went wrong.
|
||||
guard client.isRunning else { return }
|
||||
|
||||
MXLog.info("Started.")
|
||||
|
||||
// Catch and log crashes
|
||||
// MXLogger.logCrashes(true)
|
||||
// MXLogger.setBuildVersion(Bundle.bundleShortVersionString)
|
||||
}
|
||||
|
||||
/// Use the analytics settings from the supplied user session to configure analytics.
|
||||
/// For now this is only used for (pseudonymous) identification.
|
||||
/// - Parameter userSession: The user session to read analytics settings from.
|
||||
func useAnalyticsSettings(from userSession: UserSessionProtocol) async {
|
||||
guard
|
||||
ServiceLocator.shared.settings.enableAnalytics,
|
||||
!ServiceLocator.shared.settings.isIdentifiedForAnalytics
|
||||
else { return }
|
||||
|
||||
let service = AnalyticsService(userSession: userSession)
|
||||
self.service = service
|
||||
|
||||
switch await service.settings() {
|
||||
case .success(let settings):
|
||||
identify(with: settings)
|
||||
self.service = nil
|
||||
case .failure:
|
||||
MXLog.error("Failed to use analytics settings. Will continue to run without analytics ID.")
|
||||
self.service = nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the any IDs and event queues in the analytics client. This method should
|
||||
@ -119,13 +83,8 @@ class Analytics {
|
||||
/// Note: **MUST** be called before stopping PostHog or the reset is ignored.
|
||||
func reset() {
|
||||
client.reset()
|
||||
// monitoringClient.reset()
|
||||
|
||||
ServiceLocator.shared.bugReportService.reset()
|
||||
MXLog.info("Reset.")
|
||||
ServiceLocator.shared.settings.isIdentifiedForAnalytics = false
|
||||
|
||||
// Stop collecting crash logs
|
||||
// MXLogger.logCrashes(false)
|
||||
}
|
||||
|
||||
/// Flushes the event queue in the analytics client, uploading all pending events.
|
||||
@ -137,33 +96,23 @@ class Analytics {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Identify (pseudonymously) any future events with the ID from the analytics account data settings.
|
||||
/// - Parameter settings: The settings to use for identification. The ID must be set *before* calling this method.
|
||||
private func identify(with settings: AnalyticsSettings) {
|
||||
guard let id = settings.id else {
|
||||
MXLog.error("identify(with:) called before an ID has been generated.")
|
||||
return
|
||||
}
|
||||
|
||||
client.identify(id: id)
|
||||
MXLog.info("Identified.")
|
||||
ServiceLocator.shared.settings.isIdentifiedForAnalytics = true
|
||||
}
|
||||
|
||||
/// Capture an event in the `client`.
|
||||
/// - Parameter event: The event to capture.
|
||||
private func capture(event: AnalyticsEventProtocol) {
|
||||
MXLog.debug("\(event)")
|
||||
client.capture(event)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public tracking methods
|
||||
|
||||
// The following methods are exposed for compatibility with Objective-C as
|
||||
// the `capture` method and the generated events cannot be bridged from Swift.
|
||||
extension Analytics { }
|
||||
|
||||
// MARK: - MXAnalyticsDelegate
|
||||
|
||||
// extension Analytics: MXAnalyticsDelegate {
|
||||
// }
|
||||
extension Analytics {
|
||||
/// Track the presentation of a screen
|
||||
/// - Parameter screen: The screen that was shown
|
||||
/// - Parameter duration: An optional value representing how long the screen was shown for in milliseconds.
|
||||
func track(screen: AnalyticsScreen, duration milliseconds: Int? = nil) {
|
||||
MXLog.debug("\(screen)")
|
||||
let event = AnalyticsEvent.MobileScreen(durationMs: milliseconds, screenName: screen.screenName)
|
||||
client.screen(event)
|
||||
}
|
||||
}
|
||||
|
@ -24,10 +24,6 @@ protocol AnalyticsClientProtocol {
|
||||
/// Starts the analytics client reporting data.
|
||||
func start()
|
||||
|
||||
/// Associate the client with an ID. This is persisted until `reset` is called.
|
||||
/// - Parameter id: The ID to associate with the user.
|
||||
func identify(id: String)
|
||||
|
||||
/// Reset all stored properties and any event queues on the client. Note that
|
||||
/// the client will remain active, but in a fresh unidentified state.
|
||||
func reset()
|
||||
@ -54,3 +50,6 @@ protocol AnalyticsClientProtocol {
|
||||
/// as part of the next event that gets captured.
|
||||
func updateUserProperties(_ userProperties: AnalyticsEvent.UserProperties)
|
||||
}
|
||||
|
||||
// sourcery: AutoMockable
|
||||
extension AnalyticsClientProtocol { }
|
||||
|
150
ElementX/Sources/Services/Analytics/AnalyticsScreen.swift
Normal file
150
ElementX/Sources/Services/Analytics/AnalyticsScreen.swift
Normal file
@ -0,0 +1,150 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import AnalyticsEvents
|
||||
import Foundation
|
||||
|
||||
enum AnalyticsScreen: Int {
|
||||
case welcome
|
||||
case login
|
||||
case register
|
||||
case forgotPassword
|
||||
case sidebar
|
||||
case home
|
||||
case favourites
|
||||
case people
|
||||
case rooms
|
||||
case searchRooms
|
||||
case searchMessages
|
||||
case searchPeople
|
||||
case searchFiles
|
||||
case room
|
||||
case roomPreview
|
||||
case roomDetails
|
||||
case roomMembers
|
||||
case user
|
||||
case roomSearch
|
||||
case roomUploads
|
||||
case roomSettings
|
||||
case roomNotifications
|
||||
case roomDirectory
|
||||
case switchDirectory
|
||||
case startChat
|
||||
case createRoom
|
||||
case settings
|
||||
case settingsSecurity
|
||||
case settingsDefaultNotifications
|
||||
case settingsMentionsAndKeywords
|
||||
case settingsNotifications
|
||||
case deactivateAccount
|
||||
case inviteFriends
|
||||
case threadList
|
||||
case spaceMenu
|
||||
case spaceMembers
|
||||
case spaceExploreRooms
|
||||
case dialpad
|
||||
case spaceBottomSheet
|
||||
case invites
|
||||
case createSpace
|
||||
|
||||
/// The screen name reported to the AnalyticsEvent.
|
||||
var screenName: AnalyticsEvent.MobileScreen.ScreenName {
|
||||
switch self {
|
||||
case .welcome:
|
||||
return .Welcome
|
||||
case .login:
|
||||
return .Login
|
||||
case .register:
|
||||
return .Register
|
||||
case .forgotPassword:
|
||||
return .ForgotPassword
|
||||
case .sidebar:
|
||||
return .Sidebar
|
||||
case .home:
|
||||
return .Home
|
||||
case .favourites:
|
||||
return .Favourites
|
||||
case .people:
|
||||
return .People
|
||||
case .rooms:
|
||||
return .Rooms
|
||||
case .searchRooms:
|
||||
return .SearchRooms
|
||||
case .searchMessages:
|
||||
return .SearchMessages
|
||||
case .searchPeople:
|
||||
return .SearchPeople
|
||||
case .searchFiles:
|
||||
return .SearchFiles
|
||||
case .room:
|
||||
return .Room
|
||||
case .roomDetails:
|
||||
return .RoomDetails
|
||||
case .roomMembers:
|
||||
return .RoomMembers
|
||||
case .user:
|
||||
return .User
|
||||
case .roomPreview:
|
||||
return .RoomPreview
|
||||
case .roomSearch:
|
||||
return .RoomSearch
|
||||
case .roomUploads:
|
||||
return .RoomUploads
|
||||
case .roomSettings:
|
||||
return .RoomSettings
|
||||
case .roomNotifications:
|
||||
return .RoomNotifications
|
||||
case .roomDirectory:
|
||||
return .RoomDirectory
|
||||
case .switchDirectory:
|
||||
return .SwitchDirectory
|
||||
case .startChat:
|
||||
return .StartChat
|
||||
case .createRoom:
|
||||
return .CreateRoom
|
||||
case .settings:
|
||||
return .Settings
|
||||
case .settingsSecurity:
|
||||
return .SettingsSecurity
|
||||
case .settingsDefaultNotifications:
|
||||
return .SettingsDefaultNotifications
|
||||
case .settingsMentionsAndKeywords:
|
||||
return .SettingsMentionsAndKeywords
|
||||
case .settingsNotifications:
|
||||
return .SettingsNotifications
|
||||
case .deactivateAccount:
|
||||
return .DeactivateAccount
|
||||
case .inviteFriends:
|
||||
return .InviteFriends
|
||||
case .threadList:
|
||||
return .ThreadList
|
||||
case .spaceMenu:
|
||||
return .SpaceMenu
|
||||
case .spaceMembers:
|
||||
return .SpaceMembers
|
||||
case .spaceExploreRooms:
|
||||
return .SpaceExploreRooms
|
||||
case .dialpad:
|
||||
return .Dialpad
|
||||
case .spaceBottomSheet:
|
||||
return .SpaceBottomSheet
|
||||
case .invites:
|
||||
return .Invites
|
||||
case .createSpace:
|
||||
return .CreateSpace
|
||||
}
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum AnalyticsServiceError: Error {
|
||||
/// The user session supplied to the service does not have a state of `MXSessionStateRunning`.
|
||||
case sessionIsNotRunning
|
||||
/// The service failed to get or update the analytics settings event from the user's account data.
|
||||
case accountDataFailure
|
||||
}
|
||||
|
||||
/// A service responsible for handling the `im.vector.analytics` event from the user's account data.
|
||||
class AnalyticsService {
|
||||
let userSession: UserSessionProtocol
|
||||
|
||||
/// Creates an analytics service with the supplied user session.
|
||||
/// - Parameter userSession: The user session to use when reading analytics settings from account data.
|
||||
init(userSession: UserSessionProtocol) {
|
||||
self.userSession = userSession
|
||||
}
|
||||
|
||||
/// The analytics settings for the current user. Calling this method will check whether the settings already
|
||||
/// contain an `id` property and if not, will add one to the account data before calling the completion.
|
||||
/// - Parameter completion: A completion handler that will be called when the request completes.
|
||||
///
|
||||
/// The request will fail if the service's session does not have the `MXSessionStateRunning` state.
|
||||
func settings() async -> Result<AnalyticsSettings, AnalyticsServiceError> {
|
||||
// Only use the session if it is running otherwise we could wipe out an existing analytics ID.
|
||||
fatalWithoutUnreachableCodeWarning()
|
||||
// guard userSession.state == .running else {
|
||||
// MXLog.warning("Aborting attempt to read analytics settings. The session may not be up-to-date.")
|
||||
// return .failure(.sessionIsNotRunning)
|
||||
// }
|
||||
|
||||
let result: Result<AnalyticsSettings?, ClientProxyError> = await userSession.clientProxy.accountDataEvent(type: AnalyticsSettings.eventType)
|
||||
switch result {
|
||||
case .failure:
|
||||
return .failure(.accountDataFailure)
|
||||
case .success(let settings):
|
||||
// The id has already be set so we are done here.
|
||||
if let settings, settings.id != nil {
|
||||
return .success(settings)
|
||||
}
|
||||
|
||||
let newSettings = AnalyticsSettings.new(currentEvent: settings)
|
||||
switch await userSession.clientProxy.setAccountData(content: newSettings, type: AnalyticsSettings.eventType) {
|
||||
case .failure:
|
||||
MXLog.error("Failed to update analytics settings.")
|
||||
return .failure(.accountDataFailure)
|
||||
case .success:
|
||||
MXLog.debug("Successfully updated analytics settings in account data.")
|
||||
return .success(newSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Silences a warning on some intentionally unreachable code.
|
||||
func fatalWithoutUnreachableCodeWarning() {
|
||||
fatalError("Missing running state detection.")
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// An analytics settings event from the user's account data.
|
||||
struct AnalyticsSettings: Codable {
|
||||
static let eventType = "im.vector.analytics"
|
||||
|
||||
/// A randomly generated analytics token for this user.
|
||||
/// This is suggested to be a UUID string.
|
||||
let id: String?
|
||||
|
||||
/// Whether the user has opted in on web or not. This is unused on iOS but necessary
|
||||
/// to store here so that it's value is preserved when updating the account data if we
|
||||
/// generated an ID on iOS.
|
||||
///
|
||||
/// `true` if opted in on web, `false` if opted out on web and `nil` if the web prompt is not yet seen.
|
||||
private let webOptIn: Bool?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case webOptIn = "pseudonymousAnalyticsOptIn"
|
||||
}
|
||||
}
|
||||
|
||||
extension AnalyticsSettings {
|
||||
/// Generates a new AnalyticsSettings value (inc an ID if necessary) based upon an
|
||||
/// existing value. This is the only way the type should be created so as to avoid wiping
|
||||
/// out the `webOptIn` value that the user may already have set.
|
||||
///
|
||||
/// **Note:** Please don't pass a `nil` literal to this method.
|
||||
static func new(currentEvent: AnalyticsSettings?) -> AnalyticsSettings {
|
||||
AnalyticsSettings(id: currentEvent?.id ?? UUID().uuidString,
|
||||
webOptIn: currentEvent?.webOptIn)
|
||||
}
|
||||
}
|
@ -38,16 +38,6 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
|
||||
postHog?.enable()
|
||||
}
|
||||
|
||||
func identify(id: String) {
|
||||
if let userProperties = pendingUserProperties {
|
||||
// As user properties overwrite old ones, compactMap the dictionary to avoid resetting any missing properties
|
||||
postHog?.identify(id, properties: userProperties.properties.compactMapValues { $0 })
|
||||
pendingUserProperties = nil
|
||||
} else {
|
||||
postHog?.identify(id)
|
||||
}
|
||||
}
|
||||
|
||||
func reset() {
|
||||
postHog?.reset()
|
||||
pendingUserProperties = nil
|
||||
@ -65,10 +55,12 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
|
||||
}
|
||||
|
||||
func capture(_ event: AnalyticsEventProtocol) {
|
||||
guard isRunning else { return }
|
||||
postHog?.capture(event.eventName, properties: attachUserProperties(to: event.properties))
|
||||
}
|
||||
|
||||
func screen(_ event: AnalyticsScreenProtocol) {
|
||||
guard isRunning else { return }
|
||||
postHog?.screen(event.screenName.rawValue, properties: attachUserProperties(to: event.properties))
|
||||
}
|
||||
|
||||
|
@ -39,13 +39,27 @@ class BugReportService: NSObject, BugReportServiceProtocol {
|
||||
self.session = session
|
||||
super.init()
|
||||
|
||||
// enable SentrySDK
|
||||
// set build version for logger
|
||||
MXLogger.buildVersion = InfoPlistReader.main.bundleShortVersionString
|
||||
}
|
||||
|
||||
// MARK: - BugReportServiceProtocol
|
||||
|
||||
var isRunning: Bool {
|
||||
SentrySDK.isEnabled
|
||||
}
|
||||
|
||||
var crashedLastRun: Bool {
|
||||
SentrySDK.crashedLastRun
|
||||
}
|
||||
|
||||
func start() {
|
||||
guard !isRunning else { return }
|
||||
SentrySDK.start { options in
|
||||
#if DEBUG
|
||||
options.enabled = false
|
||||
#endif
|
||||
|
||||
options.dsn = sentryURL.absoluteString
|
||||
options.dsn = self.sentryURL.absoluteString
|
||||
|
||||
// Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
|
||||
// We recommend adjusting this value in production.
|
||||
@ -61,17 +75,20 @@ class BugReportService: NSObject, BugReportServiceProtocol {
|
||||
self?.lastCrashEventId = event.eventId.sentryIdString
|
||||
}
|
||||
}
|
||||
|
||||
// also enable logging crashes, to send them with bug reports
|
||||
MXLogger.logCrashes(true)
|
||||
// set build version for logger
|
||||
MXLogger.buildVersion = InfoPlistReader.main.bundleShortVersionString
|
||||
MXLog.info("Started.")
|
||||
}
|
||||
|
||||
// MARK: - BugReportServiceProtocol
|
||||
func stop() {
|
||||
guard isRunning else { return }
|
||||
SentrySDK.close()
|
||||
MXLogger.logCrashes(false)
|
||||
MXLog.info("Stopped.")
|
||||
}
|
||||
|
||||
var crashedLastRun: Bool {
|
||||
SentrySDK.crashedLastRun
|
||||
func reset() {
|
||||
lastCrashEventId = nil
|
||||
MXLog.info("Reset.")
|
||||
}
|
||||
|
||||
func crash() {
|
||||
|
@ -33,8 +33,16 @@ struct SubmitBugReportResponse: Decodable {
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol BugReportServiceProtocol {
|
||||
var isRunning: Bool { get }
|
||||
|
||||
var crashedLastRun: Bool { get }
|
||||
|
||||
func start()
|
||||
|
||||
func stop()
|
||||
|
||||
func reset()
|
||||
|
||||
func crash()
|
||||
|
||||
func submitBugReport(_ bugReport: BugReport,
|
||||
|
@ -31,6 +31,8 @@ class UITestsAppCoordinator: AppCoordinatorProtocol {
|
||||
AppSettings.configureWithSuiteName("io.element.elementx.uitests")
|
||||
AppSettings.reset()
|
||||
ServiceLocator.shared.register(appSettings: AppSettings())
|
||||
ServiceLocator.shared.register(bugReportService: BugReportServiceMock())
|
||||
ServiceLocator.shared.register(analytics: Analytics(client: AnalyticsClientMock()))
|
||||
}
|
||||
|
||||
func start() {
|
||||
@ -76,8 +78,12 @@ class MockScreen: Identifiable {
|
||||
userIndicatorController: MockUserIndicatorController(),
|
||||
isModallyPresented: false))
|
||||
case .analyticsPrompt:
|
||||
return AnalyticsPromptCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"),
|
||||
mediaProvider: MockMediaProvider())))
|
||||
return AnalyticsPromptCoordinator()
|
||||
case .analyticsSettingsScreen:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = AnalyticsSettingsScreenCoordinator()
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .authenticationFlow:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = AuthenticationCoordinator(authenticationService: MockAuthenticationServiceProxy(),
|
||||
|
@ -23,6 +23,7 @@ enum UITestsScreenIdentifier: String {
|
||||
case authenticationFlow
|
||||
case softLogout
|
||||
case analyticsPrompt
|
||||
case analyticsSettingsScreen
|
||||
case simpleRegular
|
||||
case simpleUpgrade
|
||||
case home
|
||||
|
@ -6,4 +6,4 @@ output:
|
||||
../../ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
|
||||
args:
|
||||
automMockableTestableImports: []
|
||||
autoMockableImports: [Combine, Foundation, MatrixRustSDK]
|
||||
autoMockableImports: [Combine, Foundation, MatrixRustSDK, AnalyticsEvents]
|
||||
|
26
UITests/Sources/AnalyticsSettingsScreenUITests.swift
Normal file
26
UITests/Sources/AnalyticsSettingsScreenUITests.swift
Normal file
@ -0,0 +1,26 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import ElementX
|
||||
import XCTest
|
||||
|
||||
class AnalyticsSettingsScreenUITests: XCTestCase {
|
||||
/// Verify that the analytics option screen is displayed correctly.
|
||||
func testAnalyticsSettingsScreen() {
|
||||
let app = Application.launch(.analyticsSettingsScreen)
|
||||
app.assertScreenshot(.analyticsSettingsScreen)
|
||||
}
|
||||
}
|
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.analyticsPrompt.png
(Stored with Git LFS)
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.analyticsPrompt.png
(Stored with Git LFS)
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.analyticsSettingsScreen.png
(Stored with Git LFS)
Normal file
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.analyticsSettingsScreen.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.settings.png
(Stored with Git LFS)
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.settings.png
(Stored with Git LFS)
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.analyticsPrompt.png
(Stored with Git LFS)
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.analyticsPrompt.png
(Stored with Git LFS)
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.analyticsSettingsScreen.png
(Stored with Git LFS)
Normal file
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.analyticsSettingsScreen.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.settings.png
(Stored with Git LFS)
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.settings.png
(Stored with Git LFS)
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.analyticsPrompt.png
(Stored with Git LFS)
BIN
UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.analyticsPrompt.png
(Stored with Git LFS)
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.analyticsSettingsScreen.png
(Stored with Git LFS)
Normal file
BIN
UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.analyticsSettingsScreen.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.settings.png
(Stored with Git LFS)
BIN
UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.settings.png
(Stored with Git LFS)
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.analyticsPrompt.png
(Stored with Git LFS)
BIN
UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.analyticsPrompt.png
(Stored with Git LFS)
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.analyticsSettingsScreen.png
(Stored with Git LFS)
Normal file
BIN
UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.analyticsSettingsScreen.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.settings.png
(Stored with Git LFS)
BIN
UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.settings.png
(Stored with Git LFS)
Binary file not shown.
@ -0,0 +1,54 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import ElementX
|
||||
|
||||
@MainActor
|
||||
class AnalyticsSettingsScreenViewModelTests: XCTestCase {
|
||||
private var applicationSettings: AppSettings!
|
||||
private var viewModel: AnalyticsSettingsScreenViewModelProtocol!
|
||||
private var context: AnalyticsSettingsScreenViewModelType.Context!
|
||||
|
||||
@MainActor override func setUpWithError() throws {
|
||||
AppSettings.configureWithSuiteName("io.element.elementx.unitests")
|
||||
AppSettings.reset()
|
||||
applicationSettings = AppSettings()
|
||||
ServiceLocator.shared.register(appSettings: applicationSettings)
|
||||
let analyticsClient = AnalyticsClientMock()
|
||||
analyticsClient.isRunning = false
|
||||
ServiceLocator.shared.register(analytics: Analytics(client: analyticsClient))
|
||||
|
||||
viewModel = AnalyticsSettingsScreenViewModel()
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
func testInitialState() {
|
||||
XCTAssertFalse(context.enableAnalytics)
|
||||
}
|
||||
|
||||
func testOptIn() {
|
||||
context.send(viewAction: .toggleAnalytics)
|
||||
XCTAssertTrue(context.enableAnalytics)
|
||||
}
|
||||
|
||||
func testOptOut() {
|
||||
applicationSettings.enableAnalytics = true
|
||||
context.send(viewAction: .toggleAnalytics)
|
||||
XCTAssertFalse(context.enableAnalytics)
|
||||
}
|
||||
}
|
@ -20,17 +20,26 @@ import XCTest
|
||||
|
||||
class AnalyticsTests: XCTestCase {
|
||||
private var applicationSettings: AppSettings!
|
||||
private var analyticsClient: AnalyticsClientMock!
|
||||
private var bugReportService: BugReportServiceMock!
|
||||
|
||||
override func setUp() {
|
||||
AppSettings.configureWithSuiteName("io.element.elementx.unitests")
|
||||
AppSettings.reset()
|
||||
applicationSettings = AppSettings()
|
||||
ServiceLocator.shared.register(appSettings: applicationSettings)
|
||||
bugReportService = BugReportServiceMock()
|
||||
bugReportService.isRunning = false
|
||||
ServiceLocator.shared.register(bugReportService: bugReportService)
|
||||
analyticsClient = AnalyticsClientMock()
|
||||
analyticsClient.isRunning = false
|
||||
ServiceLocator.shared.register(analytics: Analytics(client: analyticsClient))
|
||||
}
|
||||
|
||||
func testAnalyticsPromptNewUser() {
|
||||
// Given a fresh install of the app (without PostHog analytics having been set).
|
||||
// When the user is prompted for analytics.
|
||||
let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt
|
||||
let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt
|
||||
|
||||
// Then the prompt should be shown.
|
||||
XCTAssertTrue(showPrompt, "A prompt should be shown for a new user.")
|
||||
@ -41,7 +50,7 @@ class AnalyticsTests: XCTestCase {
|
||||
applicationSettings.enableAnalytics = false
|
||||
|
||||
// When the user is prompted for analytics
|
||||
let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt
|
||||
let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt
|
||||
|
||||
// Then no prompt should be shown.
|
||||
XCTAssertFalse(showPrompt, "A prompt should not be shown any more.")
|
||||
@ -52,12 +61,65 @@ class AnalyticsTests: XCTestCase {
|
||||
applicationSettings.enableAnalytics = true
|
||||
|
||||
// When the user is prompted for analytics
|
||||
let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt
|
||||
let showPrompt = ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt
|
||||
|
||||
// Then no prompt should be shown.
|
||||
XCTAssertFalse(showPrompt, "A prompt should not be shown any more.")
|
||||
}
|
||||
|
||||
func testAnalyticsPromptNotDisplayed() {
|
||||
// Given a fresh install of the app both Analytics and BugReportService should be disabled
|
||||
XCTAssertFalse(ServiceLocator.shared.settings.enableAnalytics)
|
||||
XCTAssertFalse(ServiceLocator.shared.analytics.isRunning)
|
||||
XCTAssertFalse(analyticsClient.startCalled)
|
||||
XCTAssertFalse(bugReportService.startCalled)
|
||||
}
|
||||
|
||||
func testAnalyticsOptOut() {
|
||||
// Given a fresh install of the app (without PostHog analytics having been set).
|
||||
// When analytics is opt-out
|
||||
ServiceLocator.shared.analytics.optOut()
|
||||
// Then analytics should be disabled
|
||||
XCTAssertFalse(applicationSettings.enableAnalytics)
|
||||
XCTAssertFalse(ServiceLocator.shared.analytics.isRunning)
|
||||
XCTAssertFalse(analyticsClient.isRunning)
|
||||
XCTAssertFalse(bugReportService.isRunning)
|
||||
// Analytics client and the bug report service should have been stopped
|
||||
XCTAssertTrue(analyticsClient.stopCalled)
|
||||
XCTAssertTrue(bugReportService.stopCalled)
|
||||
}
|
||||
|
||||
func testAnalyticsOptIn() {
|
||||
// Given a fresh install of the app (without PostHog analytics having been set).
|
||||
// When analytics is opt-in
|
||||
ServiceLocator.shared.analytics.optIn()
|
||||
// The analytics should be enabled
|
||||
XCTAssertTrue(applicationSettings.enableAnalytics)
|
||||
// Analytics client and the bug report service should have been started
|
||||
XCTAssertTrue(analyticsClient.startCalled)
|
||||
XCTAssertTrue(bugReportService.startCalled)
|
||||
}
|
||||
|
||||
func testAnalyticsStartIfNotEnabled() {
|
||||
// Given an existing install of the app where the user previously declined the tracking
|
||||
applicationSettings.enableAnalytics = false
|
||||
// Analytics should not start
|
||||
ServiceLocator.shared.analytics.startIfEnabled()
|
||||
XCTAssertFalse(ServiceLocator.shared.settings.enableAnalytics)
|
||||
XCTAssertFalse(analyticsClient.startCalled)
|
||||
XCTAssertFalse(bugReportService.startCalled)
|
||||
}
|
||||
|
||||
func testAnalyticsStartIfEnabled() {
|
||||
// Given an existing install of the app where the user previously accpeted the tracking
|
||||
applicationSettings.enableAnalytics = true
|
||||
// Analytics should start
|
||||
ServiceLocator.shared.analytics.startIfEnabled()
|
||||
XCTAssertTrue(ServiceLocator.shared.settings.enableAnalytics)
|
||||
XCTAssertTrue(analyticsClient.startCalled)
|
||||
XCTAssertTrue(bugReportService.startCalled)
|
||||
}
|
||||
|
||||
func testAddingUserProperties() {
|
||||
// Given a client with no user properties set
|
||||
let client = PostHogAnalyticsClient()
|
||||
@ -120,23 +182,4 @@ class AnalyticsTests: XCTestCase {
|
||||
// Then the properties should be cleared
|
||||
XCTAssertNil(client.pendingUserProperties, "The user properties should be cleared.")
|
||||
}
|
||||
|
||||
func testSendingUserPropertiesWithIdentify() {
|
||||
// Given a client with user properties set
|
||||
let client = PostHogAnalyticsClient()
|
||||
client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging,
|
||||
numFavouriteRooms: nil,
|
||||
numSpaces: nil,
|
||||
allChatsActiveFilter: nil))
|
||||
client.start()
|
||||
|
||||
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
|
||||
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.")
|
||||
|
||||
// When calling identify (tests run under Debug configuration so this is sent to the development instance)
|
||||
client.identify(id: UUID().uuidString)
|
||||
|
||||
// Then the properties should be cleared
|
||||
XCTAssertNil(client.pendingUserProperties, "The user properties should be cleared.")
|
||||
}
|
||||
}
|
||||
|
@ -27,9 +27,14 @@ final class NotificationManagerTests: XCTestCase {
|
||||
private var shouldDisplayInAppNotificationReturnValue = false
|
||||
private var handleInlineReplyDelegateCalled = false
|
||||
private var notificationTappedDelegateCalled = false
|
||||
private let settings = ServiceLocator.shared.settings
|
||||
private var settings: AppSettings!
|
||||
|
||||
override func setUp() {
|
||||
AppSettings.configureWithSuiteName("io.element.elementx.unitests")
|
||||
AppSettings.reset()
|
||||
settings = AppSettings()
|
||||
ServiceLocator.shared.register(appSettings: settings)
|
||||
|
||||
notificationManager = NotificationManager(notificationCenter: notificationCenter)
|
||||
notificationManager.start()
|
||||
notificationManager.setClientProxy(clientProxy)
|
||||
@ -114,7 +119,7 @@ final class NotificationManagerTests: XCTestCase {
|
||||
|
||||
func test_whenStart_requestAuthorizationCalledWithCorrectParams() async throws {
|
||||
notificationManager.requestAuthorization()
|
||||
await Task.yield()
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
XCTAssertEqual(notificationCenter.requestAuthorizationOptions, [.alert, .sound, .badge])
|
||||
}
|
||||
|
||||
|
@ -56,4 +56,15 @@ class SettingsScreenViewModelTests: XCTestCase {
|
||||
await Task.yield()
|
||||
XCTAssert(correctResult)
|
||||
}
|
||||
|
||||
func testAnalytics() async throws {
|
||||
var correctResult = false
|
||||
viewModel.callback = { result in
|
||||
correctResult = result == .analytics
|
||||
}
|
||||
|
||||
context.send(viewAction: .analytics)
|
||||
await Task.yield()
|
||||
XCTAssert(correctResult)
|
||||
}
|
||||
}
|
||||
|
1
changelog.d/106.feature
Normal file
1
changelog.d/106.feature
Normal file
@ -0,0 +1 @@
|
||||
Set up Analytics to track data.
|
Loading…
x
Reference in New Issue
Block a user