mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-30 21:59:46 +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
|
|
@ -49,6 +49,10 @@ import { useMessagesSync } from "@/hooks/use-messages-sync";
|
|||
import { getAgentFilesystemSelection } from "@/lib/agent-filesystem";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
import { getBearerToken } from "@/lib/auth-utils";
|
||||
import {
|
||||
classifyChatError,
|
||||
type ChatFlow,
|
||||
} from "@/lib/chat/chat-error-classifier";
|
||||
import { convertToThreadMessage } from "@/lib/chat/message-utils";
|
||||
import {
|
||||
isPodcastGenerating,
|
||||
|
|
@ -84,7 +88,8 @@ import {
|
|||
import { NotFoundError } from "@/lib/error";
|
||||
import {
|
||||
trackChatCreated,
|
||||
trackChatError,
|
||||
trackChatBlocked,
|
||||
trackChatErrorDetailed,
|
||||
trackChatMessageSent,
|
||||
trackChatResponseReceived,
|
||||
} from "@/lib/posthog/events";
|
||||
|
|
@ -201,26 +206,6 @@ const BASE_TOOLS_WITH_UI = new Set([
|
|||
// "write_todos", // Disabled for now
|
||||
]);
|
||||
|
||||
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 getPinnedPremiumQuotaErrorMessage(error: unknown): string | null {
|
||||
if (!(error instanceof Error)) return null;
|
||||
const withCode = error as Error & { errorCode?: string };
|
||||
if (withCode.errorCode === "PREMIUM_QUOTA_EXHAUSTED") {
|
||||
return error.message;
|
||||
}
|
||||
const normalized = error.message.toLowerCase();
|
||||
if (
|
||||
!normalized.includes("premium tokens exhausted")
|
||||
&& !normalized.includes("premium token quota exceeded")
|
||||
&& !normalized.includes("buy more tokens")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return error.message;
|
||||
}
|
||||
|
||||
export default function NewChatPage() {
|
||||
const params = useParams();
|
||||
const queryClient = useQueryClient();
|
||||
|
|
@ -378,6 +363,81 @@ export default function NewChatPage() {
|
|||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
}, [params.chat_id]);
|
||||
|
||||
const handleChatFailure = useCallback(
|
||||
async ({
|
||||
error,
|
||||
flow,
|
||||
threadId,
|
||||
assistantMsgId,
|
||||
}: {
|
||||
error: unknown;
|
||||
flow: ChatFlow;
|
||||
threadId: number | null;
|
||||
assistantMsgId: string;
|
||||
}) => {
|
||||
const normalized = classifyChatError({
|
||||
error,
|
||||
flow,
|
||||
context: {
|
||||
searchSpaceId,
|
||||
threadId,
|
||||
},
|
||||
});
|
||||
|
||||
const logger =
|
||||
normalized.severity === "error"
|
||||
? console.error
|
||||
: normalized.severity === "warn"
|
||||
? console.warn
|
||||
: console.info;
|
||||
logger(`[NewChatPage] ${flow} ${normalized.kind}:`, error);
|
||||
|
||||
const telemetryPayload = {
|
||||
flow,
|
||||
kind: normalized.kind,
|
||||
error_code: normalized.errorCode,
|
||||
severity: normalized.severity,
|
||||
is_expected: normalized.isExpected,
|
||||
message: normalized.userMessage,
|
||||
};
|
||||
if (normalized.telemetryEvent === "chat_blocked") {
|
||||
trackChatBlocked(searchSpaceId, threadId, telemetryPayload);
|
||||
} else {
|
||||
trackChatErrorDetailed(searchSpaceId, threadId, telemetryPayload);
|
||||
}
|
||||
|
||||
if (normalized.channel === "silent") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalized.channel === "pinned_inline") {
|
||||
if (threadId) {
|
||||
setPremiumAlertForThread({
|
||||
threadId,
|
||||
message: normalized.userMessage,
|
||||
userId: currentUser?.id ?? null,
|
||||
});
|
||||
}
|
||||
if (normalized.assistantMessage) {
|
||||
await persistAssistantErrorMessage({
|
||||
threadId,
|
||||
assistantMsgId,
|
||||
text: normalized.assistantMessage,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error(normalized.userMessage);
|
||||
},
|
||||
[
|
||||
currentUser?.id,
|
||||
persistAssistantErrorMessage,
|
||||
searchSpaceId,
|
||||
setPremiumAlertForThread,
|
||||
]
|
||||
);
|
||||
|
||||
// Initialize thread and load messages
|
||||
// For new chats (no urlChatId), we use lazy creation - thread is created on first message
|
||||
const initializeThread = useCallback(async () => {
|
||||
|
|
@ -1018,36 +1078,11 @@ export default function NewChatPage() {
|
|||
}
|
||||
return;
|
||||
}
|
||||
const premiumQuotaAlertMessage = getPinnedPremiumQuotaErrorMessage(error);
|
||||
if (premiumQuotaAlertMessage) {
|
||||
console.info("[NewChatPage] Premium quota exhausted:", error);
|
||||
} else {
|
||||
console.error("[NewChatPage] Chat error:", error);
|
||||
}
|
||||
|
||||
// Track chat error
|
||||
trackChatError(
|
||||
searchSpaceId,
|
||||
currentThreadId,
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
);
|
||||
|
||||
if (premiumQuotaAlertMessage) {
|
||||
setPremiumAlertForThread({
|
||||
threadId: currentThreadId,
|
||||
message: premiumQuotaAlertMessage,
|
||||
userId: currentUser?.id ?? null,
|
||||
});
|
||||
} else {
|
||||
toast.error("Failed to get response. Please try again.");
|
||||
}
|
||||
await persistAssistantErrorMessage({
|
||||
await handleChatFailure({
|
||||
error,
|
||||
flow: "new",
|
||||
threadId: currentThreadId,
|
||||
assistantMsgId,
|
||||
text:
|
||||
(premiumQuotaAlertMessage
|
||||
? PREMIUM_QUOTA_ASSISTANT_MESSAGE
|
||||
: undefined) ?? "Sorry, there was an error. Please try again.",
|
||||
});
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
|
|
@ -1071,8 +1106,7 @@ export default function NewChatPage() {
|
|||
pendingUserImageUrls,
|
||||
setPendingUserImageUrls,
|
||||
toolsWithUI,
|
||||
setPremiumAlertForThread,
|
||||
persistAssistantErrorMessage,
|
||||
handleChatFailure,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
@ -1333,28 +1367,11 @@ export default function NewChatPage() {
|
|||
if (error instanceof Error && error.name === "AbortError") {
|
||||
return;
|
||||
}
|
||||
const premiumQuotaAlertMessage = getPinnedPremiumQuotaErrorMessage(error);
|
||||
if (premiumQuotaAlertMessage) {
|
||||
console.info("[NewChatPage] Premium quota exhausted during resume:", error);
|
||||
} else {
|
||||
console.error("[NewChatPage] Resume error:", error);
|
||||
}
|
||||
if (premiumQuotaAlertMessage) {
|
||||
setPremiumAlertForThread({
|
||||
threadId: resumeThreadId,
|
||||
message: premiumQuotaAlertMessage,
|
||||
userId: currentUser?.id ?? null,
|
||||
});
|
||||
} else {
|
||||
toast.error("Failed to resume. Please try again.");
|
||||
}
|
||||
await persistAssistantErrorMessage({
|
||||
await handleChatFailure({
|
||||
error,
|
||||
flow: "resume",
|
||||
threadId: resumeThreadId,
|
||||
assistantMsgId,
|
||||
text:
|
||||
(premiumQuotaAlertMessage
|
||||
? PREMIUM_QUOTA_ASSISTANT_MESSAGE
|
||||
: undefined) ?? "Sorry, there was an error. Please try again.",
|
||||
});
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
|
|
@ -1365,11 +1382,9 @@ export default function NewChatPage() {
|
|||
pendingInterrupt,
|
||||
messages,
|
||||
searchSpaceId,
|
||||
currentUser?.id,
|
||||
tokenUsageStore,
|
||||
toolsWithUI,
|
||||
setPremiumAlertForThread,
|
||||
persistAssistantErrorMessage,
|
||||
handleChatFailure,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
@ -1491,15 +1506,6 @@ export default function NewChatPage() {
|
|||
userQueryToDisplay = newUserQuery;
|
||||
}
|
||||
|
||||
// Remove the last two messages (user + assistant) from the UI immediately
|
||||
// The backend will also delete them from the database
|
||||
setMessages((prev) => {
|
||||
if (prev.length >= 2) {
|
||||
return prev.slice(0, -2);
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
|
||||
// Start streaming
|
||||
setIsRunning(true);
|
||||
const controller = new AbortController();
|
||||
|
|
@ -1530,19 +1536,9 @@ export default function NewChatPage() {
|
|||
createdAt: new Date(),
|
||||
metadata: isEdit ? undefined : originalUserMessageMetadata,
|
||||
};
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
|
||||
// Add placeholder assistant message
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: assistantMsgId,
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "" }],
|
||||
createdAt: new Date(),
|
||||
},
|
||||
]);
|
||||
|
||||
const userContentToPersist = isEdit
|
||||
? (editExtras?.userMessageContent ?? [{ type: "text", text: newUserQuery ?? "" }])
|
||||
: originalUserMessageContent || [{ type: "text", text: userQueryToDisplay || "" }];
|
||||
try {
|
||||
const selection = await getAgentFilesystemSelection(searchSpaceId);
|
||||
const requestBody: Record<string, unknown> = {
|
||||
|
|
@ -1570,6 +1566,22 @@ export default function NewChatPage() {
|
|||
throw new Error(`Backend error: ${response.status}`);
|
||||
}
|
||||
|
||||
// Only switch UI to regenerated placeholder messages after the backend accepts
|
||||
// regenerate. This avoids local message loss when regenerate fails early (e.g. 400).
|
||||
setMessages((prev) => {
|
||||
const base = prev.length >= 2 ? prev.slice(0, -2) : prev;
|
||||
return [
|
||||
...base,
|
||||
userMessage,
|
||||
{
|
||||
id: assistantMsgId,
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "" }],
|
||||
createdAt: new Date(),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const flushMessages = () => {
|
||||
setMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
|
|
@ -1654,10 +1666,6 @@ export default function NewChatPage() {
|
|||
if (contentParts.length > 0) {
|
||||
try {
|
||||
// Persist user message (for both edit and reload modes, since backend deleted it)
|
||||
const userContentToPersist = isEdit
|
||||
? (editExtras?.userMessageContent ?? [{ type: "text", text: newUserQuery ?? "" }])
|
||||
: originalUserMessageContent || [{ type: "text", text: userQueryToDisplay || "" }];
|
||||
|
||||
const savedUserMessage = await appendMessage(threadId, {
|
||||
role: "user",
|
||||
content: userContentToPersist,
|
||||
|
|
@ -1692,33 +1700,11 @@ export default function NewChatPage() {
|
|||
return;
|
||||
}
|
||||
batcher.dispose();
|
||||
const premiumQuotaAlertMessage = getPinnedPremiumQuotaErrorMessage(error);
|
||||
if (premiumQuotaAlertMessage) {
|
||||
console.info("[NewChatPage] Premium quota exhausted during regeneration:", error);
|
||||
} else {
|
||||
console.error("[NewChatPage] Regeneration error:", error);
|
||||
}
|
||||
trackChatError(
|
||||
searchSpaceId,
|
||||
threadId,
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
);
|
||||
if (premiumQuotaAlertMessage) {
|
||||
setPremiumAlertForThread({
|
||||
threadId,
|
||||
message: premiumQuotaAlertMessage,
|
||||
userId: currentUser?.id ?? null,
|
||||
});
|
||||
} else {
|
||||
toast.error("Failed to regenerate response. Please try again.");
|
||||
}
|
||||
await persistAssistantErrorMessage({
|
||||
await handleChatFailure({
|
||||
error,
|
||||
flow: "regenerate",
|
||||
threadId,
|
||||
assistantMsgId,
|
||||
text:
|
||||
(premiumQuotaAlertMessage
|
||||
? PREMIUM_QUOTA_ASSISTANT_MESSAGE
|
||||
: undefined) ?? "Sorry, there was an error. Please try again.",
|
||||
});
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
|
|
@ -1730,11 +1716,9 @@ export default function NewChatPage() {
|
|||
searchSpaceId,
|
||||
messages,
|
||||
disabledTools,
|
||||
currentUser?.id,
|
||||
tokenUsageStore,
|
||||
toolsWithUI,
|
||||
setPremiumAlertForThread,
|
||||
persistAssistantErrorMessage,
|
||||
handleChatFailure,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
|||
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 },
|
||||
};
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import posthog from "posthog-js";
|
||||
import { getConnectorTelemetryMeta } from "@/components/assistant-ui/connector-popup/constants/connector-constants";
|
||||
import type { ChatErrorKind, ChatFlow, ChatErrorSeverity } from "@/lib/chat/chat-error-classifier";
|
||||
|
||||
/**
|
||||
* PostHog Analytics Event Definitions
|
||||
|
|
@ -139,6 +140,55 @@ export function trackChatError(searchSpaceId: number, chatId: number, error?: st
|
|||
});
|
||||
}
|
||||
|
||||
export interface ChatFailureTelemetry {
|
||||
flow: ChatFlow;
|
||||
kind: ChatErrorKind;
|
||||
error_code?: string;
|
||||
severity: ChatErrorSeverity;
|
||||
is_expected: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export function trackChatBlocked(
|
||||
searchSpaceId: number,
|
||||
chatId: number | null,
|
||||
payload: ChatFailureTelemetry
|
||||
) {
|
||||
safeCapture(
|
||||
"chat_blocked",
|
||||
compact({
|
||||
search_space_id: searchSpaceId,
|
||||
chat_id: chatId ?? undefined,
|
||||
flow: payload.flow,
|
||||
kind: payload.kind,
|
||||
error_code: payload.error_code,
|
||||
severity: payload.severity,
|
||||
is_expected: payload.is_expected,
|
||||
message: payload.message,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function trackChatErrorDetailed(
|
||||
searchSpaceId: number,
|
||||
chatId: number | null,
|
||||
payload: ChatFailureTelemetry
|
||||
) {
|
||||
safeCapture(
|
||||
"chat_error",
|
||||
compact({
|
||||
search_space_id: searchSpaceId,
|
||||
chat_id: chatId ?? undefined,
|
||||
flow: payload.flow,
|
||||
kind: payload.kind,
|
||||
error_code: payload.error_code,
|
||||
severity: payload.severity,
|
||||
is_expected: payload.is_expected,
|
||||
message: payload.message,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a message sent from the unauthenticated "free" / anonymous chat
|
||||
* flow. This is intentionally a separate event from `chat_message_sent`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue