Skip to content

Commit a976fff

Browse files
authored
Merge pull request #11 from comet-ml/collinc/cc-inline-metrics-and-identity
Inline per-trace metrics + Claude Code identity on traces
2 parents cb8830f + d8e55cf commit a976fff

5 files changed

Lines changed: 395 additions & 32 deletions

File tree

src/api.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ func (a *API) Patch(endpoint string, data interface{}) error {
2929
return a.request("PATCH", endpoint, data)
3030
}
3131

32+
func (a *API) Put(endpoint string, data interface{}) error {
33+
return a.request("PUT", endpoint, data)
34+
}
35+
3236
func (a *API) request(method, endpoint string, data interface{}) error {
3337
jsonData, err := json.Marshal(data)
3438
if err != nil {
@@ -66,16 +70,17 @@ func (a *API) request(method, endpoint string, data interface{}) error {
6670
}
6771

6872
type Trace struct {
69-
ID string `json:"id"`
70-
Name string `json:"name"`
71-
StartTime string `json:"start_time"`
72-
EndTime string `json:"end_time,omitempty"`
73-
ProjectName string `json:"project_name"`
74-
ThreadID string `json:"thread_id,omitempty"`
75-
Tags []string `json:"tags,omitempty"`
76-
Input map[string]string `json:"input,omitempty"`
77-
Output map[string]string `json:"output,omitempty"`
78-
Model string `json:"model,omitempty"`
73+
ID string `json:"id"`
74+
Name string `json:"name"`
75+
StartTime string `json:"start_time"`
76+
EndTime string `json:"end_time,omitempty"`
77+
ProjectName string `json:"project_name"`
78+
ThreadID string `json:"thread_id,omitempty"`
79+
Tags []string `json:"tags,omitempty"`
80+
Input map[string]string `json:"input,omitempty"`
81+
Output map[string]string `json:"output,omitempty"`
82+
Model string `json:"model,omitempty"`
83+
Metadata map[string]interface{} `json:"metadata,omitempty"`
7984
}
8085

8186
type Span struct {

src/identity.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
"path/filepath"
7+
)
8+
9+
// ClaudeIdentity is Claude Code's OAuth identity, read from ~/.claude.json.
10+
// Used as the source of truth for who is running this session.
11+
type ClaudeIdentity struct {
12+
UserUUID string `json:"user_uuid,omitempty"`
13+
Email string `json:"email,omitempty"`
14+
DisplayName string `json:"display_name,omitempty"`
15+
OrgUUID string `json:"org_uuid,omitempty"`
16+
OrgName string `json:"org_name,omitempty"`
17+
}
18+
19+
// loadClaudeIdentity reads ~/.claude.json and pulls the oauthAccount block.
20+
// Returns a zero-value identity on any read/parse failure — identity is best-effort.
21+
func loadClaudeIdentity() ClaudeIdentity {
22+
home, err := os.UserHomeDir()
23+
if err != nil {
24+
return ClaudeIdentity{}
25+
}
26+
data, err := os.ReadFile(filepath.Join(home, ".claude.json"))
27+
if err != nil {
28+
return ClaudeIdentity{}
29+
}
30+
var raw struct {
31+
OAuthAccount struct {
32+
AccountUUID string `json:"accountUuid"`
33+
EmailAddress string `json:"emailAddress"`
34+
DisplayName string `json:"displayName"`
35+
OrganizationUUID string `json:"organizationUuid"`
36+
OrganizationName string `json:"organizationName"`
37+
} `json:"oauthAccount"`
38+
}
39+
if err := json.Unmarshal(data, &raw); err != nil {
40+
return ClaudeIdentity{}
41+
}
42+
return ClaudeIdentity{
43+
UserUUID: raw.OAuthAccount.AccountUUID,
44+
Email: raw.OAuthAccount.EmailAddress,
45+
DisplayName: raw.OAuthAccount.DisplayName,
46+
OrgUUID: raw.OAuthAccount.OrganizationUUID,
47+
OrgName: raw.OAuthAccount.OrganizationName,
48+
}
49+
}
50+
51+
// applyToTrace stamps identity onto a Trace's metadata and adds a `user:<email>`
52+
// tag for quick filtering in the Opik UI. No-op when identity is empty.
53+
func (i ClaudeIdentity) applyToTrace(t *Trace) {
54+
if i.Email == "" && i.UserUUID == "" {
55+
return
56+
}
57+
if t.Metadata == nil {
58+
t.Metadata = map[string]interface{}{}
59+
}
60+
cc := map[string]interface{}{}
61+
if i.Email != "" {
62+
cc["user_email"] = i.Email
63+
}
64+
if i.UserUUID != "" {
65+
cc["user_uuid"] = i.UserUUID
66+
}
67+
if i.DisplayName != "" {
68+
cc["user_display_name"] = i.DisplayName
69+
}
70+
if i.OrgUUID != "" {
71+
cc["org_uuid"] = i.OrgUUID
72+
}
73+
if i.OrgName != "" {
74+
cc["org_name"] = i.OrgName
75+
}
76+
t.Metadata["cc"] = cc
77+
78+
if i.Email != "" {
79+
t.Tags = append(t.Tags, "user:"+i.Email)
80+
}
81+
}

src/main.go

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,16 @@ func onPrompt() {
9393
}
9494
ts := isoNow()
9595

96+
cwd, headStart := captureCwdAndHead()
9697
state := &State{
97-
TraceID: traceID,
98-
StartTime: ts,
99-
SessionID: input.SessionID,
100-
Transcript: input.TranscriptPath,
101-
StartLine: startLine,
102-
LastFlush: time.Now().Unix(),
98+
TraceID: traceID,
99+
StartTime: ts,
100+
SessionID: input.SessionID,
101+
Transcript: input.TranscriptPath,
102+
StartLine: startLine,
103+
LastFlush: time.Now().Unix(),
104+
Cwd: cwd,
105+
HeadSHAStart: headStart,
103106
}
104107
if err := SaveState(state); err != nil {
105108
debugLog("save state: %v", err)
@@ -117,6 +120,7 @@ func onPrompt() {
117120
Tags: []string{"claude-code"},
118121
Input: map[string]string{"text": input.Prompt},
119122
}
123+
loadClaudeIdentity().applyToTrace(&trace)
120124
if err := api.Post("/traces", trace); err != nil {
121125
debugLog("create trace: %v", err)
122126
}
@@ -135,9 +139,10 @@ func onTool() {
135139
debugLog("flushing (%ds)", now-state.LastFlush)
136140
flush(state)
137141
state.LastFlush = now
138-
if err := SaveState(state); err != nil {
139-
debugLog("save state: %v", err)
140-
}
142+
}
143+
144+
if err := SaveState(state); err != nil {
145+
debugLog("save state: %v", err)
141146
}
142147
}
143148

@@ -151,6 +156,7 @@ func onStop() {
151156
}
152157

153158
flush(state)
159+
postTraceMetrics(state)
154160

155161
output := getLastOutput(state)
156162
ts := isoNow()
@@ -180,6 +186,7 @@ func onSessionEnd() {
180186
state, err := LoadState(input.SessionID)
181187
if err == nil {
182188
flush(state)
189+
postTraceMetrics(state)
183190
ts := isoNow()
184191
finalUpdate := map[string]interface{}{
185192
"project_name": config.Project,
@@ -202,13 +209,17 @@ func onCompact() {
202209
traceID = uuid7()
203210
}
204211
ts := isoNow()
212+
startLine := countLines(input.TranscriptPath)
213+
cwd, headStart := captureCwdAndHead()
205214
state = &State{
206-
TraceID: traceID,
207-
StartTime: ts,
208-
SessionID: input.SessionID,
209-
Transcript: input.TranscriptPath,
210-
StartLine: countLines(input.TranscriptPath),
211-
LastFlush: time.Now().Unix(),
215+
TraceID: traceID,
216+
StartTime: ts,
217+
SessionID: input.SessionID,
218+
Transcript: input.TranscriptPath,
219+
StartLine: startLine,
220+
LastFlush: time.Now().Unix(),
221+
Cwd: cwd,
222+
HeadSHAStart: headStart,
212223
}
213224

214225
if config.ParentTraceID == "" {
@@ -220,6 +231,7 @@ func onCompact() {
220231
ThreadID: input.SessionID,
221232
Tags: []string{"claude-code"},
222233
}
234+
loadClaudeIdentity().applyToTrace(&trace)
223235
if err := api.Post("/traces", trace); err != nil {
224236
debugLog("compact: create trace: %v", err)
225237
}
@@ -230,6 +242,7 @@ func onCompact() {
230242
}
231243
} else {
232244
flush(state)
245+
postTraceMetrics(state)
233246
}
234247

235248
compactTraceID := uuid7()
@@ -250,6 +263,7 @@ func onCompact() {
250263
ThreadID: input.SessionID,
251264
Tags: []string{"claude-code", "compaction"},
252265
}
266+
loadClaudeIdentity().applyToTrace(&trace)
253267
if err := api.Post("/traces", trace); err != nil {
254268
debugLog("compact: create trace: %v", err)
255269
}
@@ -272,6 +286,7 @@ func onCompact() {
272286
state.TraceID = compactTraceID
273287
state.StartLine = countLines(input.TranscriptPath)
274288
state.LastFlush = time.Now().Unix()
289+
state.Cwd, state.HeadSHAStart = captureCwdAndHead()
275290
if err := SaveState(state); err != nil {
276291
debugLog("save state: %v", err)
277292
}
@@ -802,3 +817,4 @@ func debugLog(format string, args ...interface{}) {
802817
fmt.Fprintf(f, "[%s] ", ts)
803818
fmt.Fprintf(f, format+"\n", args...)
804819
}
820+

0 commit comments

Comments
 (0)