Skip to content

Commit

Permalink
Merge pull request #7801 from element-hq/valere/super_properties
Browse files Browse the repository at this point in the history
Analytics | Add support for super properties and appPlatform
  • Loading branch information
BillCarsonFr authored Jun 5, 2024
2 parents fddd6e2 + fb60f87 commit f38a422
Show file tree
Hide file tree
Showing 10 changed files with 376 additions and 17 deletions.
4 changes: 2 additions & 2 deletions Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/matrix-org/matrix-analytics-events",
"state" : {
"revision" : "44d5a0e898a71f8abbbe12afe9d73e82d370a9a1",
"version" : "0.15.0"
"revision" : "de0cac487e5e7f607ee17045882204c91585461f",
"version" : "0.23.1"
}
},
{
Expand Down
14 changes: 13 additions & 1 deletion Riot/Modules/Analytics/Analytics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ import AnalyticsEvents

guard let session = session else { return }
useAnalyticsSettings(from: session)
client.updateSuperProperties(.init(appPlatform: .EI,
cryptoSDK: .Rust,
cryptoSDKVersion: session.crypto.version))
}

/// Stops analytics tracking and calls `reset` to clear any IDs and event queues.
Expand Down Expand Up @@ -148,6 +151,13 @@ import AnalyticsEvents
switch result {
case .success(let settings):
self.identify(with: settings)
self.client.updateSuperProperties(
AnalyticsEvent.SuperProperties(
appPlatform: .EI,
cryptoSDK: .Rust,
cryptoSDKVersion: session.crypto.version
)
)
self.service = nil
case .failure:
MXLog.error("[Analytics] Failed to use analytics settings. Will continue to run without analytics ID.")
Expand Down Expand Up @@ -242,7 +252,9 @@ extension Analytics {
let userProperties = AnalyticsEvent.UserProperties(allChatsActiveFilter: allChatsActiveFilter?.analyticsName,
ftueUseCaseSelection: ftueUseCase?.analyticsName,
numFavouriteRooms: numFavouriteRooms,
numSpaces: numSpaces)
numSpaces: numSpaces,
recoveryState: nil,
verificationState: nil)
client.updateUserProperties(userProperties)
}

Expand Down
7 changes: 7 additions & 0 deletions Riot/Modules/Analytics/AnalyticsClientProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,11 @@ protocol AnalyticsClientProtocol {
/// be a delay when updating user properties as these are cached to be included
/// as part of the next event that gets captured.
func updateUserProperties(_ userProperties: AnalyticsEvent.UserProperties)


/// Updates the super properties.
/// Super properties added to all captured events and screen.
/// - Parameter superProperties: The properties event to capture.
func updateSuperProperties(_ event: AnalyticsEvent.SuperProperties)

}
56 changes: 49 additions & 7 deletions Riot/Modules/Analytics/PostHogAnalyticsClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,39 @@ import AnalyticsEvents

/// 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: PostHogSDK?
private var postHog: PostHogProtocol?

/// 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?

static let shared = PostHogAnalyticsClient()

var isRunning: Bool { postHog != nil && !postHog!.isOptOut() }
var isRunning: Bool {
guard let postHog else { return false }
return !postHog.isOptOut()
}

func start() {
// Only start if analytics have been configured in BuildSettings
guard let configuration = PostHogConfig.standard else { return }

if postHog == nil {
PostHogSDK.shared.setup(configuration)
postHog = PostHogSDK.shared
postHog = posthogFactory.createPostHog(config: configuration)
}

postHog?.optIn()
Expand Down Expand Up @@ -67,13 +83,13 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
}

func capture(_ event: AnalyticsEventProtocol) {
postHog?.capture(event.eventName, properties: event.properties, userProperties: pendingUserProperties?.properties.compactMapValues { $0 })
postHog?.capture(event.eventName, properties: attachSuperProperties(to: event.properties), userProperties: pendingUserProperties?.properties.compactMapValues { $0 })
// Pending user properties have been added
self.pendingUserProperties = nil
}

func screen(_ event: AnalyticsScreenProtocol) {
postHog?.screen(event.screenName.rawValue, properties: event.properties)
postHog?.screen(event.screenName.rawValue, properties: attachSuperProperties(to: event.properties))
}

