223 lines
6.1 KiB
TypeScript
223 lines
6.1 KiB
TypeScript
type LogLevel = "error" | "warn" | "info" | "http" | "debug";
|
|
|
|
export interface LogEntry {
|
|
level: LogLevel;
|
|
timestamp: string;
|
|
message: any;
|
|
metadata?: any;
|
|
}
|
|
|
|
interface Error {
|
|
code: string;
|
|
message: string;
|
|
description?: string;
|
|
detail?: string;
|
|
error?: any;
|
|
actionable?: boolean;
|
|
}
|
|
|
|
class BrowserLogger {
|
|
private queue: LogEntry[] = [];
|
|
private timer: ReturnType<typeof setInterval> | null = null;
|
|
private readonly BATCH_INTERVAL = 5000; // 5 seconds
|
|
private readonly BATCH_SIZE_LIMIT = 50;
|
|
private readonly isDev: boolean;
|
|
|
|
constructor(isDev: boolean) {
|
|
this.isDev = isDev;
|
|
if (!this.isDev) {
|
|
this.startBatchTimer();
|
|
this.setupBeforeUnloadHandler();
|
|
}
|
|
}
|
|
|
|
private startBatchTimer() {
|
|
this.timer = setInterval(() => this.flush(), this.BATCH_INTERVAL);
|
|
}
|
|
|
|
private setupBeforeUnloadHandler() {
|
|
// Flush logs before page unload to avoid losing them
|
|
if (typeof window !== "undefined") {
|
|
window.addEventListener("beforeunload", () => {
|
|
this.flushSync();
|
|
});
|
|
}
|
|
}
|
|
|
|
private async flush() {
|
|
if (this.queue.length === 0) return;
|
|
|
|
const batch = [...this.queue];
|
|
this.queue = [];
|
|
|
|
try {
|
|
// Forward batch to Hono route
|
|
await fetch("/api/logs", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ logs: batch }),
|
|
});
|
|
} catch (err) {
|
|
console.error("Axiom batch upload failed", err);
|
|
// Re-add failed logs back to queue (up to a limit to avoid memory issues)
|
|
if (this.queue.length < this.BATCH_SIZE_LIMIT * 2) {
|
|
this.queue.push(...batch);
|
|
}
|
|
}
|
|
}
|
|
|
|
private flushSync() {
|
|
// Synchronous flush for beforeunload using sendBeacon
|
|
if (this.queue.length === 0) return;
|
|
|
|
const batch = [...this.queue];
|
|
this.queue = [];
|
|
|
|
try {
|
|
const blob = new Blob([JSON.stringify({ logs: batch })], {
|
|
type: "application/json",
|
|
});
|
|
navigator.sendBeacon("/api/logs", blob);
|
|
} catch (err) {
|
|
console.error("Failed to send logs via sendBeacon", err);
|
|
}
|
|
}
|
|
|
|
private serializeError(error: unknown): any {
|
|
if (error instanceof Error) {
|
|
return {
|
|
name: error.name,
|
|
message: error.message,
|
|
stack: error.stack,
|
|
...(error.cause && { cause: this.serializeError(error.cause) }),
|
|
};
|
|
}
|
|
return error;
|
|
}
|
|
|
|
private createLogEntry(
|
|
level: LogLevel,
|
|
message: any,
|
|
metadata?: any,
|
|
): LogEntry {
|
|
// Handle Error serialization for message
|
|
const cleanMessage =
|
|
message instanceof Error ? this.serializeError(message) : message;
|
|
|
|
// Handle Error serialization for metadata
|
|
const cleanMetadata =
|
|
metadata instanceof Error
|
|
? this.serializeError(metadata)
|
|
: metadata;
|
|
|
|
return {
|
|
level,
|
|
timestamp: new Date().toISOString(),
|
|
message: cleanMessage,
|
|
metadata: cleanMetadata || {},
|
|
};
|
|
}
|
|
|
|
private async sendLog(entry: LogEntry) {
|
|
// Always log errors to console, even in production (for user debugging)
|
|
// In dev, log everything to console
|
|
const shouldConsoleLog = this.isDev || entry.level === "error";
|
|
|
|
if (shouldConsoleLog) {
|
|
const consoleMethod =
|
|
entry.level === "http" ? "debug" : entry.level;
|
|
console[consoleMethod](
|
|
`[client-${entry.level}] ${entry.timestamp}:`,
|
|
entry.message,
|
|
entry.metadata && Object.keys(entry.metadata).length > 0
|
|
? entry.metadata
|
|
: "",
|
|
);
|
|
}
|
|
|
|
// In production, add to queue for batching
|
|
if (!this.isDev) {
|
|
this.queue.push(entry);
|
|
|
|
// Safety flush if queue gets too large
|
|
if (this.queue.length >= this.BATCH_SIZE_LIMIT) {
|
|
await this.flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
error(message: any, metadata?: any) {
|
|
this.sendLog(this.createLogEntry("error", message, metadata));
|
|
}
|
|
|
|
warn(message: any, metadata?: any) {
|
|
this.sendLog(this.createLogEntry("warn", message, metadata));
|
|
}
|
|
|
|
info(message: any, metadata?: any) {
|
|
this.sendLog(this.createLogEntry("info", message, metadata));
|
|
}
|
|
|
|
http(message: any, metadata?: any) {
|
|
this.sendLog(this.createLogEntry("http", message, metadata));
|
|
}
|
|
|
|
debug(message: any, metadata?: any) {
|
|
this.sendLog(this.createLogEntry("debug", message, metadata));
|
|
}
|
|
|
|
// Manual flush method for advanced use cases
|
|
async forceFlush() {
|
|
await this.flush();
|
|
}
|
|
|
|
// Cleanup method
|
|
destroy() {
|
|
if (this.timer) {
|
|
clearInterval(this.timer);
|
|
this.timer = null;
|
|
}
|
|
this.flushSync();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Factory function to create a BrowserLogger instance
|
|
*
|
|
* @param isDev - Whether the app is running in development mode
|
|
* @returns A new BrowserLogger instance
|
|
*
|
|
* @example
|
|
* // SvelteKit
|
|
* import { dev } from '$app/environment';
|
|
* const logger = getLoggerInstance(dev);
|
|
*
|
|
* @example
|
|
* // Next.js
|
|
* const logger = getLoggerInstance(process.env.NODE_ENV === 'development');
|
|
*
|
|
* @example
|
|
* // Vite
|
|
* const logger = getLoggerInstance(import.meta.env.DEV);
|
|
*/
|
|
function getLoggerInstance(isDev: boolean): BrowserLogger {
|
|
return new BrowserLogger(isDev);
|
|
}
|
|
|
|
function getError(logger: BrowserLogger, payload: Error, error?: any) {
|
|
logger.error(payload);
|
|
if (error) {
|
|
logger.error(error);
|
|
}
|
|
return {
|
|
code: payload.code,
|
|
message: payload.message,
|
|
description: payload.description,
|
|
detail: payload.detail,
|
|
error: error,
|
|
actionable: payload.actionable,
|
|
} as Error;
|
|
}
|
|
|
|
export { getError, getLoggerInstance };
|