diff --git a/Riot/Managers/Analytics/Analytics.swift b/Riot/Managers/Analytics/Analytics.swift index 16ced46cb..4dd67a5ed 100644 --- a/Riot/Managers/Analytics/Analytics.swift +++ b/Riot/Managers/Analytics/Analytics.swift @@ -89,23 +89,17 @@ import AnalyticsEvents func useAnalyticsSettings(from session: MXSession) { guard RiotSettings.shared.enableAnalytics, - !RiotSettings.shared.isIdentifiedForAnalytics, - session.state == .running // Only use the session if it is running otherwise we could wipe out an existing analytics ID. + !RiotSettings.shared.isIdentifiedForAnalytics else { return } - var settings = AnalyticsSettings(session: session) - - if settings.id == nil { - settings.generateID() - - session.setAccountData(settings.dictionary, forType: AnalyticsSettings.eventType) { - MXLog.debug("[Analytics] Successfully updated analytics settings in account data.") + let service = AnalyticsService(session: session) + service.settings { result in + switch result { + case .success(let settings): self.identify(with: settings) - } failure: { error in - MXLog.error("[Analytics] Failed to update analytics settings.") + case .failure: + MXLog.error("[Analytics] Failed to use analytics settings. Will continue to run without analytics ID.") } - } else { - self.identify(with: settings) } } @@ -135,7 +129,7 @@ import AnalyticsEvents /// - 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.warning("[Analytics] identify(with:) called before an ID has been generated.") + MXLog.error("[Analytics] identify(with:) called before an ID has been generated.") return } diff --git a/Riot/Managers/Analytics/AnalyticsService.swift b/Riot/Managers/Analytics/AnalyticsService.swift new file mode 100644 index 000000000..820102c85 --- /dev/null +++ b/Riot/Managers/Analytics/AnalyticsService.swift @@ -0,0 +1,72 @@ +// +// 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 session supplied to the service does not have a state of `MXSessionStateRunning`. + case sessionIsNotRunning + /// An error occurred but the session did not report what it was. + case unknown +} + +/// A service responsible for handling the `im.vector.analytics` event from the user's account data. +class AnalyticsService { + let session: MXSession + + /// Creates an analytics service with the supplied session. + /// - Parameter session: The session to use when reading analytics settings from account data. + init(session: MXSession) { + self.session = session + } + + /// 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(completion: @escaping (Result) -> Void) { + // Only use the session if it is running otherwise we could wipe out an existing analytics ID. + guard session.state == .running else { + MXLog.warning("[AnalyticsService] Aborting attempt to read analytics settings. The session may not be up-to-date.") + completion(.failure(AnalyticsServiceError.sessionIsNotRunning)) + return + } + + let settings = AnalyticsSettings(accountData: session.accountData) + + // The id has already be set so we are done here. + if settings.id != nil { + completion(.success(settings)) + return + } + + // Create a new ID and modify the event dictionary. + let id = UUID().uuidString + + var eventDictionary = settings.dictionary + eventDictionary[AnalyticsSettings.Constants.idKey] = id + + session.setAccountData(eventDictionary, forType: AnalyticsSettings.eventType) { + MXLog.debug("[AnalyticsService] Successfully updated analytics settings in account data.") + let settings = AnalyticsSettings(accountData: self.session.accountData) + completion(.success(settings)) + } failure: { error in + MXLog.warning("[AnalyticsService] Failed to update analytics settings.") + completion(.failure(error ?? AnalyticsServiceError.unknown)) + } + } +} diff --git a/Riot/Managers/Analytics/AnalyticsSettings.swift b/Riot/Managers/Analytics/AnalyticsSettings.swift index 7d3352548..e847f0668 100644 --- a/Riot/Managers/Analytics/AnalyticsSettings.swift +++ b/Riot/Managers/Analytics/AnalyticsSettings.swift @@ -16,17 +16,18 @@ import Foundation +/// An analytics settings event from the user's account data. struct AnalyticsSettings { static let eventType = "im.vector.analytics" - private enum Constants { + enum Constants { static let idKey = "id" static let webOptInKey = "pseudonymousAnalyticsOptIn" } /// A randomly generated analytics token for this user. - /// This is suggested to be a 128-bit hex encoded string. - private(set) var id: String? + /// 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 @@ -34,12 +35,6 @@ struct AnalyticsSettings { /// /// `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? - - /// Generate a new random analytics ID. This method has no effect if an ID already exists. - mutating func generateID() { - guard id == nil else { return } - id = UUID().uuidString - } } extension AnalyticsSettings { @@ -49,6 +44,7 @@ extension AnalyticsSettings { self.webOptIn = dictionary?[Constants.webOptInKey] as? Bool } + /// A dictionary representation of the settings. var dictionary: Dictionary { var dictionary = [AnyHashable: Any]() dictionary[Constants.idKey] = id @@ -61,7 +57,9 @@ extension AnalyticsSettings { // MARK: - Public initializer extension AnalyticsSettings { - init(session: MXSession) { - self.init(dictionary: session.accountData.accountData(forEventType: AnalyticsSettings.eventType)) + /// Create the analytics settings from account data. + /// - Parameter accountData: The account data to read the event from. + init(accountData: MXAccountData) { + self.init(dictionary: accountData.accountData(forEventType: AnalyticsSettings.eventType)) } }