File: Implementation\BreezePartialContent.cs
Web Access
Project: src\sdk\src\Cli\Microsoft.DotNet.Cli.Telemetry\Microsoft.DotNet.Cli.Telemetry.csproj (Microsoft.DotNet.Cli.Telemetry)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text.Json;

namespace Microsoft.DotNet.Cli.Telemetry.Implementation;

/// <summary>
/// Helpers for handling an HTTP 206 (Partial Content) response from the Breeze
/// <c>/v2.1/track</c> endpoint. When ingestion partially succeeds, the response body is a
/// <see cref="TrackResponse"/> listing per-item errors by their (zero-based) index within the
/// NDJSON payload. This class re-slices the original payload down to just the envelopes that
/// failed with a <em>retriable</em> status so they can be persisted and re-sent later.
/// </summary>
internal static class BreezePartialContent
{
    // Per-item status codes the Azure Monitor exporter treats as retriable
    // (ProcessPartialSuccessWithCounting): request timeout, throttling, and server errors.
    private static readonly HashSet<int> s_retriableStatusCodes = [408, 429, 439, 500, 503];

    /// <summary>Deserializes a Breeze partial-success response body. Returns <see langword="null"/> on malformed input.</summary>
    public static TrackResponse? ParseResponse(ReadOnlySpan<byte> responseBody)
    {
        if (responseBody.IsEmpty)
        {
            return null;
        }

        try
        {
            return JsonSerializer.Deserialize(responseBody, TelemetryJsonContext.Default.TrackResponse);
        }
        catch (JsonException)
        {
            return null;
        }
    }

    /// <summary>
    /// Given the original NDJSON <paramref name="payload"/> and the parsed partial-success
    /// <paramref name="response"/>, returns a new NDJSON payload containing only the envelopes
    /// that were rejected with a retriable status code, or <see langword="null"/> when nothing
    /// is worth retrying.
    /// </summary>
    public static byte[]? GetRetriablePayload(byte[] payload, TrackResponse? response)
    {
        if (response?.Errors is not { Count: > 0 } errors)
        {
            return null;
        }

        var retriableIndices = new HashSet<int>();
        foreach (var error in errors)
        {
            if (s_retriableStatusCodes.Contains(error.StatusCode))
            {
                retriableIndices.Add(error.Index);
            }
        }

        if (retriableIndices.Count == 0)
        {
            return null;
        }

        // Each envelope is a single JSON line terminated by '\n', so the line index maps
        // directly to the Breeze error index.
        using var stream = new MemoryStream();
        var lineIndex = 0;
        var lineStart = 0;
        for (var i = 0; i < payload.Length; i++)
        {
            if (payload[i] != (byte)'\n')
            {
                continue;
            }

            if (retriableIndices.Contains(lineIndex))
            {
                // Re-emit the envelope bytes plus the terminating newline.
                stream.Write(payload, lineStart, i - lineStart + 1);
            }

            lineIndex++;
            lineStart = i + 1;
        }

        return stream.Length == 0 ? null : stream.ToArray();
    }
}