From 0591580014db2283d797d670a50d73b04a458e18 Mon Sep 17 00:00:00 2001 From: arc53-machine <232052973+arc53-machine@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:42:05 +0100 Subject: [PATCH] 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. --- docsgpt/logging.py | 4 +++- docsgpt/tracing/core.py | 15 +++++++++++++-- tests/test_logging.py | 5 ++--- tests/tracing/test_trace.py | 15 ++++++++++++++- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/docsgpt/logging.py b/docsgpt/logging.py index 38ddf6da..5440631f 100644 --- a/docsgpt/logging.py +++ b/docsgpt/logging.py @@ -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) diff --git a/docsgpt/tracing/core.py b/docsgpt/tracing/core.py index 47c5dbba..6e3aef52 100644 --- a/docsgpt/tracing/core.py +++ b/docsgpt/tracing/core.py @@ -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( diff --git a/tests/test_logging.py b/tests/test_logging.py index 69642c82..07fba72a 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -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 diff --git a/tests/tracing/test_trace.py b/tests/tracing/test_trace.py index e728e0d3..b6e8c558 100644 --- a/tests/tracing/test_trace.py +++ b/tests/tracing/test_trace.py @@ -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: ") + 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")