---
title: "Analytics API"
description: "Ring first-party analytics ingestion and admin reads — app events, device telemetry, Web Vitals, error capture, and platform stats over verified /api/analytics routes"
locale: "en"
---
# Analytics API

> **Info**
> Use **Founder** / **Developer** tabs in the docs sidebar to filter this page. Admin console surfaces consume these routes; third-party providers are optional env hooks.

Ring ships a **first-party analytics pipeline**: client telemetry is ingested over `/api/analytics/*`, Zod-validated at the route boundary, persisted to `analytics_events` / `web_vitals` / `analytics_errors`, and read back by admin-scoped GET routes. Storage can be disabled entirely per environment — every route then acknowledges without writing.

| Previous (GA-only setup) | Ring equivalent |
|--------------------------|-----------------|
| Third-party SDK only, data leaves your clone | First-party ingestion into your own database |
| One opaque dashboard | Admin-scoped reads + platform stats with role gates |
| No device/session context | `sessionId` batches, device telemetry with tunnel fan-out |
| Web Vitals left unmeasured | Core Web Vitals ingestion + computed `performanceScore` |

## Route surface

| Endpoint | Method | Auth | Purpose |
|----------|--------|------|---------|
| `/api/analytics/app` | POST | optional userId | Batched app events → `analytics_events` |
| `/api/analytics/device` | POST | session required | Device telemetry → `device_telemetry` + user tunnel fan-out |
| `/api/analytics/navigation` | POST | — | Light acknowledgement stub (no storage) |
| `/api/analytics/errors` | POST | optional userId | Client error capture → `analytics_errors` (deferred write) |
| `/api/analytics/errors` | GET | platform admin | Error listing with severity/component filters |
| `/api/analytics/web-vitals` | POST | optional userId | Core Web Vitals + `performanceScore` → `web_vitals` (deferred write) |
| `/api/analytics/web-vitals` | GET | platform admin | `scope=platform` summary or per-user; timeframe `24h` / `7d` / `30d` |
| `/api/analytics/platform-stats` | GET | platform admin | Live counts for `users`, `entities`, `opportunities` |

### For founders

## Why this matters for your clone

- **Your data stays on your Ring.** Ingestion writes to your own database; nothing is shipped to a third party unless you opt into GA/Mixpanel/Amplitude env hooks.
- **Admin console stats** — platform counts and Web Vitals summaries are role-gated to `platform admin`.
- **Kill switch** — `ANALYTICS_DISABLE_STORAGE=true` turns persistence off without code changes; endpoints keep answering.
- **Docs and personal-page telemetry** — the platform summary also aggregates docs 404s and personal-page visits (`getPlatformAnalytics`).

### Optional third-party hooks (env only)

| Variable | Provider |
|----------|----------|
| `GA_TRACKING_ID` / `GA4_MEASUREMENT_PROTOCOL_SECRET` | Google Analytics |
| `MIXPANEL_TOKEN` | Mixpanel |
| `AMPLITUDE_API_KEY` | Amplitude |
| `SENTRY_DSN` | Sentry error tracking |
| `DD_API_KEY` / `DD_APP_KEY` | Datadog |

None are required — Ring's own pipeline works without them.

### For developers

## Ingestion contract

All writes are **non-blocking**: validation happens synchronously, then persistence runs in Next.js `after()` so the client response is never held up. Batch schema (verified in `features/analytics/lib/analytics-db.ts`):

{`{
  sessionId: string,        // 1–128 chars: a-zA-Z0-9 _ - : .
  userId: string | null,    // enriched from session when omitted
  events: [                 // 1–50 events per batch
    { type: string, data?: unknown, timestamp?: number }
  ]
}`}

Invalid payloads return `400` with the first Zod issue. Batches beyond 50 events are rejected, not silently truncated.

### Where each route writes

| Route | Validation module | Storage |
|-------|-------------------|---------|
| `app` | `appAnalyticsBatchSchema` | `analytics_events` |
| `device` | `deviceTelemetryBodySchema` (`device-telemetry-db.ts`) | device telemetry + `publishToUserTunnel` via `telemetryChannelForDomain` |
| `errors` | `analyticsErrorPayloadSchema` | `analytics_errors` (userId enriched inside `after()`) |
| `web-vitals` | `webVitalsInputSchema` + `calculatePerformanceScore` | `web_vitals` with detected platform/browser |
| `navigation` | none (stub) | nothing — returns `{ ok: true }` |

### Admin reads

- `GET /api/analytics/platform-stats` — `isPlatformAdmin(session.user.role)` gate; concurrent `countDocs` over users / entities / opportunities.
- `GET /api/analytics/web-vitals?scope=platform&timeframe=30d` — platform summary via `getPlatformAnalyticsSummary`; omit `scope` for per-user metrics.
- Aggregation SSOT: `features/analytics/services/get-platform-analytics.ts` (core summary + docs not-found + personal-page stats).

## Configuration

| Variable | Default | Effect |
|----------|---------|--------|
| `ANALYTICS_DISABLE_STORAGE` | unset | `"true"` acknowledges events without writing to the database |

## Related documentation

  
- [api/admin](/docs/api/admin.md) — Next-step: admin-scoped API routes and RBAC patterns after analytics ingestion.

  
- [features/performance](/docs/features/performance.md) — Same-workflow: Web Vitals telemetry feeds the performance feature pages.

  
- [architecture/data-validation](/docs/architecture/data-validation.md) — Deep-dive: the Zod route-boundary guards every ingestion route uses.
