chore(docs): 📝 Update documentation files in /docs directory (README, guides, or API references)

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Lilith 2026-01-29 08:20:58 -08:00
parent 85235e6977
commit dc5329e885
5 changed files with 2072 additions and 0 deletions

453
docs/api-reference.md Normal file
View file

@ -0,0 +1,453 @@
# API Reference
Complete reference for the Analytics API endpoints.
## Base URL
```
Production: https://analytics.example.com
Development: http://localhost:4001
```
## Authentication
API endpoints use API key authentication:
```
Authorization: Bearer <api-key>
```
Or via header:
```
X-API-Key: <api-key>
```
## Collector Endpoints
### POST /collect/engagement
Collect engagement events from clients.
**Request Body:**
```json
{
"sessionId": "string (required)",
"userId": "string (optional)",
"type": "string (required)",
"action": "string (required)",
"timestamp": "ISO8601 string (optional)",
"metadata": "object (optional)",
"source": {
"app": "string",
"environment": "string"
}
}
```
**Response:** `202 Accepted`
```json
{
"success": true,
"eventId": "evt_abc123"
}
```
**Example:**
```bash
curl -X POST https://analytics.example.com/collect/engagement \
-H "Content-Type: application/json" \
-d '{
"sessionId": "sess_123",
"type": "navigation",
"action": "page_view",
"metadata": {
"path": "/products",
"referrer": "https://google.com"
}
}'
```
### POST /collect/batch
Collect multiple events in a single request.
**Request Body:**
```json
{
"events": [
{
"sessionId": "string",
"type": "string",
"action": "string",
"timestamp": "ISO8601",
"metadata": {}
}
]
}
```
**Response:** `202 Accepted`
```json
{
"success": true,
"processed": 10,
"failed": 0
}
```
### POST /collect/device
Collect device fingerprint information.
**Request Body:**
```json
{
"sessionId": "string (required)",
"fingerprint": {
"screenWidth": 1920,
"screenHeight": 1080,
"viewportWidth": 1200,
"viewportHeight": 800,
"devicePixelRatio": 2,
"colorDepth": 24,
"timezone": "America/New_York",
"language": "en-US",
"platform": "MacIntel",
"vendor": "Google Inc.",
"cookiesEnabled": true,
"doNotTrack": false
}
}
```
## Query API Endpoints
### GET /api/trends
Get trend data over time.
**Query Parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `metric` | string | Yes | Metric to query (pageViews, sessions, users) |
| `startDate` | ISO8601 | Yes | Start of date range |
| `endDate` | ISO8601 | Yes | End of date range |
| `granularity` | string | No | hour, day, week, month (default: day) |
| `filters` | JSON | No | Filter criteria |
**Response:**
```json
{
"metric": "pageViews",
"granularity": "day",
"data": [
{ "date": "2024-01-01", "value": 1523 },
{ "date": "2024-01-02", "value": 1687 },
{ "date": "2024-01-03", "value": 1456 }
],
"total": 4666,
"change": 12.5
}
```
### GET /api/funnels
Get funnel conversion data.
**Query Parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `funnelId` | string | Yes | Funnel identifier |
| `startDate` | ISO8601 | Yes | Start of date range |
| `endDate` | ISO8601 | Yes | End of date range |
| `steps` | JSON array | No | Custom funnel steps |
**Response:**
```json
{
"funnelId": "checkout",
"steps": [
{ "name": "Cart View", "count": 10000, "rate": 100 },
{ "name": "Begin Checkout", "count": 6500, "rate": 65 },
{ "name": "Add Payment", "count": 4200, "rate": 42 },
{ "name": "Purchase", "count": 3100, "rate": 31 }
],
"overallConversion": 31,
"dropoffs": [
{ "from": "Cart View", "to": "Begin Checkout", "lost": 3500, "rate": 35 },
{ "from": "Begin Checkout", "to": "Add Payment", "lost": 2300, "rate": 35.4 },
{ "from": "Add Payment", "to": "Purchase", "lost": 1100, "rate": 26.2 }
]
}
```
### GET /api/cohorts
Get cohort retention data.
**Query Parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `cohortType` | string | Yes | signup_date, first_purchase, etc. |
| `startDate` | ISO8601 | Yes | Start of cohort range |
| `endDate` | ISO8601 | Yes | End of cohort range |
| `periods` | number | No | Number of retention periods (default: 12) |
**Response:**
```json
{
"cohortType": "signup_date",
"cohorts": [
{
"date": "2024-01",
"size": 1000,
"retention": [100, 45, 38, 32, 28, 25]
},
{
"date": "2024-02",
"size": 1200,
"retention": [100, 48, 41, 35, 30]
}
]
}
```
### GET /api/segments
Get data segmented by dimension.
**Query Parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `dimension` | string | Yes | Segmentation dimension |
| `metric` | string | Yes | Metric to aggregate |
| `startDate` | ISO8601 | Yes | Start of date range |
| `endDate` | ISO8601 | Yes | End of date range |
| `limit` | number | No | Max segments (default: 10) |
**Available Dimensions:**
- `country` - Geographic country
- `device` - Device type (mobile, tablet, desktop)
- `browser` - Browser name
- `os` - Operating system
- `source` - Traffic source
- `medium` - Traffic medium
- `campaign` - Campaign name
**Response:**
```json
{
"dimension": "device",
"metric": "sessions",
"segments": [
{ "name": "desktop", "value": 45000, "percentage": 56.2 },
{ "name": "mobile", "value": 30000, "percentage": 37.5 },
{ "name": "tablet", "value": 5000, "percentage": 6.3 }
],
"total": 80000
}
```
### GET /api/acquisition
Get traffic acquisition data.
**Query Parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `startDate` | ISO8601 | Yes | Start of date range |
| `endDate` | ISO8601 | Yes | End of date range |
| `groupBy` | string | No | source, medium, campaign |
**Response:**
```json
{
"channels": [
{
"channel": "Organic Search",
"sessions": 25000,
"users": 20000,
"newUsers": 15000,
"bounceRate": 45.2,
"avgSessionDuration": 180,
"conversions": 500,
"conversionRate": 2.0
},
{
"channel": "Direct",
"sessions": 18000,
"users": 12000,
"newUsers": 3000,
"bounceRate": 35.8,
"avgSessionDuration": 240,
"conversions": 800,
"conversionRate": 4.4
}
]
}
```
### GET /api/engagement
Get engagement metrics.
**Query Parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `startDate` | ISO8601 | Yes | Start of date range |
| `endDate` | ISO8601 | Yes | End of date range |
**Response:**
```json
{
"metrics": {
"avgSessionDuration": 185,
"avgPageViewsPerSession": 3.2,
"bounceRate": 42.5,
"avgScrollDepth": 65,
"engagementRate": 57.5
},
"topPages": [
{ "path": "/", "views": 50000, "avgTime": 45 },
{ "path": "/products", "views": 35000, "avgTime": 120 },
{ "path": "/pricing", "views": 28000, "avgTime": 90 }
],
"topEvents": [
{ "action": "add_to_cart", "count": 8500 },
{ "action": "signup_click", "count": 6200 },
{ "action": "video_play", "count": 4800 }
]
}
```
## Realtime Endpoints
### WebSocket /realtime
Connect to receive real-time analytics updates.
**Connection:**
```javascript
const ws = new WebSocket('wss://analytics.example.com/realtime');
ws.onopen = () => {
// Subscribe to channels
ws.send(JSON.stringify({
type: 'subscribe',
channels: ['active_users', 'page_views', 'events']
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log(data);
};
```
**Messages:**
```json
{
"channel": "active_users",
"data": {
"count": 156,
"byPage": {
"/": 45,
"/products": 32,
"/checkout": 12
}
}
}
```
### GET /api/realtime/active
Get current active users.
**Response:**
```json
{
"activeUsers": 156,
"activeSessions": 142,
"byCountry": [
{ "country": "US", "count": 65 },
{ "country": "UK", "count": 23 },
{ "country": "DE", "count": 18 }
],
"byPage": [
{ "path": "/", "count": 45 },
{ "path": "/products", "count": 32 }
],
"recentEvents": [
{ "action": "purchase", "timestamp": "2024-01-15T10:30:00Z" },
{ "action": "signup", "timestamp": "2024-01-15T10:29:45Z" }
]
}
```
## Error Responses
All endpoints return consistent error responses:
```json
{
"error": {
"code": "INVALID_PARAMETER",
"message": "Invalid date format for startDate",
"details": {
"parameter": "startDate",
"provided": "2024-13-01",
"expected": "ISO8601 date string"
}
}
}
```
**Error Codes:**
| Code | HTTP Status | Description |
|------|-------------|-------------|
| `INVALID_PARAMETER` | 400 | Invalid request parameter |
| `MISSING_PARAMETER` | 400 | Required parameter missing |
| `UNAUTHORIZED` | 401 | Invalid or missing API key |
| `FORBIDDEN` | 403 | Insufficient permissions |
| `NOT_FOUND` | 404 | Resource not found |
| `RATE_LIMITED` | 429 | Too many requests |
| `INTERNAL_ERROR` | 500 | Server error |
## Rate Limits
| Endpoint Type | Limit |
|---------------|-------|
| Collector | 1000 req/min per session |
| Query API | 100 req/min per API key |
| Realtime | 10 connections per API key |
Rate limit headers:
```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1705312800
```

