Skip to content

fix(otel): apply mask function to object metadata attributes#835

Open
i-anubhav-anand wants to merge 2 commits into
langfuse:mainfrom
i-anubhav-anand:fix/mask-object-metadata
Open

fix(otel): apply mask function to object metadata attributes#835
i-anubhav-anand wants to merge 2 commits into
langfuse:mainfrom
i-anubhav-anand:fix/mask-object-metadata

Conversation

@i-anubhav-anand

@i-anubhav-anand i-anubhav-anand commented Jun 14, 2026

Copy link
Copy Markdown

A configured mask function is never applied to object metadata, so secrets in metadata are exported verbatim.

applyMaskInPlace only masks exact-key attributes:

for (const maskCandidate of maskCandidates) {
  if (maskCandidate in span.attributes) {   // exact key only
    span.attributes[maskCandidate] = await this.applyMask(span.attributes[maskCandidate]);
  }
}

But object metadata is flattened into ${candidate}.${key} attributes (e.g. langfuse.observation.metadata.apiKey), which the bare-key in check never matches. So:

startObservation("s", { metadata: { apiKey: "secret-123" } });
// exported: langfuse.observation.metadata.apiKey = "secret-123"  // unmasked, leaks

input/output mask correctly (they use the bare key); only the uncommon primitive-metadata path was masked.

Fix scans attribute keys by prefix — mirroring MediaService.process, which walks the same attribute list immediately after masking using startsWith. Now flattened metadata keys are masked too.

Test

Adds an integration test masking object metadata ({ apiKey: "secret-123" }"***-123"). Fails before (exported as "secret-123"), passes after. All existing mask tests still pass; prettier/eslint/tsc clean (pre-commit).

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Greptile Summary

This PR fixes a bug where a configured mask function was never applied to object-valued metadata: because metadata objects are flattened into ${candidate}.${key} span attributes (e.g., langfuse.observation.metadata.apiKey), the previous exact-key in check in applyMaskInPlace never matched them, so the values leaked verbatim. The fix replaces the exact-key lookup with a prefix scan — identical to the existing pattern in MediaService.process.

  • span-processor.ts: Replaces if (maskCandidate in span.attributes) with an Object.keys prefix filter, masking both the bare key (primitive metadata) and all flattened child keys (object metadata).
  • Integration test: Adds a test that starts an observation with { apiKey: "secret-123" } metadata, runs the mask function, and asserts each flattened attribute receives the transformed value.

Confidence Score: 4/5

Safe to merge — the change is a two-line fix confined to a single private method, the logic is correct, and the new integration test validates the full round-trip.

The fix is correct and well-tested, but the startsWith prefix match without a dot separator is a loose pattern inherited from MediaService — a future attribute whose name starts with the same string would be silently masked. No current attribute conflicts exist, so this is a robustness concern rather than a present defect.

No files require special attention; the single-method change in span-processor.ts is straightforward and self-contained.

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
packages/otel/src/span-processor.ts:526-528
The prefix check `startsWith(maskCandidate)` does not require a separator, so it would also match an attribute whose name happens to begin with the same string by coincidence — for example, a future `langfuse.observation.metadata_schema` key would be silently masked. The same gap exists in `MediaService.ts` where this pattern was adopted from. A precise check of either an exact match or a dot-continuation is safer and future-proof.

```suggestion
      const maskRelevantAttributeKeys = Object.keys(span.attributes).filter(
        (attributeName) =>
          attributeName === maskCandidate ||
          attributeName.startsWith(maskCandidate + "."),
      );
```

Reviews (1): Last reviewed commit: "fix(otel): apply mask function to object..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

applyMaskInPlace only masked exact-key attributes (`maskCandidate in
span.attributes`). Object metadata is flattened into `${candidate}.${key}`
attributes, so object metadata was never masked — e.g. metadata
{ apiKey: "secret-123" } was exported verbatim despite a configured mask,
leaking secrets. (input/output mask fine because they use the bare key;
masking only worked for the uncommon primitive-metadata case.)

Scan attribute keys by prefix (mirroring MediaService.process, which walks
the same attribute list right after masking) so flattened metadata keys are
masked too. Adds an integration test (fails before: apiKey exported as
"secret-123").

@claude claude 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.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@vercel

vercel Bot commented Jun 14, 2026

Copy link
Copy Markdown

@i-anubhav-anand is attempting to deploy a commit to the langfuse Team on Vercel.

A member of the Team first needs to authorize it.

Comment on lines +526 to +528
const maskRelevantAttributeKeys = Object.keys(span.attributes).filter(
(attributeName) => attributeName.startsWith(maskCandidate),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The prefix check startsWith(maskCandidate) does not require a separator, so it would also match an attribute whose name happens to begin with the same string by coincidence — for example, a future langfuse.observation.metadata_schema key would be silently masked. The same gap exists in MediaService.ts where this pattern was adopted from. A precise check of either an exact match or a dot-continuation is safer and future-proof.

Suggested change
const maskRelevantAttributeKeys = Object.keys(span.attributes).filter(
(attributeName) => attributeName.startsWith(maskCandidate),
);
const maskRelevantAttributeKeys = Object.keys(span.attributes).filter(
(attributeName) =>
attributeName === maskCandidate ||
attributeName.startsWith(maskCandidate + "."),
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/otel/src/span-processor.ts
Line: 526-528

Comment:
The prefix check `startsWith(maskCandidate)` does not require a separator, so it would also match an attribute whose name happens to begin with the same string by coincidence — for example, a future `langfuse.observation.metadata_schema` key would be silently masked. The same gap exists in `MediaService.ts` where this pattern was adopted from. A precise check of either an exact match or a dot-continuation is safer and future-proof.

```suggestion
      const maskRelevantAttributeKeys = Object.keys(span.attributes).filter(
        (attributeName) =>
          attributeName === maskCandidate ||
          attributeName.startsWith(maskCandidate + "."),
      );
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Adopted — switched to an exact-or-dot-prefix match (name === candidate || name.startsWith(candidate + ".")) so coincidental keys like ${candidate}_schema aren't masked. Thanks!

Address review feedback: startsWith(maskCandidate) could match a coincidental
key like `${candidate}_schema`. Match the candidate itself or its dot-prefixed
children instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant