feat(usage): reproduce TokenTracker session_hash for task attribution
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Usage.TokenTracker;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reproduces TokenTracker's session identity so its export rows can be joined onto our
|
||||||
|
/// <c>task_runs.session_id</c>. The formula is <c>sha256(source + "\0" + id)</c>, hex, first
|
||||||
|
/// 24 chars (TokenTracker's <c>lib/session-analytics.js</c>, <c>sessionHash()</c>). The export
|
||||||
|
/// never carries the raw session id, so this is the only way to attribute a row to a task.
|
||||||
|
/// Verified against a real export on 2026-08-24 — the test vectors are the proof, don't touch them.
|
||||||
|
/// </summary>
|
||||||
|
public static class SessionHash
|
||||||
|
{
|
||||||
|
private const int HashLength = 24;
|
||||||
|
|
||||||
|
public static string? ForClaudeSession(string? sessionId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(sessionId)) return null;
|
||||||
|
|
||||||
|
var bytes = Encoding.UTF8.GetBytes($"claude\0{sessionId}");
|
||||||
|
var hash = SHA256.HashData(bytes);
|
||||||
|
return Convert.ToHexString(hash).ToLowerInvariant()[..HashLength];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using ClaudeDo.Worker.Usage.TokenTracker;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Tests.Usage.TokenTracker;
|
||||||
|
|
||||||
|
public sealed class SessionHashTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("77470328-94f7-49d3-b378-a562d1501b5f", "b24babcbf2b615730459773f")]
|
||||||
|
[InlineData("20ebc1bc-3e6a-4c09-a4b3-19472dffa841", "3e2a08fe8f2b941817fb554f")]
|
||||||
|
[InlineData("559e7ebb-7215-4112-9faf-be86bece9613", "03c30f4bc0bfe82c3fbb17c1")]
|
||||||
|
public void ForClaudeSession_MatchesTokenTrackerHash(string sessionId, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, SessionHash.ForClaudeSession(sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ForClaudeSession_IsLowercaseHexOf24Chars()
|
||||||
|
{
|
||||||
|
var hash = SessionHash.ForClaudeSession("any-session-id");
|
||||||
|
|
||||||
|
Assert.Equal(24, hash!.Length);
|
||||||
|
Assert.All(hash, c => Assert.Contains(c, "0123456789abcdef"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null)]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData(" ")]
|
||||||
|
public void ForClaudeSession_BlankId_ReturnsNull(string? sessionId)
|
||||||
|
{
|
||||||
|
Assert.Null(SessionHash.ForClaudeSession(sessionId));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user