# Next.js Installation Guide

> Install Virent Human Analytics and AI Crawler Analytics on Next.js.

Canonical: https://virent.app/docs/installation/nextjs
Last updated: 2026-08-26

This is the canonical Virent installation guide for Next.js projects. It covers Human Analytics browser tracking and AI Analytics server-side crawler tracking.

Coverage limits: `proxy.ts` runs on Node.js before rendering, so the final
response status is unknown, and requests answered from a CDN cache never reach
it. Import `@virent.app/sdk/next` only from server files; from SDK 0.4.0 a client
bundle that imports it fails to load. Edge isolates need provider logs for
crawler coverage. See [Supported runtimes and coverage limits](/docs/installation/runtimes-and-coverage).

### Install with your coding agent

Paste this prompt into Claude Code, Codex, Cursor or another coding agent.

```text
Install Virent analytics on my Next.js website.

Read the canonical guide first and follow its code exactly:
https://virent.app/docs/installation/nextjs.md

Install two separate things:
1. Human Analytics (browser): Add the Virent browser script through next/script in the root App Router layout.
2. AI Crawler Analytics (server): Install the SDK helper, set server-only environment values, and add a root proxy.ts file.

Values I will give you (they are in Virent under Settings > Connection):
- Publishable browser key, starts with vha_pk_. Safe in browser code.
- Site ID (VIRENT_SITE_ID) and server secret (VIRENT_INGEST_SECRET, starts with vha_sk_). Server only.
Ask me for them. Never invent a key or use a masked preview such as vha_sk_••••.

Human Analytics best practices:
- Install the browser script once in app/layout.tsx so it loads across all App Router routes.
- Use next/script instead of a raw script tag when working inside a Next.js layout.
- Keep the publishable key in the data-write-key attribute; it is safe for the browser, unlike the crawler secret.

AI Crawler Analytics best practices:
- Use proxy.ts for Next.js 16+ projects and keep the matcher narrow enough to exclude API routes, static assets, images, robots.txt, sitemaps, and common files.
- Store the crawler secret in server environment variables only. Never expose it through NEXT_PUBLIC_ variables or client bundles.
- Capture crawler requests before rendering so bots that never execute JavaScript are still measured.

Rules:
- Keep VIRENT_INGEST_SECRET in server environment variables. Never put it in client code, public env variables (NEXT_PUBLIC_, VITE_, PUBLIC_) or committed files.
- Exclude API routes, static assets, images, favicons, robots.txt and sitemaps from crawler capture.
- If the site sends a Content-Security-Policy, allow https://app.virent.app in script-src and connect-src.
- Do not change unrelated code.

When you finish, list the files you changed and how I verify each path:
- Open a production or preview page and check Network for /js/script.js.
- Confirm a pageview request is sent after navigation.
- Refresh Virent installation status and confirm Human Analytics changes to receiving events.
- Run the app and request a page with curl -A "GPTBot".
- Refresh Virent installation status and confirm AI Analytics changes to receiving crawler events.
- Inspect server logs for failed outbound requests to the Virent ingest endpoint.
```

## Research Notes

- Next.js 16 App Router supports global third-party scripts through `next/script` in layouts.
- Next.js 16 renamed the `middleware.ts` file convention to `proxy.ts`. New AI Analytics installs should use `proxy.ts`.
- Proxy runs before routes render and should use a matcher that excludes static assets and framework internals.

Primary references:

- https://nextjs.org/docs/app/guides/scripts
- https://nextjs.org/docs/app/api-reference/file-conventions/proxy

## Human Analytics

Install Human Analytics in `app/layout.tsx` so the browser script loads on every App Router route.

```tsx
import Script from "next/script"

export default function RootLayout({ children }) {
  return (
    <html lang="en">
	  <body>
		{children}
		<Script
		  src="https://app.virent.app/js/script.js"
		  data-write-key="vha_pk_..."
		  strategy="afterInteractive"
		/>
	  </body>
    </html>
  )
}
```

Use the npm SDK only when custom product events, identities, or goals are needed.

```sh
pnpm add @virent.app/sdk@^0.3.0
```

```ts
import { identify, initVirent, pageview, trackEvent, trackGoal } from "@virent.app/sdk/browser"

initVirent({ writeKey: "vha_pk_..." })

await pageview({ path: "/pricing" })
await trackEvent({ name: "signup_started", properties: { plan: "pro" } })
await identify({ userId: "customer_123", traits: { plan: "pro" } })
await trackGoal({ name: "signup_completed", idempotencyKey: "order_123" })
```

Best practices:

- Use `next/script` instead of a raw script tag in React layouts.
- Keep `strategy="afterInteractive"` for normal analytics so hydration is not blocked.
- Keep the publishable key in `data-write-key`; do not use the server secret in client code.
- If Content Security Policy is enabled, allow the Virent origin in `script-src` and `connect-src`.

Verification:

- Open a production or preview page and verify `/js/script.js` loads.
- Navigate between App Router routes and confirm pageview requests are sent.
- Refresh Virent installation status and confirm Human Analytics is receiving events.

Troubleshooting:

