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/nextjssends 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:
npx bugotter init --token bo_… # token from Agent access in BugOtterIt 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:
Script in the root layout
One tag inapp/layout.tsx: analytics, goals and browser errors. DetailsUpload source maps
Readable stacks, maps kept private. DetailsAfter next build:npx bugotter sourcemaps .next --deleteUptime and deploys
A health route, deploy markers, a status page. DetailsLast deploy step:npx bugotter deploy
Analytics: the script tag in your root layout
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-releaseis 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.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 withdata-exclude="/dashboard,/settings".Track goals from client components
Call
window.bo?.track()from any event handler. The?.matters: the script loads withdefer, 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.boonce, sotrack()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 {};

- Payments on the chart. A green marker shows the day a big payment came in.
- Error spikes, too. A red marker lines up a bad day with the error behind it.
- Last period, dashed, so a drop or a launch spike is obvious at a glance.
- Bots kept out of every number, with the count one click away.
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
next.config.ts bring them back. Only these three paths reach BugOtter.// 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 (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.
Install the SDK
terminal npm install @sentry/nextjsInit it in instrumentation.ts
Next.js runs
register()once when the server starts, for both the Node.js and Edge runtimes.onRequestErrorreports 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
dataCollectionline 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: falseinstead. LeavetracesSampleRateout: BugOtter doesn't store traces.Skip the browser SDK
The script tag already catches browser errors with page and request breadcrumbs. No
instrumentation-client.ts, no wizard, nowithSentryConfig. If you already have those, keep them, swap the DSN, and setsourcemaps: { disable: true }so the build doesn't try to upload to Sentry.
Kept
- Exceptions and
captureMessageevents, 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 plain summary written from the evidence: page, browser, release and visits.
- Fix with agent hands the whole issue to Claude Code, Cursor or Codex.
- Where your code broke, mapped back to your file and line with your source maps.
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
const config: NextConfig = {
productionBrowserSourceMaps: true,
};
// package.json
"scripts": {
"build": "next build && npx bugotter sourcemaps .next --delete"
}- Keys: the CLI reads
BUGOTTER_PROJECTandBUGOTTER_KEY(the upload key, a secret: build and CI only). - Paths: it maps
.next/staticto/_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, orgit rev-parseelsewhere). That's the same value asdata-releaseabove.
More in the source maps guide.
Uptime, a status page and deploys
3 regions
check your app from the moment you add the URL
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
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.

- Every day at a glance. Amber means a short outage that day.
- Response time, median and p95, and the slowest region.
- Three regions check your URL. An outage only counts when two agree.
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 mcp add --transport http --scope user bugotter https://bugotter.com/api/mcpThen 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.