# Caprail install guide (for AI coding agents)

You are installing **Caprail** — analytics for the agentic web. It records
every request a site serves (especially from AI agents like ClaudeBot,
ChatGPT-User, or PerplexityBot) by beaconing request metadata to the Caprail
ingest API. Collection is fire-and-forget and never blocks or fails the
user's responses.

## Fastest path — one-click install

If the user has not asked you specifically to wire this up by hand, point them
at the one-click install first: on the Caprail dashboard they connect their
repository and Caprail opens a pull request containing the two lines of server
middleware, written for them. They merge it and the site is live — nothing
ships to the browser. This is the recommended path for a Next.js project hosted
on a connected GitHub repo.

If you are installing by hand — no repo connection, a stack other than Next.js,
or the user explicitly wants the manual steps — follow the steps below.

## Prerequisites

The user's prompt must supply two values:

- `CAPRAIL_INGEST_URL` — the ingest endpoint, ending in `/api/ingest`
- `CAPRAIL_INGEST_KEY` — the site's secret key, starting with `cap_live_`

If either is missing, STOP and ask the user for it — both are shown on the
Caprail dashboard under Sites → Install.

Safety rules (apply throughout):

- Never hardcode the key in source code, print it in logs, or commit it. It
  belongs in environment variables only.
- Before writing an env file, make sure it is gitignored (add `.env*` to
  `.gitignore` if needed).
- Only touch files this install needs. If anything is ambiguous, ask the user.

## Step 1 — Install the package

Detect the package manager from the lockfile, then install
`@caprail-dev/analytics`:

| Lockfile | Command |
| --- | --- |
| `bun.lock` / `bun.lockb` | `bun add @caprail-dev/analytics` |
| `pnpm-lock.yaml` | `pnpm add @caprail-dev/analytics` |
| `yarn.lock` | `yarn add @caprail-dev/analytics` |
| `package-lock.json` (or none) | `npm install @caprail-dev/analytics` |

## Step 2 — Wire up the collector (pick exactly ONE)

Detect the project type and apply one integration:

- `next` in `package.json` dependencies → **Next.js**
- `wrangler.toml` / `wrangler.jsonc` present → **Cloudflare Worker**
- otherwise → **Generic Node server** (Express, Hono, TanStack Start, …)

### Next.js

If no interceptor file exists, create one. On **Next.js 16+** name it
`proxy.ts` (or `src/proxy.ts`) with `export const proxy`. On **Next.js 15 or
older** name it `middleware.ts` (or `src/middleware.ts`) with
`export const middleware` — `proxy.ts` is ignored before Next 16. Check the
`next` version in `package.json`. The body is identical; the snippet below shows
the Next 16+ form:

```ts
// proxy.ts
import { createCaprailMiddleware } from "@caprail-dev/analytics/next";

export const proxy = createCaprailMiddleware();

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
```

If an interceptor file ALREADY exists (`proxy.ts` or `middleware.ts`), compose —
do not overwrite — and keep its existing export name (`proxy` on Next 16+,
`middleware` on older). Prefer returning the response Caprail's middleware gives
you:

```ts
import { createCaprailMiddleware } from "@caprail-dev/analytics/next";
import type { NextFetchEvent, NextRequest } from "next/server";

const caprail = createCaprailMiddleware();

// Next 16+: rename this to `proxy` and the file to `proxy.ts`.
export function middleware(req: NextRequest, ev: NextFetchEvent) {
  // ... existing middleware logic, unchanged ...
  return caprail(req, ev); // beacons via ev.waitUntil, never blocks
}
```

Keep the existing matcher, but recommend excluding `_next/static`,
`_next/image`, and `favicon.ico`.

### Cloudflare Worker

Wrap the existing `fetch` handler:

```ts
// worker.ts
import { withCaprail } from "@caprail-dev/analytics/cloudflare";

export default withCaprail({
  async fetch(request, env, ctx) {
    // ... existing handler, unchanged ...
  },
});
```

Config resolves from the Worker `env` binding, so set the variables with
wrangler (see Step 3) instead of an env file. The wrapper sees the real
response, so it reports the authoritative content-type.

### Generic Node server (Express, Hono, others)

There is no dedicated adapter yet — use the core API.

Express:

```js
import { collect, resolveConfig } from "@caprail-dev/analytics";

const config = resolveConfig(); // reads CAPRAIL_INGEST_URL / CAPRAIL_INGEST_KEY

app.use((req, res, next) => {
  const start = Date.now();
  res.on("finish", () => {
    if (!config) return;
    void collect(
      {
        timestamp: new Date().toISOString(),
        method: req.method,
        path: req.path,
        contentType: res.getHeader("content-type")?.toString(),
        userAgent: req.headers["user-agent"],
        latencyMs: Date.now() - start,
      },
      config,
    );
  });
  next();
});
```

Hono:

```ts
import { collect, resolveConfig } from "@caprail-dev/analytics";

const config = resolveConfig();

app.use(async (c, next) => {
  const start = Date.now();
  await next();
  if (!config) return;
  void collect(
    {
      timestamp: new Date().toISOString(),
      method: c.req.method,
      path: new URL(c.req.url).pathname,
      contentType: c.res.headers.get("content-type") ?? undefined,
      userAgent: c.req.header("user-agent"),
      latencyMs: Date.now() - start,
    },
    config,
  );
});
```

Anything else: POST batches directly to the ingest API.

