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        username: 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? Get in touch with us via our Contact Us or join our Slack channel.