How to Add Analytics to Your Next.js App (Without GA4)

Google Analytics 4 is the default choice for most Next.js apps, but it comes with real downsides: cookie consent banners that lose 30-50% of your data, a complex setup with Google Tag Manager, bundle size impact, and privacy concerns that require careful handling in server-rendered environments.

This guide walks through adding lightweight, cookieless analytics to a Next.js application — covering both App Router and Pages Router, SPA navigation tracking, custom events, and revenue attribution.

Why GA4 is a poor fit for Next.js

GA4 was designed for traditional multi-page websites. Next.js applications are fundamentally different:

Client-side navigation. Next.js uses the router to navigate between pages without full page reloads. GA4's default pageview tracking relies on the load event, which does not fire during client-side navigation. You need additional configuration (listening to routeChangeComplete in Pages Router or using usePathname in App Router) to track page transitions.

Server components. In the App Router, most components are server components by default. GA4's tracking script runs on the client. You need to carefully manage where the script loads to avoid hydration mismatches and ensure it does not accidentally run during server-side rendering.

Cookie consent in SSR. If you implement cookie consent for GA4, you need to handle the consent state across server and client rendering. The consent banner must load before any GA cookies are set, which means blocking the analytics script until consent is given — adding complexity and latency.

Bundle size. The GA4 gtag.js library is approximately 80KB (compressed). For a framework that obsesses over bundle optimization, adding 80KB of analytics JavaScript is a meaningful trade-off.

Privacy-focused analytics tools avoid most of these problems. They do not use cookies (no consent management needed), handle SPA navigation natively, and ship lightweight scripts (under 5KB).

Option 1: App Router setup (Next.js 13+)

The App Router is the recommended approach for new Next.js projects. Here is how to add DataSaaS analytics.

Basic setup with next/script

Add the tracking script to your root layout. Using next/script with strategy="afterInteractive" ensures the script loads after the page is interactive, without blocking rendering:

// 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://datasaas.co/js/script.js"
          data-website-id="your-website-id"
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}

That is it. The script automatically:

  • Tracks the initial pageview
  • Intercepts history.pushState and history.replaceState to track client-side navigation
  • Handles back/forward button navigation via the popstate event
  • Manages session tracking (30-minute inactivity timeout)
  • Works with cookieless mode by default (no consent banner needed)

Why SPA tracking works automatically

Next.js App Router uses history.pushState() under the hood for client-side navigation. The DataSaaS tracking script overrides history.pushState and history.replaceState to detect page transitions:

// This happens inside the tracking script — you do not need to write this code.
// Shown here to explain how SPA tracking works.

const originalPushState = history.pushState;
history.pushState = function (...args) {
  originalPushState.apply(this, args);
  // Track the new page
  trackPageview();
};

window.addEventListener("popstate", () => {
  trackPageview();
});

This approach works with any framework that uses the History API — Next.js, Remix, Nuxt, SvelteKit, or vanilla SPAs. No framework-specific integration is needed.

Environment-specific website IDs

If you use different website IDs for development and production, use an environment variable:

// app/layout.tsx
import Script from "next/script";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}
        {process.env.NEXT_PUBLIC_DATASAAS_ID && (
          <Script
            src="https://datasaas.co/js/script.js"
            data-website-id={process.env.NEXT_PUBLIC_DATASAAS_ID}
            strategy="afterInteractive"
          />
        )}
      </body>
    </html>
  );
}
# .env.production
NEXT_PUBLIC_DATASAAS_ID=ds_abc123

# .env.development (optional — omit to disable tracking in dev)
# NEXT_PUBLIC_DATASAAS_ID=ds_dev456

This pattern ensures the script only loads when the environment variable is set. During local development, you can leave it unset to avoid polluting your analytics with localhost traffic.

Option 2: Pages Router setup

If your Next.js app uses the Pages Router, add the script to _app.tsx:

// pages/_app.tsx
import type { AppProps } from "next/app";
import Script from "next/script";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Component {...pageProps} />
      <Script
        src="https://datasaas.co/js/script.js"
        data-website-id="your-website-id"
        strategy="afterInteractive"
      />
    </>
  );
}

