mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-04 05:12:38 +02:00
feat(chat): unify error handling and logging for chat operations, enhancing clarity and consistency in error reporting
This commit is contained in:
parent
222b27183f
commit
d64543686f
6 changed files with 831 additions and 171 deletions
273
surfsense_web/lib/chat/chat-error-classifier.ts
Normal file
273
surfsense_web/lib/chat/chat-error-classifier.ts
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
export type ChatFlow = "new" | "resume" | "regenerate";
|
||||
|
||||
export type ChatErrorKind =
|
||||
| "premium_quota_exhausted"
|
||||
| "auth_expired"
|
||||
| "rate_limited"
|
||||
| "network_offline"
|
||||
| "stream_interrupted"
|
||||
| "stream_parse_error"
|
||||
| "tool_execution_error"
|
||||
| "persist_message_failed"
|
||||
| "server_error"
|
||||
| "unknown";
|
||||
|
||||
export type ChatErrorChannel = "pinned_inline" | "toast" | "silent";
|
||||
export type ChatTelemetryEvent = "chat_blocked" | "chat_error";
|
||||
export type ChatErrorSeverity = "info" | "warn" | "error";
|
||||
|
||||
export interface NormalizedChatError {
|
||||
kind: ChatErrorKind;
|
||||
channel: ChatErrorChannel;
|
||||
severity: ChatErrorSeverity;
|
||||
telemetryEvent: ChatTelemetryEvent;
|
||||
isExpected: boolean;
|
||||
userMessage: string;
|
||||
assistantMessage?: string;
|
||||
rawMessage?: string;
|
||||
errorCode?: string;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface RawChatErrorInput {
|
||||
error: unknown;
|
||||
flow: ChatFlow;
|
||||
context?: {
|
||||
searchSpaceId?: number;
|
||||
threadId?: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
export const PREMIUM_QUOTA_ASSISTANT_MESSAGE =
|
||||
"I can’t continue with the current premium model because your premium tokens are exhausted. Switch to a free model or buy more tokens to continue.";
|
||||
|
||||
function getErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) return error.message;
|
||||
if (typeof error === "string") return error;
|
||||
try {
|
||||
return JSON.stringify(error);
|
||||
} catch {
|
||||
return "Unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
function getErrorCode(error: unknown, parsedJson: Record<string, unknown> | null): string | undefined {
|
||||
if (error instanceof Error) {
|
||||
const withCode = error as Error & { errorCode?: string };
|
||||
if (withCode.errorCode) return withCode.errorCode;
|
||||
}
|
||||
|
||||
if (typeof error === "object" && error !== null) {
|
||||
const withCode = error as { errorCode?: unknown };
|
||||
if (typeof withCode.errorCode === "string" && withCode.errorCode) {
|
||||
return withCode.errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedJson) {
|
||||
const topLevelCode = parsedJson.errorCode;
|
||||
if (typeof topLevelCode === "string" && topLevelCode) {
|
||||
return topLevelCode;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseEmbeddedJson(text: string): Record<string, unknown> | null {
|
||||
const candidates = [text];
|
||||
const firstBraceIdx = text.indexOf("{");
|
||||
if (firstBraceIdx >= 0) {
|
||||
candidates.push(text.slice(firstBraceIdx));
|
||||
}
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const parsed = JSON.parse(candidate);
|
||||
if (typeof parsed === "object" && parsed !== null) {
|
||||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function inferProviderErrorType(parsedJson: Record<string, unknown> | null): string | undefined {
|
||||
if (!parsedJson) return undefined;
|
||||
const topLevelType = parsedJson.type;
|
||||
if (typeof topLevelType === "string" && topLevelType) return topLevelType;
|
||||
const nestedError = parsedJson.error;
|
||||
if (typeof nestedError === "object" && nestedError !== null) {
|
||||
const nestedType = (nestedError as Record<string, unknown>).type;
|
||||
if (typeof nestedType === "string" && nestedType) return nestedType;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function classifyChatError(input: RawChatErrorInput): NormalizedChatError {
|
||||
const { error } = input;
|
||||
const rawMessage = getErrorMessage(error);
|
||||
const parsedJson = parseEmbeddedJson(rawMessage);
|
||||
const errorCode = getErrorCode(error, parsedJson);
|
||||
const providerErrorType = inferProviderErrorType(parsedJson);
|
||||
const providerTypeNormalized = providerErrorType?.toLowerCase() ?? "";
|
||||
const errorName = error instanceof Error ? error.name : undefined;
|
||||
|
||||
if (errorName === "AbortError") {
|
||||
return {
|
||||
kind: "stream_interrupted",
|
||||
channel: "silent",
|
||||
severity: "info",
|
||||
telemetryEvent: "chat_error",
|
||||
isExpected: true,
|
||||
userMessage: "Request canceled.",
|
||||
rawMessage,
|
||||
errorCode,
|
||||
details: { flow: input.flow },
|
||||
};
|
||||
}
|
||||
|
||||
if (errorCode === "PREMIUM_QUOTA_EXHAUSTED") {
|
||||
return {
|
||||
kind: "premium_quota_exhausted",
|
||||
channel: "pinned_inline",
|
||||
severity: "info",
|
||||
telemetryEvent: "chat_blocked",
|
||||
isExpected: true,
|
||||
userMessage:
|
||||
"Buy more tokens to continue with this model, or switch to a free model.",
|
||||
assistantMessage: PREMIUM_QUOTA_ASSISTANT_MESSAGE,
|
||||
rawMessage,
|
||||
errorCode: errorCode ?? "PREMIUM_QUOTA_EXHAUSTED",
|
||||
details: { flow: input.flow },
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
errorCode === "AUTH_EXPIRED" ||
|
||||
errorCode === "UNAUTHORIZED"
|
||||
) {
|
||||
return {
|
||||
kind: "auth_expired",
|
||||
channel: "toast",
|
||||
severity: "warn",
|
||||
telemetryEvent: "chat_error",
|
||||
isExpected: true,
|
||||
userMessage: "Your session expired. Please sign in again.",
|
||||
rawMessage,
|
||||
errorCode: errorCode ?? "AUTH_EXPIRED",
|
||||
details: { flow: input.flow },
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
errorCode === "RATE_LIMITED" ||
|
||||
providerTypeNormalized === "rate_limit_error"
|
||||
) {
|
||||
return {
|
||||
kind: "rate_limited",
|
||||
channel: "toast",
|
||||
severity: "warn",
|
||||
telemetryEvent: "chat_blocked",
|
||||
isExpected: true,
|
||||
userMessage:
|
||||
"This model is temporarily rate-limited. Please try again in a few seconds or switch models.",
|
||||
rawMessage,
|
||||
errorCode: errorCode ?? "RATE_LIMITED",
|
||||
details: { flow: input.flow, providerErrorType },
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
errorCode === "NETWORK_ERROR"
|
||||
) {
|
||||
return {
|
||||
kind: "network_offline",
|
||||
channel: "toast",
|
||||
severity: "warn",
|
||||
telemetryEvent: "chat_error",
|
||||
isExpected: true,
|
||||
userMessage: "Connection issue detected. Check your internet and try again.",
|
||||
rawMessage,
|
||||
errorCode: errorCode ?? "NETWORK_ERROR",
|
||||
details: { flow: input.flow },
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
errorCode === "STREAM_PARSE_ERROR"
|
||||
) {
|
||||
return {
|
||||
kind: "stream_parse_error",
|
||||
channel: "toast",
|
||||
severity: "error",
|
||||
telemetryEvent: "chat_error",
|
||||
isExpected: false,
|
||||
userMessage: "We hit a response formatting issue. Please try again.",
|
||||
rawMessage,
|
||||
errorCode: errorCode ?? "STREAM_PARSE_ERROR",
|
||||
details: { flow: input.flow },
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
errorCode === "TOOL_EXECUTION_ERROR"
|
||||
) {
|
||||
return {
|
||||
kind: "tool_execution_error",
|
||||
channel: "toast",
|
||||
severity: "error",
|
||||
telemetryEvent: "chat_error",
|
||||
isExpected: false,
|
||||
userMessage: "A tool failed while processing your request. Please try again.",
|
||||
rawMessage,
|
||||
errorCode: errorCode ?? "TOOL_EXECUTION_ERROR",
|
||||
details: { flow: input.flow },
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
errorCode === "PERSIST_MESSAGE_FAILED"
|
||||
) {
|
||||
return {
|
||||
kind: "persist_message_failed",
|
||||
channel: "toast",
|
||||
severity: "error",
|
||||
telemetryEvent: "chat_error",
|
||||
isExpected: false,
|
||||
userMessage: "Response generated, but saving failed. Please retry once.",
|
||||
rawMessage,
|
||||
errorCode: errorCode ?? "PERSIST_MESSAGE_FAILED",
|
||||
details: { flow: input.flow },
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
errorCode === "SERVER_ERROR"
|
||||
) {
|
||||
return {
|
||||
kind: "server_error",
|
||||
channel: "toast",
|
||||
severity: "error",
|
||||
telemetryEvent: "chat_error",
|
||||
isExpected: false,
|
||||
userMessage: "We couldn’t complete this response right now. Please try again.",
|
||||
rawMessage,
|
||||
errorCode: errorCode ?? "SERVER_ERROR",
|
||||
details: { flow: input.flow, providerErrorType },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "unknown",
|
||||
channel: "toast",
|
||||
severity: "error",
|
||||
telemetryEvent: "chat_error",
|
||||
isExpected: false,
|
||||
userMessage: "We couldn’t complete this response right now. Please try again.",
|
||||
rawMessage,
|
||||
errorCode,
|
||||
details: { flow: input.flow, providerErrorType },
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue