Skip to content

[AAI-236] Mask exported third party spans#96

Open
kxzk wants to merge 3 commits into
mainfrom
feature/aai-236-mask-exported-third-party-spans
Open

[AAI-236] Mask exported third party spans#96
kxzk wants to merge 3 commits into
mainfrom
feature/aai-236-mask-exported-third-party-spans

Conversation

@kxzk

@kxzk kxzk commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

TL;DR

Add 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.

Why

The existing Config#mask runs while the SDK creates Langfuse-owned input/output/metadata attributes; it never sees raw gen_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's mask_otel_spans) as a transforming exporter that wraps the Langfuse OTLP exporter.

  • Hook receives a frozen mapping of "<trace_id>:<span_id>" identifiers to immutable OtelSpanSnapshot values (Data objects: ids, name, scope name, frozen span/resource attributes) — never mutable SDK spans.
  • Returns nil (no changes) or sparse patches: { delete: [...], set: {...} }; deletes run before sets.
  • Fail-closed: hook exception or invalid result drops the batch; malformed patch drops that span; invalid replacement value omits the attribute rather than exporting the original. Attribute values are never logged.
  • Only the Langfuse export copy is transformed — patched spans are clones; other exporters receive the originals. should_export_span runs before masking, so rejected spans never reach the hook.
  • Config#mask is unchanged; the callable validators in Config/OtelSetup were collapsed into one validate_callable! helper.

Live-validated against Langfuse Cloud: a third-party gen_ai.prompt secret was replaced with [REDACTED-BY-HOOK] in the stored trace while a second (non-Langfuse) exporter received the original value.

Checklist

  • Has label
  • Has linked issue
  • Tests added for new behavior
  • Docs updated (if user-facing)

Closes 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.*) that mask never sees at observation creation time.

When set, OtelSetup wraps the Langfuse OTLP exporter in MaskingExporter, which passes the hook a frozen map of "trace_id:span_id"OtelSpanSnapshot and applies sparse delete / set patches 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_span still runs in SpanProcessor before 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 to mask.

Reviewed by Cursor Bugbot for commit 3c444c6. Bugbot is set up for automated code reviews on this repo. Configure here.

Copilot AI review requested due to automatic review settings July 13, 2026 14:37
@kxzk kxzk added the enhancement New feature or request label Jul 13, 2026
@linear-code

linear-code Bot commented Jul 13, 2026

Copy link
Copy Markdown

AAI-236

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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::MaskingExporter and Langfuse::OtelSpanSnapshot, wrapping the Langfuse OTLP exporter when mask_otel_spans is configured.
  • Extends Langfuse::Config with mask_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.

Comment thread lib/langfuse/masking_exporter.rb Outdated

apply_patches(batch, patches)
rescue StandardError => e
@logger.error("Langfuse mask_otel_spans raised #{e.class}: #{e.message}; dropping the Langfuse export batch")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cb77bfe — the rescue now logs only the exception class; a spec asserts the exception message never reaches the log.

Comment thread lib/langfuse/masking_exporter.rb Outdated
Comment on lines +131 to +136
def frozen_copy(value)
case value
when String, Array then value.dup.freeze
else value
end
end

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cb77bfefrozen_copy now copies array elements recursively, so snapshot arrays never alias mutable strings from the original span data. Spec added.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread lib/langfuse/masking_exporter.rb Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants