Browser RUM - NextJS

Install

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.)

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({
        name: 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.

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 APM
  • tracePropagationTargets are 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/navigation does; memory-only routers in tests won’t emit new views).

“No Before Interactive Script Outside Document”:

  • Place beforeInteractive scripts in app/layout.tsx (App Router) or pages/_document.tsx (Pages Router). Otherwise, change to afterInteractive. (Next.js)

CSP / corporate network blocks:

  • Allow the SDK host in script-src and your ingest target in connect-src:
    Content-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" in defaultAttributes to match your build; correlate errors to releases on the dashboard.

Script strategy tips:

  • beforeInteractive is for critical, global scripts; afterInteractive loads after some hydration; lazyOnload during idle. Pick the lightest strategy that meets your needs.

Need assistance or want to learn more about Middleware? Contact our support team at [email protected] or join our Slack channel.