Install guides / Next.js

Bug reports from your Next.js app

Brainbox puts a feedback button in your app. A user marks what broke on a screenshot, or records what they did, and says what happened. You get a report with the page, the browser, the viewport and the console errors, in your Brainbox inbox and in GitHub Issues or Azure DevOps if you connect them.

There are two ways in. The script tag needs nothing installed. The React components know who is signed in, so every report says who sent it.

The script tag, in your root layout

Add it once in app/layout.tsx and it is on every page. Your project key is in the dashboard; it is public by design, since it ships in your HTML.

app/layout.tsx
import Script from "next/script"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          src="https://app.brainbox.sh/widget.js"
          data-project="pk_your_project_key"
          data-endpoint="https://app.brainbox.sh/ingest"
          strategy="afterInteractive"
        />
      </body>
    </html>
  )
}

afterInteractive loads it once the page is interactive, so it never holds up your first render. The launcher is all that loads up front: the code that takes screenshots and recordings is fetched only when someone starts a report.

On the Pages Router

The same <Script> goes in pages/_app.tsx, beside your page component.

pages/_app.tsx
import type { AppProps } from "next/app"
import Script from "next/script"

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Component {...pageProps} />
      <Script
        src="https://app.brainbox.sh/widget.js"
        data-project="pk_your_project_key"
        data-endpoint="https://app.brainbox.sh/ingest"
        strategy="afterInteractive"
      />
    </>
  )
}

React components, with the signed-in user

Install the widget and the two packages it sits on. They need React 19, which Next.js 15 and later use.

npm install @ganeshdole/brainbox-widget @ganeshdole/brainbox-react @ganeshdole/brainbox-core

The provider and the widget are client components, so they live in a file of their own marked "use client". Make the transport once, outside render: a new transport means a new Brainbox.

app/feedback.tsx
"use client"

import type { ReactNode } from "react"
import { brainboxTransport } from "@ganeshdole/brainbox-core"
import { BrainboxProvider } from "@ganeshdole/brainbox-react"
import { BrainboxWidget } from "@ganeshdole/brainbox-widget"

const transport = brainboxTransport({
  endpoint: "https://app.brainbox.sh/ingest",
  projectKey: "pk_your_project_key",
})

type User = { id: string; email: string; name: string }

export function Feedback({ user, children }: { user: User | null; children: ReactNode }) {
  return (
    <BrainboxProvider transport={transport} identity={user ?? undefined}>
      {children}
      <BrainboxWidget />
    </BrainboxProvider>
  )
}
app/layout.tsx
import { Feedback } from "./feedback"
import { getUser } from "@/lib/auth" // however your app knows who is signed in

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const user = await getUser()
  return (
    <html lang="en">
      <body>
        <Feedback user={user}>{children}</Feedback>
      </body>
    </html>
  )
}

Server rendering and your styles

Nothing runs on the server. The provider starts Brainbox in an effect and the widget renders nothing until then, so a server-rendered layout stays side-effect free. Your server components pass through <Feedback> as children and stay server components.

The widget draws into a shadow root on <body>, so Tailwind, your CSS reset and your global styles cannot reach it, and it cannot reach them.

Your own button

Under the provider, useBrainboxWidget() opens the widget from anywhere. Use <BrainboxWidget trigger="manual" /> to hide our floating button and keep only yours.

app/report-problem-button.tsx
"use client"

import { useBrainboxWidget } from "@ganeshdole/brainbox-widget"

export function ReportProblemButton() {
  const { open } = useBrainboxWidget()
  return <button onClick={open}>Report a problem</button>
}

With the script tag, any element marked data-brainbox opens it, even in a server component: screenshot goes straight to marking up the page, record to a recording and feature to a feature request.

<button data-brainbox="screenshot">Report a bug</button>

Details from your app

Pass fields to the provider for whatever you want beside every report, such as the plan or the account ID. They show in the report under the person who sent it, with your labels, in your order.

<BrainboxProvider
  transport={transport}
  identity={user ?? undefined}
  fields={[
    { label: "Plan", value: account.plan, highlight: true },
    { label: "Account ID", value: account.id, type: "id" },
  ]}
>

Passwords and personal data

Typed values are always masked in recordings, so passwords, emails and card numbers never leave the browser as plain text. Add the rr-block or rr-mask class to anything else you want kept out.

Only on preview deployments

To try it with your team before your users see it, render it only outside production. On Vercel, VERCEL_ENV is production, preview or development, and a server component can read it.

app/layout.tsx
const showFeedback = process.env.VERCEL_ENV !== "production"

// in RootLayout's <body>:
{showFeedback && (
  <Script
    src="https://app.brainbox.sh/widget.js"
    data-project="pk_your_project_key"
    data-endpoint="https://app.brainbox.sh/ingest"
    strategy="afterInteractive"
  />
)}

Try it before you install it

The button in the corner of this page is the real widget. Report something on this page and it lands in our inbox, the way your users’ reports will land in yours.

Get started