From dc5329e8858cbf4b43a76a3a197455f1f99fcce9 Mon Sep 17 00:00:00 2001 From: Lilith Date: Thu, 29 Jan 2026 08:20:58 -0800 Subject: [PATCH] =?UTF-8?q?chore(docs):=20=F0=9F=93=9D=20Update=20document?= =?UTF-8?q?ation=20files=20in=20/docs=20directory=20(README,=20guides,=20o?= =?UTF-8?q?r=20API=20references)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- docs/api-reference.md | 453 +++++++++++++++++++++++++++++++++ docs/client-sdk.md | 266 ++++++++++++++++++++ docs/deployment.md | 455 +++++++++++++++++++++++++++++++++ docs/nestjs-integration.md | 500 +++++++++++++++++++++++++++++++++++++ docs/react-integration.md | 398 +++++++++++++++++++++++++++++ 5 files changed, 2072 insertions(+) create mode 100644 docs/api-reference.md create mode 100644 docs/client-sdk.md create mode 100644 docs/deployment.md create mode 100644 docs/nestjs-integration.md create mode 100644 docs/react-integration.md diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..a8ddfd4 --- /dev/null +++ b/docs/api-reference.md @@ -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 +``` + +Or via header: + +``` +X-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 +``` diff --git a/docs/client-sdk.md b/docs/client-sdk.md new file mode 100644 index 0000000..50227cb --- /dev/null +++ b/docs/client-sdk.md @@ -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'; +``` diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..f660fef --- /dev/null +++ b/docs/deployment.md @@ -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 diff --git a/docs/nestjs-integration.md b/docs/nestjs-integration.md new file mode 100644 index 0000000..4e5493b --- /dev/null +++ b/docs/nestjs-integration.md @@ -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; + }): 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 { + 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; + extractMetadata?: (context: { + request: any; + response: any; + result: any; + }) => Record; +} + +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 { + 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 { + 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 +} +``` diff --git a/docs/react-integration.md b/docs/react-integration.md new file mode 100644 index 0000000..8a67bd6 --- /dev/null +++ b/docs/react-integration.md @@ -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) => void; + identify: (userId: string, traits?: Record) => void; +} + +const AnalyticsContext = createContext(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 ( + + {children} + + ); +} + +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 ( + + + + ); +} +``` + +## 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 ; +} +``` + +### 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 + ) => { + trackEvent('click', elementName, metadata); + }, [trackEvent]); +} + +// Usage +function CTAButton() { + const trackClick = useClickTracking(); + + return ( + + ); +} +``` + +### 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()); + + 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; +} + +export function TrackedLink({ + trackingName, + trackingMetadata, + onClick, + ...props +}: TrackedLinkProps) { + const trackClick = useClickTracking(); + + const handleClick = (e: React.MouseEvent) => { + trackClick(trackingName, trackingMetadata); + onClick?.(e); + }; + + return ; +} +``` + +### Tracked Button + +```tsx +interface TrackedButtonProps extends React.ButtonHTMLAttributes { + trackingName: string; + trackingMetadata?: Record; +} + +export function TrackedButton({ + trackingName, + trackingMetadata, + onClick, + ...props +}: TrackedButtonProps) { + const trackClick = useClickTracking(); + + const handleClick = (e: React.MouseEvent) => { + trackClick(trackingName, trackingMetadata); + onClick?.(e); + }; + + return