v1.0.110 makerequest теперь сообщает о ошибках статуса сразу в обработчик + добавлен обработчик ошибок, анализирующий куда ему там слаться
This commit is contained in:
parent
96ab34c3a0
commit
80414f1f28
@ -1,76 +1,155 @@
|
|||||||
import axios, { AxiosResponse, Method, ResponseType } from "axios";
|
import axios, { AxiosResponse, Method, ResponseType } from "axios";
|
||||||
import { getAuthToken, setAuthToken } from "../stores/auth";
|
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>({
|
export async function makeRequest<TRequest = unknown, TResponse = unknown>({
|
||||||
method = "post",
|
method = "post",
|
||||||
url,
|
url,
|
||||||
body,
|
body,
|
||||||
useToken = true,
|
useToken = true,
|
||||||
contentType = false,
|
contentType = false,
|
||||||
responseType = "json",
|
responseType = "json",
|
||||||
signal,
|
signal,
|
||||||
withCredentials,
|
withCredentials,
|
||||||
}: {
|
}: {
|
||||||
method?: Method;
|
method?: Method;
|
||||||
url: string;
|
url: string;
|
||||||
body?: TRequest;
|
body?: TRequest;
|
||||||
/** Send access token */
|
useToken?: boolean;
|
||||||
useToken?: boolean;
|
contentType?: boolean;
|
||||||
contentType?: boolean;
|
responseType?: ResponseType;
|
||||||
responseType?: ResponseType;
|
signal?: AbortSignal;
|
||||||
signal?: AbortSignal;
|
withCredentials?: boolean;
|
||||||
/** Send refresh token */
|
|
||||||
withCredentials?: boolean;
|
|
||||||
}): Promise<TResponse> {
|
}): Promise<TResponse> {
|
||||||
const headers: Record<string, string> = {};
|
const config = getMakeRequestConfig();
|
||||||
if (useToken) headers["Authorization"] = getAuthToken() ? `Bearer ${getAuthToken()}` : "";
|
const headers: Record<string, string> = {};
|
||||||
if (contentType) headers["Content-Type"] = "application/json";
|
if (useToken) {
|
||||||
|
const token = getAuthToken();
|
||||||
|
headers["Authorization"] = token ? `Bearer ${token}` : "";
|
||||||
|
}
|
||||||
|
if (contentType) headers["Content-Type"] = "application/json";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios<TRequest, AxiosResponse<TResponse & { accessToken?: string; }>>({
|
const response = await axios<TRequest, AxiosResponse<TResponse & { accessToken?: string; }>>({
|
||||||
url,
|
url,
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
data: body,
|
data: body,
|
||||||
signal,
|
signal,
|
||||||
responseType,
|
responseType,
|
||||||
withCredentials,
|
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"
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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; },
|
shown: { [key: string]: number; },
|
||||||
request_screenshot: string,
|
request_screenshot: string,
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
system: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetMessagesRequest {
|
export interface GetMessagesRequest {
|
||||||
|
125
lib/utils/errorReporter.ts
Normal file
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 "./cart";
|
||||||
export * from "./devlog";
|
export * from "./devlog";
|
||||||
export * from "./getInitials";
|
export * from "./getInitials";
|
||||||
|
export * from "./errorReporter";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@frontend/kitui",
|
"name": "@frontend/kitui",
|
||||||
"version": "1.0.108",
|
"version": "1.0.109",
|
||||||
"description": "test",
|
"description": "test",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
|
Loading…
Reference in New Issue
Block a user