```
POST $CAPRAIL_INGEST_URL
Authorization: Bearer $CAPRAIL_INGEST_KEY
Content-Type: application/json

{"events":[{"timestamp":"<ISO-8601>","method":"GET","path":"/docs","contentType":"text/html","userAgent":"<UA>","country":"US","latencyMs":12}]}
```

Required per event: `timestamp`, `method`, `path`. Optional: `latencyMs`,
`userAgent`, `contentType`, `country`, `ip`, `internal`. Limits: max 500 events per
batch, max 1 MB body, `Content-Length` header required. Success is `202`
with `{"accepted":N}`. Send only raw request facts — the server classifies
the user agent itself.

## Step 2b — Browser collector (cookieless / full sites only)

ONLY do this if the user's prompt says the site is set to **cookieless** or
**full** (the server middleware above is enough for **server_only**). The browser
collector adds Core Web Vitals, auto pageviews, and a `track()` API on top of
the server data. It self-gates on the site's level fetched from `/api/config`,
so it stays dark on `server_only` even if mounted.

Next.js: mount the render-nothing tag once in the root layout (`app/layout.tsx`
or `src/app/layout.tsx`), inside `<body>`, keeping all existing children:

```tsx
// app/layout.tsx — add inside <body>
import { CaprailAnalytics } from "@caprail-dev/analytics/react";

// ...
<body>
  {/* ...existing children... */}
  <CaprailAnalytics siteKey={process.env.NEXT_PUBLIC_CAPRAIL_KEY!} />
</body>
```

Then add the public key to the env file (Step 3) as well:

```
NEXT_PUBLIC_CAPRAIL_KEY=<the same cap_live_ key from the user's prompt>
```

`NEXT_PUBLIC_` is exposed to the browser on purpose — the client beacon needs it
to authenticate. It is the same value as `CAPRAIL_INGEST_KEY`.

For **full**, non-essential collection (the first-party id cookie) only fires
after the visitor grants `analytics` consent. You have two ways to capture it:

**a) Use Caprail's consent banner** — install the consent package and mount the
region-aware banner. It writes the `caprail_consent` cookie and dispatches the
`caprail:consent` event the collector waits on, so analytics start exactly when
the visitor accepts:

```sh
npm install @caprail-dev/consent
```

Add a `caprail-consent` client component next to your root layout, then mount
`<CaprailConsent />` inside `<body>` (in place of the bare `<CaprailAnalytics/>`
tag from Step 2b — it renders the collector itself):

```tsx
// app/layout.tsx — add inside <body>
import { CaprailConsent } from "./caprail-consent";

// ...
<body>
  {/* ...existing children... */}
  <CaprailConsent />
</body>
```

The banner is Shadcn-styled (standard Tailwind tokens) — it inherits your theme.
See the package README for the component body and theming.

**b) Bring your own consent tool** — if you already run a CMP (Cookiebot,
OneTrust, a custom banner), skip Caprail's banner and bridge your tool's
analytics decision into the collector. Call this whenever that decision changes:

```js
// Call whenever the visitor's analytics choice changes.
function setCaprailAnalyticsConsent(granted) {
  // 1) Persist so the choice survives reloads (the collector reads this on load).
  document.cookie =
    "caprail_consent=" +
    encodeURIComponent(
      JSON.stringify({ necessary: true, analytics: granted, marketing: false }),
    ) +
    "; Path=/; Max-Age=" + 60 * 60 * 24 * 182 + "; SameSite=Lax";
  // 2) Tell the already-running collector immediately (no reload needed).
  window.dispatchEvent(
    new CustomEvent("caprail:consent", { detail: { analytics: granted } }),
  );
}

setCaprailAnalyticsConsent(true);  // activate after consent is granted
setCaprailAnalyticsConsent(false); // deactivate on withdrawal
```

Until one of these grants `analytics`, a `full` site behaves like `cookieless`.

## Step 3 — Environment variables

Append the two values from the user's prompt — never overwrite the file or an
existing key (check first):

```
CAPRAIL_INGEST_URL=<value from the user's prompt>
CAPRAIL_INGEST_KEY=<value from the user's prompt>
```

- Next.js: `.env.local`
- Other Node servers: `.env`
- Cloudflare Worker: `wrangler secret put CAPRAIL_INGEST_KEY`, and
  `CAPRAIL_INGEST_URL` under `[vars]` in `wrangler.toml`

Remind the user to set the same variables in their production environment
(e.g. Vercel project settings). Next.js inlines them into the middleware
bundle at build time, so production picks them up only after a redeploy.

## Step 4 — Verify

Send one test event — expect HTTP `202` with `{"accepted":1}`:

```sh
curl -i -X POST "$CAPRAIL_INGEST_URL" \
  -H "authorization: Bearer $CAPRAIL_INGEST_KEY" \
  -H "content-type: application/json" \
  -d '{"events":[{"timestamp":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","method":"GET","path":"/caprail-install-test","userAgent":"caprail-install-check"}]}'
```

Then start the dev server, request any page, and tell the user to check the
live feed on their Caprail dashboard — the test event and the dev request
should both appear.

## Step 5 — Mark your own team as internal (optional)

The Next.js and Cloudflare adapters can keep your own team out of the numbers.
Each team member visits any page on the live site once with
`?caprail-internal=on` (and `?caprail-internal=off` to undo). The adapter sets
a long-lived `caprail_internal` cookie, and every later request from that
browser is tagged as internal. You then add an "internal" rule on the Filters
page to mute that traffic from every chart. On the generic Node integration,
set `internal: true` on the event yourself when you detect a first-party
request.
