mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-22 01:22:46 +02:00
Merge commit '56d9e1f6a55a93dc71149ae429eaa615a98de0d5' into feature/6076_foss_merge
* commit '56d9e1f6a55a93dc71149ae429eaa615a98de0d5': (79 commits) finish version++ version++ Translated using Weblate (Hungarian) Translated using Weblate (Italian) Translated using Weblate (Ukrainian) Translated using Weblate (Hungarian) Translated using Weblate (Slovak) Translated using Weblate (Swedish) Translated using Weblate (Indonesian) Translated using Weblate (Albanian) Translated using Weblate (Estonian) Translated using Weblate (Estonian) updated the submodule updated SDK Update the SDK. (#7819) Prepare for new sprint finish version++ version++ fix Changelog. ... # Conflicts: # Config/AppVersion.xcconfig # Podfile # Podfile.lock # Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved # Riot/Managers/Settings/RiotSettings.swift # Riot/Modules/Analytics/Analytics.swift # Riot/Modules/Analytics/DecryptionFailure.swift # Riot/Modules/Analytics/PHGPostHogConfiguration.swift # Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewAction.swift # Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift # Riot/Modules/Room/Views/Title/Preview/PreviewRoomTitleView.m # Riot/Modules/Settings/SettingsViewController.m # Riot/Utils/EventFormatter.m # Riot/Utils/Tools.m # RiotNSE/target.yml # fastlane/Fastfile # project.yml
This commit is contained in:
@@ -106,6 +106,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.
|
||||
@@ -171,6 +174,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.")
|
||||
@@ -275,7 +285,9 @@ extension Analytics {
|
||||
let userProperties = AnalyticsEvent.UserProperties(allChatsActiveFilter: allChatsActiveFilter?.analyticsName,
|
||||
ftueUseCaseSelection: ftueUseCase?.analyticsName,
|
||||
numFavouriteRooms: numFavouriteRooms,
|
||||
numSpaces: numSpaces)
|
||||
numSpaces: numSpaces,
|
||||
recoveryState: nil,
|
||||
verificationState: nil)
|
||||
#if POSTHOG
|
||||
client.updateUserProperties(userProperties)
|
||||
#endif
|
||||
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
@@ -21,24 +21,34 @@ extension DecryptionFailure {
|
||||
|
||||
public func toAnalyticsEvent() -> AnalyticsEvent.Error {
|
||||
|
||||
let timeToDecryptMillis: Int = if self.timeToDecrypt != nil {
|
||||
Int(self.timeToDecrypt! * 1000)
|
||||
let timeToDecryptMillis: Int = if let ttd = self.timeToDecrypt {
|
||||
Int(ttd * 1000)
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
|
||||
let isHistoricalEvent = if let localAge = self.eventLocalAgeMillis {
|
||||
localAge < 0
|
||||
} else { false }
|
||||
|
||||
let errorName = if isHistoricalEvent && self.trustOwnIdentityAtTimeOfFailure == false {
|
||||
AnalyticsEvent.Error.Name.HistoricalMessage
|
||||
} else {
|
||||
self.reason.errorName
|
||||
}
|
||||
|
||||
return AnalyticsEvent.Error(
|
||||
context: self.context,
|
||||
cryptoModule: .Rust,
|
||||
cryptoSDK: .Rust,
|
||||
domain: .E2EE,
|
||||
|
||||
eventLocalAgeMillis: nil,
|
||||
isFederated: nil,
|
||||
isMatrixDotOrg: nil,
|
||||
name: self.reason.errorName,
|
||||
eventLocalAgeMillis: self.eventLocalAgeMillis,
|
||||
isFederated: self.isFederated,
|
||||
isMatrixDotOrg: self.isMatrixOrg,
|
||||
name: errorName,
|
||||
timeToDecryptMillis: timeToDecryptMillis,
|
||||
userTrustsOwnIdentity: nil,
|
||||
wasVisibleToUser: nil
|
||||
userTrustsOwnIdentity: self.trustOwnIdentityAtTimeOfFailure,
|
||||
wasVisibleToUser: self.wasVisibleToUser
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,13 +52,27 @@ import AnalyticsEvents
|
||||
|
||||
/// UTDs can be permanent or temporary. If temporary, this field will contain the time it took to decrypt the message in milliseconds. If permanent should be nil
|
||||
var timeToDecrypt: TimeInterval?
|
||||
|
||||
init(failedEventId: String, reason: DecryptionFailureReason, context: String, errorCode: NSInteger, deviceCount: NSInteger, unspecifiedErrorMessage: String?) {
|
||||
|
||||
/// Was the current cross-signing identity trusted at the time of decryption
|
||||
var trustOwnIdentityAtTimeOfFailure: Bool?
|
||||
|
||||
var eventLocalAgeMillis: Int?
|
||||
|
||||
/// Is the current user on matrix org
|
||||
var isMatrixOrg: Bool?
|
||||
/// Are the sender and recipient on the same homeserver
|
||||
var isFederated: Bool?
|
||||
|
||||
/// As for now the ios App only reports UTDs visible to user (error are reported from EventFormatter
|
||||
var wasVisibleToUser: Bool = true
|
||||
|
||||
init(failedEventId: String, reason: DecryptionFailureReason, context: String, errorCode: NSInteger, deviceCount: NSInteger, unspecifiedErrorMessage: String?, ts: TimeInterval) {
|
||||
self.failedEventId = failedEventId
|
||||
self.reason = reason
|
||||
self.context = context
|
||||
self.errorCode = errorCode
|
||||
self.deviceCount = deviceCount
|
||||
self.unspecifiedErrorMessage = unspecifiedErrorMessage
|
||||
self.ts = ts
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,14 +63,14 @@ class DecryptionFailureTracker: NSObject {
|
||||
selector: #selector(eventDidDecrypt(_:)),
|
||||
name: .mxEventDidDecrypt,
|
||||
object: nil)
|
||||
|
||||
}
|
||||
|
||||
@objc
|
||||
func reportUnableToDecryptError(forEvent event: MXEvent, withRoomState roomState: MXRoomState, myUser userId: String) {
|
||||
func reportUnableToDecryptError(forEvent event: MXEvent, withRoomState roomState: MXRoomState, mySession: MXSession) {
|
||||
if reportedFailures[event.eventId] != nil || trackedEvents.contains(event.eventId) {
|
||||
return
|
||||
}
|
||||
guard let userId = mySession.myUserId else { return }
|
||||
|
||||
// Filter out "expected" UTDs
|
||||
// We cannot decrypt messages sent before the user joined the room
|
||||
@@ -83,6 +83,12 @@ class DecryptionFailureTracker: NSObject {
|
||||
|
||||
guard let error = event.decryptionError as? NSError else { return }
|
||||
|
||||
let eventOrigin = event.originServerTs
|
||||
let deviceTimestamp = mySession.crypto.deviceCreationTs
|
||||
// If negative it's an historical event relative to the current session
|
||||
let eventRelativeAgeMillis = Int(eventOrigin) - Int(deviceTimestamp)
|
||||
let isSessionVerified = mySession.crypto.crossSigning.canTrustCrossSigning
|
||||
|
||||
var reason = DecryptionFailureReason.unspecified
|
||||
|
||||
if error.code == MXDecryptingErrorUnknownInboundSessionIdCode.rawValue {
|
||||
@@ -93,7 +99,24 @@ class DecryptionFailureTracker: NSObject {
|
||||
|
||||
let context = String(format: "code: %ld, description: %@", error.code, event.decryptionError.localizedDescription)
|
||||
|
||||
reportedFailures[failedEventId] = DecryptionFailure(failedEventId: failedEventId, reason: reason, context: context, ts: self.timeProvider.nowTs())
|
||||
let failure = DecryptionFailure(failedEventId: failedEventId, reason: reason, context: context, ts: self.timeProvider.nowTs())
|
||||
|
||||
failure.eventLocalAgeMillis = Int(exactly: eventRelativeAgeMillis)
|
||||
failure.trustOwnIdentityAtTimeOfFailure = isSessionVerified
|
||||
|
||||
let myDomain = userId.components(separatedBy: ":").last
|
||||
failure.isMatrixOrg = myDomain == "matrix.org"
|
||||
|
||||
if MXTools.isMatrixUserIdentifier(event.sender) {
|
||||
let senderDomain = event.sender.components(separatedBy: ":").last
|
||||
failure.isFederated = senderDomain != nil && senderDomain != myDomain
|
||||
}
|
||||
|
||||
/// XXX for future work, as for now only the event formatter reports UTDs. That means that it's only UTD ~visible to users
|
||||
failure.wasVisibleToUser = true
|
||||
|
||||
reportedFailures[failedEventId] = failure
|
||||
|
||||
|
||||
// Start the ticker if needed. There is no need to have a ticker if no failures are tracked
|
||||
if checkFailuresTimer == nil {
|
||||
|
||||
@@ -16,15 +16,20 @@
|
||||
|
||||
#if POSTHOG
|
||||
import PostHog
|
||||
|
||||
extension PHGPostHogConfiguration {
|
||||
static var standard: PHGPostHogConfiguration? {
|
||||
// BWI: #6076 need to be checked after merge
|
||||
// extension PHGPostHogConfiguration {
|
||||
// static var standard: PHGPostHogConfiguration? {
|
||||
// let analyticsConfiguration = BWIBuildSettings.shared.analyticsConfiguration
|
||||
extension PostHogConfig {
|
||||
static var standard: PostHogConfig? {
|
||||
let analyticsConfiguration = BWIBuildSettings.shared.analyticsConfiguration
|
||||
guard analyticsConfiguration.isEnabled else { return nil }
|
||||
|
||||
let postHogConfiguration = PHGPostHogConfiguration(apiKey: analyticsConfiguration.apiKey, host: analyticsConfiguration.host)
|
||||
postHogConfiguration.shouldSendDeviceID = false
|
||||
|
||||
let postHogConfiguration = PostHogConfig(apiKey: analyticsConfiguration.apiKey, host: analyticsConfiguration.host)
|
||||
// We capture screens manually
|
||||
postHogConfiguration.captureScreenViews = false
|
||||
|
||||
|
||||
return postHogConfiguration
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,31 +20,48 @@ 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: PHGPostHog?
|
||||
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?.enabled ?? false }
|
||||
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 = PHGPostHogConfiguration.standard else { return }
|
||||
guard let configuration = PostHogConfig.standard else { return }
|
||||
|
||||
if postHog == nil {
|
||||
postHog = PHGPostHog(configuration: configuration)
|
||||
postHog = posthogFactory.createPostHog(config: configuration)
|
||||
}
|
||||
|
||||
postHog?.enable()
|
||||
postHog?.optIn()
|
||||
}
|
||||
|
||||
func identify(id: String) {
|
||||
if let userProperties = pendingUserProperties {
|
||||
// As user properties overwrite old ones, compactMap the dictionary to avoid resetting any missing properties
|
||||
postHog?.identify(id, properties: userProperties.properties.compactMapValues { $0 })
|
||||
postHog?.identify(id, userProperties: userProperties.properties.compactMapValues { $0 })
|
||||
pendingUserProperties = nil
|
||||
} else {
|
||||
postHog?.identify(id)
|
||||
@@ -57,10 +74,9 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
|
||||
}
|
||||
|
||||
func stop() {
|
||||
postHog?.disable()
|
||||
postHog?.optOut()
|
||||
|
||||
// As of PostHog 1.4.4, setting the client to nil here doesn't release
|
||||
// it. Keep it around to avoid having multiple instances if the user re-enables
|
||||
self.postHog = nil
|
||||
}
|
||||
|
||||
func flush() {
|
||||
@@ -68,11 +84,13 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
|
||||
}
|
||||
|
||||
func capture(_ event: AnalyticsEventProtocol) {
|
||||
postHog?.capture(event.eventName, properties: attachUserProperties(to: event.properties))
|
||||
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: attachUserProperties(to: event.properties))
|
||||
postHog?.screen(event.screenName.rawValue, properties: attachSuperProperties(to: event.properties))
|
||||
}
|
||||
|
||||
func updateUserProperties(_ userProperties: AnalyticsEvent.UserProperties) {
|
||||
@@ -85,25 +103,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)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
func updateSuperProperties(_ updatedProperties: AnalyticsEvent.SuperProperties) {
|
||||
self.superProperties = AnalyticsEvent.SuperProperties(
|
||||
appPlatform: updatedProperties.appPlatform ?? superProperties?.appPlatform,
|
||||
cryptoSDK: updatedProperties.cryptoSDK ?? superProperties?.cryptoSDK,
|
||||
cryptoSDKVersion: updatedProperties.cryptoSDKVersion ?? superProperties?.cryptoSDKVersion
|
||||
)
|
||||
}
|
||||
|
||||
/// Given a dictionary containing properties from an event, this method will return those properties
|
||||
/// with any pending user properties included under the `$set` key.
|
||||
/// - Parameter properties: A dictionary of properties from an event.
|
||||
/// - Returns: The `properties` dictionary with any user properties included.
|
||||
private func attachUserProperties(to properties: [String: Any]) -> [String: Any] {
|
||||
guard isRunning, let userProperties = pendingUserProperties else { 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
|
||||
|
||||
// As user properties overwrite old ones via $set, compactMap the dictionary to avoid resetting any missing properties
|
||||
properties["$set"] = userProperties.properties.compactMapValues { $0 }
|
||||
pendingUserProperties = nil
|
||||
superProperties.properties.forEach { (key: String, value: Any) in
|
||||
if properties[key] == nil {
|
||||
properties[key] = value
|
||||
}
|
||||
}
|
||||
return properties
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension PostHogAnalyticsClient: RemoteFeaturesClientProtocol {
|
||||
|
||||
@@ -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 { }
|
||||
Reference in New Issue
Block a user