From 08fa891c14bda33318900dfa17b8fff2f29e9738 Mon Sep 17 00:00:00 2001 From: Santiago Morelle Date: Wed, 13 Aug 2025 14:15:56 -0300 Subject: [PATCH 1/4] Add retry logic and update fetchResponse to handle network issues --- browser-extension/src/background.ts | 6 ++--- browser-extension/src/constants.ts | 1 + browser-extension/src/pages/Index.vue | 9 +++++-- .../src/scripts/agent-session.ts | 23 +++++++++++++++-- browser-extension/src/scripts/http.ts | 25 ++++++++++++++++++- 5 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 browser-extension/src/constants.ts diff --git a/browser-extension/src/background.ts b/browser-extension/src/background.ts index 522b2a9..592f7c5 100644 --- a/browser-extension/src/background.ts +++ b/browser-extension/src/background.ts @@ -5,7 +5,7 @@ import { AgentSession } from "./scripts/agent-session"; import { findAgentSession, saveAgentSession, removeAgentSession, } from "./scripts/agent-session-repository"; import { saveAgentPrompts } from "./scripts/prompt-repository"; import { BrowserMessage, ToggleSidebar, ActiveTabListener, ActivateAgent, AgentActivation, InteractionSummary, } from "./scripts/browser-message"; -import { HttpServiceError } from "./scripts/http"; +import { HttpServiceError, NetworkError } from "./scripts/http"; import { isActiveTabListener, setTabListenerActive, removeTabListenerStatus, } from "./scripts/tab-listener-status-repository"; import { removeTabState } from "./scripts/tab-state-repository"; @@ -174,8 +174,8 @@ async function asyncProcessRequest(req: RequestEvent) { sendToTab( tabId, new InteractionSummary( - false, - e instanceof HttpServiceError ? e.detail : undefined + false, + (e instanceof HttpServiceError || e instanceof NetworkError) ? e.detail : undefined ) ); } diff --git a/browser-extension/src/constants.ts b/browser-extension/src/constants.ts new file mode 100644 index 0000000..9eba24c --- /dev/null +++ b/browser-extension/src/constants.ts @@ -0,0 +1 @@ +export const NETWORK_ERROR = "NETWORK_ERROR" \ No newline at end of file diff --git a/browser-extension/src/pages/Index.vue b/browser-extension/src/pages/Index.vue index bb46aa6..6d07ede 100644 --- a/browser-extension/src/pages/Index.vue +++ b/browser-extension/src/pages/Index.vue @@ -14,6 +14,7 @@ import CopilotChat from '../components/CopilotChat.vue' import CopilotList from '../components/CopilotList.vue' import ToastMessage from '../components/ToastMessage.vue' import { HttpServiceError } from "../scripts/http" +import { NETWORK_ERROR } from "../constants" const toast = useToast() const { t } = useI18n() @@ -165,7 +166,9 @@ const onAgentActivation = (msg: AgentActivation) => { } const onInteractionSummary = (msg: InteractionSummary) => { - const text = msg.text ? msg.text : t('interactionSummaryError', { contactEmail: agent.value!.manifest.contactEmail }) + const text = msg.text + ? (msg.text === NETWORK_ERROR ? t('networkError') : msg.text) + : t('interactionSummaryError', { contactEmail: agent.value!.manifest.contactEmail }) const lastMessage = messages.value[messages.value.length - 1] const messagePosition = lastMessage.isComplete ? messages.value.length : messages.value.length - 1 messages.value.splice(messagePosition, 0, msg.success ? ChatMessage.agentMessage(text) : ChatMessage.agentErrorMessage(text)) @@ -257,12 +260,14 @@ const sidebarClasses = computed(() => [ "interactionSummaryError": "I could not process some information from the current site. This might impact the information and answers I provide. If the issue persists please contact [support](mailto:{contactEmail}?subject=Interaction%20issue)", "agentAnswerError": "I am currently unable to complete your request. You can try again and if the issue persists contact [support](mailto:{contactEmail}?subject=Question%20issue)", "flowStepMissingElement": "I could not find the element '{selector}'. This might be due to recent changes in the page which I am not aware of. Please try again and if the issue persists contact [support](mailto:{contactEmail}?subject=Navigation%20element).", + "networkError": "It seems there is a problem with the network connection. Please check your connection and try again." }, "es": { "activationError": "No se pudo activar el Copiloto {agentName}. Puedes intentar de nuevo y si el problema persiste contactar al [soporte del Copiloto {agentName}](mailto:{contactEmail}?subject=Activation%20issue)", "interactionSummaryError": "No pude procesar informacion generada por la página actual. Esto puede impactar en la información y respuestas que te puedo dar. Si el problema persiste por favor contacta a [soporte](mailto:{contactEmail})?subject=Interaction%20issue", "agentAnswerError": "Ahora no puedo completar tu pedido. Puedes intentar de nuevo y si el problema persiste contactar a [soporte](mailto:{contactEmail}?subject=Question%20issue)", - "flowStepMissingElement": "No pude encontrar el elemento '{selector}'. Esto puede ser debido a cambios recientes en la página de los cuales no tengo conocimiento. Por favor intenta de nuevo y si el problema persiste contacta a [soporte](mailto:{contactEmail}?subject=Navigation%20element).", + "flowStepMissingElement": "No pude encontrar el elemento '{selector}'. Esto puede ser debido a cambios recientes en la página de los cuales no tengo conocimiento. Por favor intenta de nuevo y si el problema persiste contacta a [soporte](mailto:{contactEmail}?subject=Navigation%20element).", + "networkError": "Parece que hay un problema con la conexión a la red. Por favor verifica tu conexión y vuelve a intentarlo." } } diff --git a/browser-extension/src/scripts/agent-session.ts b/browser-extension/src/scripts/agent-session.ts index f67038e..0ae48da 100644 --- a/browser-extension/src/scripts/agent-session.ts +++ b/browser-extension/src/scripts/agent-session.ts @@ -105,7 +105,7 @@ export class AgentSession { private async pollInteraction(msgSender: (msg: BrowserMessage) => void) { try { - const summary = await this.agent.solveInteractionSummary(undefined, this.id!, this.authService) + const summary = await this.retryInteractionSummary(undefined, this.id!, this.authService) if (summary) { await msgSender(new InteractionSummary(true, summary)) } @@ -120,7 +120,7 @@ export class AgentSession { for (const a of actions) { if (a.recordInteraction) { const interactionDetail = await this.findInteraction(req, a.recordInteraction) - return await this.agent.solveInteractionSummary(interactionDetail, this.id!, this.authService) + return await this.retryInteractionSummary(interactionDetail, this.id!, this.authService) } } } @@ -189,4 +189,23 @@ export class AgentSession { } } + private async retryInteractionSummary(interactionDetail: any | undefined, sessionId: string, authService?: AuthService, maxRetries = 2, delayMs = 5000): Promise { + let attempts = 0 + let lastError: any + + while (attempts <= maxRetries) { + try { + return await this.agent.solveInteractionSummary(interactionDetail, sessionId, authService) + } catch (error) { + lastError = error + attempts++ + + if (attempts <= maxRetries) { + await new Promise(resolve => setTimeout(resolve, delayMs)) + } + } + } + + throw lastError + } } diff --git a/browser-extension/src/scripts/http.ts b/browser-extension/src/scripts/http.ts index 49f3c26..b2e74de 100644 --- a/browser-extension/src/scripts/http.ts +++ b/browser-extension/src/scripts/http.ts @@ -1,10 +1,24 @@ +import { NETWORK_ERROR } from "../constants" + export const fetchJson = async (url: string, options?: RequestInit) => { let ret = await fetchResponse(url, options) return await ret.json() } const fetchResponse = async (url: string, options?: RequestInit) => { - let ret = await fetch(url, options) + let ret + + try { + ret = await fetch(url, options) + } catch (error) { + // This handles the case where the user is temporarily disconnected from the internet + if (error instanceof TypeError && error.message.includes("Failed to fetch")) { + throw new NetworkError(NETWORK_ERROR) + } + + throw error + } + if (ret.status < 200 || ret.status >= 300) { let body = await ret.text() console.warn(`Problem with ${options?.method ? options.method : 'GET'} ${url}`, { status: ret.status, body: body }) @@ -29,6 +43,15 @@ export class HttpServiceError extends Error { } +export class NetworkError extends TypeError { + detail?: string + + constructor(detail?: string) { + super() + this.detail = detail + } +} + export async function* fetchStreamJson(url: string, options?: RequestInit): AsyncIterable { let resp = await fetchResponse(url, options) let contentType = resp.headers.get("content-type") From a2f66f1fd7d99def5cba9788c80ef7bf487dfd5a Mon Sep 17 00:00:00 2001 From: Santiago Morelle Date: Wed, 13 Aug 2025 14:31:19 -0300 Subject: [PATCH 2/4] Improve error classes to avoid duplicate code --- browser-extension/src/scripts/http.ts | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/browser-extension/src/scripts/http.ts b/browser-extension/src/scripts/http.ts index b2e74de..6de57fd 100644 --- a/browser-extension/src/scripts/http.ts +++ b/browser-extension/src/scripts/http.ts @@ -33,24 +33,20 @@ const fetchResponse = async (url: string, options?: RequestInit) => { return ret } -export class HttpServiceError extends Error { - detail?: string +function errorWithDetail any>(Base: T) { + return class extends Base { + detail?: string - constructor(detail?: string) { - super() - this.detail = detail + constructor(...args: any[]) { + super(args[0]) + this.detail = args[0] + } } - } -export class NetworkError extends TypeError { - detail?: string +export class HttpServiceError extends errorWithDetail(Error) {} - constructor(detail?: string) { - super() - this.detail = detail - } -} +export class NetworkError extends errorWithDetail(TypeError) {} export async function* fetchStreamJson(url: string, options?: RequestInit): AsyncIterable { let resp = await fetchResponse(url, options) From a6538902d72b70f67220819061325e582558aace Mon Sep 17 00:00:00 2001 From: Santiago Morelle Date: Thu, 14 Aug 2025 12:48:47 -0300 Subject: [PATCH 3/4] Add BaseError class and remove constants file --- browser-extension/src/constants.ts | 1 - browser-extension/src/pages/Index.vue | 3 +-- browser-extension/src/scripts/http.ts | 24 +++++++++++------------- 3 files changed, 12 insertions(+), 16 deletions(-) delete mode 100644 browser-extension/src/constants.ts diff --git a/browser-extension/src/constants.ts b/browser-extension/src/constants.ts deleted file mode 100644 index 9eba24c..0000000 --- a/browser-extension/src/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const NETWORK_ERROR = "NETWORK_ERROR" \ No newline at end of file diff --git a/browser-extension/src/pages/Index.vue b/browser-extension/src/pages/Index.vue index 6d07ede..2e31d9e 100644 --- a/browser-extension/src/pages/Index.vue +++ b/browser-extension/src/pages/Index.vue @@ -14,7 +14,6 @@ import CopilotChat from '../components/CopilotChat.vue' import CopilotList from '../components/CopilotList.vue' import ToastMessage from '../components/ToastMessage.vue' import { HttpServiceError } from "../scripts/http" -import { NETWORK_ERROR } from "../constants" const toast = useToast() const { t } = useI18n() @@ -167,7 +166,7 @@ const onAgentActivation = (msg: AgentActivation) => { const onInteractionSummary = (msg: InteractionSummary) => { const text = msg.text - ? (msg.text === NETWORK_ERROR ? t('networkError') : msg.text) + ? (msg.text === "Failed to fetch" ? t('networkError') : msg.text) : t('interactionSummaryError', { contactEmail: agent.value!.manifest.contactEmail }) const lastMessage = messages.value[messages.value.length - 1] const messagePosition = lastMessage.isComplete ? messages.value.length : messages.value.length - 1 diff --git a/browser-extension/src/scripts/http.ts b/browser-extension/src/scripts/http.ts index 6de57fd..09700c4 100644 --- a/browser-extension/src/scripts/http.ts +++ b/browser-extension/src/scripts/http.ts @@ -1,5 +1,3 @@ -import { NETWORK_ERROR } from "../constants" - export const fetchJson = async (url: string, options?: RequestInit) => { let ret = await fetchResponse(url, options) return await ret.json() @@ -12,8 +10,10 @@ const fetchResponse = async (url: string, options?: RequestInit) => { ret = await fetch(url, options) } catch (error) { // This handles the case where the user is temporarily disconnected from the internet - if (error instanceof TypeError && error.message.includes("Failed to fetch")) { - throw new NetworkError(NETWORK_ERROR) + const partialErrorMessage = "Failed to fetch" + + if (error instanceof TypeError && error.message.includes(partialErrorMessage)) { + throw new NetworkError(partialErrorMessage) } throw error @@ -33,20 +33,18 @@ const fetchResponse = async (url: string, options?: RequestInit) => { return ret } -function errorWithDetail any>(Base: T) { - return class extends Base { - detail?: string +class BaseError extends Error { + detail?: string - constructor(...args: any[]) { - super(args[0]) - this.detail = args[0] - } + constructor(detail?: string) { + super() + this.detail = detail } } -export class HttpServiceError extends errorWithDetail(Error) {} +export class HttpServiceError extends BaseError {} -export class NetworkError extends errorWithDetail(TypeError) {} +export class NetworkError extends BaseError {} export async function* fetchStreamJson(url: string, options?: RequestInit): AsyncIterable { let resp = await fetchResponse(url, options) From d8b48ed8a7dc10375f6338303cd76b5fe039e90d Mon Sep 17 00:00:00 2001 From: Santiago Morelle Date: Fri, 15 Aug 2025 11:35:45 -0300 Subject: [PATCH 4/4] Add errorType field to InteractionSummary to handle errors based on error type and not content --- browser-extension/src/background.ts | 5 +++-- browser-extension/src/pages/Index.vue | 11 ++++++++--- browser-extension/src/scripts/browser-message.ts | 6 ++++-- browser-extension/src/scripts/http.ts | 15 ++++++++++++--- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/browser-extension/src/background.ts b/browser-extension/src/background.ts index 592f7c5..6abb706 100644 --- a/browser-extension/src/background.ts +++ b/browser-extension/src/background.ts @@ -5,7 +5,7 @@ import { AgentSession } from "./scripts/agent-session"; import { findAgentSession, saveAgentSession, removeAgentSession, } from "./scripts/agent-session-repository"; import { saveAgentPrompts } from "./scripts/prompt-repository"; import { BrowserMessage, ToggleSidebar, ActiveTabListener, ActivateAgent, AgentActivation, InteractionSummary, } from "./scripts/browser-message"; -import { HttpServiceError, NetworkError } from "./scripts/http"; +import { BaseError } from "./scripts/http"; import { isActiveTabListener, setTabListenerActive, removeTabListenerStatus, } from "./scripts/tab-listener-status-repository"; import { removeTabState } from "./scripts/tab-state-repository"; @@ -175,7 +175,8 @@ async function asyncProcessRequest(req: RequestEvent) { tabId, new InteractionSummary( false, - (e instanceof HttpServiceError || e instanceof NetworkError) ? e.detail : undefined + e instanceof BaseError ? e.detail : undefined, + e instanceof BaseError ? e.getType() : undefined ) ); } diff --git a/browser-extension/src/pages/Index.vue b/browser-extension/src/pages/Index.vue index 2e31d9e..8bcc2eb 100644 --- a/browser-extension/src/pages/Index.vue +++ b/browser-extension/src/pages/Index.vue @@ -165,9 +165,14 @@ const onAgentActivation = (msg: AgentActivation) => { } const onInteractionSummary = (msg: InteractionSummary) => { - const text = msg.text - ? (msg.text === "Failed to fetch" ? t('networkError') : msg.text) - : t('interactionSummaryError', { contactEmail: agent.value!.manifest.contactEmail }) + let text = msg.text ?? '' + + if (!msg.success) { + text = msg.errorType === 'NetworkError' ? t('networkError') : t('interactionSummaryError', { + contactEmail: agent.value!.manifest.contactEmail + }) + } + const lastMessage = messages.value[messages.value.length - 1] const messagePosition = lastMessage.isComplete ? messages.value.length : messages.value.length - 1 messages.value.splice(messagePosition, 0, msg.success ? ChatMessage.agentMessage(text) : ChatMessage.agentErrorMessage(text)) diff --git a/browser-extension/src/scripts/browser-message.ts b/browser-extension/src/scripts/browser-message.ts index 241254d..ed53fd1 100644 --- a/browser-extension/src/scripts/browser-message.ts +++ b/browser-extension/src/scripts/browser-message.ts @@ -102,15 +102,17 @@ export class AgentActivation extends BrowserMessage { export class InteractionSummary extends BrowserMessage { text?: string success: boolean + errorType?: string - constructor(success: boolean, text?: string) { + constructor(success: boolean, text?: string, errorType?: string) { super("interactionSummary") this.text = text this.success = success + this.errorType = errorType } public static fromJsonObject(obj: any): InteractionSummary { - return new InteractionSummary(obj.success, obj.text) + return new InteractionSummary(obj.success, obj.text, obj.errorType) } } diff --git a/browser-extension/src/scripts/http.ts b/browser-extension/src/scripts/http.ts index 09700c4..3efd444 100644 --- a/browser-extension/src/scripts/http.ts +++ b/browser-extension/src/scripts/http.ts @@ -33,18 +33,27 @@ const fetchResponse = async (url: string, options?: RequestInit) => { return ret } -class BaseError extends Error { +export class BaseError extends Error { detail?: string + readonly type: string = 'BaseError' constructor(detail?: string) { super() this.detail = detail } + + getType(): string { + return this.type + } } -export class HttpServiceError extends BaseError {} +export class HttpServiceError extends BaseError { + readonly type: string = 'HttpServiceError' +} -export class NetworkError extends BaseError {} +export class NetworkError extends BaseError { + readonly type: string = 'NetworkError' +} export async function* fetchStreamJson(url: string, options?: RequestInit): AsyncIterable { let resp = await fetchResponse(url, options)