Skip to content

Next.js Integration Coming Soon ​

Coming Soon

The @nadi-pro/nextjs package is currently under development. The documentation below describes the planned API and features.

Integrate Nadi with Next.js applications for comprehensive error tracking across client and server.

Installation ​

bash
npm install @nadi-pro/browser @nadi-pro/nextjs

Setup ​

Configuration File ​

Create nadi.client.config.ts:

typescript
// nadi.client.config.ts
import { init } from '@nadi-pro/browser'

init({
  appKey: process.env.NEXT_PUBLIC_NADI_APP_KEY!,
  environment: process.env.NODE_ENV,
  release: process.env.NEXT_PUBLIC_VERSION,
  enableWebVitals: true,
})

Create nadi.server.config.ts:

typescript
// nadi.server.config.ts
import { init } from '@nadi-pro/node'

init({
  apiKey: process.env.NADI_API_KEY!,
  appKey: process.env.NADI_APP_KEY!,
  environment: process.env.NODE_ENV,
  release: process.env.NEXT_PUBLIC_VERSION,
})

Next.js Config ​

javascript
// next.config.js
const { withNadi } = require('@nadi-pro/nextjs')

module.exports = withNadi({
  // Your Next.js config
})({
  nadi: {
    hideSourceMaps: true,
  },
})

App Router (Next.js 13+) ​

typescript
// app/layout.tsx
import './globals.css'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

// app/error.tsx (client error boundary)
'use client'

import { useEffect } from 'react'
import { captureException } from '@nadi-pro/browser'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    captureException(error)
  }, [error])

  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  )
}

// app/global-error.tsx (root error boundary)
'use client'

import { useEffect } from 'react'
import { captureException } from '@nadi-pro/browser'

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    captureException(error)
  }, [error])

  return (
    <html>
      <body>
        <h2>Something went wrong!</h2>
        <button onClick={() => reset()}>Try again</button>
      </body>
    </html>
  )
}

Pages Router ​

typescript
// pages/_app.tsx
import type { AppProps } from 'next/app'
import '../nadi.client.config'

export default function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />
}

// pages/_error.tsx
import { captureException } from '@nadi-pro/browser'
import type { NextPage, NextPageContext } from 'next'

interface ErrorProps {
  statusCode?: number
}

const Error: NextPage<ErrorProps> = ({ statusCode }) => {
  return (
    <div>
      <h1>
        {statusCode
          ? `An error ${statusCode} occurred on server`
          : 'An error occurred on client'}
      </h1>
    </div>
  )
}

Error.getInitialProps = async ({ res, err }: NextPageContext) => {
  const statusCode = res ? res.statusCode : err ? err.statusCode : 404

  if (err) {
    captureException(err)
  }

  return { statusCode }
}

export default Error

Server Components ​

Error Handling ​

typescript
// app/dashboard/page.tsx
import { captureException } from '@nadi-pro/node'

async function getData() {
  try {
    const res = await fetch('https://api.example.com/data')
    if (!res.ok) throw new Error('Failed to fetch')
    return res.json()
  } catch (error) {
    captureException(error)
    throw error
  }
}

export default async function Dashboard() {
  const data = await getData()
  return <div>{/* render data */}</div>
}

Route Handlers ​

typescript
// app/api/users/route.ts
import { NextResponse } from 'next/server'
import { captureException } from '@nadi-pro/node'

export async function GET() {
  try {
    const users = await getUsers()
    return NextResponse.json(users)
  } catch (error) {
    captureException(error)
    return NextResponse.json(
      { error: 'Internal Server Error' },
      { status: 500 }
    )
  }
}

Client Components ​

Error Boundary ​

typescript
// components/ErrorBoundary.tsx
'use client'

import { Component, ReactNode } from 'react'
import { captureException } from '@nadi-pro/browser'

interface Props {
  children: ReactNode
  fallback?: ReactNode
}

interface State {
  hasError: boolean
}

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = { hasError: false }
  }

  static getDerivedStateFromError(): State {
    return { hasError: true }
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
    captureException(error, {
      extra: {
        componentStack: errorInfo.componentStack,
      },
    })
  }

  render(): ReactNode {
    if (this.state.hasError) {
      return this.props.fallback || <div>Something went wrong</div>
    }

    return this.props.children
  }
}

Event Handlers ​

