mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
fix(web): log dropped AI chat connections as warnings so RUM does not record them as errors
This commit is contained in:
@@ -486,6 +486,22 @@ describe("useAiChat", () => {
|
||||
expect(result).toBe(mockResponse);
|
||||
});
|
||||
|
||||
it("should log a dropped connection as a warning, not an error", async () => {
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const error = new TypeError("Failed to fetch");
|
||||
mockFetch.mockRejectedValue(error);
|
||||
|
||||
const result = await aiChatComposable.fetchAiChat(mockMessages, "gpt-4", "test-org");
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(errorSpy).not.toHaveBeenCalledWith("Error fetching AI chat:", expect.anything());
|
||||
expect(warnSpy).toHaveBeenCalledWith("Error fetching AI chat:", error);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should return null when fetch throws an error", async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
mockFetch.mockRejectedValue(new Error("Network error"));
|
||||
|
||||
@@ -3,6 +3,7 @@ import { contextRegistry, createDefaultContextProvider } from "@/composables/con
|
||||
import { generateTraceContext, getUUIDv7 } from "@/utils/zincutils";
|
||||
import type { ImageAttachment } from "@/ts/interfaces/chat";
|
||||
import analytics from "@/services/product_analytics";
|
||||
import { isNetworkFailure } from "@/utils/networkFailure";
|
||||
|
||||
let contextHandler: any;
|
||||
|
||||
@@ -140,7 +141,9 @@ const useAiChat = () => {
|
||||
// Return a special response to indicate cancellation
|
||||
return { cancelled: true, error };
|
||||
} else {
|
||||
console.error("Error fetching AI chat:", error);
|
||||
// A dropped connection is not an app defect, and RUM records console.error.
|
||||
if (isNetworkFailure(error)) console.warn("Error fetching AI chat:", error);
|
||||
else console.error("Error fetching AI chat:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,6 +183,44 @@ describe("useChatStream", () => {
|
||||
await turn;
|
||||
});
|
||||
|
||||
describe("dropped stream", () => {
|
||||
const droppingResponse = (error: Error) => ({
|
||||
ok: true,
|
||||
body: {
|
||||
getReader: () => ({
|
||||
read: async () => {
|
||||
throw error;
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
it("logs a dropped connection as a warning, not an error", async () => {
|
||||
const error = new TypeError("network error");
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
mockFetchAiChat.mockResolvedValue(droppingResponse(error));
|
||||
const { stream } = makeStream();
|
||||
|
||||
await stream.runTurn(false, []);
|
||||
|
||||
expect(errorSpy).not.toHaveBeenCalledWith("Error reading stream:", expect.anything());
|
||||
expect(warnSpy).toHaveBeenCalledWith("Error reading stream:", error);
|
||||
expect(trackedEvents()).toContain("ai_assistant_answer_failed");
|
||||
});
|
||||
|
||||
it("still logs an unexpected read failure as an error", async () => {
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const error = new Error("boom");
|
||||
mockFetchAiChat.mockResolvedValue(droppingResponse(error));
|
||||
const { stream } = makeStream();
|
||||
|
||||
await stream.runTurn(false, []);
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith("Error reading stream:", error);
|
||||
});
|
||||
});
|
||||
|
||||
it("runs teardown when the fetch throws, clearing loading and the controller", async () => {
|
||||
mockFetchAiChat.mockRejectedValue(new Error("network"));
|
||||
const { stream } = makeStream();
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from "@/components/O2AIChat.reducer";
|
||||
import { isChatPersistenceEnabled } from "@/components/ai-assistant/share/chatShare";
|
||||
import useAiChat from "@/composables/useAiChat";
|
||||
import { isNetworkFailure } from "@/utils/networkFailure";
|
||||
import {
|
||||
isPaidOverageConsentError,
|
||||
usePaidOverageConsent,
|
||||
@@ -572,7 +573,9 @@ export function useChatStream(options: UseChatStreamOptions) {
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
console.error("Error reading stream:", error);
|
||||
// A dropped connection is not an app defect, and RUM records console.error.
|
||||
if (isNetworkFailure(error)) console.warn("Error reading stream:", error);
|
||||
else console.error("Error reading stream:", error);
|
||||
trackAnswerOutcome(turn, "ai_assistant_answer_failed", { stage: "stream" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { getUUID } from "@/utils/zincutils";
|
||||
import { expireSession } from "@/utils/sessionServices";
|
||||
import { attemptTokenRefresh } from "@/services/http";
|
||||
import { patchLargeNumbersInJson } from "@/utils/nsFieldsPatch";
|
||||
import { isNetworkFailure } from "@/utils/networkFailure";
|
||||
|
||||
// Create and manage stream workers
|
||||
let streamWorker: Worker | null = null;
|
||||
@@ -34,11 +35,6 @@ const createStreamWorker = () => {
|
||||
return streamWorker;
|
||||
};
|
||||
|
||||
// Chrome, Safari and Firefox wordings of a fetch body read that lost its connection.
|
||||
const NETWORK_FAILURE_PATTERN = /network error|Load failed|Failed to fetch|NetworkError/i;
|
||||
const isNetworkFailure = (error: unknown): boolean =>
|
||||
error instanceof TypeError && NETWORK_FAILURE_PATTERN.test(error.message);
|
||||
|
||||
// Status and message sit on prototypes of a Response or Error, which the error spread would drop.
|
||||
const withErrorFields = (error: unknown): unknown =>
|
||||
error instanceof Response
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2026 OpenObserve Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isNetworkFailure } from "@/utils/networkFailure";
|
||||
|
||||
describe("isNetworkFailure", () => {
|
||||
it.each(["network error", "Load failed", "Failed to fetch", "NetworkError when attempting"])(
|
||||
"matches a TypeError saying %s",
|
||||
(message) => expect(isNetworkFailure(new TypeError(message))).toBe(true),
|
||||
);
|
||||
|
||||
it("rejects other errors", () => {
|
||||
expect(isNetworkFailure(new Error("network error"))).toBe(false);
|
||||
expect(isNetworkFailure(new TypeError("x is undefined"))).toBe(false);
|
||||
expect(isNetworkFailure("Failed to fetch")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2026 OpenObserve Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Chrome, Safari and Firefox wordings of a fetch body read that lost its connection.
|
||||
const NETWORK_FAILURE_PATTERN = /network error|Load failed|Failed to fetch|NetworkError/i;
|
||||
|
||||
export const isNetworkFailure = (error: unknown): boolean =>
|
||||
error instanceof TypeError && NETWORK_FAILURE_PATTERN.test(error.message);
|
||||
Reference in New Issue
Block a user