fix errors and add logger
This commit is contained in:
parent
5b8fc5047d
commit
e424cc7f23
@ -4,6 +4,8 @@ import { io, Socket } from "socket.io-client";
|
||||
import { createStore, useStore } from "zustand";
|
||||
import { subscribeWithSelector } from "zustand/middleware";
|
||||
|
||||
import { devLogger } from "@/shared/utils/logger";
|
||||
|
||||
import type { ClientToServerEvents, ServerToClientEvents } from "./events";
|
||||
|
||||
export enum ESocketStatus {
|
||||
@ -90,21 +92,31 @@ export const useSocketStore = createStore<SocketStore>()(
|
||||
socket.removeAllListeners();
|
||||
};
|
||||
|
||||
// Universal incoming event logger
|
||||
socket.onAny((event: string, ...args: unknown[]) => {
|
||||
// Skip logging built-in socket.io events to avoid noise
|
||||
const systemEvents = ['connect', 'disconnect', 'connect_error', 'reconnect'];
|
||||
if (!systemEvents.includes(event)) {
|
||||
devLogger.socketIncoming(event, ...args);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("connect", () => {
|
||||
devLogger.socketConnected();
|
||||
set({ status: ESocketStatus.CONNECTED, reconnectAttempt: 0, socket });
|
||||
get().clearReconnectTimer();
|
||||
// get().flushQueue();
|
||||
});
|
||||
|
||||
socket.on("disconnect", _reason => {
|
||||
socket.on("disconnect", reason => {
|
||||
devLogger.socketDisconnected(reason);
|
||||
set({ status: ESocketStatus.DISCONNECTED });
|
||||
cleanListeners();
|
||||
scheduleReconnect();
|
||||
});
|
||||
|
||||
socket.on("connect_error", err => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Socket connect_error:", err);
|
||||
devLogger.socketError(err);
|
||||
set({ status: ESocketStatus.ERROR, error: err.message });
|
||||
scheduleReconnect();
|
||||
});
|
||||
@ -138,6 +150,7 @@ export const useSocketStore = createStore<SocketStore>()(
|
||||
emit: (event, ...args) => {
|
||||
const { socket, status } = get();
|
||||
if (status === ESocketStatus.CONNECTED && socket && socket.connected) {
|
||||
devLogger.socketOutgoing(event as string, ...args);
|
||||
socket.emit(event, ...args);
|
||||
} else {
|
||||
// get().enqueue(event, ...args);
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
/* eslint-disable no-console */
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerAccessToken } from "../auth/token";
|
||||
import { devLogger } from "../utils/logger";
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
@ -55,12 +57,35 @@ class HttpClient {
|
||||
...rest
|
||||
} = opts;
|
||||
|
||||
const fullUrl = this.buildUrl(rootUrl, path, query);
|
||||
const startTime = Date.now();
|
||||
|
||||
// Log API request (both client and server with ENV control)
|
||||
if (typeof window !== 'undefined') {
|
||||
// Client-side logging
|
||||
devLogger.apiRequest(fullUrl, method, body);
|
||||
} else {
|
||||
// Server-side logging (requires ENV variable)
|
||||
if (typeof devLogger.serverApiRequest === 'function') {
|
||||
devLogger.serverApiRequest(fullUrl, method, body);
|
||||
} else {
|
||||
// Fallback server logging
|
||||
if (process.env.DEV_LOGGER_SERVER_ENABLED === 'true') {
|
||||
console.group(`\n🚀 [SERVER] API REQUEST: ${method} ${fullUrl}`);
|
||||
if (body !== undefined) {
|
||||
console.log('📦 Request Body:', JSON.stringify(body, null, 2));
|
||||
}
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
const accessToken = await getServerAccessToken();
|
||||
if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`);
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
const res = await fetch(this.buildUrl(rootUrl, path, query), {
|
||||
const res = await fetch(fullUrl, {
|
||||
method,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
headers,
|
||||
@ -69,8 +94,30 @@ class HttpClient {
|
||||
});
|
||||
|
||||
const payload = await res.json().catch(() => null);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (!res.ok) {
|
||||
// Log API error response (both client and server)
|
||||
if (typeof window !== 'undefined') {
|
||||
devLogger.apiResponse(fullUrl, method, res.status, payload, duration);
|
||||
} else {
|
||||
if (typeof devLogger.serverApiResponse === 'function') {
|
||||
devLogger.serverApiResponse(fullUrl, method, res.status, payload, duration);
|
||||
} else {
|
||||
// Fallback server logging
|
||||
if (process.env.DEV_LOGGER_SERVER_ENABLED === 'true') {
|
||||
const emoji = res.status >= 200 && res.status < 300 ? '✅' : '❌';
|
||||
console.group(`\n${emoji} [SERVER] API ERROR: ${method} ${fullUrl}`);
|
||||
console.log(`📊 Status: ${res.status}`);
|
||||
console.log(`⏱️ Duration: ${duration}ms`);
|
||||
if (payload !== undefined) {
|
||||
console.log('📦 Error Response:', payload);
|
||||
}
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (res.status === 401 && !skipAuthRedirect) {
|
||||
redirect(process.env.NEXT_PUBLIC_AUTH_REDIRECT_URL || "");
|
||||
}
|
||||
@ -78,7 +125,34 @@ class HttpClient {
|
||||
}
|
||||
|
||||
const data = payload as T;
|
||||
return schema ? schema.parse(data) : data;
|
||||
const validatedData = schema ? schema.parse(data) : data;
|
||||
|
||||
// Log successful API response (both client and server)
|
||||
if (typeof window !== 'undefined') {
|
||||
devLogger.apiResponse(fullUrl, method, res.status, validatedData, duration);
|
||||
} else {
|
||||
if (typeof devLogger.serverApiResponse === 'function') {
|
||||
devLogger.serverApiResponse(fullUrl, method, res.status, validatedData, duration);
|
||||
} else {
|
||||
// Fallback server logging
|
||||
if (process.env.DEV_LOGGER_SERVER_ENABLED === 'true') {
|
||||
console.group(`\n✅ [SERVER] API SUCCESS: ${method} ${fullUrl}`);
|
||||
console.log(`📊 Status: ${res.status}`);
|
||||
console.log(`⏱️ Duration: ${duration}ms`);
|
||||
if (validatedData !== undefined) {
|
||||
const responsePreview = typeof validatedData === 'object' && validatedData !== null
|
||||
? Array.isArray(validatedData)
|
||||
? `Array[${validatedData.length}]`
|
||||
: `Object{${Object.keys(validatedData).slice(0, 5).join(', ')}${Object.keys(validatedData).length > 5 ? '...' : ''}}`
|
||||
: validatedData;
|
||||
console.log('📦 Response Preview:', responsePreview);
|
||||
}
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return validatedData;
|
||||
}
|
||||
|
||||
get = <T>(p: string, o?: RequestOpts, u?: string) =>
|
||||
|
||||
325
src/shared/utils/logger.ts
Normal file
325
src/shared/utils/logger.ts
Normal file
@ -0,0 +1,325 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
"use client";
|
||||
/* eslint-disable no-console */
|
||||
|
||||
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));
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user