mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
Refactor error logging across the application to use a centralized logger
- Introduced a new logger utility (`logError`) to standardize error logging. - Replaced all instances of `console.error` with `logError` in various API routes and libraries. - Enhanced error logging to sanitize sensitive information, particularly for Prisma and Stripe errors. - Ensured consistent error handling and logging practices throughout the codebase.
This commit is contained in:
+6
-5
@@ -3,6 +3,7 @@ import { db } from '@/lib/db';
|
||||
import { r2Client, R2_BUCKET_NAME } from '@/lib/r2';
|
||||
import { ListObjectsV2Command, type ListObjectsV2CommandInput } from '@aws-sdk/client-s3';
|
||||
import { isBunnyUploadsFeatureEnabled } from '@/lib/feature-flags';
|
||||
import { logError } from '@/lib/logger';
|
||||
|
||||
const BUNNY_API_BASE = 'https://video.bunnycdn.com';
|
||||
const STORAGE_CACHE_SECONDS = 600;
|
||||
@@ -174,7 +175,7 @@ export async function getCachedTotalStorage(): Promise<number> {
|
||||
const snapshot = await getR2StorageSnapshot();
|
||||
return snapshot.totalBytes;
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch total storage stats:', err);
|
||||
logError('Failed to fetch total storage stats:', err);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -184,7 +185,7 @@ export const getCachedBunnyStorageStats = unstable_cache(
|
||||
try {
|
||||
return await fetchBunnyStorageStats();
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch Bunny storage stats:', err);
|
||||
logError('Failed to fetch Bunny storage stats:', err);
|
||||
return { totalBytes: -1, byVideoId: {} } as BunnyStorageStats;
|
||||
}
|
||||
},
|
||||
@@ -247,7 +248,7 @@ export const getCachedUserBunnyStorage = unstable_cache(
|
||||
perUserStorage[billedUserId] = (perUserStorage[billedUserId] || 0) + size;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to calculate per-user Bunny storage:', err);
|
||||
logError('Failed to calculate per-user Bunny storage:', err);
|
||||
}
|
||||
return perUserStorage;
|
||||
},
|
||||
@@ -376,7 +377,7 @@ export async function getCachedUserMediaStorage(): Promise<Record<string, { tota
|
||||
userStorage[billedUserId].total += size;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to parse user storage:', err);
|
||||
logError('Failed to parse user storage:', err);
|
||||
}
|
||||
return userStorage;
|
||||
}
|
||||
@@ -398,7 +399,7 @@ export const getCachedUserDownloadEgress = unstable_cache(
|
||||
: 0;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to calculate per-user download egress:', err);
|
||||
logError('Failed to calculate per-user download egress:', err);
|
||||
}
|
||||
|
||||
return perUserDownloadEgress;
|
||||
|
||||
+2
-1
@@ -10,6 +10,7 @@ import {
|
||||
emailRow,
|
||||
escapeHtml,
|
||||
} from '@/lib/email-brand';
|
||||
import { logError } from '@/lib/logger';
|
||||
|
||||
const INVITATION_TTL_DAYS = 7;
|
||||
const MAX_INVITATION_RETRIES = 3;
|
||||
@@ -79,7 +80,7 @@ export async function sendInvitationEmail(input: {
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Invitation email send failed:', error);
|
||||
logError('Invitation email send failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Structured error logger that prevents sensitive internals from leaking to
|
||||
* log aggregators (Datadog, Sentry, etc.).
|
||||
*
|
||||
* - Prisma errors: only the error code is logged (messages can embed raw SQL
|
||||
* fragments, WHERE-clause values, and internal column/table names).
|
||||
* - Stripe errors: message is safe and included; HTTP status code is appended.
|
||||
* - All other Error instances: only the message string is logged; stack traces
|
||||
* are suppressed.
|
||||
* - Non-Error values (structured objects, strings, numbers): passed through
|
||||
* unchanged, since they were already controlled by the caller.
|
||||
*/
|
||||
|
||||
type SanitizedError = {
|
||||
type: string;
|
||||
message: string;
|
||||
code?: string;
|
||||
};
|
||||
|
||||
function sanitizeError(err: unknown): SanitizedError | unknown {
|
||||
if (!(err instanceof Error)) {
|
||||
// Let structured objects, numbers, strings, etc. pass through as-is.
|
||||
return err;
|
||||
}
|
||||
|
||||
const name = err.constructor?.name ?? err.name ?? 'Error';
|
||||
const anyErr = err as unknown as Record<string, unknown>;
|
||||
|
||||
// Prisma client errors: their `.message` can embed raw SQL, WHERE-clause
|
||||
// values, and schema internals. Only safe to expose the Prisma error code.
|
||||
if (name.startsWith('PrismaClient')) {
|
||||
const code = typeof anyErr.code === 'string' ? anyErr.code : 'UNKNOWN';
|
||||
return {
|
||||
type: 'PrismaError',
|
||||
code,
|
||||
message: `Database error [${code}]`,
|
||||
} satisfies SanitizedError;
|
||||
}
|
||||
|
||||
// Stripe SDK errors carry a `type` string and numeric `statusCode`; their
|
||||
// `.message` values are designed to be user-safe.
|
||||
if (typeof anyErr.type === 'string' && typeof anyErr.statusCode === 'number') {
|
||||
return {
|
||||
type: anyErr.type,
|
||||
code: String(anyErr.statusCode),
|
||||
message: err.message,
|
||||
} satisfies SanitizedError;
|
||||
}
|
||||
|
||||
// All other Error instances: include message and type, never the stack.
|
||||
return { type: name, message: err.message } satisfies SanitizedError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an error with sanitized details.
|
||||
*
|
||||
* Use this everywhere in server-side code instead of `console.error(msg, error)`.
|
||||
*/
|
||||
export function logError(context: string, err: unknown): void {
|
||||
console.error(context, sanitizeError(err));
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
emailRow,
|
||||
escapeHtml,
|
||||
} from '@/lib/email-brand';
|
||||
import { logError } from '@/lib/logger';
|
||||
|
||||
// ============================================
|
||||
// NOTIFICATION CHANNELS
|
||||
@@ -50,7 +51,7 @@ async function sendTelegram(
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Telegram send failed:', err);
|
||||
logError('Telegram send failed:', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -93,7 +94,7 @@ async function sendEmail(to: string, subject: string, html: string): Promise<boo
|
||||
await transporter.sendMail({ from: fromAddress, to, subject, html });
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Email send failed:', err);
|
||||
logError('Email send failed:', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -476,7 +477,7 @@ export async function notifyUsers(userIds: string[], event: NotificationEvent):
|
||||
await Promise.allSettled(promises);
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error('Notification dispatch failed:', err);
|
||||
logError('Notification dispatch failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,7 +485,7 @@ export async function notifyProjectOwner(ownerId: string, event: NotificationEve
|
||||
try {
|
||||
await notifyUsers([ownerId], event);
|
||||
} catch (err) {
|
||||
console.error('Notification dispatch failed:', err);
|
||||
logError('Notification dispatch failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ import { DeleteObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { r2Client, R2_BUCKET_NAME } from '@/lib/r2';
|
||||
import { db } from '@/lib/db';
|
||||
import { runWithConcurrency } from '@/lib/async-pool';
|
||||
import { logError } from '@/lib/logger';
|
||||
|
||||
/** The path prefix for images served by the upload API. */
|
||||
const IMAGE_PATH_PREFIX = '/api/upload/image/';
|
||||
@@ -62,7 +63,7 @@ export async function deleteMediaFilesBestEffort(mediaUrls: string[]): Promise<R
|
||||
);
|
||||
} catch (err) {
|
||||
failedKeys.add(key);
|
||||
console.error(`Failed to delete media from R2 (key: ${key}):`, err);
|
||||
logError(`Failed to delete media from R2 (key: ${key}):`, err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Readable } from 'node:stream';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { apiErrors } from '@/lib/api-response';
|
||||
import { r2Client, R2_BUCKET_NAME } from '@/lib/r2';
|
||||
import { logError } from '@/lib/logger';
|
||||
|
||||
type ProxyR2MediaOptions = {
|
||||
request: Request;
|
||||
@@ -132,7 +133,7 @@ export async function proxyR2MediaObject({
|
||||
},
|
||||
});
|
||||
}
|
||||
console.error('Error proxying R2 object:', retryError);
|
||||
logError('Error proxying R2 object:', retryError);
|
||||
return apiErrors.internalError(internalErrorMessage);
|
||||
}
|
||||
} else if (isNotFoundError(error)) {
|
||||
@@ -146,7 +147,7 @@ export async function proxyR2MediaObject({
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.error('Error proxying R2 object:', error);
|
||||
logError('Error proxying R2 object:', error);
|
||||
return apiErrors.internalError(internalErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -1,5 +1,6 @@
|
||||
import { db } from '@/lib/db';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { logError } from '@/lib/logger';
|
||||
|
||||
const RATE_LIMIT_CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
|
||||
|
||||
@@ -131,7 +132,7 @@ export async function checkRateLimit(
|
||||
return { allowed, remaining, resetAt };
|
||||
} catch (error) {
|
||||
// If table doesn't exist, allow the request but log warning
|
||||
console.error('Rate limit check failed (table may not exist):', error);
|
||||
logError('Rate limit check failed (table may not exist):', error);
|
||||
return {
|
||||
allowed: true,
|
||||
remaining: maxRequests,
|
||||
@@ -215,14 +216,14 @@ export async function cleanupRateLimits(): Promise<void> {
|
||||
try {
|
||||
await db.$executeRaw`SELECT cleanup_rate_limits()`;
|
||||
} catch (error) {
|
||||
console.error('Rate limit cleanup failed:', error);
|
||||
logError('Rate limit cleanup failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Start cleanup interval once per process to avoid duplicate scheduling on module reload.
|
||||
if (!globalForRateLimitCleanup.rateLimitCleanupIntervalStarted && typeof setInterval !== 'undefined') {
|
||||
const interval = setInterval(() => {
|
||||
cleanupRateLimits().catch(console.error);
|
||||
cleanupRateLimits().catch((err) => logError('Unexpected error:', err));
|
||||
}, RATE_LIMIT_CLEANUP_INTERVAL_MS);
|
||||
|
||||
// Avoid keeping Node.js process alive because of housekeeping timers.
|
||||
|
||||
@@ -4,6 +4,7 @@ import { youtubeProvider } from './youtube';
|
||||
import { directProvider } from './direct';
|
||||
import { bunnyProvider } from './bunny';
|
||||
import type { VideoProvider, VideoSource, VideoMetadata, VideoProviderType } from './types';
|
||||
import { logError } from '@/lib/logger';
|
||||
|
||||
// Export types
|
||||
export * from './types';
|
||||
@@ -82,7 +83,7 @@ export async function fetchVideoMetadata(source: VideoSource): Promise<VideoMeta
|
||||
try {
|
||||
return await provider.getMetadata(source.videoId);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch video metadata:', error);
|
||||
logError('Failed to fetch video metadata:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user