266
docs/client-sdk.md Normal file
View file

@ -0,0 +1,266 @@
# Client SDK Documentation
The `@analytics/client` package provides browser and server-side analytics tracking.
## Installation
```bash
npm install @analytics/client
# or
yarn add @analytics/client
# or
bun add @analytics/client
```
## Quick Start
### Browser (Vanilla JavaScript)
```typescript
import { AnalyticsClient } from '@analytics/client';
const analytics = new AnalyticsClient({
apiBaseUrl: 'https://analytics.example.com',
appName: 'my-app',
});
// Track a page view
analytics.trackEngagement({
type: 'navigation',
action: 'page_view',
metadata: { path: window.location.pathname },
});
// Track a custom event
analytics.trackEngagement({
type: 'user_action',
action: 'button_click',
metadata: { buttonId: 'signup-cta' },
});
// Identify a user
analytics.identify('user-123', {
email: 'user@example.com',
plan: 'pro',
});
```
### Server-Side (Node.js)
```typescript
import { BackendAnalyticsClient } from '@analytics/client';
const analytics = new BackendAnalyticsClient({
apiBaseUrl: 'http://analytics-collector:4001',
appName: 'my-api',
});
// Track from server
analytics.trackEngagement({
sessionId: req.headers['x-session-id'] || 'server',
userId: req.user?.id,
type: 'api_request',
action: 'GET /users',
metadata: {
statusCode: 200,
durationMs: 45,
},
});
```
## Configuration
### AnalyticsConfig
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `apiBaseUrl` | `string` | required | Analytics collector URL |
| `appName` | `string` | required | Application identifier |
| `enabled` | `boolean` | `true` | Enable/disable tracking |
| `enableDebugLogging` | `boolean` | `false` | Log tracking calls |
| `batchSize` | `number` | `10` | Events per batch |
| `flushInterval` | `number` | `5000` | Batch flush interval (ms) |
| `autoCapture` | `object` | see below | Auto-capture settings |
### Auto-Capture Settings
```typescript
{
autoCapture: {
pageViews: true, // Track page navigation
clicks: true, // Track click events
scrollDepth: true, // Track scroll percentage
performance: true, // Track Core Web Vitals
errors: false, // Track JavaScript errors
}
}
```
## Core Methods
### trackEngagement()
Track user interactions and events.
```typescript
analytics.trackEngagement({
type: string, // Event category
action: string, // Event name
metadata?: object, // Additional data
sessionId?: string, // Override session ID
userId?: string, // User identifier
});
```
### identify()
Associate a user ID with the current session.
```typescript
analytics.identify(userId: string, traits?: object);
```
### setUserId() / clearUserId()
Manage user identity.
```typescript
analytics.setUserId('user-123');
analytics.clearUserId();
```
### flush()
Force send queued events immediately.
```typescript
await analytics.flush();
```
## Event Types
### Navigation Events
```typescript
// Page view
analytics.trackEngagement({
type: 'navigation',
action: 'page_view',
metadata: {
path: '/products',
title: 'Products Page',
referrer: document.referrer,
},
});
// Route change (SPA)
analytics.trackEngagement({
type: 'navigation',
action: 'route_change',
metadata: {
from: '/home',
to: '/products',
},
});
```
### User Actions
```typescript
// Button click
analytics.trackEngagement({
type: 'click',
action: 'cta_button',
metadata: {
buttonText: 'Get Started',
location: 'hero',
},
});
// Form submission
analytics.trackEngagement({
type: 'form',
action: 'submit',
metadata: {
formName: 'contact',
success: true,
},
});
```
### E-commerce Events
```typescript
// Product view
analytics.trackEngagement({
type: 'ecommerce',
action: 'product_view',
metadata: {
productId: 'prod-123',
productName: 'Blue Widget',
price: 29.99,
},
});
// Purchase
analytics.trackEngagement({
type: 'ecommerce',
action: 'purchase',
metadata: {
orderId: 'ord-456',
total: 99.99,
currency: 'USD',
isConversion: true,
conversionValue: 99.99,
},
});
```
## Consent-Free Mode
The SDK uses in-memory session IDs by default, requiring no cookies or localStorage:
- Session IDs generated per page load
- No persistent storage used
- UTM parameters captured from URL only
- GDPR/ePrivacy compliant without consent banners
## Device Information
Automatically captured (no storage required):
- Screen resolution
- Viewport size
- Device type (mobile/tablet/desktop)
- Browser name and version
- Operating system
- Language preference
- Timezone
## Error Handling
The SDK silently handles errors to never break your application:
```typescript
// All tracking calls are fire-and-forget
analytics.trackEngagement({...}); // Never throws
// Enable debug logging to see issues
const analytics = new AnalyticsClient({
apiBaseUrl: '...',
appName: '...',
enableDebugLogging: true, // Logs to console
});
```
## TypeScript Support
Full TypeScript support with exported types:
```typescript
import type {
AnalyticsConfig,
EngagementEvent,
DeviceInfo,
AttributionData,
} from '@analytics/client';
```

