From 439b33d4e4a02b7e1c9ced52730aa60989c70d0c Mon Sep 17 00:00:00 2001 From: Hassieb Pakzad <68423100+hassiebp@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:02:03 +0100 Subject: [PATCH] feat(client): allow propagating trace name --- langfuse/_client/propagation.py | 9 ++ tests/test_propagate_attributes.py | 225 +++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+) diff --git a/langfuse/_client/propagation.py b/langfuse/_client/propagation.py index 49d34a99f..0baca7e87 100644 --- a/langfuse/_client/propagation.py +++ b/langfuse/_client/propagation.py @@ -32,6 +32,7 @@ "metadata", "version", "tags", + "trace_name", ] InternalPropagatedKeys = Literal[ @@ -50,6 +51,7 @@ "metadata", "version", "tags", + "trace_name", "experiment_id", "experiment_name", "experiment_metadata", @@ -77,6 +79,7 @@ def propagate_attributes( metadata: Optional[Dict[str, str]] = None, version: Optional[str] = None, tags: Optional[List[str]] = None, + trace_name: Optional[str] = None, as_baggage: bool = False, ) -> _AgnosticContextManager[Any]: """Propagate trace-level attributes to all spans created within this context. @@ -109,6 +112,8 @@ def propagate_attributes( - AVOID: large payloads, sensitive data, non-string values (will be dropped with warning) version: Version identfier for parts of your application that are independently versioned, e.g. agents tags: List of tags to categorize the group of observations + trace_name: Name to assign to the trace. Must be US-ASCII string, ≤200 characters. + Use this to set a consistent trace name for all spans created within this context. as_baggage: If True, propagates attributes using OpenTelemetry baggage for cross-process/service propagation. **Security warning**: When enabled, attribute values are added to HTTP headers on ALL outbound requests. @@ -195,6 +200,7 @@ def propagate_attributes( metadata=metadata, version=version, tags=tags, + trace_name=trace_name, as_baggage=as_baggage, ) @@ -207,6 +213,7 @@ def _propagate_attributes( metadata: Optional[Dict[str, str]] = None, version: Optional[str] = None, tags: Optional[List[str]] = None, + trace_name: Optional[str] = None, as_baggage: bool = False, experiment: Optional[PropagatedExperimentAttributes] = None, ) -> Generator[Any, Any, Any]: @@ -218,6 +225,7 @@ def _propagate_attributes( "session_id": session_id, "version": version, "tags": tags, + "trace_name": trace_name, } propagated_string_attributes = propagated_string_attributes | ( @@ -456,6 +464,7 @@ def _get_propagated_span_key(key: str) -> str: "user_id": LangfuseOtelSpanAttributes.TRACE_USER_ID, "version": LangfuseOtelSpanAttributes.VERSION, "tags": LangfuseOtelSpanAttributes.TRACE_TAGS, + "trace_name": LangfuseOtelSpanAttributes.TRACE_NAME, "experiment_id": LangfuseOtelSpanAttributes.EXPERIMENT_ID, "experiment_name": LangfuseOtelSpanAttributes.EXPERIMENT_NAME, "experiment_metadata": LangfuseOtelSpanAttributes.EXPERIMENT_METADATA, diff --git a/tests/test_propagate_attributes.py b/tests/test_propagate_attributes.py index d724b6d65..83c88e48a 100644 --- a/tests/test_propagate_attributes.py +++ b/tests/test_propagate_attributes.py @@ -2769,3 +2769,228 @@ def task_with_child(*, item, **kwargs): LangfuseOtelSpanAttributes.ENVIRONMENT, LANGFUSE_SDK_EXPERIMENT_ENVIRONMENT, ) + + +class TestPropagateAttributesTraceName(TestPropagateAttributesBase): + """Tests for trace_name parameter propagation.""" + + def test_trace_name_propagates_to_child_spans( + self, langfuse_client, memory_exporter + ): + """Verify trace_name propagates to all child spans within context.""" + with langfuse_client.start_as_current_span(name="parent-span"): + with propagate_attributes(trace_name="my-trace-name"): + child1 = langfuse_client.start_span(name="child-span-1") + child1.end() + + child2 = langfuse_client.start_span(name="child-span-2") + child2.end() + + # Verify both children have trace_name + child1_span = self.get_span_by_name(memory_exporter, "child-span-1") + self.verify_span_attribute( + child1_span, + LangfuseOtelSpanAttributes.TRACE_NAME, + "my-trace-name", + ) + + child2_span = self.get_span_by_name(memory_exporter, "child-span-2") + self.verify_span_attribute( + child2_span, + LangfuseOtelSpanAttributes.TRACE_NAME, + "my-trace-name", + ) + + def test_trace_name_propagates_to_grandchildren( + self, langfuse_client, memory_exporter + ): + """Verify trace_name propagates through multiple levels of nesting.""" + with langfuse_client.start_as_current_span(name="parent-span"): + with propagate_attributes(trace_name="nested-trace"): + with langfuse_client.start_as_current_span(name="child-span"): + grandchild = langfuse_client.start_span(name="grandchild-span") + grandchild.end() + + # Verify all three levels have trace_name + parent_span = self.get_span_by_name(memory_exporter, "parent-span") + child_span = self.get_span_by_name(memory_exporter, "child-span") + grandchild_span = self.get_span_by_name(memory_exporter, "grandchild-span") + + for span in [parent_span, child_span, grandchild_span]: + self.verify_span_attribute( + span, LangfuseOtelSpanAttributes.TRACE_NAME, "nested-trace" + ) + + def test_trace_name_with_user_and_session(self, langfuse_client, memory_exporter): + """Verify trace_name works together with user_id and session_id.""" + with langfuse_client.start_as_current_span(name="parent-span"): + with propagate_attributes( + user_id="user_123", + session_id="session_abc", + trace_name="combined-trace", + ): + child = langfuse_client.start_span(name="child-span") + child.end() + + # Verify child has all attributes + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user_123" + ) + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_SESSION_ID, "session_abc" + ) + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_NAME, "combined-trace" + ) + + def test_trace_name_with_version(self, langfuse_client, memory_exporter): + """Verify trace_name works together with version.""" + with langfuse_client.start_as_current_span(name="parent-span"): + with propagate_attributes( + trace_name="versioned-trace", + version="1.0.0", + ): + child = langfuse_client.start_span(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_NAME, "versioned-trace" + ) + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.VERSION, "1.0.0" + ) + + def test_trace_name_with_metadata(self, langfuse_client, memory_exporter): + """Verify trace_name works together with metadata.""" + with langfuse_client.start_as_current_span(name="parent-span"): + with propagate_attributes( + trace_name="metadata-trace", + metadata={"env": "production", "region": "us-east"}, + ): + child = langfuse_client.start_span(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_NAME, "metadata-trace" + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", + "production", + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.region", + "us-east", + ) + + def test_trace_name_validation_over_200_chars( + self, langfuse_client, memory_exporter + ): + """Verify trace_name over 200 characters is dropped with warning.""" + long_name = "trace-" + "a" * 200 # Create a very long trace name + + with langfuse_client.start_as_current_span(name="parent-span"): + with propagate_attributes(trace_name=long_name): + child = langfuse_client.start_span(name="child-span") + child.end() + + # Verify child does NOT have trace_name + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_missing_attribute(child_span, LangfuseOtelSpanAttributes.TRACE_NAME) + + def test_trace_name_exactly_200_chars(self, langfuse_client, memory_exporter): + """Verify exactly 200 character trace_name is accepted.""" + trace_name_200 = "t" * 200 + + with langfuse_client.start_as_current_span(name="parent-span"): + with propagate_attributes(trace_name=trace_name_200): + child = langfuse_client.start_span(name="child-span") + child.end() + + # Verify child HAS trace_name + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_NAME, trace_name_200 + ) + + def test_trace_name_nested_contexts_inner_overwrites( + self, langfuse_client, memory_exporter + ): + """Verify inner context overwrites outer trace_name.""" + with langfuse_client.start_as_current_span(name="parent-span"): + with propagate_attributes(trace_name="outer-trace"): + # Create span in outer context + span1 = langfuse_client.start_span(name="span-1") + span1.end() + + # Inner context with different trace_name + with propagate_attributes(trace_name="inner-trace"): + span2 = langfuse_client.start_span(name="span-2") + span2.end() + + # Back to outer context + span3 = langfuse_client.start_span(name="span-3") + span3.end() + + # Verify: span1 and span3 have outer-trace, span2 has inner-trace + span1_data = self.get_span_by_name(memory_exporter, "span-1") + self.verify_span_attribute( + span1_data, LangfuseOtelSpanAttributes.TRACE_NAME, "outer-trace" + ) + + span2_data = self.get_span_by_name(memory_exporter, "span-2") + self.verify_span_attribute( + span2_data, LangfuseOtelSpanAttributes.TRACE_NAME, "inner-trace" + ) + + span3_data = self.get_span_by_name(memory_exporter, "span-3") + self.verify_span_attribute( + span3_data, LangfuseOtelSpanAttributes.TRACE_NAME, "outer-trace" + ) + + def test_trace_name_sets_on_current_span(self, langfuse_client, memory_exporter): + """Verify trace_name is set on the current span when entering context.""" + with langfuse_client.start_as_current_span(name="parent-span"): + with propagate_attributes(trace_name="current-trace"): + pass # Just enter and exit context + + # Verify parent span has trace_name set + parent_span = self.get_span_by_name(memory_exporter, "parent-span") + self.verify_span_attribute( + parent_span, LangfuseOtelSpanAttributes.TRACE_NAME, "current-trace" + ) + + def test_trace_name_non_string_dropped(self, langfuse_client, memory_exporter): + """Verify non-string trace_name is dropped with warning.""" + with langfuse_client.start_as_current_span(name="parent-span"): + with propagate_attributes(trace_name=123): # type: ignore + child = langfuse_client.start_span(name="child-span") + child.end() + + # Verify child does NOT have trace_name + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_missing_attribute(child_span, LangfuseOtelSpanAttributes.TRACE_NAME) + + def test_trace_name_with_baggage(self, langfuse_client, memory_exporter): + """Verify trace_name propagates through baggage.""" + with langfuse_client.start_as_current_span(name="parent-span"): + with propagate_attributes( + trace_name="baggage-trace", + user_id="user_123", + as_baggage=True, + ): + child = langfuse_client.start_span(name="child-span") + child.end() + + # Verify child has trace_name + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_NAME, "baggage-trace" + ) + self.verify_span_attribute( + child_span, LangfuseOtelSpanAttributes.TRACE_USER_ID, "user_123" + )