fix errors and add logger

This commit is contained in:
dev.daminik00 2025-07-22 03:23:37 +03:00 committed by gofnnp
parent 5b8fc5047d
commit e424cc7f23
3 changed files with 417 additions and 5 deletions

View File

@ -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);

View File

@ -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
View 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));
};