Логирование и ошибки
Правила
MUST
- Логи не раскрывают внутреннюю архитектуру: пути файлов, имена сервисов, структуру проекта
- Source maps отключены для production или отправляются только в систему мониторинга (Sentry)
- В системах сбора ошибок настроена маскировка чувствительных данных через
beforeSend/beforeBreadcrumb
FORBIDDEN
- Логирование токенов, паролей, PII (email, телефон, данные карт)
- Публикация
.mapфайлов на публичных серверах в production - Debug-логирование в production-сборках
- Логирование полного stack trace в production
SHOULD
- Использовать структурированное логирование
- Логировать только необходимый минимум информации
Безопасное логирование
Запрещённые практики
// ❌ FORBIDDEN: Логирование чувствительных данных
function login(credentials: { email: string; password: string }) {
console.log('Login attempt:', credentials); // Логируется пароль!
}
function fetchUserData(token: string) {
console.log('API token:', token); // Логируется токен!
}
// ❌ FORBIDDEN: Раскрытие внутренней архитектуры
function handleError(error: Error) {
console.error('Full error:', error.stack); // Раскрывает структуру проекта!
}
// ❌ FORBIDDEN: Debug-логи в production
function processPayment(data: PaymentData) {
console.log('Processing payment:', data); // Останется в production!
}
Правильный подход
// ✅ Безопасное логирование
function login(credentials: { email: string; password: string }) {
console.log('Login attempt for:', credentials.email); // Только email
}
function handleError(error: ApiError) {
// Production: только код ошибки
console.error('API Error:', error.code);
// Development: расширенная информация
if (process.env.NODE_ENV === 'development') {
console.error('API Error details:', {
code: error.code,
message: error.message,
});
}
}
Автоматическая санитизация логов
Все данные, попадающие в логи, проходят через слой санитизации — это предотвращает случайную утечку секретов, токенов и чувствительных полей, даже если разработчик забыл об этом при логировании.
Санитизация работает в двух направлениях: по имени ключа (например, password, token) и по значению — через регулярные выражения, покрывающие основные форматы секретов.
secrets.ts — паттерны и хелперы
export interface SecretPattern {
id: string;
regex: RegExp;
}
// Список известных форматов секретов для проверки по значению
export const SECRET_PATTERNS: SecretPattern[] = [
{ id: 'jwt', regex: /eyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*/g },
{ id: 'bearer', regex: /Bearer\s+[A-Za-z0-9_-]{20,}/gi },
{ id: 'github-pat', regex: /ghp_[A-Za-z0-9]{36}/g },
{ id: 'github-oauth', regex: /gho_[A-Za-z0-9]{36}/g },
{ id: 'openai', regex: /sk-[A-Za-z0-9]{48}/g },
{ id: 'stripe-secret', regex: /sk_(live|test)_[A-Za-z0-9]{24,}/g },
{ id: 'stripe-pk', regex: /pk_(live|test)_[A-Za-z0-9]{24,}/g },
{ id: 'google-oauth', regex: /ya29\.[A-Za-z0-9_-]+/g },
{ id: 'firebase', regex: /AIza[A-Za-z0-9_-]{35}/g },
{ id: 'slack', regex: /xox[baprs]-[A-Za-z0-9-]{10,}/g },
{
id: 'discord',
regex: /[MN][A-Za-z0-9]{23,}\.[A-Za-z0-9-_]{6}\.[A-Za-z0-9-_]{27}/g,
},
{ id: 'sendgrid', regex: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g },
{ id: 'twilio', regex: /SK[a-f0-9]{32}/g },
{ id: 'aws', regex: /(AKIA|ASIA)[A-Z0-9]{16}/g },
{ id: 'private-key', regex: /-----BEGIN[A-Z ]*PRIVATE KEY-----/g },
];
// Проверка по имени ключа — если само название поля намекает на секрет
export const SENSITIVE_KEY_PATTERN =
/^(password|passwd|pwd|secret|token|apikey|api_key|auth|authorization|credential|bearer|private_key|access_token|refresh_token)$/i;
// Проверяет, содержит ли строка секрет по одному из паттернов
// lastIndex сбрасывается вручную, так как все паттерны используют флаг /g
export const isSecretValue = (value: unknown): boolean => {
if (typeof value !== 'string') return false;
return SECRET_PATTERNS.some((pattern) => {
pattern.regex.lastIndex = 0;
return pattern.regex.test(value);
});
};
// Проверяет, является ли имя ключа чувствительным
export const isSensitiveKey = (key: string): boolean => {
return SENSITIVE_KEY_PATTERN.test(key);
};
logger.ts — Logger с встроенной санитизацией
Logger рекурсивно обходит объект перед выводом и заменяет все чувствительные значения на [SECRET]. В production из ошибок выводится только тип и код — без стека и деталей.
import { isSecretValue, isSensitiveKey } from './secrets';
class Logger {
static log(message: string, data?: unknown) {
if (process.env.NODE_ENV !== 'development') return;
console.log(message, this.sanitize(data));
}
static error(message: string, error?: unknown) {
if (process.env.NODE_ENV === 'development') {
console.error(message, this.sanitize(error));
} else {
// Production: только безопасные метаданные ошибки
console.error(message, {
type: (error as Error)?.name || 'Error',
code: (error as any)?.code,
});
}
}
private static sanitize(data: unknown): unknown {
if (typeof data !== 'object' || data === null) {
return isSecretValue(data) ? '[SECRET]' : data;
}
if (Array.isArray(data)) {
return data.map((item) => this.sanitize(item));
}
return Object.fromEntries(
Object.entries(data as Record<string, unknown>).map(([key, value]) => {
if (isSensitiveKey(key)) return [key, '[SECRET]']; // → по имени ключа
if (isSecretValue(value)) return [key, '[SECRET]']; // → по значению
return [key, this.sanitize(value)]; // → рекурсия вглубь
})
);
}
}
Пример работы
Logger.log('user session', {
userId: 'u_123',
token: 'ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ123456789',
meta: {
ip: '192.168.1.1',
authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9...',
},
});
// Вывод:
// {
// userId: 'u_123',
// token: '[SECRET]', ← чувствительный ключ
// meta: {
// ip: '192.168.1.1',
// authorization: '[SECRET]', ← чувствительный ключ
// }
// }
Source Maps
MUST
// next.config.js
// ❌ FORBIDDEN: Публичные source maps в production
productionBrowserSourceMaps: false, // MUST быть false!
webpack: (config, { isServer, dev }) => {
if (!dev && !isServer) {
// ✅ Генерируем для Sentry, но не публикуем в бандл
config.devtool = 'hidden-source-map';
}
return config;
}
Загрузка source maps только в Sentry
// next.config.js
const { withSentryConfig } = require('@sentry/nextjs');
module.exports = withSentryConfig(
{
productionBrowserSourceMaps: false, // Не публикуем
},
{
hideSourceMaps: true, // Source maps только в Sentry
widenClientFileUpload: true,
}
);
# ✅ Проверка: source maps не должны быть публично доступны
ls .next/static/**/*.map
# Если файлы найдены — это ошибка!
# В .js файлах не должно быть:
# //# sourceMappingURL=