Integrate Zenovay analytics into your Next.js application with support for both App Router and Pages Router using the tracking script tag.
Installation
No npm package is needed. Add the Zenovay tracking script to your app using Next.js Script component or a standard <script> tag.
You can find your tracking code in the Zenovay app: open Domains, click your site, then open its General settings page. The Tracking script card shows the snippet and a Verify Installation button.
App Router Setup (Next.js 13+)
Add to Root Layout
// 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://api.zenovay.com/z.js"
data-tracking-code="YOUR_TRACKING_CODE"
strategy="afterInteractive"
/>
</body>
</html>
);
}
With Environment Variable
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Script
src="https://api.zenovay.com/z.js"
data-tracking-code={process.env.NEXT_PUBLIC_ZENOVAY_TRACKING_CODE}
strategy="afterInteractive"
/>
</body>
</html>
);
}
Client Component for Events
Create a client component for tracking page views on client-side (SPA) navigation:
// components/Analytics.tsx
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
export function Analytics() {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
// Track page view on route change (SPA navigation)
if (window.zenovay) {
window.zenovay('page');
}
}, [pathname, searchParams]);
return null;
}
Add to layout:
// app/layout.tsx
import Script from 'next/script';
import { Analytics } from '@/components/Analytics';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Analytics />
<Script
src="https://api.zenovay.com/z.js"
data-tracking-code="YOUR_TRACKING_CODE"
strategy="afterInteractive"
/>
</body>
</html>
);
}
Pages Router Setup
Add to _app.tsx
// pages/_app.tsx
import Script from 'next/script';
import type { AppProps } from 'next/app';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Component {...pageProps} />
<Script
src="https://api.zenovay.com/z.js"
data-tracking-code="YOUR_TRACKING_CODE"
strategy="afterInteractive"
/>
</>
);
}
Or use Script in _document.tsx
// pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document';
import Script from 'next/script';
export default function Document() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
<Script
src="https://api.zenovay.com/z.js"
data-tracking-code="YOUR_TRACKING_CODE"
strategy="afterInteractive"
/>
</body>
</Html>
);
}
Event Tracking
Client Component
'use client';
export function SignupButton() {
const handleClick = () => {
if (window.zenovay) {
window.zenovay('track', 'signup_click', {
plan: 'pro',
source: 'pricing'
});
}
};
return (
<button onClick={handleClick}>
Start Free Trial
</button>
);
}
User Identification
After Authentication
'use client';
import { useEffect } from 'react';
import { useSession } from 'next-auth/react';
export function UserIdentifier() {
const { data: session } = useSession();
useEffect(() => {
if (session?.user && window.zenovay) {
window.zenovay('identify', session.user.id, {
email: session.user.email,
name: session.user.name
});
}
}, [session]);
return null;
}
Revenue Tracking
E-commerce Checkout
'use client';
import { useEffect } from 'react';
export function OrderConfirmation({ order }) {
useEffect(() => {
if (window.zenovay) {
window.zenovay('revenue', order.total, 'USD', {
order_id: order.id,
items: order.items
});
}
}, [order.id]);
return (
<div>
<h1>Order Confirmed!</h1>
</div>
);
}
Info
For purchases captured on your backend (e.g. a Stripe webhook), report revenue from the server instead — see Server-Side Tracking below. That keeps revenue attribution accurate even if the browser never reaches a confirmation page.
Server-Side Tracking
For events your backend is the source of truth for — purchases, sign-ups, off-site activity — send them from the server with the POST /api/v1/events endpoint. This is a separate, authenticated ingestion API.
Warning
Server-side ingestion requires an API key and a paid plan (Pro, Scale, or Enterprise). Free accounts receive 403 API_PAID_PLAN_REQUIRED. Create a key under Settings → Security → API keys — see Getting an API Key. The browser tracking script above stays free on every plan.
Authenticate with a zv_* API key in the Authorization: Bearer header. Each event has a type (pageview, event, identify, goal, or purchase), a millisecond ts, and a type-specific props object. Pass an idempotencyKey to make retries safe.
Server Action
// app/actions.ts
'use server';
export async function submitForm(formData: FormData) {
// Process form
const email = formData.get('email');
// Track a custom event server-side
await fetch('https://api.zenovay.com/api/v1/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.ZENOVAY_API_KEY}`,
},
body: JSON.stringify({
trackingCode: process.env.ZENOVAY_TRACKING_CODE,
events: [{
type: 'event',
ts: Date.now(),
props: {
name: 'form_submitted',
form: 'contact',
has_email: !!email,
},
idempotencyKey: `form-${crypto.randomUUID()}`,
}],
}),
});
return { success: true };
}
API Route Handler
// app/api/track/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const body = await request.json();
await fetch('https://api.zenovay.com/api/v1/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.ZENOVAY_API_KEY}`,
},
body: JSON.stringify({
trackingCode: process.env.ZENOVAY_TRACKING_CODE,
events: [{
type: 'event',
ts: Date.now(),
props: {
name: body.name || 'custom_event',
...body.props,
},
idempotencyKey: crypto.randomUUID(),
}],
serverContext: {
clientIp: request.headers.get('x-forwarded-for') ?? undefined,
userAgent: request.headers.get('user-agent') ?? undefined,
},
})
});
return NextResponse.json({ success: true });
}
Info
pageview events need a real RFC-4122 visitorId and sessionId (the columns are UUID-typed), so they're usually best left to the browser tracker. The server endpoint is ideal for event, identify, goal, and purchase types. See Server-Side Tracking for the full event schema and rejection reasons.
Purchase from a Webhook
// app/api/webhooks/stripe/route.ts
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const event = await request.json();
// ... verify the Stripe signature first ...
await fetch('https://api.zenovay.com/api/v1/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.ZENOVAY_API_KEY}`,
},
body: JSON.stringify({
trackingCode: process.env.ZENOVAY_TRACKING_CODE,
events: [{
type: 'purchase',
ts: Date.now(),
// The visitorId you stored against this customer on your side
visitorId: event.data.object.metadata?.visitor_id,
props: {
amount: event.data.object.amount_total / 100,
currency: event.data.object.currency?.toUpperCase() || 'USD',
payment_provider: 'stripe',
},
idempotencyKey: event.id,
}],
})
});
return NextResponse.json({ received: true });
}
Environment Variables
Setup
# .env.local
NEXT_PUBLIC_ZENOVAY_TRACKING_CODE=your-tracking-code # client-side script (public)
ZENOVAY_TRACKING_CODE=your-tracking-code # server-side (same value)
ZENOVAY_API_KEY=zv_your-api-key # server-side only — never expose
Warning
Only the tracking code (used in the data-tracking-code attribute) is safe to expose with the NEXT_PUBLIC_ prefix. Your zv_* API key must stay server-side — never prefix it with NEXT_PUBLIC_.
Usage
// Client-side (in layout)
<Script
src="https://api.zenovay.com/z.js"
data-tracking-code={process.env.NEXT_PUBLIC_ZENOVAY_TRACKING_CODE}
strategy="afterInteractive"
/>
// Server-side (in API routes or server actions)
const response = await fetch('https://api.zenovay.com/api/v1/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.ZENOVAY_API_KEY}`,
},
body: JSON.stringify({
trackingCode: process.env.ZENOVAY_TRACKING_CODE,
events: [{ type: 'event', ts: Date.now(), props: { name: 'custom_event' } }],
}),
});
Route Groups Analytics
// app/(marketing)/layout.tsx
import Script from 'next/script';
export default function MarketingLayout({ children }) {
return (
<>
{children}
<Script
src="https://api.zenovay.com/z.js"
data-tracking-code="MARKETING_TRACKING_CODE"
strategy="afterInteractive"
/>
</>
);
}
// app/(app)/layout.tsx
import Script from 'next/script';
export default function AppLayout({ children }) {
return (
<>
{children}
<Script
src="https://api.zenovay.com/z.js"
data-tracking-code="APP_TRACKING_CODE"
strategy="afterInteractive"
/>
</>
);
}
TypeScript Types
// types/zenovay.d.ts
interface ZenovayFunction {
(command: 'track', name: string, properties?: Record<string, unknown>): void;
(command: 'identify', userId: string, traits?: Record<string, unknown>): void;
(command: 'goal', name: string, properties?: Record<string, unknown>): void;
(command: 'page'): void;
(command: 'revenue', amount: number, currency: string, meta?: Record<string, unknown>): void;
}
declare global {
interface Window {
zenovay: ZenovayFunction;
}
}
export {};
Common Patterns
With Next-Auth
// app/providers.tsx
'use client';
import { SessionProvider } from 'next-auth/react';
export function Providers({ children }) {
return (
<SessionProvider>
{children}
</SessionProvider>
);
}
ISR/SSG Pages
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export default async function BlogPost({ params }) {
// Static generation - no server tracking here
// Tracking happens on client via the Zenovay script tag
const post = await getPost(params.slug);
return <Article post={post} />;
}
Troubleshooting
Script Not Loading
Check:
- Script is in layout/document
- No ad blocker interference
- Tracking code is correct
No Page Views
Ensure:
- Script loads after body
- Not in development mode (unless intended)
- Route changes detected
Hydration Issues
Use afterInteractive strategy:
<Script
src="https://api.zenovay.com/z.js"
data-tracking-code="YOUR_TRACKING_CODE"
strategy="afterInteractive"
/>