455
docs/deployment.md Normal file
View file

@ -0,0 +1,455 @@
# Deployment Guide
Deploy the analytics platform to production.
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────────┐
│ Load Balancer │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────┼─────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Collector │ │ Collector │ │ Collector │
│ Service │ │ Service │ │ Service │
└───────────────┘ └───────────────┘ └───────────────┘
│ │ │
└─────────────────────┼─────────────────────┘
┌───────────────┐
│ Redis │
│ (BullMQ) │
└───────────────┘
┌─────────────────────┼─────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Processor │ │ Processor │ │ Processor │
│ Worker │ │ Worker │ │ Worker │
└───────────────┘ └───────────────┘ └───────────────┘
│ │ │
└─────────────────────┼─────────────────────┘
┌───────────────┐
│ PostgreSQL │
│ (TimescaleDB)│
└───────────────┘
┌───────────────┐
│ API Service │
└───────────────┘
```
## Services
| Service | Port | Description |
|---------|------|-------------|
| Collector | 4001 | Event ingestion |
| Processor | - | Queue worker (no HTTP) |
| API | 4002 | Query endpoints |
| Realtime | 4003 | WebSocket server |
## Docker Deployment
### docker-compose.yml
```yaml
version: '3.8'
services:
collector:
image: analytics/collector:latest
ports:
- "4001:4001"
environment:
- NODE_ENV=production
- REDIS_URL=redis://redis:6379
- LOG_LEVEL=info
depends_on:
- redis
deploy:
replicas: 3
resources:
limits:
memory: 512M
cpus: '0.5'
processor:
image: analytics/processor:latest
environment:
- NODE_ENV=production
- REDIS_URL=redis://redis:6379
- DATABASE_URL=postgresql://postgres:password@postgres:5432/analytics
- CONCURRENCY=10
depends_on:
- redis
- postgres
deploy:
replicas: 2
resources:
limits:
memory: 1G
cpus: '1'
api:
image: analytics/api:latest
ports:
- "4002:4002"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://postgres:password@postgres:5432/analytics
- REDIS_URL=redis://redis:6379
depends_on:
- postgres
- redis
deploy:
replicas: 2
resources:
limits:
memory: 512M
cpus: '0.5'
realtime:
image: analytics/realtime:latest
ports:
- "4003:4003"
environment:
- NODE_ENV=production
- REDIS_URL=redis://redis:6379
depends_on:
- redis
deploy:
replicas: 2
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
command: redis-server --appendonly yes
postgres:
image: timescale/timescaledb:latest-pg15
environment:
- POSTGRES_DB=analytics
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
redis_data:
postgres_data:
```
## Kubernetes Deployment
### Collector Deployment
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: analytics-collector
spec:
replicas: 3
selector:
matchLabels:
app: analytics-collector
template:
metadata:
labels:
app: analytics-collector
spec:
containers:
- name: collector
image: analytics/collector:latest
ports:
- containerPort: 4001
env:
- name: NODE_ENV
value: production
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: analytics-secrets
key: redis-url
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
readinessProbe:
httpGet:
path: /health
port: 4001
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 4001
initialDelaySeconds: 15
periodSeconds: 20
---
apiVersion: v1
kind: Service
metadata:
name: analytics-collector
spec:
selector:
app: analytics-collector
ports:
- port: 4001
targetPort: 4001
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: analytics-collector-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: analytics-collector
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
```
## Environment Variables
### Collector Service
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `NODE_ENV` | Yes | - | Environment (production/development) |
| `PORT` | No | 4001 | HTTP port |
| `REDIS_URL` | Yes | - | Redis connection URL |
| `LOG_LEVEL` | No | info | Logging level |
| `CORS_ORIGINS` | No | * | Allowed CORS origins |
### Processor Service
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `NODE_ENV` | Yes | - | Environment |
| `REDIS_URL` | Yes | - | Redis connection URL |
| `DATABASE_URL` | Yes | - | PostgreSQL connection URL |
| `CONCURRENCY` | No | 5 | Worker concurrency |
| `BATCH_SIZE` | No | 100 | Events per batch |
### API Service
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `NODE_ENV` | Yes | - | Environment |
| `PORT` | No | 4002 | HTTP port |
| `DATABASE_URL` | Yes | - | PostgreSQL connection URL |
| `REDIS_URL` | Yes | - | Redis for caching |
| `API_KEYS` | Yes | - | Comma-separated API keys |
## Database Setup
### PostgreSQL with TimescaleDB
```sql
-- Create database
CREATE DATABASE analytics;
-- Enable TimescaleDB
CREATE EXTENSION IF NOT EXISTS timescaledb;
-- Create tables
CREATE TABLE raw_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id VARCHAR(64) NOT NULL,
user_id VARCHAR(255),
event_type VARCHAR(100) NOT NULL,
event_action VARCHAR(255) NOT NULL,
metadata JSONB DEFAULT '{}',
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Convert to hypertable for time-series optimization
SELECT create_hypertable('raw_events', 'timestamp');
-- Create indexes
CREATE INDEX idx_raw_events_session ON raw_events(session_id);
CREATE INDEX idx_raw_events_user ON raw_events(user_id);
CREATE INDEX idx_raw_events_type ON raw_events(event_type);
CREATE INDEX idx_raw_events_metadata ON raw_events USING GIN(metadata);
-- Aggregated tables
CREATE TABLE daily_metrics (
date DATE NOT NULL,
metric_name VARCHAR(100) NOT NULL,
dimension_key VARCHAR(255),
dimension_value VARCHAR(255),
value BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (date, metric_name, dimension_key, dimension_value)
);
-- Retention policy: keep raw events for 90 days
SELECT add_retention_policy('raw_events', INTERVAL '90 days');
```
## Nginx Configuration
```nginx
upstream collector {
least_conn;
server collector-1:4001;
server collector-2:4001;
server collector-3:4001;
}
upstream api {
server api-1:4002;
server api-2:4002;
}
upstream realtime {
ip_hash; # Sticky sessions for WebSocket
server realtime-1:4003;
server realtime-2:4003;
}
server {
listen 443 ssl http2;
server_name analytics.example.com;
ssl_certificate /etc/ssl/certs/analytics.crt;
ssl_certificate_key /etc/ssl/private/analytics.key;
# Collector - high throughput
location /collect {
proxy_pass http://collector;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Don't buffer - fast response
proxy_buffering off;
# Allow large batches
client_max_body_size 1m;
}
# API - standard REST
location /api {
proxy_pass http://api;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Cache GET requests
proxy_cache api_cache;
proxy_cache_valid 200 1m;
proxy_cache_key "$request_method$request_uri";
add_header X-Cache-Status $upstream_cache_status;
}
# WebSocket - realtime
location /realtime {
proxy_pass http://realtime;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
# Long-lived connections
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
}
```
## Monitoring
### Health Checks
All services expose `/health` endpoint:
```json
{
"status": "healthy",
"version": "1.0.0",
"uptime": 86400,
"checks": {
"redis": "ok",
"database": "ok"
}
}
```
### Metrics (Prometheus)
Services expose `/metrics` endpoint:
```
# Collector metrics
analytics_events_received_total{type="engagement"} 1234567
analytics_events_queued_total 1234500
analytics_batch_size_histogram_bucket{le="10"} 50000
# Processor metrics
analytics_events_processed_total 1234000
analytics_processing_duration_seconds_bucket{le="0.1"} 1200000
analytics_queue_depth 500
# API metrics
analytics_api_requests_total{endpoint="/trends",status="200"} 50000
analytics_api_latency_seconds_bucket{le="0.5"} 49000
```
### Grafana Dashboards
Import pre-built dashboards from `/dashboards/`:
- `collector-metrics.json` - Ingestion throughput
- `processor-metrics.json` - Processing performance
- `api-metrics.json` - Query latency and errors
- `business-metrics.json` - Analytics KPIs
## Scaling Guidelines
### Collector Service
- Scale horizontally based on incoming event rate
- Target: <100ms p99 response time
- Rule of thumb: 1 replica per 10,000 events/minute
### Processor Service
- Scale based on queue depth
- Target: Queue depth < 1000
- Increase `CONCURRENCY` before adding replicas
### API Service
- Scale based on query latency
- Target: <500ms p95 for complex queries
- Add read replicas to PostgreSQL for heavy read load
### Database
- Use TimescaleDB compression for historical data
- Partition by month for large deployments
- Consider ClickHouse for >1B events/day

