Files
jobtrackingapp/JobTrackerApi/Controllers/ClientErrorsController.cs
T

101 lines
3.4 KiB
C#

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<ClientErrorsController> _logger;
public ClientErrorsController(ILogger<ClientErrorsController> 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();
}
}
}