Setup Sentry instrumentation on top of the existing Signposter (#2985)

* Move Sentry setup outside of the BugReportService

* Setup Sentry SwiftUI tracing for the homeScreen and roomScreen roots

* Setup Sentry instrumentation on top of the existing Signposter

* Various tweaks
This commit is contained in:
Stefan Ceriu
2024-06-28 18:15:57 +03:00
committed by GitHub
parent 7e999f0a17
commit 3c500887ab
14 changed files with 149 additions and 177 deletions
@@ -19,6 +19,7 @@ import BackgroundTasks
import Combine
import Intents
import MatrixRustSDK
import Sentry
import SwiftUI
import Version
@@ -96,17 +97,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
Self.setupServiceLocator(appSettings: appSettings)
ServiceLocator.shared.analytics.isRunningPublisher
.removeDuplicates()
.sink { isRunning in
if isRunning {
ServiceLocator.shared.bugReportService.start()
} else {
ServiceLocator.shared.bugReportService.stop()
}
}
.store(in: &cancellables)
ServiceLocator.shared.analytics.startIfEnabled()
stateMachine = AppCoordinatorStateMachine()
@@ -150,6 +140,17 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
registerBackgroundAppRefresh()
ServiceLocator.shared.analytics.isRunningPublisher
.removeDuplicates()
.sink { [weak self] isRunning in
if isRunning {
self?.setupSentry()
} else {
self?.teardownSentry()
}
}
.store(in: &cancellables)
elementCallService.actions
.receive(on: DispatchQueue.main)
.sink { [weak self] action in
@@ -344,7 +345,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
ServiceLocator.shared.register(appSettings: appSettings)
ServiceLocator.shared.register(networkMonitor: NetworkMonitor())
ServiceLocator.shared.register(bugReportService: BugReportService(withBaseURL: appSettings.bugReportServiceBaseURL,
sentryURL: appSettings.bugReportSentryURL,
applicationId: appSettings.bugReportApplicationId,
sdkGitSHA: sdkGitSha(),
maxUploadSize: appSettings.bugReportMaxUploadSize))
@@ -733,6 +733,62 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
}
}
private func setupSentry() {
SentrySDK.start { [weak self] options in
#if DEBUG
options.enabled = false
#endif
options.dsn = self?.appSettings.bugReportSentryURL.absoluteString
// Sentry swizzling shows up quite often as the heaviest stack trace when profiling
// We don't need any of the features it powers (see docs)
options.enableSwizzling = false
// WatchdogTermination is currently the top issue but we've had zero complaints
// so it might very well just all be false positives
options.enableWatchdogTerminationTracking = false
// Disabled as it seems to report a lot of false positives
options.enableAppHangTracking = false
// Most of the network requests are made Rust side, this is useless
options.enableNetworkBreadcrumbs = false
// Doesn't seem to work at all well with SwiftUI
options.enableAutoBreadcrumbTracking = false
// Experimental. Stitches stack traces of asynchronous code together
options.swiftAsyncStacktraces = true
// Uniform sample rate: 1.0 captures 100% of transactions
// In Production you will probably want a smaller number such as 0.5 for 50%
if AppSettings.isDevelopmentBuild {
options.sampleRate = 1.0
options.tracesSampleRate = 1.0
options.profilesSampleRate = 1.0
} else {
options.sampleRate = 0.5
options.tracesSampleRate = 0.5
options.profilesSampleRate = 0.5
}
// This callback is only executed once during the entire run of the program to avoid
// multiple callbacks if there are multiple crash events to send (see method documentation)
options.onCrashedLastRun = { event in
MXLog.error("Sentry detected a crash in the previous run: \(event.eventId.sentryIdString)")
ServiceLocator.shared.bugReportService.lastCrashEventID = event.eventId.sentryIdString
}
MXLog.info("SentrySDK started")
}
}
private func teardownSentry() {
SentrySDK.close()
MXLog.info("SentrySDK stopped")
}
// MARK: Toasts and loading indicators
private static let loadingIndicatorIdentifier = "\(AppCoordinator.self)-Loading"
@@ -762,7 +818,10 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
private func startSync() {
guard let userSession else { return }
ServiceLocator.shared.analytics.signpost.beginFirstSync()
// FIXME: replace this with `user_id_server_name` from https://github.com/matrix-org/matrix-rust-sdk/pull/3617
let serverName = String(userSession.clientProxy.userID.split(separator: ":").last ?? "Unknown")
ServiceLocator.shared.analytics.signpost.beginFirstSync(serverName: serverName)
userSession.clientProxy.startSync()
guard clientProxyObserver == nil else {
@@ -1815,87 +1815,13 @@ class AudioSessionMock: AudioSessionProtocol {
}
}
class BugReportServiceMock: BugReportServiceProtocol {
var isRunning: Bool {
get { return underlyingIsRunning }
set(value) { underlyingIsRunning = value }
}
var underlyingIsRunning: Bool!
var crashedLastRun: Bool {
get { return underlyingCrashedLastRun }
set(value) { underlyingCrashedLastRun = value }
}
var underlyingCrashedLastRun: Bool!
var lastCrashEventID: String?
//MARK: - start
var startUnderlyingCallsCount = 0
var startCallsCount: Int {
get {
if Thread.isMainThread {
return startUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = startUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
startUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
startUnderlyingCallsCount = newValue
}
}
}
}
var startCalled: Bool {
return startCallsCount > 0
}
var startClosure: (() -> Void)?
func start() {
startCallsCount += 1
startClosure?()
}
//MARK: - stop
var stopUnderlyingCallsCount = 0
var stopCallsCount: Int {
get {
if Thread.isMainThread {
return stopUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = stopUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
stopUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
stopUnderlyingCallsCount = newValue
}
}
}
}
var stopCalled: Bool {
return stopCallsCount > 0
}
var stopClosure: (() -> Void)?
func stop() {
stopCallsCount += 1
stopClosure?()
}
//MARK: - submitBugReport
var submitBugReportProgressListenerUnderlyingCallsCount = 0
@@ -75,6 +75,7 @@ struct HomeScreen: View {
gradientView.alpha = 1
}
}
.sentryTrace("\(Self.self)")
}
// MARK: - Private
@@ -15,6 +15,7 @@
//
import Compound
import SentrySwiftUI
import SwiftUI
struct HomeScreenContent: View {
@@ -29,6 +30,7 @@ struct HomeScreenContent: View {
migrationView
default:
roomList
.sentryTrace("\(Self.self)")
}
}
@@ -81,6 +81,7 @@ struct RoomScreen: View {
context.send(viewAction: .handlePasteOrDrop(provider: provider))
return true
}
.sentryTrace("\(Self.self)")
}
private var timeline: some View {
@@ -38,7 +38,7 @@ class AnalyticsService {
/// A signpost client for performance testing the app. This client doesn't respect the
/// `isRunning` state or behave any differently when `start`/`reset` are called.
let signpost = Signposter()
let signpost = Signposter(isDevelopmentBuild: AppSettings.isDevelopmentBuild)
/// Whether or not the object is enabled and sending events to the server.
private let isRunningSubject: CurrentValueSubject<Bool, Never> = .init(false)
@@ -15,6 +15,7 @@
//
import OSLog
import Sentry
/// A simple wrapper around OSSignposter for easy performance testing.
class Signposter {
@@ -24,22 +25,37 @@ class Signposter {
private let logger = Logger(subsystem: subsystem, category: category)
/// Signpost name constants.
enum Name {
private enum Name {
static let login: StaticString = "Login"
static let firstSync: StaticString = "FirstSync"
static let firstRooms: StaticString = "FirstRooms"
static let roomFlow: StaticString = "RoomFlow"
static let appStartup = "AppStartup"
static let appStarted = "AppStarted"
static let homeserver = "homeserver"
static let isDevelopmentBuild = "isDevelopmentBuild"
}
static let subsystem = "ElementX"
static let category = "PerformanceTests"
private var appStartupSpan: Span
init(isDevelopmentBuild: Bool) {
appStartupSpan = SentrySDK.startTransaction(name: Name.appStartup, operation: Name.appStarted)
appStartupSpan.setData(value: isDevelopmentBuild, key: Name.isDevelopmentBuild)
}
// MARK: - Login
private var loginState: OSSignpostIntervalState?
private var loginSpan: Span?
func beginLogin() {
loginState = signposter.beginInterval(Name.login)
loginSpan = appStartupSpan.startChild(operation: "\(Name.login)")
}
func endLogin() {
@@ -49,37 +65,56 @@ class Signposter {
}
signposter.endInterval(Name.login, loginState)
loginSpan?.finish()
self.loginState = nil
loginSpan = nil
}
// MARK: - FirstSync
private var firstSyncState: OSSignpostIntervalState?
private var firstSyncSpan: Span?
func beginFirstSync() {
func beginFirstSync(serverName: String) {
appStartupSpan.setTag(value: serverName, key: Name.homeserver)
firstSyncState = signposter.beginInterval(Name.firstSync)
firstSyncSpan = appStartupSpan.startChild(operation: "\(Name.firstSync)")
}
func endFirstSync() {
guard let firstSyncState else { return }
signposter.endInterval(Name.firstSync, firstSyncState)
firstSyncSpan?.finish()
self.firstSyncState = nil
firstSyncSpan = nil
}
// MARK: - FirstRooms
private var firstRoomsState: OSSignpostIntervalState?
private var firstRoomsSpan: Span?
func beginFirstRooms() {
firstRoomsState = signposter.beginInterval(Name.firstRooms)
firstRoomsSpan = appStartupSpan.startChild(operation: "\(Name.firstRooms)")
}
func endFirstRooms() {
defer {
appStartupSpan.finish()
}
guard let firstRoomsState else { return }
signposter.endInterval(Name.firstRooms, firstRoomsState)
firstRoomsSpan?.finish()
self.firstRoomsState = nil
firstRoomsSpan = nil
}
// MARK: - Room Flow
@@ -22,23 +22,21 @@ import UIKit
class BugReportService: NSObject, BugReportServiceProtocol {
private let baseURL: URL
private let sentryURL: URL
private let applicationId: String
private let sdkGitSHA: String
private let maxUploadSize: Int
private let session: URLSession
private var lastCrashEventId: String?
private let progressSubject = PassthroughSubject<Double, Never>()
private var cancellables = Set<AnyCancellable>()
var lastCrashEventID: String?
init(withBaseURL baseURL: URL,
sentryURL: URL,
applicationId: String,
sdkGitSHA: String,
maxUploadSize: Int,
session: URLSession = .shared) {
self.baseURL = baseURL
self.sentryURL = sentryURL
self.applicationId = applicationId
self.sdkGitSHA = sdkGitSHA
self.maxUploadSize = maxUploadSize
@@ -47,64 +45,10 @@ class BugReportService: NSObject, BugReportServiceProtocol {
}
// MARK: - BugReportServiceProtocol
var isRunning: Bool {
SentrySDK.isEnabled
}
var crashedLastRun: Bool {
SentrySDK.crashedLastRun
}
func start() {
guard !isRunning else { return }
SentrySDK.start { options in
#if DEBUG
options.enabled = false
#endif
options.dsn = self.sentryURL.absoluteString
// Sentry swizzling shows up quite often as the heaviest stack trace when profiling
// We don't need any of the features it powers (see docs)
options.enableSwizzling = false
// WatchdogTermination is currently the top issue but we've had zero complaints
// so it might very well just all be false positives
options.enableWatchdogTerminationTracking = false
// Disabled as it seems to report a lot of false positives
options.enableAppHangTracking = false
// Most of the network requests are made Rust side, this is useless
options.enableNetworkBreadcrumbs = false
// Doesn't seem to work at all well with SwiftUI
options.enableAutoBreadcrumbTracking = false
// Experimental. Stitches stack traces of asynchronous code together
options.swiftAsyncStacktraces = true
// Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
// We recommend adjusting this value in production.
options.tracesSampleRate = 1.0
// This callback is only executed once during the entire run of the program to avoid
// multiple callbacks if there are multiple crash events to send (see method documentation)
options.onCrashedLastRun = { [weak self] event in
MXLog.error("Sentry detected a crash in the previous run: \(event.eventId.sentryIdString)")
self?.lastCrashEventId = event.eventId.sentryIdString
}
}
MXLog.info("Started.")
}
func stop() {
guard isRunning else { return }
SentrySDK.close()
MXLog.info("Stopped.")
}
// swiftlint:disable:next cyclomatic_complexity
func submitBugReport(_ bugReport: BugReport,
@@ -140,8 +84,8 @@ class BugReportService: NSObject, BugReportServiceProtocol {
}
}
if let crashEventId = lastCrashEventId {
params.append(MultipartFormData(key: "crash_report", type: .text(value: "<https://sentry.tools.element.io/organizations/element/issues/?project=44&query=\(crashEventId)>")))
if let crashEventID = lastCrashEventID {
params.append(MultipartFormData(key: "crash_report", type: .text(value: "<https://sentry.tools.element.io/organizations/element/issues/?project=44&query=\(crashEventID)>")))
}
for url in bugReport.files {
@@ -194,7 +138,7 @@ class BugReportService: NSObject, BugReportServiceProtocol {
let uploadResponse = try decoder.decode(SubmitBugReportResponse.self, from: data)
if !uploadResponse.reportUrl.isEmpty {
lastCrashEventId = nil
lastCrashEventID = nil
}
MXLog.info("Feedback submitted.")
@@ -52,15 +52,10 @@ enum BugReportServiceError: LocalizedError {
}
// sourcery: AutoMockable
protocol BugReportServiceProtocol {
// periphery: ignore
var isRunning: Bool { get }
protocol BugReportServiceProtocol: AnyObject {
var crashedLastRun: Bool { get }
func start()
func stop()
var lastCrashEventID: String? { get set }
func submitBugReport(_ bugReport: BugReport,
progressListener: CurrentValueSubject<Double, Never>) async -> Result<SubmitBugReportResponse, BugReportServiceError>
+3
View File
@@ -196,6 +196,9 @@ targets:
- package: SwiftState
- package: GZIP
- package: Sentry
products:
- Sentry
- SentrySwiftUI
- package: Version
- package: Emojibase
- package: WysiwygComposer