# Express Installation Guide

> Install Virent Human Analytics and AI Crawler Analytics on Express.

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

This is the canonical Virent installation guide for Express applications.

Coverage limits: crawler middleware observes only requests handled by this
Node.js process. Pages served by a CDN or static host in front of Express need
provider logs. Consent, Do Not Track (opt-in) and repeated-initialization rules
are in [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 Express website.

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

Install two separate things:
1. Human Analytics (browser): Place the script in the common EJS, Pug, Handlebars, or HTML layout.
2. AI Crawler Analytics (server): Add an Express middleware that posts crawler request metadata after responses finish.

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:
- Add the browser script to the shared HTML template or layout rendered by Express.
- Keep the script in the head or at the end of body consistently across all routes.
- Use the npm SDK in browser bundles only for custom events.

AI Crawler Analytics best practices:
- Register the Virent middleware before route handlers so every page request can be observed.
- Send telemetry after the response finishes so the final HTTP status code is accurate.
- Skip static asset middleware paths and health checks.

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:
- Render a page and view source for the Virent script.
- Open the page in a browser and check Network requests.
- Refresh Human Analytics status.
- Run the Express server.
- Request a page with curl -A "GPTBot".
- Check server logs and refresh Virent status.
```

## Research Notes

- Express middleware is registered with `app.use()` and runs in registration order.
- Middleware should call `next()` unless it intentionally ends the request-response cycle.
- Express has no fixed template system; Human Analytics belongs in the shared template or HTML partial used by the app.

Primary references:

- https://expressjs.com/en/5x/guide/writing-middleware/

## Human Analytics

Add the script to your shared template.

```html
<!-- views/layout.ejs, views/partials/head.ejs, or your shared template -->
<head>
  <meta charset="utf-8" />
  <script
    defer
    data-write-key="vha_pk_..."
    src="https://app.virent.app/js/script.js"
  ></script>
</head>
```

Best practices:

- Add the script to the common layout used by all public pages.
- If Express serves a React/Vue bundle, edit the static app `index.html` instead.
- Use the npm SDK only in browser assets for custom events.
- Update Helmet or reverse proxy CSP headers if present.

Verification:

- Render a page and view source for the Virent script.
- Open the page in a browser and check Network requests.
- Refresh Human Analytics status.

Troubleshooting:

- If only some pages track, identify additional layouts.
- If CSP blocks Virent, update Helmet or proxy headers.
- If custom event code runs on the server, move it into browser assets.

### 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.

With nginx, add these locations to the site's server block:

```text
location = /_vr/js/script.js {
  proxy_pass https://app.virent.app/js/script.js;
  proxy_set_header Host app.virent.app;
  proxy_ssl_server_name on;
  proxy_set_header Cookie "";
}

location /_vr/v1/ {
  proxy_pass https://app.virent.app/v1/;
  proxy_set_header Host app.virent.app;
  proxy_ssl_server_name on;
  proxy_set_header Cookie "";
  proxy_set_header Authorization "";
  proxy_set_header X-Virent-Forwarded-For $remote_addr;
  # Your website's secret key: Virent trusts the forwarded IP only with it.
  proxy_set_header X-Virent-Proxy-Key "vha_sk_...";
}
```

On Cloudflare, deploy a Worker on the `example.com/_vr/*` route that copies only `accept-language`, `content-type`, `origin` and `user-agent`, sets `x-virent-forwarded-for` from `cf-connecting-ip`, `x-virent-forwarded-country` from `request.cf.country` and `x-virent-proxy-key` from a Worker secret holding your website's secret key, and fetches the same path without `/_vr` from `https://app.virent.app`. The dashboard install panel has the full Worker.

Then load the script from your domain:

```html
<script
  defer
  data-api="/_vr"
  data-write-key="vha_pk_..."
  src="/_vr/js/script.js"
></script>
```

## AI Analytics

Set server environment values:

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

Register middleware before page routes.

```ts
import type { NextFunction, Request, Response } from "express"

const staticAssetPattern = /\.(?:png|jpg|jpeg|gif|svg|ico|webp|css|js|map|txt|xml)$/i
const aiCrawlerPattern = /GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|claudebot|Claude-Web|Claude-User|Claude-SearchBot|PerplexityBot|PerplexityBot\/|Perplexity-User|Perplexity-User\/|Google-CloudVertexBot|DeepSeekBot|DeepSeekSpider|CCBot|Bytespider|meta-externalagent|meta-webindexer|meta-externalfetcher|Amazonbot|DuckAssistBot|YouBot/i

export function virentCrawlerAnalytics(req: Request, res: Response, next: NextFunction) {
  res.on("finish", () => {
    const userAgent = req.get("user-agent") ?? ""

    if (staticAssetPattern.test(req.path) || !aiCrawlerPattern.test(userAgent)) {
      return
    }

    void fetch(process.env.VIRENT_INGEST_ENDPOINT!, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "x-api-key": process.env.VIRENT_INGEST_SECRET!,
      },
      body: JSON.stringify({
        siteId: process.env.VIRENT_SITE_ID!,
        url: `${req.protocol}://${req.get("host")}${req.originalUrl}`,
        host: req.get("host"),
        path: req.path,
        method: req.method,
        referer: req.get("referer") ?? null,
        userAgent,
        statusCode: res.statusCode,
        requestId: crypto.randomUUID(),
        timestamp: new Date().toISOString(),
      }),
    }).catch((error) => {
      console.error("Virent crawler analytics failed", error)
    })
  })

  next()
}

app.use(virentCrawlerAnalytics)
```

Best practices:

- Register before route handlers.
- Send after `finish` so the final response status is available.
- Skip static assets and health checks.
- Node 18+ includes `fetch`; older runtimes need an HTTP client.

Verification:

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

- Confirm Express logs the request and posts to Virent.
- Refresh AI Analytics status.

Troubleshooting:

- If no events arrive, confirm `app.use(virentCrawlerAnalytics)` runs before routes.
- If static files are counted, add extension exclusions.
- If `fetch` is undefined, upgrade Node or use the app's existing HTTP client.
