Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

Analytics

Experimental

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.

  • Hybrid event schema — System events ($pageview, $identify, $error) get hot columns; custom events use free-form properties.
  • Anonymous + authenticated ingest — Project keys for client-side; JWT for server-side.
  • Identity stitching$identify and $alias merge 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 trackingctx.analytics.track() in Functions, Jobs, and Sites.

Create a project, issue a public key, and track an event from the browser.

Terminal window
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"}'

Count unique users who completed checkout in the last 30 days, grouped by plan.

Terminal window
reactor analytics query --project web-prod --file query.json

query.json:

{
"kind": "aggregate",
"time_range": { "last": "30d" },
"filter": { "all": [{ "event": { "eq": "checkout_completed" } }] },
"measure": "unique_users",
"group_by": [{ "prop": "plan" }],
"time_bucket": "1d"
}
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' }],
});
Terminal window
reactor analytics erase web-prod --user-id u_42
[analytics]
retention_days_default = 90
honor_dnt = true
quota_per_org_monthly = 1000000
batch_interval_ms = 200
batch_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:

Terminal window
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:

VariableDefaultDescription
REACTOR_ANALYTICS_BIND0.0.0.0:8006HTTP bind
REACTOR_ANALYTICS_RETENTION_DAYS_DEFAULT90Event retention
REACTOR_ANALYTICS_QUOTA_PER_ORG_MONTHLY1000000Free-tier cap
REACTOR_ANALYTICS_HONOR_DNT1Drop events when DNT/Sec-GPC set
REACTOR_ANALYTICS_QUERY_TIMEOUT_MS30000Query statement timeout
REACTOR_ANALYTICS_MAX_PROPERTIES_BYTES32768Per-event size cap
LimitDefaultNotes
Events per batch100Max 1 MiB body
Properties + context size32 KiBPer event
Raw query time range90 daysWider ranges need rollups (v0.2)
Aggregate query time range730 days
Query rows scanned100,000Hard cap per request
Monthly events per org1,000,000Configurable; returns 429
IP storage/24 truncatedNever raw IP
Custom event namesNo $ prefixReserved for system events
Rollupsv0.2 deferredv0 scans raw events only

System events (reserved $ prefix):

EventPurpose
$pageviewPage navigation
$identifyUser traits assert
$aliasMerge anon → user
$session_start / $session_endSession boundaries
$autocaptureClick/submit (opt-in)
$errorUncaught errors

Query kinds:

KindUse case
eventsRaw event rows (debugging, capped at 1000)
aggregateCounts, unique users, sums, averages
funnelStep conversion with window
retentionCohort return rate
breakdownTop-N by property
pathUser journey sequences
MethodPathAuthDescription
POST/analytics/v1/trackProject key or JWTSingle event
POST/analytics/v1/batchProject key or JWTBatch ingest
POST/analytics/v1/identifyProject key or JWTIdentity assert
POST/analytics/v1/queryJWTRun query
POST/analytics/v1/eraseJWTGDPR erasure
POST/analytics/v1/consent/opt-outProject keyOpt out anon ID

Permissions:

PermissionScope
analytics:project:createCreate projects
analytics:{project_id}:ingestServer-side ingest
analytics:{project_id}:queryRun queries
analytics:{project_id}:eraseGDPR erasure
  1. Confirm the project key is valid and not revoked
  2. Check for opt-out tombstone: GET /analytics/v1/consent/status?anonymous_id=...
  3. DNT/Sec-GPC may silently drop events (returns 204)
  4. Ingest is async—allow up to 200ms batch flush before querying
Terminal window
reactor analytics keys list web-prod
reactor analytics doctor

Custom event names cannot start with $. Rename your event (e.g. checkout_completed not $checkout_completed).

Monthly org quota exhausted. Upgrade plan or increase quota_per_org_monthly on self-hosted deployments.

Query scanned too many rows or ran too long. Narrow the time range, add filters, or wait for rollup support in v0.2.

/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 }
]
}
}

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.