< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Langfuse.LangfuseCommentRecorder
Assembly: NexusLabs.Needlr.AgentFramework.Langfuse
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Langfuse/LangfuseCommentRecorder.cs
Line coverage
90%
Covered lines: 36
Uncovered lines: 4
Coverable lines: 40
Total lines: 95
Line coverage: 90%
Branch coverage
50%
Covered branches: 9
Total branches: 18
Branch coverage: 50%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
AddTraceCommentAsync()33.33%6685%
ResolveProjectIdAsync()58.33%121292.85%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Langfuse/LangfuseCommentRecorder.cs

#LineLine coverage
 1namespace NexusLabs.Needlr.AgentFramework.Langfuse;
 2
 3/// <summary>
 4/// Posts comments to the Langfuse public Comments API (<c>POST /api/public/comments</c>) via the
 5/// shared <see cref="LangfuseApiClient"/>. Comment failures are non-fatal — they are routed to the
 6/// diagnostics callback rather than thrown — because a comment is auxiliary context, never the
 7/// result of an eval.
 8/// </summary>
 9/// <remarks>
 10/// Langfuse requires an explicit project id on each comment. The API key maps to exactly one
 11/// project, so the id is resolved once from <c>GET /api/public/projects</c> and cached.
 12/// </remarks>
 13internal sealed class LangfuseCommentRecorder
 14{
 15    private const string TraceObjectType = "TRACE";
 16
 17    private readonly LangfuseApiClient _apiClient;
 18    private readonly Action<string>? _diagnostics;
 319    private readonly SemaphoreSlim _projectIdLock = new(1, 1);
 20    private string? _projectId;
 21
 322    public LangfuseCommentRecorder(LangfuseApiClient apiClient, Action<string>? diagnostics)
 23    {
 324        ArgumentNullException.ThrowIfNull(apiClient);
 25
 326        _apiClient = apiClient;
 327        _diagnostics = diagnostics;
 328    }
 29
 30    /// <summary>Attaches a comment to a trace.</summary>
 31    /// <param name="traceId">The trace to comment on.</param>
 32    /// <param name="content">The comment content (Langfuse limits this to 5000 characters).</param>
 33    /// <param name="cancellationToken">A cancellation token.</param>
 34    public async Task AddTraceCommentAsync(string traceId, string content, CancellationToken cancellationToken)
 35    {
 436        ArgumentException.ThrowIfNullOrWhiteSpace(traceId);
 437        ArgumentException.ThrowIfNullOrWhiteSpace(content);
 38
 39        try
 40        {
 441            var projectId = await ResolveProjectIdAsync(cancellationToken).ConfigureAwait(false);
 442            if (projectId is null)
 43            {
 044                _diagnostics?.Invoke(
 045                    "Langfuse comment skipped: could not resolve the project id for the configured API key.");
 046                return;
 47            }
 48
 449            var request = new LangfuseCommentRequest
 450            {
 451                ProjectId = projectId,
 452                ObjectType = TraceObjectType,
 453                ObjectId = traceId,
 454                Content = content,
 455            };
 56
 457            await _apiClient.PostAsync("api/public/comments", request, cancellationToken).ConfigureAwait(false);
 358        }
 159        catch (LangfuseException ex)
 60        {
 161            _diagnostics?.Invoke($"Langfuse comment on trace '{traceId}' failed: {ex.Message}");
 162        }
 463    }
 64
 65    private async Task<string?> ResolveProjectIdAsync(CancellationToken cancellationToken)
 66    {
 467        if (_projectId is not null)
 68        {
 169            return _projectId;
 70        }
 71
 372        await _projectIdLock.WaitAsync(cancellationToken).ConfigureAwait(false);
 73        try
 74        {
 375            if (_projectId is not null)
 76            {
 077                return _projectId;
 78            }
 79
 380            var response = await _apiClient
 381                .GetAsync<LangfuseProjectsResponse>("api/public/projects", cancellationToken)
 382                .ConfigureAwait(false);
 83
 384            _projectId = response?.Data is { Count: > 0 } data && !string.IsNullOrWhiteSpace(data[0].Id)
 385                ? data[0].Id
 386                : null;
 87
 388            return _projectId;
 89        }
 90        finally
 91        {
 392            _projectIdLock.Release();
 93        }
 494    }
 95}