Keep exception text out of stored and exported span errors

Span.fail stored the exception message in span.error, which is stored and
exported whatever the content settings; a provider's content-filter error
can quote the prompt. span.error now holds the exception type, and the
message is a capture-gated preview, like tool errors already were. A
yielded stream error on the agent span is treated the same way.
This commit is contained in:
arc53-machine committed 2026-09-23 22:42:05 +01:00
1 parent 0bbbac0eb2
commit 0591580014
4 files changed
+32 -7

No files matched your search

+3 -1
View File
@@ -234,7 +234,9 @@ def _end_agent_span(span: Any, context: "LogContext", *, error: BaseException |
span.end(error=error, attributes=attributes)
elif context.stream_error:
span.set(**attributes, **{"error.type": "StreamError"})
span.error = context.stream_error
# The error event's text is gated like any other content.
span.preview("error", context.stream_error)
span.error = "StreamError"
span.end(tracing.STATUS_ERROR)
else:
span.end(None if completed else tracing.STATUS_CANCELLED, attributes=attributes)
+13 -2
View File
@@ -146,10 +146,21 @@ class Span:
return self
def fail(self, exc: BaseException) -> "Span":
"""Record ``exc`` on the span without ending it."""
"""Record ``exc`` on the span without ending it.
``error`` and ``error.type`` hold only the exception type: they are
stored and exported whatever the content settings, and exception text
can quote the prompt (a provider's content-filter error) or a tool
result. The message itself becomes the capture-gated ``error``
preview, dropped with every other preview when capture is off or a
guardrail fired.
"""
self.status = STATUS_ERROR
self.error = str(exc)[:500] or type(exc).__name__
self.error = type(exc).__name__
self.attributes["error.type"] = type(exc).__name__
message = str(exc)
if message:
self.preview("error", message)
return self
def end(
+2 -3
View File
@@ -398,8 +398,6 @@ class TestAccumulateResponseSummary:
assert ctx.tool_call_count == 2
class TestLogActivityTraceSpan:
"""``@log_activity`` opens the ``invoke_agent`` span for every agent run."""
@@ -475,7 +473,8 @@ class TestLogActivityTraceSpan:
with patch("docsgpt.logging._log_activity_to_db"), tracing.activate(trace):
list(gen(self._Agent()))
assert trace.spans[0].status == "error"
assert trace.spans[0].error == "node failed"
assert trace.spans[0].error == "StreamError"
assert trace.spans[0].previews["error"] == "node failed"
def test_raised_error_marks_span_error(self):
from docsgpt import tracing
+14 -1
View File
@@ -133,7 +133,20 @@ class TestNesting:
span = trace.spans[0]
assert span.status == "error"
assert span.attributes["error.type"] == "ValueError"
assert span.error == "boom"
# Exception text can quote a prompt: it is a capture-gated preview,
# never the always-stored/exported ``error``.
assert span.error == "ValueError"
assert span.previews["error"] == "boom"
def test_exception_text_is_dropped_when_capture_is_off(self, monkeypatch):
monkeypatch.setattr(settings, "TRACES_CAPTURE_CONTENT", False)
trace = tracing.start_trace(source="stream")
with tracing.activate(trace):
with pytest.raises(ValueError):
with tracing.span(tracing.KIND_LLM, "chat"):
raise ValueError("content filter: <the user's prompt>")
trace.finish()
assert "the user's prompt" not in str(trace.to_record())
def test_generator_exit_marks_span_cancelled(self):
trace = tracing.start_trace(source="stream")