Next.js integration

Next.js analytics, error tracking and uptime

BugOtter works with any Next.js app. One script tag in app/layout.tsx gives you cookieless analytics and browser errors. @sentry/nextjs, pointed at your BugOtter DSN, catches server errors. Uptime checks and a status page start when you add the URL.

  • 1 taganalytics and browser errors
  • @sentry/nextjsserver errors, no Sentry account
  • 3 regionsuptime checks

What BugOtter gives a Next.js app

  • Cookieless analytics

    One tag in the root layout. App Router navigation counts on its own, with Web Vitals per page.
  • Errors from both sides

    The script catches browser errors. @sentry/nextjs sends server errors to your BugOtter DSN.
  • Uptime and deploys

    Checks from three regions, a status page, and every issue tied to the deploy it started after.

The fast way: one command

Add your app in BugOtter (paste the URL, or import it from Vercel), then run this in the project:

terminal
npx bugotter init --token bo_…   # token from Agent access in BugOtter

It finds app/layout.tsx (or pages/_document.tsx) and adds the script tag. It writes BUGOTTER_DSN, BUGOTTER_PROJECT and BUGOTTER_KEY to .env.local. Add the same three to your host's production environment.

The rest of this page is the same setup by hand, plus what init leaves to you:

  1. Script in the root layout

    One tag in app/layout.tsx: analytics, goals and browser errors. Details
  2. Sentry SDK on the server

    Pointed at your BugOtter DSN. Details
    Terminal: npm install @sentry/nextjs
  3. Upload source maps

    Readable stacks, maps kept private. Details
    After next build: npx bugotter sourcemaps .next --delete
  4. Uptime and deploys

    A health route, deploy markers, a status page. Details
    Last deploy step: npx bugotter deploy

Analytics: the script tag in your root layout

  1. Add the script to app/layout.tsx

    Put it in <head> of the root layout. It loads after the page, sets no cookies, and needs no consent banner. Your app id is on the app's page in BugOtter.

    app/layout.tsx
    // app/layout.tsx
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="en">
          <head>
            <script
              defer
              src="https://bugotter.com/bo.js"
              data-app="YOUR_APP_ID"
              data-release={process.env.VERCEL_GIT_COMMIT_SHA?.slice(0, 12)}
            />
          </head>
          <body>{children}</body>
        </html>
      );
    }

    data-release is optional. It tags browser errors with the commit, so they match the source maps you upload for that build. On Vercel the variable is set for you.

  2. Route changes count on their own

    The App Router moves between pages with history.pushState. The script listens for it and for the back button, so each new path is a page view, with time on page, scroll depth and Web Vitals for each. Keep your signed-in pages out with data-exclude="/dashboard,/settings".

  3. Track goals from client components

    Call window.bo?.track() from any event handler. The ?. matters: the script loads with defer, so it may not be there on the very first render. Form submissions, file downloads and outbound links are counted without code.

    app/pricing/upgrade-button.tsx
    "use client";
    // app/pricing/upgrade-button.tsx
    export function UpgradeButton() {
      return (
        <button onClick={() => window.bo?.track("upgrade_click", { plan: "pro" })}>
          Upgrade
        </button>
      );
    }

    Using TypeScript? Declare window.bo once, so track() type-checks:

    types/bo.d.ts
    declare global {
      interface Window {
        bo?: {
          track(name: string, props?: Record<string, string | number | boolean>): void;
          visitorId(): string | null;
        };
      }
    }
    export {};
BugOtter visitors chart for one site: daily bars, the previous period as a dashed line, and markers for big payments and an error spike
  1. Payments on the chart. A green marker shows the day a big payment came in.
  2. Error spikes, too. A red marker lines up a bad day with the error behind it.
  3. Last period, dashed, so a drop or a launch spike is obvious at a glance.
  4. Bots kept out of every number, with the count one click away.
One site's visitors in the live demo, with payments, error spikes and the last period on the same chart.

Get past ad blockers with Next.js rewrites

Serve the script from your own domain and ad blockers can't tell it apart from your app.

20–40%

of visitors are dropped by ad blockers for most analytics

Three rewrites in next.config.ts bring them back. Only these three paths reach BugOtter.
next.config.ts
// next.config.ts
import type { NextConfig } from "next";

