Lightweight, privacy-first analytics for Next.js. Drop in a script tag, write four lines of code, and ship in under ten minutes.
After setup:
No Janus account yet? Add Janus in 5 minutes to grab a free API key. The free tier covers 10,000 events a month.
These steps cover the App Router. On the Pages Router? Scroll to the Pages Router section near the end of this guide.
Use next/script with the afterInteractive strategy. Initialize with trackSPANavigation: false; the App Router emits its own route-change events, and Step 2 catches those.
This key ships to the browser, so use an Ingest key — the default scope in Settings. Ingest keys can only send events. A Full access key here would let anyone who views source read your analytics and create more keys.
import Script from 'next/script';import { Analytics } from './components/Analytics';export default function RootLayout({children,}: {children: React.ReactNode;}) {return (<html lang="en"><body>{children}<Scriptsrc="https://addjanus.ca/janus.js"strategy="afterInteractive"/><Script id="janus-init" strategy="afterInteractive">{`Janus.init('jns_your_api_key', {trackSPANavigation: false});`}</Script><Analytics /></body></html>);}
Server Components can’t reach window. Drop this Client Component into your root layout. It watches usePathname and fires Janus.page() on each route change.
'use client';import { usePathname } from 'next/navigation';import { useEffect } from 'react';declare global {interface Window {Janus?: {page: () => void;track: (name: string, props?: Record<string, unknown>) => void;};}}export function Analytics() {const pathname = usePathname();useEffect(() => {window.Janus?.page();}, [pathname]);return null;}
Custom events fire from any Client Component. One method, two arguments: a name and an optional properties object.
'use client';export function SignupButton() {return (<buttononClick={() =>window.Janus?.track('signup_click', {plan: 'pro',location: 'hero'})}>Sign up</button>);}
Tracking from a Server Action or Route Handler? Server-side calls bypass the SDK. Use the HTTP API with your API key, or pass the data to a Client Component that calls Janus.track.
Within a minute or two of your first visit, your dashboard starts to fill in. Here's what it looks like with a handful of real page views and one custom event:
No tutorial needed. You can tell what's happening with your app at a glance.
No configuration, no funnel setup, no goal creation. Open the dashboard and it already makes sense.
trackSPANavigation: true double-counts. The SDK fires on replaceState, then the usePathname effect fires again. Set it to false.Janus.track from a Server Component throws. Mark the component 'use client' or use the HTTP API.next dev is expected. next start against a production build fires one.Full access key in NEXT_PUBLIC_ or inline in a component is readable by every visitor. Only Ingest keys belong in client code; keep full-access keys in server-only environment variables.Skip Steps 1 and 2. Use this instead. The SDK’s default SPA tracking handles route changes via the History API.
// pages/_document.tsximport { Html, Head, Main, NextScript } from 'next/document';export default function Document() {return (<Html><Head><scriptsrc="https://addjanus.ca/janus.js"defer/></Head><body><Main /><NextScript /></body></Html>);}// pages/_app.tsximport type { AppProps } from 'next/app';import { useEffect } from 'react';export default function App({ Component, pageProps }: AppProps) {useEffect(() => {window.Janus?.init('jns_your_api_key');}, []);return <Component {...pageProps} />;}
Janus is a hosted script. The whole integration is one Script tag, one init call, and one Client Component.
npm install. The SDK loads from our CDN at runtime.Once events are flowing, connect Janus to your AI coding agent via MCP and ask about your traffic from the editor. Migrating from Google Analytics? Replace GA on a side project covers porting custom events and removing gtag.js. Working in another framework? See the framework guides for Nuxt, Remix, Astro, and SvelteKit.
Yes. Load the SDK via next/script in your root layout, then call Janus.page() from a Client Component that watches usePathname. Server Components stay untouched. Only the Analytics component needs the 'use client' directive.
The App Router fires replaceState on every navigation. Janus auto-detects those calls. If SPA tracking stays on while the usePathname effect also fires, each route change counts twice. Disabling SPA tracking and calling Janus.page from the Client Component gives you exactly one event per route.
Not via the SDK. There is no window on the server. Either return the data to a Client Component that calls Janus.track, or call the Janus HTTP API directly from your server with an API key.
No. The loader is under 1 KB and loads asynchronously after the page becomes interactive. It is not bundled with your app. The SDK loads from the Janus CDN at runtime, so your build output is unchanged.
No. The integration is identical. Janus is a hosted script, so it works the same wherever your Next.js app runs.
No. React Strict Mode double-invokes effects in development to surface concurrency bugs. Production fires each event once. Verify by running next start against a production build.
Yes. The Pages Router is simpler. One script tag in _document.tsx and one init call in _app.tsx. The SDK default SPA tracking handles route changes via the History API.
Free tier. No credit card. No bundler config.
Get started with Janus