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 (plushttp://localhost:3000if you want to measure in dev) under Allowed origins.
Installation
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_KEYThe
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.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 firstuseEffect; 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 }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> ) }4On the Pages Router? Do it in
_appSame idea: create the instance once and call
pageview()on the router’srouteChangeCompleteevent.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.
'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
- Open the published site in a normal window, with no ad blocker.
- In your browser devtools, Network tab, filter by
v1/i: you should see aPOSTwith status200within 3 seconds (or when you leave the page). - In the dashboard, events show up in about 30 seconds.
- Navigate between two pages with
<Link>: each change produces a new pageview in the nextPOST.
Troubleshooting
- Only the first page is counted
<AnalyticsTracker />ended up in a page instead of the root layout, or the effect doesn’t depend onpathname. It must live inapp/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 restartnext 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 withoutwww, plus test addresses likehttp://localhost:3000if 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