diff --git a/langfuse/_client/propagation.py b/langfuse/_client/propagation.py index 597d8126e..2cdc6acf4 100644 --- a/langfuse/_client/propagation.py +++ b/langfuse/_client/propagation.py @@ -90,12 +90,11 @@ def _detach_context_token_safely(token: Any) -> None: except Exception: pass - def propagate_attributes( *, user_id: Optional[str] = None, session_id: Optional[str] = None, - metadata: Optional[Dict[str, str]] = None, + metadata: Optional[Dict[str, Any]] = None, version: Optional[str] = None, tags: Optional[List[str]] = None, trace_name: Optional[str] = None, @@ -205,8 +204,9 @@ def propagate_attributes( Note: - **Validation**: All attribute values (user_id, session_id, metadata values) - must be strings ≤200 characters. Invalid values will be dropped with a - warning logged. Ensure values meet constraints before calling. + must be ≤200 characters. Non-string metadata values (int, float, bool, etc.) + are automatically coerced to their string representation. Values exceeding + 200 characters will be dropped with a warning logged. - **OpenTelemetry**: This uses OpenTelemetry context propagation under the hood, making it compatible with other OTel-instrumented libraries. @@ -229,7 +229,7 @@ def _propagate_attributes( *, user_id: Optional[str] = None, session_id: Optional[str] = None, - metadata: Optional[Dict[str, str]] = None, + metadata: Optional[Dict[str, Any]] = None, version: Optional[str] = None, tags: Optional[List[str]] = None, trace_name: Optional[str] = None, @@ -247,10 +247,9 @@ def _propagate_attributes( "trace_name": trace_name, } - propagated_metadata_attributes: Dict[str, Optional[Dict[str, str]]] = { + propagated_metadata_attributes: Dict[str, Optional[Dict[str, Any]]] = { "metadata": metadata, } - if experiment: for key, value in experiment.items(): if key in ("experiment_metadata", "experiment_item_metadata"): @@ -286,8 +285,20 @@ def _propagate_attributes( validated_metadata: Dict[str, str] = {} for key, value in metadata_value.items(): - if _validate_string_value(value=value, key=f"{metadata_key}.{key}"): - validated_metadata[key] = value + if value is None: + continue + if isinstance(value, str): + string_value = value + elif isinstance(value, bool): + string_value = "true" if value else "false" + elif isinstance(value, (dict, list, tuple)): + import json + string_value = json.dumps(value) + else: + string_value = str(value) + + if _validate_string_value(value=string_value, key=f"{metadata_key}.{key}"): + validated_metadata[key] = string_value if validated_metadata: context = _set_propagated_attribute( diff --git a/tests/unit/test_propagate_attributes.py b/tests/unit/test_propagate_attributes.py index c783e65dd..8230a23ac 100644 --- a/tests/unit/test_propagate_attributes.py +++ b/tests/unit/test_propagate_attributes.py @@ -490,6 +490,48 @@ def test_mixed_valid_invalid_metadata(self, langfuse_client, memory_exporter): child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.invalid_key" ) + def test_metadata_coercion_of_non_string_values(self, langfuse_client, memory_exporter): + """Verify metadata values that are not strings (int, float, bool, dict, list) are coerced to strings.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + metadata={ + "int_val": 42, # type: ignore + "float_val": 3.14, # type: ignore + "bool_val": True, # type: ignore + "dict_val": {"nested": "val"}, # type: ignore + "list_val": [1, 2], # type: ignore + } + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + # Verify child has all metadata keys coerced to string values + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.int_val", + "42", + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.float_val", + "3.14", + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.bool_val", + "true", + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.dict_val", + '{"nested": "val"}', + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.list_val", + "[1, 2]", + ) class TestPropagateAttributesNesting(TestPropagateAttributesBase): """Tests for nested propagate_attributes contexts."""