Skip to content

[OTel] _flatten_nested_dict bypasses the shared decoder, so nested-format traces can still produce unhashable tags #207

Description

@Leroyyyyyyyyy

Description

Split out of the #187 review, as agreed there.

OtlpJsonLoader._extract_attributes has two branches, and only one goes through the shared AnyValue decoder:

def _extract_attributes(self, attrs) -> dict:
    if isinstance(attrs, dict):
        return self._flatten_nested_dict(attrs)   # no decoding, no allowlist
    return decode_attributes(attrs)               # decoder + SPEC_CONTAINER_ATTRS

The dict branch handles the flat and nested (ClickHouse JSON column) formats.
It recurses into dicts and passes everything else through untouched:

for key, value in d.items():
    full_key = f"{prefix}{key}" if not prefix else f"{prefix}.{key}"
    if isinstance(value, dict):
        result.update(OtlpJsonLoader._flatten_nested_dict(value, full_key))
    else:
        result[full_key] = value       # a list lands in span.tags as a list

So the guarantee #187 establishes on the OTLP-array path — containers only for allowlisted keys — does not hold here. A list reaches span.tags, and gcp.vertex.agent.invocation_id is then used as a dict key in eight places in incremental_processor.py (llm_spans_by_invocation, token_totals, seen_tool_calls).

This predates #187 — the dict branch never had any narrowing — so it is not a regression from that PR, but it is the same hazard on a path the PR does not cover.

Expected behavior

Both branches of _extract_attributes should end up with the same contract: container values only for keys on the allowlist, narrowed otherwise. A nested-format trace should not be able to put an unhashable value where the OTLP-array format cannot.

Steps to reproduce

Load a nested-format (ClickHouse JSON column) trace whose attributes carry a list where a scalar is expected:

{"gcp": {"vertex": {"agent": {"invocation_id": ["a", "b"]}}}}

_flatten_nested_dict flattens it to {"gcp.vertex.agent.invocation_id": ["a", "b"]}, which reaches self.seen_tool_calls[invocation_id] in incremental_processor.py and raises TypeError: unhashable type: 'list'.

How are you using agentevals?

CLI file evaluation (agentevals run)

Config dump (ZIP from the web UI)

No response

Version information

agentevals version: 0.9.9.dev10+gc65f82d19.d20260828
Python version: 3.14.6
OS: macOS 26.4 (arm64)

Eval config (if applicable)

Trace and eval set files

No response

Relevant logs or error output


Additional context

Likely fix: apply the same allowlist rule in _flatten_nested_dict's leaf case, via a shared helper rather than a second copy of the rule.

I can take this one if it's useful — same area as #187.

Human confirmation

  • I am a human (not a bot, agent, or AI) filing this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions