Analytics
Overview
Section titled “Overview”Analytics captures product behavior: pageviews, custom events, user identification, funnels, and retention—all scoped to your organization. Reactor uses Analytics internally for the marketing site, dashboard, and SDK telemetry, but the capability is a first-class BaaS surface. Any Reactor customer can call reactor.analytics.track('signup_completed', { plan: 'pro' }) from their app.
Two ingestion modes ship at v0: anonymous (public project key for top-of-funnel) and authenticated (bearer JWT for trusted server-side enrichment). A single POST /analytics/v1/query endpoint supports aggregates, funnels, retention, and breakdowns—designed so agents and Studio learn one JSON schema.
Analytics is experimental: the ingestion path and query grammar are usable, but rollups, ClickHouse adapters, and saved insights land in v0.2.
Key features
Section titled “Key features”- Hybrid event schema — System events (
$pageview,$identify,$error) get hot columns; custom events use free-formproperties. - Anonymous + authenticated ingest — Project keys for client-side; JWT for server-side.
- Identity stitching —
$identifyand$aliasmerge anonymous IDs to user IDs. - Privacy-first defaults — IP truncated to /24, DNT honored, opt-out tombstones, GDPR erasure endpoint.
- Agent-friendly query API — One endpoint, many
kinds:events,aggregate,funnel,retention,breakdown,path. - JS SDK — Auto-pageview, auto-identify (wires to auth), auto-error capture, beacon on unload.
- Server-side tracking —
ctx.analytics.track()in Functions, Jobs, and Sites.
Quickstart
Section titled “Quickstart”Create a project, issue a public key, and track an event from the browser.
reactor auth login user@example.com --password '...'
reactor analytics projects create web-prod --name "Production Web"reactor analytics keys create web-prod --name "browser-key"
export RAPK_KEY="rapk_..." # shown once on create
reactor analytics track --key "$RAPK_KEY" \ --event signup_completed \ --properties '{"plan":"pro"}'import { createClient } from '@reactor/client';
const reactor = createClient({ url: 'https://api.reactor.cloud', key: 'rapk_...', // project public key analytics: { enabled: true, autoPageview: true, autoIdentify: true, autoErrors: true, },});
// Custom eventreactor.analytics.track('signup_completed', { plan: 'pro' });
// Manual identify (autoIdentify handles this on sign-in)reactor.analytics.identify('u_42', { email: 'user@example.com', plan: 'pro' });# Track (anonymous — project key auth)curl -s -X POST "$REACTOR_URL/analytics/v1/track" \ -H "X-Reactor-Project-Key: rapk_..." \ -H "Content-Type: application/json" \ -d '{ "event": "signup_completed", "anonymous_id": "anon_01HF...", "properties": { "plan": "pro" }, "context": { "page": { "url": "https://app.example.com/signup", "path": "/signup" } } }'How-to guides
Section titled “How-to guides”Run an aggregate query
Section titled “Run an aggregate query”Count unique users who completed checkout in the last 30 days, grouped by plan.
reactor analytics query --project web-prod --file query.jsonquery.json:
{ "kind": "aggregate", "time_range": { "last": "30d" }, "filter": { "all": [{ "event": { "eq": "checkout_completed" } }] }, "measure": "unique_users", "group_by": [{ "prop": "plan" }], "time_bucket": "1d"}await reactor.auth.signInWithPassword({ email, password });
const result = await reactor.analytics.query({ projectId: 'web-prod', kind: 'aggregate', timeRange: { last: '30d' }, filter: { all: [{ event: { eq: 'checkout_completed' } }], }, measure: 'unique_users', groupBy: [{ prop: 'plan' }], timeBucket: '1d',});console.log(result.rows);curl -s -X POST "$REACTOR_URL/analytics/v1/query" \ -H "Authorization: Bearer $REACTOR_TOKEN" \ -H "X-Reactor-Project: p_..." \ -H "Content-Type: application/json" \ -d '{ "project_id": "p_...", "kind": "aggregate", "time_range": { "last": "30d" }, "filter": { "all": [{ "event": { "eq": "checkout_completed" } }] }, "measure": "unique_users", "group_by": [{ "prop": "plan" }], "time_bucket": "1d" }'Build a signup funnel
Section titled “Build a signup funnel”const funnel = await reactor.analytics.query({ projectId: 'web-prod', kind: 'funnel', timeRange: { last: '30d' }, steps: [ { event: 'page_viewed', filter: { prop: { path: { eq: '/' } } } }, { event: 'signup_started' }, { event: 'signup_completed' }, { event: 'first_deploy' }, ], conversionWindow: '7d', groupBy: [{ prop: 'utm_source' }],});curl -s -X POST "$REACTOR_URL/analytics/v1/query" \ -H "Authorization: Bearer $REACTOR_TOKEN" \ -H "X-Reactor-Project: p_..." \ -H "Content-Type: application/json" \ -d '{ "project_id": "p_...", "kind": "funnel", "time_range": { "last": "30d" }, "steps": [ { "event": "page_viewed", "filter": { "prop": { "path": { "eq": "/" } } } }, { "event": "signup_started" }, { "event": "signup_completed" } ], "conversion_window": "7d" }'GDPR erasure for a user
Section titled “GDPR erasure for a user”reactor analytics erase web-prod --user-id u_42await reactor.analytics.erase({ projectId: 'web-prod', userId: 'u_42',});curl -s -X POST "$REACTOR_URL/analytics/v1/erase" \ -H "Authorization: Bearer $REACTOR_TOKEN" \ -H "X-Reactor-Project: p_..." \ -H "Content-Type: application/json" \ -d '{"user_id":"u_42"}'Configuration
Section titled “Configuration”[analytics]retention_days_default = 90honor_dnt = truequota_per_org_monthly = 1000000batch_interval_ms = 200batch_max_rows = 500
# Optional GeoIP database path# geo_db = "/var/lib/reactor/GeoLite2-Country.mmdb"Project keys are created via admin API—not in reactor.toml. Store the rapk_ key in your frontend environment:
NEXT_PUBLIC_REACTOR_ANALYTICS_KEY=rapk_...Sites can auto-inject the SDK snippet via manifest:
{ "analytics": { "enabled": true, "project_key": "rapk_...", "auto_pageview": true, "auto_errors": true }}Environment variables:
| Variable | Default | Description |
|---|---|---|
REACTOR_ANALYTICS_BIND | 0.0.0.0:8006 | HTTP bind |
REACTOR_ANALYTICS_RETENTION_DAYS_DEFAULT | 90 | Event retention |
REACTOR_ANALYTICS_QUOTA_PER_ORG_MONTHLY | 1000000 | Free-tier cap |
REACTOR_ANALYTICS_HONOR_DNT | 1 | Drop events when DNT/Sec-GPC set |
REACTOR_ANALYTICS_QUERY_TIMEOUT_MS | 30000 | Query statement timeout |
REACTOR_ANALYTICS_MAX_PROPERTIES_BYTES | 32768 | Per-event size cap |
Limits and quotas
Section titled “Limits and quotas”| Limit | Default | Notes |
|---|---|---|
| Events per batch | 100 | Max 1 MiB body |
| Properties + context size | 32 KiB | Per event |
| Raw query time range | 90 days | Wider ranges need rollups (v0.2) |
| Aggregate query time range | 730 days | |
| Query rows scanned | 100,000 | Hard cap per request |
| Monthly events per org | 1,000,000 | Configurable; returns 429 |
| IP storage | /24 truncated | Never raw IP |
| Custom event names | No $ prefix | Reserved for system events |
| Rollups | v0.2 deferred | v0 scans raw events only |
System events (reserved $ prefix):
| Event | Purpose |
|---|---|
$pageview | Page navigation |
$identify | User traits assert |
$alias | Merge anon → user |
$session_start / $session_end | Session boundaries |
$autocapture | Click/submit (opt-in) |
$error | Uncaught errors |
Query kinds:
| Kind | Use case |
|---|---|
events | Raw event rows (debugging, capped at 1000) |
aggregate | Counts, unique users, sums, averages |
funnel | Step conversion with window |
retention | Cohort return rate |
breakdown | Top-N by property |
path | User journey sequences |
API and SDK links
Section titled “API and SDK links”- HTTP base path:
/analytics/v1/ - OpenAPI reference: Analytics API
- JavaScript SDK:
@reactor/analytics,reactor.analytics - CLI:
reactor analytics
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /analytics/v1/track | Project key or JWT | Single event |
POST | /analytics/v1/batch | Project key or JWT | Batch ingest |
POST | /analytics/v1/identify | Project key or JWT | Identity assert |
POST | /analytics/v1/query | JWT | Run query |
POST | /analytics/v1/erase | JWT | GDPR erasure |
POST | /analytics/v1/consent/opt-out | Project key | Opt out anon ID |
Permissions:
| Permission | Scope |
|---|---|
analytics:project:create | Create projects |
analytics:{project_id}:ingest | Server-side ingest |
analytics:{project_id}:query | Run queries |
analytics:{project_id}:erase | GDPR erasure |
Troubleshooting
Section titled “Troubleshooting”Events not appearing in queries
Section titled “Events not appearing in queries”- Confirm the project key is valid and not revoked
- Check for opt-out tombstone:
GET /analytics/v1/consent/status?anonymous_id=... - DNT/Sec-GPC may silently drop events (returns
204) - Ingest is async—allow up to 200ms batch flush before querying
reactor analytics keys list web-prodreactor analytics doctoranalytics.event.system_reserved (400)
Section titled “analytics.event.system_reserved (400)”Custom event names cannot start with $. Rename your event (e.g. checkout_completed not $checkout_completed).
analytics.quota.exceeded (429)
Section titled “analytics.quota.exceeded (429)”Monthly org quota exhausted. Upgrade plan or increase quota_per_org_monthly on self-hosted deployments.
analytics.query.timeout (504)
Section titled “analytics.query.timeout (504)”Query scanned too many rows or ran too long. Narrow the time range, add filters, or wait for rollup support in v0.2.
Batch partially rejected
Section titled “Batch partially rejected”/batch returns per-index rejection reasons. Fix invalid events and resend—accepted events are not rolled back.
{ "data": { "accepted": 98, "rejected": [ { "index": 17, "reason": "properties_too_large", "limit_bytes": 32768 } ] }}Auto-identify not linking users
Section titled “Auto-identify not linking users”Ensure autoIdentify: true and auth events fire (SIGNED_IN). Call reactor.analytics.identify() explicitly after sign-in if using a custom auth flow. On sign-out, reset() generates a new anonymous ID.