Skip to content

Commit cd821f6

Browse files
Cueclaude
authored andcommitted
fix(tracing): keep raw errors behind content opt-in
Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f067762 commit cd821f6

6 files changed

Lines changed: 53 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
turn, chat, and tool spans end with `cubepi.run.outcome="suspended"`; observer
1616
failures cannot hide the terminal event, cancellation still propagates, and
1717
`respond()` opens a distinct correlated activation trace.
18+
- **Raw provider error details now follow the content-recording opt-in.** With
19+
`record_content=False`, traces retain typed error classification and ERROR
20+
status but use bounded generic descriptions and omit exception messages and
21+
stack traces. `record_content=True` preserves the prior diagnostic detail.
1822

1923
## [0.13.3] - 2026-08-02
2024

cubepi/tracing/recorder.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -844,7 +844,11 @@ def _on_turn_end(self, event: TurnEndEvent) -> None:
844844
# "error", mark turn ERROR. Abort path leaves UNSET + sets
845845
# cubepi.aborted on the invoke_agent root.
846846
if stop_reason == "error":
847-
err_msg = getattr(msg, "error_message", None) or "model error"
847+
err_msg = (
848+
getattr(msg, "error_message", None) or "model error"
849+
if self._record_content
850+
else "model error"
851+
)
848852
run.turn_span.set_status(Status(StatusCode.ERROR, err_msg[:256]))
849853
run.turn_span.set_attribute(ERROR_TYPE, "cubepi.error")
850854
elif stop_reason == "aborted":
@@ -1267,17 +1271,24 @@ def _on_provider_response(
12671271
span.set_attribute(CUBEPI_ABORTED, True)
12681272
span.set_attribute(ERROR_TYPE, "cubepi.aborted")
12691273
else:
1270-
span.set_status(Status(StatusCode.ERROR, str(exc)[:256]))
1274+
description = (
1275+
str(exc)[:256] if self._record_content else "provider error"
1276+
)
1277+
span.set_status(Status(StatusCode.ERROR, description))
12711278
span.set_attribute(ERROR_TYPE, cubepi_error_type_for(exc))
1279+
exception_attrs = {"exception.type": type(exc).__name__}
1280+
if self._record_content:
1281+
exception_attrs.update(
1282+
{
1283+
"exception.message": str(exc),
1284+
"exception.stacktrace": "".join(
1285+
traceback.format_exception(exc)
1286+
),
1287+
}
1288+
)
12721289
span.add_event(
12731290
name=EVENT_GEN_AI_EXCEPTION,
1274-
attributes={
1275-
"exception.type": type(exc).__name__,
1276-
"exception.message": str(exc),
1277-
"exception.stacktrace": "".join(
1278-
traceback.format_exception(exc)
1279-
),
1280-
},
1291+
attributes=exception_attrs,
12811292
)
12821293
finally:
12831294
span.end()

dev/plans/2026-08-04-tracing-hitl-suspension.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ OpenTelemetry SDK, pytest, Ruff, mypy, uv.
5050
- Move assistant output accumulation to `MessageEndEvent` so suspension before
5151
`TurnEndEvent` still records the partial output.
5252
- Do not change `_close_open_spans()` cancellation semantics.
53+
- When `record_content=False`, preserve typed ERROR classification while using
54+
generic status descriptions and omitting exception messages/stack traces.
5355

5456
## Task 4: Document the lifecycle contract
5557

dev/specs/2026-08-04-tracing-hitl-suspension.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ left the old `_RunState` retained in that task's context.
3737
- Remove tool-span and MCP-provider registrations during normal tracing detach.
3838
- Count the assistant tool-call message as partial activation output.
3939
- Keep tracing observational: recorder failures must not affect the agent.
40+
- Keep raw provider error messages and stack traces behind `record_content=True`;
41+
privacy-default traces retain only typed error classification.
4042

4143
## Non-goals
4244

tests/tracing/test_content_recording.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,26 @@ async def test_no_content_attrs_when_record_content_false(self):
102102
f"{span.name} should not carry {forbidden} with record_content=False"
103103
)
104104

105+
async def test_error_details_are_private_when_record_content_false(self):
106+
secret = "Authorization: Bearer TOPSECRET"
107+
agent, provider, exporter, tracer = await _build(record_content=False)
108+
109+
def explode(*_args):
110+
raise RuntimeError(secret)
111+
112+
provider.append_responses([explode])
113+
114+
await agent.prompt("private prompt")
115+
await agent.wait_for_idle()
116+
await tracer.shutdown()
117+
118+
assert any(span.status.status_code.name == "ERROR" for span in exporter.spans)
119+
for span in exporter.spans:
120+
assert secret not in (span.status.description or "")
121+
assert secret not in repr(dict(span.attributes or {}))
122+
for event in span.events:
123+
assert secret not in repr(dict(event.attributes or {}))
124+
105125

106126
class TestRootContent:
107127
async def test_invoke_agent_records_input_output_system(self):

website/docs/guides/tracing/getting-started.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ The recorder treats cancellation as a control signal, not a failure:
147147
- A provider raising → chat/turn/root close with **status ERROR**, an
148148
`exception` event on the chat span, and `error.type` derived from the
149149
exception class (`timeout`, `connection_error`, fully-qualified class name, …).
150+
With the default `record_content=False`, the status description is generic and
151+
the event contains only `exception.type`; raw exception messages and stack
152+
traces are included only when content recording is explicitly enabled.
150153
- An MCP `tools/call` returning `isError=true` → CLIENT span closes
151154
ERROR + `error.type=mcp.is_error`.
152155

@@ -182,7 +185,8 @@ Optional, opt-in via `Tracer(record_content=True)`:
182185
`gen_ai.input.messages`, `gen_ai.output.messages`, `gen_ai.system_instructions`,
183186
`gen_ai.tool.definitions`, `gen_ai.tool.call.arguments`,
184187
`gen_ai.tool.call.result`, `cubepi.llm.raw_request`,
185-
`cubepi.llm.raw_response`. See [Content & Redaction](./content-recording).
188+
`cubepi.llm.raw_response`, provider exception messages, and provider exception
189+
stack traces. See [Content & Redaction](./content-recording).
186190

187191
## Multiple agents, one process
188192

0 commit comments

Comments
 (0)