Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Analytics | Add support for super properties and appPlatform #7801

Merged
merged 6 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
17 changes: 16 additions & 1 deletion Riot/Modules/Analytics/Analytics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ import AnalyticsEvents

guard let session = session else { return }
useAnalyticsSettings(from: session)
self.client.updateSuperProperties(
AnalyticsEvent.SuperProperties(
appPlatform: .EI,
cryptoSDK: .Rust,
cryptoSDKVersion: session.crypto.version
)
)
BillCarsonFr marked this conversation as resolved.
Show resolved Hide resolved
}

/// Stops analytics tracking and calls `reset` to clear any IDs and event queues.
Expand Down Expand Up @@ -148,6 +155,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 +256,8 @@ extension Analytics {
let userProperties = AnalyticsEvent.UserProperties(allChatsActiveFilter: allChatsActiveFilter?.analyticsName,
ftueUseCaseSelection: ftueUseCase?.analyticsName,
numFavouriteRooms: numFavouriteRooms,
numSpaces: numSpaces)
numSpaces: numSpaces,
recoveryState: nil, verificationState: nil)
BillCarsonFr marked this conversation as resolved.
Show resolved Hide resolved
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 user properties.
BillCarsonFr marked this conversation as resolved.
Show resolved Hide resolved
/// 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()
}
Comment on lines -30 to +47
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks much more Swifty 👌


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
Loading