Skip to content

fix(tracing): serialize non-string propagated metadata as JSON - #1945

Closed
niklassemmler wants to merge 3 commits into
mainfrom
fix/propagated-metadata-json-coercion
Closed

niklassemmler wants to merge 3 commits into
mainfrom
fix/propagated-metadata-json-coercion

Conversation

@niklassemmler

@niklassemmler niklassemmler commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Sibling of #1944 (LFE-17153). #1944 moves observation metadata to one JSON attribute and leaves propagated trace metadata per-key. This PR fixes how the per-key trace metadata values are encoded.

Problem

propagate_attributes(metadata=...) coerces non-string values with str() (propagation.py). The server stores per-key trace metadata (langfuse.trace.metadata.<key>) as the raw string without parsing it, so Python reprs end up in storage:

Value Stored before Stored after
["branch:agent"] ['branch:agent'] ["branch:agent"]
("root", "agent") ('root', 'agent') ["root","agent"]
{"model": "gpt-4"} {'model': 'gpt-4'} {"model":"gpt-4"}
True True true
datetime(2024, 1, 1, tzinfo=utc) 2024-01-01 00:00:00+00:00 "2024-01-01T00:00:00Z"
None None (skipped)
1, 0.5, "text" 1, 0.5, text unchanged

This hits LangChain/LangGraph users in particular: the callback handler forwards run metadata such as langgraph_triggers and langgraph_path to propagate_attributes.

Change

  • Non-string values are serialized as compact JSON with EventSerializer (same serializer and separators as observation metadata in feat(tracing)!: send observation metadata as one JSON attribute #1944). Strings are kept as they are.
  • None values are skipped instead of being stored as "None", matching how observation metadata treats None.
  • The 200 character limit still applies, now to the serialized value.

Behavior change to call out

Stored values change for non-string trace metadata (see table). Saved filters on the old repr values (e.g. enabled = True, langgraph_triggers = ['branch:agent']) won't match new data. Strings and numbers, the common case, are unchanged. This ships as fix, but it could ride along with the #1944 major if reviewers prefer to group stored-value changes.

Parity with langfuse-js

JS propagateAttributes takes Record<string, string> and drops non-string values with a warning. Python keeps coercing on purpose: test_non_string_metadata_values_coerced pins coercion for LangGraph keys. This PR doesn't change that decision, it only makes the coerced value valid JSON. Whether both SDKs should coerce to JSON or both drop non-strings is an open question for the metadata workstream.

Verification

  • uv run --frozen ruff check . ✅
  • uv run --frozen ruff format ✅
  • uv run --frozen mypy langfuse --no-error-summary ✅
  • Wrote the tests first and confirmed they failed on current code (Expected ... ["branch:agent"], got ['branch:agent'], and None stored as an attribute).
  • uv run --frozen pytest -n auto --dist worksteal tests/unit --deselect tests/unit/test_prompt.py: 690 passed ✅
  • tests/unit/test_prompt.py: 18 errors, same on main (Langfuse client is not initialized, a local env issue).
  • tests/e2e: not run, no Langfuse server available.

🤖 Generated with Claude Code

RetriggerConfidence Score: 5/5

The PR appears safe to merge with its explicitly acknowledged stored-value changes.

What we checked:

  • Bad metadata stops tracing: EventSerializer catches encoding errors and returns a fallback string. Its depth limit also bounds circular collections.
  • New import prevents startup: propagation.py already imports attributes.py, which imports EventSerializer. The new direct import does not add a module to that path.
Summary

Changes propagate_attributes to serialize non-string metadata as compact JSON, leave strings unchanged, and skip None.

  • Keeps the 200-character limit after serialization.
  • Adds tests for common value types, skipped values, and oversized JSON.
  • No actionable issues found.
  • Author niklassemmler explicitly acknowledges that saved filters using old Python representations will not match newly encoded values. This stored-value change is intentional.

Reviews (1) · Last reviewed commit: "fix(tracing): serialize non-string propa..." · Reviewed by Greptile

propagate_attributes coerced non-string trace metadata values with str(), so
lists, tuples and dicts were stored as Python reprs (`['branch:agent']`,
`('root', 'agent')`) that are not valid JSON, booleans became `True`, and
None became the string `None`.

Serialize non-string values as compact JSON instead, and skip None values.
Strings are unchanged. The 200 character limit applies to the serialized
value.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

@claude review

@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

No review was started: this request came from a bot account. Manual reviews can only be requested by someone with write access to this repository. Ask a maintainer to comment @claude review, or have your automation post the comment from a user account with write access.

Tip: disable this comment in your organization's Code Review settings.

Comment thread langfuse/_client/propagation.py Outdated
json.dumps escapes non-ASCII characters as \uXXXX by default, so a list or
dict value with non-ASCII text counted up to 6 characters per character
against the 200 character limit and was dropped. Serialize with
ensure_ascii=False, which also matches JSON.stringify output in the JS SDK.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@niklassemmler

Copy link
Copy Markdown
Contributor Author

@claude review

🤖 Written by Claude (an AI agent) on behalf of Niklas

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

Code review found no issues

No high-confidence issues detected in this change.

@niklassemmler

Copy link
Copy Markdown
Contributor Author

Closing in favor of #1932, which JSON-serializes propagated metadata values on prepare-v5-release (it also keeps exact integer digits and drops NaN/Infinity). Changing how stored metadata values look is a breaking change, so it ships with v5 rather than in a v4 minor release.

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