[AAI-236] Mask exported third party spans#96
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds an export-stage OpenTelemetry masking boundary to the langfuse-rb SDK via a new config.mask_otel_spans hook, allowing attribute deletion/replacement on all spans exported to Langfuse (including third-party instrumentations) with fail-closed behavior.
Changes:
- Introduces
Langfuse::MaskingExporterandLangfuse::OtelSpanSnapshot, wrapping the Langfuse OTLP exporter whenmask_otel_spansis configured. - Extends
Langfuse::Configwithmask_otel_spans, updates validation, and consolidates callable validation logic. - Adds specs and documentation describing the new masking boundary and its contract.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/langfuse/otel_setup_spec.rb | Adds coverage for exporter wrapping and end-to-end export-stage masking behavior. |
| spec/langfuse/masking_exporter_spec.rb | New unit specs for snapshotting, patch application, and fail-closed behavior. |
| spec/langfuse/config_spec.rb | Adds config validation coverage for mask_otel_spans. |
| lib/langfuse/otel_setup.rb | Wraps the OTLP exporter with MaskingExporter when configured; adds callable validation helper. |
| lib/langfuse/masking_exporter.rb | Implements export-stage masking with immutable snapshots and sparse patching. |
| lib/langfuse/config.rb | Adds mask_otel_spans, documents behavior, and validates callables via a shared helper. |
| lib/langfuse.rb | Requires the new masking exporter implementation. |
| docs/TRACING.md | Documents how creation-time mask relates to export-stage mask_otel_spans. |
| docs/CONFIGURATION.md | Documents the mask_otel_spans contract and validation rules. |
| docs/API_REFERENCE.md | Lists mask_otel_spans in the configuration API reference. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| apply_patches(batch, patches) | ||
| rescue StandardError => e | ||
| @logger.error("Langfuse mask_otel_spans raised #{e.class}: #{e.message}; dropping the Langfuse export batch") |
There was a problem hiding this comment.
Fixed in cb77bfe — the rescue now logs only the exception class; a spec asserts the exception message never reaches the log.
| def frozen_copy(value) | ||
| case value | ||
| when String, Array then value.dup.freeze | ||
| else value | ||
| end | ||
| end |
There was a problem hiding this comment.
Fixed in cb77bfe — frozen_copy now copies array elements recursively, so snapshot arrays never alias mutable strings from the original span data. Spec added.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6e942b1. Configure here.

TL;DRAdd
config.mask_otel_spans, an export-stage OpenTelemetry masking hook that can delete or replace attributes on every span the SDK exports to Langfuse — including spans created by third-party instrumentations — with fail-closed error handling.WhyThe existing
Config#maskruns while the SDK creates Langfuse-owned input/output/metadata attributes; it never sees rawgen_ai.*attributes set by third-party instrumentations, so sensitive payloads in those spans reached Langfuse unmasked. This adds the second masking boundary (matching the Python SDK'smask_otel_spans) as a transforming exporter that wraps the Langfuse OTLP exporter."<trace_id>:<span_id>"identifiers to immutableOtelSpanSnapshotvalues (Data objects: ids, name, scope name, frozen span/resource attributes) — never mutable SDK spans.nil(no changes) or sparse patches:{ delete: [...], set: {...} }; deletes run before sets.should_export_spanruns before masking, so rejected spans never reach the hook.Config#maskis unchanged; the callable validators inConfig/OtelSetupwere collapsed into onevalidate_callable!helper.Live-validated against Langfuse Cloud: a third-party
gen_ai.promptsecret was replaced with[REDACTED-BY-HOOK]in the stored trace while a second (non-Langfuse) exporter received the original value.ChecklistCloses AAI-236
Note
Medium Risk
Changes the telemetry export path and fail-closed drop behavior can remove whole Langfuse batches on hook errors, but scope is limited to optional masking around the Langfuse OTLP exporter.
Overview
Adds
config.mask_otel_spans, an export-stage hook that can redact or rewrite OpenTelemetry span attributes on the copy sent to Langfuse—including third-party instrumentations (e.g.gen_ai.*) thatmasknever sees at observation creation time.When set,
OtelSetupwraps the Langfuse OTLP exporter inMaskingExporter, which passes the hook a frozen map of"trace_id:span_id"→OtelSpanSnapshotand applies sparsedelete/setpatches on cloned spans so other exporters still get unmodified data. Errors are fail-closed (drop batch, span, or attribute rather than leak originals); logging avoids sensitive values.should_export_spanstill runs inSpanProcessorbefore batches reach the hook.Callable validation is consolidated via
validate_callable!in config and otel setup. User-facing docs cover the new option and how it relates tomask.Reviewed by Cursor Bugbot for commit 3c444c6. Bugbot is set up for automated code reviews on this repo. Configure here.