The SPA tracking works identically in Pages Router because Next.js uses history.pushState for page transitions in both router implementations.

Alternative: plain script tag in _document.tsx

If you prefer not to use next/script, you can add the script directly to _document.tsx. This gives you less control over loading strategy but is simpler:

// pages/_document.tsx
import { Html, Head, Main, NextScript } from "next/document";

export default function Document() {
  return (
    <Html lang="en">
      <Head />
      <body>
        <Main />
        <NextScript />
        <script
          defer
          data-website-id="your-website-id"
          src="https://datasaas.co/js/script.js"
        ></script>
      </body>
    </Html>
  );
}

The defer attribute ensures the script does not block page rendering. It will execute after the HTML is parsed.

Add DataSaaS to Next.js in 5 min

One script tag, automatic SPA tracking, zero config. Works with App Router and Pages Router.

Try DataSaaS free

Tracking custom events

Pageviews are tracked automatically. For product-specific events — signups, feature usage, purchases — use the datasaas.track() function.

Basic event tracking

// components/signup-button.tsx
"use client";

export function SignupButton() {
  const handleSignup = async () => {
    // Your signup logic here
    await createAccount();

    // Track the event
    window.datasaas?.track("signup", {
      plan: "pro",
      source: "pricing-page",
    });
  };

  return <button onClick={handleSignup}>Start free trial</button>;
}

TypeScript support

Add type declarations for the tracking function to avoid TypeScript errors:

// types/datasaas.d.ts
interface DataSaaSTracker {
  track: (
    eventName: string,
    properties?: Record<string, string | number | boolean>
  ) => void;
}

declare global {
  interface Window {
    datasaas?: DataSaaSTracker;
  }
}

export {};

Common events worth tracking

Here are events that most Next.js SaaS applications should track:

// Signup completed
window.datasaas?.track("signup", { plan: "free" });

// User upgraded to a paid plan
window.datasaas?.track("upgrade", { plan: "pro", interval: "yearly" });

// Feature activated for the first time
window.datasaas?.track("feature_activated", { feature: "api-keys" });

// Documentation search
window.datasaas?.track("docs_search", { query: "authentication" });

// Pricing page viewed (tracked as an event, not just a pageview)
window.datasaas?.track("pricing_viewed", { source: "nav" });

The track() call uses optional chaining (datasaas?.track) so it silently does nothing if the script has not loaded yet or if you are in a development environment without the tracking script.

Revenue tracking

If you sell a SaaS product, connecting your analytics to revenue data is where the real value is. This lets you see Revenue Per Visitor — which traffic sources generate actual paying customers, not just signups.

Setting up revenue attribution

Revenue tracking requires connecting your payment provider (Stripe, LemonSqueezy, or Polar) to DataSaaS. This is a one-time setup in the DataSaaS dashboard — not a code change.

  1. Go to your DataSaaS dashboard → Settings → Integrations
  2. Click "Connect Stripe" (or your payment provider)
  3. Authorize the connection via OAuth
  4. DataSaaS automatically matches payments to the visitor sessions that preceded them

Once connected, your analytics dashboard shows:

  • Revenue Per Visitor by traffic source — Is organic search driving more revenue per visitor than paid ads?
  • Revenue Per Visitor by landing page — Which blog posts produce the most revenue per reader?
  • Revenue Per Visitor by country — Are US visitors worth more than UK visitors?
  • Revenue Per Visitor by UTM campaign — Which newsletter issue drove the highest-value traffic?

How attribution works

When a visitor lands on your Next.js app, the tracking script records their session — including referrer, UTM parameters, landing page, and a hashed visitor identifier. When that visitor later makes a payment through Stripe, DataSaaS matches the Stripe customer email to the visitor's session history.

No additional code is needed in your Next.js app. The matching happens server-side through the payment provider webhook.

Performance impact

