import { clientIp, createInvisit, InvisitError } from '@invisitdev/server'createInvisit
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 |
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
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
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
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.
track
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.
| 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. |
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
function clientIp(request: Request): string | nullThe 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.
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 aninvisit_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.
- Crawler reads all show status 200. You are using
handle, which runs before the page. Useproxybefore andcrawlersafter your app.