Set up Analytics to track data per session (#780)

This commit is contained in:
Nicolas Mauri
2023-04-18 09:33:32 +02:00
committed by GitHub
parent d01349a60e
commit 5b7ec6c9e2
58 changed files with 953 additions and 375 deletions
@@ -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,14 +68,11 @@ 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() {
let params = BugReportCoordinatorParameters(bugReportService: parameters.bugReportService,
userID: parameters.userSession.userID,
@@ -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")
}