Skip to content

Commit e12de69

Browse files
committed
fix: preserve JSON structure for redacted message attributes
When OmitIO is true, wrap REDACTED placeholder in proper JSON structure so the UI can parse roles and show user/assistant entries in the conversation timeline.
1 parent 88a872e commit e12de69

3 files changed

Lines changed: 28 additions & 7 deletions

File tree

internal/otlp/otlp.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,9 +324,12 @@ func eventAttributes(event map[string]any, cfg Config) []Attribute {
324324
key = mapped
325325
}
326326
if t, ok := attrTransformMap[k]; ok {
327-
key = t.key
327+
// Apply transform with redacted placeholder to preserve JSON structure
328+
redactedTransformed := t.transform(redactedValue)
329+
attrs = append(attrs, Attribute{Key: t.key, Value: StringVal(redactedTransformed)})
330+
} else {
331+
attrs = append(attrs, Attribute{Key: key, Value: StringVal(redactedValue)})
328332
}
329-
attrs = append(attrs, Attribute{Key: key, Value: StringVal(redactedValue)})
330333
continue
331334
}
332335
if t, ok := attrTransformMap[k]; ok {

internal/otlp/otlp_test.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,10 +237,14 @@ func TestSendLogOmitIO(t *testing.T) {
237237
assertAttr(t, lr.Attributes, "gen_ai.tool.name", "Bash")
238238

239239
// Content attributes are present but redacted.
240+
// Tool I/O uses plain redaction.
240241
assertAttr(t, lr.Attributes, "gen_ai.tool.call.arguments", "<REDACTED>")
241242
assertAttr(t, lr.Attributes, "gen_ai.tool.call.result", "<REDACTED>")
242-
assertAttr(t, lr.Attributes, "gen_ai.output.messages", "<REDACTED>")
243-
assertAttr(t, lr.Attributes, "gen_ai.input.messages", "<REDACTED>")
243+
// Message attributes preserve JSON structure for UI parsing.
244+
assertAttrContains(t, lr.Attributes, "gen_ai.output.messages", `"role":"assistant"`)
245+
assertAttrContains(t, lr.Attributes, "gen_ai.output.messages", `REDACTED`)
246+
assertAttrContains(t, lr.Attributes, "gen_ai.input.messages", `"role":"user"`)
247+
assertAttrContains(t, lr.Attributes, "gen_ai.input.messages", `REDACTED`)
244248
}
245249

246250
func TestTruncateContent(t *testing.T) {
@@ -320,6 +324,18 @@ func assertAttr(t *testing.T, attrs []Attribute, key, want string) {
320324
t.Errorf("attribute %s not found", key)
321325
}
322326

327+
func assertAttrContains(t *testing.T, attrs []Attribute, key, substr string) {
328+
t.Helper()
329+
for _, a := range attrs {
330+
if a.Key == key {
331+
require.NotNil(t, a.Value.StringValue, "attribute %s: stringValue is nil", key)
332+
assert.Contains(t, *a.Value.StringValue, substr, "attribute %s should contain %q", key, substr)
333+
return
334+
}
335+
}
336+
t.Errorf("attribute %s not found", key)
337+
}
338+
323339
func assertIntAttr(t *testing.T, attrs []Attribute, key string, want int64) {
324340
t.Helper()
325341
for _, a := range attrs {

internal/otlp/trace_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,11 @@ func TestNewLLMSpanOmitIO(t *testing.T) {
278278

279279
// Model is still present.
280280
assertAttr(t, span.Attributes, "gen_ai.request.model", "claude-sonnet-4-20250514")
281-
// Content attributes are present but redacted.
282-
assertAttr(t, span.Attributes, "gen_ai.input.messages", "<REDACTED>")
283-
assertAttr(t, span.Attributes, "gen_ai.output.messages", "<REDACTED>")
281+
// Content attributes are present but redacted, preserving JSON structure for UI parsing.
282+
assertAttrContains(t, span.Attributes, "gen_ai.input.messages", `"role":"user"`)
283+
assertAttrContains(t, span.Attributes, "gen_ai.input.messages", `REDACTED`)
284+
assertAttrContains(t, span.Attributes, "gen_ai.output.messages", `"role":"assistant"`)
285+
assertAttrContains(t, span.Attributes, "gen_ai.output.messages", `REDACTED`)
284286
}
285287

286288
func TestSendTraceSkipsWhenNotConfigured(t *testing.T) {

0 commit comments

Comments
 (0)