How to Add Analytics to a Next.js App (2026 Guide)
If you have shipped a Next.js app and are wondering how to add analytics to it, you have probably already discovered that it is not as straightforward as dropping a script tag into an HTML file. Next.js has server components, client components, the App Router, streaming, and server-side rendering -- all of which affect when and where your tracking code runs.
This guide walks through everything you need to know about how to add analytics to a Next.js app. We will cover three integration methods (script tag, NPM package, and AI-assisted setup), then dig into SPA navigation tracking, custom events, and funnel analysis. Every code example uses the Next.js 15 App Router with TypeScript.
Why Next.js Needs Special Handling
Traditional analytics scripts assume a simple browser environment: the page loads, the script runs, done. Next.js breaks that assumption in several ways.
Server Components run on the server. The default in the App Router is that components are server-rendered. Any analytics code that references window, document, or browser APIs will throw a runtime error if it executes on the server. You need to make sure your tracking code only runs on the client.
The "use client" boundary matters. To use React hooks like useEffect or context providers, the component must be marked with the "use client" directive. This means your analytics provider needs to live inside a client component, even if your root layout is a server component.
SPA navigation does not trigger full page loads. When a user clicks a <Link> component in Next.js, the framework performs a client-side navigation. The browser never fires a traditional load event. If your analytics tool only listens for page loads, it will miss every navigation after the first one.
Script loading strategy affects initialization. Next.js provides a <Script> component with loading strategies like beforeInteractive, afterInteractive, and lazyOnload. Choosing the wrong strategy can either block rendering or delay your first page view event.
Understanding these constraints helps you pick the right integration approach.
Option 1: Script Tag (Simplest)
If you want the fastest possible setup, the script tag approach works in under a minute. This uses the EasyFunnel CDN bundle and the Next.js <Script> component.
Open your root layout file and add the script:
// 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://easyfunnel.co/sdk.js"
data-api-key="ef_your_project_key"
strategy="afterInteractive"
/>
</body>
</html>
)
}
That is it. The SDK auto-initializes when the script loads, reads the data-api-key attribute, and starts tracking page views immediately. It also exposes a global window.easyfunnel object so you can call easyfunnel.track() and easyfunnel.identify() from anywhere.
A few things to note:
strategy="afterInteractive"is the right choice here. It loads the script after the page becomes interactive, so it does not block rendering. AvoidbeforeInteractivefor analytics -- there is no reason to delay your first paint for tracking code.- The script is ~11KB. It is an IIFE bundle that includes session management, event batching, and SPA navigation detection.
- SPA navigation is handled automatically. The SDK monkey-patches
history.pushStateandhistory.replaceState, plus listens forpopstateevents. Every client-side navigation fires apage_viewevent without any extra configuration.
You can also enable optional modules via data attributes:
<Script
src="https://easyfunnel.co/sdk.js"
data-api-key="ef_your_project_key"
data-web-vitals
data-engagement
data-form-tracking
data-error-tracking
strategy="afterInteractive"
/>
This adds Core Web Vitals collection, engagement tracking, form interaction tracking, and JavaScript error tracking -- all with zero additional code.
Option 2: NPM Package with React Provider
For tighter integration and access to React hooks, install the SDK and React bindings:
npm install @easyfunnel/sdk @easyfunnel/react
Step 1: Create a Client Component Wrapper
The EasyFunnelProvider must run on the client because it uses React context and useEffect internally. Create a wrapper component:
// components/analytics-provider.tsx
'use client'
import { EasyFunnelProvider } from '@easyfunnel/react'
export function AnalyticsProvider({ children }: { children: React.ReactNode }) {
return (
<EasyFunnelProvider apiKey={process.env.NEXT_PUBLIC_EASYFUNNEL_KEY!}>
{children}
</EasyFunnelProvider>
)
}
The "use client" directive at the top is required. Without it, Next.js will try to render this as a server component and the React hooks inside the provider will fail.
Step 2: Add the Provider to Your Layout
Wrap your app with the analytics provider in the root layout:
// app/layout.tsx
import { AnalyticsProvider } from '@/components/analytics-provider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<AnalyticsProvider>
{children}
</AnalyticsProvider>
</body>
</html>
)
}
The provider automatically tracks page views, detects SPA navigations, manages sessions (with a 30-minute inactivity timeout), and batches events (flushing every 5 seconds or when 10 events accumulate).
Step 3: Track Custom Events with useTrack()
Use the useTrack hook in any client component to fire custom events:
// components/pricing-card.tsx
'use client'
import { useTrack } from '@easyfunnel/react'
export function PricingCard({ plan }: { plan: string }) {
const track = useTrack()
function handleSelectPlan() {
track('plan_selected', { plan, source: 'pricing_page' })
// ... continue with checkout logic
}
return (
<div>
<h3>{plan}</h3>
<button onClick={handleSelectPlan}>
Get Started
</button>
</div>
)
}
The useTrack hook returns a stable callback (wrapped in useCallback) so it is safe to use in dependency arrays and will not cause unnecessary re-renders.
Step 4: Identify Logged-In Users
When a user logs in or signs up, call useIdentify to link their anonymous session to a known user ID. This powers identity stitching on the server, which backfills all their anonymous events with the real user ID:
// components/auth-listener.tsx
'use client'
import { useEffect } from 'react'
import { useIdentify } from '@easyfunnel/react'
export function AuthListener({ userId }: { userId: string | null }) {
const identify = useIdentify()
useEffect(() => {
if (userId) {
identify(userId)
}
}, [userId, identify])
return null
}
Add this component inside your dashboard layout, passing the authenticated user's ID from Supabase (or whatever auth provider you use). Identity stitching means you can track a user's entire journey from anonymous visitor through signup and beyond, all connected to a single profile.
Step 5: Declarative Click Tracking
For simple click tracking without writing event handlers, use the data-ef-track attribute on any HTML element:
<button data-ef-track="cta_clicked">
Start Free Trial
</button>
<a href="/pricing" data-ef-track="pricing_link_clicked">
View Pricing
</a>
<div data-ef-track="feature_card_clicked">
<h3>Funnel Analytics</h3>
<p>See where users drop off</p>
</div>
The SDK listens for clicks on the document and walks up to three ancestor levels looking for the data-ef-track attribute. When found, it fires an event with the attribute value as the event name, plus the element's text content and current URL as properties. This works with both the script tag and NPM approaches.
Option 3: AI-Powered Setup with Claude
If you use Claude or another AI coding assistant, the EasyFunnel MCP server lets your AI tool set up analytics for you. Install it with:
npx @easyfunnel/mcp
Point it at your account API key (the efa_ key from your dashboard), and your AI assistant can list your projects, create funnels, query event data, and generate tracking code -- all through natural language. Instead of writing integration code manually, you describe what you want to track and the AI handles it.
Tracking SPA Navigation
This is the gotcha that catches most developers. In a traditional multi-page app, every navigation triggers a full page load, and analytics scripts re-run automatically. In Next.js, the App Router performs client-side navigations using the History API.
The EasyFunnel SDK handles this automatically by intercepting history.pushState, history.replaceState, and popstate events. Every route change fires a page_view event with a 500ms debounce to prevent duplicates from React's double-render in development mode.
If you are using a different analytics tool that does not handle SPA navigation, you would typically need to set up a custom hook:
'use client'
import { usePathname } from 'next/navigation'
import { useEffect } from 'react'
export function usePageTracking() {
const pathname = usePathname()
useEffect(() => {
// Your analytics page view call here
}, [pathname])
}
With EasyFunnel, you do not need this. The SDK intercepts navigations at the History API level, which is more reliable than watching the pathname because it also catches programmatic navigations (like router.push() calls) and hash changes.
Custom Events and Properties
Beyond page views, you will want to track specific user actions that matter to your product. Here are common patterns for Next.js apps:
const track = useTrack()
// Signup flow
track('signup_started', { method: 'email' })
track('signup_completed', { method: 'github_oauth' })
// Feature usage
track('project_created', { template: 'blank' })
track('dashboard_viewed', { project_count: 3 })
// Commerce
track('checkout_started', { plan: 'indie', price: 500 })
track('payment_completed', { plan: 'indie' })
The SDK automatically enriches every event with browser info (device type, OS, viewport size, language, timezone), attribution data (UTM parameters, referrer), and session metadata. You do not need to manually attach any of this.
Properties are capped at 4KB per event and event names at 100 characters. Events are batched (up to 10 at a time) and sent via navigator.sendBeacon on page unload, so you do not lose events when users close the tab.
Building Funnels
Tracking events is step one. The real value comes from seeing where users drop off. A funnel in EasyFunnel is a sequence of events that represents a user journey:
page_view(landing page)pricing_link_clickedplan_selectedcheckout_startedpayment_completed
You define funnels in the EasyFunnel dashboard by picking the events that make up each step. The platform then calculates conversion rates between steps, shows you where the biggest drop-offs happen, and lets you filter by time range, device, country, or any custom property.
This is the piece that most lightweight analytics tools skip entirely. Knowing your total page view count is not actionable. Knowing that 40% of users who view pricing never start checkout -- that is something you can fix.
For a deeper walkthrough on designing effective funnels, see our guide on simple event tracking.
Comparison: Analytics Options for Next.js
Here is how the main options stack up for a typical Next.js project:
| Tool | Setup | Bundle Size | SPA Tracking | Funnels | AI Features | Price | |---|---|---|---|---|---|---| | Vercel Analytics | Built-in | 1KB | Yes | No | No | $14/mo+ | | Google Analytics | Script tag | 45KB+ | Manual | Basic | No | Free (with data trade-offs) | | Plausible | Script tag | <1KB | Yes | No | No | $9/mo+ | | PostHog | NPM / Script | 60KB+ | Yes | Yes | Some | Free tier, then $0.00031/event | | EasyFunnel | Script or NPM | ~11KB | Auto | Yes | Yes (MCP + AI chat agent) | $5/mo |
Vercel Analytics is convenient if you are already on Vercel, but it only gives you page-level metrics. No custom events, no funnels, no user identification.
Google Analytics (GA4) is powerful but heavy. The bundle is large, the interface is complex, and you are handing your users' data to Google's ad platform. For indie projects, it is overkill and comes with privacy concerns.
Plausible is excellent for privacy-focused page analytics, but it does not support custom events with properties or funnel analysis. If you just need visitor counts, it is a great choice. If you need to understand user journeys, you will outgrow it quickly.
PostHog is the most feature-rich option, with session replay, feature flags, A/B testing, and funnels. But it is also the most complex to set up and maintain. The bundle is large, and the learning curve is steep. For a solo founder, it can be more tool than you need.
EasyFunnel sits in the middle: lightweight enough for indie projects, but with funnel analytics, identity stitching, and an AI chat agent that can answer questions about your data. The MCP server means your AI coding assistant can query your analytics directly. At $5/month for 50K events, it is built for indie hackers and solo founders who need actionable insights without the complexity.
Environment Variable Setup
Whichever approach you choose, store your API key in an environment variable. For Next.js, prefix it with NEXT_PUBLIC_ so it is available on the client:
# .env.local
NEXT_PUBLIC_EASYFUNNEL_KEY=ef_your_project_key
Then reference it in your code:
<EasyFunnelProvider apiKey={process.env.NEXT_PUBLIC_EASYFUNNEL_KEY!}>
Or in the script tag approach:
<Script
src="https://easyfunnel.co/sdk.js"
data-api-key={process.env.NEXT_PUBLIC_EASYFUNNEL_KEY!}
strategy="afterInteractive"
/>
Never hard-code API keys directly in your source files, even client-side ones. Environment variables keep your keys out of version control and make it easy to use different keys for development and production.
Common Next.js Gotchas
A few issues that come up repeatedly when adding analytics to Next.js:
Hydration mismatches. If your analytics code renders anything to the DOM (like a cookie banner), make sure it is wrapped in a client component with proper useEffect guards. The EasyFunnel SDK does not render any UI for tracking, so this is not an issue.
Double page views in development. React 18's Strict Mode mounts components twice in development. The EasyFunnel SDK debounces page views with a 500ms window on the same URL, so duplicates are filtered automatically.
Middleware and edge runtime. Do not try to import analytics SDKs in middleware.ts or edge API routes. These run in a limited runtime without full browser APIs. Analytics tracking belongs in client components.
Static generation. If you use generateStaticParams or static exports, the analytics script still works fine -- it initializes at runtime in the browser, not at build time.
Getting Started
Adding analytics to a Next.js app does not need to be complicated. Here is the quickest path:
- Create a free EasyFunnel account
- Create a project and grab your
ef_API key - Add the script tag to your
layout.tsx(Option 1) or install the NPM packages (Option 2) - Deploy and start seeing events in your dashboard
You will have page views, session tracking, and SPA navigation working within five minutes. From there, add custom events for the actions that matter to your business, build funnels to visualize conversion paths, and use the AI chat agent to ask questions about your data in plain English.
Read the full SDK documentation for advanced configuration options, or check out our guide on analytics for indie hackers for strategies on what to track when you are building solo.
Ready to track your funnels?
EasyFunnel gives you funnel analytics + AI chat for $5/mo. 3-day free trial.
Start Free