2021-10-27 13:51:46 +00:00
|
|
|
//
|
|
|
|
// 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 PostHog
|
2021-11-24 18:04:54 +00:00
|
|
|
import AnalyticsEvents
|
2021-10-27 13:51:46 +00:00
|
|
|
|
2021-12-07 12:09:26 +00:00
|
|
|
/// A class responsible for managing an analytics client
|
|
|
|
/// and sending events through this client.
|
2022-01-13 17:23:35 +00:00
|
|
|
///
|
|
|
|
/// ## Creating Analytics Events
|
|
|
|
///
|
|
|
|
/// Events are managed in a shared repo for all Element clients https://github.com/matrix-org/matrix-analytics-events
|
|
|
|
/// To add a new event create a PR to that repo with the new/updated schema.
|
|
|
|
/// Once merged into `main`, follow the steps below to integrate the changes into the project:
|
|
|
|
/// 1. Merge `main` into the `release/swift` branch.
|
|
|
|
/// 2. Run `bundle exec pod update AnalyticsEvents` to update the pod.
|
|
|
|
/// 3. Make sure to commit `Podfile.lock` with the new commit hash.
|
|
|
|
///
|
2021-10-27 13:51:46 +00:00
|
|
|
@objcMembers class Analytics: NSObject {
|
|
|
|
|
|
|
|
// MARK: - Properties
|
|
|
|
|
2021-12-07 12:09:26 +00:00
|
|
|
/// The singleton instance to be used within the Riot target.
|
2021-10-27 13:51:46 +00:00
|
|
|
static let shared = Analytics()
|
|
|
|
|
2021-12-07 12:09:26 +00:00
|
|
|
/// The analytics client to send events with.
|
2021-12-13 17:30:38 +00:00
|
|
|
private var client: AnalyticsClientProtocol = PostHogAnalyticsClient()
|
2021-10-27 13:51:46 +00:00
|
|
|
|
2021-12-16 16:58:29 +00:00
|
|
|
/// The service used to interact with account data settings.
|
|
|
|
private var service: AnalyticsService?
|
|
|
|
|
2021-12-07 12:09:26 +00:00
|
|
|
/// Whether or not the object is enabled and sending events to the server.
|
2021-12-02 16:56:04 +00:00
|
|
|
var isRunning: Bool { client.isRunning }
|
2021-10-27 13:51:46 +00:00
|
|
|
|
2021-12-08 12:00:23 +00:00
|
|
|
/// Whether to show the user the analytics opt in prompt.
|
2021-11-19 10:00:24 +00:00
|
|
|
var shouldShowAnalyticsPrompt: Bool {
|
2021-12-08 12:00:23 +00:00
|
|
|
// Only show the prompt once, and when analytics are configured in BuildSettings.
|
|
|
|
!RiotSettings.shared.hasSeenAnalyticsPrompt && PHGPostHogConfiguration.standard != nil
|
2021-10-27 13:51:46 +00:00
|
|
|
}
|
|
|
|
|
2021-12-07 12:09:26 +00:00
|
|
|
/// Indicates whether the user previously accepted Matomo analytics and should be shown the upgrade prompt.
|
2021-11-19 10:00:24 +00:00
|
|
|
var promptShouldDisplayUpgradeMessage: Bool {
|
|
|
|
RiotSettings.shared.hasAcceptedMatomoAnalytics
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Public
|
|
|
|
|
2021-12-07 12:09:26 +00:00
|
|
|
/// Opts in to analytics tracking with the supplied session.
|
2021-12-13 17:30:38 +00:00
|
|
|
/// - Parameter session: An optional session to use to when reading/generating the analytics ID.
|
|
|
|
/// The session will be ignored if not running.
|
2021-10-27 13:51:46 +00:00
|
|
|
func optIn(with session: MXSession?) {
|
2021-11-19 10:00:24 +00:00
|
|
|
RiotSettings.shared.enableAnalytics = true
|
|
|
|
startIfEnabled()
|
|
|
|
|
2021-12-13 17:30:38 +00:00
|
|
|
guard let session = session else { return }
|
|
|
|
useAnalyticsSettings(from: session)
|
2021-10-27 13:51:46 +00:00
|
|
|
}
|
|
|
|
|
2021-12-13 17:30:38 +00:00
|
|
|
/// Stops analytics tracking and calls `reset` to clear any IDs and event queues.
|
2021-11-19 10:00:24 +00:00
|
|
|
func optOut() {
|
|
|
|
RiotSettings.shared.enableAnalytics = false
|
2021-12-13 17:30:38 +00:00
|
|
|
|
|
|
|
// The order is important here. PostHog ignores the reset if stopped.
|
2021-11-19 10:00:24 +00:00
|
|
|
reset()
|
2021-12-13 17:30:38 +00:00
|
|
|
client.stop()
|
|
|
|
|
|
|
|
MXLog.debug("[Analytics] Stopped.")
|
2021-11-19 10:00:24 +00:00
|
|
|
}
|
|
|
|
|
2021-12-07 12:09:26 +00:00
|
|
|
/// Starts the analytics client if the user has opted in, otherwise does nothing.
|
2021-11-19 10:00:24 +00:00
|
|
|
func startIfEnabled() {
|
|
|
|
guard RiotSettings.shared.enableAnalytics, !isRunning else { return }
|
2021-10-27 13:51:46 +00:00
|
|
|
|
2021-12-02 16:56:04 +00:00
|
|
|
client.start()
|
|
|
|
|
|
|
|
// Sanity check in case something went wrong.
|
|
|
|
guard client.isRunning else { return }
|
2021-11-24 11:07:34 +00:00
|
|
|
|
2021-10-27 13:51:46 +00:00
|
|
|
MXLog.debug("[Analytics] Started.")
|
2021-11-19 16:32:39 +00:00
|
|
|
|
|
|
|
// Catch and log crashes
|
|
|
|
MXLogger.logCrashes(true)
|
|
|
|
MXLogger.setBuildVersion(AppDelegate.theDelegate().build)
|
2021-11-19 10:00:24 +00:00
|
|
|
}
|
|
|
|
|
2021-12-13 17:30:38 +00:00
|
|
|
/// Use the analytics settings from the supplied session to configure analytics.
|
|
|
|
/// For now this is only used for (pseudonymous) identification.
|
|
|
|
/// - Parameter session: The session to read analytics settings from.
|
|
|
|
func useAnalyticsSettings(from session: MXSession) {
|
|
|
|
guard
|
|
|
|
RiotSettings.shared.enableAnalytics,
|
2021-12-16 13:48:14 +00:00
|
|
|
!RiotSettings.shared.isIdentifiedForAnalytics
|
2021-12-13 17:30:38 +00:00
|
|
|
else { return }
|
|
|
|
|
2021-12-16 13:48:14 +00:00
|
|
|
let service = AnalyticsService(session: session)
|
2021-12-16 16:58:29 +00:00
|
|
|
self.service = service
|
|
|
|
|
|
|
|
service.settings { [weak self] result in
|
|
|
|
guard let self = self else { return }
|
|
|
|
|
2021-12-16 13:48:14 +00:00
|
|
|
switch result {
|
|
|
|
case .success(let settings):
|
2021-12-13 17:30:38 +00:00
|
|
|
self.identify(with: settings)
|
2021-12-16 16:58:29 +00:00
|
|
|
self.service = nil
|
2021-12-16 13:48:14 +00:00
|
|
|
case .failure:
|
|
|
|
MXLog.error("[Analytics] Failed to use analytics settings. Will continue to run without analytics ID.")
|
2021-12-16 16:58:29 +00:00
|
|
|
self.service = nil
|
2021-12-13 17:30:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Resets the any IDs and event queues in the analytics client. This method should
|
|
|
|
/// be called on sign-out to maintain opt-in status, whilst ensuring the next
|
2021-12-07 12:09:26 +00:00
|
|
|
/// account used isn't associated with the previous one.
|
2021-12-13 17:30:38 +00:00
|
|
|
/// Note: **MUST** be called before stopping PostHog or the reset is ignored.
|
2021-10-27 13:51:46 +00:00
|
|
|
func reset() {
|
2021-12-02 16:56:04 +00:00
|
|
|
client.reset()
|
2021-12-13 17:30:38 +00:00
|
|
|
MXLog.debug("[Analytics] Reset.")
|
2021-11-19 10:00:24 +00:00
|
|
|
RiotSettings.shared.isIdentifiedForAnalytics = false
|
2021-10-27 13:51:46 +00:00
|
|
|
|
2021-12-02 16:56:04 +00:00
|
|
|
// Stop collecting crash logs
|
2021-11-19 16:32:39 +00:00
|
|
|
MXLogger.logCrashes(false)
|
2021-10-27 13:51:46 +00:00
|
|
|
}
|
|
|
|
|
2021-12-07 12:09:26 +00:00
|
|
|
/// Flushes the event queue in the analytics client, uploading all pending events.
|
|
|
|
/// Normally events are sent in batches. Call this method when you need an event
|
|
|
|
/// to be sent immediately.
|
2021-10-27 13:51:46 +00:00
|
|
|
func forceUpload() {
|
2021-12-02 16:56:04 +00:00
|
|
|
client.flush()
|
2021-10-27 13:51:46 +00:00
|
|
|
}
|
|
|
|
|
2021-12-07 12:09:26 +00:00
|
|
|
// 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 {
|
2021-12-16 13:48:14 +00:00
|
|
|
MXLog.error("[Analytics] identify(with:) called before an ID has been generated.")
|
2021-12-07 12:09:26 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
client.identify(id: id)
|
|
|
|
MXLog.debug("[Analytics] Identified.")
|
|
|
|
RiotSettings.shared.isIdentifiedForAnalytics = true
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Capture an event in the `client`.
|
|
|
|
/// - Parameter event: The event to capture.
|
2021-11-30 12:54:29 +00:00
|
|
|
private func capture(event: AnalyticsEventProtocol) {
|
2021-12-02 16:56:04 +00:00
|
|
|
client.capture(event)
|
2021-10-27 13:51:46 +00:00
|
|
|
}
|
2021-12-07 12:09:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
/// Track the presentation of a screen
|
|
|
|
/// - Parameters:
|
|
|
|
/// - screen: The screen that was shown.
|
|
|
|
/// - milliseconds: An optional value representing how long the screen was shown for in milliseconds.
|
2021-12-02 14:25:45 +00:00
|
|
|
func trackScreen(_ screen: AnalyticsScreen, duration milliseconds: Int?) {
|
|
|
|
let event = AnalyticsEvent.Screen(durationMs: milliseconds, screenName: screen.screenName)
|
2021-12-02 16:56:04 +00:00
|
|
|
client.screen(event)
|
2021-10-27 13:51:46 +00:00
|
|
|
}
|
2021-11-24 10:43:22 +00:00
|
|
|
|
2021-12-15 14:40:45 +00:00
|
|
|
/// The the presentation of a screen without including a duration
|
|
|
|
/// - Parameter screen: The screen that was shown
|
|
|
|
func trackScreen(_ screen: AnalyticsScreen) {
|
|
|
|
trackScreen(screen, duration: nil)
|
|
|
|
}
|
|
|
|
|
2021-12-08 12:49:57 +00:00
|
|
|
/// Track an element that has been tapped
|
|
|
|
/// - Parameters:
|
|
|
|
/// - tap: The element that was tapped
|
|
|
|
/// - index: The index of the element, if it's in a list of elements
|
2021-12-15 14:40:45 +00:00
|
|
|
func trackTap(_ tap: AnalyticsUIElement, index: Int?) {
|
2021-12-08 12:49:57 +00:00
|
|
|
let event = AnalyticsEvent.Click(index: index, name: tap.elementName)
|
|
|
|
client.capture(event)
|
|
|
|
}
|
|
|
|
|
2021-12-15 14:40:45 +00:00
|
|
|
/// Track an element that has been tapped without including an index
|
|
|
|
/// - Parameters:
|
|
|
|
/// - tap: The element that was tapped
|
|
|
|
func trackTap(_ tap: AnalyticsUIElement) {
|
|
|
|
trackTap(tap, index: nil)
|
|
|
|
}
|
|
|
|
|
2021-12-07 12:09:26 +00:00
|
|
|
/// Track an E2EE error that occurred
|
|
|
|
/// - Parameters:
|
|
|
|
/// - reason: The error that occurred.
|
|
|
|
/// - count: The number of times that error occurred.
|
2021-11-24 10:43:22 +00:00
|
|
|
func trackE2EEError(_ reason: DecryptionFailureReason, count: Int) {
|
|
|
|
for _ in 0..<count {
|
2021-11-30 12:54:29 +00:00
|
|
|
let event = AnalyticsEvent.Error(context: nil, domain: .E2EE, name: reason.errorName)
|
|
|
|
capture(event: event)
|
2021-11-24 10:43:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-07 12:09:26 +00:00
|
|
|
/// Track whether the user accepted or declined the terms to an identity server.
|
|
|
|
/// **Note** This method isn't currently implemented.
|
2021-12-07 12:29:02 +00:00
|
|
|
/// - Parameter accepted: Whether the terms were accepted.
|
|
|
|
func trackIdentityServerAccepted(_ accepted: Bool) {
|
|
|
|
// Do we still want to track this?
|
|
|
|
}
|
2021-10-27 13:51:46 +00:00
|
|
|
}
|
|
|
|
|
2021-11-24 10:43:22 +00:00
|
|
|
// MARK: - MXAnalyticsDelegate
|
2021-10-27 13:51:46 +00:00
|
|
|
extension Analytics: MXAnalyticsDelegate {
|
2021-12-02 14:25:45 +00:00
|
|
|
func trackDuration(_ milliseconds: Int, name: MXTaskProfileName, units: UInt) {
|
|
|
|
guard let analyticsName = name.analyticsName else {
|
|
|
|
MXLog.warning("[Analytics] Attempt to capture unknown profile task: \(name.rawValue)")
|
|
|
|
return
|
2021-11-30 12:54:29 +00:00
|
|
|
}
|
2021-12-02 14:25:45 +00:00
|
|
|
|
|
|
|
let event = AnalyticsEvent.PerformanceTimer(context: nil, itemCount: Int(units), name: analyticsName, timeMs: milliseconds)
|
|
|
|
capture(event: event)
|
2021-11-30 12:54:29 +00:00
|
|
|
}
|
2021-11-24 10:43:22 +00:00
|
|
|
|
2021-11-25 18:28:32 +00:00
|
|
|
func trackCallStarted(withVideo isVideo: Bool, numberOfParticipants: Int, incoming isIncoming: Bool) {
|
2021-11-30 12:54:29 +00:00
|
|
|
let event = AnalyticsEvent.CallStarted(isVideo: isVideo, numParticipants: numberOfParticipants, placed: !isIncoming)
|
|
|
|
capture(event: event)
|
2021-11-24 10:43:22 +00:00
|
|
|
}
|
|
|
|
|
2021-11-25 18:28:32 +00:00
|
|
|
func trackCallEnded(withDuration duration: Int, video isVideo: Bool, numberOfParticipants: Int, incoming isIncoming: Bool) {
|
2021-11-30 12:54:29 +00:00
|
|
|
let event = AnalyticsEvent.CallEnded(durationMs: duration, isVideo: isVideo, numParticipants: numberOfParticipants, placed: !isIncoming)
|
|
|
|
capture(event: event)
|
2021-11-24 10:43:22 +00:00
|
|
|
}
|
|
|
|
|
2021-11-25 18:28:32 +00:00
|
|
|
func trackCallError(with reason: __MXCallHangupReason, video isVideo: Bool, numberOfParticipants: Int, incoming isIncoming: Bool) {
|
2021-11-30 12:54:29 +00:00
|
|
|
let callEvent = AnalyticsEvent.CallError(isVideo: isVideo, numParticipants: numberOfParticipants, placed: !isIncoming)
|
|
|
|
let event = AnalyticsEvent.Error(context: nil, domain: .VOIP, name: reason.errorName)
|
|
|
|
capture(event: callEvent)
|
|
|
|
capture(event: event)
|
2021-10-27 13:51:46 +00:00
|
|
|
}
|
|
|
|
|
2021-12-02 14:25:45 +00:00
|
|
|
func trackCreatedRoom(asDM isDM: Bool) {
|
|
|
|
let event = AnalyticsEvent.CreatedRoom(isDM: isDM)
|
|
|
|
capture(event: event)
|
|
|
|
}
|
|
|
|
|
|
|
|
func trackJoinedRoom(asDM isDM: Bool, memberCount: UInt) {
|
|
|
|
guard let roomSize = AnalyticsEvent.JoinedRoom.RoomSize(memberCount: memberCount) else {
|
|
|
|
MXLog.warning("[Analytics] Attempt to capture joined room with invalid member count: \(memberCount)")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let event = AnalyticsEvent.JoinedRoom(isDM: isDM, roomSize: roomSize)
|
|
|
|
capture(event: event)
|
|
|
|
}
|
2021-12-07 15:08:48 +00:00
|
|
|
|
|
|
|
/// **Note** This method isn't currently implemented.
|
|
|
|
func trackContactsAccessGranted(_ granted: Bool) {
|
|
|
|
// Do we still want to track this?
|
|
|
|
}
|
2021-10-27 13:51:46 +00:00
|
|
|
}
|