UNPKG

@ahmedhegazee/nestjs-telescope

Version:

Advanced observability and monitoring solution for NestJS applications with ML-powered analytics, enterprise features, and production-ready scaling

811 lines 32.8 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var AnalyticsService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.AnalyticsService = void 0; const common_1 = require("@nestjs/common"); const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const telescope_service_1 = require("./telescope.service"); const performance_correlation_service_1 = require("./performance-correlation.service"); let AnalyticsService = AnalyticsService_1 = class AnalyticsService { constructor(telescopeService, performanceCorrelationService) { this.telescopeService = telescopeService; this.performanceCorrelationService = performanceCorrelationService; this.logger = new common_1.Logger(AnalyticsService_1.name); this.destroy$ = new rxjs_1.Subject(); this.analyticsSubject = new rxjs_1.Subject(); this.currentAnalytics = this.initializeAnalytics(); } async onModuleInit() { this.startAnalyticsProcessing(); this.logger.log('Analytics service initialized'); } initializeAnalytics() { const now = new Date(); const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); return { timestamp: now, timeRange: { start: oneHourAgo, end: now, }, overview: { totalRequests: 0, totalErrors: 0, totalQueries: 0, totalCacheOps: 0, totalJobs: 0, averageResponseTime: 0, errorRate: 0, throughput: 0, activeUsers: 0, peakConcurrency: 0, }, performance: { responseTimeDistribution: { buckets: [], percentiles: { p50: 0, p75: 0, p90: 0, p95: 0, p99: 0 }, }, slowestEndpoints: [], resourceUsage: { cpu: [], memory: [], connections: [], diskIO: [], }, bottleneckAnalysis: [], }, errors: { errorDistribution: { byType: [], bySeverity: [], byComponent: [], }, topErrors: [], errorTrends: [], impactAnalysis: [], }, database: { queryDistribution: { byType: [], byTable: [], byComplexity: [], }, slowQueries: [], connectionHealth: { totalConnections: 0, activeConnections: 0, idleConnections: 0, maxConnections: 0, healthScore: 100, issues: [], }, indexUsage: [], }, cache: { hitRateDistribution: { hitRate: 0, missRate: 0, byOperation: [], byKeyPattern: [], }, topKeys: [], performanceMetrics: { averageResponseTime: 0, throughput: 0, memoryUsage: 0, evictionRate: 0, trends: [], }, evictionAnalysis: [], }, jobs: { queueHealth: [], processingTimes: { byType: [], byQueue: [], byStatus: [], }, failureAnalysis: [], throughputMetrics: [], }, users: { activeUsers: [], sessionAnalysis: { totalSessions: 0, averageSessionDuration: 0, averageRequestsPerSession: 0, bounceRate: 0, mostActiveHours: [], }, geographicDistribution: [], deviceAnalysis: [], }, trends: { trafficTrends: [], performanceTrends: [], errorTrends: [], predictions: [], }, alerts: { activeAlerts: [], alertTrends: [], anomalies: [], }, }; } startAnalyticsProcessing() { (0, rxjs_1.interval)(5 * 60 * 1000) .pipe((0, operators_1.takeUntil)(this.destroy$)) .subscribe(() => { this.updateAnalytics(); }); this.updateAnalytics(); } async updateAnalytics() { try { const entries = await this.telescopeService.getEntries(); const performanceMetrics = this.performanceCorrelationService.getMetrics(); const correlations = this.performanceCorrelationService.getRecentCorrelations(1000); this.updateOverview(entries, performanceMetrics); this.updatePerformanceAnalytics(entries, correlations); this.updateErrorAnalytics(entries); this.updateDatabaseAnalytics(entries); this.updateCacheAnalytics(entries); this.updateJobAnalytics(entries); this.updateUserAnalytics(entries); this.updateTrends(entries); this.updateAlertsAndAnomalies(); this.currentAnalytics.timestamp = new Date(); this.analyticsSubject.next({ ...this.currentAnalytics }); this.logger.debug('Analytics updated successfully'); } catch (error) { this.logger.error('Failed to update analytics:', error); } } updateOverview(entries, performanceMetrics) { const requests = entries.filter((e) => e.type === 'request'); const errors = entries.filter((e) => e.type === 'exception'); const queries = entries.filter((e) => e.type === 'query'); const cacheOps = entries.filter((e) => e.type === 'cache'); const jobs = entries.filter((e) => e.type === 'job'); this.currentAnalytics.overview = { totalRequests: requests.length, totalErrors: errors.length, totalQueries: queries.length, totalCacheOps: cacheOps.length, totalJobs: jobs.length, averageResponseTime: performanceMetrics.averageResponseTime || 0, errorRate: performanceMetrics.errorRate || 0, throughput: performanceMetrics.throughput || 0, activeUsers: this.calculateActiveUsers(requests), peakConcurrency: this.calculatePeakConcurrency(requests), }; } updatePerformanceAnalytics(entries, correlations) { const requests = entries.filter((e) => e.type === 'request'); const responseTimes = requests.map((r) => r.content?.duration || 0).filter((d) => d > 0); this.currentAnalytics.performance.responseTimeDistribution = this.calculateResponseTimeDistribution(responseTimes); this.currentAnalytics.performance.slowestEndpoints = this.calculateSlowestEndpoints(requests); this.currentAnalytics.performance.resourceUsage = this.calculateResourceUsage(entries); this.currentAnalytics.performance.bottleneckAnalysis = this.calculateBottleneckAnalysis(correlations); } updateErrorAnalytics(entries) { const errors = entries.filter((e) => e.type === 'exception'); this.currentAnalytics.errors.errorDistribution = this.calculateErrorDistribution(errors); this.currentAnalytics.errors.topErrors = this.calculateTopErrors(errors); this.currentAnalytics.errors.errorTrends = this.convertErrorsToTimeSeries(errors); this.currentAnalytics.errors.impactAnalysis = this.calculateErrorImpact(errors); } updateDatabaseAnalytics(entries) { const queries = entries.filter((e) => e.type === 'query'); this.currentAnalytics.database.queryDistribution = this.calculateQueryDistribution(queries); this.currentAnalytics.database.slowQueries = this.calculateSlowQueries(queries); this.currentAnalytics.database.connectionHealth = this.calculateConnectionHealth(queries); this.currentAnalytics.database.indexUsage = this.calculateIndexUsage(queries); } updateCacheAnalytics(entries) { const cacheOps = entries.filter((e) => e.type === 'cache'); this.currentAnalytics.cache.hitRateDistribution = this.calculateCacheDistribution(cacheOps); this.currentAnalytics.cache.topKeys = this.calculateTopCacheKeys(cacheOps); this.currentAnalytics.cache.performanceMetrics = this.calculateCachePerformance(cacheOps); this.currentAnalytics.cache.evictionAnalysis = this.calculateEvictionAnalysis(cacheOps); } updateJobAnalytics(entries) { const jobs = entries.filter((e) => e.type === 'job'); this.currentAnalytics.jobs.queueHealth = this.calculateQueueHealth(jobs); this.currentAnalytics.jobs.processingTimes = this.calculateJobDistribution(jobs); this.currentAnalytics.jobs.failureAnalysis = this.calculateJobFailureAnalysis(jobs); this.currentAnalytics.jobs.throughputMetrics = this.calculateJobThroughput(jobs); } updateUserAnalytics(entries) { const requests = entries.filter((e) => e.type === 'request'); this.currentAnalytics.users.activeUsers = this.calculateActiveUserMetrics(requests); this.currentAnalytics.users.sessionAnalysis = this.calculateSessionAnalysis(requests); this.currentAnalytics.users.geographicDistribution = this.calculateGeographicDistribution(requests); this.currentAnalytics.users.deviceAnalysis = this.calculateDeviceAnalysis(requests); } updateTrends(entries) { this.currentAnalytics.trends.trafficTrends = this.calculateTrafficTrends(entries); this.currentAnalytics.trends.performanceTrends = this.calculatePerformanceTrends(entries); this.currentAnalytics.trends.errorTrends = this.convertErrorsToTimeSeries(entries.filter((e) => e.type === 'exception')); this.currentAnalytics.trends.predictions = this.calculatePredictions(entries); } updateAlertsAndAnomalies() { this.currentAnalytics.alerts = { activeAlerts: [], alertTrends: [], anomalies: [], }; } calculateActiveUsers(requests) { const uniqueUsers = new Set(requests.map((r) => r.content?.userId || r.content?.ip).filter(Boolean)); return uniqueUsers.size; } calculatePeakConcurrency(requests) { return Math.max(1, Math.floor(requests.length / 60)); } calculateResponseTimeDistribution(responseTimes) { if (responseTimes.length === 0) { return { buckets: [], percentiles: { p50: 0, p75: 0, p90: 0, p95: 0, p99: 0 }, }; } const sorted = [...responseTimes].sort((a, b) => a - b); const buckets = [ { range: '<100ms', count: 0, percentage: 0 }, { range: '100-500ms', count: 0, percentage: 0 }, { range: '500ms-1s', count: 0, percentage: 0 }, { range: '1-5s', count: 0, percentage: 0 }, { range: '>5s', count: 0, percentage: 0 }, ]; sorted.forEach((time) => { if (time !== undefined) { if (time < 100) buckets[0].count++; else if (time < 500) buckets[1].count++; else if (time < 1000) buckets[2].count++; else if (time < 5000) buckets[3].count++; else buckets[4].count++; } }); buckets.forEach((bucket) => { bucket.percentage = (bucket.count / sorted.length) * 100; }); return { buckets, percentiles: { p50: sorted[Math.floor(sorted.length * 0.5)] || 0, p75: sorted[Math.floor(sorted.length * 0.75)] || 0, p90: sorted[Math.floor(sorted.length * 0.9)] || 0, p95: sorted[Math.floor(sorted.length * 0.95)] || 0, p99: sorted[Math.floor(sorted.length * 0.99)] || 0, }, }; } calculateSlowestEndpoints(requests) { const endpointMap = new Map(); requests.forEach((request) => { const endpoint = `${request.content?.method || 'GET'} ${request.content?.path || '/'}`; const duration = request.content?.duration || 0; const hasError = request.content?.statusCode >= 400; if (!endpointMap.has(endpoint)) { endpointMap.set(endpoint, { count: 0, totalTime: 0, times: [], errors: 0, }); } const data = endpointMap.get(endpoint); data.count++; data.totalTime += duration; data.times.push(duration); if (hasError) data.errors++; }); return Array.from(endpointMap.entries()) .map(([endpoint, data]) => { const parts = endpoint.split(' ', 2); const method = parts[0] || 'GET'; const path = parts[1] || '/'; const sortedTimes = data.times.sort((a, b) => a - b); const p95Index = Math.floor(sortedTimes.length * 0.95); return { endpoint: path, method, count: data.count, averageTime: data.totalTime / data.count, p95Time: sortedTimes[p95Index] || 0, errorRate: (data.errors / data.count) * 100, throughput: data.count / 3600, }; }) .sort((a, b) => b.averageTime - a.averageTime) .slice(0, 10); } calculateResourceUsage(entries) { return { cpu: [], memory: [], connections: [], diskIO: [], }; } calculateBottleneckAnalysis(correlations) { const bottleneckMap = new Map(); correlations.forEach((correlation) => { correlation.bottlenecks?.forEach((bottleneck) => { const key = bottleneck.component; if (!bottleneckMap.has(key)) { bottleneckMap.set(key, { count: 0, totalImpact: 0, severities: [], }); } const data = bottleneckMap.get(key); data.count++; data.totalImpact += bottleneck.percentage || 0; data.severities.push(bottleneck.severity); }); }); return Array.from(bottleneckMap.entries()) .map(([component, data]) => { const mostCommonSeverity = this.getMostCommon(data.severities); return { component, severity: mostCommonSeverity, frequency: data.count, averageImpact: data.totalImpact / data.count, description: `${component} bottleneck occurred ${data.count} times`, recommendation: `Optimize ${component} performance`, }; }) .sort((a, b) => b.frequency - a.frequency) .slice(0, 10); } getMostCommon(items) { const counts = new Map(); items.forEach((item) => { counts.set(item, (counts.get(item) || 0) + 1); }); let maxCount = 0; let mostCommon = ''; counts.forEach((count, item) => { if (count > maxCount) { maxCount = count; mostCommon = item; } }); return mostCommon; } calculateErrorDistribution(errors) { const typeMap = new Map(); const severityMap = new Map(); const componentMap = new Map(); errors.forEach((error) => { const errorType = error.content?.error?.name || 'Unknown'; const severity = error.content?.severity || 'medium'; const component = error.content?.component || 'application'; typeMap.set(errorType, (typeMap.get(errorType) || 0) + 1); severityMap.set(severity, (severityMap.get(severity) || 0) + 1); componentMap.set(component, (componentMap.get(component) || 0) + 1); }); const total = errors.length || 1; return { byType: Array.from(typeMap.entries()).map(([type, count]) => ({ type, count, percentage: (count / total) * 100, })), bySeverity: Array.from(severityMap.entries()).map(([severity, count]) => ({ severity, count, percentage: (count / total) * 100, })), byComponent: Array.from(componentMap.entries()).map(([component, count]) => ({ component, count, percentage: (count / total) * 100, })), }; } calculateTopErrors(errors) { const errorMap = new Map(); errors.forEach((error) => { const errorType = error.content?.error?.name || 'Unknown'; const message = error.content?.error?.message || 'No message'; const userId = error.content?.userId || error.content?.ip; if (!errorMap.has(errorType)) { errorMap.set(errorType, { count: 0, firstSeen: error.timestamp, lastSeen: error.timestamp, affectedUsers: new Set(), message, }); } const data = errorMap.get(errorType); data.count++; data.lastSeen = new Date(Math.max(data.lastSeen.getTime(), error.timestamp.getTime())); data.firstSeen = new Date(Math.min(data.firstSeen.getTime(), error.timestamp.getTime())); if (userId) data.affectedUsers.add(userId); }); return Array.from(errorMap.entries()) .map(([errorType, data]) => ({ errorType, message: data.message, count: data.count, firstSeen: data.firstSeen, lastSeen: data.lastSeen, impactScore: this.calculateErrorImpactScore(data.count, data.affectedUsers.size), affectedUsers: data.affectedUsers.size, })) .sort((a, b) => b.impactScore - a.impactScore) .slice(0, 10); } calculateErrorImpact(errors) { const impactMap = new Map(); errors.forEach((error) => { const errorType = error.content?.error?.name || 'Unknown'; const responseTime = error.content?.responseTime || 0; if (!impactMap.has(errorType)) { impactMap.set(errorType, { count: 0, performanceImpact: [], userCount: 0, }); } const data = impactMap.get(errorType); data.count++; if (responseTime > 0) data.performanceImpact.push(responseTime); }); return Array.from(impactMap.entries()) .map(([errorType, data]) => { const avgPerformanceImpact = data.performanceImpact.length > 0 ? data.performanceImpact.reduce((sum, val) => sum + val, 0) / data.performanceImpact.length : 0; const userImpact = (data.userCount / this.currentAnalytics.overview.activeUsers) * 100; const businessImpact = this.calculateBusinessImpact(data.count, avgPerformanceImpact); return { errorType, performanceImpact: avgPerformanceImpact, userImpact, businessImpact, recommendations: this.generateErrorRecommendations(errorType, avgPerformanceImpact, data.count), }; }) .sort((a, b) => b.businessImpact - a.businessImpact); } calculateQueryDistribution(queries) { return { byType: [], byTable: [], byComplexity: [], }; } calculateSlowQueries(queries) { return []; } calculateConnectionHealth(queries) { return { totalConnections: 0, activeConnections: 0, idleConnections: 0, maxConnections: 0, healthScore: 100, issues: [], }; } calculateIndexUsage(queries) { return []; } calculateCacheDistribution(cacheOps) { if (cacheOps.length === 0) { return { hitRate: 0, missRate: 0, byOperation: [], byKeyPattern: [], }; } const hits = cacheOps.filter((op) => op.content?.cache?.hit === true).length; const total = cacheOps.length; const hitRate = (hits / total) * 100; const missRate = 100 - hitRate; const operationMap = new Map(); cacheOps.forEach((op) => { const operation = op.content?.cache?.operation || 'unknown'; const isHit = op.content?.cache?.hit === true; if (!operationMap.has(operation)) { operationMap.set(operation, { total: 0, hits: 0 }); } const data = operationMap.get(operation); data.total++; if (isHit) data.hits++; }); const byOperation = Array.from(operationMap.entries()).map(([operation, data]) => ({ operation, hitRate: (data.hits / data.total) * 100, count: data.total, })); const patternMap = new Map(); cacheOps.forEach((op) => { const pattern = op.content?.cache?.keyPattern || this.extractKeyPattern(op.content?.cache?.key || ''); const isHit = op.content?.cache?.hit === true; if (!patternMap.has(pattern)) { patternMap.set(pattern, { total: 0, hits: 0 }); } const data = patternMap.get(pattern); data.total++; if (isHit) data.hits++; }); const byKeyPattern = Array.from(patternMap.entries()).map(([pattern, data]) => ({ pattern, hitRate: (data.hits / data.total) * 100, count: data.total, })); return { hitRate, missRate, byOperation, byKeyPattern, }; } calculateTopCacheKeys(cacheOps) { return []; } calculateCachePerformance(cacheOps) { return { averageResponseTime: 0, throughput: 0, memoryUsage: 0, evictionRate: 0, trends: [], }; } calculateEvictionAnalysis(cacheOps) { return []; } calculateQueueHealth(jobs) { return []; } calculateJobDistribution(jobs) { return { byType: [], byQueue: [], byStatus: [], }; } calculateJobFailureAnalysis(jobs) { return []; } calculateJobThroughput(jobs) { return []; } calculateActiveUserMetrics(requests) { const userMap = new Map(); requests.forEach((request) => { const userId = request.content?.userId || request.content?.ip || 'anonymous'; const responseTime = request.content?.duration || 0; const hasError = request.content?.responseStatus >= 400; if (!userMap.has(userId)) { userMap.set(userId, { sessionCount: 0, requestCount: 0, errorCount: 0, responseTimes: [], lastActive: request.timestamp, }); } const data = userMap.get(userId); data.requestCount++; if (hasError) data.errorCount++; if (responseTime > 0) data.responseTimes.push(responseTime); data.lastActive = new Date(Math.max(data.lastActive.getTime(), request.timestamp.getTime())); }); return Array.from(userMap.entries()) .map(([userId, data]) => ({ userId, sessionCount: data.sessionCount, requestCount: data.requestCount, errorCount: data.errorCount, averageResponseTime: data.responseTimes.length > 0 ? data.responseTimes.reduce((sum, val) => sum + val, 0) / data.responseTimes.length : 0, lastActive: data.lastActive, })) .sort((a, b) => b.requestCount - a.requestCount) .slice(0, 100); } calculateSessionAnalysis(requests) { return { totalSessions: 0, averageSessionDuration: 0, averageRequestsPerSession: 0, bounceRate: 0, mostActiveHours: [], }; } calculateGeographicDistribution(requests) { return []; } calculateDeviceAnalysis(requests) { return []; } calculateTrafficTrends(entries) { const requests = entries.filter((e) => e.type === 'request'); const hourlyBuckets = new Map(); requests.forEach((request) => { const hour = new Date(request.timestamp); hour.setMinutes(0, 0, 0); const hourKey = hour.toISOString(); hourlyBuckets.set(hourKey, (hourlyBuckets.get(hourKey) || 0) + 1); }); return Array.from(hourlyBuckets.entries()) .map(([timestamp, value]) => ({ timestamp: new Date(timestamp), value, label: 'Requests per hour', })) .sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); } calculatePerformanceTrends(entries) { const requests = entries.filter((e) => e.type === 'request' && e.content?.duration); const hourlyBuckets = new Map(); requests.forEach((request) => { const hour = new Date(request.timestamp); hour.setMinutes(0, 0, 0); const hourKey = hour.toISOString(); if (!hourlyBuckets.has(hourKey)) { hourlyBuckets.set(hourKey, []); } hourlyBuckets.get(hourKey).push(request.content.duration); }); return Array.from(hourlyBuckets.entries()) .map(([timestamp, durations]) => ({ timestamp: new Date(timestamp), value: durations.reduce((sum, val) => sum + val, 0) / durations.length, label: 'Average response time (ms)', })) .sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); } calculatePredictions(entries) { const predictions = []; const trafficTrend = this.calculateTrafficTrends(entries); if (trafficTrend.length >= 3) { const recent = trafficTrend.slice(-3); const avgChange = (recent[2].value - recent[0].value) / 2; const predicted = recent[2].value + avgChange; predictions.push({ metric: 'traffic', prediction: Math.max(0, predicted), confidence: 0.7, timeframe: 'next_hour', factors: ['historical_trend', 'time_of_day'], }); } const perfTrend = this.calculatePerformanceTrends(entries); if (perfTrend.length >= 3) { const recent = perfTrend.slice(-3); const avgChange = (recent[2].value - recent[0].value) / 2; const predicted = recent[2].value + avgChange; predictions.push({ metric: 'response_time', prediction: Math.max(0, predicted), confidence: 0.6, timeframe: 'next_hour', factors: ['performance_trend', 'system_load'], }); } return predictions; } getAnalytics() { return { ...this.currentAnalytics }; } getAnalyticsStream() { return this.analyticsSubject.asObservable().pipe((0, operators_1.shareReplay)(1)); } getAnalyticsForTimeRange(start, end) { return Promise.resolve({ ...this.currentAnalytics }); } async refreshAnalytics() { await this.updateAnalytics(); } onDestroy() { this.destroy$.next(); this.destroy$.complete(); } calculateErrorImpactScore(count, affectedUsers) { const frequencyScore = Math.min(count / 100, 1) * 50; const userScore = Math.min(affectedUsers / 10, 1) * 50; return frequencyScore + userScore; } calculateBusinessImpact(errorCount, avgResponseTime) { const errorImpact = Math.min(errorCount / 100, 1) * 60; const performanceImpact = Math.min(avgResponseTime / 5000, 1) * 40; return errorImpact + performanceImpact; } generateErrorRecommendations(errorType, performanceImpact, count) { const recommendations = []; if (count > 10) { recommendations.push('Investigate error frequency and implement preventive measures'); } if (performanceImpact > 1000) { recommendations.push('Optimize error handling to reduce performance impact'); } if (errorType.toLowerCase().includes('database')) { recommendations.push('Review database queries and connection handling'); } if (errorType.toLowerCase().includes('timeout')) { recommendations.push('Increase timeout values or optimize slow operations'); } return recommendations.length > 0 ? recommendations : ['Monitor error patterns and implement appropriate fixes']; } extractKeyPattern(key) { if (!key) return 'unknown'; return key .replace(/\d+/g, 'N') .replace(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/gi, 'UUID') .replace(/[a-f0-9]{32}/gi, 'HASH32') .replace(/[a-f0-9]{40}/gi, 'HASH40') .replace(/[a-f0-9]{64}/gi, 'HASH64') .replace(/\d{4}-\d{2}-\d{2}/g, 'DATE') .replace(/\d{2}:\d{2}:\d{2}/g, 'TIME'); } convertErrorsToTimeSeries(errors) { const now = new Date(); const hourlyBuckets = new Map(); for (let i = 23; i >= 0; i--) { const hourAgo = new Date(now.getTime() - i * 60 * 60 * 1000); const hourKey = hourAgo.toISOString().slice(0, 13); hourlyBuckets.set(hourKey, 0); } errors.forEach((error) => { const timestamp = new Date(error.timestamp); const hourKey = timestamp.toISOString().slice(0, 13); const currentCount = hourlyBuckets.get(hourKey) || 0; hourlyBuckets.set(hourKey, currentCount + 1); }); return Array.from(hourlyBuckets.entries()).map(([hourKey, count]) => ({ timestamp: new Date(hourKey + ':00:00.000Z'), value: count, })); } }; exports.AnalyticsService = AnalyticsService; exports.AnalyticsService = AnalyticsService = AnalyticsService_1 = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [telescope_service_1.TelescopeService, performance_correlation_service_1.PerformanceCorrelationService]) ], AnalyticsService); //# sourceMappingURL=analytics.service.js.map