feat: implement debug mode with enhanced logging and filtering capabilities
This commit is contained in:
11
devbook.md
11
devbook.md
@@ -200,6 +200,17 @@ Application web moderne pour la lecture de BD/mangas/comics via un serveur Komga
|
|||||||
- [x] API responses
|
- [x] API responses
|
||||||
- [x] Static assets
|
- [x] Static assets
|
||||||
- [x] Images
|
- [x] Images
|
||||||
|
- [x] Mode Debug
|
||||||
|
- [x] Système de logging des requêtes
|
||||||
|
- [x] Historique des requêtes API
|
||||||
|
- [x] Mesure des temps de réponse
|
||||||
|
- [x] Détection des accès cache/MongoDB
|
||||||
|
- [x] Logging des rendus de pages
|
||||||
|
- [x] Interface de debug améliorée
|
||||||
|
- [x] Filtres par type de requête
|
||||||
|
- [x] Distinction visuelle page courante/historique
|
||||||
|
- [x] Historique étendu (500 entrées)
|
||||||
|
- [x] Rafraîchissement intelligent
|
||||||
- [x] SEO
|
- [x] SEO
|
||||||
- [x] Meta tags
|
- [x] Meta tags
|
||||||
- [x] Sitemap
|
- [x] Sitemap
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
X,
|
X,
|
||||||
@@ -11,9 +11,12 @@ import {
|
|||||||
Layout,
|
Layout,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Globe,
|
Globe,
|
||||||
|
Filter,
|
||||||
|
Calendar,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { CacheType } from "@/lib/services/base-api.service";
|
import type { CacheType } from "@/lib/services/base-api.service";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useDebug } from "@/contexts/DebugContext";
|
||||||
|
|
||||||
interface RequestTiming {
|
interface RequestTiming {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -45,10 +48,13 @@ function formatDuration(duration: number) {
|
|||||||
return Math.round(duration);
|
return Math.round(duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FilterType = "all" | "current-page" | "api" | "cache" | "mongodb" | "page-render";
|
||||||
|
|
||||||
export function DebugInfo() {
|
export function DebugInfo() {
|
||||||
const [logs, setLogs] = useState<RequestTiming[]>([]);
|
const { logs, setLogs, clearLogs: clearLogsContext, isRefreshing, setIsRefreshing } = useDebug();
|
||||||
const [isMinimized, setIsMinimized] = useState(false);
|
const [isMinimized, setIsMinimized] = useState(false);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [filter, setFilter] = useState<FilterType>("all");
|
||||||
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -67,32 +73,46 @@ export function DebugInfo() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Rafraîchir les logs au montage et à chaque changement de page
|
|
||||||
useEffect(() => {
|
|
||||||
fetchLogs();
|
|
||||||
}, [pathname]);
|
|
||||||
|
|
||||||
// Rafraîchir les logs périodiquement si la fenêtre n'est pas minimisée
|
|
||||||
useEffect(() => {
|
|
||||||
if (isMinimized) return;
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
fetchLogs();
|
|
||||||
}, 5000); // Rafraîchir toutes les 5 secondes
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [isMinimized]);
|
|
||||||
|
|
||||||
const clearLogs = async () => {
|
const clearLogs = async () => {
|
||||||
try {
|
try {
|
||||||
await fetch("/api/debug", { method: "DELETE" });
|
await fetch("/api/debug", { method: "DELETE" });
|
||||||
setLogs([]);
|
clearLogsContext();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la suppression des logs:", error);
|
console.error("Erreur lors de la suppression des logs:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortedLogs = [...logs].reverse();
|
// Fonction pour déterminer si une requête appartient à la page courante
|
||||||
|
const isCurrentPageRequest = (log: RequestTiming): boolean => {
|
||||||
|
if (log.pageRender) {
|
||||||
|
return log.pageRender.page === pathname;
|
||||||
|
}
|
||||||
|
// Pour les requêtes API, on considère qu'elles appartiennent à la page courante
|
||||||
|
// si elles ont été faites récemment (dans les 30 dernières secondes)
|
||||||
|
const logTime = new Date(log.timestamp).getTime();
|
||||||
|
const now = Date.now();
|
||||||
|
return now - logTime < 30000; // 30 secondes
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filtrer les logs selon le filtre sélectionné
|
||||||
|
const filteredLogs = logs.filter((log) => {
|
||||||
|
switch (filter) {
|
||||||
|
case "current-page":
|
||||||
|
return isCurrentPageRequest(log);
|
||||||
|
case "api":
|
||||||
|
return !log.fromCache && !log.mongoAccess && !log.pageRender;
|
||||||
|
case "cache":
|
||||||
|
return log.fromCache;
|
||||||
|
case "mongodb":
|
||||||
|
return log.mongoAccess;
|
||||||
|
case "page-render":
|
||||||
|
return log.pageRender;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedLogs = [...filteredLogs].reverse();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -110,6 +130,15 @@ export function DebugInfo() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
{!isMinimized && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowFilters(!showFilters)}
|
||||||
|
className={`hover:bg-zinc-700 rounded-full p-1.5 ${showFilters ? "bg-zinc-700" : ""}`}
|
||||||
|
aria-label="Filtres"
|
||||||
|
>
|
||||||
|
<Filter className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={fetchLogs}
|
onClick={fetchLogs}
|
||||||
className="hover:bg-zinc-700 rounded-full p-1.5"
|
className="hover:bg-zinc-700 rounded-full p-1.5"
|
||||||
@@ -135,13 +164,50 @@ export function DebugInfo() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!isMinimized && showFilters && (
|
||||||
|
<div className="mb-4 p-3 bg-zinc-800 rounded-lg">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{[
|
||||||
|
{ key: "all", label: "Toutes", icon: Calendar },
|
||||||
|
{ key: "current-page", label: "Page courante", icon: Layout },
|
||||||
|
{ key: "api", label: "API", icon: Globe },
|
||||||
|
{ key: "cache", label: "Cache", icon: Database },
|
||||||
|
{ key: "mongodb", label: "MongoDB", icon: CircleDot },
|
||||||
|
{ key: "page-render", label: "Rendu", icon: Layout },
|
||||||
|
].map(({ key, label, icon: Icon }) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
onClick={() => setFilter(key as FilterType)}
|
||||||
|
className={`flex items-center gap-1 px-3 py-1.5 rounded-full text-xs transition-colors ${
|
||||||
|
filter === key
|
||||||
|
? "bg-blue-600 text-white"
|
||||||
|
: "bg-zinc-700 text-zinc-300 hover:bg-zinc-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className="h-3 w-3" />
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isMinimized && (
|
{!isMinimized && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{sortedLogs.length === 0 ? (
|
{sortedLogs.length === 0 ? (
|
||||||
<p className="text-sm opacity-75">{t("debug.noRequests")}</p>
|
<p className="text-sm opacity-75">{t("debug.noRequests")}</p>
|
||||||
) : (
|
) : (
|
||||||
sortedLogs.map((log, index) => (
|
sortedLogs.map((log, index) => {
|
||||||
<div key={index} className="text-sm space-y-1.5 bg-zinc-800 p-2 rounded">
|
const isCurrentPage = isCurrentPageRequest(log);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`text-sm space-y-1.5 p-2 rounded border-l-2 ${
|
||||||
|
isCurrentPage
|
||||||
|
? "bg-blue-900/20 border-blue-500"
|
||||||
|
: "bg-zinc-800 border-zinc-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||||
{log.fromCache && (
|
{log.fromCache && (
|
||||||
@@ -231,7 +297,8 @@ export function DebugInfo() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
);
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { usePathname } from "next/navigation";
|
|||||||
import { registerServiceWorker } from "@/lib/registerSW";
|
import { registerServiceWorker } from "@/lib/registerSW";
|
||||||
import { NetworkStatus } from "../ui/NetworkStatus";
|
import { NetworkStatus } from "../ui/NetworkStatus";
|
||||||
import { DebugWrapper } from "@/components/debug/DebugWrapper";
|
import { DebugWrapper } from "@/components/debug/DebugWrapper";
|
||||||
|
import { DebugProvider } from "@/contexts/DebugContext";
|
||||||
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
|
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
|
||||||
|
|
||||||
// Routes qui ne nécessitent pas d'authentification
|
// Routes qui ne nécessitent pas d'authentification
|
||||||
@@ -68,6 +69,7 @@ export default function ClientLayout({ children, initialLibraries = [], initialF
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||||
|
<DebugProvider>
|
||||||
<div className="relative min-h-screen h-full">
|
<div className="relative min-h-screen h-full">
|
||||||
{!isPublicRoute && <Header onToggleSidebar={handleToggleSidebar} />}
|
{!isPublicRoute && <Header onToggleSidebar={handleToggleSidebar} />}
|
||||||
{!isPublicRoute && (
|
{!isPublicRoute && (
|
||||||
@@ -84,6 +86,7 @@ export default function ClientLayout({ children, initialLibraries = [], initialF
|
|||||||
<NetworkStatus />
|
<NetworkStatus />
|
||||||
<DebugWrapper />
|
<DebugWrapper />
|
||||||
</div>
|
</div>
|
||||||
|
</DebugProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback, useRef } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import type { PageCache } from "../types";
|
import type { PageCache } from "../types";
|
||||||
import type { KomgaBook } from "@/types/komga";
|
import type { KomgaBook } from "@/types/komga";
|
||||||
|
import { usePreferences } from "@/contexts/PreferencesContext";
|
||||||
|
|
||||||
interface UsePageCacheProps {
|
interface UsePageCacheProps {
|
||||||
book: KomgaBook;
|
book: KomgaBook;
|
||||||
@@ -9,6 +10,7 @@ interface UsePageCacheProps {
|
|||||||
|
|
||||||
export const usePageCache = ({ book, pages }: UsePageCacheProps) => {
|
export const usePageCache = ({ book, pages }: UsePageCacheProps) => {
|
||||||
const pageCache = useRef<PageCache>({});
|
const pageCache = useRef<PageCache>({});
|
||||||
|
const { preferences } = usePreferences();
|
||||||
|
|
||||||
const preloadPage = useCallback(
|
const preloadPage = useCallback(
|
||||||
async (pageNumber: number) => {
|
async (pageNumber: number) => {
|
||||||
@@ -32,9 +34,29 @@ export const usePageCache = ({ book, pages }: UsePageCacheProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const startTime = performance.now();
|
||||||
const response = await fetch(`/api/komga/books/${book.id}/pages/${pageNumber}`);
|
const response = await fetch(`/api/komga/books/${book.id}/pages/${pageNumber}`);
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
const endTime = performance.now();
|
||||||
|
|
||||||
|
// Logger la requête côté client seulement si le mode debug est activé et ce n'est pas une requête de debug
|
||||||
|
if (!url.includes('/api/debug') && preferences.debug) {
|
||||||
|
try {
|
||||||
|
await fetch("/api/debug", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: `/api/komga/books/${book.id}/pages/${pageNumber}`,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
fromCache: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Ignorer les erreurs de logging
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pageCache.current[pageNumber] = {
|
pageCache.current[pageNumber] = {
|
||||||
blob,
|
blob,
|
||||||
@@ -49,12 +71,30 @@ export const usePageCache = ({ book, pages }: UsePageCacheProps) => {
|
|||||||
resolveLoading!();
|
resolveLoading!();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[book.id, pages.length]
|
[book.id, pages.length, preferences.debug]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getPageUrl = useCallback(
|
const getPageUrl = useCallback(
|
||||||
async (pageNumber: number) => {
|
async (pageNumber: number) => {
|
||||||
if (pageCache.current[pageNumber]?.url) {
|
if (pageCache.current[pageNumber]?.url) {
|
||||||
|
// Logger l'utilisation du cache côté client seulement si le mode debug est activé et ce n'est pas une requête de debug
|
||||||
|
const cacheUrl = `[CLIENT-CACHE] /api/komga/books/${book.id}/pages/${pageNumber}`;
|
||||||
|
if (!cacheUrl.includes('/api/debug') && preferences.debug) {
|
||||||
|
try {
|
||||||
|
await fetch("/api/debug", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: cacheUrl,
|
||||||
|
startTime: performance.now(),
|
||||||
|
endTime: performance.now(),
|
||||||
|
fromCache: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Ignorer les erreurs de logging
|
||||||
|
}
|
||||||
|
}
|
||||||
return pageCache.current[pageNumber].url;
|
return pageCache.current[pageNumber].url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +109,7 @@ export const usePageCache = ({ book, pages }: UsePageCacheProps) => {
|
|||||||
`/api/komga/images/books/${book.id}/pages/${pageNumber}`
|
`/api/komga/images/books/${book.id}/pages/${pageNumber}`
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[book.id, preloadPage]
|
[book.id, preloadPage, preferences.debug]
|
||||||
);
|
);
|
||||||
|
|
||||||
const cleanCache = useCallback(
|
const cleanCache = useCallback(
|
||||||
|
|||||||
97
src/contexts/DebugContext.tsx
Normal file
97
src/contexts/DebugContext.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createContext, useContext, useState, useEffect, ReactNode } from "react";
|
||||||
|
import type { RequestTiming } from "@/lib/services/debug.service";
|
||||||
|
import { usePreferences } from "./PreferencesContext";
|
||||||
|
|
||||||
|
interface DebugContextType {
|
||||||
|
logs: RequestTiming[];
|
||||||
|
setLogs: (logs: RequestTiming[]) => void;
|
||||||
|
addLog: (log: RequestTiming) => void;
|
||||||
|
clearLogs: () => void;
|
||||||
|
isRefreshing: boolean;
|
||||||
|
setIsRefreshing: (refreshing: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DebugContext = createContext<DebugContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
interface DebugProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DebugProvider({ children }: DebugProviderProps) {
|
||||||
|
const [logs, setLogs] = useState<RequestTiming[]>([]);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
const { preferences } = usePreferences();
|
||||||
|
|
||||||
|
const addLog = (log: RequestTiming) => {
|
||||||
|
setLogs(prevLogs => {
|
||||||
|
// Éviter les doublons basés sur l'URL et le timestamp
|
||||||
|
const exists = prevLogs.some(existingLog =>
|
||||||
|
existingLog.url === log.url && existingLog.timestamp === log.timestamp
|
||||||
|
);
|
||||||
|
if (exists) return prevLogs;
|
||||||
|
|
||||||
|
return [...prevLogs, log].sort((a, b) =>
|
||||||
|
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearLogs = () => {
|
||||||
|
setLogs([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Charger les logs au montage du provider et les rafraîchir périodiquement
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchLogs = async () => {
|
||||||
|
try {
|
||||||
|
// Ne pas faire de requête si le debug n'est pas activé
|
||||||
|
if (!preferences.debug) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRefreshing(true);
|
||||||
|
const debugResponse = await fetch("/api/debug");
|
||||||
|
if (debugResponse.ok) {
|
||||||
|
const serverLogs = await debugResponse.json();
|
||||||
|
setLogs(serverLogs);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération des logs:", error);
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchLogs();
|
||||||
|
|
||||||
|
// Rafraîchir toutes les 10 secondes (moins fréquent pour éviter les conflits)
|
||||||
|
const interval = setInterval(fetchLogs, 10000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [preferences.debug]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DebugContext.Provider
|
||||||
|
value={{
|
||||||
|
logs,
|
||||||
|
setLogs,
|
||||||
|
addLog,
|
||||||
|
clearLogs,
|
||||||
|
isRefreshing,
|
||||||
|
setIsRefreshing
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</DebugContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDebug() {
|
||||||
|
const context = useContext(DebugContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useDebug must be used within a DebugProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { AuthConfig } from "@/types/auth";
|
import type { AuthConfig } from "@/types/auth";
|
||||||
import { getServerCacheService } from "./server-cache.service";
|
import { getServerCacheService } from "./server-cache.service";
|
||||||
import { ConfigDBService } from "./config-db.service";
|
import { ConfigDBService } from "./config-db.service";
|
||||||
import { DebugService } from "./debug.service";
|
|
||||||
import { ERROR_CODES } from "../../constants/errorCodes";
|
import { ERROR_CODES } from "../../constants/errorCodes";
|
||||||
import { AppError } from "../../utils/errors";
|
import { AppError } from "../../utils/errors";
|
||||||
import type { KomgaConfig } from "@/types/komga";
|
import type { KomgaConfig } from "@/types/komga";
|
||||||
import type { ServerCacheService } from "./server-cache.service";
|
import type { ServerCacheService } from "./server-cache.service";
|
||||||
|
import { fetchWithCacheDetection } from "../utils/fetch-with-cache-detection";
|
||||||
// Types de cache disponibles
|
// Types de cache disponibles
|
||||||
export type CacheType = "DEFAULT" | "HOME" | "LIBRARIES" | "SERIES" | "BOOKS" | "IMAGES";
|
export type CacheType = "DEFAULT" | "HOME" | "LIBRARIES" | "SERIES" | "BOOKS" | "IMAGES";
|
||||||
|
|
||||||
@@ -86,7 +86,6 @@ export abstract class BaseApiService {
|
|||||||
headersOptions = {},
|
headersOptions = {},
|
||||||
options: KomgaRequestInit = {}
|
options: KomgaRequestInit = {}
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const startTime = performance.now();
|
|
||||||
const config: AuthConfig = await this.getKomgaConfig();
|
const config: AuthConfig = await this.getKomgaConfig();
|
||||||
const { path, params } = urlBuilder;
|
const { path, params } = urlBuilder;
|
||||||
const url = this.buildUrl(config, path, params);
|
const url = this.buildUrl(config, path, params);
|
||||||
@@ -99,16 +98,7 @@ export abstract class BaseApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, { headers, ...options });
|
const response = await fetchWithCacheDetection(url, { headers, ...options });
|
||||||
const endTime = performance.now();
|
|
||||||
|
|
||||||
// Log la requête
|
|
||||||
await DebugService.logRequest({
|
|
||||||
url: path,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
fromCache: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new AppError(ERROR_CODES.KOMGA.HTTP_ERROR, {
|
throw new AppError(ERROR_CODES.KOMGA.HTTP_ERROR, {
|
||||||
@@ -117,18 +107,8 @@ export abstract class BaseApiService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return options.isImage ? response : response.json();
|
return options.isImage ? (response as T) : response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const endTime = performance.now();
|
|
||||||
|
|
||||||
// Log aussi les erreurs
|
|
||||||
await DebugService.logRequest({
|
|
||||||
url: path,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
fromCache: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export interface RequestTiming {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class DebugService {
|
export class DebugService {
|
||||||
|
private static writeQueues = new Map<string, Promise<void>>();
|
||||||
|
|
||||||
private static async getCurrentUserId(): Promise<string> {
|
private static async getCurrentUserId(): Promise<string> {
|
||||||
const user = await AuthServerService.getCurrentUser();
|
const user = await AuthServerService.getCurrentUser();
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -59,14 +61,117 @@ export class DebugService {
|
|||||||
try {
|
try {
|
||||||
const content = await fs.readFile(filePath, "utf-8");
|
const content = await fs.readFile(filePath, "utf-8");
|
||||||
return JSON.parse(content);
|
return JSON.parse(content);
|
||||||
|
} catch {
|
||||||
|
// Essayer de lire un fichier de sauvegarde
|
||||||
|
try {
|
||||||
|
const backupPath = filePath + '.backup';
|
||||||
|
const backupContent = await fs.readFile(backupPath, "utf-8");
|
||||||
|
return JSON.parse(backupContent);
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static async writeLogs(filePath: string, logs: RequestTiming[]): Promise<void> {
|
private static async writeLogs(filePath: string, logs: RequestTiming[]): Promise<void> {
|
||||||
const trimmedLogs = logs.slice(-99);
|
// Obtenir la queue existante ou créer une nouvelle
|
||||||
await fs.writeFile(filePath, JSON.stringify(trimmedLogs, null, 2));
|
const existingQueue = this.writeQueues.get(filePath);
|
||||||
|
|
||||||
|
// Créer une nouvelle promesse qui attend la queue précédente
|
||||||
|
const newQueue = existingQueue
|
||||||
|
? existingQueue.then(() => this.performAppend(filePath, logs))
|
||||||
|
: this.performAppend(filePath, logs);
|
||||||
|
|
||||||
|
// Mettre à jour la queue
|
||||||
|
this.writeQueues.set(filePath, newQueue);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await newQueue;
|
||||||
|
} finally {
|
||||||
|
// Nettoyer la queue si c'est la dernière opération
|
||||||
|
if (this.writeQueues.get(filePath) === newQueue) {
|
||||||
|
this.writeQueues.delete(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async performAppend(filePath: string, logs: RequestTiming[]): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Lire le fichier existant
|
||||||
|
const existingLogs = await this.readLogs(filePath);
|
||||||
|
|
||||||
|
// Fusionner avec les nouveaux logs
|
||||||
|
const allLogs = [...existingLogs, ...logs];
|
||||||
|
|
||||||
|
// Garder seulement les 1000 derniers logs
|
||||||
|
const trimmedLogs = allLogs.slice(-1000);
|
||||||
|
|
||||||
|
// Créer une sauvegarde avant d'écrire
|
||||||
|
try {
|
||||||
|
await fs.copyFile(filePath, filePath + '.backup');
|
||||||
|
} catch {
|
||||||
|
// Ignorer si le fichier n'existe pas encore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Écrire le fichier complet (c'est nécessaire pour maintenir l'ordre chronologique)
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(trimmedLogs, null, 2), { flag: 'w' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Erreur lors de l'écriture des logs pour ${filePath}:`, error);
|
||||||
|
// Ne pas relancer l'erreur pour éviter de casser l'application
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async appendLog(filePath: string, log: RequestTiming): Promise<void> {
|
||||||
|
// Obtenir la queue existante ou créer une nouvelle
|
||||||
|
const existingQueue = this.writeQueues.get(filePath);
|
||||||
|
|
||||||
|
// Créer une nouvelle promesse qui attend la queue précédente
|
||||||
|
const newQueue = existingQueue
|
||||||
|
? existingQueue.then(() => this.performSingleAppend(filePath, log))
|
||||||
|
: this.performSingleAppend(filePath, log);
|
||||||
|
|
||||||
|
// Mettre à jour la queue
|
||||||
|
this.writeQueues.set(filePath, newQueue);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await newQueue;
|
||||||
|
} finally {
|
||||||
|
// Nettoyer la queue si c'est la dernière opération
|
||||||
|
if (this.writeQueues.get(filePath) === newQueue) {
|
||||||
|
this.writeQueues.delete(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async performSingleAppend(filePath: string, log: RequestTiming): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Lire le fichier existant
|
||||||
|
const existingLogs = await this.readLogs(filePath);
|
||||||
|
|
||||||
|
// Vérifier les doublons avec des tolérances différentes selon le type
|
||||||
|
const isPageRender = log.pageRender !== undefined;
|
||||||
|
const timeTolerance = isPageRender ? 500 : 50; // 500ms pour les rendus, 50ms pour les requêtes
|
||||||
|
|
||||||
|
const exists = existingLogs.some(existingLog =>
|
||||||
|
existingLog.url === log.url &&
|
||||||
|
Math.abs(existingLog.duration - log.duration) < 10 && // Durée similaire (10ms de tolérance)
|
||||||
|
Math.abs(new Date(existingLog.timestamp).getTime() - new Date(log.timestamp).getTime()) < timeTolerance
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
// Ajouter le nouveau log
|
||||||
|
const allLogs = [...existingLogs, log];
|
||||||
|
|
||||||
|
// Garder seulement les 1000 derniers logs
|
||||||
|
const trimmedLogs = allLogs.slice(-1000);
|
||||||
|
|
||||||
|
// Écrire le fichier complet avec gestion d'erreur
|
||||||
|
await fs.writeFile(filePath, JSON.stringify(trimmedLogs, null, 2), { flag: 'w' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Erreur lors de l'écriture du log pour ${filePath}:`, error);
|
||||||
|
// Ne pas relancer l'erreur pour éviter de casser l'application
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static createTiming(
|
private static createTiming(
|
||||||
@@ -95,7 +200,6 @@ export class DebugService {
|
|||||||
await this.ensureDebugDir();
|
await this.ensureDebugDir();
|
||||||
const filePath = this.getLogFilePath(userId);
|
const filePath = this.getLogFilePath(userId);
|
||||||
|
|
||||||
const logs = await this.readLogs(filePath);
|
|
||||||
const newTiming = this.createTiming(
|
const newTiming = this.createTiming(
|
||||||
timing.url,
|
timing.url,
|
||||||
timing.startTime,
|
timing.startTime,
|
||||||
@@ -108,7 +212,8 @@ export class DebugService {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.writeLogs(filePath, [...logs, newTiming]);
|
// Utiliser un système d'append atomique
|
||||||
|
await this.appendLog(filePath, newTiming);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de l'enregistrement du log:", error);
|
console.error("Erreur lors de l'enregistrement du log:", error);
|
||||||
}
|
}
|
||||||
@@ -143,13 +248,13 @@ export class DebugService {
|
|||||||
await this.ensureDebugDir();
|
await this.ensureDebugDir();
|
||||||
const filePath = this.getLogFilePath(userId);
|
const filePath = this.getLogFilePath(userId);
|
||||||
|
|
||||||
const logs = await this.readLogs(filePath);
|
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
const newTiming = this.createTiming(`Page Render: ${page}`, now - duration, now, false, {
|
const newTiming = this.createTiming(`Page Render: ${page}`, now - duration, now, false, {
|
||||||
pageRender: { page, duration },
|
pageRender: { page, duration },
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.writeLogs(filePath, [...logs, newTiming]);
|
// Utiliser le même système d'append atomique
|
||||||
|
await this.appendLog(filePath, newTiming);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de l'enregistrement du log de rendu:", error);
|
console.error("Erreur lors de l'enregistrement du log de rendu:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -400,9 +400,9 @@ class ServerCacheService {
|
|||||||
if (cached !== null) {
|
if (cached !== null) {
|
||||||
const endTime = performance.now();
|
const endTime = performance.now();
|
||||||
|
|
||||||
// Log la requête avec l'indication du cache
|
// Log la requête avec l'indication du cache (URL plus claire)
|
||||||
await DebugService.logRequest({
|
await DebugService.logRequest({
|
||||||
url: cacheKey,
|
url: `[CACHE] ${key}`,
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
fromCache: true,
|
fromCache: true,
|
||||||
|
|||||||
57
src/lib/utils/fetch-with-cache-detection.ts
Normal file
57
src/lib/utils/fetch-with-cache-detection.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// Wrapper pour détecter le cache du navigateur
|
||||||
|
export async function fetchWithCacheDetection(url: string, options: RequestInit = {}) {
|
||||||
|
const startTime = performance.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, options);
|
||||||
|
const endTime = performance.now();
|
||||||
|
|
||||||
|
// Détecter si la réponse vient du cache du navigateur
|
||||||
|
const fromBrowserCache = response.headers.get('x-cache') === 'HIT' ||
|
||||||
|
response.headers.get('cf-cache-status') === 'HIT' ||
|
||||||
|
(endTime - startTime) < 5; // Si très rapide, probablement du cache
|
||||||
|
|
||||||
|
// Logger la requête seulement si ce n'est pas une requête de debug
|
||||||
|
// Note: La vérification du mode debug se fait côté serveur dans DebugService
|
||||||
|
if (!url.includes('/api/debug')) {
|
||||||
|
try {
|
||||||
|
await fetch("/api/debug", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: url,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
fromCache: fromBrowserCache,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Ignorer les erreurs de logging
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
const endTime = performance.now();
|
||||||
|
|
||||||
|
// Logger aussi les erreurs seulement si ce n'est pas une requête de debug
|
||||||
|
if (!url.includes('/api/debug')) {
|
||||||
|
try {
|
||||||
|
await fetch("/api/debug", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: url,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
fromCache: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Ignorer les erreurs de logging
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user