fix(langfuse): keep trace_public true when a guardrail span is exported

Langfuse folds langfuse.trace.public across every observation in the trace and reads a missing attribute as false, so a guardrail child span without the flag turned a trace_public: true request private on Langfuse Cloud. Child spans now repeat the generation's value

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yucheng 2026-09-17 05:22:03 +00:00
parent 5192f92eb5
commit 2dbbea5b6b
2 changed files with 36 additions and 1 deletions

View file

@ -178,12 +178,23 @@ def start_child_span(
The server derives the trace's name and I/O from every observation marked
root, last start time wins, so only the generation may claim root. Nesting
the rest under it keeps a post-call guardrail from rewriting the trace.
The trace's ``public`` flag is folded the same way, with a missing attribute
read as ``False``, so the child repeats the generation's value.
"""
parent_span: Final = parent._otel_span # pyright: ignore[reportPrivateUsage] # the wrapper exposes no public span handle
otel_span: Final = client._otel_tracer.start_span( # pyright: ignore[reportPrivateUsage] # only route to a historical start time
name=name,
context=otel_trace.set_span_in_context(parent._otel_span), # pyright: ignore[reportPrivateUsage] # the wrapper exposes no public span handle
context=otel_trace.set_span_in_context(parent_span),
start_time=to_unix_nanos(start_time),
)
public: Final = (
parent_span.attributes.get(PUBLIC_ATTRIBUTE)
if isinstance(parent_span, ReadableSpan) and parent_span.attributes is not None
else None
)
if public is not None:
otel_span.set_attribute(PUBLIC_ATTRIBUTE, public)
return LangfuseSpan(otel_span=otel_span, langfuse_client=client, **attributes) # pyright: ignore[reportArgumentType] # kwargs-ok: callback-built params, v2 accepted the same shapes

View file

@ -227,6 +227,30 @@ def test_trace_public_flag_is_absent_when_not_requested(client):
assert PUBLIC_ATTRIBUTE not in _only_span(exporter, "gen").attributes
@pytest.mark.parametrize("public", [True, False, None], ids=["public", "private", "unset"])
def test_child_span_repeats_the_generation_public_flag(client, public):
"""The server folds ``public`` across observations and reads a missing value as False.
A guardrail span without the flag turned a ``trace_public: true`` request private on Langfuse Cloud.
"""
lf, exporter = client
context, claim_root = open_trace_context(client=lf, trace_id="c" * 32, parent_observation_id=None)
generation = start_generation(
client=lf,
context=context,
name="gen",
start_time=CALL_START,
claim_trace_root=claim_root,
public=public,
attributes={},
)
start_child_span(client=lf, parent=generation, name="guardrail", start_time=CALL_END, attributes={}).end()
generation.end(end_time=to_unix_nanos(CALL_END))
lf.flush()
assert _only_span(exporter, "guardrail").attributes.get(PUBLIC_ATTRIBUTE) is public
def test_request_release_beats_the_client_wide_release(monkeypatch):
"""A client configured with its own release must not overwrite trace_release."""
monkeypatch.setenv("LANGFUSE_RELEASE", "client-wide-release")