typescript
'use client'

import { captureException, addBreadcrumb } from '@nadi-pro/browser'

export function CheckoutButton() {
  const handleClick = async () => {
    addBreadcrumb({
      category: 'ui',
      message: 'Checkout button clicked',
    })

    try {
      await processCheckout()
    } catch (error) {
      captureException(error)
    }
  }

  return <button onClick={handleClick}>Checkout</button>
}

Middleware ​

typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // Add request ID for tracking
  const requestId = crypto.randomUUID()

  const response = NextResponse.next()
  response.headers.set('x-request-id', requestId)

  return response
}

API Routes (Pages Router) ​

typescript
// pages/api/data.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { captureException, withScope } from '@nadi-pro/node'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    const data = await fetchData()
    res.status(200).json(data)
  } catch (error) {
    withScope((scope) => {
      scope.setTag('api', 'data')
      scope.setExtra('query', req.query)
      captureException(error)
    })

    res.status(500).json({ error: 'Internal Server Error' })
  }
}

Source Maps ​

Automatic Upload ​

javascript
// next.config.js
const { withNadi } = require('@nadi-pro/nextjs')

module.exports = withNadi({
  nadi: {
    // Upload source maps during build
    uploadSourceMaps: true,

    // Delete source maps after upload
    hideSourceMaps: true,

    // Release version
    release: process.env.NEXT_PUBLIC_VERSION,
  },
})()

Environment Variables ​

bash
# .env.local
NADI_API_KEY=your-api-key
NADI_APP_KEY=your-app-key
NEXT_PUBLIC_NADI_APP_KEY=your-app-key
NEXT_PUBLIC_VERSION=1.0.0

User Context ​

typescript
// app/providers.tsx
'use client'

import { useEffect } from 'react'
import { setUser } from '@nadi-pro/browser'
import { useSession } from 'next-auth/react'

export function NadiUserProvider({ children }: { children: React.ReactNode }) {
  const { data: session } = useSession()

  useEffect(() => {
    if (session?.user) {
      setUser({
        id: session.user.id,
        email: session.user.email!,
        name: session.user.name!,
      })
    } else {
      setUser(null)
    }
  }, [session])

  return <>{children}</>
}

Web Vitals ​

typescript
// app/layout.tsx
import { WebVitals } from './web-vitals'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <body>
        <WebVitals />
        {children}
      </body>
    </html>
  )
}

// app/web-vitals.tsx
'use client'

import { useReportWebVitals } from 'next/web-vitals'
import { recordMetric } from '@nadi-pro/browser'

export function WebVitals() {
  useReportWebVitals((metric) => {
    recordMetric({
      name: metric.name,
      value: metric.value,
      tags: {
        path: window.location.pathname,
      },
    })
  })

  return null
}

Testing ​

Mock SDK ​

typescript
// __mocks__/@nadi-pro/browser.ts
export const init = jest.fn()
export const captureException = jest.fn()
export const captureMessage = jest.fn()
export const setUser = jest.fn()
export const addBreadcrumb = jest.fn()

Test Setup ​

typescript
// jest.setup.ts
jest.mock('@nadi-pro/browser')

Nuxt.js Alternative ​

For Nuxt.js, use a similar approach:

typescript
// plugins/nadi.client.ts
import { init } from '@nadi-pro/browser'

export default defineNuxtPlugin(() => {
  init({
    appKey: useRuntimeConfig().public.nadiAppKey,
    environment: process.env.NODE_ENV,
  })
})

Best Practices ​

Do ​

  • Use separate configs for client/server
  • Upload source maps in CI/CD
  • Track Web Vitals
  • Set user context after auth

Don't ​

  • Expose API keys to client
  • Forget server-side error handling
  • Skip error boundaries
  • Miss API route errors

Troubleshooting ​

Client Errors Not Captured ​

  1. Verify nadi.client.config.ts is imported
  2. Check NEXT_PUBLIC_NADI_APP_KEY is set
  3. Ensure error boundaries are in place

Server Errors Not Captured ​

  1. Verify nadi.server.config.ts is imported
  2. Check NADI_API_KEY is set
  3. Ensure try/catch in API routes

Source Maps Not Working ​

  1. Check uploadSourceMaps: true in config
  2. Verify API key has upload permissions
  3. Check release version matches

Next Steps ​

Released under the MIT License.