NextJS
Add Middleware Real User Monitoring (RUM) to a Next.js app to capture page performance, Core Web Vitals, errors and session replays from real users. Load the browser SDK with next/script and initialize it once in your root layout or document.
Install#
Create a Browser application on the RUM installation page in Middleware and select the Next.js tab to see this setup with your application name and token filled in.
When using Next.js, you don’t edit a raw index.html. Load the RUM browser SDK with next/script, then initialise early so views/resources/errors are captured reliably.
Use beforeInteractive for the SDK so it loads before any Next.js code. In the App Router, place it in app/layout.tsx. In the Pages Router, place it in pages/_document.tsx. If you put beforeInteractive elsewhere, Next.js raises “No Before Interactive Script Outside Document”.
App Router#
Define the structure in app/layout.tsx and load the SDK + init there.
// app/layout.tsx
import Script from "next/script";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
{/* 1) Load the RUM SDK as early as possible */}
<Script
id="mw-rum-sdk"
src="https://cdnjs.middleware.io/browser/libs/0.0.2/middleware-rum.min.js"
strategy="beforeInteractive"
crossOrigin="anonymous"
/>
{/* 2) Initialise RUM (inline) */}
<Script
id="mw-rum-init"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: `
if (window.Middleware) {
Middleware.track({
projectName: "my-application",
serviceName: "mw-application",
accountKey: "your-account-token",
target: "https://<UID>.middleware.io",
env: "production",
defaultAttributes: { "app.version": "1.0.0" }
});
}
`,
}}
/>
{/* Optional: consent-gated init (see "Consent" below) */}
</head>
<body>{children}</body>
</html>
);
}Soft navigations (SPA routes). Next.js updates the URL via the History API; the SDK treats these as new views, so route changes are measured like page loads. No extra code is needed if the URL updates normally.
Pages Router#
Use pages/_document.tsx for beforeInteractive (global scripts). If you prefer putting code in _app.tsx, switch strategy to afterInteractive.
// pages/_document.tsx
import Document, { Html, Head, Main, NextScript } from "next/document";
import Script from "next/script";
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
<Script
id="mw-rum-sdk"
src="https://cdnjs.middleware.io/browser/libs/0.0.2/middleware-rum.min.js"
strategy="beforeInteractive"
crossOrigin="anonymous"
/>
<Script
id="mw-rum-init"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: `
if (window.Middleware) {
Middleware.track({
projectName: "my-application",
serviceName: "mw-application",
accountKey: "your-account-token",
target: "https://<UID>.middleware.io",
env: "production",
defaultAttributes: { "app.version": "1.0.0" }
});
}
`,
}}
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}If you initialise in pages/_app.tsx, use strategy="afterInteractive" to avoid the beforeInteractive error and keep behaviour correct.
Privacy quick-start (Session Recording)#
Tighten privacy at init using recordingOptions. These affect Session Recording only; masking happens client-side before data is sent.
// add inside the init block shown above
recording: "1", // default full session capture; set "0" to disable recording
recordingOptions: {
maskAllInputs: true, // mask form values as ***
maskTextSelector: ".pii, [data-pii]",// mask likely-PII text
blockSelector: ".auth-widget, .payment-form" // exclude sensitive regions
}See Session Recording Privacy for the full option matrix (blockClass, ignoreSelector, custom mask functions). (This is additive to your current config.)
Consent (GDPR/CCPA)#
Gate initialisation on your CMP signal. Example: only start RUM if a cookie indicates analytics consent.
// Replace the inline init in App/Pages with this consent-gated version:
dangerouslySetInnerHTML={{
__html: `
(function() {
function hasAnalyticsConsent() {
return document.cookie.includes("consent_analytics=true");
}
if (hasAnalyticsConsent() && window.Middleware) {
Middleware.track({
projectName: "my-application",
serviceName: "mw-application",
accountKey: "your-account-token",
target: "https://<UID>.middleware.io",
env: "production",
defaultAttributes: { "app.version": "1.0.0" }
// recording: "0" // example: disable only the replay if needed
});
}
})();
`,
}}Add User Information (React patterns)#
Update attributes after login or when identity changes. In App Router, use a small Client Component; in Pages Router, a useEffect in _app.
// app/components/ClientAttributes.tsx
"use client";
import { useEffect } from "react";
export default function ClientAttributes(props: { user?: { name?: string; email?: string; role?: string } }) {
useEffect(() => {
if (window.Middleware && props.user) {
window.Middleware.setAttributes({
username: props.user.name,
email: props.user.email,
user_role: props.user.role,
});
}
}, [props.user]);
return null;
}Include <ClientAttributes user={currentUser} /> in your app shell when identity is available.
The SDK remembers these attributes across page reloads, tabs, and new sessions, so a user who stays logged in isn't shown as Anonymous after a period of inactivity. Clear them when the user logs out:
async function handleLogout() {
window.Middleware?.clearAttributes();
// your existing logout code, e.g. signOut()
}To keep attributes for the current page only, set persistUserAttributes: false in track(). See Add User Information for details.
Add Custom Logs#
Use SDK helpers anywhere (components, error boundaries, services):
if (window.Middleware) {
Middleware.error("Your error message");
Middleware.error(new Error("Your error message"));
Middleware.info("info message");
Middleware.warn("warn message");
Middleware.debug("debug message");
}Data appears on the RUM Dashboard within minutes.
Distributed Tracing (RUM ↔ APM)#
Correlate frontend views/resources with backend traces. Ensure a Middleware APM agent is installed. Then add propagation config:
// add inside your init block
tracePropagationTargets: [/localhost:3000/i, /api\.domain\.com/i],
tracePropagationFormat: "b3" // or "w3c" to match your backend APMtracePropagationTargetsare RegExp that match the browser-called hosts.- Some backends require enabling matching propagators (e.g.,
OTEL_PROPAGATORS=b3). This mirrors standard RUM↔APM correlation guidance.
Troubleshooting#
Nothing shows up (SPA):
- Ensure the router updates the URL (App
Router/next/navigationdoes; memory-only routers in tests won’t emit new views).
“No Before Interactive Script Outside Document”:
- Place
beforeInteractivescripts inapp/layout.tsx(App Router) orpages/_document.tsx(Pages Router). Otherwise, change toafterInteractive. (Next.js)
CSP / corporate network blocks:
Allow the SDK host in
script-srcand your ingesttargetinconnect-src:textContent-Security-Policy: default-src 'self'; script-src 'self' https://cdnjs.middleware.io 'unsafe-inline'; connect-src 'self' https://<UID>.middleware.io; img-src 'self' data:; style-src 'self' 'unsafe-inline';
Re-validate CSP after SDK/CDN changes.
Minified stack traces:
- Upload source maps and set
"app.version"indefaultAttributesto match your build; correlate errors to releases on the dashboard.
Script strategy tips:
beforeInteractiveis for critical, global scripts;afterInteractiveloads after some hydration;lazyOnloadduring idle. Pick the lightest strategy that meets your needs.
Need assistance or want to learn more about Middleware? Get in touch with us via our Contact Us or join our Slack channel.
What did you think of this content?