From 444d71fe24870fa5652f64aa79b77d0c4f8fa500 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 16 Oct 2026 07:13:05 +0000 Subject: [PATCH 1/2] docs(masking): explain JSON-encoded metadata values in mask_otel_spans Co-authored-by: hassieb.pakzad --- langfuse/_client/client.py | 2 ++ langfuse/types.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/langfuse/_client/client.py b/langfuse/_client/client.py index c474f0640..d2feee535 100644 --- a/langfuse/_client/client.py +++ b/langfuse/_client/client.py @@ -220,6 +220,8 @@ class Langfuse: Return `None` to leave the batch unchanged. Return `MaskOtelSpansResult` with `OtelSpanPatch` values to delete or replace attributes on selected spans. If a batch contains duplicate trace and span identifiers, Langfuse keeps only the last matching span. If the hook raises or returns an invalid batch result, Langfuse drops the whole export batch. If one returned span patch is invalid, Langfuse drops only that span from the Langfuse export. + Values of `langfuse.trace.metadata.*` and `langfuse.observation.metadata.*` attributes are JSON-encoded strings on every exported span, including third-party instrumentation spans such as OpenInference (`acme` arrives as `'"acme"'`). Decode them with `json.loads` before applying masking rules and encode replacements with `json.dumps`, for example `OtelSpanPatch(set_attributes={key: json.dumps("[REDACTED]")})`. See `MaskOtelSpansFunction` for a full example. + Example: ```python from typing import Optional diff --git a/langfuse/types.py b/langfuse/types.py index d20bd6a55..63ef92274 100644 --- a/langfuse/types.py +++ b/langfuse/types.py @@ -277,6 +277,38 @@ def mask_otel_spans( langfuse = Langfuse(mask_otel_spans=mask_otel_spans) ``` + + Values of `langfuse.trace.metadata.*` and `langfuse.observation.metadata.*` + attributes are JSON-encoded strings on every span the SDK exports, + including spans from third-party instrumentations such as OpenInference, + because propagated metadata is copied onto them. A string value `acme` arrives as + `'"acme"'` and the number `3` as `'3'`. Decode each value with `json.loads` + before applying masking rules, and encode replacements with `json.dumps`: + + ```python + import json + + METADATA_PREFIXES = ( + "langfuse.trace.metadata.", + "langfuse.observation.metadata.", + ) + + def mask_otel_spans( + *, params: MaskOtelSpansParams + ) -> Optional[MaskOtelSpansResult]: + patches = {} + + for identifier, span in params.spans.items(): + updates = {} + for key, value in span.attributes.items(): + if key.startswith(METADATA_PREFIXES) and isinstance(value, str): + if json.loads(value) == "acme": + updates[key] = json.dumps("[REDACTED]") + if updates: + patches[identifier] = OtelSpanPatch(set_attributes=updates) + + return MaskOtelSpansResult(span_patches=patches) + ``` """ def __call__( From 1d3cd284c71c20ea15f27d10fdbf835c5879d622 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 16 Oct 2026 07:17:55 +0000 Subject: [PATCH 2/2] docs(masking): guard metadata decode in mask_otel_spans example Co-authored-by: hassieb.pakzad --- langfuse/_client/client.py | 2 +- langfuse/types.py | 26 ++++++++++++++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/langfuse/_client/client.py b/langfuse/_client/client.py index d2feee535..35ea6f1f8 100644 --- a/langfuse/_client/client.py +++ b/langfuse/_client/client.py @@ -220,7 +220,7 @@ class Langfuse: Return `None` to leave the batch unchanged. Return `MaskOtelSpansResult` with `OtelSpanPatch` values to delete or replace attributes on selected spans. If a batch contains duplicate trace and span identifiers, Langfuse keeps only the last matching span. If the hook raises or returns an invalid batch result, Langfuse drops the whole export batch. If one returned span patch is invalid, Langfuse drops only that span from the Langfuse export. - Values of `langfuse.trace.metadata.*` and `langfuse.observation.metadata.*` attributes are JSON-encoded strings on every exported span, including third-party instrumentation spans such as OpenInference (`acme` arrives as `'"acme"'`). Decode them with `json.loads` before applying masking rules and encode replacements with `json.dumps`, for example `OtelSpanPatch(set_attributes={key: json.dumps("[REDACTED]")})`. See `MaskOtelSpansFunction` for a full example. + Values of `langfuse.trace.metadata.*` and `langfuse.observation.metadata.*` attributes written by the SDK are JSON-encoded strings, including on third-party instrumentation spans such as OpenInference (`acme` arrives as `'"acme"'`). Decode them with `json.loads` before applying masking rules and encode replacements with `json.dumps`, for example `OtelSpanPatch(set_attributes={key: json.dumps("[REDACTED]")})`. Values from other sources, such as baggage from an older upstream service, can be raw strings, so catch `json.JSONDecodeError` rather than letting the hook raise. See `MaskOtelSpansFunction` for a full example. Example: ```python diff --git a/langfuse/types.py b/langfuse/types.py index 63ef92274..991b1e0a3 100644 --- a/langfuse/types.py +++ b/langfuse/types.py @@ -279,11 +279,14 @@ def mask_otel_spans( ``` Values of `langfuse.trace.metadata.*` and `langfuse.observation.metadata.*` - attributes are JSON-encoded strings on every span the SDK exports, - including spans from third-party instrumentations such as OpenInference, - because propagated metadata is copied onto them. A string value `acme` arrives as - `'"acme"'` and the number `3` as `'3'`. Decode each value with `json.loads` - before applying masking rules, and encode replacements with `json.dumps`: + attributes written by the SDK are JSON-encoded strings. This includes spans + from third-party instrumentations such as OpenInference, because propagated + metadata is copied onto them. A string value `acme` arrives as `'"acme"'` + and the number `3` as `'3'`. Decode each value with `json.loads` before + applying masking rules, and encode replacements with `json.dumps`. Values + that reach a span another way, such as baggage from an older or + non-Langfuse upstream service, can be raw strings. Guard the decode, + because an exception from the hook drops the whole export batch: ```python import json @@ -301,9 +304,16 @@ def mask_otel_spans( for identifier, span in params.spans.items(): updates = {} for key, value in span.attributes.items(): - if key.startswith(METADATA_PREFIXES) and isinstance(value, str): - if json.loads(value) == "acme": - updates[key] = json.dumps("[REDACTED]") + if not key.startswith(METADATA_PREFIXES): + continue + if not isinstance(value, str): + continue + try: + decoded = json.loads(value) + except json.JSONDecodeError: + decoded = value + if decoded == "acme": + updates[key] = json.dumps("[REDACTED]") if updates: patches[identifier] = OtelSpanPatch(set_attributes=updates)