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:
parent
85235e6977
commit
dc5329e885
5 changed files with 2072 additions and 0 deletions
453
docs/api-reference.md
Normal file
453
docs/api-reference.md
Normal 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
266
docs/client-sdk.md
Normal 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
455
docs/deployment.md
Normal 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
500
docs/nestjs-integration.md
Normal 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
398
docs/react-integration.md
Normal 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>
|
||||
);
|
||||
}
|
||||
```
|
||||
Loading…
Add table
Reference in a new issue