mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat(telemetry): extract error messages from JSON payloads for better PostHog grouping (#10163)
This commit is contained in:
parent
495664c261
commit
45dbe4d028
2 changed files with 197 additions and 6 deletions
|
|
@ -3,6 +3,7 @@
|
|||
import {
|
||||
getErrorStatusCode,
|
||||
getErrorMessage,
|
||||
extractMessageFromJsonPayload,
|
||||
shouldReportApiErrorToTelemetry,
|
||||
EXPECTED_API_ERROR_CODES,
|
||||
ApiProviderError,
|
||||
|
|
@ -48,10 +49,11 @@ describe("telemetry error utilities", () => {
|
|||
})
|
||||
|
||||
describe("getErrorMessage", () => {
|
||||
it("should return undefined for non-OpenAI SDK errors", () => {
|
||||
it("should return undefined for null, undefined, or objects without message", () => {
|
||||
expect(getErrorMessage(null)).toBeUndefined()
|
||||
expect(getErrorMessage(undefined)).toBeUndefined()
|
||||
expect(getErrorMessage({ message: "error" })).toBeUndefined()
|
||||
expect(getErrorMessage({})).toBeUndefined()
|
||||
expect(getErrorMessage({ code: 500 })).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should return the primary message for simple OpenAI SDK errors", () => {
|
||||
|
|
@ -59,6 +61,10 @@ describe("telemetry error utilities", () => {
|
|||
expect(getErrorMessage(error)).toBe("Bad request")
|
||||
})
|
||||
|
||||
it("should return message from plain objects with message property", () => {
|
||||
expect(getErrorMessage({ message: "error" })).toBe("error")
|
||||
})
|
||||
|
||||
it("should prioritize nested error.message over primary message", () => {
|
||||
const error = {
|
||||
status: 500,
|
||||
|
|
@ -100,6 +106,132 @@ describe("telemetry error utilities", () => {
|
|||
}
|
||||
expect(getErrorMessage(error)).toBe("Forbidden")
|
||||
})
|
||||
|
||||
it("should extract message from JSON payload in error message", () => {
|
||||
const error = {
|
||||
status: 503,
|
||||
message: '503 {"error":{"code":"","message":"Model unavailable"}}',
|
||||
}
|
||||
expect(getErrorMessage(error)).toBe("Model unavailable")
|
||||
})
|
||||
|
||||
it("should extract message from JSON payload with status prefix", () => {
|
||||
const error = {
|
||||
status: 503,
|
||||
message:
|
||||
'503 {"error":{"code":"","message":"所有令牌分组 Tier 3 下对于模型 claude-sonnet-4-5 均无可用渠道,请更换分组尝试"}}',
|
||||
}
|
||||
expect(getErrorMessage(error)).toBe(
|
||||
"所有令牌分组 Tier 3 下对于模型 claude-sonnet-4-5 均无可用渠道,请更换分组尝试",
|
||||
)
|
||||
})
|
||||
|
||||
it("should extract message from nested error.message containing JSON", () => {
|
||||
const error = {
|
||||
status: 500,
|
||||
message: "Request failed",
|
||||
error: { message: '{"error":{"message":"Upstream provider error"}}' },
|
||||
}
|
||||
expect(getErrorMessage(error)).toBe("Upstream provider error")
|
||||
})
|
||||
|
||||
it("should return original message when JSON has no message field", () => {
|
||||
const error = {
|
||||
status: 500,
|
||||
message: '{"error":{"code":"123"}}',
|
||||
}
|
||||
expect(getErrorMessage(error)).toBe('{"error":{"code":"123"}}')
|
||||
})
|
||||
|
||||
it("should return original message when JSON is invalid", () => {
|
||||
const error = {
|
||||
status: 500,
|
||||
message: "503 {invalid json}",
|
||||
}
|
||||
expect(getErrorMessage(error)).toBe("503 {invalid json}")
|
||||
})
|
||||
|
||||
it("should extract message from standard Error object", () => {
|
||||
const error = new Error("Simple error message")
|
||||
expect(getErrorMessage(error)).toBe("Simple error message")
|
||||
})
|
||||
|
||||
it("should extract message from standard Error with JSON payload", () => {
|
||||
const error = new Error('503 {"error":{"code":"","message":"Model unavailable"}}')
|
||||
expect(getErrorMessage(error)).toBe("Model unavailable")
|
||||
})
|
||||
|
||||
it("should extract message from ApiProviderError", () => {
|
||||
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage")
|
||||
expect(getErrorMessage(error)).toBe("Test error")
|
||||
})
|
||||
|
||||
it("should extract message from ApiProviderError with JSON payload", () => {
|
||||
const jsonMessage =
|
||||
'503 {"error":{"code":"","message":"所有令牌分组 Tier 3 下对于模型 claude-sonnet-4-5 均无可用渠道"}}'
|
||||
const error = new ApiProviderError(jsonMessage, "Anthropic", "claude-sonnet-4-5", "createMessage")
|
||||
expect(getErrorMessage(error)).toBe("所有令牌分组 Tier 3 下对于模型 claude-sonnet-4-5 均无可用渠道")
|
||||
})
|
||||
|
||||
it("should handle ApiProviderError with errorCode but no status property", () => {
|
||||
const error = new ApiProviderError("Test error", "Anthropic", "claude-3-opus", "createMessage", 500)
|
||||
expect(getErrorMessage(error)).toBe("Test error")
|
||||
})
|
||||
})
|
||||
|
||||
describe("extractMessageFromJsonPayload", () => {
|
||||
it("should return undefined for messages without JSON", () => {
|
||||
expect(extractMessageFromJsonPayload("Simple error message")).toBeUndefined()
|
||||
expect(extractMessageFromJsonPayload("Error: something went wrong")).toBeUndefined()
|
||||
expect(extractMessageFromJsonPayload("")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should extract message from error.message structure", () => {
|
||||
const json = '{"error":{"message":"Model unavailable"}}'
|
||||
expect(extractMessageFromJsonPayload(json)).toBe("Model unavailable")
|
||||
})
|
||||
|
||||
it("should extract message from error.message with code structure", () => {
|
||||
const json = '{"error":{"code":"","message":"Model unavailable"}}'
|
||||
expect(extractMessageFromJsonPayload(json)).toBe("Model unavailable")
|
||||
})
|
||||
|
||||
it("should extract message from status prefix followed by JSON", () => {
|
||||
const message = '503 {"error":{"code":"","message":"Model unavailable"}}'
|
||||
expect(extractMessageFromJsonPayload(message)).toBe("Model unavailable")
|
||||
})
|
||||
|
||||
it("should extract message from simple message structure", () => {
|
||||
const json = '{"message":"Simple error"}'
|
||||
expect(extractMessageFromJsonPayload(json)).toBe("Simple error")
|
||||
})
|
||||
|
||||
it("should return undefined for JSON without message field", () => {
|
||||
const json = '{"error":{"code":"500"}}'
|
||||
expect(extractMessageFromJsonPayload(json)).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should return undefined for invalid JSON", () => {
|
||||
expect(extractMessageFromJsonPayload("{invalid json}")).toBeUndefined()
|
||||
expect(extractMessageFromJsonPayload("503 {not: valid: json}")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should handle nested error structure with empty code", () => {
|
||||
const json = '{"error":{"code":"","message":"Token quota exceeded"}}'
|
||||
expect(extractMessageFromJsonPayload(json)).toBe("Token quota exceeded")
|
||||
})
|
||||
|
||||
it("should handle Unicode messages correctly", () => {
|
||||
const json = '{"error":{"message":"所有令牌分组 Tier 3 下对于模型 claude-sonnet-4-5 均无可用渠道"}}'
|
||||
expect(extractMessageFromJsonPayload(json)).toBe(
|
||||
"所有令牌分组 Tier 3 下对于模型 claude-sonnet-4-5 均无可用渠道",
|
||||
)
|
||||
})
|
||||
|
||||
it("should return undefined when message field is not a string", () => {
|
||||
const json = '{"error":{"message":123}}'
|
||||
expect(extractMessageFromJsonPayload(json)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("shouldReportApiErrorToTelemetry", () => {
|
||||
|
|
|
|||
|
|
@ -337,17 +337,76 @@ export function getErrorStatusCode(error: unknown): number | undefined {
|
|||
}
|
||||
|
||||
/**
|
||||
* Extracts the most descriptive error message from an OpenAI SDK error.
|
||||
* Extracts a message from a JSON payload embedded in an error string.
|
||||
* Handles cases like "503 {"error":{"message":"actual error message"}}"
|
||||
* or just '{"error":{"message":"actual error message"}}'
|
||||
*
|
||||
* @param message - The message string that may contain JSON
|
||||
* @returns The extracted message from the JSON payload, or undefined if not found
|
||||
*/
|
||||
export function extractMessageFromJsonPayload(message: string): string | undefined {
|
||||
// Find the first occurrence of '{' which may indicate JSON content
|
||||
const jsonStartIndex = message.indexOf("{")
|
||||
if (jsonStartIndex === -1) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const potentialJson = message.slice(jsonStartIndex)
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(potentialJson)
|
||||
|
||||
// Handle structure: {"error":{"message":"..."}} or {"error":{"code":"","message":"..."}}
|
||||
if (parsed?.error?.message && typeof parsed.error.message === "string") {
|
||||
return parsed.error.message
|
||||
}
|
||||
|
||||
// Handle structure: {"message":"..."}
|
||||
if (parsed?.message && typeof parsed.message === "string") {
|
||||
return parsed.message
|
||||
}
|
||||
} catch {
|
||||
// JSON parsing failed - not valid JSON
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the most descriptive error message from an error object.
|
||||
* Prioritizes nested metadata (upstream provider errors) over the standard message.
|
||||
* Also handles JSON payloads embedded in error messages.
|
||||
* @param error - The error to extract message from
|
||||
* @returns The best available error message, or undefined if not an OpenAI SDK error
|
||||
* @returns The best available error message, or undefined if not extractable
|
||||
*/
|
||||
export function getErrorMessage(error: unknown): string | undefined {
|
||||
let message: string | undefined
|
||||
|
||||
if (isOpenAISdkError(error)) {
|
||||
// Prioritize nested metadata which may contain upstream provider details
|
||||
return error.error?.metadata?.raw || error.error?.message || error.message
|
||||
message = error.error?.metadata?.raw || error.error?.message || error.message
|
||||
} else if (error instanceof Error) {
|
||||
// Handle standard Error objects (including ApiProviderError)
|
||||
message = error.message
|
||||
} else if (typeof error === "object" && error !== null && "message" in error) {
|
||||
// Handle plain objects with a message property
|
||||
const msgValue = (error as { message: unknown }).message
|
||||
if (typeof msgValue === "string") {
|
||||
message = msgValue
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
|
||||
if (!message) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// If the message contains JSON, try to extract the message from it
|
||||
const extractedMessage = extractMessageFromJsonPayload(message)
|
||||
if (extractedMessage) {
|
||||
return extractedMessage
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue