Забезпечення надійності корпоративних веб-платформ на базі 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.Критичним аспектом безпеки корпоративних систем є суворе дотримання регуляторних вимог GDPR, HIPAA та PCI-DSS. За замовчуванням системи відстеження збоїв можуть перехоплювати значення полів введення паролів, заголовки авторизації (Authorization: Bearer ...) та сесійні cookies. Продакшен-конфігурація зобов'язана містити програмне маскування чутливих даних (PII Scrubbing) у хуку beforeSend до надсилання корисного навантаження мережею.
Архітектура розподілу потоків телеметрії між шарами Next.js 15 представлена на схемі:
[ Браузер користувача ]
│
├─► [ Client Errors / Replay / RUM ] ──► (HTTPS) ──► [ Sentry Cloud ]
│
▼ (HTTP-запит)
[ Edge Middleware ] ──► [ Edge Spans / Errors ] ────────────┘
│
▼
[ Кластер Node.js SSR (Next.js 15) ]
│
├─► [ Серверні винятки ] ────────────────────────────► [ Sentry Cloud ]
│
├─► [ Pino JSON Stream (stdout) ] ────► [ Logtail / Vector Collector ]
│
└─► [ prom-client Registry ]
▲
│ (HTTP Pull Scrape кожні 15с)
│
[ Prometheus / VictoriaMetrics ] ──────► [ Grafana Dashboards ]nodejs_heap_size_used_bytes та Event Loop Lag у Prometheus для виявлення витоків пам'яті в серверних кешах SSR.x-request-id між структурованими записами логів Pino в Logtail та подіями помилок у Sentry.Побудова корпоративної системи спостережуваності складається з налаштування Sentry з фільтрацією PII, безпечного завантаження прихованих source maps під час збірки в CI, розгортання реєстру метрик 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',
],
});Для запобігання публічному витоку оригінального вихідного коду, карти 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,
// Приховуємо карти вихідного коду від публічного доступу в браузері
hideSourceMaps: true,
// Видаляємо діагностичні логи Sentry з фінального клієнтського бандла
disableLogger: true,
// Автоматичний пошук та завантаження карт для всіх чанків Next.js
widenClientFileUpload: true,
});Необроблена помилка в Server Action перехоплюється глобальним слухачем Sentry.
Хук beforeSend очищає заголовки авторизації, сесійні cookies та маскує платіжні дані.
SDK неблокуючим фоновим POST-запитом надсилає пакет події на кінцеву точку Sentry Relay.
Sentry зіставляє хеш релізу із завантаженими картами Source Maps та відновлює код TypeScript.
Якщо частота помилок перевищує 10 збоїв за хвилину, PagerDuty/Slack надсилає сповіщення команді.
Нижче наведено продакшен-реєстр метрик на базі prom-client, який збирає стандартні системні показники Node.js, гістограму затримок HTTP-запитів та рівень активної паралельності:
import { Registry, collectDefaultMetrics, Histogram, Counter, Gauge } from 'prom-client';
/**
* Синглтон-реєстр метрик для запобігання дублюванню під час Hot Module Replacement (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 (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. Доступ захищено спільним Bearer-токеном:
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);
}Якщо параметр productionBrowserSourceMaps увімкнено в next.config.mjs, сервер віддаватиме файли .js.map за публічними HTTP-запитами. Будь-хто зможе завантажити повний вихідний код TypeScript з усіма внутрішніми інтерфейсами та коментарями. Завжди налаштовуйте hideSourceMaps: true у конфігурації Sentry та вимикайте публічне роздавання карт.
Додавання динамічних параметрів сутностей (наприклад, userId, orderId або повного URL-шляху /order/4912) до списку labelNames гістограми Prometheus. Кожна унікальна комбінація міток створює новий часовий ряд у пам'яті TSDB. Під навантаженням це призводить до гігабайтів споживання RAM та аварії Prometheus через OOM. Мітки повинні містити лише нормалізовані шаблони: /order/[id].
Клієнтський SDK Sentry автоматично слухає виклики fetch та XMLHttpRequest, фіксуючи тіла запитів у ланцюжку дій. Якщо користувач надсилає форму авторизації, пароль потрапляє до дашборду Sentry у відкритому вигляді. Завжди конфігуруйте хук beforeBreadcrumb та регулярні вирази для повного маскування полів password, token і secret.
Стандартні виклики console.log у Node.js виконують синхронні операції вводу-виводу при виведенні у термінал або буферизований пайп Docker. Під високим трафіком (>1000 RPS) синхронний I/O блокує єдиний потік виконання, збільшуючи час генерації SSR-сторінок до кількох секунд. Використовуйте асинхронний логер Pino.
Налаштування sentry.client, sentry.server та sentry.edge із вбудованими фільтрами beforeSend.
Інтеграція @sentry/nextjs у пайплайн CI/CD з прапорцем hideSourceMaps: true.
Створення синглтона prom-client для обліку системних показників Node.js та затримок HTTP.
Впровадження перевірки Bearer-токена для запобігання несанкціонованому скрапінгу телеметрії.
Перехід на Pino зі збагаченням записів correlation_id та trace_id для наскрізного аудиту.