---
title: "Monitoring & Analytics"
description: "Health checks, first-party analytics (including personal_page_view), Web Vitals, structured logging, and optional Docker Prometheus"
locale: "en"
---
# Monitoring & Analytics

> **Info**
> Filter with **Founder** / **Developer** in the docs sidebar. Monitoring truth lives in `app/api/health`, `app/api/analytics/*`, `lib/logger.ts`, `public/scripts/analytics.js` + `RingAnalyticsBeacon`, and the admin analytics UI. No separate `features/analytics` doc page — this article is the ring-wide analytics overview.

Ring Platform ships **first-party telemetry** into PostgreSQL JSONB tables, a **container-friendly health endpoint**, and **structured server logs**. External APM (Sentry, Vercel Analytics) is optional via env vars — not required for core operation.

## Monitoring layers (what actually exists)

| Layer | Mechanism | Primary signal |
|-------|-----------|----------------|
| **Liveness** | `GET /api/health` (+ `HEAD`) | Process up, env warnings, memory |
| **UX performance** | `WebVitalsProvider` → `POST /api/analytics/web-vitals` | LCP, CLS, INP, TTFB, FCP |
| **Product events** | `analytics.js` + `RingAnalyticsBeacon` → `POST /api/analytics/app` | Sessions, page loads, custom events (`analytics_events`) |
| **Personal profiles** | `recordPersonalPageView` → `personal_page_view` | Unique visits 24h/7d by role on `/{username}` |
| **Client errors** | `POST /api/analytics/errors` | Stack traces, component, severity |
| **Admin dashboard** | `/admin/analytics` | `getPlatformAnalytics()` incl. `personalPages` |
| **Realtime connectivity** | `/api/tunnel/ping`, `/api/tunnel/heartbeat` | Tunnel latency / session health |
| **Server logs** | `lib/logger.ts` | JSON lines to stdout (`LOG_LEVEL`) |
| **Optional infra** | Docker profile `monitoring` | Prometheus + Grafana (local dev only) |

### For founders

## Why operators should monitor

A Ring clone combines **auth**, **marketplace data**, **payments**, and **realtime notifications**. Silent failure shows up as “the site loads but orders don’t sync” — not as a red error page. Personal-page unique visits tell you whether member trust faces are getting traffic without counting owners refreshing their own pages.

### What to watch without reading code

  
- **[Health endpoint](/docs/deployment/docker.md)** — Probe `GET /api/health` from your load balancer or uptime checker — `503` means degraded (often missing `AUTH_SECRET`).

  
- **[Admin analytics](/docs/features/admin.md)** — **Admin → Analytics** for user counts, Web Vitals medians, recent client errors, and personalPages (top profiles + role buckets).

  
- **[Public profiles](/docs/features/public-profile.md)** — Owners see unique 24h/7d visit stats on their personal page widget.

  
- **[Backup correlation](/docs/deployment/backup.md)** — Monitoring tells you *when* things broke; backups tell you *what* you can restore.

### Typical alert scenarios

- **Health flips to `degraded`** — missing critical env after deploy; check secrets before blaming Postgres.
- **Web Vitals `poor` ratings climb** — CDN, image weight, or SSR regression; correlate with a release tag (`BUILD_DATE` / `GIT_COMMIT` on health JSON when set).
- **Spike in `analytics_errors`** — broken client bundle or third-party script; admin dashboard lists recent messages.
- **Tunnel disconnect storms** — see [Realtime transport](/docs/architecture/real-time.md); often edge/WSS config, not database.

> **Tip**
> Define **who gets paged** (operator vs developer) and **what “down” means** for your clone — Ring OSS does not ship PagerDuty wiring; you attach health checks to your provider.

### For developers

## Architecture

```mermaid
flowchart LR
  Browser[Browser / PWA]
  WV[WebVitalsProvider]
  Beacon[RingAnalyticsBeacon]
  Script["/scripts/analytics.js"]
  Profile["recordPersonalPageView"]
  API["/api/analytics/*"]
  ADB[analytics-db.ts]
  PG[(PostgreSQL JSONB)]

  Browser --> WV --> API
  Browser --> Beacon --> Script --> API
  Profile --> ADB
  API --> ADB --> PG
  Admin["/admin/analytics"] --> ADB
```

### Health check — `app/api/health/route.ts`

Returns JSON: `status` (`healthy` | `degraded` | `unhealthy`), uptime, memory, inferred `services.database` (`postgresql` vs `firebase`), optional Docker `container` block. Missing `AUTH_SECRET` → `degraded` + HTTP **503**.

{`curl -sS https://your-clone.example/api/health | jq .
curl -I https://your-clone.example/api/health   # HEAD — 200 if alive`}

### Client beacon (verified)

| Piece | Path |
|-------|------|
| Script | `public/scripts/analytics.js` — batches to `POST /api/analytics/app` via `sendBeacon` / fetch; exposes `window.ringAnalytics` |
| App Router glue | `components/providers/ring-analytics-beacon.tsx` — loads the script; calls `pageView` on pathname/searchParams change (hard load alone is not enough) |
| Mount | `components/providers/app-client-shell.tsx` |

Legacy `public/scripts/app-analytics.js` exists in tree but is **not** the mounted SSOT — do not document it as live.

