littleshop/LittleShop/Services/BotMetricsService.cs
2025-08-27 18:02:39 +01:00

382 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using LittleShop.Data;
using LittleShop.DTOs;
using LittleShop.Models;
namespace LittleShop.Services;
public class BotMetricsService : IBotMetricsService
{
private readonly LittleShopContext _context;
private readonly ILogger<BotMetricsService> _logger;
public BotMetricsService(LittleShopContext context, ILogger<BotMetricsService> logger)
{
_context = context;
_logger = logger;
}
// Metrics Methods
public async Task<BotMetricDto> RecordMetricAsync(Guid botId, CreateBotMetricDto dto)
{
var metric = new BotMetric
{
Id = Guid.NewGuid(),
BotId = botId,
MetricType = dto.MetricType,
Value = dto.Value,
Category = dto.Category,
Description = dto.Description,
Metadata = JsonSerializer.Serialize(dto.Metadata),
RecordedAt = DateTime.UtcNow
};
_context.BotMetrics.Add(metric);
await _context.SaveChangesAsync();
return MapMetricToDto(metric);
}
public async Task<bool> RecordMetricsBatchAsync(Guid botId, BotMetricsBatchDto dto)
{
try
{
var metrics = dto.Metrics.Select(m => new BotMetric
{
Id = Guid.NewGuid(),
BotId = botId,
MetricType = m.MetricType,
Value = m.Value,
Category = m.Category,
Description = m.Description,
Metadata = JsonSerializer.Serialize(m.Metadata),
RecordedAt = DateTime.UtcNow
}).ToList();
_context.BotMetrics.AddRange(metrics);
await _context.SaveChangesAsync();
_logger.LogInformation("Recorded {Count} metrics for bot {BotId}", metrics.Count, botId);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to record metrics batch for bot {BotId}", botId);
return false;
}
}
public async Task<IEnumerable<BotMetricDto>> GetBotMetricsAsync(Guid botId, DateTime? startDate = null, DateTime? endDate = null)
{
var query = _context.BotMetrics.Where(m => m.BotId == botId);
if (startDate.HasValue)
query = query.Where(m => m.RecordedAt >= startDate.Value);
if (endDate.HasValue)
query = query.Where(m => m.RecordedAt <= endDate.Value);
var metrics = await query
.OrderByDescending(m => m.RecordedAt)
.Take(1000) // Limit to prevent large results
.ToListAsync();
return metrics.Select(MapMetricToDto);
}
public async Task<BotMetricsSummaryDto> GetMetricsSummaryAsync(Guid botId, DateTime? startDate = null, DateTime? endDate = null)
{
var bot = await _context.Bots.FindAsync(botId);
if (bot == null)
return new BotMetricsSummaryDto { BotId = botId };
var start = startDate ?? DateTime.UtcNow.AddDays(-30);
var end = endDate ?? DateTime.UtcNow;
var metrics = await _context.BotMetrics
.Where(m => m.BotId == botId && m.RecordedAt >= start && m.RecordedAt <= end)
.ToListAsync();
var sessions = await _context.BotSessions
.Where(s => s.BotId == botId && s.StartedAt >= start && s.StartedAt <= end)
.ToListAsync();
var summary = new BotMetricsSummaryDto
{
BotId = botId,
BotName = bot.Name,
PeriodStart = start,
PeriodEnd = end,
TotalSessions = sessions.Count,
UniqueSessions = sessions.Select(s => s.SessionIdentifier).Distinct().Count(),
TotalOrders = sessions.Sum(s => s.OrderCount),
TotalRevenue = sessions.Sum(s => s.TotalSpent),
TotalMessages = sessions.Sum(s => s.MessageCount),
TotalErrors = (int)metrics.Where(m => m.MetricType == Enums.MetricType.Error).Sum(m => m.Value)
};
// Calculate average response time
var responseTimeMetrics = metrics.Where(m => m.MetricType == Enums.MetricType.ResponseTime).ToList();
if (responseTimeMetrics.Any())
summary.AverageResponseTime = responseTimeMetrics.Average(m => m.Value);
// Calculate uptime percentage
var uptimeMetrics = metrics.Where(m => m.MetricType == Enums.MetricType.Uptime).ToList();
if (uptimeMetrics.Any())
{
var totalPossibleUptime = (end - start).TotalMinutes;
var actualUptime = uptimeMetrics.Sum(m => m.Value);
summary.UptimePercentage = (actualUptime / (decimal)totalPossibleUptime) * 100;
}
// Group metrics by type
summary.MetricsByType = metrics
.GroupBy(m => m.MetricType.ToString())
.ToDictionary(g => g.Key, g => g.Sum(m => m.Value));
// Generate time series data (daily aggregation)
summary.TimeSeries = GenerateTimeSeries(metrics, start, end);
return summary;
}
// Session Methods
public async Task<BotSessionDto> StartSessionAsync(Guid botId, CreateBotSessionDto dto)
{
var session = new BotSession
{
Id = Guid.NewGuid(),
BotId = botId,
SessionIdentifier = dto.SessionIdentifier,
Platform = dto.Platform,
StartedAt = DateTime.UtcNow,
LastActivityAt = DateTime.UtcNow,
Language = dto.Language,
Country = dto.Country,
IsAnonymous = dto.IsAnonymous,
Metadata = JsonSerializer.Serialize(dto.Metadata),
OrderCount = 0,
MessageCount = 0,
TotalSpent = 0
};
_context.BotSessions.Add(session);
await _context.SaveChangesAsync();
_logger.LogInformation("Started session {SessionId} for bot {BotId}", session.Id, botId);
return MapSessionToDto(session);
}
public async Task<bool> UpdateSessionAsync(Guid sessionId, UpdateBotSessionDto dto)
{
var session = await _context.BotSessions.FindAsync(sessionId);
if (session == null)
return false;
session.LastActivityAt = DateTime.UtcNow;
if (dto.OrderCount.HasValue)
session.OrderCount = dto.OrderCount.Value;
if (dto.MessageCount.HasValue)
session.MessageCount = dto.MessageCount.Value;
if (dto.TotalSpent.HasValue)
session.TotalSpent = dto.TotalSpent.Value;
if (dto.EndSession.HasValue && dto.EndSession.Value)
session.EndedAt = DateTime.UtcNow;
if (dto.Metadata != null)
session.Metadata = JsonSerializer.Serialize(dto.Metadata);
await _context.SaveChangesAsync();
return true;
}
public async Task<BotSessionDto?> GetSessionAsync(Guid sessionId)
{
var session = await _context.BotSessions.FindAsync(sessionId);
return session != null ? MapSessionToDto(session) : null;
}
public async Task<IEnumerable<BotSessionDto>> GetBotSessionsAsync(Guid botId, bool activeOnly = false)
{
var query = _context.BotSessions.Where(s => s.BotId == botId);
if (activeOnly)
query = query.Where(s => !s.EndedAt.HasValue);
var sessions = await query
.OrderByDescending(s => s.StartedAt)
.Take(100)
.ToListAsync();
return sessions.Select(MapSessionToDto);
}
public async Task<BotSessionSummaryDto> GetSessionSummaryAsync(Guid botId, DateTime? startDate = null, DateTime? endDate = null)
{
var query = _context.BotSessions.Where(s => s.BotId == botId);
if (startDate.HasValue)
query = query.Where(s => s.StartedAt >= startDate.Value);
if (endDate.HasValue)
query = query.Where(s => s.StartedAt <= endDate.Value);
var sessions = await query.ToListAsync();
var summary = new BotSessionSummaryDto
{
TotalSessions = sessions.Count,
ActiveSessions = sessions.Count(s => !s.EndedAt.HasValue),
CompletedSessions = sessions.Count(s => s.EndedAt.HasValue)
};
if (sessions.Any())
{
var completedSessions = sessions.Where(s => s.EndedAt.HasValue).ToList();
if (completedSessions.Any())
{
summary.AverageSessionDuration = (decimal)completedSessions
.Average(s => (s.EndedAt!.Value - s.StartedAt).TotalMinutes);
}
summary.AverageOrdersPerSession = (decimal)sessions.Average(s => s.OrderCount);
summary.AverageSpendPerSession = sessions.Average(s => s.TotalSpent);
summary.SessionsByPlatform = sessions
.GroupBy(s => s.Platform)
.ToDictionary(g => g.Key, g => g.Count());
summary.SessionsByCountry = sessions
.Where(s => !string.IsNullOrEmpty(s.Country))
.GroupBy(s => s.Country)
.ToDictionary(g => g.Key, g => g.Count());
summary.SessionsByLanguage = sessions
.GroupBy(s => s.Language)
.ToDictionary(g => g.Key, g => g.Count());
}
return summary;
}
public async Task<bool> EndSessionAsync(Guid sessionId)
{
var session = await _context.BotSessions.FindAsync(sessionId);
if (session == null || session.EndedAt.HasValue)
return false;
session.EndedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
_logger.LogInformation("Ended session {SessionId}", sessionId);
return true;
}
public async Task<int> CleanupInactiveSessionsAsync(int inactiveMinutes = 30)
{
var cutoffTime = DateTime.UtcNow.AddMinutes(-inactiveMinutes);
var inactiveSessions = await _context.BotSessions
.Where(s => !s.EndedAt.HasValue && s.LastActivityAt < cutoffTime)
.ToListAsync();
foreach (var session in inactiveSessions)
{
session.EndedAt = DateTime.UtcNow;
}
await _context.SaveChangesAsync();
_logger.LogInformation("Cleaned up {Count} inactive sessions", inactiveSessions.Count);
return inactiveSessions.Count;
}
// Helper Methods
private BotMetricDto MapMetricToDto(BotMetric metric)
{
var metadata = new Dictionary<string, object>();
try
{
metadata = JsonSerializer.Deserialize<Dictionary<string, object>>(metric.Metadata)
?? new Dictionary<string, object>();
}
catch { }
return new BotMetricDto
{
Id = metric.Id,
BotId = metric.BotId,
MetricType = metric.MetricType,
Value = metric.Value,
Category = metric.Category,
Description = metric.Description,
RecordedAt = metric.RecordedAt,
Metadata = metadata
};
}
private BotSessionDto MapSessionToDto(BotSession session)
{
var metadata = new Dictionary<string, object>();
try
{
metadata = JsonSerializer.Deserialize<Dictionary<string, object>>(session.Metadata)
?? new Dictionary<string, object>();
}
catch { }
return new BotSessionDto
{
Id = session.Id,
BotId = session.BotId,
SessionIdentifier = session.SessionIdentifier,
Platform = session.Platform,
StartedAt = session.StartedAt,
LastActivityAt = session.LastActivityAt,
EndedAt = session.EndedAt,
OrderCount = session.OrderCount,
MessageCount = session.MessageCount,
TotalSpent = session.TotalSpent,
Language = session.Language,
Country = session.Country,
IsAnonymous = session.IsAnonymous,
Metadata = metadata
};
}
private List<TimeSeriesDataPoint> GenerateTimeSeries(List<BotMetric> metrics, DateTime start, DateTime end)
{
var dataPoints = new List<TimeSeriesDataPoint>();
var currentDate = start.Date;
while (currentDate <= end.Date)
{
var dayMetrics = metrics.Where(m => m.RecordedAt.Date == currentDate).ToList();
if (dayMetrics.Any())
{
dataPoints.Add(new TimeSeriesDataPoint
{
Timestamp = currentDate,
Label = currentDate.ToString("yyyy-MM-dd"),
Value = dayMetrics.Sum(m => m.Value)
});
}
currentDate = currentDate.AddDays(1);
}
return dataPoints;
}
}