500
docs/nestjs-integration.md Normal file
View file

@ -0,0 +1,500 @@
# NestJS Integration Guide
Server-side analytics tracking for NestJS applications.
## Installation
```bash
npm install @analytics/client
```
## Module Setup
### 1. Create Analytics Module
```typescript
// analytics.module.ts
import { Module, Global, DynamicModule } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { BackendAnalyticsClient, type BackendAnalyticsConfig } from '@analytics/client';
export const ANALYTICS_CLIENT = 'ANALYTICS_CLIENT';
export interface AnalyticsModuleOptions {
collectorUrl: string;
appName: string;
enabled?: boolean;
debug?: boolean;
}
@Global()
@Module({})
export class AnalyticsModule {
static register(options: AnalyticsModuleOptions): DynamicModule {
return {
module: AnalyticsModule,
providers: [
{
provide: ANALYTICS_CLIENT,
useFactory: () => {
const config: BackendAnalyticsConfig = {
apiBaseUrl: options.collectorUrl,
appName: options.appName,
enabled: options.enabled ?? true,
enableDebugLogging: options.debug ?? false,
};
return new BackendAnalyticsClient(config);
},
},
],
exports: [ANALYTICS_CLIENT],
};
}
static registerAsync(options: {
inject?: any[];
useFactory: (...args: any[]) => AnalyticsModuleOptions | Promise<AnalyticsModuleOptions>;
}): DynamicModule {
return {
module: AnalyticsModule,
providers: [
{
provide: ANALYTICS_CLIENT,
inject: options.inject || [],
useFactory: async (...args: any[]) => {
const moduleOptions = await options.useFactory(...args);
const config: BackendAnalyticsConfig = {
apiBaseUrl: moduleOptions.collectorUrl,
appName: moduleOptions.appName,
enabled: moduleOptions.enabled ?? true,
enableDebugLogging: moduleOptions.debug ?? false,
};
return new BackendAnalyticsClient(config);
},
},
],
exports: [ANALYTICS_CLIENT],
};
}
}
```
### 2. Register in AppModule
```typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AnalyticsModule } from './analytics.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
AnalyticsModule.registerAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
collectorUrl: config.get('ANALYTICS_URL', 'http://localhost:4001'),
appName: config.get('APP_NAME', 'my-api'),
enabled: config.get('NODE_ENV') !== 'test',
debug: config.get('NODE_ENV') === 'development',
}),
}),
],
})
export class AppModule {}
```
## Request Tracking Interceptor
Automatically track all HTTP requests.
```typescript
// analytics.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Inject,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
import { BackendAnalyticsClient } from '@analytics/client';
import { ANALYTICS_CLIENT } from './analytics.module';
@Injectable()
export class AnalyticsInterceptor implements NestInterceptor {
constructor(
@Inject(ANALYTICS_CLIENT)
private readonly analytics: BackendAnalyticsClient,
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
const startTime = Date.now();
const sessionId = request.headers['x-session-id'] || this.generateSessionId();
const userId = request.user?.id;
return next.handle().pipe(
tap(() => {
this.trackRequest(request, response, sessionId, userId, startTime, 'success');
}),
catchError((error) => {
this.trackRequest(request, response, sessionId, userId, startTime, 'error', error);
throw error;
}),
);
}
private generateSessionId(): string {
return `srv_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
}
private trackRequest(
request: any,
response: any,
sessionId: string,
userId: string | undefined,
startTime: number,
outcome: 'success' | 'error',
error?: Error,
): void {
const duration = Date.now() - startTime;
this.analytics.trackEngagement({
sessionId,
userId,
type: 'api_request',
action: `${request.method} ${request.route?.path || request.url}`,
metadata: {
method: request.method,
path: request.route?.path || request.url,
statusCode: response.statusCode,
durationMs: duration,
outcome,
errorMessage: error?.message,
ip: this.extractIp(request),
userAgent: request.headers['user-agent'],
},
});
}
private extractIp(request: any): string {
return (
request.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
request.headers['x-real-ip'] ||
request.ip ||
'0.0.0.0'
);
}
}
```
### Register Globally
```typescript
// app.module.ts
import { APP_INTERCEPTOR } from '@nestjs/core';
import { AnalyticsInterceptor } from './analytics.interceptor';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: AnalyticsInterceptor,
},
],
})
export class AppModule {}
```
## Method Decorator
Track specific endpoints with custom events.
```typescript
// track-analytics.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const TRACK_ANALYTICS_KEY = 'track_analytics';
export interface TrackAnalyticsOptions {
event: string;
category?: string;
metadata?: Record<string, unknown>;
extractMetadata?: (context: {
request: any;
response: any;
result: any;
}) => Record<string, unknown>;
}
export const TrackAnalytics = (options: TrackAnalyticsOptions) =>
SetMetadata(TRACK_ANALYTICS_KEY, options);
```
### Usage
```typescript
// user.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { TrackAnalytics } from './track-analytics.decorator';
@Controller('users')
export class UserController {
@Post('signup')
@TrackAnalytics({
event: 'user_signup',
category: 'auth',
extractMetadata: ({ result }) => ({
userId: result.id,
plan: result.plan,
}),
})
async signup(@Body() dto: SignupDto) {
// Your signup logic
return { id: 'user-123', plan: 'free' };
}
}
```
## Service-Level Tracking
Track events directly from services.
```typescript
// order.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { BackendAnalyticsClient } from '@analytics/client';
import { ANALYTICS_CLIENT } from './analytics.module';
@Injectable()
export class OrderService {
constructor(
@Inject(ANALYTICS_CLIENT)
private readonly analytics: BackendAnalyticsClient,
) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
const order = await this.orderRepo.save(dto);
// Track order creation
this.analytics.trackEngagement({
sessionId: dto.sessionId || 'server',
userId: dto.userId,
type: 'commerce',
action: 'order_created',
metadata: {
orderId: order.id,
total: order.total,
itemCount: order.items.length,
},
});
return order;
}
async processPayment(orderId: string, paymentMethod: string): Promise<Order> {
const order = await this.orderRepo.findOne(orderId);
// Process payment...
// Track conversion
this.analytics.trackEngagement({
sessionId: 'server',
userId: order.userId,
type: 'commerce',
action: 'payment_completed',
metadata: {
orderId,
total: order.total,
paymentMethod,
isConversion: true,
conversionValue: order.total,
},
});
return order;
}
}
```
## WebSocket Tracking
Track WebSocket events.
```typescript
// events.gateway.ts
import {
WebSocketGateway,
SubscribeMessage,
ConnectedSocket,
OnGatewayConnection,
OnGatewayDisconnect,
} from '@nestjs/websockets';
import { Inject } from '@nestjs/common';
import { Socket } from 'socket.io';
import { BackendAnalyticsClient } from '@analytics/client';
import { ANALYTICS_CLIENT } from './analytics.module';
@WebSocketGateway()
export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
constructor(
@Inject(ANALYTICS_CLIENT)
private readonly analytics: BackendAnalyticsClient,
) {}
handleConnection(client: Socket) {
const sessionId = client.handshake.query.sessionId as string;
this.analytics.trackEngagement({
sessionId: sessionId || client.id,
type: 'websocket',
action: 'connected',
metadata: {
socketId: client.id,
transport: client.conn.transport.name,
},
});
}
handleDisconnect(client: Socket) {
this.analytics.trackEngagement({
sessionId: client.id,
type: 'websocket',
action: 'disconnected',
metadata: {
socketId: client.id,
reason: client.disconnected ? 'client' : 'server',
},
});
}
@SubscribeMessage('message')
handleMessage(@ConnectedSocket() client: Socket, payload: any) {
this.analytics.trackEngagement({
sessionId: client.id,
type: 'websocket',
action: 'message_received',
metadata: {
messageType: payload.type,
},
});
}
}
```
## Queue Job Tracking
Track background job execution.
```typescript
// email.processor.ts
import { Processor, Process } from '@nestjs/bull';
import { Inject } from '@nestjs/common';
import { Job } from 'bull';
import { BackendAnalyticsClient } from '@analytics/client';
import { ANALYTICS_CLIENT } from './analytics.module';
@Processor('email')
export class EmailProcessor {
constructor(
@Inject(ANALYTICS_CLIENT)
private readonly analytics: BackendAnalyticsClient,
) {}
@Process('send')
async handleSend(job: Job<{ to: string; template: string }>) {
const startTime = Date.now();
try {
await this.sendEmail(job.data);
this.analytics.trackEngagement({
sessionId: 'queue',
type: 'background_job',
action: 'email_sent',
metadata: {
jobId: job.id,
template: job.data.template,
durationMs: Date.now() - startTime,
success: true,
},
});
} catch (error) {
this.analytics.trackEngagement({
sessionId: 'queue',
type: 'background_job',
action: 'email_failed',
metadata: {
jobId: job.id,
template: job.data.template,
durationMs: Date.now() - startTime,
success: false,
error: error.message,
},
});
throw error;
}
}
}
```
## Best Practices
### 1. Use Session ID Header
Pass session ID from frontend:
```typescript
// Frontend
fetch('/api/orders', {
headers: {
'x-session-id': analytics.getSessionId(),
},
});
```
### 2. Track Business Events, Not HTTP
```typescript
// ❌ Don't just track HTTP
this.analytics.trackEngagement({
type: 'api_request',
action: 'POST /orders',
});
// ✅ Track business meaning
this.analytics.trackEngagement({
type: 'commerce',
action: 'order_created',
metadata: { orderId, total, itemCount },
});
```
### 3. Fire-and-Forget
Never await analytics in request path:
```typescript
// ✅ Non-blocking tracking
this.analytics.trackEngagement({...}); // No await
// ❌ Don't block requests
await this.analytics.trackEngagement({...});
```
### 4. Handle Failures Silently
Analytics should never break your app:
```typescript
try {
this.analytics.trackEngagement({...});
} catch {
// Silent failure - analytics issues shouldn't affect users
}
```

398
docs/react-integration.md Normal file
View file

@ -0,0 +1,398 @@
# React Integration Guide
Integrate analytics into React applications with hooks and context.
## Installation
```bash
npm install @analytics/client
```
## Setup
### 1. Create Analytics Provider
```tsx
// analytics-provider.tsx
import { createContext, useContext, useMemo, type ReactNode } from 'react';
import { AnalyticsClient, type AnalyticsConfig } from '@analytics/client';
interface AnalyticsContextValue {
client: AnalyticsClient;
trackEvent: (type: string, action: string, metadata?: Record<string, unknown>) => void;
identify: (userId: string, traits?: Record<string, unknown>) => void;
}
const AnalyticsContext = createContext<AnalyticsContextValue | null>(null);
interface AnalyticsProviderProps {
children: ReactNode;
config: AnalyticsConfig;
}
export function AnalyticsProvider({ children, config }: AnalyticsProviderProps) {
const value = useMemo(() => {
const client = new AnalyticsClient(config);
return {
client,
trackEvent: (type, action, metadata) => {
client.trackEngagement({ type, action, metadata });
},
identify: (userId, traits) => {
client.identify(userId, traits);
},
};
}, [config]);
return (
<AnalyticsContext.Provider value={value}>
{children}
</AnalyticsContext.Provider>
);
}
export function useAnalytics() {
const context = useContext(AnalyticsContext);
if (!context) {
throw new Error('useAnalytics must be used within AnalyticsProvider');
}
return context;
}
```
### 2. Wrap Your App
```tsx
// App.tsx
import { AnalyticsProvider } from './analytics-provider';
function App() {
return (
<AnalyticsProvider
config={{
apiBaseUrl: import.meta.env.VITE_ANALYTICS_URL,
appName: 'my-react-app',
enabled: import.meta.env.PROD,
}}
>
<Router />
</AnalyticsProvider>
);
}
```
## Hooks
### usePageTracking
Track page views on route changes.
```tsx
// hooks/use-page-tracking.ts
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useAnalytics } from '../analytics-provider';
export function usePageTracking() {
const location = useLocation();
const { trackEvent } = useAnalytics();
useEffect(() => {
trackEvent('navigation', 'page_view', {
path: location.pathname,
search: location.search,
});
}, [location.pathname, location.search, trackEvent]);
}
// Usage: Call once in your root component
function AppContent() {
usePageTracking();
return <Outlet />;
}
```
### useClickTracking
Track element clicks.
```tsx
// hooks/use-click-tracking.ts
import { useCallback } from 'react';
import { useAnalytics } from '../analytics-provider';
export function useClickTracking() {
const { trackEvent } = useAnalytics();
return useCallback((
elementName: string,
metadata?: Record<string, unknown>
) => {
trackEvent('click', elementName, metadata);
}, [trackEvent]);
}
// Usage
function CTAButton() {
const trackClick = useClickTracking();
return (
<button onClick={() => trackClick('signup_cta', { location: 'hero' })}>
Sign Up
</button>
);
}
```
### useFormTracking
Track form interactions.
```tsx
// hooks/use-form-tracking.ts
import { useCallback } from 'react';
import { useAnalytics } from '../analytics-provider';
export function useFormTracking(formName: string) {
const { trackEvent } = useAnalytics();
const trackStart = useCallback(() => {
trackEvent('form', 'start', { formName });
}, [trackEvent, formName]);
const trackField = useCallback((fieldName: string) => {
trackEvent('form', 'field_focus', { formName, fieldName });
}, [trackEvent, formName]);
const trackSubmit = useCallback((success: boolean, error?: string) => {
trackEvent('form', success ? 'submit_success' : 'submit_error', {
formName,
success,
error,
});
}, [trackEvent, formName]);
const trackAbandonment = useCallback((lastField?: string) => {
trackEvent('form', 'abandonment', { formName, lastField });
}, [trackEvent, formName]);
return { trackStart, trackField, trackSubmit, trackAbandonment };
}
```
### useScrollTracking
Track scroll depth.
```tsx
// hooks/use-scroll-tracking.ts
import { useEffect, useRef } from 'react';
import { useAnalytics } from '../analytics-provider';
export function useScrollTracking() {
const { trackEvent } = useAnalytics();
const trackedDepths = useRef(new Set<number>());
useEffect(() => {
const thresholds = [25, 50, 75, 90, 100];
const handleScroll = () => {
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
const scrollPercent = Math.round((window.scrollY / scrollHeight) * 100);
for (const threshold of thresholds) {
if (scrollPercent >= threshold && !trackedDepths.current.has(threshold)) {
trackedDepths.current.add(threshold);
trackEvent('scroll', 'depth', { percent: threshold });
}
}
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, [trackEvent]);
}
```
## Component Patterns
### Tracked Link
```tsx
import { Link, type LinkProps } from 'react-router-dom';
import { useClickTracking } from '../hooks/use-click-tracking';
interface TrackedLinkProps extends LinkProps {
trackingName: string;
trackingMetadata?: Record<string, unknown>;
}
export function TrackedLink({
trackingName,
trackingMetadata,
onClick,
...props
}: TrackedLinkProps) {
const trackClick = useClickTracking();
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
trackClick(trackingName, trackingMetadata);
onClick?.(e);
};
return <Link {...props} onClick={handleClick} />;
}
```
### Tracked Button
```tsx
interface TrackedButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
trackingName: string;
trackingMetadata?: Record<string, unknown>;
}
export function TrackedButton({
trackingName,
trackingMetadata,
onClick,
...props
}: TrackedButtonProps) {
const trackClick = useClickTracking();
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
trackClick(trackingName, trackingMetadata);
onClick?.(e);
};
return <button {...props} onClick={handleClick} />;
}
```
### Impression Tracking
Track when elements become visible.
```tsx
import { useEffect, useRef } from 'react';
import { useAnalytics } from '../analytics-provider';
interface UseImpressionTrackingOptions {
elementName: string;
metadata?: Record<string, unknown>;
threshold?: number;
trackOnce?: boolean;
}
export function useImpressionTracking({
elementName,
metadata,
threshold = 0.5,
trackOnce = true,
}: UseImpressionTrackingOptions) {
const { trackEvent } = useAnalytics();
const ref = useRef<HTMLElement>(null);
const hasTracked = useRef(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
if (trackOnce && hasTracked.current) return;
hasTracked.current = true;
trackEvent('impression', elementName, metadata);
}
},
{ threshold }
);
observer.observe(element);
return () => observer.disconnect();
}, [elementName, metadata, threshold, trackOnce, trackEvent]);
return ref;
}
// Usage
function ProductCard({ product }) {
const ref = useImpressionTracking({
elementName: 'product_card',
metadata: { productId: product.id },
});
return <div ref={ref}>...</div>;
}
```
## Best Practices
### 1. Track at the Right Level
```tsx
// ❌ Don't track everything
onClick={() => {
trackClick('button');
trackClick('cta');
trackClick('hero_cta');
}}
// ✅ Track meaningful, specific events
onClick={() => trackClick('hero_signup_cta')}
```
### 2. Use Consistent Naming
```tsx
// ❌ Inconsistent naming
trackEvent('click', 'SignUp');
trackEvent('user_action', 'sign-up');
trackEvent('button', 'signup_button');
// ✅ Consistent naming convention
trackEvent('click', 'signup_cta');
trackEvent('click', 'login_cta');
trackEvent('click', 'pricing_link');
```
### 3. Include Context
```tsx
// ❌ Missing context
trackEvent('click', 'buy_button');
// ✅ Rich context
trackEvent('click', 'buy_button', {
productId: product.id,
price: product.price,
location: 'product_page',
variant: selectedVariant,
});
```
### 4. Handle Loading States
```tsx
function AnalyticsWrapper({ children }) {
const [ready, setReady] = useState(false);
useEffect(() => {
// Initialize analytics
setReady(true);
}, []);
if (!ready) {
// Render children without tracking during init
return <>{children}</>;
}
return (
<AnalyticsProvider config={...}>
{children}
</AnalyticsProvider>
);
}
```