diff --git a/lib/api/makeRequest.ts b/lib/api/makeRequest.ts index 8ea7a28..ba9ebbf 100644 --- a/lib/api/makeRequest.ts +++ b/lib/api/makeRequest.ts @@ -1,76 +1,155 @@ import axios, { AxiosResponse, Method, ResponseType } from "axios"; import { getAuthToken, setAuthToken } from "../stores/auth"; +export interface MakeRequestConfig { + getAuthToken: () => string | undefined; + setAuthToken: (token: string) => void; + refreshUrl: string; + logoutFn: () => void; + handleComponentError?: (error: Error, info?: any) => void; + clearAuthDataFn?: () => void; + clearErrorHandlingConfig?: () => void; + allowedDomains?: string[]; + debugSecretKey?: string; + logErrorFn?: (message: string, error?: any) => void; +} + +let makeRequestConfig: MakeRequestConfig | null = null; + + +export function createMakeRequestConfig( + getAuthToken: () => string | undefined, + setAuthToken: (token: string) => void, + refreshUrl: string, + logoutFn?: () => void, + handleComponentError?: (error: Error, info?: any) => void, + clearAuthDataFn?: () => void, + clearErrorHandlingConfig?: () => void, + allowedDomains?: string[], + debugSecretKey?: string, + logErrorFn?: (message: string, error?: any) => void, +) { + makeRequestConfig = { + getAuthToken, + setAuthToken, + refreshUrl, + logoutFn: () => { + clearMakeRequestConfig(); + if (typeof clearErrorHandlingConfig === 'function') clearErrorHandlingConfig(); + if (logoutFn) logoutFn(); + }, + handleComponentError, + clearAuthDataFn, + clearErrorHandlingConfig, + allowedDomains, + debugSecretKey, + logErrorFn, + }; +} + +export function getMakeRequestConfig(): MakeRequestConfig | null { + return makeRequestConfig; +} + +export function clearMakeRequestConfig() { + makeRequestConfig = null; +} export async function makeRequest({ - method = "post", - url, - body, - useToken = true, - contentType = false, - responseType = "json", - signal, - withCredentials, + method = "post", + url, + body, + useToken = true, + contentType = false, + responseType = "json", + signal, + withCredentials, }: { - method?: Method; - url: string; - body?: TRequest; - /** Send access token */ - useToken?: boolean; - contentType?: boolean; - responseType?: ResponseType; - signal?: AbortSignal; - /** Send refresh token */ - withCredentials?: boolean; + method?: Method; + url: string; + body?: TRequest; + useToken?: boolean; + contentType?: boolean; + responseType?: ResponseType; + signal?: AbortSignal; + withCredentials?: boolean; }): Promise { - const headers: Record = {}; - if (useToken) headers["Authorization"] = getAuthToken() ? `Bearer ${getAuthToken()}` : ""; - if (contentType) headers["Content-Type"] = "application/json"; + const config = getMakeRequestConfig(); + const headers: Record = {}; + if (useToken) { + const token = getAuthToken(); + headers["Authorization"] = token ? `Bearer ${token}` : ""; + } + if (contentType) headers["Content-Type"] = "application/json"; - try { - const response = await axios>({ - url, - method, - headers, - data: body, - signal, - responseType, - withCredentials, - }); - - if (response.data?.accessToken) { - setAuthToken(response.data.accessToken); - } - - return response.data; - } catch (error) { - if (axios.isAxiosError(error) && error.response?.status === 401 && !withCredentials) { - const refreshResponse = await refresh(getAuthToken()); - if (refreshResponse.data?.accessToken) setAuthToken(refreshResponse.data.accessToken); - - headers["Authorization"] = refreshResponse.data.accessToken; - const response = await axios.request>({ - url, - method, - headers, - data: body, - signal, - }); - - return response.data; - } - - throw error; - } -} - -function refresh(token: string) { - - return axios>(process.env.REACT_APP_DOMAIN + "/auth/refresh", { - headers: { - "Authorization": `Bearer ${token}`, - "Content-Type": "application/json", - }, - method: "post" + try { + const response = await axios>({ + url, + method, + headers, + data: body, + signal, + responseType, + withCredentials, }); + + if (response.data?.accessToken) { + setAuthToken(response.data.accessToken); + } + + return response.data; + } catch (error: any) { + if (axios.isAxiosError(error) && error.response?.status === 401 && !withCredentials) { + const refreshResponse = await refresh(getAuthToken()); + + if (axios.isAxiosError(refreshResponse) && error.response?.status === 401 && config !== null) { + //токен так сильно сдох, что восстановлению не подлежит + config.logoutFn(); + throw new Error("Пожалуйста, войдите в свой профиль."); + } + if (refreshResponse.data?.accessToken) { + setAuthToken(refreshResponse.data.accessToken); + } + headers["Authorization"] = refreshResponse.data.accessToken ? `Bearer ${refreshResponse.data.accessToken}` : ""; + const response = await axios.request>({ + url, + method, + headers, + data: body, + signal, + }); + return response.data; + } + // Централизованная обработка ошибок (400/500+) + if ( + error.response?.status && + (error.response.status === 400 || error.response.status >= 500) && + config?.handleComponentError + ) { + const errorMessage = `HTTP ${error.response.status}: ${error.response?.data?.message || error.message}`; + const httpError = new Error(errorMessage); + httpError.stack = error.stack; + config.handleComponentError(httpError, { componentStack: null }); + } + // refreshToken is empty + if ( + error.response?.status === 400 && + error.response?.data?.message === "refreshToken is empty" && + config?.clearAuthDataFn + ) { + config.clearAuthDataFn(); + } + throw error; + } } + +export async function refresh(token?: string): Promise> { + if (!token) throw new Error("No refresh token provided"); + return axios>(process.env.REACT_APP_DOMAIN + "/auth/refresh", { + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json", + }, + method: "post" + }); +} \ No newline at end of file diff --git a/lib/model/ticket.ts b/lib/model/ticket.ts index 63dc4c3..e51b519 100644 --- a/lib/model/ticket.ts +++ b/lib/model/ticket.ts @@ -58,6 +58,7 @@ export interface TicketMessage { shown: { [key: string]: number; }, request_screenshot: string, created_at: string; + system: boolean; } export interface GetMessagesRequest { diff --git a/lib/utils/errorReporter.ts b/lib/utils/errorReporter.ts new file mode 100644 index 0000000..345c348 --- /dev/null +++ b/lib/utils/errorReporter.ts @@ -0,0 +1,125 @@ + +import { ErrorInfo } from "react"; +import { Ticket, createTicket, getAuthToken, sendTicketMessage } from ".."; + +let errorsQueue: ComponentError[] = []; +let timeoutId: ReturnType; + + +interface ComponentError { + timestamp: number; + message: string; + callStack: string | undefined; + componentStack: string | null | undefined; +} + + +function isErrorReportingAllowed(error?: Error): boolean { + // Если ошибка помечена как debug-override — всегда отправлять + if (error && (error as any).__forceSend) return true; + + // Проверяем домен + const currentDomain = window.location.hostname; + + return currentDomain !== 'localhost'; +} + +export function handleComponentError(error: Error, info: ErrorInfo, tickets: Ticket[]) { + //репортим только о авторизонышах + if (!getAuthToken()) return; + + // Проверяем разрешение на отправку ошибок (по домену) + if (!isErrorReportingAllowed(error)) { + console.log('❌ Отправка ошибки заблокирована:', error.message); + return; + } + + console.log(`✅ Обработка ошибки: ${error.message}`); + + // Копируем __forceSend если есть + const componentError: ComponentError & { __forceSend?: boolean } = { + timestamp: Math.floor(Date.now() / 1000), + message: error.message, + callStack: error.stack, + componentStack: info.componentStack, + ...(error && (error as any).__forceSend ? { __forceSend: true } : {}) + }; + + queueErrorRequest(componentError, tickets); +} + +//Ставит ошибку в очередь для отправки +//Через 1 секунду вызывает sendErrorsToServer +export function queueErrorRequest(error: ComponentError, tickets: Ticket[]) { + errorsQueue.push(error); + + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + sendErrorsToServer(tickets); + }, 1000); +} + + +//Отправляет накопленные ошибки в тикеты +//Ищет существующий тикет с system: true или создает новый + +export async function sendErrorsToServer( + tickets: Ticket[] +) { + if (errorsQueue.length === 0) return; + + // Проверяем разрешение на отправку ошибок (по домену и debug-override) + // Если хотя бы одна ошибка в очереди с __forceSend, отправляем всё + const forceSend = errorsQueue.some(e => (e as any).__forceSend); + if (!forceSend && !isErrorReportingAllowed()) { + console.log('❌ Отправка ошибок заблокирована, очищаем очередь'); + errorsQueue = []; + return; + } + + try { + // Формируем сообщение об ошибке + const errorMessage = errorsQueue.map(error => { + return `[${new Date(error.timestamp * 1000).toISOString()}] ${error.message}\n\nCall Stack:\n${error.callStack || 'N/A'}\n\nComponent Stack:\n${error.componentStack || 'N/A'}`; + }).join('\n\n---\n\n'); + + // ВСЕГДА ищем тикет через API + const existingSystemTicket = await findSystemTicket(tickets); + + if (existingSystemTicket) { + sendTicketMessage({ + ticketId: existingSystemTicket, + message: errorMessage, + systemError: true, + }); + } else { + // Создаем новый тикет для ошибки + createTicket({ + message: errorMessage, + useToken: true, + systemError: true, + }); + } + } catch (error) { + console.error('Error in sendErrorsToServer:', error); + } finally { + // Очищаем очередь ошибок + errorsQueue = []; + } +} + +// Ищет существующий тикет с system: true +export async function findSystemTicket( + tickets: Ticket[] +) { + for (const ticket of tickets) { + console.log("[findSystemTicket] Проверяем тикет:", ticket); + if (!('messages' in ticket)) { + if (ticket.top_message && ticket.top_message.system === true) { + console.log("[findSystemTicket] Найден тикет по top_message.system:true:", ticket.id); + return ticket.id; + } + + } + } +} \ No newline at end of file diff --git a/lib/utils/index.ts b/lib/utils/index.ts index 1813450..7ae4bc6 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -2,3 +2,4 @@ export * from "./backendMessageHandler"; export * from "./cart"; export * from "./devlog"; export * from "./getInitials"; +export * from "./errorReporter"; diff --git a/package.json b/package.json index 7806254..2f3c504 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@frontend/kitui", - "version": "1.0.108", + "version": "1.0.109", "description": "test", "main": "./dist/index.js", "module": "./dist/index.js",