400 lines
10 KiB
TypeScript
400 lines
10 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||
"use client";
|
||
|
||
export enum LogType {
|
||
API = "API",
|
||
SOCKET = "SOCKET",
|
||
ERROR = "ERROR",
|
||
INFO = "INFO",
|
||
}
|
||
|
||
export enum LogDirection {
|
||
REQUEST = "REQUEST",
|
||
RESPONSE = "RESPONSE",
|
||
INCOMING = "INCOMING",
|
||
OUTGOING = "OUTGOING",
|
||
}
|
||
|
||
interface LogEntry {
|
||
type: LogType;
|
||
direction?: LogDirection;
|
||
event: string;
|
||
data?: unknown;
|
||
url?: string;
|
||
method?: string;
|
||
status?: number;
|
||
timestamp: Date;
|
||
duration?: number;
|
||
}
|
||
|
||
class DevLogger {
|
||
private enabled = false;
|
||
private enabledTypes = new Set<LogType>(Object.values(LogType));
|
||
private envEnabled = false;
|
||
private serverLoggingEnabled = false;
|
||
|
||
constructor() {
|
||
// Check ENV variables first
|
||
if (typeof window !== "undefined") {
|
||
this.envEnabled = process.env.NEXT_PUBLIC_DEV_LOGGER_ENABLED !== "false";
|
||
} else {
|
||
// Server side - check server env
|
||
this.serverLoggingEnabled =
|
||
process.env.DEV_LOGGER_SERVER_ENABLED === "true";
|
||
this.envEnabled = process.env.DEV_LOGGER_ENABLED !== "false";
|
||
}
|
||
|
||
// Check localStorage for logging preferences (client-side only)
|
||
if (typeof window !== "undefined") {
|
||
const stored = localStorage.getItem("dev-logger-enabled");
|
||
this.enabled = stored ? JSON.parse(stored) : this.envEnabled;
|
||
|
||
const storedTypes = localStorage.getItem("dev-logger-types");
|
||
if (storedTypes) {
|
||
this.enabledTypes = new Set(JSON.parse(storedTypes));
|
||
}
|
||
} else {
|
||
this.enabled = this.envEnabled;
|
||
}
|
||
}
|
||
|
||
private shouldLog(type: LogType): boolean {
|
||
// Check ENV first, then user preferences
|
||
return this.envEnabled && this.enabled && this.enabledTypes.has(type);
|
||
}
|
||
|
||
private shouldLogServer(type: LogType): boolean {
|
||
// Server logging requires explicit ENV enable
|
||
return (
|
||
this.serverLoggingEnabled &&
|
||
this.envEnabled &&
|
||
this.enabled &&
|
||
this.enabledTypes.has(type)
|
||
);
|
||
}
|
||
|
||
private getLogStyle(
|
||
type: LogType,
|
||
direction?: LogDirection
|
||
): { emoji: string; color: string; bgColor?: string } {
|
||
const styles: Record<LogType, any> = {
|
||
[LogType.API]: {
|
||
[LogDirection.REQUEST]: {
|
||
emoji: "🚀",
|
||
color: "#3b82f6",
|
||
bgColor: "#eff6ff",
|
||
},
|
||
[LogDirection.RESPONSE]: {
|
||
emoji: "📨",
|
||
color: "#10b981",
|
||
bgColor: "#f0fdf4",
|
||
},
|
||
},
|
||
[LogType.SOCKET]: {
|
||
[LogDirection.OUTGOING]: { emoji: "🟢", color: "#16a34a" },
|
||
[LogDirection.INCOMING]: { emoji: "🔵", color: "#2563eb" },
|
||
},
|
||
[LogType.ERROR]: { emoji: "❌", color: "#ef4444" },
|
||
[LogType.INFO]: { emoji: "ℹ️", color: "#6366f1" },
|
||
};
|
||
|
||
const typeStyles = styles[type];
|
||
if (
|
||
direction &&
|
||
typeof typeStyles === "object" &&
|
||
direction in typeStyles
|
||
) {
|
||
return typeStyles[direction];
|
||
}
|
||
return typeof typeStyles === "object"
|
||
? { emoji: "📝", color: "#6b7280" }
|
||
: typeStyles;
|
||
}
|
||
|
||
private formatTime(date: Date): string {
|
||
return date.toLocaleTimeString("en-US", {
|
||
hour12: false,
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
second: "2-digit",
|
||
fractionalSecondDigits: 3,
|
||
});
|
||
}
|
||
|
||
log(entry: Omit<LogEntry, "timestamp">) {
|
||
if (!this.shouldLog(entry.type)) return;
|
||
|
||
const timestamp = new Date();
|
||
const { emoji, color, bgColor } = this.getLogStyle(
|
||
entry.type,
|
||
entry.direction
|
||
);
|
||
const timeStr = this.formatTime(timestamp);
|
||
const baseStyle = `color: ${color}; font-weight: bold;`;
|
||
const groupStyle = bgColor
|
||
? `${baseStyle} background: ${bgColor}; padding: 2px 6px; border-radius: 3px;`
|
||
: baseStyle;
|
||
|
||
// Create compact collapsible group
|
||
const groupTitle = `${emoji} ${entry.type}${
|
||
entry.direction ? ` ${entry.direction}` : ""
|
||
}: ${entry.event}`;
|
||
|
||
// Always use groupCollapsed for cleaner output
|
||
console.groupCollapsed(`%c${groupTitle} [${timeStr}]`, groupStyle);
|
||
|
||
// Compact one-line summary with key info
|
||
const summaryParts = [];
|
||
if (entry.method) summaryParts.push(`${entry.method}`);
|
||
if (entry.status) {
|
||
const statusColor =
|
||
entry.status >= 200 && entry.status < 300 ? "✅" : "❌";
|
||
summaryParts.push(`${statusColor} ${entry.status}`);
|
||
}
|
||
if (entry.duration !== undefined)
|
||
summaryParts.push(`⏱️ ${entry.duration}ms`);
|
||
|
||
if (summaryParts.length > 0) {
|
||
console.log(
|
||
`%c${summaryParts.join(" • ")}`,
|
||
"color: #6b7280; font-size: 11px;"
|
||
);
|
||
}
|
||
|
||
if (entry.data !== undefined) {
|
||
// Show preview for objects/arrays, full value for primitives
|
||
if (typeof entry.data === "object" && entry.data !== null) {
|
||
const preview = Array.isArray(entry.data)
|
||
? `Array[${entry.data.length}]`
|
||
: `Object{${Object.keys(entry.data).slice(0, 3).join(", ")}${
|
||
Object.keys(entry.data).length > 3 ? "..." : ""
|
||
}}`;
|
||
console.log(`%c📦 Data:`, "color: #6b7280; font-size: 11px;", preview);
|
||
console.log(entry.data);
|
||
} else {
|
||
console.log(
|
||
`%c📦 Data:`,
|
||
"color: #6b7280; font-size: 11px;",
|
||
entry.data
|
||
);
|
||
}
|
||
}
|
||
|
||
console.groupEnd();
|
||
}
|
||
|
||
// API logging methods
|
||
apiRequest(url: string, method: string, data?: unknown) {
|
||
this.log({
|
||
type: LogType.API,
|
||
direction: LogDirection.REQUEST,
|
||
event: `${method.toUpperCase()} ${url.split("?")[0]}`,
|
||
url,
|
||
method,
|
||
data,
|
||
});
|
||
}
|
||
|
||
apiResponse(
|
||
url: string,
|
||
method: string,
|
||
status: number,
|
||
data?: unknown,
|
||
duration?: number
|
||
) {
|
||
this.log({
|
||
type: LogType.API,
|
||
direction: LogDirection.RESPONSE,
|
||
event: `${method.toUpperCase()} ${url.split("?")[0]}`,
|
||
url,
|
||
method,
|
||
status,
|
||
data,
|
||
duration,
|
||
});
|
||
}
|
||
|
||
// Socket logging methods
|
||
socketOutgoing(event: string, data?: unknown) {
|
||
this.log({
|
||
type: LogType.SOCKET,
|
||
direction: LogDirection.OUTGOING,
|
||
event,
|
||
data,
|
||
});
|
||
}
|
||
|
||
socketIncoming(event: string, data?: unknown) {
|
||
this.log({
|
||
type: LogType.SOCKET,
|
||
direction: LogDirection.INCOMING,
|
||
event,
|
||
data,
|
||
});
|
||
}
|
||
|
||
// Connection state logging
|
||
socketConnected() {
|
||
console.log(
|
||
`%c✅ SOCKET CONNECTED`,
|
||
"color: #10b981; font-weight: bold; background: #f0fdf4; padding: 2px 6px; border-radius: 3px;"
|
||
);
|
||
}
|
||
|
||
socketDisconnected(reason?: string) {
|
||
console.log(
|
||
`%c❌ SOCKET DISCONNECTED`,
|
||
"color: #ef4444; font-weight: bold; background: #fef2f2; padding: 2px 6px; border-radius: 3px;",
|
||
reason || ""
|
||
);
|
||
}
|
||
|
||
socketError(error?: unknown) {
|
||
console.log(
|
||
`%c⚠️ SOCKET ERROR`,
|
||
"color: #f59e0b; font-weight: bold; background: #fffbeb; padding: 2px 6px; border-radius: 3px;",
|
||
error || ""
|
||
);
|
||
}
|
||
|
||
// Server-side logging methods
|
||
serverApiRequest(url: string, method: string, body?: unknown) {
|
||
if (!this.shouldLogServer(LogType.API)) return;
|
||
|
||
console.group(`\n🚀 [SERVER] API REQUEST: ${method} ${url}`);
|
||
if (body !== undefined) {
|
||
console.log("📦 Request Body:", JSON.stringify(body, null, 2));
|
||
}
|
||
console.groupEnd();
|
||
}
|
||
|
||
serverApiResponse(
|
||
url: string,
|
||
method: string,
|
||
status: number,
|
||
data?: unknown,
|
||
duration?: number
|
||
) {
|
||
if (!this.shouldLogServer(LogType.API)) return;
|
||
|
||
const emoji = status >= 200 && status < 300 ? "✅" : "❌";
|
||
console.group(
|
||
`\n${emoji} [SERVER] API ${
|
||
status >= 200 && status < 300 ? "SUCCESS" : "ERROR"
|
||
}: ${method} ${url}`
|
||
);
|
||
console.log(`📊 Status: ${status}`);
|
||
if (duration !== undefined) {
|
||
console.log(`⏱️ Duration: ${duration}ms`);
|
||
}
|
||
if (data !== undefined) {
|
||
// Limit response data display to avoid overwhelming logs
|
||
const responsePreview =
|
||
typeof data === "object" && data !== null
|
||
? Array.isArray(data)
|
||
? `Array[${data.length}]`
|
||
: `Object{${Object.keys(data).slice(0, 5).join(", ")}${
|
||
Object.keys(data).length > 5 ? "..." : ""
|
||
}}`
|
||
: data;
|
||
console.log("📦 Response Preview:", responsePreview);
|
||
// Full response data (collapsed)
|
||
console.groupCollapsed("📄 Full Response Data:");
|
||
console.log(data);
|
||
console.groupEnd();
|
||
}
|
||
console.groupEnd();
|
||
}
|
||
|
||
// Control methods
|
||
enable() {
|
||
this.enabled = true;
|
||
if (typeof window !== "undefined") {
|
||
localStorage.setItem("dev-logger-enabled", "true");
|
||
}
|
||
console.log(
|
||
"%c📝 Dev Logger ENABLED",
|
||
"color: #10b981; font-weight: bold;"
|
||
);
|
||
}
|
||
|
||
disable() {
|
||
this.enabled = false;
|
||
if (typeof window !== "undefined") {
|
||
localStorage.setItem("dev-logger-enabled", "false");
|
||
}
|
||
console.log(
|
||
"%c📝 Dev Logger DISABLED",
|
||
"color: #ef4444; font-weight: bold;"
|
||
);
|
||
}
|
||
|
||
enableType(type: LogType) {
|
||
this.enabledTypes.add(type);
|
||
this.saveEnabledTypes();
|
||
console.log(
|
||
`%c📝 ${type} logging ENABLED`,
|
||
"color: #10b981; font-weight: bold;"
|
||
);
|
||
}
|
||
|
||
disableType(type: LogType) {
|
||
this.enabledTypes.delete(type);
|
||
this.saveEnabledTypes();
|
||
console.log(
|
||
`%c📝 ${type} logging DISABLED`,
|
||
"color: #ef4444; font-weight: bold;"
|
||
);
|
||
}
|
||
|
||
private saveEnabledTypes() {
|
||
if (typeof window !== "undefined") {
|
||
localStorage.setItem(
|
||
"dev-logger-types",
|
||
JSON.stringify(Array.from(this.enabledTypes))
|
||
);
|
||
}
|
||
}
|
||
|
||
// Helper method to show current settings
|
||
status() {
|
||
console.group(
|
||
"%c🔧 Dev Logger Status",
|
||
"color: #6366f1; font-weight: bold;"
|
||
);
|
||
console.log("Enabled:", this.enabled);
|
||
console.log("Active Types:", Array.from(this.enabledTypes));
|
||
console.groupEnd();
|
||
}
|
||
}
|
||
|
||
// Create singleton instance
|
||
export const devLogger = new DevLogger();
|
||
|
||
// Make it available globally for easy console access
|
||
if (typeof window !== "undefined") {
|
||
(window as any).devLogger = devLogger;
|
||
}
|
||
|
||
// Export convenience methods for quick filtering
|
||
export const filterAPI = () => {
|
||
console.clear();
|
||
devLogger.disableType(LogType.SOCKET);
|
||
devLogger.disableType(LogType.ERROR);
|
||
devLogger.disableType(LogType.INFO);
|
||
devLogger.enableType(LogType.API);
|
||
};
|
||
|
||
export const filterSocket = () => {
|
||
console.clear();
|
||
devLogger.disableType(LogType.API);
|
||
devLogger.disableType(LogType.ERROR);
|
||
devLogger.disableType(LogType.INFO);
|
||
devLogger.enableType(LogType.SOCKET);
|
||
};
|
||
|
||
export const showAll = () => {
|
||
console.clear();
|
||
Object.values(LogType).forEach((type) => devLogger.enableType(type));
|
||
};
|