### Analytics API surface

| Route | Method | Auth | Persists to |
|-------|--------|------|-------------|
| `/api/analytics/web-vitals` | POST | Optional session | `web_vitals` |
| `/api/analytics/web-vitals` | GET | Admin | Query `web_vitals` or `?scope=platform` summary |
| `/api/analytics/app` | POST | Optional session | `analytics_events` |
| `/api/analytics/errors` | POST | Optional session | `analytics_errors` |
| `/api/analytics/errors` | GET | Admin | List errors |
| `/api/analytics/device` | POST | Session required | `user_device_telemetry` |
| `/api/analytics/platform-stats` | GET | Admin | Counts: users, entities, opportunities |
| `/api/analytics/navigation` | POST | Optional session | Stub — returns `{ ok: true }`, no persistence |

Implementation: `features/analytics/lib/analytics-db.ts` (`insertAnalyticsEventBatch`, `insertWebVitalsRecord`, `insertAnalyticsErrors`). Client Web Vitals: `components/providers/web-vitals-provider.tsx` — `useReportWebVitals` from `next/web-vitals` buffers CLS, FCP, LCP, TTFB, INP into one POST per ~1.5s debounce (INP replaces FID).

### Personal page events

| Piece | Path |
|-------|------|
| Event type | `personal_page_view` |
| Writer | `features/analytics/lib/personal-page-analytics.ts` → `recordPersonalPageView` |
| Call site | `app/[locale]/[username]/page.tsx` — skips owners |
| Owner widget | `getPersonalPageViewStats` (`features/auth/services/personal-page-stats.ts`) |
| Admin | `getPersonalPagePlatformStats` → `PlatformAnalyticsSummary.personalPages` |
| Engagement | `personal_page_view` aggregated alongside `page_view` / `app_load` / `docs_page_view` event types in admin pageViews |

Details and unique-by-role math: [Public Profile Pages](/docs/features/public-profile.md).

### Database tables

Included in `data/schema.sql` and migration `017_ring_analytics_schema.sql`:

- `analytics_events` — batched client telemetry **and** server `personal_page_view` rows  
- `web_vitals` — Core Web Vitals batches  
- `analytics_errors` — client-side error log  

### Disable storage (privacy / load testing)

{`ANALYTICS_DISABLE_STORAGE=true`}

When set, ingest routes **acknowledge** payloads but skip `DatabaseService` writes (`isAnalyticsStorageDisabled()`). Personal-page recorder also no-ops.

### Structured logging

{`import { logger } from '@/lib/logger'

logger.info('Subscription created', { userId })
logger.warn('syncDiscovery: tunnel publish failed', { channel, error })
logger.error('Failed to create subscription', { userId, error })`}

Env: `LOG_LEVEL` (`debug` | `info` | `warn` | `error`; default `info` in production, `warn` in development), `LOG_SILENT=true` to mute. Logs are JSON lines to stdout; development skips expected JWT/SSE noise.

### Optional external APM

Commented/optional in `env.local.template` / `docker.env.template`:

- `SENTRY_DSN` — wire via your Sentry Next.js integration (not auto-enabled in repo)  
- `NEXT_PUBLIC_ANALYTICS_ID`, `VERCEL_ANALYTICS_ID` — Vercel-hosted clones only  

### Realtime probes

| Route | Purpose |
|-------|---------|
| `POST /api/tunnel/ping` | Authenticated pong + timestamp |
| `POST /api/tunnel/heartbeat` | Connection keep-alive |

See [Tunnel protocol](/docs/features/tunnel-protocol.md).

### Cron / pipeline monitoring

Cron routes (`/api/cron/*`) fail closed without `CRON_SECRET`. Example: `GET /api/cron/email-analytics` runs ProcessConductor pipeline `email-analytics`.

### Optional Docker Prometheus + Grafana

{`docker compose --profile monitoring up -d`}

Services: `ring-prometheus` (:9090), `ring-grafana` (:3001). Config: `docker/prometheus/prometheus.yml`.

  The bundled Prometheus job references `/api/metrics`, which **is not implemented** in this tree, and scrapes `/api/health` as JSON (not Prometheus exposition format). Treat the profile as a **starting scaffold**.

### Admin UI entrypoint

Server page: `app/[locale]/admin/analytics/page.tsx` — requires `isPlatformAdmin`, loads `getPlatformAnalytics('7d')` including `personalPages`.

## Related documentation

  
- [features/public-profile](/docs/features/public-profile.md) — Same-workflow: personal_page_view unique visit stats and owner skip.

  
- [deployment/backup](/docs/deployment/backup.md) — Next-step: restore after incidents detected here.

  
- [deployment/performance](/docs/deployment/performance.md) — See-also: Web Vitals tuning and performance ops.

  
- [deployment/environment](/docs/deployment/environment.md) — Depends-on: CRON_SECRET, LOG_LEVEL, ANALYTICS_DISABLE_STORAGE, optional Sentry.

  
- [architecture/real-time](/docs/architecture/real-time.md) — Deep-dive: tunnel transports and heartbeat behavior.

  
- [features/admin](/docs/features/admin.md) — Next-step: Admin console entry for /admin/analytics.
