From 06dc7528b95ff1894573093560ef4217bc90750a Mon Sep 17 00:00:00 2001 From: Apoorv Saraogee <136734128+asaraog@users.noreply.github.com> Date: Fri, 25 Sep 2026 19:16:37 -0700 Subject: [PATCH 1/4] test: mask applies to dataset and score payloads --- tests/unit/test_mask_api_payloads.py | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/unit/test_mask_api_payloads.py diff --git a/tests/unit/test_mask_api_payloads.py b/tests/unit/test_mask_api_payloads.py new file mode 100644 index 000000000..6c689c8b8 --- /dev/null +++ b/tests/unit/test_mask_api_payloads.py @@ -0,0 +1,43 @@ +from unittest.mock import Mock + +from langfuse import Langfuse + + +def _client(): + client = Langfuse( + public_key="pk", + secret_key="sk", + host="https://mock-host.com", + tracing_enabled=False, + mask=lambda data: "masked", + ) + client.api = Mock() + client._resources.add_score_task = Mock() + return client + + +def test_create_dataset_item_masks_payload(): + client = _client() + client.create_dataset_item( + dataset_name="ds", input="secret", expected_output="secret", metadata="secret" + ) + kwargs = client.api.dataset_items.create.call_args.kwargs + assert (kwargs["input"], kwargs["expected_output"], kwargs["metadata"]) == ( + "masked", + "masked", + "masked", + ) + + +def test_create_dataset_masks_metadata(): + client = _client() + client.create_dataset(name="ds", metadata="secret") + assert client.api.datasets.create.call_args.kwargs["metadata"] == "masked" + + +def test_create_score_masks_comment(): + client = _client() + client.create_score(name="s", value=1, trace_id="t" * 32, comment="secret") + assert ( + client._resources.add_score_task.call_args.args[0]["body"].comment == "masked" + ) From 15d31ed18f62236ebdae0c5440bf26e518398628 Mon Sep 17 00:00:00 2001 From: Apoorv Saraogee <136734128+asaraog@users.noreply.github.com> Date: Fri, 25 Sep 2026 20:13:39 -0700 Subject: [PATCH 2/4] fix(client): apply mask to dataset and score payloads --- langfuse/_client/client.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/langfuse/_client/client.py b/langfuse/_client/client.py index af06e69ef..e7cb8e71e 100644 --- a/langfuse/_client/client.py +++ b/langfuse/_client/client.py @@ -1952,6 +1952,20 @@ def create_score( environment: Optional[str] = None, ) -> None: ... + def _apply_mask(self, data: Any) -> Any: + """Apply the configured mask to data sent outside a span, matching span masking.""" + if data is None or not self._mask: + return data + try: + return self._mask(data=data) + except Exception as e: + langfuse_logger.error( + "Masking error: Custom mask function threw exception when processing " + "data. Using fallback masking. Error: %s", + e, + ) + return "" + def create_score( self, *, @@ -2032,7 +2046,7 @@ def create_score( name=name, value=value, dataType=data_type, # type: ignore - comment=comment, + comment=self._apply_mask(comment), configId=config_id, environment=environment or self._environment, metadata=metadata, @@ -3553,7 +3567,7 @@ def create_dataset( result = self.api.datasets.create( name=name, description=description, - metadata=metadata, + metadata=self._apply_mask(metadata), input_schema=input_schema, expected_output_schema=expected_output_schema, ) @@ -3652,9 +3666,9 @@ def create_dataset_item( result = self.api.dataset_items.create( dataset_name=dataset_name, - input=input, - expected_output=expected_output, - metadata=metadata, + input=self._apply_mask(input), + expected_output=self._apply_mask(expected_output), + metadata=self._apply_mask(metadata), source_trace_id=source_trace_id, source_observation_id=source_observation_id, status=status, From 86fd1e8896a1a8d7099654ddc658424f4325e75a Mon Sep 17 00:00:00 2001 From: Apoorv Saraogee <136734128+asaraog@users.noreply.github.com> Date: Fri, 25 Sep 2026 20:18:20 -0700 Subject: [PATCH 3/4] test: enable tracing so create_score reaches the score queue --- tests/unit/test_mask_api_payloads.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/test_mask_api_payloads.py b/tests/unit/test_mask_api_payloads.py index 6c689c8b8..e73b0ea6a 100644 --- a/tests/unit/test_mask_api_payloads.py +++ b/tests/unit/test_mask_api_payloads.py @@ -37,6 +37,7 @@ def test_create_dataset_masks_metadata(): def test_create_score_masks_comment(): client = _client() + client._tracing_enabled = True # create_score is a no-op otherwise client.create_score(name="s", value=1, trace_id="t" * 32, comment="secret") assert ( client._resources.add_score_task.call_args.args[0]["body"].comment == "masked" From e9106d9a844ea2aabd66666781b295a2e2cc20d5 Mon Sep 17 00:00:00 2001 From: Apoorv Saraogee <136734128+asaraog@users.noreply.github.com> Date: Fri, 25 Sep 2026 21:23:26 -0700 Subject: [PATCH 4/4] fix(client): serialize non-string masked score comments --- langfuse/_client/client.py | 6 +++++- tests/unit/test_mask_api_payloads.py | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/langfuse/_client/client.py b/langfuse/_client/client.py index e7cb8e71e..580d0154a 100644 --- a/langfuse/_client/client.py +++ b/langfuse/_client/client.py @@ -4,6 +4,7 @@ """ import asyncio +import json import logging import os import re @@ -2035,6 +2036,9 @@ def create_score( return score_id = score_id or self._create_observation_id() + comment = self._apply_mask(comment) + if comment is not None and not isinstance(comment, str): + comment = json.dumps(comment) try: new_body = ScoreBody( @@ -2046,7 +2050,7 @@ def create_score( name=name, value=value, dataType=data_type, # type: ignore - comment=self._apply_mask(comment), + comment=comment, configId=config_id, environment=environment or self._environment, metadata=metadata, diff --git a/tests/unit/test_mask_api_payloads.py b/tests/unit/test_mask_api_payloads.py index e73b0ea6a..a0237c656 100644 --- a/tests/unit/test_mask_api_payloads.py +++ b/tests/unit/test_mask_api_payloads.py @@ -42,3 +42,12 @@ def test_create_score_masks_comment(): assert ( client._resources.add_score_task.call_args.args[0]["body"].comment == "masked" ) + + +def test_create_score_serializes_non_string_masked_comment(): + client = _client() + client._tracing_enabled = True + client._mask = lambda data: {"redacted": True} + client.create_score(name="s", value=1, trace_id="t" * 32, comment="secret") + body = client._resources.add_score_task.call_args.args[0]["body"] + assert body.comment == '{"redacted": true}'