Перейти к основному содержимому

Логирование и ошибки

Правила

FORBIDDEN

FORBIDDEN: Логирование токенов, паролей, PII (Personally Identifiable Information)

FORBIDDEN: Генерация и публикация .map файлов на публичных серверах в production

FORBIDDEN: Debug-логирование в production-сборках

FORBIDDEN: Логирование полного stack trace в production

MUST

MUST: Логи не должны раскрывать внутреннее устройство приложения

MUST: Source maps должны быть отключены для production или отправляться только в систему мониторинга (Sentry)

SHOULD

SHOULD: Использовать структурированное логирование

SHOULD: Логировать только необходимый минимум информации

Безопасное логирование

Запрещенные практики

// ❌ FORBIDDEN: Логирование чувствительных данных
function login(credentials: { email: string; password: string }) {
console.log('Login attempt:', credentials); // Опасно! Логируется пароль
api.login(credentials);
}

function handlePayment(cardData: CardData) {
console.log('Payment data:', cardData); // Опасно! Логируются данные карты
}

function fetchUserData(token: string) {
console.log('API token:', token); // Опасно! Логируется токен
return fetch('/api/user', {
headers: { Authorization: `Bearer ${token}` }
});
}

// ❌ FORBIDDEN: Раскрытие внутренностей приложения
function handleError(error: Error) {
console.error('API Error:', error.stack); // Опасно! Раскрывает структуру
console.error('Full error object:', error); // Опасно!
}

// ❌ FORBIDDEN: Debug-логи в production
function processPayment(data: PaymentData) {
console.log('Processing payment:', data); // Останется в production!

if (process.env.NODE_ENV === 'development') {
console.log('Debug:', data); // Все равно опасно!
}
}

Правильный подход

// ✅ Безопасное логирование
function login(credentials: { email: string; password: string }) {
console.log('Login attempt for:', credentials.email); // OK - только email
api.login(credentials);
}

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,
// Stack trace только в dev
});
}
}

// ✅ Структурированное логирование
interface LogEntry {
level: 'info' | 'warn' | 'error';
message: string;
context?: Record<string, unknown>;
timestamp: number;
}

function log(entry: LogEntry) {
const sanitized = sanitizeLogEntry(entry);

if (process.env.NODE_ENV === 'development') {
console.log(JSON.stringify(sanitized, null, 2));
} else {
// В production отправляем только в систему мониторинга
sendToMonitoring(sanitized);
}
}

// Использование
log({
level: 'info',
message: 'User login successful',
context: { userId: user.id }, // Только ID, не email/phone
timestamp: Date.now()
});

Санитизация логов

// ✅ Автоматическая санитизация чувствительных данных
class Logger {
private static isDevelopment = process.env.NODE_ENV === 'development';

private static SENSITIVE_KEYS = [
'password',
'token',
'secret',
'apiKey',
'creditCard',
'cvv',
'ssn',
'accessToken',
'refreshToken'
];

static log(message: string, data?: unknown) {
if (!this.isDevelopment) return;
console.log(message, this.sanitize(data));
}

static error(message: string, error?: unknown) {
const sanitized = this.sanitize(error);

if (this.isDevelopment) {
console.error(message, sanitized);
} else {
// В production только минимум информации
console.error(message, {
type: sanitized?.name || 'Error',
code: sanitized?.code
});
}
}

private static sanitize(data: unknown): unknown {
if (typeof data !== 'object' || data === null) {
return data;
}

if (Array.isArray(data)) {
return data.map(item => this.sanitize(item));
}

const sanitized: Record<string, unknown> = {};

for (const [key, value] of Object.entries(data)) {
if (this.SENSITIVE_KEYS.some(sk => key.toLowerCase().includes(sk))) {
sanitized[key] = '[REDACTED]';
} else if (typeof value === 'object' && value !== null) {
sanitized[key] = this.sanitize(value);
} else {
sanitized[key] = value;
}
}

return sanitized;
}
}

// Использование
Logger.log('User data:', {
email: '[email protected]',
password: '12345', // Будет заменено на [REDACTED]
profile: {
name: 'John',
apiKey: 'secret123' // Будет заменено на [REDACTED]
}
});

// Вывод:
// {
// email: '[email protected]',
// password: '[REDACTED]',
// profile: {
// name: 'John',
// apiKey: '[REDACTED]'
// }
// }

Source Maps

Конфигурация для Next.js

// next.config.js

module.exports = {
// ❌ FORBIDDEN: Публичные source maps в production
productionBrowserSourceMaps: false, // MUST быть false!

webpack: (config, { isServer, dev }) => {
if (!dev && !isServer) {
// ✅ Генерируем source maps для Sentry, но не публикуем
config.devtool = 'hidden-source-map';

// Source maps генерируются, но не ссылаются в бандле
// Можно загрузить в Sentry через CLI
}

return config;
}
};

Загрузка source maps в Sentry

// sentry.client.config.js
import * as Sentry from '@sentry/nextjs';

Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,

