diff --git a/langfuse/_client/span.py b/langfuse/_client/span.py index 96879499c..958141f1a 100644 --- a/langfuse/_client/span.py +++ b/langfuse/_client/span.py @@ -121,6 +121,8 @@ def __init__( cost_details: Cost information for the model call prompt: Associated prompt template from Langfuse prompt management """ + if isinstance(metadata, dict): + metadata = {k: v for k, v in metadata.items() if v is not None and v != ""} self._otel_span = otel_span self._otel_span.set_attribute( LangfuseOtelSpanAttributes.OBSERVATION_TYPE, as_type @@ -676,6 +678,9 @@ def update( if not self._otel_span.is_recording(): return self + if isinstance(metadata, dict): + metadata = {k: v for k, v in metadata.items() if v is not None and v != ""} + processed_input = self._process_media_and_apply_mask( data=input, field="input", span=self._otel_span ) diff --git a/tests/unit/test_span_sanitization.py b/tests/unit/test_span_sanitization.py new file mode 100644 index 000000000..68308d5cb --- /dev/null +++ b/tests/unit/test_span_sanitization.py @@ -0,0 +1,41 @@ +from unittest.mock import MagicMock, patch + +from opentelemetry import trace as otel_trace_api + +from langfuse._client.span import LangfuseObservationWrapper + + +@patch("langfuse._client.span.LangfuseObservationWrapper._process_media_and_apply_mask") +def test_langfuse_observation_wrapper_metadata_sanitization(mock_process): + mock_otel_span = MagicMock(spec=otel_trace_api.Span) + mock_otel_span.is_recording.return_value = True + + mock_client = MagicMock() + + # Input with empty string value and None value + bad_metadata = {"valid_key": "value", "bad_empty": "", "bad_none": None} + + wrapper = LangfuseObservationWrapper( + otel_span=mock_otel_span, + langfuse_client=mock_client, + as_type="span", + metadata=bad_metadata, + ) + + # Check that _process_media_and_apply_mask was called for metadata with the sanitized dict + mock_process.assert_any_call( + data={"valid_key": "value"}, field="metadata", span=mock_otel_span + ) + + # Reset mock and test update method + mock_process.reset_mock() + bad_metadata_update = { + "another_valid": "value2", + "another_bad": "", + "another_none": None, + } + + wrapper.update(metadata=bad_metadata_update) + mock_process.assert_any_call( + data={"another_valid": "value2"}, field="metadata", span=mock_otel_span + )