func updateUserProperties(_ userProperties: AnalyticsEvent.UserProperties) {
Expand All @@ -86,9 +102,35 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
self.pendingUserProperties = AnalyticsEvent.UserProperties(allChatsActiveFilter: userProperties.allChatsActiveFilter ?? pendingUserProperties.allChatsActiveFilter,
ftueUseCaseSelection: userProperties.ftueUseCaseSelection ?? pendingUserProperties.ftueUseCaseSelection,
numFavouriteRooms: userProperties.numFavouriteRooms ?? pendingUserProperties.numFavouriteRooms,
numSpaces: userProperties.numSpaces ?? pendingUserProperties.numSpaces)
numSpaces: userProperties.numSpaces ?? pendingUserProperties.numSpaces,
// Not yet supported
recoveryState: nil, verificationState: nil)
}

func updateSuperProperties(_ updatedProperties: AnalyticsEvent.SuperProperties) {
self.superProperties = AnalyticsEvent.SuperProperties(
appPlatform: updatedProperties.appPlatform ?? superProperties?.appPlatform,
cryptoSDK: updatedProperties.cryptoSDK ?? superProperties?.cryptoSDK,
cryptoSDKVersion: updatedProperties.cryptoSDKVersion ?? superProperties?.cryptoSDKVersion
)
}

/// 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
}


}

extension PostHogAnalyticsClient: RemoteFeaturesClientProtocol {
Expand Down
53 changes: 53 additions & 0 deletions Riot/Modules/Analytics/PosthogProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// 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

protocol PostHogProtocol {
func optIn()

func optOut()

func reset()

func flush()

func capture(_ event: String, properties: [String: Any]?, userProperties: [String: Any]?)

func screen(_ screenTitle: String, properties: [String: Any]?)

func isFeatureEnabled(_ feature: String) -> Bool

func identify(_ distinctId: String)

func identify(_ distinctId: String, userProperties: [String: Any]?)

func isOptOut() -> Bool
}

protocol PostHogFactory {
func createPostHog(config: PostHogConfig) -> PostHogProtocol
}

class DefaultPostHogFactory: PostHogFactory {
func createPostHog(config: PostHogConfig) -> PostHogProtocol {
PostHogSDK.shared.setup(config)
return PostHogSDK.shared
}
}

extension PostHogSDK: PostHogProtocol { }
12 changes: 6 additions & 6 deletions RiotTests/AnalyticsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class AnalyticsTests: XCTestCase {
XCTAssertNil(client.pendingUserProperties, "No user properties should have been set yet.")

// When updating the user properties
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: 4, numSpaces: 5))
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: 4, numSpaces: 5, recoveryState: nil, verificationState: nil))

// Then the properties should be cached
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
Expand All @@ -90,15 +90,15 @@ class AnalyticsTests: XCTestCase {
func testMergingUserProperties() {
// Given a client with a cached use case user properties
let client = PostHogAnalyticsClient()
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil))
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil, recoveryState: nil, verificationState: nil))

XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.")
XCTAssertNil(client.pendingUserProperties?.numFavouriteRooms, "The number of favorite rooms should not be set.")
XCTAssertNil(client.pendingUserProperties?.numSpaces, "The number of spaces should not be set.")

// When updating the number of spaces
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: nil, numFavouriteRooms: 4, numSpaces: 5))
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: nil, numFavouriteRooms: 4, numSpaces: 5, recoveryState: nil, verificationState: nil))

// Then the new properties should be updated and the existing properties should remain unchanged
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
Expand All @@ -107,7 +107,7 @@ class AnalyticsTests: XCTestCase {
XCTAssertEqual(client.pendingUserProperties?.numSpaces, 5, "The number of spaces should have been updated.")

// When updating the number of spaces
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: .Favourites, ftueUseCaseSelection: nil, numFavouriteRooms: nil, numSpaces: nil))
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: .Favourites, ftueUseCaseSelection: nil, numFavouriteRooms: nil, numSpaces: nil, recoveryState: nil, verificationState: nil))

// Then the new properties should be updated and the existing properties should remain unchanged
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
Expand All @@ -120,7 +120,7 @@ class AnalyticsTests: XCTestCase {
func testSendingUserProperties() {
// Given a client with user properties set
let client = PostHogAnalyticsClient()
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil))
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil, recoveryState: nil, verificationState: nil))
client.start()

XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
Expand All @@ -137,7 +137,7 @@ class AnalyticsTests: XCTestCase {
func testSendingUserPropertiesWithIdentify() {
// Given a client with user properties set
let client = PostHogAnalyticsClient()
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil))
client.updateUserProperties(AnalyticsEvent.UserProperties(allChatsActiveFilter: nil, ftueUseCaseSelection: .PersonalMessaging, numFavouriteRooms: nil, numSpaces: nil, recoveryState: nil, verificationState: nil))
client.start()

XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
Expand Down
Loading

0 comments on commit f38a422

Please sign in to comment.