Add production observability: Sentry, pino, and sharp badge encoding.
Local Build and Deploy / deploy (push) Successful in 1m9s

Sentry is opt-in via DSN env vars; logger uses structured pino JSON in prod; badge uploads are normalized to GIF with sharp.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-17 23:09:57 +02:00
1 parent 6c81af69ea
commit 09f1bc2bd6
16 files changed
+2206 -121

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
import "server-only";
import sharp from "sharp";
/** Habbo-style badge icons are small; keep within this box without upscaling. */
const MAX_BADGE_EDGE = 64;
/**
* Normalize an uploaded badge image to GIF bytes for the emulator album folder.
* Accepts PNG or GIF input; re-encodes and optionally downsizes oversized assets.
*/
export async function toBadgeGif(input: Buffer): Promise<Buffer> {
return sharp(input)
.resize({
width: MAX_BADGE_EDGE,
height: MAX_BADGE_EDGE,
fit: "inside",
withoutEnlargement: true,
})
.gif()
.toBuffer();
}
+26 -72
View File
@@ -1,25 +1,28 @@
import pino from "pino";
type LogLevel = "debug" | "info" | "warn" | "error";
interface LogEntry {
level: LogLevel;
message: string;
timestamp: string;
requestId?: string;
module?: string;
[key: string]: unknown;
}
const LOG_LEVELS: Record<LogLevel, number> = {
debug: 0,
info: 1,
warn: 2,
error: 3,
};
const currentLevel: LogLevel =
(process.env.LOG_LEVEL as LogLevel) ??
const level: LogLevel =
(process.env.LOG_LEVEL as LogLevel | undefined) ??
(process.env.NODE_ENV === "production" ? "info" : "debug");
const isProd = process.env.NODE_ENV === "production";
const isTest =
process.env.NODE_ENV === "test" || process.env.VITEST === "true";
const pinoLogger = pino({
level,
base: { service: "atomcms-next" },
...(isProd || isTest
? {}
: {
transport: {
target: "pino-pretty",
options: { colorize: true, translateTime: "SYS:standard" },
},
}),
});
let requestIdCounter = 0;
export function generateRequestId(): string {
@@ -27,67 +30,18 @@ export function generateRequestId(): string {
return `${Date.now().toString(36)}-${requestIdCounter.toString(36)}`;
}
function shouldLog(level: LogLevel): boolean {
// eslint-disable-next-line security/detect-object-injection -- LOG_LEVELS keyed by LogLevel union
return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
}
function formatLog(entry: LogEntry): string {
return JSON.stringify(entry);
}
function writeLog(entry: LogEntry): void {
if (!shouldLog(entry.level)) return;
const formatted = formatLog(entry);
switch (entry.level) {
case "error":
console.error(formatted);
break;
case "warn":
console.warn(formatted);
break;
default:
console.log(formatted);
break;
}
}
/** Compatibility wrapper — existing call sites use (message, meta). */
export const logger = {
debug(message: string, meta: Record<string, unknown> = {}): void {
writeLog({
level: "debug",
message,
timestamp: new Date().toISOString(),
...meta,
});
pinoLogger.debug(meta, message);
},
info(message: string, meta: Record<string, unknown> = {}): void {
writeLog({
level: "info",
message,
timestamp: new Date().toISOString(),
...meta,
});
pinoLogger.info(meta, message);
},
warn(message: string, meta: Record<string, unknown> = {}): void {
writeLog({
level: "warn",
message,
timestamp: new Date().toISOString(),
...meta,
});
pinoLogger.warn(meta, message);
},
error(message: string, meta: Record<string, unknown> = {}): void {
writeLog({
level: "error",
message,
timestamp: new Date().toISOString(),
...meta,
});
pinoLogger.error(meta, message);
},
};
+51
View File
@@ -0,0 +1,51 @@
import type { ErrorEvent, EventHint } from "@sentry/nextjs";
const SENSITIVE_KEY_RE =
/password|secret|token|otp|recovery|authTicket|two_factor|api_key/i;
const REDACTED = "[Redacted]";
function redactObject(input: unknown, depth = 0): unknown {
if (depth > 4 || input == null) return input;
if (Array.isArray(input)) return input.map((v) => redactObject(v, depth + 1));
if (typeof input !== "object") return input;
const out: Record<string, unknown> = {};
for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
if (SENSITIVE_KEY_RE.test(key)) {
out[key] = REDACTED;
} else {
out[key] = redactObject(value, depth + 1);
}
}
return out;
}
/** Scrub cookies/auth headers and sensitive keys before sending to Sentry. */
export function redactSentryEvent(
event: ErrorEvent,
_hint: EventHint,
): ErrorEvent | null {
if (event.request) {
if (event.request.cookies) {
event.request.cookies =
REDACTED as unknown as typeof event.request.cookies;
}
if (event.request.headers) {
const headers = event.request.headers as Record<string, string>;
if (headers.cookie) headers.cookie = REDACTED;
if (headers.Cookie) headers.Cookie = REDACTED;
if (headers.authorization) headers.authorization = REDACTED;
if (headers.Authorization) headers.Authorization = REDACTED;
}
if (event.request.data) {
event.request.data = redactObject(
event.request.data,
) as typeof event.request.data;
}
}
if (event.extra) event.extra = redactObject(event.extra) as typeof event.extra;
if (event.contexts) {
event.contexts = redactObject(event.contexts) as typeof event.contexts;
}
return event;
}