382 lines
13 KiB
C#
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;
|
|
}
|
|
} |