One of the main reasons to avoid GA4 in a Next.js app is performance. Here is how the numbers compare:

| Metric | GA4 (gtag.js) | DataSaaS | |--------|--------------|----------| | Script size (compressed) | ~80KB | ~4.8KB | | Cookies set | 2-4 | 0 | | DNS lookups | 2+ (google-analytics.com, googletagmanager.com) | 1 (datasaas.co) | | Third-party requests | 3-5 | 1 | | Consent banner overhead | ~20-50KB (consent management platform) | 0 |

With strategy="afterInteractive" in next/script, the analytics script loads after the page is interactive, so it does not affect Largest Contentful Paint (LCP) or First Input Delay (FID). The 4.8KB script size has negligible impact on Total Blocking Time (TBT).

See revenue from your Next.js app

Connect Stripe and see Revenue Per Visitor by traffic source. No GTM, no custom events needed.

Try DataSaaS free

Handling ad blockers

Some visitors use ad blockers that block analytics requests. This affects all analytics tools, including privacy-focused ones. Here are two approaches to handle this:

Approach 1: Accept the data loss

Most privacy-focused analytics tools are blocked by fewer ad blockers than GA4. The uBlock Origin default filter list blocks Google Analytics but does not block most privacy-focused tools. In practice, this means you lose 5-10% of traffic to ad blockers with a privacy-focused tool, vs 15-25% with GA4.

Approach 2: Proxy through your domain

You can proxy the analytics script through your Next.js app so it is served from your own domain, which makes it invisible to ad blockers:

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  async rewrites() {
    return [
      {
        source: "/js/script.js",
        destination: "https://datasaas.co/js/script.js",
      },
      {
        source: "/api/collect",
        destination: "https://datasaas.co/api/events",
      },
    ];
  },
};

module.exports = nextConfig;

Then update your script tag to use the proxied path:

<Script
  src="/js/script.js"
  data-website-id="your-website-id"
  data-host-url=""
  strategy="afterInteractive"
/>

This approach serves the tracking script from your own domain and routes event data through your own API endpoint. Ad blockers cannot distinguish it from your first-party JavaScript.

Common mistakes to avoid

Loading the script in a client component

Do not import the analytics script inside a client component. It should be in your root layout (App Router) or _app.tsx (Pages Router) so it loads once and persists across navigation.

// BAD: Script loads and unloads as component mounts/unmounts
"use client";
export function AnalyticsProvider({ children }) {
  return (
    <>
      {children}
      <Script src="https://datasaas.co/js/script.js" ... />
    </>
  );
}

// GOOD: Script in root layout, loads once
// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Script src="https://datasaas.co/js/script.js" ... />
      </body>
    </html>
  );
}

Double-tracking pageviews

If you manually call a pageview tracking function on route change in addition to the script's automatic SPA tracking, you will double-count pageviews. The DataSaaS script handles SPA navigation automatically — do not add manual pageview tracking on top of it.

Blocking rendering with the script

Never use strategy="beforeInteractive" for analytics scripts. Analytics should never block page rendering. Use afterInteractive (default) or lazyOnload.

Forgetting to exclude development traffic

If you include the tracking script in development, your analytics will include localhost traffic. Use an environment variable to conditionally load the script (shown in the App Router setup above).

Verifying your setup

After adding the script, verify that tracking works:

  1. Open your Next.js app in a browser (production or a preview deployment).
  2. Open your analytics dashboard and check for real-time pageviews.
  3. Navigate between pages using client-side links. Each navigation should register as a separate pageview.
  4. Check the referrer — if you navigated from a search engine or social media, the source should appear correctly.
  5. Test custom events — trigger an event and verify it appears in your dashboard.
  6. Test the back button — navigate to a few pages, then use the browser back button. Each back navigation should register as a pageview.

If pageviews appear for the initial load but not for client-side navigation, the SPA tracking is not working. Check that the script loads in the root layout (not in a page-level component) and that it is not being blocked by a content security policy.

For a detailed Next.js integration guide, see the DataSaaS Next.js integration docs.


Related reading: