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.

1// app/layout.tsx
2import Script from "next/script";
3
4export default function RootLayout({ children }: { children: React.ReactNode }) {
5  return (
6    <html lang="en">
7      <head>
8        {/* 1) Load the RUM SDK as early as possible */}
9        <Script
10          id="mw-rum-sdk"
11          src="https://cdnjs.middleware.io/browser/libs/0.0.2/middleware-rum.min.js"
12          strategy="beforeInteractive"
13          crossOrigin="anonymous"
14        />
15        {/* 2) Initialise RUM (inline) */}
16        <Script
17          id="mw-rum-init"
18          strategy="beforeInteractive"
19          dangerouslySetInnerHTML={{
20            __html: `
21              if (window.Middleware) {
22                Middleware.track({
23                  projectName: "my-application",
24                  serviceName: "mw-application",
25                  accountKey: "your-account-token",
26                  target: "https://<UID>.middleware.io",
27                  env: "production",
28                  defaultAttributes: { "app.version": "1.0.0" }
29                });
30              }
31            `,
32          }}
33        />
34        {/* Optional: consent-gated init (see "Consent" below) */}
35      </head>
36      <body>{children}</body>
37    </html>
38  );
39}

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.

1// pages/_document.tsx
2import Document, { Html, Head, Main, NextScript } from "next/document";
3import Script from "next/script";
4
5export default class MyDocument extends Document {
6  render() {
7    return (
8      <Html lang="en">
9        <Head>
10          <Script
11            id="mw-rum-sdk"
12            src="https://cdnjs.middleware.io/browser/libs/0.0.2/middleware-rum.min.js"
13            strategy="beforeInteractive"
14            crossOrigin="anonymous"
15          />
16          <Script
17            id="mw-rum-init"
18            strategy="beforeInteractive"
19            dangerouslySetInnerHTML={{
20              __html: `
21                if (window.Middleware) {
22                  Middleware.track({
23                    projectName: "my-application",
24                    serviceName: "mw-application",
25                    accountKey: "your-account-token",
26                    target: "https://<UID>.middleware.io",
27                    env: "production",
28                    defaultAttributes: { "app.version": "1.0.0" }
29                  });
30                }
31              `,
32            }}
33          />
34        </Head>
35        <body>
36          <Main />
37          <NextScript />
38        </body>
39      </Html>
40    );
41  }
42}

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.

1// add inside the init block shown above
2recording: "1", // default full session capture; set "0" to disable recording
3recordingOptions: {
4  maskAllInputs: true,                 // mask form values as ***
5  maskTextSelector: ".pii, [data-pii]",// mask likely-PII text
6  blockSelector: ".auth-widget, .payment-form" // exclude sensitive regions
7}

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.

1// Replace the inline init in App/Pages with this consent-gated version:
2dangerouslySetInnerHTML={{
3  __html: `
4    (function() {
5      function hasAnalyticsConsent() {
6        return document.cookie.includes("consent_analytics=true");
7      }
8      if (hasAnalyticsConsent() && window.Middleware) {
9        Middleware.track({
10          projectName: "my-application",
11          serviceName: "mw-application",
12          accountKey: "your-account-token",
13          target: "https://<UID>.middleware.io",
14          env: "production",
15          defaultAttributes: { "app.version": "1.0.0" }
16          // recording: "0" // example: disable only the replay if needed
17        });
18      }
19    })();
20  `,
21}}

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.

1// app/components/ClientAttributes.tsx
2"use client";
3import { useEffect } from "react";
4
5export default function ClientAttributes(props: { user?: { name?: string; email?: string; role?: string } }) {
6  useEffect(() => {
7    if (window.Middleware && props.user) {
8      window.Middleware.setAttributes({
9        name: props.user.name,
10        email: props.user.email,
11        user_role: props.user.role,
12      });
13    }
14  }, [props.user]);
15  return null;
16}

Include <ClientAttributes user={currentUser} /> in your app shell when identity is available.

Add Custom Logs

Use SDK helpers anywhere (components, error boundaries, services):

1if (window.Middleware) {
2  Middleware.error("Your error message");
3  Middleware.error(new Error("Your error message"));
4  Middleware.info("info message");
5  Middleware.warn("warn message");
6  Middleware.debug("debug message");
7}

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:

1// add inside your init block
2tracePropagationTargets: [/localhost:3000/i, /api\.domain\.com/i],
3tracePropagationFormat: "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:
    1Content-Security-Policy:
    2default-src 'self';
    3script-src 'self' https://cdnjs.middleware.io 'unsafe-inline';
    4connect-src 'self' https://<UID>.middleware.io;
    5img-src 'self' data:;
    6style-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.