From 5367bef43a9fbe756f79760592ace0e898523410 Mon Sep 17 00:00:00 2001 From: "sp.wack" <83104063+amanape@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:55:11 +0400 Subject: [PATCH] fix: detect team/org-level budget errors in error banner (#13003) --- .../conversation-websocket-handler.test.tsx | 24 +++++++++++ frontend/__tests__/posthog-tracking.test.tsx | 35 +++++++++++++++- .../conversation-websocket-context.tsx | 40 ++++++++++++++++++- 3 files changed, 97 insertions(+), 2 deletions(-) diff --git a/frontend/__tests__/conversation-websocket-handler.test.tsx b/frontend/__tests__/conversation-websocket-handler.test.tsx index 6e6dadaca4..284aaee287 100644 --- a/frontend/__tests__/conversation-websocket-handler.test.tsx +++ b/frontend/__tests__/conversation-websocket-handler.test.tsx @@ -358,6 +358,30 @@ describe("Conversation WebSocket Handler", () => { }); }); + it("should show friendly i18n message for budget ConversationErrorEvent", async () => { + const mockBudgetConversationError = createMockConversationErrorEvent({ + detail: + "Budget has been exceeded! Current cost: 18.51, Max budget: 18.24", + }); + + mswServer.use( + wsLink.addEventListener("connection", ({ client, server }) => { + server.connect(); + client.send(JSON.stringify(mockBudgetConversationError)); + }), + ); + + renderWithWebSocketContext(); + + expect(screen.getByTestId("error-message")).toHaveTextContent("none"); + + await waitFor(() => { + expect(screen.getByTestId("error-message")).toHaveTextContent( + "STATUS$ERROR_LLM_OUT_OF_CREDITS", + ); + }); + }); + it("should set error message store on WebSocket connection errors", async () => { // Simulate a connect-then-fail sequence (the MSW server auto-connects by default). // This should surface an error message because the app has previously connected. diff --git a/frontend/__tests__/posthog-tracking.test.tsx b/frontend/__tests__/posthog-tracking.test.tsx index 5d76649013..a2b42657d8 100644 --- a/frontend/__tests__/posthog-tracking.test.tsx +++ b/frontend/__tests__/posthog-tracking.test.tsx @@ -9,7 +9,10 @@ import { } from "vitest"; import { screen, waitFor, render, cleanup } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { createMockAgentErrorEvent } from "#/mocks/mock-ws-helpers"; +import { + createMockAgentErrorEvent, + createMockConversationErrorEvent, +} from "#/mocks/mock-ws-helpers"; import { ConversationWebSocketProvider } from "#/contexts/conversation-websocket-context"; import { conversationWebSocketTestSetup } from "./helpers/msw-websocket-setup"; import { ConnectionStatusComponent } from "./helpers/websocket-test-components"; @@ -229,5 +232,35 @@ describe("PostHog Analytics Tracking", () => { }), ); }); + + it("should track credit_limit_reached when ConversationErrorEvent contains budget error", async () => { + const mockBudgetConversationError = createMockConversationErrorEvent({ + detail: + "Budget has been exceeded! Current cost: 18.51, Max budget: 18.24", + }); + + mswServer.use( + wsLink.addEventListener("connection", ({ client, server }) => { + server.connect(); + client.send(JSON.stringify(mockBudgetConversationError)); + }), + ); + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByTestId("connection-state")).toHaveTextContent( + "OPEN", + ); + }); + + await waitFor(() => { + expect(mockTrackCreditLimitReached).toHaveBeenCalledWith( + expect.objectContaining({ + conversationId: "test-conversation-123", + }), + ); + }); + }); }); }); diff --git a/frontend/src/contexts/conversation-websocket-context.tsx b/frontend/src/contexts/conversation-websocket-context.tsx index ca858b6b47..572ab4fd75 100644 --- a/frontend/src/contexts/conversation-websocket-context.tsx +++ b/frontend/src/contexts/conversation-websocket-context.tsx @@ -358,7 +358,14 @@ export function ConversationWebSocketProvider({ }, posthog, }); - setErrorMessage(event.detail); + if (isBudgetOrCreditError(event.detail)) { + setErrorMessage(I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS); + trackCreditLimitReached({ + conversationId: conversationId || "unknown", + }); + } else { + setErrorMessage(event.detail); + } } else { // Clear error message on any non-ConversationErrorEvent removeErrorMessage(); @@ -498,6 +505,31 @@ export function ConversationWebSocketProvider({ }; addEvent(eventWithPlanningFlag); + // Handle ConversationErrorEvent specifically - show error banner + // AgentErrorEvent errors are displayed inline in the chat, not as banners + if (isConversationErrorEvent(event)) { + trackError({ + message: event.detail, + source: "planning_conversation", + metadata: { + eventId: event.id, + errorCode: event.code, + }, + posthog, + }); + if (isBudgetOrCreditError(event.detail)) { + setErrorMessage(I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS); + trackCreditLimitReached({ + conversationId: conversationId || "unknown", + }); + } else { + setErrorMessage(event.detail); + } + } else { + // Clear error message on any non-ConversationErrorEvent + removeErrorMessage(); + } + // Handle AgentErrorEvent specifically if (isAgentErrorEvent(event)) { trackError({ @@ -513,6 +545,9 @@ export function ConversationWebSocketProvider({ // Use friendly i18n message for budget/credit errors instead of raw error if (isBudgetOrCreditError(event.error)) { setErrorMessage(I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS); + trackCreditLimitReached({ + conversationId: conversationId || "unknown", + }); } else { setErrorMessage(event.error); } @@ -609,15 +644,18 @@ export function ConversationWebSocketProvider({ isLoadingHistoryPlanning, expectedEventCountPlanning, setErrorMessage, + removeErrorMessage, removeOptimisticUserMessage, queryClient, subConversations, + conversationId, setExecutionStatus, appendInput, appendOutput, readConversationFile, setPlanContent, updateMetricsFromStats, + trackCreditLimitReached, posthog, ], );