Upgrade Sentry: tracing, breadcrumbs, dSYM upload (#366)

* Upgrade Sentry: tracing, breadcrumbs, dSYM upload

- Enhanced Sentry SDK init with performance tracing (10% sample),
  explicit app hang timeout, stack trace attachment, and HTTP
  failure capture
- Added breadcrumbs for key user actions: workspace switch/create/close,
  split creation, command palette open/close, app focus — these give
  context to hang/crash reports
- Added dSYM upload step to nightly and release CI workflows so hang
  stacks are fully symbolicated (requires SENTRY_AUTH_TOKEN secret)
- Created SentryHelper.swift with lightweight breadcrumb helper

Closes https://github.com/manaflow-ai/cmux/issues/365

* Remove command palette breadcrumbs

Not useful for hang diagnosis — keep only workspace/tab/split/focus
breadcrumbs that correlate with heavy operations.
This commit is contained in:
Lawrence Chen 2026-02-23 17:11:01 -08:00 committed by GitHub
parent 396942c7e4
commit 53ef6a5f7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 58 additions and 0 deletions

View file

@ -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 }}

View file

@ -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:

View file

@ -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 = "<group>"; };
A5001018 /* cmux-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "cmux-Bridging-Header.h"; sourceTree = "<group>"; };
A5001019 /* TerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalController.swift; sourceTree = "<group>"; };
A5001600 /* SentryHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryHelper.swift; sourceTree = "<group>"; };
A5001510 /* CmuxWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/CmuxWebView.swift; sourceTree = "<group>"; };
A5001511 /* UITestRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestRecorder.swift; sourceTree = "<group>"; };
A5001520 /* PostHogAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalytics.swift; sourceTree = "<group>"; };
@ -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 */,

View file

@ -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")

View file

@ -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)
}

View file

@ -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)
}