From 5f133e043c8f7f7b51d58189760f96b10d967fc5 Mon Sep 17 00:00:00 2001 From: hariom123-dev Date: Sat, 5 Sep 2026 17:30:11 +0530 Subject: [PATCH 1/3] fix(sdk): add input sanitization for LangfuseObservationWrapper metadata --- langfuse/_client/span.py | 2 ++ tests/unit/test_span_sanitization.py | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 tests/unit/test_span_sanitization.py diff --git a/langfuse/_client/span.py b/langfuse/_client/span.py index 96879499c..629225422 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 k is not None and k != ""} self._otel_span = otel_span self._otel_span.set_attribute( LangfuseOtelSpanAttributes.OBSERVATION_TYPE, as_type diff --git a/tests/unit/test_span_sanitization.py b/tests/unit/test_span_sanitization.py new file mode 100644 index 000000000..0665feb67 --- /dev/null +++ b/tests/unit/test_span_sanitization.py @@ -0,0 +1,25 @@ +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 key and None key + bad_metadata = {"valid_key": "value", "": "bad_value", None: "another_bad"} + + 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) + From 7c5e067da51712fbadc079134fa6817677d40fad Mon Sep 17 00:00:00 2001 From: hariom123-dev Date: Sat, 5 Sep 2026 17:51:35 +0530 Subject: [PATCH 2/3] fix(sdk): address review comments by sanitizing values and patching update method --- langfuse/_client/span.py | 5 ++++- tests/unit/test_span_sanitization.py | 12 +++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/langfuse/_client/span.py b/langfuse/_client/span.py index 629225422..958141f1a 100644 --- a/langfuse/_client/span.py +++ b/langfuse/_client/span.py @@ -122,7 +122,7 @@ def __init__( prompt: Associated prompt template from Langfuse prompt management """ if isinstance(metadata, dict): - metadata = {k: v for k, v in metadata.items() if k is not None and k != ""} + 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 @@ -678,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 index 0665feb67..ef63c9b70 100644 --- a/tests/unit/test_span_sanitization.py +++ b/tests/unit/test_span_sanitization.py @@ -10,8 +10,8 @@ def test_langfuse_observation_wrapper_metadata_sanitization(mock_process): mock_client = MagicMock() - # Input with empty string key and None key - bad_metadata = {"valid_key": "value", "": "bad_value", None: "another_bad"} + # 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, @@ -22,4 +22,10 @@ def test_langfuse_observation_wrapper_metadata_sanitization(mock_process): # 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) From 2348d5c420532edc8fad610e40d96e7e3e9c09db Mon Sep 17 00:00:00 2001 From: hariom123-dev Date: Sat, 5 Sep 2026 18:05:30 +0530 Subject: [PATCH 3/3] style(tests): apply ruff formatting to span sanitization test --- tests/unit/test_span_sanitization.py | 30 ++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/tests/unit/test_span_sanitization.py b/tests/unit/test_span_sanitization.py index ef63c9b70..68308d5cb 100644 --- a/tests/unit/test_span_sanitization.py +++ b/tests/unit/test_span_sanitization.py @@ -1,31 +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 + 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) - + 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} - + 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) + mock_process.assert_any_call( + data={"another_valid": "value2"}, field="metadata", span=mock_otel_span + )