docs

how it counts

visitors
a salted hash of site, day, IP and user agent. the day is part of the hash, so tuesday's visitor and wednesday's visitor are two unrelated strings and nothing on our side can join them.
sessions
page views from one visitor less than 30 minutes apart.
what's skipped
non-GET requests, non-HTML responses, known bots, asset paths, and any status outside 2xx, redirects and 404. all optional.

sveltekit

install @addilytics/sveltekit and export the handle from your hooks file. on cloudflare it hands delivery to waitUntil so the response isn't held up.

// src/hooks.server.ts
import { addilytics } from '@addilytics/sveltekit';
import { env } from '$env/dynamic/private';

export const handle = addilytics({
	endpoint: 'https://addilytics.app',
	siteKey: env.ADDILYTICS_KEY
});

bundled navigation

use client mode when a cdn or prerendering serves pages without your server ever seeing them. the browser helper posts a small event to your own origin, and your server attaches the visitor hash before sending it on.

// src/hooks.server.ts
export const handle = addilytics({
	endpoint: 'https://addilytics.app',
	siteKey: env.ADDILYTICS_KEY,
	mode: 'client'
});

// src/routes/+layout.svelte (instance script)
import { trackAddilyticsNavigations } from '@addilytics/sveltekit/browser';

trackAddilyticsNavigations({ mode: 'client' });
					

use hybrid when every first document reaches the server. the server counts that request and the browser counts SPA navigations after it. hash-only changes don't count unless you turn them on. keep the mode and relay path identical on both sides or you'll double count.

next.js App Router always uses client mode, since its middleware can't see the final response. a browser navigation has no real http status, so we store 0 and skip the status filter for it.

all of this ships through your normal build. there is no CDN loader to block or tamper with. the browser payload carries no site key, and the relay only accepts small same-origin posts.

wintercg runtimes

workers, deno, bun, vercel edge. if it has a fetch(request, env, ctx) handler, wrap it.

import { withAddilytics } from '@addilytics/wintercg';

export default {
	fetch: withAddilytics(handler, {
		endpoint: 'https://addilytics.app',
		siteKey: 'ak_...'
	})
};

framework adapters

each adapter reads the final response through that framework's public hook and skips json, redirects, bots and data requests.

@addilytics/astro
outermost Astro middleware plus astro:page-load tracking.
@addilytics/hono
outermost Hono middleware with a same-origin browser relay.
@addilytics/react-router
Framework Mode middleware and committed-location tracking.
@addilytics/nextjs
Route Handler wrappers and committed App Router tracking.
@addilytics/nuxt
Nitro response capture and Nuxt page:finish tracking.
@addilytics/express
middleware that reads final metadata after the response finish event.
@addilytics/fastify
a plugin that reads final metadata in onResponse.

same rule as sveltekit. client mode for prerendered pages and CDN hits, hybrid when the server always sees the first document. data loads and prefetches never count as a page view.

anything else

the core package runs on any javascript server. or skip the packages and post json straight to the ingest endpoint with your site key as a bearer token.

POST https://addilytics.app/api/ingest
authorization: Bearer ak_...
content-type: application/json

{
	"path": "/blog/hello",
	"status": 200,
	"referrer": "https://news.ycombinator.com/",
	"userAgent": "...",
	"language": "en-US,en;q=0.9",
	"ip": "203.0.113.4",
	"country": "US",
	"host": "example.com"
}

custom events

page views are automatic. for anything else, call track from a server action. props are optional.

// src/hooks.server.ts
export const handle = addilytics({ endpoint: 'https://addilytics.app', siteKey: env.ADDILYTICS_KEY });

// src/routes/signup/+page.server.ts
import { handle } from '../../hooks.server';

export const actions = {
	default: async (event) => {
		await createAccount(event);
		await handle.track(event, 'signup', { props: { plan: 'free' } });
	}
};

the wintercg wrapper exposes fetch.client.track. on the raw endpoint, set a name field.

utm tags

nothing to configure. utm_source, utm_medium, utm_campaign, utm_term and utm_content get their own breakdowns and filters. the hook strips every other query parameter before the event leaves your server, so ids and tokens in urls never reach us.

funnels

an ordered list of pages and events. a visitor counts for a step only after hitting the previous one within the selected range, so the numbers can only go down as you read left to right.

active users

fingerprints rotate daily, so on their own they can't follow a person across days. if your app has accounts, tell the hook who's signed in and the users tab starts counting daily, weekly and monthly actives plus weekly retention.

// src/hooks.server.ts
export const handle = addilytics({
	endpoint: 'https://addilytics.app',
	siteKey: env.ADDILYTICS_KEY,
	user: (event) => event.locals.user?.id
});

// per call, if you'd rather
await handle.track(event, 'upgrade', { userId: session.userId });

the hook hashes the id with your site key before it leaves your server, and the ingest endpoint hashes it again on arrival, so the original never reaches us. the core package takes the same callback as identify. on the raw endpoint, set a userId field. we keep one row per user per day, so this history survives event pruning.

event properties

click an event in the breakdown and each prop key gets its own breakdown. strings, numbers and booleans work. nested objects get dropped.

public dashboards

one read-only link per site, from settings. you pick which breakdowns are visible. totals and the chart always are. the link is noindex and dies the moment you revoke it.

retention and rollups

a nightly job rolls each finished utc day into daily counts, then prunes raw events past the retention window. unfiltered ranges read the rollups, so they go back as far as you've had the site. filters and the 24 hour range need raw events, so they stop at the retention window.

options

mode
server, hybrid or client. server is the default.
relayPath
same-origin browser endpoint. defaults to /__addilytics.
ignorePaths
extra strings or regexes to skip.
trackBots
count known crawlers. off by default.
trackStatuses
which server statuses count. defaults to 2xx, 3xx redirects and 404.
trustProxy
trust forwarded IP and country headers.
deliveryTimeoutMs
max ingest wait. defaults to 5000.
onError
receives delivery and filtering errors.
fetch
override the fetch used to send events.

ready? add a site.