All integration guides

Integration guide

Analytics in Next.js

A client component in the root layout counts the first pageview and every navigation made by the Next router. Works with both the App Router and the Pages Router.

Before you start

  • Next.js 13 or newer (App Router), or any version with the Pages Router.
  • Your site’s site_key, and your domain (plus http://localhost:3000 if you want to measure in dev) under Allowed origins.

Installation

  1. 1Install the SDK

    The package is the same SDK as the script snippet, with TypeScript types.

    npm install @racoelho/analytics
    .env.local
    NEXT_PUBLIC_ANALISA_SITE_KEY=YOUR_SITE_KEY

    The NEXT_PUBLIC_ prefix is required because the key is used in the browser. It’s public by nature — what protects ingestion is the Allowed origins list.

  2. 2Create the client component (App Router)

    The SDK needs window, so it lives in a 'use client' component. The instance is created once, in the first useEffect; on route changes the effect runs again and records the new page.

    app/analytics.tsx
    'use client'
    
    import { useEffect } from 'react'
    import { usePathname } from 'next/navigation'
    import { Analytics } from '@racoelho/analytics'
    
    let analytics: Analytics | null = null
    
    /** For events in client components: getAnalytics()?.track(…) */
    export function getAnalytics() {
      return analytics
    }
    
    export function AnalyticsTracker() {
      const pathname = usePathname()
    
      useEffect(() => {
        if (!analytics) {
          // first load: the constructor already sends the pageview
          analytics = new Analytics({ siteKey: process.env.NEXT_PUBLIC_ANALISA_SITE_KEY! })
          return
        }
        // client-side navigation: count the new page
        analytics.pageview()
      }, [pathname])
    
      return null
    }
  3. 3Add it to the root layout

    Inside <body>, after the content. The layout stays a Server Component.

    app/layout.tsx
    import { AnalyticsTracker } from './analytics'
    
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="en">
          <body>
            {children}
            <AnalyticsTracker />
          </body>
        </html>
      )
    }
  4. 4On the Pages Router? Do it in _app

    Same idea: create the instance once and call pageview() on the router’s routeChangeComplete event.

    pages/_app.tsx
    import type { AppProps } from 'next/app'
    import { useEffect } from 'react'
    import { useRouter } from 'next/router'
    import { Analytics } from '@racoelho/analytics'
    
    let analytics: Analytics | null = null
    
    export default function App({ Component, pageProps }: AppProps) {
      const router = useRouter()
    
      useEffect(() => {
        // create once; the constructor sends the first pageview
        analytics ??= new Analytics({ siteKey: process.env.NEXT_PUBLIC_ANALISA_SITE_KEY! })
        const onRouteChange = () => analytics?.pageview()
        router.events.on('routeChangeComplete', onRouteChange)
        return () => router.events.off('routeChangeComplete', onRouteChange)
      }, [router.events])
    
      return <Component {...pageProps} />
    }

Events and goals

In any client component, import getAnalytics() and call track(name, properties). The ?. avoids an error if the click happens before the first effect ran.

app/signup-button.tsx
'use client'

import { getAnalytics } from '@/app/analytics'

export function SignupButton() {
  return (
    <button
      onClick={() => {
        // custom event, with properties
        getAnalytics()?.track('signup_click', { plan: 'pro' })
      }}
    >
      Sign up
    </button>
  )
}
// after login
getAnalytics()?.identify({ user_id: user.id, traits: { plan: user.plan } })

// on logout
getAnalytics()?.reset()

Create an event goal named signup_click in the dashboard. With identify(), anonymous sessions from before login are linked to the user.

Page navigation

The SDK sends the first-load pageview on its own but does not listen to router navigations — hence the useEffect on pathname (App Router) or routeChangeComplete (Pages Router). Changing only the query string doesn’t count as a new page: the SDK records the path. In development, React runs effects twice; the SDK drops a repeated pageview for the same path within 2.5 seconds.

Check that it works

  1. Open the published site in a normal window, with no ad blocker.
  2. In your browser devtools, Network tab, filter by v1/i: you should see a POST with status 200 within 3 seconds (or when you leave the page).
  3. In the dashboard, events show up in about 30 seconds.
  4. Navigate between two pages with <Link>: each change produces a new pageview in the next POST.

Troubleshooting

Only the first page is counted
<AnalyticsTracker /> ended up in a page instead of the root layout, or the effect doesn’t depend on pathname. It must live in app/layout.tsx, which isn’t unmounted between navigations.
siteKey is required
The environment variable didn’t reach the browser. Check the NEXT_PUBLIC_ prefix and restart next dev (or rebuild) after changing .env.
The request returns 403 (origin_not_allowed)
Your domain isn’t in Allowed origins in the site settings. Add the exact origin — with https://, with and without www, plus test addresses like http://localhost:3000 if you want to measure in development.
No request shows up in the Network tab
The SDK honors Do Not Track by default: if your browser sends DNT, nothing is sent. Test in another browser or profile. Ad blockers can also block the script.
Pageviews show up twice
The SDK was initialized twice (for example, pasted in the theme and also added by a plugin). Keep a single instance per page.

Create your account and grab the site_key

Free up to 10K events a month, no credit card.

Create a free account
Next.js analytics: App Router and Pages Router · Analisa