< Summary

Information
Class: OzonOrdersWeb.Observability.ClientTelemetryController
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Observability/ClientTelemetryController.cs
Line coverage
97%
Covered lines: 42
Uncovered lines: 1
Coverable lines: 43
Total lines: 111
Line coverage: 97.6%
Branch coverage
75%
Covered branches: 15
Total branches: 20
Branch coverage: 75%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Get()100%11100%
Post(...)75%202097.29%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Observability/ClientTelemetryController.cs

#LineLine coverage
 1using System.ComponentModel.DataAnnotations;
 2using System.Security.Claims;
 3using Microsoft.AspNetCore.Authorization;
 4using Microsoft.AspNetCore.Mvc;
 5using Microsoft.AspNetCore.RateLimiting;
 6using Microsoft.Extensions.Options;
 7using OzonOrdersWeb.Build;
 8
 9namespace OzonOrdersWeb.Observability;
 10
 11[ApiController]
 12[AllowAnonymous]
 13[IgnoreAntiforgeryToken]
 14[EnableRateLimiting("client-diagnostics")]
 15[Route("api/diagnostics/client-event")]
 3516public sealed class ClientTelemetryController(
 3517    ILogger<ClientTelemetryController> logger,
 3518    SensitiveDataRedactor redactor,
 3519    IOptions<ObservabilityOptions> options) : ControllerBase
 20{
 3521    private readonly ObservabilityOptions _options = options.Value;
 22
 23    [HttpGet]
 124    public IActionResult Get() => NoContent();
 25
 26    [HttpPost]
 27    public IActionResult Post([FromBody] ClientTelemetryEvent input)
 28    {
 3429        var origin = Request.Headers.Origin.ToString();
 3430        if (!Uri.TryCreate(origin, UriKind.Absolute, out var originUri)
 3431            || !string.Equals(originUri.Authority, Request.Host.Value, StringComparison.OrdinalIgnoreCase))
 32        {
 33            // Не даём внешнему источнику превращать endpoint в генератор GitHub Issues.
 034            return NoContent();
 35        }
 36
 3437        if (string.Equals(input.Kind, "ResourceLoadError", StringComparison.OrdinalIgnoreCase)
 3438            && (input.Message.Contains("IMG", StringComparison.OrdinalIgnoreCase)
 3439                || input.Message.Contains("VIDEO", StringComparison.OrdinalIgnoreCase)
 3440                || input.Message.Contains("AUDIO", StringComparison.OrdinalIgnoreCase)))
 41        {
 142            return NoContent();
 43        }
 44
 3345        var page = redactor.Redact(input.PageUrl, _options.MaxPayloadCharacters);
 3346        var target = redactor.Redact(input.TargetUrl, _options.MaxPayloadCharacters);
 3347        var message = redactor.Redact(input.Message, _options.MaxPayloadCharacters);
 3348        var stack = redactor.Redact(input.Stack, _options.MaxPayloadCharacters);
 49
 3350        using var scope = logger.BeginScope(new Dictionary<string, object?>
 3351        {
 3352            ["EventKind"] = string.IsNullOrWhiteSpace(input.Kind) ? "FrontendError" : input.Kind,
 3353            ["EventCategory"] = "frontend",
 3354            ["PageUrl"] = page,
 3355            ["RequestUri"] = target,
 3356            ["StatusCode"] = input.StatusCode,
 3357            ["ElapsedMilliseconds"] = input.ElapsedMilliseconds,
 3358            ["FrontendStack"] = stack,
 3359            ["Browser"] = redactor.Redact(Request.Headers.UserAgent.ToString(), 1_024),
 3360            ["FrontendVersion"] = redactor.Redact(input.FrontendVersion, 256),
 3361            ["ApplicationVersion"] = AppBuildInfo.CommitHash,
 3362            ["ApplicationBuildDate"] = AppBuildInfo.CommitDate,
 3363            ["UserAction"] = redactor.Redact(input.UserAction, 1_024),
 3364            ["TraceId"] = string.IsNullOrWhiteSpace(input.TraceId) ? HttpContext.TraceIdentifier : input.TraceId,
 3365            ["UserId"] = User.FindFirstValue(ClaimTypes.NameIdentifier),
 3366            ["RemoteIp"] = HttpContext.Connection.RemoteIpAddress?.ToString()
 3367        });
 68
 3369        if (FrontendOutageNoiseClassifier.IsServerOutageNoise(input))
 70        {
 71            // Недоступность самого Studio2 (деплой, 502 от Nginx, обрыв соединения) дефектом
 72            // фронтенда не является: в Seq запись остаётся, в errors-*.log — нет, и наблюдатель
 73            // логов не заводит по ней issue на каждый упавший запрос.
 2774            logger.LogInformation("Недоступность сервера в клиенте {Kind} на {Page}: {Message}", input.Kind, page, messa
 2775            return Accepted();
 76        }
 77
 678        logger.LogWarning("Ошибка frontend {Kind} на {Page}: {Message}", input.Kind, page, message);
 679        return Accepted();
 3380    }
 81}
 82
 83public sealed class ClientTelemetryEvent
 84{
 85    [Required, StringLength(100)]
 86    public string Kind { get; init; } = "FrontendError";
 87
 88    [Required, StringLength(16_384)]
 89    public string Message { get; init; } = string.Empty;
 90
 91    [StringLength(16_384)]
 92    public string? Stack { get; init; }
 93
 94    [StringLength(4_096)]
 95    public string? PageUrl { get; init; }
 96
 97    [StringLength(4_096)]
 98    public string? TargetUrl { get; init; }
 99
 100    [StringLength(256)]
 101    public string? TraceId { get; init; }
 102
 103    public int? StatusCode { get; init; }
 104    public long? ElapsedMilliseconds { get; init; }
 105
 106    [StringLength(256)]
 107    public string? FrontendVersion { get; init; }
 108
 109    [StringLength(1_024)]
 110    public string? UserAction { get; init; }
 111}