// // Copyright 2025 Element Creations Ltd. // Copyright 2023-2025 New Vector Ltd. // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. // Please see LICENSE files in the repository root for full details. // import CryptoKit import Sentry /// A simple wrapper around Sentry for easy instrumentation class Signposter { private var transactions = [TransactionName: any Sentry.Span]() private var globalTags = [TagName: String]() enum TransactionName: Hashable { case cachedRoomList case upToDateRoomList case notificationToMessage case openRoom case sendMessage(uuid: String) 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" } } } enum SpanName: String { case timelineLoad = "Timeline load" } struct Span { fileprivate let innerSpan: Sentry.Span func finish() { innerSpan.finish() } } enum TagName: String { case 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 } func resetTransactions() { transactions.removeAll() } // 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 } return Span(innerSpan: transaction.startChild(operation: spanName.rawValue)) } // MARK: - Tags func addGlobalTag(_ tagName: TagName, value: String) { let value = switch tagName { case .homeserver: sha512(value) } globalTags[tagName] = value } func removeGlobalTag(_ tagName: TagName) { globalTags[tagName] = nil } // MARK: - Private func sha512(_ string: String) -> String { let data = Data(string.utf8) let hash = SHA512.hash(data: data) return hash.map { String(format: "%02x", $0) }.joined() } }