using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.Mvc; namespace JobTrackerApi.Controllers { [ApiController] [Route("api/client-errors")] [RequestSizeLimit(32 * 1024)] public class ClientErrorsController : ControllerBase { private const int MaxFieldLength = 512; private const int MaxStackSummaryLength = 1024; private readonly ILogger _logger; public ClientErrorsController(ILogger logger) { _logger = logger; } public sealed record ClientErrorReport( string? ErrorId, string? Message, string? Stack, string? ComponentStack, string? Url, string? UserAgent, string? At ); [HttpPost] public IActionResult Report([FromBody] ClientErrorReport report) { var errorId = Normalize(report.ErrorId, 128) ?? "unknown"; var at = Normalize(report.At, 128) ?? "unknown"; var url = Normalize(report.Url, MaxFieldLength) ?? "unknown"; var userAgent = Normalize(report.UserAgent, MaxFieldLength) ?? "unknown"; var message = Normalize(report.Message, MaxFieldLength) ?? "unknown"; var stackHash = Hash(report.Stack); var componentStackHash = Hash(report.ComponentStack); var stackPreview = SummarizeStack(report.Stack); var componentPreview = SummarizeStack(report.ComponentStack); _logger.LogError( "ClientError {ErrorId} at {At} url={Url} ua={UserAgent} msg={Message} stackHash={StackHash} componentHash={ComponentStackHash} stackPreview={StackPreview} componentPreview={ComponentPreview}", errorId, at, url, userAgent, message, stackHash, componentStackHash, stackPreview, componentPreview ); return NoContent(); } internal static string? Normalize(string? value, int maxLength) { if (string.IsNullOrWhiteSpace(value)) return null; var normalized = value.Trim().Replace("\r", " ").Replace("\n", " "); if (normalized.Length <= maxLength) { return normalized; } return normalized[..maxLength]; } internal static string? SummarizeStack(string? value) { if (string.IsNullOrWhiteSpace(value)) return null; var lines = value .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Select(line => line.Replace("\r", string.Empty).Trim()) .Where(line => line.Length > 0) .Take(2) .ToArray(); if (lines.Length == 0) return null; var summary = string.Join(" | ", lines); return summary.Length <= MaxStackSummaryLength ? summary : summary[..MaxStackSummaryLength]; } internal static string? Hash(string? value) { if (string.IsNullOrWhiteSpace(value)) return null; var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(value)); return Convert.ToHexString(bytes).ToLowerInvariant(); } } }