native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #63 from supabitapp/swift-wolf

Add Sentry structured logging for actions and git errors

authored by

khoi and committed by
GitHub
25d56400 03773477

+70 -23
+1
supacode/App/supacodeApp.swift
··· 38 38 SentrySDK.start { options in 39 39 options.dsn = "https://fb4d394e0bd3e72871b01c7ef3cac129@o1224589.ingest.us.sentry.io/4510770231050240" 40 40 options.tracesSampleRate = 1.0 41 + options.enableLogs = true 41 42 } 42 43 #endif 43 44 if let resourceURL = Bundle.main.resourceURL?.appendingPathComponent("ghostty") {
+65 -22
supacode/Clients/Git/GitClient.swift
··· 1 1 import Foundation 2 + import Sentry 3 + 4 + enum GitOperation: String { 5 + case repoRoot = "repo_root" 6 + case worktreeList = "worktree_list" 7 + case worktreeCreate = "worktree_create" 8 + case worktreeRemove = "worktree_remove" 9 + case branchNames = "branch_names" 10 + case dirtyCheck = "dirty_check" 11 + } 2 12 3 13 enum GitClientError: LocalizedError { 4 14 case commandFailed(command: String, message: String) ··· 31 41 let normalizedPath = Self.directoryURL(for: path) 32 42 let wtURL = try wtScriptURL() 33 43 let output = try await runLoginShellProcess( 44 + operation: .repoRoot, 34 45 executableURL: wtURL, 35 46 arguments: ["root"], 36 47 currentDirectoryURL: normalizedPath ··· 85 96 86 97 nonisolated func localBranchNames(for repoRoot: URL) async throws -> Set<String> { 87 98 let path = repoRoot.path(percentEncoded: false) 88 - let output = try await runGit(arguments: [ 89 - "-C", 90 - path, 91 - "for-each-ref", 92 - "--format=%(refname:short)", 93 - "refs/heads", 94 - ]) 99 + let output = try await runGit( 100 + operation: .branchNames, 101 + arguments: [ 102 + "-C", 103 + path, 104 + "for-each-ref", 105 + "--format=%(refname:short)", 106 + "refs/heads", 107 + ] 108 + ) 95 109 let names = 96 110 output 97 111 .split(whereSeparator: \.isNewline) ··· 105 119 let wtURL = try wtScriptURL() 106 120 let baseDir = SupacodePaths.repositoryDirectory(for: repositoryRootURL) 107 121 let output = try await runLoginShellProcess( 122 + operation: .worktreeCreate, 108 123 executableURL: wtURL, 109 124 arguments: ["--base-dir", baseDir.path(percentEncoded: false), "sw", name], 110 125 currentDirectoryURL: repoRoot ··· 129 144 nonisolated func isWorktreeDirty(at worktreeURL: URL) async -> Bool { 130 145 let path = worktreeURL.path(percentEncoded: false) 131 146 do { 132 - let output = try await runGit(arguments: ["-C", path, "status", "--porcelain"]) 147 + let output = try await runGit( 148 + operation: .dirtyCheck, 149 + arguments: ["-C", path, "status", "--porcelain"] 150 + ) 133 151 return WorktreeDirtCheck.isDirty(statusOutput: output) 134 152 } catch { 135 153 return true ··· 140 158 if !worktree.name.isEmpty { 141 159 let wtURL = try wtScriptURL() 142 160 _ = try await runLoginShellProcess( 161 + operation: .worktreeRemove, 143 162 executableURL: wtURL, 144 163 arguments: ["rm", "-f", worktree.name], 145 164 currentDirectoryURL: worktree.repositoryRootURL ··· 148 167 } 149 168 let rootPath = worktree.repositoryRootURL.path(percentEncoded: false) 150 169 let worktreePath = worktree.workingDirectory.path(percentEncoded: false) 151 - _ = try await runGit(arguments: [ 152 - "-C", 153 - rootPath, 154 - "worktree", 155 - "remove", 156 - "--force", 157 - worktreePath, 158 - ]) 170 + _ = try await runGit( 171 + operation: .worktreeRemove, 172 + arguments: [ 173 + "-C", 174 + rootPath, 175 + "worktree", 176 + "remove", 177 + "--force", 178 + worktreePath, 179 + ] 180 + ) 159 181 return worktree.workingDirectory 160 182 } 161 183 162 - nonisolated private func runGit(arguments: [String]) async throws -> String { 184 + nonisolated private func runGit( 185 + operation: GitOperation, 186 + arguments: [String] 187 + ) async throws -> String { 163 188 let env = URL(fileURLWithPath: "/usr/bin/env") 164 189 let command = ([env.path(percentEncoded: false)] + ["git"] + arguments).joined(separator: " ") 165 190 do { 166 191 return try await shell.run(env, ["git"] + arguments, nil).stdout 167 192 } catch { 168 - throw wrapShellError(error, command: command) 193 + throw wrapShellError(error, operation: operation, command: command) 169 194 } 170 195 } 171 196 ··· 176 201 "\(wtURL.lastPathComponent) \(arguments.joined(separator: " "))" 177 202 ) 178 203 let output = try await runLoginShellProcess( 204 + operation: .worktreeList, 179 205 executableURL: wtURL, 180 206 arguments: arguments, 181 207 currentDirectoryURL: repoRoot ··· 193 219 } 194 220 195 221 nonisolated private func runLoginShellProcess( 222 + operation: GitOperation, 196 223 executableURL: URL, 197 224 arguments: [String], 198 225 currentDirectoryURL: URL? ··· 201 228 do { 202 229 return try await shell.runLogin(executableURL, arguments, currentDirectoryURL).stdout 203 230 } catch { 204 - throw wrapShellError(error, command: command) 231 + throw wrapShellError(error, operation: operation, command: command) 205 232 } 206 233 } 207 234 ··· 236 263 237 264 } 238 265 239 - nonisolated private func wrapShellError(_ error: Error, command: String) -> GitClientError { 266 + nonisolated private func wrapShellError( 267 + _ error: Error, 268 + operation: GitOperation, 269 + command: String 270 + ) -> GitClientError { 271 + let gitError: GitClientError 272 + var exitCode: Int32 = -1 240 273 if let shellError = error as? ShellClientError { 274 + exitCode = shellError.exitCode 241 275 var messageParts: [String] = [] 242 276 if !shellError.stdout.isEmpty { 243 277 messageParts.append("stdout:\n\(shellError.stdout)") ··· 246 280 messageParts.append("stderr:\n\(shellError.stderr)") 247 281 } 248 282 let message = messageParts.joined(separator: "\n") 249 - return .commandFailed(command: command, message: message) 283 + gitError = .commandFailed(command: command, message: message) 284 + } else { 285 + gitError = .commandFailed(command: command, message: error.localizedDescription) 250 286 } 251 - return .commandFailed(command: command, message: error.localizedDescription) 287 + SentrySDK.logger.error( 288 + "git command failed", 289 + attributes: [ 290 + "operation": operation.rawValue, 291 + "exit_code": Int(exitCode), 292 + ] 293 + ) 294 + return gitError 252 295 } 253 296 254 297 struct GitWtWorktreeEntry: Decodable {
+4 -1
supacode/Features/App/Reducer/AppFeature.swift
··· 1 1 import AppKit 2 2 import ComposableArchitecture 3 + import Sentry 3 4 import SwiftUI 4 5 5 6 private let notificationSound: NSSound? = { ··· 234 235 let base: Base 235 236 236 237 func reduce(into state: inout Base.State, action: Base.Action) -> Effect<Base.Action> { 237 - print("received action: \(debugCaseOutput(action))") 238 + let actionLabel = debugCaseOutput(action) 239 + print("received action: \(actionLabel)") 240 + SentrySDK.logger.info("received action: \(actionLabel)") 238 241 return base.reduce(into: &state, action: action) 239 242 } 240 243 }