Ensuring enterprise reliability for Next.js 15 applications requires segregating observability into four complementary telemetry pillars:
prom-client.In Next.js 15, integrating the @sentry/nextjs SDK is complicated by three isolated execution runtimes. To prevent runtime crashes and bundle contamination, initialization is strictly separated across three dedicated files:
sentry.client.config.ts: Evaluated in the browser; captures client-side exceptions, records Session Replays, and measures web vitals.sentry.server.config.ts: Evaluated in the Node.js runtime; instruments Server Components (RSC), Server Actions, and Route Handlers.sentry.edge.config.ts: Evaluated in the lightweight Edge Runtime; instruments edge middleware (middleware.ts).A vital security mandate in enterprise platforms is strict adherence to regulatory standards such as GDPR, HIPAA, and PCI-DSS. By default, crash reporters may capture raw input fields, authentication headers (Authorization: Bearer ...), and session cookies. Production configurations must enforce algorithmic data scrubbing (PII redaction) inside the beforeSend lifecycle hook before payloads egress over the network.
The telemetry ingestion topology across Next.js 15 layers is structured as follows:
[ End-User Browser ]
│
├─► [ Client Errors / Replay / RUM ] ──► (HTTPS) ──► [ Sentry Cloud ]
│
▼ (HTTP Request)
[ 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 every 15s)
│
[ Prometheus / VictoriaMetrics ] ──────► [ Grafana Dashboards ]nodejs_heap_size_used_bytes and Event Loop Lag metrics in Prometheus to detect memory leaks in server-side cache abstractions.x-request-id header across structured Pino JSON log entries in Logtail and Sentry error events.Building a hardened production observability pipeline requires a multi-runtime Sentry configuration with PII scrubbing, CI source maps upload with browser hiding, a Prometheus prom-client registry exposed via a secured /api/metrics route handler, and structured Pino logging.
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,
// Performance tracing sample rate (20% in production)
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.2 : 1.0,
// Scrub sensitive PII before payload leaves the server
beforeSend(event, hint) {
// 1. Strip sensitive request headers
if (event.request?.headers) {
delete event.request.headers['authorization'];
delete event.request.headers['cookie'];
delete event.request.headers['x-api-key'];
}
// 2. Anonymize user identification
if (event.user) {
// Retain only internal pseudonymous identifier for error grouping
event.user = { id: event.user.id };
}
// 3. Mask credit card numbers and passwords from error messages
if (event.message) {
event.message = event.message.replace(/\b(?:\d[ -]*?){13,16}\b/g, '[REDACTED_CARD]');
}
return event;
},
// Ignore benign framework redirects and aborted connections
ignoreErrors: [
'NEXT_NOT_FOUND',
'NEXT_REDIRECT',
'ECONNRESET',
'ClientAbortException',
],
});To prevent public exposure of proprietary source code in production, source maps must be uploaded directly to Sentry during CI/CD and suppressed from browser serving:
import { withSentryConfig } from '@sentry/nextjs';
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
productionBrowserSourceMaps: false, // Prevent public serving of .map files
};
export default withSentryConfig(nextConfig, {
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
silent: !process.env.CI,
// Strip source maps from public browser bundles
hideSourceMaps: true,
// Remove verbose Sentry logs from production client bundle
disableLogger: true,
// Automatically discover and upload source maps for all Next.js chunks
widenClientFileUpload: true,
});Uncaught exception in a Server Action is intercepted by the global Sentry runtime listener.
The beforeSend hook purges authorization headers, session cookies, and credit card numbers.
SDK queues the envelope and transmits it non-blockingly via HTTP POST to the Sentry intake endpoint.
Sentry maps the release SHA against privately uploaded source maps to reconstruct TypeScript source files.
If error rate crosses the alert rule threshold (10 errors/min), PagerDuty/Slack notifies on-call engineers.
Below is a production-grade metrics registry implemented with prom-client that collects standard Node.js runtime metrics and tracks HTTP request latencies and active concurrency:
import { Registry, collectDefaultMetrics, Histogram, Counter, Gauge } from 'prom-client';
/**
* Singleton metrics registry avoiding registration duplication during Next.js hot module reloading (HMR)
*/
class PrometheusMetricsRegistry {
private static instance: PrometheusMetricsRegistry;
public readonly registry: Registry;
// 1. Histogram for measuring HTTP request durations in seconds
public readonly httpRequestDuration: Histogram<string>;
// 2. Counter for total inbound HTTP requests segmented by status code
public readonly httpRequestsTotal: Counter<string>;
// 3. Gauge for currently active in-flight HTTP requests
public readonly httpActiveRequests: Gauge<string>;
private constructor() {
this.registry = new Registry();
// Collect default Node.js runtime metrics (Memory RSS/Heap, GC duration, 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 request duration in seconds',
labelNames: ['method', 'route', 'status_code'],
// Exponential buckets tailored for p95/p99 web interface latency analysis
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: 'Total count of inbound HTTP requests',
labelNames: ['method', 'route', 'status_code'],
registers: [this.registry],
});
this.httpActiveRequests = new Gauge({
name: 'nextjs_http_active_requests',
help: 'Number of currently active in-flight requests',
registers: [this.registry],
});
}
public static getInstance(): PrometheusMetricsRegistry {
if (!PrometheusMetricsRegistry.instance) {
PrometheusMetricsRegistry.instance = new PrometheusMetricsRegistry();
}
return PrometheusMetricsRegistry.instance;
}
}
export const metrics = PrometheusMetricsRegistry.getInstance();The /api/metrics endpoint provides metrics to Prometheus scrapers in the OpenMetrics format. Access is secured using a shared Bearer secret header:
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) {
// Prevent unauthorized access to internal infrastructure metrics
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,
});
/**
* Structured logger helper binding trace context to every log line
*/
export function logWithContext(
level: 'info' | 'warn' | 'error',
message: string,
contextData: { requestId: string; traceId?: string; [key: string]: unknown }
) {
logger[level](contextData, message);
}Enabling productionBrowserSourceMaps: true in next.config.mjs configures the server to serve .js.map files over HTTP. Anyone can download these files and inspect original TypeScript sources, comments, and internal logic. Always configure hideSourceMaps: true within withSentryConfig and disable public browser source map emission.
Adding dynamic entity parameters (such as userId, orderId, or raw URL paths /order/a4b8-9f12) into Prometheus labelNames creates an unbounded number of distinct time series in memory. Under traffic, the TSDB consumes gigabytes of RAM and crashes with out-of-memory errors. Labels must always use normalized route patterns, such as /order/[id].
The client Sentry SDK instruments fetch and XMLHttpRequest calls automatically, capturing request bodies into breadcrumbs. If a login form submits credentials, the raw password can end up in Sentry's dashboard. Always configure beforeBreadcrumb hooks and regex masking rules to redact fields named password, token, or secret.
Using standard console.log in Node.js performs synchronous I/O when writing to terminal stdout or buffered Docker pipes. Under heavy concurrency (>1000 RPS), synchronous logging stalls the event loop and spikes SSR response times by several seconds. Always deploy high-performance asynchronous loggers like Pino.
Configure sentry.client, sentry.server, and sentry.edge independently with beforeSend PII filters.
Integrate @sentry/nextjs in CI/CD with hideSourceMaps: true to symbolicate stack traces securely.
Instantiate singleton prom-client registry collecting default Node.js and HTTP latency metrics.
Implement Bearer token verification to protect internal telemetry from unauthorized scraping.
Transition from console.log to Pino, enriching JSON output with correlation IDs and trace IDs.