// Source maps будут автоматически загружены в Sentry
// при сборке с помощью @sentry/webpack-plugin

environment: process.env.NODE_ENV,

beforeSend(event, hint) {
// ✅ Санитизация перед отправкой
if (event.request) {
delete event.request.cookies;
delete event.request.headers?.Authorization;
}

return event;
}
});
// next.config.js с Sentry plugin
const { withSentryConfig } = require('@sentry/nextjs');

module.exports = withSentryConfig(
{
// Next.js config
productionBrowserSourceMaps: false,
},
{
// Sentry webpack plugin options
silent: true,

// ✅ Source maps загружаются в Sentry, но не публикуются
hideSourceMaps: true,

// Удаление source maps после загрузки
widenClientFileUpload: true,
}
);

Проверка source maps

# ❌ FORBIDDEN: Source maps в публичной директории
ls .next/static/**/*.map
# Если файлы найдены - это ошибка!

# ✅ Проверка, что source maps не загружаются браузером
# В DevTools → Network не должно быть запросов к .map файлам
# В .js файлах не должно быть строки: //# sourceMappingURL=

Интеграция с системами мониторинга

Sentry

// ✅ Безопасная настройка Sentry
import * as Sentry from '@sentry/nextjs';

Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NODE_ENV,

// Процент событий для отправки
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,

beforeSend(event, hint) {
// ✅ Санитизация чувствительных данных
if (event.request) {
// Удаляем cookies
delete event.request.cookies;

// Удаляем auth headers
if (event.request.headers) {
delete event.request.headers.Authorization;
delete event.request.headers.Cookie;
}

// Маскируем query parameters с токенами
if (event.request.query_string) {
const params = new URLSearchParams(event.request.query_string);
if (params.has('token')) {
params.set('token', '[REDACTED]');
}
event.request.query_string = params.toString();
}
}

// Удаляем PII из context
if (event.user) {
delete event.user.email;
delete event.user.ip_address;
}

return event;
},

// ✅ Игнорирование известных безвредных ошибок
ignoreErrors: [
'ResizeObserver loop limit exceeded',
'Non-Error promise rejection captured',
],
});

// Использование
try {
await dangerousOperation();
} catch (error) {
Sentry.captureException(error, {
level: 'error',
tags: {
section: 'payment'
},
contexts: {
operation: {
name: 'processPayment',
userId: user.id // Только ID, не PII
}
}
});
}

Структурированное логирование для production

// ✅ Production-ready logger
interface LogContext {
userId?: string;
requestId?: string;
[key: string]: unknown;
}

class ProductionLogger {
private static sendToService(
level: 'info' | 'warn' | 'error',
message: string,
context?: LogContext
) {
// Отправка в централизованную систему логирования
// (CloudWatch, Datadog, etc.)
if (typeof window !== 'undefined') {
// Клиентское логирование через API
fetch('/api/logs', {
method: 'POST',
body: JSON.stringify({ level, message, context, timestamp: Date.now() })
}).catch(() => {}); // Игнорируем ошибки логирования
}
}

static info(message: string, context?: LogContext) {
if (process.env.NODE_ENV === 'production') {
this.sendToService('info', message, context);
} else {
console.log(`[INFO] ${message}`, context);
}
}

static error(message: string, error?: Error, context?: LogContext) {
const errorContext = {
...context,
errorType: error?.name,
errorMessage: error?.message,
// Stack trace только в development
...(process.env.NODE_ENV === 'development' && { stack: error?.stack })
};

if (process.env.NODE_ENV === 'production') {
this.sendToService('error', message, errorContext);
Sentry.captureException(error);
} else {
console.error(`[ERROR] ${message}`, errorContext);
}
}
}

// Использование
ProductionLogger.info('User logged in', { userId: user.id });
ProductionLogger.error('Payment failed', error, { userId: user.id, amount: 100 });

Частые ошибки

// ❌ Ошибка 1: Логирование объектов запросов целиком
fetch('/api/data').then(response => {
console.log('Response:', response); // Может содержать headers с токенами!
});

// ✅ Правильно: Логировать только безопасные поля
fetch('/api/data').then(response => {
console.log('Response status:', response.status);
console.log('Response OK:', response.ok);
});

// ❌ Ошибка 2: Использование console.log для production логирования
console.log('User action:', data); // Останется в production!

// ✅ Правильно: Использовать logger с условной логикой
Logger.log('User action:', data); // Санитизация + только в dev

// ❌ Ошибка 3: Логирование всего объекта ошибки
catch (error) {
console.error('Error:', error); // Может содержать чувствительные данные!
}

// ✅ Правильно: Логировать только безопасные поля
catch (error) {
Logger.error('Operation failed', error, {
operation: 'paymentProcessing',
userId: user.id
});
}

📌 Ключевые моменты:

  • FORBIDDEN: Логирование токенов, паролей, PII
  • FORBIDDEN: Публичные source maps в production
  • MUST: Санитизация логов перед отправкой
  • MUST: productionBrowserSourceMaps: false в Next.js
  • Source maps только для систем мониторинга (Sentry)