[invisit](https://invisit.dev) · [Docs](https://invisit.dev/docs) · [llms.txt](https://invisit.dev/llms.txt) · [llms-full.txt](https://invisit.dev/llms-full.txt)

# @invisitdev/server reference

> Every function, option and error code in @invisitdev/server, the first-party proxy, crawler analytics and server-side events client.

Source: https://invisit.dev/docs/reference/server

```ts title="@invisitdev/server"
import { clientIp, createInvisit, InvisitError } from '@invisitdev/server'
```

## createInvisit

```ts title="@invisitdev/server"
function createInvisit(options: InvisitOptions): Invisit
```

| Option       | Type                                   | Default                   | What it is                                                                                                             |
| ------------ | -------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `key`        | `string`                               | required                  | An API key of the site with the ingest scope (`invisit_sk_…`).                                                         |
| `baseUrl`    | `string`                               | `https://api.invisit.dev` | The collector. Leave it unless support gives you another address.                                                      |
| `clientIp`   | `(request: Request) => string \| null` | [`clientIp`](#clientip)   | Reads the visitor's address from a request.                                                                            |
| `fetch`      | `typeof fetch`                         | global `fetch`            | The fetch used to reach the collector, for instance to instrument it.                                                  |
| `onError`    | `(error: unknown) => void`             | `console.error`           | Receives failures that are not thrown: proxy 502s, crawler reads not accepted, `CLIENT_IP_UNRESOLVED` once per client. |
| `respectGpc` | `boolean`                              | `false`                   | Turned on, leaves out events of visitors whose browser sends Global Privacy Control.                                   |

Throws a `TypeError` when the key is missing or empty, or is not an `invisit_sk_` key. Requests to the collector time out after 10 seconds.

## handle

```ts title="@invisitdev/server"
handle(request: Request, options?: HandleOptions): Promise<Response | null>
```

The middleware one-liner. Proxies tracker requests and resolves to their response; for every other request it records a crawler read in the background and resolves to `null`.

| Option      | Type                                  | What it is                                                                |
| ----------- | ------------------------------------- | ------------------------------------------------------------------------- |
| `prefix`    | `string`                              | The path the tracker is served under. Default `'/_i'`.                    |
| `ip`        | `string \| null`                      | The visitor's address for this request. Overrides `clientIp`.             |
| `waitUntil` | `(promise: Promise<unknown>) => void` | Keeps the crawler read alive after the response, such as `ctx.waitUntil`. |

`handle` runs before your app, so crawler reads are recorded with status 200. Use `proxy` and `crawlers` separately for the real status.

## proxy

```ts title="@invisitdev/server"
proxy(request: Request, options?: { prefix?: string; ip?: string | null }): Promise<Response | null>
```

Forwards a tracker request under `prefix` to the collector and resolves to its response, or to `null` for any other path. It never rejects:

| Situation                           | Response                               |
| ----------------------------------- | -------------------------------------- |
| Method other than GET, HEAD or POST | `405`                                  |
| Body larger than 1 MiB              | `413`                                  |
| Collector unreachable or timed out  | `502`, and the error goes to `onError` |

## crawlers

```ts title="@invisitdev/server"
crawlers(request: Request, options?: { status?: number }): Promise<void>
```

Records the request as a crawler read when it is one. `status` is what your server answered, `200` by default. Never rejects. See [Crawler reads](https://invisit.dev/docs/server/sdk.md#crawler-reads).

## track

```ts title="@invisitdev/server"
track(name: string, options: TrackOptions): Promise<{ accepted: number; duplicate: number }>
```

Sends a custom event for the visitor behind `options.request`, or for `options.visitor` when there is no request of theirs. Rejects with an `InvisitError`. How and when to use it: [Server-side events](https://invisit.dev/docs/server/sdk.md#server-side-events).

| Option       | Type                                                  | What it is                                                                                                               |
| ------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `request`    | `Request`                                             | The visitor's own request; their address, user agent, `Accept-Language` and `Sec-GPC` come from it.                      |
| `ip`         | `string \| null`                                      | With `request`: the visitor's address when no header carries it. Overrides `clientIp`.                                   |
| `visitor`    | `{ ip, userAgent, language?, gpc? }`                  | Instead of `request`, when only the visitor's details were kept (a Convex action, a queue consumer, a WebSocket server). |
| `url`        | `string`                                              | The page the event belongs to.                                                                                           |
| `properties` | `Record<string, string \| number \| boolean \| null>` | Event properties, under the [browser's rules](https://invisit.dev/docs/guides/events.md#names-and-properties).                                 |
| `userId`     | `string`                                              | Your id for the signed-in user, stored hashed.                                                                           |
| `eventId`    | `string`                                              | A UUID v7. Reuse it when retrying so the event counts once. Generated when absent.                                       |

Pass `request` or `visitor`, never both. A `visitor` whose `ip` is not an IP address rejects with `CLIENT_IP_UNRESOLVED`.

## clientIp

```ts title="@invisitdev/server"
function clientIp(request: Request): string | null
```

The default address reader: the first present of `CF-Connecting-IP`, `X-Real-IP`, `Fly-Client-IP`, `True-Client-IP`, `X-Vercel-Forwarded-For` and the first entry of `X-Forwarded-For`. A malformed value gives `null` rather than falling through to a weaker header. IPv4-mapped IPv6 is reduced to IPv4.

## @invisitdev/server/node

Adapters for Node `http`, in their own entry so the main one loads on edge runtimes.

| Export                          | What it does                                                                                                                               |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `middleware(invisit, options?)` | Express and Connect middleware: proxies tracker requests and records crawler reads with your app's real status once the response finishes. |
| `toRequest(req, options?)`      | An `IncomingMessage` as a web `Request`. Reads the body of anything but GET and HEAD, up to 1 MiB.                                         |
| `sendResponse(res, response)`   | Writes a web `Response` to a `ServerResponse`, streaming with backpressure.                                                                |

`middleware` and `toRequest` take `{ trustProxy?: boolean }`, and `middleware` also takes `{ prefix?: string }`. Without `trustProxy` they use the socket address and drop forwarding headers.

## Errors

`track` rejects with an `InvisitError`; `proxy`, `handle` and `crawlers` never reject and report to `onError`.

```ts title="src/routes/checkout.ts"
import { InvisitError } from '@invisitdev/server'

try {
  await invisit.track('purchase', { request })
} catch (error) {
  if (error instanceof InvisitError && error.code === 'RATE_LIMITED') {
    // back off
  }
}
```

| Property    | Type                      | What it is                                            |
| ----------- | ------------------------- | ----------------------------------------------------- |
| `code`      | `string`                  | A stable, machine-readable code.                      |
| `message`   | `string`                  | A human-readable explanation.                         |
| `status`    | `number \| null`          | The collector's HTTP status, `null` with no response. |
| `requestId` | `string \| null`          | The collector's request id. Quote it to support.      |
| `details`   | `Record<string, unknown>` | Extra context, such as validation details.            |

| Code                   | Raised when                                                                     |
| ---------------------- | ------------------------------------------------------------------------------- |
| `VALIDATION_FAILED`    | The collector rejected the event's shape or values.                             |
| `UNAUTHENTICATED`      | The API key is wrong, revoked, or lacks the ingest scope.                       |
| `RATE_LIMITED`         | Too many requests. Retry later.                                                 |
| `NETWORK_ERROR`        | The collector could not be reached or timed out. The cause is in `error.cause`. |
| `CLIENT_IP_UNRESOLVED` | No visitor address could be read. Pass `ip`, or a `clientIp` reader.            |
| `USER_AGENT_MISSING`   | The request has no `User-Agent` header.                                         |
| `HTTP_<status>`        | The collector answered with an error and no recognizable body.                  |

The Node adapter adds `BODY_CONSUMED` (a body parser ran first) and `PAYLOAD_TOO_LARGE` (a body over 1 MiB).

## Troubleshooting

- **`createInvisit: no key given`.** The variable is missing where the code runs: check build steps, preview deploys and your runtime's secret store.
- **`key must be an API key with the ingest scope`.** You passed the site id. The server needs an `invisit_sk_…` key; the site id belongs in the browser.
- **Tracker POSTs get a 403.** Your framework's CSRF check rejects them; see the SvelteKit and Astro notes in the [server frameworks](https://invisit.dev/docs/server/frameworks.md).
- **Crawler reads all show status 200.** You are using `handle`, which runs before the page. Use `proxy` before and `crawlers` after your app.
