mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 11:58:32 +00:00
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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user