feat(usage): parse the TokenTracker sessions export defensively
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ClaudeDo.Worker.Usage.TokenTracker;
|
||||
|
||||
/// <summary>
|
||||
/// Parses <c>tokentracker sessions --format json</c>. The export is not a promised API, so every
|
||||
/// field is read defensively: a missing number becomes 0, an unreadable row is skipped, and
|
||||
/// unparseable output returns null rather than throwing into the caller's face.
|
||||
/// </summary>
|
||||
public static class TokenTrackerExportParser
|
||||
{
|
||||
/// <summary>The <c>version</c> value this parser was written against (CLI v0.88.4).</summary>
|
||||
public const int SupportedFormatVersion = 11;
|
||||
|
||||
public static TokenTrackerExport? Parse(string? json, DateTime fetchedAtUtc)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return null;
|
||||
|
||||
JsonDocument doc;
|
||||
try { doc = JsonDocument.Parse(json); }
|
||||
catch (JsonException) { return null; }
|
||||
|
||||
using (doc)
|
||||
{
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object) return null;
|
||||
if (!root.TryGetProperty("sessions", out var sessions) ||
|
||||
sessions.ValueKind != JsonValueKind.Array) return null;
|
||||
|
||||
var parsed = new List<TokenTrackerSession>();
|
||||
var version = 0;
|
||||
|
||||
foreach (var row in sessions.EnumerateArray())
|
||||
{
|
||||
if (row.ValueKind != JsonValueKind.Object) continue;
|
||||
|
||||
version = Math.Max(version, (int)GetLong(row, "version"));
|
||||
|
||||
var hash = GetString(row, "session_hash");
|
||||
if (string.IsNullOrWhiteSpace(hash)) continue;
|
||||
|
||||
var source = GetString(row, "source") ?? "";
|
||||
if (!string.Equals(source, "claude", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
|
||||
if (!DateTimeOffset.TryParse(GetString(row, "started_at"), out var startedAt)) continue;
|
||||
|
||||
var tokens = row.TryGetProperty("tokens", out var t) && t.ValueKind == JsonValueKind.Object
|
||||
? t
|
||||
: default;
|
||||
|
||||
parsed.Add(new TokenTrackerSession(
|
||||
SessionHash: hash!,
|
||||
Source: source,
|
||||
Model: GetString(row, "model") ?? "",
|
||||
StartedAt: startedAt,
|
||||
InputTokens: GetLong(tokens, "input_tokens"),
|
||||
OutputTokens: GetLong(tokens, "output_tokens"),
|
||||
CacheReadTokens: GetLong(tokens, "cached_input_tokens"),
|
||||
CacheCreationTokens: GetLong(tokens, "cache_creation_input_tokens"),
|
||||
CostUsd: GetDouble(row, "cost_usd"),
|
||||
Turns: (int)GetLong(row, "turns"),
|
||||
RetryTurns: (int)GetLong(row, "retry_turns"),
|
||||
Productive: GetBool(row, "productive"),
|
||||
OneShot: GetBool(row, "one_shot")));
|
||||
}
|
||||
|
||||
return new TokenTrackerExport(version, parsed, fetchedAtUtc);
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetString(JsonElement obj, string prop) =>
|
||||
obj.ValueKind == JsonValueKind.Object &&
|
||||
obj.TryGetProperty(prop, out var el) &&
|
||||
el.ValueKind == JsonValueKind.String
|
||||
? el.GetString()
|
||||
: null;
|
||||
|
||||
private static long GetLong(JsonElement obj, string prop) =>
|
||||
obj.ValueKind == JsonValueKind.Object &&
|
||||
obj.TryGetProperty(prop, out var el) &&
|
||||
el.ValueKind == JsonValueKind.Number &&
|
||||
el.TryGetInt64(out var v)
|
||||
? v
|
||||
: 0;
|
||||
|
||||
private static double GetDouble(JsonElement obj, string prop) =>
|
||||
obj.ValueKind == JsonValueKind.Object &&
|
||||
obj.TryGetProperty(prop, out var el) &&
|
||||
el.ValueKind == JsonValueKind.Number &&
|
||||
el.TryGetDouble(out var v)
|
||||
? v
|
||||
: 0;
|
||||
|
||||
private static bool GetBool(JsonElement obj, string prop) =>
|
||||
obj.ValueKind == JsonValueKind.Object &&
|
||||
obj.TryGetProperty(prop, out var el) &&
|
||||
el.ValueKind == JsonValueKind.True;
|
||||
}
|
||||
Reference in New Issue
Block a user