SdkServer
Redis Client
Redis client for real-time analytics and caching
Redis Client
The entrolytics/redis-client package provides a Redis client for real-time analytics, caching, and stream processing with Entrolytics integration.
Installation
npm install @entrolytics/redis-clientQuick Start
Basic Redis Integration
import { EntrolyticsRedisClient } from '@entrolytics/redis-client'
import Redis from 'ioredis'
// Initialize Redis client
const redis = new Redis({
host: 'localhost',
port: 6379,
password: 'your-redis-password'
})
// Initialize Entrolytics Redis client
const analytics = new EntrolyticsRedisClient({
redis,
websiteId: 'your-website-id',
apiKey: 'your-api-key'
})
// Track events with Redis caching
await analytics.track('user_action', {
userId: 'user-123',
action: 'purchase',
amount: 99.99
})
// Real-time metrics
const metrics = await analytics.getRealtimeMetrics('user_actions', {
timeRange: '1h',
groupBy: 'action'
})
console.log('Real-time metrics:', metrics)Stream Processing
import { EntrolyticsRedisClient } from '@entrolytics/redis-client'
// Set up stream processing
await analytics.setupStream('events', {
maxLen: 10000,
consumerGroup: 'analytics-processors'
})
// Process events in real-time
analytics.processStream('events', async (event) => {
console.log('Processing event:', event)
// Enrich event data
const enrichedEvent = await enrichEventData(event)
// Send to Entrolytics
await analytics.track(enrichedEvent.event, enrichedEvent.properties)
// Update Redis metrics
await analytics.updateMetrics(enrichedEvent)
})Caching Layer
// Use Redis as caching layer for analytics
const getCachedAnalytics = async (key, ttl = 300) => {
const cached = await redis.get(`analytics:${key}`)
if (cached) {
return JSON.parse(cached)
}
const data = await analytics.fetchAnalytics(key)
await redis.setex(`analytics:${key}`, ttl, JSON.stringify(data))
return data
}
// Usage
const dashboardData = await getCachedAnalytics('dashboard:overview', 60)Configuration
Client Options
const analytics = new EntrolyticsRedisClient({
redis: redisInstance, // Redis client instance
websiteId: 'your-website-id', // required
apiKey: 'your-api-key', // optional
host: 'https://entrolytics.dev', // custom host
streamKey: 'entrolytics:events', // Redis stream key
metricsKey: 'entrolytics:metrics', // Redis metrics key
cacheKey: 'entrolytics:cache', // Redis cache key prefix
streamMaxLen: 10000, // Max stream length
batchSize: 100, // Batch size for processing
flushInterval: 5000, // Flush interval (ms)
retryAttempts: 3, // Retry attempts
retryDelay: 1000, // Retry delay (ms)
debug: false // Debug mode
})Environment Variables
# .env
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=your-redis-password
ENTROLYTICS_WEBSITE_ID=your-website-id
ENTROLYTICS_API_KEY=your-api-key
ENTROLYTICS_REDIS_STREAM_KEY=entrolytics:events
ENTROLYTICS_REDIS_METRICS_KEY=entrolytics:metrics
ENTROLYTICS_REDIS_CACHE_KEY=entrolytics:cacheRedis Configuration
// Redis client configuration
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT) || 6379,
password: process.env.REDIS_PASSWORD,
db: 0,
retryDelayOnFailover: 100,
maxRetriesPerRequest: 3,
lazyConnect: true,
keepAlive: 30000,
connectTimeout: 10000,
commandTimeout: 5000
})API Reference
Core Client
class EntrolyticsRedisClient {
constructor(options)
// Event tracking with Redis
async track(event, properties = {})
async trackBatch(events)
async page(url, properties = {})
async identify(userId, traits = {})
// Stream operations
async setupStream(streamName, options = {})
async addToStream(streamName, event)
async processStream(streamName, processor, options = {})
async getStreamLength(streamName)
// Metrics operations
async incrementMetric(metric, value = 1, labels = {})
async getMetric(metric, labels = {})
async getMetrics(pattern, timeRange = null)
async setMetric(metric, value, labels = {})
async deleteMetric(metric, labels = {})
// Cache operations
async get(key)
async set(key, value, ttl = null)
async setex(key, ttl, value)
async del(key)
async exists(key)
// Real-time analytics
async getRealtimeMetrics(metric, options = {})
async getTopEvents(timeRange = '1h', limit = 10)
async getActiveUsers(timeRange = '1h')
async getEventCounts(events, timeRange = '1h')
// Aggregation
async aggregateEvents(timeRange, groupBy, filters = {})
async aggregateMetrics(timeRange, metrics, filters = {})
// Cleanup
async flush()
async cleanup(ttl = 86400)
}Stream Processing
// Stream processor options
const streamOptions = {
consumerGroup: 'analytics-processors',
consumerName: 'processor-1',
batchSize: 100,
blockTime: 5000,
maxRetries: 3,
retryDelay: 1000,
deadLetterQueue: 'entrolytics:dlq'
}
// Event processor
const processor = async (event) => {
console.log('Processing:', event)
// Custom processing logic
const processed = await processEvent(event)
// Update metrics
await analytics.incrementMetric('events_processed', 1, {
event_type: event.event,
processor: 'main'
})
return processed
}
// Start processing
await analytics.processStream('events', processor, streamOptions)Metrics API
// Increment counters
await analytics.incrementMetric('page_views', 1, {
page: '/home',
user_type: 'authenticated'
})
// Set gauge values
await analytics.setMetric('active_users', 1250, {
region: 'us-east-1'
})
// Get metrics with time range
const metrics = await analytics.getMetrics('page_views:*', {
timeRange: '1h',
aggregation: 'sum'
})
// Real-time metrics
const realtime = await analytics.getRealtimeMetrics('user_actions', {
timeRange: '5m',
groupBy: 'action',
filters: { user_type: 'premium' }
})Advanced Usage
Real-time Dashboard
// Real-time dashboard data provider
class RealtimeDashboard {
constructor(analytics) {
this.analytics = analytics
}
async getOverview(timeRange = '1h') {
const [totalEvents, activeUsers, topPages, topEvents, errorRate] =
await Promise.all([
this.analytics.getMetric('total_events', { timeRange }),
this.analytics.getActiveUsers(timeRange),
this.analytics.getTopPages(timeRange, 10),
this.analytics.getTopEvents(timeRange, 10),
this.analytics.getMetric('error_rate', { timeRange })
])
return {
totalEvents,
activeUsers,
topPages,
topEvents,
errorRate
}
}
async getEventTimeline(event, timeRange = '1h') {
return await this.analytics.getMetrics(`event:${event}:timeline`, {
timeRange,
granularity: '1m'
})
}
async getUserActivity(userId, timeRange = '24h') {
return await this.analytics.getMetrics(`user:${userId}:*`, {
timeRange,
groupBy: 'action'
})
}
}
// Usage
const dashboard = new RealtimeDashboard(analytics)
// WebSocket endpoint for real-time updates
app.ws('/dashboard/realtime', (ws) => {
const updateDashboard = async () => {
const data = await dashboard.getOverview('5m')
ws.send(JSON.stringify(data))
}
const interval = setInterval(updateDashboard, 5000)
ws.on('close', () => clearInterval(interval))
})Event Enrichment
// Event enrichment pipeline
class EventEnricher {
constructor(analytics, redis) {
this.analytics = analytics
this.redis = redis
}
async enrichEvent(event) {
const enriched = { ...event }
// Add user context
if (event.userId) {
enriched.userContext = await this.getUserContext(event.userId)
}
// Add session context
if (event.sessionId) {
enriched.sessionContext = await this.getSessionContext(event.sessionId)
}
// Add geographic context
if (event.ip) {
enriched.geoContext = await this.getGeoContext(event.ip)
}
// Add device context
if (event.userAgent) {
enriched.deviceContext = await this.getDeviceContext(event.userAgent)
}
return enriched
}
async getUserContext(userId) {
const cacheKey = `user:${userId}:context`
let context = await this.redis.get(cacheKey)
if (!context) {
context = await this.fetchUserContext(userId)
await this.redis.setex(cacheKey, 300, JSON.stringify(context))
}
return JSON.parse(context)
}
async getSessionContext(sessionId) {
return await this.redis.hgetall(`session:${sessionId}`)
}
async getGeoContext(ip) {
// Implement IP geolocation
return {
country: 'US',
region: 'CA',
city: 'San Francisco'
}
}
async getDeviceContext(userAgent) {
// Implement device detection
return {
type: 'desktop',
os: 'macOS',
browser: 'Chrome'
}
}
}
// Usage with stream processing
const enricher = new EventEnricher(analytics, redis)
analytics.processStream('events', async (event) => {
const enriched = await enricher.enrichEvent(event)
await analytics.track(enriched.event, enriched.properties)
})Rate Limiting and Throttling
// Rate limiting with Redis
class RateLimiter {
constructor(redis) {
this.redis = redis
}
async isAllowed(key, limit, window) {
const current = await this.redis.incr(key)
if (current === 1) {
await this.redis.expire(key, window)
}
return current <= limit
}
async getRemaining(key, limit, window) {
const current = (await this.redis.get(key)) || 0
return Math.max(0, limit - parseInt(current))
}
}
// Apply rate limiting to analytics
const rateLimiter = new RateLimiter(redis)
const trackWithRateLimit = async (event, properties) => {
const key = `rate_limit:analytics:${event}`
const allowed = await rateLimiter.isAllowed(key, 100, 60) // 100 events per minute
if (allowed) {
await analytics.track(event, properties)
} else {
// Queue for later processing
await analytics.addToStream('rate_limited', { event, properties })
}
}Background Processing
// Background event processor
class BackgroundProcessor {
constructor(analytics, redis) {
this.analytics = analytics
this.redis = redis
this.processing = false
}
async start() {
this.processing = true
while (this.processing) {
try {
const events = await this.redis.xreadgroup(
'GROUP',
'processors',
'bg-processor',
'COUNT',
'100',
'BLOCK',
'5000',
'STREAMS',
'background_events',
'>'
)
if (events && events.length > 0) {
await this.processEvents(events)
}
} catch (error) {
console.error('Background processing error:', error)
await this.sleep(5000)
}
}
}
async processEvents(events) {
const processed = []
for (const [stream, streamEvents] of events) {
for (const [id, event] of streamEvents) {
try {
await this.analytics.track(event.event, event.properties)
processed.push([stream, id])
} catch (error) {
console.error('Failed to process event:', error)
}
}
}
// Acknowledge processed events
if (processed.length > 0) {
await this.redis.xack(
'background_events',
'processors',
...processed.map((p) => p[1])
)
}
}
stop() {
this.processing = false
}
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
}
// Start background processor
const processor = new BackgroundProcessor(analytics, redis)
processor.start()Testing
Unit Tests
import { EntrolyticsRedisClient } from '@entrolytics/redis-client'
import Redis from 'ioredis-mock'
describe('EntrolyticsRedisClient', () => {
let analytics
let mockRedis
beforeEach(() => {
mockRedis = new Redis()
analytics = new EntrolyticsRedisClient({
redis: mockRedis,
websiteId: 'test-website-id'
})
})
afterEach(async () => {
await mockRedis.flushall()
})
test('should track events', async () => {
await analytics.track('test_event', { property: 'value' })
const events = await mockRedis.xrange('entrolytics:events', '-', '+')
expect(events).toHaveLength(1)
const event = JSON.parse(events[0][1].event)
expect(event.event).toBe('test_event')
expect(event.properties.property).toBe('value')
})
test('should increment metrics', async () => {
await analytics.incrementMetric('test_metric', 5, { tag: 'test' })
const value = await analytics.getMetric('test_metric', { tag: 'test' })
expect(value).toBe(5)
})
test('should cache data', async () => {
await analytics.set('test_key', { data: 'test' }, 300)
const cached = await analytics.get('test_key')
expect(cached.data).toBe('test')
})
})Integration Tests
describe('Redis Integration', () => {
let analytics
let redis
beforeAll(async () => {
redis = new Redis({
host: 'localhost',
port: 6379,
db: 15 // Use test database
})
analytics = new EntrolyticsRedisClient({
redis,
websiteId: 'integration-test'
})
})
afterAll(async () => {
await redis.flushdb()
await redis.quit()
})
test('should process events in real-time', async () => {
const processedEvents = []
await analytics.setupStream('test_events')
analytics.processStream('test_events', async (event) => {
processedEvents.push(event)
})
// Add events to stream
await analytics.addToStream('test_events', {
event: 'test_event',
properties: { test: true }
})
// Wait for processing
await new Promise((resolve) => setTimeout(resolve, 1000))
expect(processedEvents).toHaveLength(1)
expect(processedEvents[0].event).toBe('test_event')
})
})Performance Optimization
Connection Pooling
import Redis from 'ioredis'
// Configure connection pool
const redis = new Redis({
host: 'localhost',
port: 6379,
maxRetriesPerRequest: 3,
retryDelayOnFailover: 100,
lazyConnect: true,
keepAlive: 30000,
family: 4,
connectTimeout: 10000,
commandTimeout: 5000
})
// Multiple Redis instances for different purposes
const analytics = new EntrolyticsRedisClient({
redis: redis, // Main operations
cacheRedis: new Redis({ db: 1 }), // Cache operations
streamRedis: new Redis({ db: 2 }) // Stream operations
})Batch Operations
// Batch event processing
class BatchProcessor {
constructor(analytics, batchSize = 100, flushInterval = 5000) {
this.analytics = analytics
this.batchSize = batchSize
this.flushInterval = flushInterval
this.batch = []
this.timer = null
}
add(event) {
this.batch.push(event)
if (this.batch.length >= this.batchSize) {
this.flush()
} else if (!this.timer) {
this.timer = setTimeout(() => this.flush(), this.flushInterval)
}
}
async flush() {
if (this.batch.length === 0) return
const events = this.batch.splice(0)
this.timer = null
await this.analytics.trackBatch(events)
}
}
// Usage
const batchProcessor = new BatchProcessor(analytics)
// Add events to batch
batchProcessor.add({ event: 'user_action', properties: { type: 'click' } })
batchProcessor.add({ event: 'page_view', properties: { page: '/home' } })Memory Management
// Memory-efficient stream processing
const processStreamEfficiently = async (streamName, processor) => {
let lastId = '$'
while (true) {
try {
const results = await redis.xread(
'COUNT',
'10',
'BLOCK',
'1000',
'STREAMS',
streamName,
lastId
)
if (!results) continue
const [[, events]] = results
for (const [id, event] of events) {
await processor(event)
lastId = id
}
// Acknowledge processed events
await redis.xack(streamName, 'processors', ...events.map((e) => e[0]))
} catch (error) {
console.error('Stream processing error:', error)
await new Promise((resolve) => setTimeout(resolve, 5000))
}
}
}Troubleshooting
Best Practices
Migration Guide
From Direct Redis Usage
// Before - Direct Redis
import Redis from 'ioredis'
const redis = new Redis()
await redis.set('analytics:event', JSON.stringify(event))
// After - Entrolytics Redis Client
import { EntrolyticsRedisClient } from '@entrolytics/redis-client'
const analytics = new EntrolyticsRedisClient({ redis, websiteId: 'your-id' })
await analytics.track(event.event, event.properties)From Other Analytics Libraries
// Before - Other analytics
import Analytics from 'other-analytics'
Analytics.track('event', properties)
// After - Entrolytics with Redis
import { EntrolyticsRedisClient } from '@entrolytics/redis-client'
const analytics = new EntrolyticsRedisClient({ redis, websiteId: 'your-id' })
await analytics.track('event', properties)
// Additional Redis features
await analytics.incrementMetric('counter', 1)
const cached = await analytics.get('cache:key')Redis client for Entrolytics - First-party growth analytics for the edge