Support for posthog super properties (#2774)

* Support for posthog super properties

* Use typed SuperProperties and add tests

* update superProperty in AppCoordinator
This commit is contained in:
Valere
2024-05-02 17:21:09 +02:00
committed by GitHub
parent e511ac3cb2
commit ec19b5a0c4
7 changed files with 405 additions and 11 deletions
@@ -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))
}
@@ -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
@@ -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
}
}
@@ -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 { }
@@ -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
}
}