mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 15:31:13 -05:00
Implement Phase 1 by adding: - supabase/functions/_shared/rateLimitMetrics.ts: in-memory rate limit metrics utilities (record, query, stats, clear, and helpers) - supabase/functions/_shared/authHelpers.ts: auth helpers for extracting userId, client IP, and rate-limit keys (code scaffolding)
145 lines
3.8 KiB
TypeScript
145 lines
3.8 KiB
TypeScript
/**
|
|
* Rate Limit Metrics Tracking
|
|
*
|
|
* In-memory metrics collection for rate limiting operations.
|
|
* Tracks accepted/rejected requests, patterns, and provides analytics.
|
|
*/
|
|
|
|
export interface RateLimitMetric {
|
|
timestamp: number;
|
|
functionName: string;
|
|
clientIP: string;
|
|
userId?: string;
|
|
allowed: boolean;
|
|
remaining: number;
|
|
retryAfter?: number;
|
|
tier: string;
|
|
}
|
|
|
|
export interface MetricsStats {
|
|
totalRequests: number;
|
|
allowedRequests: number;
|
|
blockedRequests: number;
|
|
blockRate: number;
|
|
uniqueIPs: number;
|
|
uniqueUsers: number;
|
|
topBlockedIPs: Array<{ ip: string; count: number }>;
|
|
topBlockedUsers: Array<{ userId: string; count: number }>;
|
|
tierDistribution: Record<string, number>;
|
|
}
|
|
|
|
// In-memory storage for metrics
|
|
const metricsStore: RateLimitMetric[] = [];
|
|
const MAX_METRICS = 10000; // Keep last 10k metrics
|
|
|
|
/**
|
|
* Record a rate limit check result
|
|
*/
|
|
export function recordRateLimitMetric(metric: RateLimitMetric): void {
|
|
metricsStore.push(metric);
|
|
|
|
// Trim oldest metrics if we exceed max
|
|
if (metricsStore.length > MAX_METRICS) {
|
|
metricsStore.splice(0, metricsStore.length - MAX_METRICS);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get recent metrics
|
|
*/
|
|
export function getRecentMetrics(limit: number = 100): RateLimitMetric[] {
|
|
return metricsStore.slice(-limit);
|
|
}
|
|
|
|
/**
|
|
* Get aggregated statistics for a time window
|
|
*/
|
|
export function getMetricsStats(timeWindowMs: number = 60000): MetricsStats {
|
|
const now = Date.now();
|
|
const cutoff = now - timeWindowMs;
|
|
const recentMetrics = metricsStore.filter(m => m.timestamp >= cutoff);
|
|
|
|
const allowedRequests = recentMetrics.filter(m => m.allowed).length;
|
|
const blockedRequests = recentMetrics.filter(m => !m.allowed).length;
|
|
const totalRequests = recentMetrics.length;
|
|
|
|
// Track unique IPs and users
|
|
const uniqueIPs = new Set(recentMetrics.map(m => m.clientIP)).size;
|
|
const uniqueUsers = new Set(
|
|
recentMetrics.filter(m => m.userId).map(m => m.userId)
|
|
).size;
|
|
|
|
// Find top blocked IPs
|
|
const ipBlockCounts = new Map<string, number>();
|
|
const userBlockCounts = new Map<string, number>();
|
|
const tierCounts = new Map<string, number>();
|
|
|
|
recentMetrics.forEach(metric => {
|
|
if (!metric.allowed) {
|
|
ipBlockCounts.set(metric.clientIP, (ipBlockCounts.get(metric.clientIP) || 0) + 1);
|
|
if (metric.userId) {
|
|
userBlockCounts.set(metric.userId, (userBlockCounts.get(metric.userId) || 0) + 1);
|
|
}
|
|
}
|
|
tierCounts.set(metric.tier, (tierCounts.get(metric.tier) || 0) + 1);
|
|
});
|
|
|
|
const topBlockedIPs = Array.from(ipBlockCounts.entries())
|
|
.map(([ip, count]) => ({ ip, count }))
|
|
.sort((a, b) => b.count - a.count)
|
|
.slice(0, 10);
|
|
|
|
const topBlockedUsers = Array.from(userBlockCounts.entries())
|
|
.map(([userId, count]) => ({ userId, count }))
|
|
.sort((a, b) => b.count - a.count)
|
|
.slice(0, 10);
|
|
|
|
const tierDistribution = Object.fromEntries(tierCounts);
|
|
|
|
return {
|
|
totalRequests,
|
|
allowedRequests,
|
|
blockedRequests,
|
|
blockRate: totalRequests > 0 ? blockedRequests / totalRequests : 0,
|
|
uniqueIPs,
|
|
uniqueUsers,
|
|
topBlockedIPs,
|
|
topBlockedUsers,
|
|
tierDistribution,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Clear all metrics (useful for testing)
|
|
*/
|
|
export function clearMetrics(): void {
|
|
metricsStore.length = 0;
|
|
}
|
|
|
|
/**
|
|
* Get metrics for a specific function
|
|
*/
|
|
export function getFunctionMetrics(functionName: string, limit: number = 100): RateLimitMetric[] {
|
|
return metricsStore
|
|
.filter(m => m.functionName === functionName)
|
|
.slice(-limit);
|
|
}
|
|
|
|
/**
|
|
* Get metrics for a specific user
|
|
*/
|
|
export function getUserMetrics(userId: string, limit: number = 100): RateLimitMetric[] {
|
|
return metricsStore
|
|
.filter(m => m.userId === userId)
|
|
.slice(-limit);
|
|
}
|
|
|
|
/**
|
|
* Get metrics for a specific IP
|
|
*/
|
|
export function getIPMetrics(clientIP: string, limit: number = 100): RateLimitMetric[] {
|
|
return metricsStore
|
|
.filter(m => m.clientIP === clientIP)
|
|
.slice(-limit);
|
|
}
|