Repository navigation
fix(tracing): do not mask attributes the caller never set - #1973
Open
Neumanncheng wants to merge 1 commit into
Open
Neumanncheng wants to merge 1 commit into
Neumanncheng wants to merge 1 commit into
Conversation
`_process_media_and_apply_mask` runs for input, output and metadata on
every span creation and update, regardless of whether the caller provided
them. An unset attribute is still handed to the user's mask function as
`None`. A mask that assumes a dict throws, `_mask_attribute` then fails
closed, and the fallback string is not `None` -- so it survives the
`v is not None` filters in `create_span_attributes` /
`create_generation_attributes`, and lands on the span as an attribute the
caller never set. Each unset attribute also logs one ERROR per span,
which makes it look like the user's mask function is broken.
Repro with a mask that assumes a dict and only `output` set:
mask receives [None, None, None, None, {'answer': '42'}, None]
span carries langfuse.observation.input / .metadata =
'<fully masked due to failed mask function>'
plus 5 ERROR logs ("'NoneType' object is not a mapping")
Return early when `data is None`: there is nothing to process or mask.
Attributes that do have a value keep the existing fail-closed behaviour
and the per-key metadata fallback.
Adds two regression tests asserting that only attributes the caller set
reach the mask, and that unset attributes stay unset.
chrikrah
approved these changes
Oct 10, 2026
chrikrah
left a comment
There was a problem hiding this comment.
@Neumanncheng approving: the early return covers every mask call site, since _process_media_and_apply_mask is the only caller of _mask_attribute, and both new tests fail without it.
$ pytest tests/unit/test_otel.py -q # head ab7789d, Python 3.14
69 passed, 2 skipped
$ pytest tests/unit/test_otel.py -q # same, `if data is None` block removed
FAILED ...::test_unset_attributes_are_never_passed_to_the_mask
FAILED ...::test_unset_attributes_stay_unset_when_created_with_metadata_only
2 failed, 67 passed, 2 skipped
$ ruff check langfuse/_client/span.py tests/unit/test_otel.py # ruff 0.15.7
All checks passed!
The fix also helps masks that never raise, where the bug is silent. A redacting mask and a single update(output=...):
$ python probe.py # base edf44eb, mask=lambda *, data, **kw: "[REDACTED]"
langfuse.observation.input = [REDACTED]
langfuse.observation.metadata = [REDACTED]
langfuse.observation.output = [REDACTED]
$ python probe.py # head ab7789d
langfuse.observation.output = [REDACTED]
@hassiebp this looks ready to me. Is there anything you want before merging?
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
_process_media_and_apply_maskis called forinput,outputandmetadataon every span creation and update, regardless of whether the caller provided them. When an attribute is not set,dataisNone— and it is still handed to the user'smaskfunction.A mask function that assumes a dict (a natural, common shape) raises on
None._mask_attributethen fails closed, and the fallback string is notNone, so it survives thev is not Nonefilters increate_span_attributes/create_generation_attributesand_set_span_attributes_within_limit. Result:langfuse.observation.input,langfuse.observation.metadata— turn up on the span with the value<fully masked due to failed mask function>ERRORlog per unset attribute per span, which reads as "your mask function is broken"Repro
Observed with a recording mask:
Without a mask configured, neither
inputnormetadataappears on the span at all — so the phantom attributes come from the mask call, not from another path.Expected:
maskis only invoked for attributes the caller actually set; unset attributes stay unset.Fix
Return early in
_process_media_and_apply_maskwhendata is None— there is nothing to process or mask.This keeps unset attributes
None, so the existingv is not Nonefilters drop them as intended. Attributes that do have a value keep the current fail-closed behaviour and the per-key metadata fallback from #1956 unchanged.Tests
Two regression tests added to
tests/unit/test_otel.py::TestMetadataHandling, next to the existing masking tests:test_unset_attributes_are_never_passed_to_the_masktest_unset_attributes_stay_unset_when_created_with_metadata_onlyThey assert that the mask sees exactly the values the caller set (
[{"answer": "42"}], noNone), that unset attributes do not appear on the span, and that set attributes are still masked.Without the fix both fail with
'NoneType' object is not a mapping; with it both pass. Fulltests/unit/test_otel.py: 69 passed, 2 skipped.ruff check,ruff format --checkandmypy langfuseall pass.
Confidence Score: 5/5
The PR appears safe to merge; omitted attributes stay absent while supplied values still get masked.Summary
_process_media_and_apply_masknow returns early forNone, so omitted attributes do not reach the user's mask or acquire fallback values.Reviews (1) · Last reviewed commit: "fix(tracing): do not mask attributes the..." · Reviewed by Greptile