- If the script is absent, confirm it is in the root layout used by the route.
- If the browser blocks the script, update CSP in `next.config.js`, `vercel.json`, `proxy.ts`, or the hosting platform.
- Localhost traffic is skipped by default. Add `data-allow-localhost="true"` only for intentional local testing.

### First-party (recommended)

Ad blockers and privacy tools often block third-party analytics hosts, which silently drops visits. Relay the script and events through your own domain under `/_vr`, and load the script with `data-api="/_vr"` so events go to your domain too. The relay must forward the visitor's IP and country in `x-virent-forwarded-for` and `x-virent-forwarded-country`, with your website's secret key (`vha_sk_…`) in `x-virent-proxy-key`; without the key Virent ignores the forwarded headers and sees your server's network instead of your visitors'. Relay only `/_vr/js/script.js` and `/_vr/v1/*`, and never forward cookies or authorization headers. See [First-party analytics](/docs/installation/first-party) for how Virent trusts these headers.

A `rewrites()` entry in `next.config.js` cannot add request headers, so the relay is a rewrite in `proxy.ts`. This file replaces the crawler-only `proxy.ts` from AI Analytics below.

```ts
import { createVirentBotProxy } from "@virent.app/sdk/next"
import { type NextFetchEvent, type NextRequest, NextResponse } from "next/server"

const virentOrigin = "https://app.virent.app"
const relayedHeaders = ["accept-language", "content-type", "origin", "user-agent"]

const trackBots = createVirentBotProxy({
  siteId: process.env.VIRENT_SITE_ID!,
  writeKey: process.env.VIRENT_INGEST_SECRET!,
  endpoint: process.env.VIRENT_INGEST_ENDPOINT || "https://app.virent.app/v1/ingest/bot",
  trustedProxy: "vercel",
})

export function proxy(request: NextRequest, event: NextFetchEvent) {
  const { pathname, search } = request.nextUrl

  if (pathname.startsWith("/_vr/")) {
    const headers = new Headers()
    for (const name of relayedHeaders) {
      const value = request.headers.get(name)
      if (value) headers.set(name, value)
    }
    const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim()
    const country = request.headers.get("x-vercel-ip-country")
    if (ip) headers.set("x-virent-forwarded-for", ip)
    if (country) headers.set("x-virent-forwarded-country", country)
    // The secret key lets Virent trust the forwarded IP and country.
    headers.set("x-virent-proxy-key", process.env.VIRENT_INGEST_SECRET!)

    return NextResponse.rewrite(
      new URL(pathname.slice("/_vr".length) + search, virentOrigin),
      { request: { headers } }
    )
  }

  trackBots(request, event)
}

export const config = {
  matcher: [
    "/_vr/:path*",
    "/((?!api|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|.*\\.(?:png|jpg|jpeg|gif|svg|ico|webp|css|js|map)$).*)",
  ],
}
```

Then load the script from your domain:

```tsx
<Script
  src="/_vr/js/script.js"
  data-api="/_vr"
  data-write-key="vha_pk_..."
  strategy="afterInteractive"
/>
```

With the browser SDK, pass `endpoint: "/_vr/v1/ingest/batch"`, `identifyEndpoint: "/_vr/v1/identify"` and `goalEndpoint: "/_vr/v1/track/goal"` to `initVirent`. The SDK route `createVirentAnalyticsProxy` (secret key, `/vt/` paths) remains an alternative.

## AI Analytics

Install AI Analytics in `proxy.ts` at the project root or `src/proxy.ts` if the app uses a `src` directory.

```sh
pnpm add @virent.app/sdk@^0.3.0
```

```env
VIRENT_SITE_ID=site_...
VIRENT_INGEST_SECRET=vha_sk_...
VIRENT_INGEST_ENDPOINT=https://app.virent.app/v1/ingest/bot
```

```ts
// proxy.ts
import { createVirentBotProxy } from "@virent.app/sdk/next"

export const proxy = createVirentBotProxy({
  siteId: "site_...",
  writeKey: process.env.VIRENT_INGEST_SECRET!,
  trustedProxy: "vercel",
})

export const config = {
  matcher: [
    "/((?!api|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|.*\\.(?:png|jpg|jpeg|gif|svg|ico|webp|css|js|map)$).*)",
  ],
}
```

Best practices:

- Use `proxy.ts` for Next.js 16+ rather than creating new `middleware.ts` code.
- Keep the matcher exclusions for API routes, static assets, image optimization, favicons, robots, sitemaps, and common file extensions.
- Store `VIRENT_INGEST_SECRET` only in server environment variables.
- Use `trustedProxy: "vercel"` on Vercel so the SDK can hash the real client IP without trusting arbitrary forwarding headers elsewhere.
- Do not depend on React component state or mutable globals from Proxy.

Verification:

```sh
curl -A "GPTBot" https://your-domain.com/
```

- Confirm the request reaches `proxy.ts`.
- Confirm Virent receives crawler events.
- Refresh Virent installation status and confirm AI Analytics is receiving crawler events.

Troubleshooting:

- If every static asset creates events, tighten the matcher exclusions.
- If no events arrive, confirm `proxy.ts` is at the same level as `app` or `pages`.
- If the secret is masked in the UI, rotate and reveal the key before installing.
