MODULES / ANALYTICS
Analytics
Prov0.1.0Privacy-friendly product analytics in your own database: page views and custom events from the browser and server, every module event recorded automatically, and a dashboard with trends, top events and pages, funnels and weekly retention cohorts.
terminalnpx ajolla add analyticsWhat you get
- Drop-in <AjollaAnalytics /> component: page views and track() in under 1 KB of client code, no cookies, honours Do Not Track
- Server-side track() for webhooks, jobs and server actions
- Every Ajolla module event recorded automatically (forms.submitted, booking.confirmed…)
- Rollups maintained on write, so dashboards never scan raw events
- Dashboard: visitors, page views and events with period-over-period change, accessible SVG charts
- Top events and top pages, ordered funnels with conversion and drop-off
- Weekly retention cohorts
- Bot filtering, per-client rate limits, payload caps and a retention purge cron
Routes it adds
/admin/analytics
/api/ajolla/analytics/purge/api/ajolla/analytics/track
Plus 1 Supabase migration with row-level security, and an AGENTS.md your coding agent reads after install.
Installs with
Kernel (added automatically).
Configuration
- ANALYTICS_RETENTION_DAYS
- Delete raw events (and per-visitor de-duplication rows) older than this many days when the purge cron runs. Default 90; 0 keeps them forever. Daily rollups are always kept.
- ANALYTICS_IGNORE_EVENTS
- Comma-separated module event types not to record, e.g. "notifications.requested,search.*".
- ANALYTICS_SALT
- Random secret mixed into the daily-rotating client hashes used for rate limiting. Falls back to one derived from SUPABASE_SECRET_KEY.
- ANALYTICS_RATE_LIMIT_PER_MINUTE
- Browser events accepted per client per minute (default 120).
Use it from any stack
Ajolla Hosted runs the same code as a multi-tenant API. Create an app in your dashboard, enable Analytics, and call /api/m/analytics/v1 with your app key.
OpenAPI document — feed it to Ajolla to generate SDKs in 7 languages and an MCP server.
AGENTS.md
Read the instructions installed with this module
markdown# Ajolla Analytics
Privacy-friendly product analytics in your own Supabase database. Page views and custom events from the browser and the server, every Ajolla module event recorded automatically, and a dashboard with trends, top events and pages, funnels and weekly retention cohorts. No cookies, no third-party scripts.
## What was installed
- **Admin page** `/admin/analytics` (permission `analytics.read`):
- date range presets (7 / 30 / 90 days, 12 months) and a custom UTC range (max 731 days);
- KPI tiles (unique visitors, page views, events, views per visitor) with change vs the previous period; clicking a tile charts it;
- line chart per day / week / month (inline SVG, crosshair readout on hover and arrow keys, `<title>` tooltips, data table);
- top events (share and sparkline; click one to chart it alone) and top pages;
- funnel builder (2–6 ordered steps, conversion window 1 hour – 30 days, conversion and drop-off per step);
- weekly retention cohort table; latest raw events (to check your setup).
- All state is in the URL, so every view is linkable and works without JavaScript.
- **Component** `components/ajolla/analytics/analytics.tsx` (client):
- `<AjollaAnalytics />` records a `page_view` on load and on every client-side navigation;
- `track(name, properties?)` records custom events; `optOut()` / `trackingAllowed()`;
- batched with `navigator.sendBeacon` (≤ 20 events, ≤ 7 KB per request), no cookies, a random anonymous id in `localStorage`, nothing sent with Do Not Track / Global Privacy Control on.
- **Server helper** `lib/ajolla/analytics/server.ts`: `track(name, { userId?, anonymousId?, properties?, path? })` and `trackMany([...])` for server actions, webhooks and jobs.
- **Subscriber** `lib/ajolla/subscribers/analytics.ts` (`"*"`): every module event (`forms.submitted`, `booking.confirmed`…) becomes an analytics event named after its type, the actor as the visitor, the payload reduced to small scalar properties. Idempotent per event id.
- **Routes**
- `POST /api/ajolla/analytics/track` — beacon endpoint for the component. Same-origin only. Drops bots, prefetches and DNT/GPC requests (204). Caps: 8 KB body, 20 events, `ANALYTICS_RATE_LIMIT_PER_MINUTE` events per client per minute (in-memory and database checks). Signed-in visitors are attributed from their Supabase session.
- `GET /api/ajolla/analytics/purge` — retention cron, protected by `CRON_SECRET`.
- **Tables** (all `ajolla_analytics_*`, RLS on; reading needs `analytics.read`; nobody writes through the API, only the server with the secret key):
- `events` — raw events: `occurred_at, name, source ('event'|'server'|'browser'|'api'), visitor_key, user_id, path, referrer_host, properties, ip_hash, event_id`.
- Rollups maintained by a trigger on every insert: `daily` (day × event: events, unique visitors), `daily_totals` (day: events, page views, unique visitors), `daily_pages` (day × path: views), `cohorts` (first-seen week × active week).
- De-duplication for exact unique counts: `event_visitors`, `visitor_days` (with each visitor's previous active day, so "unique visitors between A and B" is one indexed count), `visitors`.
- **Code**: `lib/ajolla/analytics/core.ts` (recording, queries, purge), `math.ts` (bucketing, funnel, cohort and formatting helpers), `ingest.ts` (bot/DNT filters, URL clean-up, beacon parsing, rate limiter), `search-params.ts`, `lib/ajolla/admin/analytics.ts`.
- **Events**: emits none; consumes every event (`"*"`).
- **Env**
- `ANALYTICS_RETENTION_DAYS` — raw events kept this many days (default 90; `0` = forever). Rollups are always kept.
- `ANALYTICS_IGNORE_EVENTS` — module events not to record, e.g. `notifications.requested,search.*`.
- `ANALYTICS_SALT` — secret for the daily-rotating client hash used for rate limiting (falls back to one derived from `SUPABASE_SECRET_KEY`).
- `ANALYTICS_RATE_LIMIT_PER_MINUTE` — default 120.
## Setup
1. Apply migrations: `supabase db push`.
2. Add the tracker to your root layout:
```tsx
import { AjollaAnalytics } from "@/components/ajolla/analytics/analytics";
// …
<body>
{children}
<AjollaAnalytics />
</body>
```
3. Schedule the purge daily in `vercel.json` (next to the kernel's events cron):
```json
{ "crons": [{ "path": "/api/ajolla/analytics/purge", "schedule": "15 3 * * *" }] }
```
4. Give someone `analytics.read`. Until Roles & Permissions is installed, admins (`app_metadata.role = "admin"`) hold it.
## Tracking events
```tsx
"use client";
import { track } from "@/components/ajolla/analytics/analytics";
<button onClick={() => track("upgrade_clicked", { plan: "pro" })}>Upgrade</button>;
```
```ts
import { track } from "@/lib/ajolla/analytics/server";
await track("invoice_paid", { userId: user.id, properties: { amount: 49, currency: "usd" } });
```
- Names: letters, digits, spaces and `_ . : / -`, at most 80 characters. `page_view` is the built-in page view.
- Properties: at most 20 strings (≤ 200 chars), numbers, booleans or nulls. Nested values are dropped, and so are keys that look like credentials or personal data (`password`, `token`, `email`, `phone`, `card`…). Don't put personal data in event names or paths either.
- Paths are stored without query strings or fragments.
## How the numbers work
- Days and weeks are UTC; weeks start on Monday.
- A visitor is the signed-in user's id, else the anonymous id. Events without either count toward totals but not unique visitors.
- Unique visitors are exact for any range inside the raw retention window. A rare late event (e.g. a module event delivered after a retry, for an earlier day) can count a visitor once more.
- Funnels read raw events (at most 20,000 per query) and count a visitor when they did every step in order within the window from step 1. Every start is tried, so an abandoned attempt doesn't hide a later conversion.
- Retention: a cohort is the week a visitor was first seen; week N shows the share of that cohort active N weeks later. Visitors who stay away longer than the retention window are treated as new when they return.
## Customising
- Dashboard layout: `app/admin/analytics/page.tsx`, `components/ajolla/analytics/dashboard.tsx`.
- Chart colours: `--an-*` variables in `components/ajolla/analytics/analytics.module.css` (chrome follows the UI kit's `--aj-*` variables, light and dark).
- Bot list: `BOT_UA` in `lib/ajolla/analytics/ingest.ts`; sensitive property keys: `SENSITIVE_PROPERTY` in `core.ts`.
- High traffic: every event updates a few rollup rows in the same transaction. For sustained thousands of events per second, batch writes (`trackMany`) or move rollups to a queue.
## Ajolla Hosted
The same module runs as a REST API (`/api/m/analytics/v1`) for any stack:
- `POST /events` (publishable or secret key) — 1–20 events per request, ≤ 8 KB. **Each request counts toward your monthly hosted request quota, so batch events.** Publishable keys are origin-checked and rate-limited per IP, can't send `userId` (send `anonymousId`), and bot / DNT requests are accepted but not stored.
- Secret-key queries: `GET /stats/overview`, `/stats/timeseries`, `/stats/top-events`, `/stats/top-pages`, `/stats/funnel?steps=a,b,c`, `/stats/retention`.
- Retention is fixed: raw events 90 days, daily rollups 400 days. Every module event of the app is recorded automatically.