Обеспечение надежности корпоративных веб-платформ на базе Next.js 15 требует разделения наблюдаемости (Observability) на четыре взаимодополняющих слоя:
prom-client.В Next.js 15 интеграция SDK @sentry/nextjs осложняется наличием трех изолированных сред выполнения. Для предотвращения падений рантайма инициализация разделяется на три независимых файла:
sentry.client.config.ts: исполняется в браузере; активирует захват клиентских ошибок, запись пользовательских сессий (Session Replay) и замеры производительности.sentry.server.config.ts: исполняется в пуле Node.js; инструментирует обработку Server Components (RSC), Server Actions и Route Handlers.sentry.edge.config.ts: исполняется в легковесном Edge Runtime; инструментирует пограничный слой middleware.ts.Критическим аспектом безопасности в Enterprise-системах является строгое соответствие стандартам GDPR, HIPAA и PCI-DSS. По умолчанию трекеры ошибок могут захватывать содержимое полей ввода паролей, заголовки авторизации (Authorization: Bearer ...) и сессионные cookies. Конфигурация обязана включать программные фильтры десенсибилизации (Data Scrubbing / PII Masking) на хуке beforeSend до отправки полезной нагрузки по сети.
Архитектура распределения телеметрических потоков Next.js 15 представлена на схеме:
[ Пользовательский браузер ]
│
├─► [ Client Errors / Replay / RUM ] ──► (HTTPS) ──► [ Sentry Cloud ]
│
▼ (HTTP-запрос)
[ Edge Middleware ] ──► [ Edge Spans / Errors ] ────────────┘
│
▼
[ Node.js SSR Cluster (Next.js 15) ]
│
├─► [ Server Exceptions ] ───────────────────────────► [ Sentry Cloud ]
│
├─► [ Pino JSON Stream (stdout) ] ────► [ Logtail / Vector Collector ]
│
└─► [ prom-client Registry ]
▲
│ (HTTP Pull Scrape каждые 15с)
│
[ Prometheus / VictoriaMetrics ] ──────► [ Grafana Dashboards ]Раздельная настройка sentry.client, sentry.server и sentry.edge с фильтром beforeSend
Интеграция @sentry/nextjs в GitHub Actions пайплайн с опцией hideSourceMaps: true
Создание Registry с дефолтными метриками Node.js и гистограммой задержек HTTP
Настройка Route Handler с проверкой Bearer-токена для скрапера Prometheus Operator
Переход на Pino с автоматическим обогащением записей correlation_id и trace_id
Почему в Next.js 15 инициализацию Sentry обязательно разделять на файлы sentry.client.config.ts, sentry.server.config.ts и sentry.edge.config.ts?
nodejs_heap_size_used_bytes и Event Loop Lag в Prometheus для обнаружения утечек памяти в глобальных кэшах SSR-компонентов.x-request-id между структурированными логами Pino в Logtail и событиями в Sentry.Для построения отказоустойчивого контура реализуется трехуровневая конфигурация Sentry с маскированием данных, интеграция со сборщиком метрик prom-client через защищенный эндпоинт /api/metrics и высокоскоростное логирование Pino.
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NODE_ENV,
release: process.env.NEXT_PUBLIC_APP_VERSION,
// Доля сэмплирования транзакций производительности (20% на проде)
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.2 : 1.0,
// Санитизация персональных данных перед передачей в Sentry
beforeSend(event, hint) {
// 1. Очистка чувствительных заголовков запроса
if (event.request?.headers) {
delete event.request.headers['authorization'];
delete event.request.headers['cookie'];
delete event.request.headers['x-api-key'];
}
// 2. Маскирование пользовательских данных
if (event.user) {
// Сохраняем только обезличенный внутренний ID для группировки
event.user = { id: event.user.id };
}
// 3. Вырезка номеров банковских карт и паролей из текста ошибки
if (event.message) {
event.message = event.message.replace(/\b(?:\d[ -]*?){13,16}\b/g, '[REDACTED_CARD]');
}
return event;
},
// Игнорирование ожидаемых сетевых прерываний
ignoreErrors: [
'NEXT_NOT_FOUND',
'NEXT_REDIRECT',
'ECONNRESET',
'ClientAbortException',
],
});Для предотвращения публичной утечки исходного кода в production, исходные карты Source Maps загружаются в Sentry при сборке в CI/CD, но скрываются от конечных пользователей:
import { withSentryConfig } from '@sentry/nextjs';
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
productionBrowserSourceMaps: false, // Запрещаем публичную раздачу .map файлов
};
export default withSentryConfig(nextConfig, {
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
silent: !process.env.CI,
// Скрываем sourcemaps от публичного доступа в браузере
hideSourceMaps: true,
// Удаляем отладочные логи Sentry из итогового клиентского бандла
disableLogger: true,
// Автоматический поиск и загрузка карт всех чанков Next.js
widenClientFileUpload: true,
});Необработанная ошибка в Server Action перехватывается глобальным обработчиком Sentry.
Хук beforeSend удаляет заголовки авторизации, cookies, токены и маскирует номера карт.
SDK пакетирует событие и выполняет неблокирующий фоновый POST в Sentry Relay эндпоинт.
Sentry сопоставляет хеш релиза с загруженными в CI скрытыми картами и восстанавливает TS-код.
Если регрессия превышает порог 10 сбоев в минуту, PagerDuty/Slack уведомляет дежурного инженера.
import { Registry, collectDefaultMetrics, Histogram, Counter, Gauge } from 'prom-client';
/**
* Изолированный реестр метрик для предотвращения дублирования при HMR в режиме разработки
*/
class PrometheusMetricsRegistry {
private static instance: PrometheusMetricsRegistry;
public readonly registry: Registry;
// 1. Гистограмма длительности обработки HTTP-запросов
public readonly httpRequestDuration: Histogram<string>;
// 2. Счетчик суммарного количества HTTP-запросов с группировкой по кодам
public readonly httpRequestsTotal: Counter<string>;
// 3. Измеритель текущих активных входящих запросов
public readonly httpActiveRequests: Gauge<string>;
private constructor() {
this.registry = new Registry();
// Сбор стандартных метрик Node.js рантайма (Memory RSS/Heap, GC, Event Loop Lag, CPU)
collectDefaultMetrics({
register: this.registry,
prefix: 'nodejs_nextjs_',
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2],
});
this.httpRequestDuration = new Histogram({
name: 'nextjs_http_request_duration_seconds',
help: 'Длительность выполнения HTTP запросов в секундах',
labelNames: ['method', 'route', 'status_code'],
// Экспоненциальные бакеты для точного вычисления p95/p99 задержек веб-интерфейса
buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
registers: [this.registry],
});
this.httpRequestsTotal = new Counter({
name: 'nextjs_http_requests_total',
help: 'Общее количество входящих HTTP запросов',
labelNames: ['method', 'route', 'status_code'],
registers: [this.registry],
});
this.httpActiveRequests = new Gauge({
name: 'nextjs_http_active_requests',
help: 'Количество одновременно обрабатываемых запросов в текущий момент',
registers: [this.registry],
});
}
public static getInstance(): PrometheusMetricsRegistry {
if (!PrometheusMetricsRegistry.instance) {
PrometheusMetricsRegistry.instance = new PrometheusMetricsRegistry();
}
return PrometheusMetricsRegistry.instance;
}
}
export const metrics = PrometheusMetricsRegistry.getInstance();Эндпоинт /api/metrics предоставляет Prometheus-серверу данные в формате OpenMetrics. Доступ ограничивается токеном авторизации заголовка Authorization: Bearer <METRICS_SCRAPE_SECRET>:
import { NextResponse } from 'next/server';
import { metrics } from '@/lib/observability/prometheus-metrics';
export const dynamic = 'force-dynamic';
export const runtime = 'nodejs';
export async function GET(request: Request) {
// Защита от несанкционированного доступа к внутренней телеметрии
const authHeader = request.headers.get('authorization');
const expectedSecret = process.env.METRICS_SCRAPE_SECRET;
if (expectedSecret && authHeader !== `Bearer ${expectedSecret}`) {
return new NextResponse('Unauthorized', { status: 401 });
}
try {
const rawMetrics = await metrics.registry.metrics();
return new NextResponse(rawMetrics, {
status: 200,
headers: {
'Content-Type': metrics.registry.contentType,
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
},
});
} catch (err) {
return new NextResponse('Internal Metric Generation Error', { status: 500 });
}
}import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: {
level: (label) => ({ level: label }),
},
base: {
service: 'nextjs-web',
environment: process.env.NODE_ENV,
version: process.env.NEXT_PUBLIC_APP_VERSION,
},
timestamp: pino.stdTimeFunctions.isoTime,
});
/**
* Логирование с обязательным контекстом распределенного запроса
*/
export function logWithContext(
level: 'info' | 'warn' | 'error',
message: string,
contextData: { requestId: string; traceId?: string; [key: string]: unknown }
) {
logger[level](contextData, message);
}Какая критическая проблема возникает при добавлении динамических параметров пользователя (userId, email) в метки (labels) гистограмм Prometheus?
Если параметр productionBrowserSourceMaps включен в next.config.mjs, сервер Next.js начинает отдавать файлы .js.map по HTTP любому клиенту. Злоумышленник получает полный оригинальный исходный код TypeScript со всеми внутренними интерфейсами и комментариями. Задавайте hideSourceMaps: true в плагине Sentry и никогда не выкладывайте карты в открытый доступ на CDN.
Добавление динамических идентификаторов сущностей (например, userId, cartId или полного пути URL /product/1284931) в параметры labelNames гистограммы Prometheus. Каждая уникальная комбинация меток создает новый временной ряд в оперативной памяти TSDB. Под нагрузкой это приводит к гигабайтам расхода RAM и падению Prometheus по OOM. В качестве меток допустимы только нормализованные шаблоны путей: /product/[id].
Клиентский SDK Sentry по умолчанию слушает сетевые запросы fetch и XMLHttpRequest, сохраняя тело запросов и URL в цепочке хлебных крошек. Если пользователь отправляет форму логина, пароль может попасть в тело лога Sentry. Всегда конфигурируйте хук beforeBreadcrumb и регулярные выражения фильтрации для полного маскирования полей password, token, secret.
Вызовы стандартного console.log в Node.js являются синхронными при выводе в стандартный поток вывода терминала или буферизованного пайпа Docker. При высокой частоте запросов (более 1000 RPS) синхронный I/O полностью парализует Event Loop Node.js, искусственно завышая задержку обработки SSR до нескольких секунд. Используйте оптимизированный асинхронный логгер Pino.