@mwjb/next-rum-monitor
v1.0.5
Published
Comprehensive RUM monitoring for Next.js (SSR, Client, Axios, Build-time)
Downloads
218
Maintainers
Readme
Next RUM Monitor
A comprehensive Real User Monitoring (RUM) solution for Next.js applications.
Features
- Client-side tracking: JS errors, unhandled rejections, fetch/XHR failures, and page load performance.
- Server-side tracking: SSR errors, metadata generation errors, and instrumentation hooks.
- Axios integration: Automatically report HTTP 4xx/5xx errors and slow requests (>500ms by default).
- Build-time tracking: Capture Webpack compilation errors during dev and build.
- API key authentication: Optional
apiKeysent asX-API-Keyon every ingest request (e.g. Laravelapplications.api_key). - Detailed context: Device info, network quality, and arbitrary
extrametadata you define.
Installation
npm install @mwjb/next-rum-monitor
# or
pnpm add @mwjb/next-rum-monitor
# or
yarn add @mwjb/next-rum-monitorSetup
1. Environment variables
Create a .env.local file in your Next.js project and add:
# RUM ingest URL (e.g. Laravel POST /api/rum-events)
NEXT_PUBLIC_RUM_API=https://rum.mwjb.net/api/rum-events
# Application API key (sent as X-API-Key header)
NEXT_PUBLIC_RUM_API_KEY=your-application-api-key| Variable | Required | Description |
| ------------------------- | -------- | ----------------------------------------------------------------- |
| NEXT_PUBLIC_RUM_API | No | RUM endpoint URL. Used on client and server. |
| NEXT_PUBLIC_RUM_API_KEY | Yes* | API key for browser ingest (X-API-Key). |
| RUM_API_KEY | No | Server-only fallback for SSR instrumentation and build reporting. |
Required when your backend enforces API key auth (e.g. Laravel RumEventController).
Note: Browser events are sent from the client, so the key must be available via NEXT_PUBLIC_* and will be included in the client bundle. Treat it as a public ingest key (similar to analytics keys), not a secret server credential.
After adding or changing these variables, restart your Next.js dev server.
2. Central configuration (recommended)
Define shared settings once and reuse them across all integration points:
// src/lib/rum/config.ts
import type {RumConfig} from "@mwjb/next-rum-monitor";
const DEFAULT_RUM_ENDPOINT = "https://rum.mwjb.net/api/rum-events";
export const rumEndpoint =
process.env.NEXT_PUBLIC_RUM_API?.trim() || DEFAULT_RUM_ENDPOINT;
export const rumApiKey =
process.env.NEXT_PUBLIC_RUM_API_KEY?.trim() ||
process.env.RUM_API_KEY?.trim() ||
undefined;
export const rumConfig: RumConfig = {
endpoint: rumEndpoint,
apiKey: rumApiKey,
lowSpeedThreshold: 500,
includeResponse: true,
includeMemory: true,
includeDeviceInfo: true,
includeNetworkInfo: true,
extra: {
// Any JSON-serializable key-value data your backend understands
...JSON.parse(process.env.NEXT_PUBLIC_RUM_EXTRA ?? "{}"),
},
};
export const rumBoundaryReportOptions = {
endpoint: rumEndpoint,
apiKey: rumApiKey,
};// src/lib/rum/index.ts
export {
rumApiKey,
rumConfig,
rumEndpoint,
rumBoundaryReportOptions,
} from "./config";3. Browser initialization
Add this to your root layout.tsx:
import {initRum} from "@mwjb/next-rum-monitor";
import {rumConfig} from "@/lib/rum";
initRum(rumConfig);4. Next.js configuration
Wrap your config in next.config.ts:
import {withRumBuildReporter} from "@mwjb/next-rum-monitor";
import {rumConfig} from "./src/lib/rum/config";
export default withRumBuildReporter(nextConfig, rumConfig);5. Server instrumentation
Create or update src/instrumentation.ts:
import {createInstrumentationHook} from "@mwjb/next-rum-monitor";
import {rumConfig} from "@/lib/rum";
export const onRequestError = createInstrumentationHook(rumConfig);6. Axios interceptors
Apply to your axios instance:
import {applyAxiosRumInterceptors} from "@mwjb/next-rum-monitor";
import {rumConfig} from "@/lib/rum";
applyAxiosRumInterceptors(axiosInstance, rumConfig, () => ({
key_1: value_1,
key_2: value_2,
// Any fields your ingest pipeline expects
}));For per-request dynamic metadata, use the callback context:
applyAxiosRumInterceptors(axiosInstance, rumConfig, ({requestConfig, type, status}) => ({
type,
status,
route: window.location.pathname,
requestId: (requestConfig as any)?.headers?.["x-request-id"],
}));7. Error boundaries (recommended)
React errors caught by error.tsx / global-error.tsx do not always reach window.onerror. Report them explicitly with your own UI (i18n, design system, etc.):
// src/app/error.tsx
"use client";
import {useRumBoundaryError} from "@mwjb/next-rum-monitor/components";
import {rumBoundaryReportOptions} from "@/lib/rum";
export default function Error({
error,
reset,
}: {
error: Error & {digest?: string};
reset: () => void;
}) {
useRumBoundaryError(error, "segment", rumBoundaryReportOptions);
return (
<div>
{/* Your localized / styled fallback */}
<button type="button" onClick={() => reset()}>
Try again
</button>
</div>
);
}// src/app/global-error.tsx
"use client";
import {useRumBoundaryError} from "@mwjb/next-rum-monitor/components";
import {rumBoundaryReportOptions} from "@/lib/rum";
export default function GlobalError({
error,
reset,
}: {
error: Error & {digest?: string};
reset: () => void;
}) {
useRumBoundaryError(error, "global", rumBoundaryReportOptions);
return (
<html>
<body>
<button type="button" onClick={() => reset()}>
Try again
</button>
</body>
</html>
);
}Without React, call reportRumBoundaryError from the main package inside useEffect:
import {reportRumBoundaryError} from "@mwjb/next-rum-monitor";
import {rumBoundaryReportOptions} from "@/lib/rum";
useEffect(() => {
reportRumBoundaryError(error, "segment", rumBoundaryReportOptions);
}, [error]);Optional: @mwjb/next-rum-monitor/components
RumError and RumGlobalError are report-only helpers (no built-in UI). They exist for quick wiring when you pass children:
import {RumError} from "@mwjb/next-rum-monitor/components";
import {rumBoundaryReportOptions} from "@/lib/rum";
export default function Error(props: {error: Error; reset: () => void}) {
return (
<RumError {...props} {...rumBoundaryReportOptions}>
<YourErrorUI error={props.error} onRetry={props.reset} />
</RumError>
);
}Prefer useRumBoundaryError or reportRumBoundaryError for production apps.
Configuration reference
RumConfig
| Option | Type | Default | Description |
| -------------------- | --------- | ------- | ------------------------------------------- |
| endpoint | string | — | RUM ingest URL (required). |
| apiKey | string | — | Sent as X-API-Key on all ingest requests. |
| lowSpeedThreshold | number | 500 | Report requests slower than this (ms). |
| includeResponse | boolean | true | Include HTTP response body in events when allowed by the rules below. |
| includeResponseOnErrorOnly | boolean | false | When true, attach extra.response only for HTTP status >= 400 (axios low-speed, axios http-error, fetch http-error). |
| includeMemory | boolean | true | Include memory usage in events. |
| includeDeviceInfo | boolean | true | Include screen and viewport size. |
| includeNetworkInfo | boolean | true | Include navigator.connection quality. |
| extra | RumExtraData \| (() => RumExtraData) | — | Arbitrary metadata merged into every event's extra object. |
Per-event overrides: sendRumEvent({ type: "custom", extra: { requestId: "abc" } }). Config keys are merged first; later keys win.
extra: {
// Any shape — strings, numbers, nested objects, arrays, etc.
tenant: process.env.NEXT_PUBLIC_TENANT,
tags: ["web", "nextjs"],
},
extra: () => ({
locale: document.documentElement.lang || "en",
path: window.location.pathname,
}),Event payload shape
Every ingest POST uses a minimal, system-agnostic envelope:
| Field | Description |
| --- | --- |
| type | Event category (js-error, http-error, page-load, …) |
| timestamp | Unix ms |
| url | Page URL (client) or request/build URL (server) |
| memory | Optional memory snapshot when enabled |
| extra | All other context — source, message, stack, HTTP details, your config metadata, etc. |
Example:
{
"type": "http-error",
"timestamp": 1710000000000,
"url": "https://app.example/en/dashboard",
"memory": { "usedJSHeapSize": "42MB" },
"extra": {
"source": "axios",
"method": "GET",
"requestUrl": "/api/users",
"status": 500,
"userId": "123"
}
}RumExtraData
Record<string, unknown> — attach any JSON-serializable metadata your ingest backend accepts.
Error boundary helpers
| Export | Package | Description |
| ----------------------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| reportRumBoundaryError(error, kind, options?) | main | Report segment ("segment") or global ("global") boundary errors. Uses sendRumEvent when initRum() ran; otherwise options.endpoint / options.apiKey for a direct ingest POST. |
| useRumBoundaryError(error, kind, options?) | ./components | React hook wrapper around reportRumBoundaryError. |
| RumError / RumGlobalError | ./components | Optional report-only wrappers; pass your UI as children. |
Laravel backend
When using a Laravel ingest endpoint that validates X-API-Key against applications.api_key:
$apiKey = $request->header('X-API-Key');
$application = Application::where('api_key', $apiKey)
->where('is_active', true)
->first();Set apiKey in rumConfig to the same value as the application's api_key column.
Integration checklist
| File | Why? |
| -------------------------- | ------------------------------------------------------------- |
| src/lib/rum/config.ts | Single source of truth for endpoint, apiKey, and options. |
| next.config.ts | Wrap with withRumBuildReporter to catch compilation errors. |
| src/instrumentation.ts | Capture SSR and Server Action errors via onRequestError. |
| src/app/layout.tsx | Call initRum() to bootstrap browser monitoring. |
| src/app/error.tsx | Catch React rendering errors within pages and segments. |
| src/app/global-error.tsx | Safety net for errors in the root layout. |
| src/lib/axios/axios.tsx | Track HTTP 4xx/5xx and latency (if using Axios). |
Docker & CI
NEXT_PUBLIC_RUM_* values are embedded in the client bundle at build time. Treat NEXT_PUBLIC_RUM_API_KEY as a public ingest key (like an analytics key), not a server secret. Use RUM_API_KEY (no NEXT_PUBLIC_ prefix) for server-only reporting when you need a value that never ships to the browser.
Do not commit keys in Dockerfiles, shell scripts, or workflow YAML. Store them in GitHub Actions secrets (or your CI provider's equivalent) and inject them at build/runtime.
1. Add repository secrets
In GitHub: Settings → Secrets and variables → Actions → New repository secret
| Secret | Used for |
| --- | --- |
| NEXT_PUBLIC_RUM_API | RUM ingest URL (client + server) |
| NEXT_PUBLIC_RUM_API_KEY | Browser ingest key (X-API-Key) |
| RUM_API_KEY | Optional server-only fallback (SSR / build reporting) |
2. Dockerfile (build args only — values come from CI)
ARG NEXT_PUBLIC_RUM_API
ARG NEXT_PUBLIC_RUM_API_KEY
ARG RUM_API_KEY
ENV NEXT_PUBLIC_RUM_API=$NEXT_PUBLIC_RUM_API
ENV NEXT_PUBLIC_RUM_API_KEY=$NEXT_PUBLIC_RUM_API_KEY
ENV RUM_API_KEY=$RUM_API_KEYDo not hardcode real URLs or keys in the Dockerfile.
3. GitHub Actions workflow
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
env:
NEXT_PUBLIC_RUM_API: ${{ secrets.NEXT_PUBLIC_RUM_API }}
NEXT_PUBLIC_RUM_API_KEY: ${{ secrets.NEXT_PUBLIC_RUM_API_KEY }}
RUM_API_KEY: ${{ secrets.RUM_API_KEY }}
run: |
docker build \
--build-arg NEXT_PUBLIC_RUM_API="$NEXT_PUBLIC_RUM_API" \
--build-arg NEXT_PUBLIC_RUM_API_KEY="$NEXT_PUBLIC_RUM_API_KEY" \
--build-arg RUM_API_KEY="$RUM_API_KEY" \
-t your-app:${{ github.sha }} .Secrets are masked in workflow logs and never committed to the repo.
Local builds
For local Docker builds, pass values from your shell environment (e.g. .env.local loaded in the terminal — not copied into the image layer history in committed files):
export $(grep -v '^#' .env.local | xargs)
docker build \
--build-arg NEXT_PUBLIC_RUM_API="$NEXT_PUBLIC_RUM_API" \
--build-arg NEXT_PUBLIC_RUM_API_KEY="$NEXT_PUBLIC_RUM_API_KEY" \
--build-arg RUM_API_KEY="$RUM_API_KEY" \
-t your-app .