Clean up the Signposter implementation and align on new metrics

This commit is contained in:
Stefan Ceriu
2026-01-28 11:59:05 +02:00
committed by Stefan Ceriu
parent 8b768f74f8
commit a2a8527ffc
10 changed files with 159 additions and 145 deletions
@@ -76,6 +76,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
appMediator = AppMediator(windowManager: windowManager, networkMonitor: networkMonitor)
let appSettings = appHooks.appSettingsHook.configure(AppSettings())
ServiceLocator.shared.register(appSettings: appSettings)
targetConfiguration = Target.mainApp.configure(logLevel: appSettings.logLevel,
traceLogPacks: appSettings.traceLogPacks,
@@ -95,8 +96,16 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
self.appDelegate = appDelegate
self.appSettings = appSettings
self.appHooks = appHooks
appRouteURLParser = AppRouteURLParser(appSettings: appSettings)
ServiceLocator.shared.register(userIndicatorController: UserIndicatorController())
let posthogAnalyticsClient = PostHogAnalyticsClient()
posthogAnalyticsClient.updateSuperProperties(AnalyticsEvent.SuperProperties(appPlatform: .EXI, cryptoSDK: .Rust, cryptoSDKVersion: sdkGitSha()))
let analyticsService = AnalyticsService(client: posthogAnalyticsClient, appSettings: appSettings)
ServiceLocator.shared.register(analytics: analyticsService)
elementCallService = ElementCallService()
navigationRootCoordinator = NavigationRootCoordinator()
@@ -107,7 +116,11 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
let keychainController = KeychainController(service: .sessions,
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
userSessionStore = UserSessionStore(keychainController: keychainController, appSettings: appSettings, appHooks: appHooks, networkMonitor: networkMonitor)
userSessionStore = UserSessionStore(keychainController: keychainController,
appSettings: appSettings,
analyticsService: analyticsService,
appHooks: appHooks,
networkMonitor: networkMonitor)
let appLockService = AppLockService(keychainController: keychainController, appSettings: appSettings)
let appLockNavigationCoordinator = NavigationRootCoordinator()
@@ -123,11 +136,10 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
sdkGitSHA: sdkGitSha(),
maxUploadSize: appSettings.bugReportMaxUploadSize,
appHooks: appHooks)
Self.setupServiceLocator(appSettings: appSettings, appHooks: appHooks)
Self.setupSentry(bugReportService: bugReportService, appSettings: appSettings)
ServiceLocator.shared.analytics.signpost.start()
ServiceLocator.shared.analytics.startIfEnabled()
analyticsService.startIfEnabled()
windowManager.delegate = self
@@ -356,6 +368,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
} else if appSettings.threadsEnabled, let threadRootEventID = content.threadRootEventID {
handleAppRoute(.thread(roomID: roomID, threadRootEventID: threadRootEventID, focusEventID: eventID))
} else if let eventID {
// Only track main timeline event deeplinking
ServiceLocator.shared.analytics.signpost.startTransaction(.notificationToMessage)
handleAppRoute(.event(eventID: eventID, roomID: roomID, via: []))
} else {
handleAppRoute(.room(roomID: roomID, via: []))
@@ -380,16 +394,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
// MARK: - Private
private static func setupServiceLocator(appSettings: AppSettings, appHooks: AppHooks) {
ServiceLocator.shared.register(userIndicatorController: UserIndicatorController())
ServiceLocator.shared.register(appSettings: appSettings)
let posthogAnalyticsClient = PostHogAnalyticsClient()
posthogAnalyticsClient.updateSuperProperties(AnalyticsEvent.SuperProperties(appPlatform: .EXI, cryptoSDK: .Rust, cryptoSDKVersion: sdkGitSha()))
ServiceLocator.shared.register(analytics: AnalyticsService(client: posthogAnalyticsClient,
appSettings: appSettings))
}
/// Perform any required migrations for the app to function correctly.
private func performMigrationsIfNecessary(from oldVersion: Version, to newVersion: Version) {
guard oldVersion != newVersion else { return }
@@ -693,6 +697,14 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
fatalError("User session not setup")
}
if let serverName = userSession.clientProxy.userIDServerName {
ServiceLocator.shared.analytics.signpost.addGlobalTag(.homeserver, value: serverName)
}
if !isNewLogin {
ServiceLocator.shared.analytics.signpost.startTransaction(.cachedRoomList)
}
let flowParameters = CommonFlowParameters(userSession: userSession,
bugReportService: bugReportService,
elementCallService: elementCallService,
@@ -1061,9 +1073,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
private func startSync() {
guard let userSession else { return }
let serverName = String(userSession.clientProxy.userIDServerName ?? "Unknown")
ServiceLocator.shared.analytics.signpost.startTransaction(.upToDateRoomList)
ServiceLocator.shared.analytics.signpost.beginFirstSync(serverName: serverName)
userSession.clientProxy.startSync()
guard clientProxyObserver == nil else {
@@ -1072,6 +1083,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
clientProxyObserver = userSession.clientProxy
.loadingStatePublisher
.dropFirst()
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] state in
@@ -1084,7 +1096,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
ServiceLocator.shared.userIndicatorController.submitIndicator(.init(id: toastIdentifier, type: .toast(progress: .indeterminate), title: L10n.commonSyncing, persistent: true))
}
case .notLoading:
ServiceLocator.shared.analytics.signpost.endFirstSync()
ServiceLocator.shared.analytics.signpost.finishTransaction(.upToDateRoomList)
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(toastIdentifier)
}
}
@@ -111,8 +111,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
self.flowParameters = flowParameters
setupStateMachine()
flowParameters.analytics.signpost.beginRoomFlow(roomID)
}
// MARK: - FlowCoordinatorProtocol
@@ -129,6 +127,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
switch appRoute {
case .room(let roomID, let via):
flowParameters.analytics.signpost.startTransaction(.openRoom)
Task {
await handleRoomRoute(roomID: roomID, via: via, animated: animated)
}
@@ -897,7 +896,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} else {
actionsSubject.send(.finished)
}
flowParameters.analytics.signpost.endRoomFlow()
}
private func presentRoomDetails(isRoot: Bool, animated: Bool) async {
+6
View File
@@ -87,6 +87,12 @@ extension SDKListener: SendQueueRoomErrorListener where T == (String, ClientErro
}
}
extension SDKListener: SendQueueRoomUpdateListener where T == (String, RoomSendQueueUpdate) {
func onUpdate(roomId: String, update: RoomSendQueueUpdate) {
onUpdateClosure((roomId, update))
}
}
// MARK: SecureBackupController
extension SDKListener: BackupStateListener where T == BackupState {
@@ -95,18 +95,15 @@ class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtoc
startLoading(isInteractionBlocking: true)
Task {
analytics.signpost.beginLogin()
switch await authenticationService.login(username: state.bindings.username,
password: state.bindings.password,
initialDeviceName: UIDevice.current.initialDeviceName,
deviceID: nil) {
case .success(let userSession):
actionsSubject.send(.signedIn(userSession))
analytics.signpost.endLogin()
stopLoading()
case .failure(let error):
stopLoading()
analytics.signpost.endLogin()
handleError(error)
}
}
@@ -292,8 +292,6 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
return
}
analyticsService.signpost.beginFirstRooms()
roomSummaryProvider.statePublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] state in
@@ -329,7 +327,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
}
if roomListMode == .rooms, state.roomListMode == .skeletons {
analyticsService.signpost.endFirstRooms()
analyticsService.signpost.finishTransaction(.cachedRoomList)
}
state.roomListMode = roomListMode
@@ -256,7 +256,9 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
}
showFocusLoadingIndicator()
defer { hideFocusLoadingIndicator() }
defer {
hideFocusLoadingIndicator()
}
switch await timelineController.focusOnEvent(eventID, timelineSize: Constants.detachedTimelineSize) {
case .success:
@@ -318,6 +320,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
focussedEvent.appearance = .hasAppeared
state.timelineState.focussedEvent = focussedEvent
hideFocusLoadingIndicator()
analyticsService.signpost.finishTransaction(.notificationToMessage)
}
}
@@ -427,6 +430,10 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
switch callback {
case .updatedTimelineItems(let updatedItems, let isSwitchingTimelines):
buildTimelineViews(timelineItems: updatedItems, isSwitchingTimelines: isSwitchingTimelines)
if !updatedItems.isEmpty {
analyticsService.signpost.finishTransaction(.openRoom)
}
case .paginationState(let paginationState):
if state.timelineState.paginationState != paginationState {
state.timelineState.paginationState = paginationState
@@ -6,140 +6,106 @@
// Please see LICENSE files in the repository root for full details.
//
import OSLog
import CryptoKit
import Sentry
/// A simple wrapper around OSSignposter for easy performance testing.
/// A simple wrapper around Sentry for easy instrumentation
class Signposter {
/// The underlying signposter.
private let signposter = OSSignposter(subsystem: subsystem, category: category)
/// A logger instance to capture any errors.
private let logger = Logger(subsystem: subsystem, category: category)
private var transactions = [TransactionName: any Sentry.Span]()
/// Signpost name constants.
enum Name {
static let login: StaticString = "Login"
static let firstSync: StaticString = "FirstSync"
static let firstRooms: StaticString = "FirstRooms"
static let roomFlow: StaticString = "RoomFlow"
private var globalTags = [TagName: String]()
static let appStartup = "AppStartup"
static let appStarted = "AppStarted"
enum TransactionName: Hashable {
case cachedRoomList
case upToDateRoomList
case notificationToMessage
case openRoom
case sendMessage(uuid: String)
static let homeserver = "homeserver"
var id: String {
switch self {
case .cachedRoomList:
"Cached room list"
case .upToDateRoomList:
"Up-to-date room list"
case .notificationToMessage:
"Notification to message"
case .openRoom:
"Open a room"
case .sendMessage:
"Send a message"
}
}
}
static let subsystem = "ElementX"
static let category = "PerformanceTests"
// MARK: - App Startup
private var appStartupTransaction: Span?
/// We have a manual start method because we need to configure the ServiceLocator *before* we configure
/// Sentry but this class is created in the AnalyticsService and so spans and transactions are dropped
/// until Sentry has been configured. Therefore doing this in the init doesn't work.
func start() {
appStartupTransaction = SentrySDK.startTransaction(name: Name.appStartup, operation: Name.appStarted)
enum SpanName: String {
case timelineLoad = "Timeline load"
}
// MARK: - Login
struct Span {
fileprivate let innerSpan: Sentry.Span
private var loginState: OSSignpostIntervalState?
private var loginTransaction: Span?
private var loginSpan: Span?
func beginLogin() {
loginState = signposter.beginInterval(Name.login)
loginTransaction = SentrySDK.startTransaction(name: "\(Name.login)", operation: "\(Name.login)")
loginSpan = appStartupTransaction?.startChild(operation: "\(Name.login)", description: "\(Name.login)")
func finish() {
innerSpan.finish()
}
}
func endLogin() {
guard let loginState else {
logger.error("Missing login state.")
return
enum TagName: String {
case homeserver = "Homeserver"
}
// MARK: - Transactions
func startTransaction(_ transactionName: TransactionName, operation: String = "ux", tags: [TagName: String] = [:]) {
let span = SentrySDK.startTransaction(name: transactionName.id, operation: operation)
tags
.merging(globalTags) { tagValue, _ in
tagValue
}
.forEach { (key: TagName, value: String) in
span.setTag(value: value, key: key.rawValue)
}
transactions[transactionName] = span
}
func finishTransaction(_ transactionName: TransactionName) {
transactions[transactionName]?.finish()
transactions[transactionName] = nil
}
// MARK: - Spans
func addSpan(_ spanName: SpanName, toTransaction transactionName: TransactionName) -> Span? {
guard let transaction = transactions[transactionName] else {
MXLog.error("Transaction not started or already finished")
return nil
}
signposter.endInterval(Name.login, loginState)
loginTransaction?.finish()
loginSpan?.finish()
self.loginState = nil
loginTransaction = nil
loginSpan = nil
return Span(innerSpan: transaction.startChild(operation: spanName.rawValue))
}
// MARK: - FirstSync
// MARK: - Tags
private var firstSyncState: OSSignpostIntervalState?
private var firstSyncTransaction: Span?
private var firstSyncSpan: Span?
func beginFirstSync(serverName: String) {
appStartupTransaction?.setTag(value: serverName, key: Name.homeserver)
firstSyncState = signposter.beginInterval(Name.firstSync)
firstSyncTransaction = SentrySDK.startTransaction(name: "\(Name.firstSync)", operation: "\(Name.firstSync)")
firstSyncTransaction?.setTag(value: serverName, key: Name.homeserver)
firstSyncSpan = appStartupTransaction?.startChild(operation: "\(Name.firstSync)", description: "\(Name.firstSync)")
}
func endFirstSync() {
guard let firstSyncState else { return }
signposter.endInterval(Name.firstSync, firstSyncState)
firstSyncTransaction?.finish()
firstSyncSpan?.finish()
self.firstSyncState = nil
firstSyncTransaction = nil
firstSyncSpan = nil
}
// MARK: - FirstRooms
private var firstRoomsState: OSSignpostIntervalState?
private var firstRoomsTransaction: Span?
private var firstRoomsSpan: Span?
func beginFirstRooms() {
firstRoomsState = signposter.beginInterval(Name.firstRooms)
firstRoomsTransaction = SentrySDK.startTransaction(name: "\(Name.firstRooms)", operation: "\(Name.firstRooms)")
firstRoomsSpan = appStartupTransaction?.startChild(operation: "\(Name.firstRooms)", description: "\(Name.firstRooms)")
}
func endFirstRooms() {
defer {
appStartupTransaction?.finish()
func addGlobalTag(_ tagName: TagName, value: String) {
let value = switch tagName {
case .homeserver:
sha512(value)
}
guard let firstRoomsState else { return }
signposter.endInterval(Name.firstRooms, firstRoomsState)
firstRoomsTransaction?.finish()
firstRoomsSpan?.finish()
self.firstRoomsState = nil
firstRoomsTransaction = nil
firstRoomsSpan = nil
globalTags[tagName] = value
}
// MARK: - Room Flow
private var roomFlowState: OSSignpostIntervalState?
func beginRoomFlow(_ name: String) {
roomFlowState = signposter.beginInterval(Name.roomFlow)
signposter.emitEvent("RoomName", "\(name, privacy: .auto(mask: .hash))")
func removeGlobalTag(_ tagName: TagName) {
globalTags[tagName] = nil
}
func endRoomFlow() {
guard let roomFlowState else { return }
// MARK: - Private
signposter.endInterval(Name.roomFlow, roomFlowState)
self.roomFlowState = nil
func sha512(_ string: String) -> String {
let data = Data(string.utf8)
let hash = SHA512.hash(data: data)
return hash.map { String(format: "%02x", $0) }.joined()
}
}
@@ -12,10 +12,12 @@ import Foundation
import MatrixRustSDK
import OrderedCollections
// swiftlint:disable:next type_body_length
class ClientProxy: ClientProxyProtocol {
private let client: ClientProtocol
private let networkMonitor: NetworkMonitorProtocol
private let appSettings: AppSettings
private let analyticsService: AnalyticsService
let mediaLoader: MediaLoaderProtocol
private let clientQueue: DispatchQueue
@@ -37,7 +39,10 @@ class ClientProxy: ClientProxyProtocol {
private var verificationStateListenerTaskHandle: TaskHandle?
// periphery:ignore - required for instance retention in the rust codebase
private var sendQueueListenerTaskHandle: TaskHandle?
private var sendQueueStatusListenerTaskHandle: TaskHandle?
// periphery:ignore - required for instance retention in the rust codebase
private var sendQueueUpdatesListenerTaskHandle: TaskHandle?
// periphery:ignore - required for instance retention in the rust codebase
private var mediaPreviewConfigListenerTaskHandle: TaskHandle?
@@ -184,10 +189,12 @@ class ClientProxy: ClientProxyProtocol {
init(client: ClientProtocol,
networkMonitor: NetworkMonitorProtocol,
appSettings: AppSettings) async throws {
appSettings: AppSettings,
analyticsService: AnalyticsService) async throws {
self.client = client
self.networkMonitor = networkMonitor
self.appSettings = appSettings
self.analyticsService = analyticsService
clientQueue = .init(label: "ClientProxyQueue", attributes: .concurrent)
@@ -243,11 +250,22 @@ class ClientProxy: ClientProxyProtocol {
Task { await self?.updateVerificationState(verificationState) }
})
sendQueueListenerTaskHandle = client.subscribeToSendQueueStatus(listener: SDKListener { [weak self] roomID, error in
sendQueueStatusListenerTaskHandle = client.subscribeToSendQueueStatus(listener: SDKListener { [weak self] roomID, error in
MXLog.error("Send queue failed in room: \(roomID) with error: \(error)")
self?.sendQueueStatusSubject.send(false)
})
sendQueueUpdatesListenerTaskHandle = try? await client.subscribeToSendQueueUpdates(listener: SDKListener { _, update in
switch update {
case .newLocalEvent(let transactionID):
analyticsService.signpost.startTransaction(.sendMessage(uuid: transactionID))
case .sentEvent(let transactionID, _):
analyticsService.signpost.finishTransaction(.sendMessage(uuid: transactionID))
default:
break
}
})
sendQueueStatusSubject
.combineLatest(homeserverReachabilityPublisher)
.debounce(for: 1.0, scheduler: DispatchQueue.main)
@@ -1136,7 +1154,8 @@ class ClientProxy: ClientProxyProtocol {
case .joined:
let roomProxy = try await JoinedRoomProxy(roomListService: roomListService,
room: room,
appSettings: appSettings)
appSettings: appSettings,
analyticsService: analyticsService)
return .joined(roomProxy)
case .left:
@@ -15,6 +15,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
private let roomListService: RoomListServiceProtocol
private let room: RoomProtocol
private let appSettings: AppSettings
private let analyticsService: AnalyticsService
// periphery:ignore - required for instance retention in the rust codebase
private var roomInfoObservationToken: TaskHandle?
@@ -76,13 +77,16 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
init(roomListService: RoomListServiceProtocol,
room: RoomProtocol,
appSettings: AppSettings) async throws {
appSettings: AppSettings,
analyticsService: AnalyticsService) async throws {
self.roomListService = roomListService
self.room = room
self.appSettings = appSettings
self.analyticsService = analyticsService
infoSubject = try await .init(RoomInfoProxy(roomInfo: room.roomInfo()))
let openRoomSpan = analyticsService.signpost.addSpan(.timelineLoad, toTransaction: .openRoom)
timeline = try await TimelineProxy(timeline: room.timelineWithConfiguration(configuration: .init(focus: .live(hideThreadedEvents: appSettings.threadsEnabled),
filter: .eventTypeFilter(filter: excludedEventsFilter),
internalIdPrefix: nil,
@@ -90,6 +94,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
trackReadReceipts: .messageLikeEvents,
reportUtds: true)),
kind: .live)
openRoomSpan?.finish()
Task {
await updateMembers()
@@ -146,6 +151,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
func timelineFocusedOnEvent(eventID: String, numberOfEvents: UInt16) async -> Result<TimelineProxyProtocol, RoomProxyError> {
do {
let openRoomSpan = analyticsService.signpost.addSpan(.timelineLoad, toTransaction: .notificationToMessage)
let sdkTimeline = try await room.timelineWithConfiguration(configuration: .init(focus: .event(eventId: eventID,
numContextEvents: numberOfEvents,
hideThreadedEvents: appSettings.threadsEnabled),
@@ -154,6 +160,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
dateDividerMode: .daily,
trackReadReceipts: .disabled,
reportUtds: true))
openRoomSpan?.finish()
return .success(TimelineProxy(timeline: sdkTimeline, kind: .detached))
} catch let error as FocusEventError {
@@ -12,6 +12,7 @@ import MatrixRustSDK
class UserSessionStore: UserSessionStoreProtocol {
private let keychainController: KeychainControllerProtocol
private let appSettings: AppSettings
private let analyticsService: AnalyticsService
private let networkMonitor: NetworkMonitorProtocol
private let appHooks: AppHooks
@@ -31,10 +32,12 @@ class UserSessionStore: UserSessionStoreProtocol {
init(keychainController: KeychainControllerProtocol,
appSettings: AppSettings,
analyticsService: AnalyticsService,
appHooks: AppHooks,
networkMonitor: NetworkMonitorProtocol) {
self.keychainController = keychainController
self.appSettings = appSettings
self.analyticsService = analyticsService
self.appHooks = appHooks
self.networkMonitor = networkMonitor
}
@@ -159,7 +162,8 @@ class UserSessionStore: UserSessionStoreProtocol {
do {
return try await ClientProxy(client: client,
networkMonitor: networkMonitor,
appSettings: appSettings)
appSettings: appSettings,
analyticsService: analyticsService)
} catch {
throw UserSessionStoreError.failedSettingUpClientProxy(error)
}