diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index e200f251..a8ebeea4 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -294,6 +294,19 @@ jobs: # by appcast URLs to prevent signature/asset mismatch races. cp "$DMG_RELEASE" "$NIGHTLY_DMG_IMMUTABLE" + - name: Upload dSYMs to Sentry + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: manaflow + SENTRY_PROJECT: cmuxterm-macos + run: | + if [ -z "$SENTRY_AUTH_TOKEN" ]; then + echo "SENTRY_AUTH_TOKEN not set, skipping dSYM upload" + exit 0 + fi + brew install getsentry/tools/sentry-cli || true + sentry-cli debug-files upload --include-sources build/Build/Products/Release/ + - name: Generate Sparkle appcast (nightly) env: SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3176697b..9063de75 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -250,6 +250,20 @@ jobs: xcrun stapler staple "$DMG_RELEASE" xcrun stapler validate "$DMG_RELEASE" + - name: Upload dSYMs to Sentry + if: steps.guard_release_assets.outputs.skip_all != 'true' + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: manaflow + SENTRY_PROJECT: cmuxterm-macos + run: | + if [ -z "$SENTRY_AUTH_TOKEN" ]; then + echo "SENTRY_AUTH_TOKEN not set, skipping dSYM upload" + exit 0 + fi + brew install getsentry/tools/sentry-cli || true + sentry-cli debug-files upload --include-sources build/Build/Products/Release/ + - name: Generate Sparkle appcast if: steps.guard_release_assets.outputs.skip_all != 'true' env: diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index 58641e08..3448b298 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ A5001500 /* CmuxWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001510 /* CmuxWebView.swift */; }; A5001501 /* UITestRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001511 /* UITestRecorder.swift */; }; A5001226 /* SocketControlSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001225 /* SocketControlSettings.swift */; }; + A5001601 /* SentryHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001600 /* SentryHelper.swift */; }; A5001400 /* Panel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001410 /* Panel.swift */; }; A5001401 /* TerminalPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001411 /* TerminalPanel.swift */; }; A5001402 /* BrowserPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001412 /* BrowserPanel.swift */; }; @@ -146,6 +147,7 @@ A5001017 /* ghostty.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ghostty.h; sourceTree = ""; }; A5001018 /* cmux-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "cmux-Bridging-Header.h"; sourceTree = ""; }; A5001019 /* TerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalController.swift; sourceTree = ""; }; + A5001600 /* SentryHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryHelper.swift; sourceTree = ""; }; A5001510 /* CmuxWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/CmuxWebView.swift; sourceTree = ""; }; A5001511 /* UITestRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestRecorder.swift; sourceTree = ""; }; A5001520 /* PostHogAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalytics.swift; sourceTree = ""; }; @@ -322,6 +324,7 @@ A5001019 /* TerminalController.swift */, A5001541 /* PortScanner.swift */, A5001225 /* SocketControlSettings.swift */, + A5001600 /* SentryHelper.swift */, A5001090 /* AppDelegate.swift */, A5001091 /* NotificationsPage.swift */, A5001092 /* TerminalNotificationStore.swift */, @@ -551,6 +554,7 @@ A5001007 /* TerminalController.swift in Sources */, A5001540 /* PortScanner.swift in Sources */, A5001226 /* SocketControlSettings.swift in Sources */, + A5001601 /* SentryHelper.swift in Sources */, A5001093 /* AppDelegate.swift in Sources */, A5001094 /* NotificationsPage.swift in Sources */, A5001095 /* TerminalNotificationStore.swift in Sources */, diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 5fc94aa0..ef7215c0 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -699,6 +699,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent options.debug = false #endif options.sendDefaultPii = true + + // Performance tracing (10% of transactions) + options.tracesSampleRate = 0.1 + // App hang timeout (default is 2s, be explicit) + options.appHangTimeoutInterval = 2.0 + // Attach stack traces to all events + options.attachStacktrace = true + // Capture failed HTTP requests + options.enableCaptureFailedRequests = true } if !isRunningUnderXCTest { @@ -804,6 +813,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent #endif func applicationDidBecomeActive(_ notification: Notification) { + sentryBreadcrumb("app.didBecomeActive", category: "lifecycle", data: [ + "tabCount": tabManager?.tabs.count ?? 0 + ]) let env = ProcessInfo.processInfo.environment if !isRunningUnderXCTest(env) { PostHogAnalytics.shared.trackDailyActive(reason: "didBecomeActive") diff --git a/Sources/SentryHelper.swift b/Sources/SentryHelper.swift new file mode 100644 index 00000000..9877a46c --- /dev/null +++ b/Sources/SentryHelper.swift @@ -0,0 +1,9 @@ +import Sentry + +/// Add a Sentry breadcrumb for user-action context in hang/crash reports. +func sentryBreadcrumb(_ message: String, category: String = "ui", data: [String: Any]? = nil) { + let crumb = Breadcrumb(level: .info, category: category) + crumb.message = message + crumb.data = data + SentrySDK.addBreadcrumb(crumb) +} diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index 0e38e366..5a59b82a 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -567,6 +567,9 @@ class TabManager: ObservableObject { @Published var selectedTabId: UUID? { didSet { guard selectedTabId != oldValue else { return } + sentryBreadcrumb("workspace.switch", data: [ + "tabCount": tabs.count + ]) let previousTabId = oldValue if let previousTabId, let previousPanelId = focusedPanelId(for: previousTabId) { @@ -752,6 +755,7 @@ class TabManager: ObservableObject { @discardableResult func addWorkspace(workingDirectory overrideWorkingDirectory: String? = nil, select: Bool = true) -> Workspace { + sentryBreadcrumb("workspace.create", data: ["tabCount": tabs.count + 1]) let workingDirectory = normalizedWorkingDirectory(overrideWorkingDirectory) ?? preferredWorkingDirectoryForNewTab() let inheritedConfig = inheritedTerminalConfigForNewWorkspace() let ordinal = Self.nextPortOrdinal @@ -963,6 +967,7 @@ class TabManager: ObservableObject { func closeWorkspace(_ workspace: Workspace) { guard tabs.count > 1 else { return } + sentryBreadcrumb("workspace.close", data: ["tabCount": tabs.count - 1]) AppDelegate.shared?.notificationStore?.clearNotifications(forTabId: workspace.id) unwireClosedBrowserTracking(for: workspace) @@ -1725,6 +1730,7 @@ class TabManager: ObservableObject { guard let selectedTabId, let tab = tabs.first(where: { $0.id == selectedTabId }), let focusedPanelId = tab.focusedPanelId else { return } + sentryBreadcrumb("split.create", data: ["direction": String(describing: direction)]) _ = newSplit(tabId: selectedTabId, surfaceId: focusedPanelId, direction: direction) }