No description
- TypeScript 97.5%
- Dockerfile 1.9%
- JavaScript 0.4%
- Shell 0.2%
| prisma | ||
| src | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| docker-entrypoint.sh | ||
| Dockerfile | ||
| jest.config.cjs | ||
| package.json | ||
| pnpm-lock.yaml | ||
| README.md | ||
| tsconfig.json | ||
analytics-be
Universal, multi-tenant analytics ingestion service. Express 5 + TypeScript +
Prisma + Postgres + Redis. Sharevent is the first consumer; any project plugs in
with a public browser API key. See ../../ANALYTICS.md for
the full design.
Run locally
cp .env.example .env # set DATABASE_URL / REDIS_URL / ADMIN_API_KEY
pnpm install
createdb analytics # one-time: separate DB on the shared Postgres
pnpm prisma:generate
pnpm migrate:dev # apply schema
pnpm dev # http://localhost:3002/v1/health
In Docker the service is wired into INFRA/sharevent-infra/docker-compose.dev.yml
(analytics service). Run CREATE DATABASE analytics; on the db container once.
API (/v1, port 3002)
| Endpoint | Auth | Purpose |
|---|---|---|
POST /ingest |
api key (body) | batch: { apiKey, session, events[], movement[] } |
POST /experiments/:key/assign |
api key (body) | { apiKey, anonId } → { variantKey }, idempotent |
POST /experiments/:key/convert |
api key (body) | { apiKey, anonId, goal }, deduped |
GET /experiments/:key/results?projectId= |
admin key | exposures/conversions/rate per variant |
POST /projects |
admin key | { name, origins[] } |
GET /projects |
admin key | list projects + keys |
POST /projects/:id/keys |
admin key | mint a public browser key |
POST /experiments |
admin key | { projectId, key, variants[] } (weights sum to 100) |
GET /health |
none | liveness |
- API key travels in the request body (sendBeacon can't set headers) and is
origin-scoped — only usable from the project's whitelisted
origins. The key→project lookup is cached in Redis forKEY_CACHE_TTL_S. - Admin key is the env
ADMIN_API_KEY, sent as thex-admin-keyheader. - SDK requests use
Content-Type: text/plain(CORS-simple → no preflight); the server parses the body as JSON regardless. Ingest is rate-limited per apiKey+IP (RATE_LIMIT_PER_MIN) and capped atMAX_BODY_BYTES(→ 413).
Retention
A best-effort sweep deletes Event/MovementSample/Session rows older than
RETENTION_DAYS (default 90) on the RETENTION_SWEEP_CRON_MS interval. Set
RETENTION_DAYS=0 to disable. Partitioning is deferred.
Quick smoke
ADMIN=dev-admin-key-1234567890
# create a project + key
curl -sX POST localhost:3002/v1/projects -H "x-admin-key: $ADMIN" \
-H 'content-type: application/json' \
-d '{"name":"sharevent","origins":["http://localhost:5173"]}'
curl -sX POST localhost:3002/v1/projects/<id>/keys -H "x-admin-key: $ADMIN" \
-H 'content-type: application/json' -d '{"label":"web"}'
# ingest a synthetic batch
curl -sX POST localhost:3002/v1/ingest -H 'content-type: text/plain' -d '{
"apiKey":"ak_...","session":{"sessionId":"s1","anonId":"a1"},
"events":[{"type":"page_view","path":"/"}],"movement":[]
}'