Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions desktop/Desktop/Sources/AnalyticsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -559,8 +559,11 @@ class AnalyticsManager {
}

/// Track when the Claude agent bridge fails to start or errors
func chatAgentError(error: String) {
let props: [String: Any] = ["error": error]
func chatAgentError(error: String, rawError: String? = nil) {
var props: [String: Any] = ["error": error]
if let raw = rawError, raw != error {
props["raw_error"] = String(raw.prefix(500))
}
MixpanelManager.shared.track(
"Chat Agent Error", properties: props.compactMapValues { $0 as? MixpanelType })
PostHogManager.shared.track("chat_agent_error", properties: props)
Expand Down
9 changes: 8 additions & 1 deletion desktop/Desktop/Sources/Providers/ChatProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2191,7 +2191,14 @@ A screenshot may be attached — use it silently only if relevant. Never mention
}

logError("Failed to get AI response", error: error)
AnalyticsManager.shared.chatAgentError(error: error.localizedDescription)
// Send both user-friendly and raw error to analytics for remote debugging
let rawError: String
if let bridgeError = error as? BridgeError {
rawError = String(describing: bridgeError)
} else {
rawError = "\(error)"
}
Comment on lines +2196 to +2200
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Raw error bypasses security sanitization before reaching analytics

String(describing: bridgeError) for BridgeError.agentError produces the raw enum representation including its full associated string value. This gets forwarded to PostHog and Mixpanel, but the BridgeError.errorDescription implementation was specifically written to redact auth/key-related content (checking for "api key", "api_key", "leaked", "unauthorized", "forbidden", etc.) before ever exposing the message. That sanitization is now bypassed for analytics.

If the node bridge surfaces an error from the upstream AI provider that embeds a credential value in its message text, the raw string would be sent to third-party analytics in full. The 500-char truncation is not a reliable mitigation because sensitive content typically appears at the beginning of such messages.

A safer approach is to apply the same redaction before sending to analytics:

case .agentError(let msg):
    let lower = msg.lowercased()
    let sensitiveKeywords = ["api key", "api_key", "leaked", "unauthorized",
                             "permission denied", "invalid key", "forbidden"]
    let isAuthError = sensitiveKeywords.contains(where: lower.contains)
    rawError = isAuthError
        ? "agentError([redacted auth/key error])"
        : "agentError(\(String(msg.prefix(200))))"

AnalyticsManager.shared.chatAgentError(error: error.localizedDescription, rawError: rawError)

// Show error to user (unless they intentionally stopped)
if let bridgeError = error as? BridgeError, case .stopped = bridgeError {
Expand Down
Loading