diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index feb53ea6b..8a2a3ac55 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -461,6 +461,7 @@ 6F26CBC84AE87EB4068D398B /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 78B28D75FF7AF8E6146DEE2A /* LRUCache */; }; 6F2AB43A1EFAD8A97AF41A15 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 9C73F37731C9FDED1BB24C1C /* Collections */; }; 6F2D5D4F2590310DFAE973E4 /* WaitingDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6D698BFD68B061350553930 /* WaitingDialog.swift */; }; + 6F86349BDEAF4495EAE38931 /* PHGPostHogMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2AD8A56CD37E23071A2F4BF /* PHGPostHogMock.swift */; }; 6FC10A00D268FCD48B631E37 /* ViewFrameReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFF7BF82A950B91BC5469E91 /* ViewFrameReader.swift */; }; 6FD8053301C5FEFA82D2F246 /* URLComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BFDCA5A09EE70BC17F2EFA7 /* URLComponents.swift */; }; 6FF51EB400DBA0668FC38B97 /* TimelineStartRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9ED8E731E21055F728E5FED /* TimelineStartRoomTimelineView.swift */; }; @@ -1036,6 +1037,7 @@ F777C6FEE7D106136E2ED2B2 /* MessageForwardingScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6E6EDC4BBF962B2ED595A4 /* MessageForwardingScreenViewModelTests.swift */; }; F78BAD28482A467287A9A5A3 /* EventBasedMessageTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */; }; F7BC744FFA7FE248FAE7F570 /* UserIndicatorToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57C8022B8A871A1DCD1750A /* UserIndicatorToastView.swift */; }; + F7D709D7ECABE46641BB8B6B /* PHGPostHogProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEEAE1BFAACD6C96B6DB731 /* PHGPostHogProtocol.swift */; }; F833D5B5BE6707F961FA88DB /* SecureBackupController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1119E9C63AE530252640D2 /* SecureBackupController.swift */; }; F86102DC2C68BBBB0521BAAE /* SoftLogoutScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB385E148DE55C85C0A02D6 /* SoftLogoutScreenModels.swift */; }; F8C87130FD999F7F1076208C /* RoomChangePermissionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89AAEA70CFF3284920811941 /* RoomChangePermissionsScreen.swift */; }; @@ -1497,6 +1499,7 @@ 5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenCoordinator.swift; sourceTree = ""; }; 5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenCoordinator.swift; sourceTree = ""; }; 5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenCoordinator.swift; sourceTree = ""; }; + 5CEEAE1BFAACD6C96B6DB731 /* PHGPostHogProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogProtocol.swift; sourceTree = ""; }; 5D26A086A8278D39B5756D6F /* project.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = project.yml; sourceTree = ""; }; 5D82F234B3576BD6268C7950 /* ScaledFrameModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaledFrameModifier.swift; sourceTree = ""; }; 5D99730313BEBF08CDE81EE3 /* EmojiDetection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiDetection.swift; sourceTree = ""; }; @@ -1810,6 +1813,7 @@ B172057567E049007A5C4D92 /* Strings+SAS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+SAS.swift"; sourceTree = ""; }; B1E227F34BE43B08E098796E /* TestablePreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestablePreview.swift; sourceTree = ""; }; B251F5B4511D1CA0BA8361FE /* CoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatorProtocol.swift; sourceTree = ""; }; + B2AD8A56CD37E23071A2F4BF /* PHGPostHogMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogMock.swift; sourceTree = ""; }; B2B1DC3B3FB40A7F4AE9B7BF /* RoomRolesAndPermissionsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreen.swift; sourceTree = ""; }; B2B5EDCD05D50BA9B815C66C /* ImageRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineItemContent.swift; sourceTree = ""; }; B2E7C987AE5DC9087BB19F7D /* MediaUploadPreviewScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenModels.swift; sourceTree = ""; }; @@ -2673,6 +2677,7 @@ 3BAC027034248429A438886B /* AppMediatorMock.swift */, E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */, 382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */, + B2AD8A56CD37E23071A2F4BF /* PHGPostHogMock.swift */, D38391154120264910D19528 /* PollMock.swift */, 90B4ED923603F6110D4960C5 /* QRCodeLoginServiceMock.swift */, 894EE8F5B399A165BA2A6634 /* RoomDirectorySearchMock.swift */, @@ -3132,6 +3137,7 @@ 57B6B383F1FD04CC0E7B60C6 /* AnalyticsConsentState.swift */, 5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */, A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */, + 5CEEAE1BFAACD6C96B6DB731 /* PHGPostHogProtocol.swift */, 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */, 752A0EB49BF5BCEA37EDF7A3 /* Signposter.swift */, 3A304097A59704AC9B869EC6 /* Helpers */, @@ -6206,6 +6212,8 @@ AA5924D3B67F7ACD98BBEFDC /* OrientationManagerProtocol.swift in Sources */, 804C15D8ADE0EA7A5268F58A /* OverridableAvatarImage.swift in Sources */, CD6A72B65D3B6076F4045C30 /* PHGPostHogConfiguration.swift in Sources */, + 6F86349BDEAF4495EAE38931 /* PHGPostHogMock.swift in Sources */, + F7D709D7ECABE46641BB8B6B /* PHGPostHogProtocol.swift in Sources */, 847DE3A7EB9FCA2C429C6E85 /* PINTextField.swift in Sources */, 7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */, 764AFCC225B044CF5F9B41E5 /* PaginationIndicatorRoomTimelineView.swift in Sources */, diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index 62d02fa1a..0011b5499 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import AnalyticsEvents import BackgroundTasks import Combine import MatrixRustSDK @@ -313,7 +314,9 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg applicationId: appSettings.bugReportApplicationId, sdkGitSHA: sdkGitSha(), maxUploadSize: appSettings.bugReportMaxUploadSize)) - ServiceLocator.shared.register(analytics: AnalyticsService(client: PostHogAnalyticsClient(), + let posthogAnalyticsClient = PostHogAnalyticsClient() + posthogAnalyticsClient.updateSuperProperties(AnalyticsEvent.SuperProperties(appPlatform: nil, cryptoSDK: .Rust, cryptoSDKVersion: sdkGitSha())) + ServiceLocator.shared.register(analytics: AnalyticsService(client: posthogAnalyticsClient, appSettings: appSettings, bugReportService: ServiceLocator.shared.bugReportService)) } diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index a7ae2e0f1..8d263f498 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 2.2.2 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 2.2.4 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT // swiftlint:disable all @@ -6585,6 +6585,197 @@ class OrientationManagerMock: OrientationManagerProtocol { lockOrientationClosure?(orientation) } } +class PHGPostHogMock: PHGPostHogProtocol { + var enabled: Bool { + get { return underlyingEnabled } + set(value) { underlyingEnabled = value } + } + var underlyingEnabled: Bool! + + //MARK: - enable + + var enableUnderlyingCallsCount = 0 + var enableCallsCount: Int { + get { + if Thread.isMainThread { + return enableUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = enableUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + enableUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + enableUnderlyingCallsCount = newValue + } + } + } + } + var enableCalled: Bool { + return enableCallsCount > 0 + } + var enableClosure: (() -> Void)? + + func enable() { + enableCallsCount += 1 + enableClosure?() + } + //MARK: - disable + + var disableUnderlyingCallsCount = 0 + var disableCallsCount: Int { + get { + if Thread.isMainThread { + return disableUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = disableUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + disableUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + disableUnderlyingCallsCount = newValue + } + } + } + } + var disableCalled: Bool { + return disableCallsCount > 0 + } + var disableClosure: (() -> Void)? + + func disable() { + disableCallsCount += 1 + disableClosure?() + } + //MARK: - reset + + var resetUnderlyingCallsCount = 0 + var resetCallsCount: Int { + get { + if Thread.isMainThread { + return resetUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = resetUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + resetUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + resetUnderlyingCallsCount = newValue + } + } + } + } + var resetCalled: Bool { + return resetCallsCount > 0 + } + var resetClosure: (() -> Void)? + + func reset() { + resetCallsCount += 1 + resetClosure?() + } + //MARK: - capture + + var capturePropertiesUnderlyingCallsCount = 0 + var capturePropertiesCallsCount: Int { + get { + if Thread.isMainThread { + return capturePropertiesUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = capturePropertiesUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + capturePropertiesUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + capturePropertiesUnderlyingCallsCount = newValue + } + } + } + } + var capturePropertiesCalled: Bool { + return capturePropertiesCallsCount > 0 + } + var capturePropertiesReceivedArguments: (event: String, properties: [String: Any]?)? + var capturePropertiesReceivedInvocations: [(event: String, properties: [String: Any]?)] = [] + var capturePropertiesClosure: ((String, [String: Any]?) -> Void)? + + func capture(_ event: String, properties: [String: Any]?) { + capturePropertiesCallsCount += 1 + capturePropertiesReceivedArguments = (event: event, properties: properties) + capturePropertiesReceivedInvocations.append((event: event, properties: properties)) + capturePropertiesClosure?(event, properties) + } + //MARK: - screen + + var screenPropertiesUnderlyingCallsCount = 0 + var screenPropertiesCallsCount: Int { + get { + if Thread.isMainThread { + return screenPropertiesUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = screenPropertiesUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + screenPropertiesUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + screenPropertiesUnderlyingCallsCount = newValue + } + } + } + } + var screenPropertiesCalled: Bool { + return screenPropertiesCallsCount > 0 + } + var screenPropertiesReceivedArguments: (screenTitle: String, properties: [String: Any]?)? + var screenPropertiesReceivedInvocations: [(screenTitle: String, properties: [String: Any]?)] = [] + var screenPropertiesClosure: ((String, [String: Any]?) -> Void)? + + func screen(_ screenTitle: String, properties: [String: Any]?) { + screenPropertiesCallsCount += 1 + screenPropertiesReceivedArguments = (screenTitle: screenTitle, properties: properties) + screenPropertiesReceivedInvocations.append((screenTitle: screenTitle, properties: properties)) + screenPropertiesClosure?(screenTitle, properties) + } +} class PollInteractionHandlerMock: PollInteractionHandlerProtocol { //MARK: - sendPollResponse diff --git a/ElementX/Sources/Mocks/PHGPostHogMock.swift b/ElementX/Sources/Mocks/PHGPostHogMock.swift new file mode 100644 index 000000000..4a637c480 --- /dev/null +++ b/ElementX/Sources/Mocks/PHGPostHogMock.swift @@ -0,0 +1,38 @@ +// +// Copyright 2024 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 +import PostHog + +extension PHGPostHogMock { + func configureMockBehavior() { + enableClosure = { + self.enabled = true + } + } +} + +class MockPostHogFactory: PostHogFactory { + var mock: PHGPostHogProtocol! + + init(mock: PHGPostHogProtocol!) { + self.mock = mock + } + + func createPostHog(config: PHGPostHogConfiguration) -> ElementX.PHGPostHogProtocol { + mock + } +} diff --git a/ElementX/Sources/Services/Analytics/PHGPostHogProtocol.swift b/ElementX/Sources/Services/Analytics/PHGPostHogProtocol.swift new file mode 100644 index 000000000..a567dd79d --- /dev/null +++ b/ElementX/Sources/Services/Analytics/PHGPostHogProtocol.swift @@ -0,0 +1,45 @@ +// +// Copyright 2024 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 +import PostHog + +// sourcery: AutoMockable +protocol PHGPostHogProtocol { + var enabled: Bool { get } + + func enable() + + func disable() + + func reset() + + func capture(_ event: String, properties: [String: Any]?) + + func screen(_ screenTitle: String, properties: [String: Any]?) +} + +protocol PostHogFactory { + func createPostHog(config: PHGPostHogConfiguration) -> PHGPostHogProtocol +} + +class DefaultPostHogFactory: PostHogFactory { + func createPostHog(config: PHGPostHogConfiguration) -> PHGPostHogProtocol { + PHGPostHog(configuration: config) + } +} + +extension PHGPostHog: PHGPostHogProtocol { } diff --git a/ElementX/Sources/Services/Analytics/PostHogAnalyticsClient.swift b/ElementX/Sources/Services/Analytics/PostHogAnalyticsClient.swift index bc1384329..941cdc51c 100644 --- a/ElementX/Sources/Services/Analytics/PostHogAnalyticsClient.swift +++ b/ElementX/Sources/Services/Analytics/PostHogAnalyticsClient.swift @@ -19,12 +19,25 @@ import PostHog /// An analytics client that reports events to a PostHog server. class PostHogAnalyticsClient: AnalyticsClientProtocol { + private var posthogFactory: PostHogFactory = DefaultPostHogFactory() + + init(posthogFactory: PostHogFactory? = nil) { + if let factory = posthogFactory { + self.posthogFactory = factory + } + } + /// The PHGPostHog object used to report events. - private var postHog: PHGPostHog? + private var postHog: PHGPostHogProtocol? /// Any user properties to be included with the next captured event. private(set) var pendingUserProperties: AnalyticsEvent.UserProperties? + /// Super Properties are properties associated with events that are set once and then sent with every capture call, be it a $screen, an autocaptured button click, or anything else. + /// It is different from user properties that will be attached to the user and not events. + /// Not persisted for now, should be set on start. + private var superProperties: AnalyticsEvent.SuperProperties? + var isRunning: Bool { postHog?.enabled ?? false } func start(analyticsConfiguration: AnalyticsConfiguration) { @@ -32,9 +45,11 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol { guard let configuration = PHGPostHogConfiguration.standard(analyticsConfiguration: analyticsConfiguration) else { return } if postHog == nil { - postHog = PHGPostHog(configuration: configuration) + postHog = posthogFactory.createPostHog(config: configuration) } - + // Add super property cryptoSDK to the captured events, to allow easy + // filtering of events across different client by using same filter. + superProperties = AnalyticsEvent.SuperProperties(appPlatform: nil, cryptoSDK: .Rust, cryptoSDKVersion: nil) postHog?.enable() } @@ -52,12 +67,12 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol { func capture(_ event: AnalyticsEventProtocol) { guard isRunning else { return } - postHog?.capture(event.eventName, properties: attachUserProperties(to: event.properties)) + postHog?.capture(event.eventName, properties: attachUserProperties(to: attachSuperProperties(to: event.properties))) } func screen(_ event: AnalyticsScreenProtocol) { guard isRunning else { return } - postHog?.screen(event.screenName.rawValue, properties: attachUserProperties(to: event.properties)) + postHog?.screen(event.screenName.rawValue, properties: attachUserProperties(to: attachSuperProperties(to: event.properties))) } func updateUserProperties(_ userProperties: AnalyticsEvent.UserProperties) { @@ -73,6 +88,20 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol { numSpaces: userProperties.numSpaces ?? pendingUserProperties.numSpaces) } + func updateSuperProperties(_ updatedProperties: AnalyticsEvent.SuperProperties) { + guard let currentProperties = superProperties else { + superProperties = updatedProperties + return + } + + superProperties = AnalyticsEvent.SuperProperties(appPlatform: updatedProperties.appPlatform ?? + currentProperties.appPlatform, + cryptoSDK: updatedProperties.cryptoSDK ?? + currentProperties.cryptoSDK, + cryptoSDKVersion: updatedProperties.cryptoSDKVersion ?? + currentProperties.cryptoSDKVersion) + } + // MARK: - Private /// Given a dictionary containing properties from an event, this method will return those properties @@ -89,4 +118,19 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol { pendingUserProperties = nil return properties } + + /// Attach super properties to events. + /// If the property is already set on the event, the already set value will be kept. + private func attachSuperProperties(to properties: [String: Any]) -> [String: Any] { + guard isRunning, let superProperties else { return properties } + + var properties = properties + + superProperties.properties.forEach { (key: String, value: Any) in + if properties[key] == nil { + properties[key] = value + } + } + return properties + } } diff --git a/UnitTests/Sources/AnalyticsTests.swift b/UnitTests/Sources/AnalyticsTests.swift index 9653c9752..8cdbc19fa 100644 --- a/UnitTests/Sources/AnalyticsTests.swift +++ b/UnitTests/Sources/AnalyticsTests.swift @@ -16,12 +16,14 @@ import AnalyticsEvents @testable import ElementX +import PostHog import XCTest class AnalyticsTests: XCTestCase { private var appSettings: AppSettings! private var analyticsClient: AnalyticsClientMock! private var bugReportService: BugReportServiceMock! + private var posthogMock: PHGPostHogMock! override func setUp() { AppSettings.resetAllSettings() @@ -35,6 +37,9 @@ class AnalyticsTests: XCTestCase { ServiceLocator.shared.register(analytics: AnalyticsService(client: analyticsClient, appSettings: appSettings, bugReportService: ServiceLocator.shared.bugReportService)) + + posthogMock = PHGPostHogMock() + posthogMock.configureMockBehavior() } override func tearDown() { @@ -80,7 +85,7 @@ class AnalyticsTests: XCTestCase { XCTAssertFalse(analyticsClient.startAnalyticsConfigurationCalled) XCTAssertFalse(bugReportService.startCalled) } - + func testAnalyticsOptOut() { // Given a fresh install of the app (without PostHog analytics having been set). // When analytics is opt-out @@ -95,7 +100,7 @@ class AnalyticsTests: XCTestCase { 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 @@ -107,7 +112,7 @@ class AnalyticsTests: XCTestCase { XCTAssertTrue(analyticsClient.startAnalyticsConfigurationCalled) XCTAssertTrue(bugReportService.startCalled) } - + func testAnalyticsStartIfNotEnabled() { // Given an existing install of the app where the user previously declined the tracking appSettings.analyticsConsentState = .optedOut @@ -192,7 +197,7 @@ class AnalyticsTests: XCTestCase { // Given an existing install of the app where the user previously accpeted the tracking appSettings.analyticsConsentState = .optedIn XCTAssertFalse(ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt) - + // When forgetting analytics consents ServiceLocator.shared.analytics.resetConsentState() @@ -200,4 +205,64 @@ class AnalyticsTests: XCTestCase { XCTAssertEqual(appSettings.analyticsConsentState, .unknown) XCTAssertTrue(ServiceLocator.shared.analytics.shouldShowAnalyticsPrompt) } + + func testSendingAndUpdatingSuperProperties() { + // Given a client with user properties set + let client = PostHogAnalyticsClient(posthogFactory: MockPostHogFactory(mock: posthogMock)) + client.start(analyticsConfiguration: appSettings.analyticsConfiguration) + + client.updateSuperProperties( + AnalyticsEvent.SuperProperties(appPlatform: "A thing", + cryptoSDK: .Rust, + cryptoSDKVersion: "000") + ) + + // When sending an event (tests run under Debug configuration so this is sent to the development instance) + client.screen(AnalyticsEvent.MobileScreen(durationMs: nil, screenName: .Home)) + + let screenEvent = posthogMock.screenPropertiesReceivedArguments + + XCTAssertEqual(screenEvent?.screenTitle, AnalyticsEvent.MobileScreen.ScreenName.Home.rawValue) + + // All the super properties should have been added + XCTAssertEqual(screenEvent?.properties?["cryptoSDK"] as? String, AnalyticsEvent.SuperProperties.CryptoSDK.Rust.rawValue) + XCTAssertEqual(screenEvent?.properties?["appPlatform"] as? String, "A thing") + XCTAssertEqual(screenEvent?.properties?["cryptoSDKVersion"] as? String, "000") + + // It should be the same for any event + let someEvent = AnalyticsEvent.Error(context: nil, + cryptoModule: .Rust, + cryptoSDK: .Rust, + domain: .E2EE, + eventLocalAgeMillis: nil, + isFederated: nil, + isMatrixDotOrg: nil, + name: .OlmKeysNotSentError, + timeToDecryptMillis: nil, + userTrustsOwnIdentity: nil, + wasVisibleToUser: nil) + client.capture(someEvent) + + let capturedEvent = posthogMock.capturePropertiesReceivedArguments + + // All the super properties should have been added + XCTAssertEqual(capturedEvent?.properties?["cryptoSDK"] as? String, AnalyticsEvent.SuperProperties.CryptoSDK.Rust.rawValue) + XCTAssertEqual(capturedEvent?.properties?["appPlatform"] as? String, "A thing") + XCTAssertEqual(capturedEvent?.properties?["cryptoSDKVersion"] as? String, "000") + + // Updating should keep the previously set properties + client.updateSuperProperties( + AnalyticsEvent.SuperProperties(appPlatform: nil, + cryptoSDK: nil, + cryptoSDKVersion: "001") + ) + + client.capture(someEvent) + let capturedEvent2 = posthogMock.capturePropertiesReceivedArguments + + // All the super properties should have been added, with the one udpated + XCTAssertEqual(capturedEvent2?.properties?["cryptoSDK"] as? String, AnalyticsEvent.SuperProperties.CryptoSDK.Rust.rawValue) + XCTAssertEqual(capturedEvent2?.properties?["appPlatform"] as? String, "A thing") + XCTAssertEqual(capturedEvent2?.properties?["cryptoSDKVersion"] as? String, "001") + } }