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,
],
);