const config: NextConfig = {
  async rewrites() {
    return [
      { source: "/_bo/bo.js", destination: "https://bugotter.com/bo.js" },
      { source: "/_bo/api/collect", destination: "https://bugotter.com/api/collect?via=YOUR_PROXY_KEY" },
      { source: "/_bo/api/errors", destination: "https://bugotter.com/api/errors?via=YOUR_PROXY_KEY" },
    ];
  },
};
export default config;

// then in app/layout.tsx: <script defer src="/_bo/bo.js" data-app="YOUR_APP_ID" />

Next.js rewrites can point at an external URL and keep the query string in the destination, so the three rules above are all you need. They work on any host that runs next start, not only Vercel.

One catch: proxy.ts (called middleware.ts before Next.js 16) runs before rewrites. If yours redirects signed-out visitors, leave /_bo/ out of its matcher, or the beacon gets redirected to your login page.

proxy.ts
// proxy.ts (Next.js 16; middleware.ts before that)
export const config = {
  matcher: ["/((?!_next/static|_next/image|_bo/|favicon.ico).*)"],
};

Check it once: curl -sI https://yourapp.com/_bo/bo.js should answer 200. Your proxy key is on the app's Traffic tab in BugOtter. Keep the ?via= part: it proves visits came through your proxy, and it only lives in server config, never in the page.

Errors: @sentry/nextjs on the server, pointed at BugOtter

BugOtter accepts the Sentry SDK's format. You don't need a Sentry account. You only change where the SDK sends.

  1. Install the SDK

    terminal
    npm install @sentry/nextjs
  2. Init it in instrumentation.ts

    Next.js runs register() once when the server starts, for both the Node.js and Edge runtimes. onRequestError reports errors thrown while rendering Server Components and running route handlers and server actions.

    instrumentation.ts
    // instrumentation.ts (next to app/, or in src/)
    import * as Sentry from "@sentry/nextjs";
    
    export function register() {
      Sentry.init({
        dsn: process.env.BUGOTTER_DSN,
        release: process.env.VERCEL_GIT_COMMIT_SHA,
        dataCollection: { userInfo: false, cookies: false, httpBodies: [], databaseQueryData: false },
      });
    }
    
    // Errors thrown in Server Components, route handlers and server actions
    export const onRequestError = Sentry.captureRequestError;

    The dataCollection line is for SDK v11, which collects headers, cookies and bodies unless told not to. BugOtter scrubs emails, keys and tokens before storage anyway.

    On v10 or older, use sendDefaultPii: false instead. Leave tracesSampleRate out: BugOtter doesn't store traces.

  3. Skip the browser SDK

    The script tag already catches browser errors with page and request breadcrumbs. No instrumentation-client.ts, no wizard, no withSentryConfig. If you already have those, keep them, swap the DSN, and set sourcemaps: { disable: true } so the build doesn't try to upload to Sentry.

Kept

  • Exceptions and captureMessage events, grouped into issues
  • Release, environment, the request path and runtime
  • Navigation and failed-request breadcrumbs
  • Your user id, hashed (never stored raw)

Accepted, then dropped

  • Performance traces and spans
  • Session replays and profiles
  • Sessions, logs and metrics
  • Console breadcrumbs and click text
A BugOtter error page: a ChunkLoadError on /pricing with a plain summary, Fix with agent, events and users, 14-day bars and the source-mapped line
  1. A plain summary written from the evidence: page, browser, release and visits.
  2. Fix with agent hands the whole issue to Claude Code, Cursor or Codex.
  3. Where your code broke, mapped back to your file and line with your source maps.
A Next.js ChunkLoadError in the live demo: what broke, who it hit, and the line in your code.

Readable stack traces from .next

Turn on browser source maps, then upload them right after the build. --delete removes the .map files from .next before deploy, so your source isn't public.

next.config.ts and package.json
// next.config.ts
const config: NextConfig = {
  productionBrowserSourceMaps: true,
};

// package.json
"scripts": {
  "build": "next build && npx bugotter sourcemaps .next --delete"
}
  • Keys: the CLI reads BUGOTTER_PROJECT and BUGOTTER_KEY (the upload key, a secret: build and CI only).
  • Paths: it maps .next/static to /_next/static, the way the browser loads it.
  • Release: it tags the upload with the commit (the first 12 characters of VERCEL_GIT_COMMIT_SHA, or git rev-parse elsewhere). That's the same value as data-release above.

