From 2ac10d8244b21be4f2d6a9d06a7e1b83a57c81ae Mon Sep 17 00:00:00 2001 From: Cayman Roden Date: Wed, 25 Mar 2026 01:09:48 -0700 Subject: [PATCH] fix: coerce non-string metadata values instead of dropping them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the v3→v4 migration guide, propagated metadata non-string values should be "automatically coerced to strings." The implementation in _propagate_attributes dropped them with a warning instead. This caused noisy warnings with LangGraph, which injects non-string metadata (langgraph_step: int, langgraph_triggers: list, langgraph_path: tuple) into RunnableConfig on every graph step. Fix: coerce values via str() before validation in the metadata processing loop, matching the documented behavior. Fixes #1571 --- langfuse/_client/propagation.py | 8 ++++-- tests/test_propagate_attributes.py | 46 ++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/langfuse/_client/propagation.py b/langfuse/_client/propagation.py index e0c68db00..6cebf6b15 100644 --- a/langfuse/_client/propagation.py +++ b/langfuse/_client/propagation.py @@ -253,8 +253,12 @@ def _propagate_attributes( validated_metadata: Dict[str, str] = {} for key, value in metadata.items(): - if _validate_string_value(value=value, key=f"metadata.{key}"): - validated_metadata[key] = value + # Coerce non-string values to strings per v3→v4 migration guide + # (LangGraph injects int/list/tuple metadata like langgraph_step) + str_value = str(value) if not isinstance(value, str) else value + + if _validate_string_value(value=str_value, key=f"metadata.{key}"): + validated_metadata[key] = str_value if validated_metadata: context = _set_propagated_attribute( diff --git a/tests/test_propagate_attributes.py b/tests/test_propagate_attributes.py index 566ba6392..8c9330837 100644 --- a/tests/test_propagate_attributes.py +++ b/tests/test_propagate_attributes.py @@ -842,6 +842,52 @@ def test_all_invalid_metadata_values(self, langfuse_client, memory_exporter): child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.key2" ) + def test_non_string_metadata_values_coerced( + self, langfuse_client, memory_exporter + ): + """Verify non-string metadata values are coerced to strings, not dropped. + + LangGraph injects int/list/tuple metadata (langgraph_step, langgraph_triggers, + langgraph_path) which should be coerced per the v3->v4 migration guide. + See: https://github.com/langfuse/langfuse-python/issues/1571 + """ + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + metadata={ + "langgraph_step": 3, + "langgraph_triggers": ["start:__start__"], + "langgraph_path": ("__pregel_pull", "agent"), + "string_key": "normal_value", + } + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + + # Non-string values should be coerced to strings + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.langgraph_step", + "3", + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.langgraph_triggers", + "['start:__start__']", + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.langgraph_path", + "('__pregel_pull', 'agent')", + ) + # String values should pass through unchanged + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.string_key", + "normal_value", + ) + def test_propagate_with_no_active_span(self, langfuse_client, memory_exporter): """Verify propagate_attributes works even with no active span.""" # Call propagate_attributes without creating a parent span first