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:
Yusuf İpek
2026-04-10 21:10:09 +03:00
parent 07f7b6fb02
commit 8014fc3986
60 changed files with 235 additions and 115 deletions
+6 -5
View File
@@ -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
View File
@@ -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;
}
}
+61
View File
@@ -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));
}
+5 -4
View File
@@ -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
View File
@@ -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 -2
View File
@@ -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
View File
@@ -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.
+2 -1
View File
@@ -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;
}
}