More in the source maps guide.

Uptime, a status page and deploys

3 regions

check your app from the moment you add the URL

An outage only counts when two of them agree, so one flaky network doesn't wake you.

Add one more check on a route that touches your database, so you hear about a dead connection pool, not just a dead server:

app/api/health/route.ts
// app/api/health/route.ts
import { db } from "@/lib/db";
import { sql } from "drizzle-orm";

export const dynamic = "force-dynamic";

export async function GET() {
  await db.execute(sql`select 1`); // touch the database, not just the server
  return Response.json({ ok: true });
}

In BugOtter, add a check for /api/health that expects the JSON value ok to be true. Cron jobs get a heartbeat URL to call when they finish, so a late or failed run alerts you.

Deploys: on Vercel, connect your account once and every production deploy is tracked by itself. Elsewhere, add npx bugotter deploy as the last deploy step. Issues then say “started 4 min after deploy c0ffee1”.

Status page: turn it on and it lives at bugotter.com/status/your-app, showing uptime and incidents only. Pro can serve it from your own domain.

BugOtter uptime for one app: 30 days of daily bars with two amber days, a response time chart, and the latest check from US East, Europe and US West
  1. Every day at a glance. Amber means a short outage that day.
  2. Response time, median and p95, and the slowest region.
  3. Three regions check your URL. An outage only counts when two agree.
Thirty days of uptime in the live demo, with response times and the latest check from each region.

Or ask your coding agent

Connect BugOtter to Claude Code, Cursor or Codex once over MCP. It signs in through your browser, read-only unless you allow changes. Then paste one line.

Claude Code, once
claude mcp add --transport http --scope user bugotter https://bugotter.com/api/mcp

Then ask, in plain words:

  • Add BugOtter to this Next.js app: the script tag in app/layout.tsx, @sentry/nextjs on the server only with my BugOtter DSN, and source map upload after next build.

  • Using BugOtter, list the new errors since the last deploy of this app and fix the top one.

  • Add the BugOtter ad-blocker rewrites to next.config.ts and make sure proxy.ts doesn't match /_bo/.

Cursor, VS Code, Codex and the rest have their own one-step setup at Docs: your agent (MCP). Every setup screen in BugOtter also has a Copy prompt for your agent button with your keys filled in.

Questions

Does BugOtter count client-side navigation in the App Router?

Yes. Next.js changes pages with history.pushState, and the BugOtter script listens for that and for the back button, so every new path is a page view. Only the path counts: a change of query string alone, or router.replace() to a new path, isn't a new page view.

Do I need next/script?

No. A plain script tag with defer in the root layout's head is enough, because the root layout never re-mounts, so the script runs once per page load. next/script with the default afterInteractive strategy also works and passes data-app through. Use one or the other, not both.

Should I install the Sentry browser SDK too?

No. The BugOtter script already catches uncaught browser errors and failed promises, with breadcrumbs. Adding the Sentry browser SDK would report the same errors twice. Use @sentry/nextjs on the server only.

Do I need withSentryConfig or the Sentry wizard?

No. withSentryConfig exists to upload source maps to Sentry and inject the browser SDK, and the wizard sets up both. If you already use them, keep your setup, change the DSN to your BugOtter one and set sourcemaps: { disable: true } in withSentryConfig.

Are server stack traces source-mapped?

Browser stacks are, from the maps you upload. Server stacks are shown as the SDK sends them: the file and line Node reports from the built server code. The CLI only uploads the browser maps in .next/static.

Does it work with the Pages Router?

Yes. Put the same script tag inside <Head> in pages/_document.tsx. npx bugotter init finds either file and adds it for you.

Sources

Checked Nov 4, 2026 against the Next.js docs for rewrites, proxy.ts, next/script and productionBrowserSourceMaps, and Sentry's Next.js manual setup and v11 migration notes. BugOtter isn't affiliated with Sentry or Vercel.

Watch every app you ship, from one calm list.

14 days of everything, no card needed. Plans from $10 a month, billed yearly.