v1.0.110 makerequest теперь сообщает о ошибках статуса сразу в обработчик + добавлен обработчик ошибок, анализирующий куда ему там слаться

This commit is contained in:
Nastya 2025-07-22 21:49:04 +03:00
parent 96ab34c3a0
commit 80414f1f28
5 changed files with 273 additions and 67 deletions

@ -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<TRequest = unknown, TResponse = unknown>({
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<TResponse> {
const headers: Record<string, string> = {};
if (useToken) headers["Authorization"] = getAuthToken() ? `Bearer ${getAuthToken()}` : "";
if (contentType) headers["Content-Type"] = "application/json";
const config = getMakeRequestConfig();
const headers: Record<string, string> = {};
if (useToken) {
const token = getAuthToken();
headers["Authorization"] = token ? `Bearer ${token}` : "";
}
if (contentType) headers["Content-Type"] = "application/json";
try {
const response = await axios<TRequest, AxiosResponse<TResponse & { accessToken?: string; }>>({
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<TRequest, AxiosResponse<TResponse>>({
url,
method,
headers,
data: body,
signal,
});
return response.data;
}
throw error;
}
}
function refresh(token: string) {
return axios<never, AxiosResponse<{ accessToken: string; }>>(process.env.REACT_APP_DOMAIN + "/auth/refresh", {
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
method: "post"
try {
const response = await axios<TRequest, AxiosResponse<TResponse & { accessToken?: string; }>>({
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<TRequest, AxiosResponse<TResponse>>({
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<AxiosResponse<{ accessToken: string }>> {
if (!token) throw new Error("No refresh token provided");
return axios<never, AxiosResponse<{ accessToken: string }>>(process.env.REACT_APP_DOMAIN + "/auth/refresh", {
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
method: "post"
});
}

@ -58,6 +58,7 @@ export interface TicketMessage {
shown: { [key: string]: number; },
request_screenshot: string,
created_at: string;
system: boolean;
}
export interface GetMessagesRequest {

125
lib/utils/errorReporter.ts Normal file

@ -0,0 +1,125 @@
import { ErrorInfo } from "react";
import { Ticket, createTicket, getAuthToken, sendTicketMessage } from "..";
let errorsQueue: ComponentError[] = [];
let timeoutId: ReturnType<typeof setTimeout>;
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;
}
}
}
}

@ -2,3 +2,4 @@ export * from "./backendMessageHandler";
export * from "./cart";
export * from "./devlog";
export * from "./getInitials";
export * from "./errorReporter";

@ -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",