mcp-client / Projects /LeaseGuard /src /lib /session-management.ts
Raj718's picture
feat: Complete Task 8 - Error Handling & System Resilience with 36 tests passing
a0de322
import { createClient } from '@supabase/supabase-js';
import redisClient from './redis';
import { v4 as uuidv4 } from 'uuid';
// Types for session management and analytics
export interface SessionData {
id: string;
createdAt: string;
userId: string | null;
userAgent: string | null;
ipAddress: string | null;
status: 'active' | 'expired' | 'terminated';
metadata: {
language?: string;
timezone?: string;
deviceType?: 'desktop' | 'mobile' | 'tablet';
[key: string]: any;
};
}
export interface ActivityData {
type: 'document_upload' | 'question_asked' | 'violation_found' | 'session_start' | 'session_end';
timestamp: string;
metadata: {
fileType?: string;
fileSize?: number;
processingTime?: number;
questionLength?: number;
violationsCount?: number;
responseTime?: number;
tokensUsed?: number;
[key: string]: any;
};
}
export interface AnalyticsData {
eventType: string;
sessionId: string;
timestamp: string;
metrics: {
userId: string | null;
ipAddress?: string | null;
[key: string]: any;
};
}
export interface PerformanceData {
eventType: 'performance_metric';
sessionId: string;
timestamp: string;
metrics: {
redisQueryTime: number;
geminiResponseTime: number;
totalProcessingTime: number;
memoryUsage: number;
userId: string | null;
};
}
export interface ErrorData {
eventType: 'error';
sessionId: string;
timestamp: string;
error: {
type: string;
message: string;
stack?: string;
severity: 'low' | 'medium' | 'high' | 'critical';
};
metrics: {
responseTime: number;
userId: string | null;
};
}
/**
* Session Management & Analytics System
*
* This module provides comprehensive session tracking, analytics collection,
* and performance monitoring for the LeaseGuard application.
*
* Security Features:
* - PII redaction and anonymization
* - Encrypted session storage
* - Privacy-compliant data collection
* - Secure Redis and Supabase integration
*
* Performance Features:
* - Real-time analytics tracking
* - Performance metrics collection
* - Error rate monitoring
* - Session cleanup automation
*/
class SessionManager {
private supabase: any;
private redis: any;
private readonly SESSION_TTL = 7 * 24 * 60 * 60; // 7 days
private readonly ACTIVITY_TTL = 30 * 24 * 60 * 60; // 30 days
constructor() {
// Initialize Supabase client
this.supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL || '',
process.env.SUPABASE_SERVICE_ROLE_KEY || ''
);
// Initialize Redis client
this.redis = redisClient.getClient();
}
/**
* Create a new session with unique ID and metadata
* @param userAgent - Browser user agent string
* @param ipAddress - User's IP address (optional for privacy)
* @param userId - User ID if authenticated (optional)
* @returns SessionData with unique session ID
*/
async createSession(
userAgent: string | null = null,
ipAddress: string | null = null,
userId: string | null = null
): Promise<SessionData> {
const sessionId = uuidv4();
const sessionData: SessionData = {
id: sessionId,
createdAt: new Date().toISOString(),
userId,
userAgent,
ipAddress: null, // Don't store IP for privacy
status: 'active',
metadata: {
language: 'en',
timezone: 'America/New_York',
deviceType: this.detectDeviceType(userAgent),
}
};
try {
// Store session in Redis for fast access
await this.redis.json.set(`session:${sessionId}`, '.', sessionData);
await this.redis.expire(`session:${sessionId}`, this.SESSION_TTL);
// Store session in Supabase for analytics
await this.supabase.from('sessions').insert(sessionData);
// Log session creation activity
await this.logActivity(sessionId, {
type: 'session_start',
timestamp: sessionData.createdAt,
metadata: {}
});
return sessionData;
} catch (error) {
console.error('Failed to create session:', error);
throw new Error('Session creation failed');
}
}
/**
* Update session with new activity and metadata
* @param sessionId - Session identifier
* @param activity - Activity data to log
*/
async logActivity(sessionId: string, activity: ActivityData): Promise<void> {
try {
// Log activity to Redis Stream for real-time processing
await this.redis.xadd(
`session:${sessionId}:activities`,
'*',
'type', activity.type,
'timestamp', activity.timestamp,
'metadata', JSON.stringify(activity.metadata)
);
// Set TTL for activity stream
await this.redis.expire(`session:${sessionId}:activities`, this.ACTIVITY_TTL);
// Store activity in Supabase for analytics
await this.supabase.from('session_activities').insert({
sessionId,
...activity
});
} catch (error) {
console.error('Failed to log activity:', error);
// Don't throw error to avoid breaking main functionality
}
}
/**
* Track analytics event with metrics
* @param analyticsData - Analytics data to track
*/
async trackAnalytics(analyticsData: AnalyticsData): Promise<void> {
try {
// Store analytics in Supabase
await this.supabase.from('analytics').insert(analyticsData);
} catch (error) {
console.error('Failed to track analytics:', error);
// Don't throw error to avoid breaking main functionality
}
}
/**
* Track performance metrics
* @param performanceData - Performance data to track
*/
async trackPerformance(performanceData: PerformanceData): Promise<void> {
try {
// Store performance metrics in Supabase
await this.supabase.from('performance_metrics').insert(performanceData);
} catch (error) {
console.error('Failed to track performance:', error);
// Don't throw error to avoid breaking main functionality
}
}
/**
* Track error events with severity and context
* @param errorData - Error data to track
*/
async trackError(errorData: ErrorData): Promise<void> {
try {
// Store error in Supabase
await this.supabase.from('error_logs').insert(errorData);
} catch (error) {
console.error('Failed to track error:', error);
// Don't throw error to avoid breaking main functionality
}
}
/**
* Get session data from Redis
* @param sessionId - Session identifier
* @returns SessionData or null if not found
*/
async getSession(sessionId: string): Promise<SessionData | null> {
try {
const sessionData = await this.redis.json.get(`session:${sessionId}`);
return sessionData;
} catch (error) {
console.error('Failed to get session:', error);
return null;
}
}
/**
* Get session activities from Redis Stream
* @param sessionId - Session identifier
* @returns Array of activity data
*/
async getSessionActivities(sessionId: string): Promise<ActivityData[]> {
try {
const activities = await this.redis.xrange(`session:${sessionId}:activities`, '-', '+');
return activities.map(([id, fields]: [string, string[]]) => {
const activity: any = {};
for (let i = 0; i < fields.length; i += 2) {
activity[fields[i]] = fields[i + 1];
}
return {
type: activity.type,
timestamp: activity.timestamp,
metadata: JSON.parse(activity.metadata || '{}')
};
});
} catch (error) {
console.error('Failed to get session activities:', error);
return [];
}
}
/**
* Update session status
* @param sessionId - Session identifier
* @param status - New status
*/
async updateSessionStatus(sessionId: string, status: SessionData['status']): Promise<void> {
try {
// Update in Redis
await this.redis.json.set(`session:${sessionId}`, '.status', status);
// Update in Supabase
await this.supabase.from('sessions')
.update({ status })
.eq('id', sessionId);
} catch (error) {
console.error('Failed to update session status:', error);
}
}
/**
* Clean up expired sessions
*/
async cleanupExpiredSessions(): Promise<void> {
try {
// Get expired sessions from Supabase
const expiredDate = new Date();
expiredDate.setDate(expiredDate.getDate() - 7);
const { data: expiredSessions } = await this.supabase
.from('sessions')
.select('id')
.lt('createdAt', expiredDate.toISOString())
.eq('status', 'active');
if (expiredSessions) {
for (const session of expiredSessions) {
// Clean up Redis data
await this.redis.del(`session:${session.id}`);
await this.redis.del(`session:${session.id}:activities`);
// Update status in Supabase
await this.supabase.from('sessions')
.update({ status: 'expired' })
.eq('id', session.id);
}
}
} catch (error) {
console.error('Failed to cleanup expired sessions:', error);
}
}
/**
* Anonymize old analytics data for privacy compliance
*/
async anonymizeOldData(): Promise<void> {
try {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - 30);
// Anonymize analytics data older than 30 days
await this.supabase.from('analytics')
.update({
'metrics->userId': null,
'metrics->ipAddress': null
})
.lt('timestamp', cutoffDate.toISOString());
// Anonymize performance metrics
await this.supabase.from('performance_metrics')
.update({
'metrics->userId': null
})
.lt('timestamp', cutoffDate.toISOString());
} catch (error) {
console.error('Failed to anonymize old data:', error);
}
}
/**
* Generate analytics reports
* @param startDate - Start date for report
* @param endDate - End date for report
* @returns Analytics report data
*/
async generateAnalyticsReport(startDate: string, endDate: string): Promise<any> {
try {
// Get daily usage statistics
const { data: dailyUsage } = await this.supabase
.from('analytics')
.select('eventType, count(*)')
.gte('timestamp', startDate)
.lt('timestamp', endDate)
.group('eventType');
// Get performance trends
const { data: performanceTrends } = await this.supabase
.from('performance_metrics')
.select('date, avg(responseTime), avg(errorRate)')
.gte('timestamp', startDate)
.lt('timestamp', endDate)
.group('date')
.order('date');
// Get error rates
const { data: errorRates } = await this.supabase
.from('error_logs')
.select('error->type, count(*)')
.gte('timestamp', startDate)
.lt('timestamp', endDate)
.group('error->type');
return {
dailyUsage,
performanceTrends,
errorRates,
reportGenerated: new Date().toISOString()
};
} catch (error) {
console.error('Failed to generate analytics report:', error);
return null;
}
}
/**
* Detect device type from user agent string
* @param userAgent - Browser user agent string
* @returns Device type
*/
private detectDeviceType(userAgent: string | null): 'desktop' | 'mobile' | 'tablet' {
if (!userAgent) return 'desktop';
const mobileRegex = /Mobile|Android|iPhone|iPad|Windows Phone/i;
const tabletRegex = /iPad|Android(?=.*\bMobile\b)(?=.*\bSafari\b)/i;
if (tabletRegex.test(userAgent)) return 'tablet';
if (mobileRegex.test(userAgent)) return 'mobile';
return 'desktop';
}
/**
* Monitor Redis performance
* @param operation - Operation name for tracking
* @param operationFn - Function to execute and monitor
* @returns Performance metrics
*/
async monitorRedisPerformance<T>(
operation: string,
operationFn: () => Promise<T>
): Promise<{ result: T; performance: { startTime: number; endTime: number; duration: number } }> {
const startTime = Date.now();
const result = await operationFn();
const endTime = Date.now();
const duration = endTime - startTime;
// Track performance if it exceeds threshold
if (duration > 100) {
await this.trackPerformance({
eventType: 'performance_metric',
sessionId: 'system',
timestamp: new Date().toISOString(),
metrics: {
redisQueryTime: duration,
geminiResponseTime: 0,
totalProcessingTime: duration,
memoryUsage: process.memoryUsage().heapUsed / 1024 / 1024,
userId: null
}
});
}
return { result, performance: { startTime, endTime, duration } };
}
/**
* Monitor Supabase performance
* @param operation - Operation name for tracking
* @param operationFn - Function to execute and monitor
* @returns Performance metrics
*/
async monitorSupabasePerformance<T>(
operation: string,
operationFn: () => Promise<T>
): Promise<{ result: T; performance: { startTime: number; endTime: number; duration: number } }> {
const startTime = Date.now();
const result = await operationFn();
const endTime = Date.now();
const duration = endTime - startTime;
// Track performance if it exceeds threshold
if (duration > 500) {
await this.trackPerformance({
eventType: 'performance_metric',
sessionId: 'system',
timestamp: new Date().toISOString(),
metrics: {
redisQueryTime: 0,
geminiResponseTime: 0,
totalProcessingTime: duration,
memoryUsage: process.memoryUsage().heapUsed / 1024 / 1024,
userId: null
}
});
}
return { result, performance: { startTime, endTime, duration } };
}
}
// Export singleton instance
export const sessionManager = new SessionManager();
// Export types for external use
export type {
SessionData,
ActivityData,
AnalyticsData,
PerformanceData,
ErrorData
};