bub-tapestore-otel exports 0 spans: ValidationError: ... Input should be a dictionary or an instance of TapeEntry when used against current bub git HEAD
Summary
bub-tapestore-otel silently exports zero spans when installed into an
environment that resolves bub to git HEAD (e.g. uv tool install bub --with bub-tapestore-otel, or any fresh resolution that isn't pinned to the workspace
lock). Every tape append throws a pydantic ValidationError, which the
exporter swallows via its try/except … warning("tapestore.otel.export_failed"),
so the only visible symptom is export_failed log lines and no telemetry.
Symptom (exact error)
pydantic_core._pydantic_core.ValidationError: 10 validation errors for StepTrace
entries.0
Input should be a dictionary or an instance of TapeEntry
[type=dataclass_type, input_value=TapeEntry(id=1, kind='anc...'), input_type=TapeEntry]
For further information visit https://errors.pydantic.dev/2.14/v/dataclass_type
entries.1
Input should be a dictionary or an instance of TapeEntry
[type=dataclass_type, input_value=TapeEntry(id=2, kind='eve...'), input_type=TapeEntry]
...
Note input_type=TapeEntry — the value is a TapeEntry, yet pydantic rejects
it. Raised from OTelTapeExporter._append → build_tape_trace → StepTrace(**…),
caught in OTelTapeStore.append and logged as tapestore.otel.export_failed.
Root cause (bisected)
It is a bub version drift, not pydantic and not republic. Controlled A/B,
identical everything except the pinned variable:
| bub |
republic |
pydantic |
result |
0.3.9.dev6+g10fa0150a (the commit uv.lock pins) |
0.5.8 |
2.13.4 |
✅ projects + exports |
0.3.9.dev6+g10fa0150a |
0.5.8 |
2.14.0a1 |
✅ projects + exports |
0.3.10.dev9+ga2b638865 (git HEAD) |
0.5.8 |
2.13.4 |
❌ export_failed, 10 validation errors |
0.3.10.dev9+ga2b638865 (git HEAD) |
0.5.8 |
2.14.0a1 |
❌ export_failed, 10 validation errors |
- The plugin's own unit tests pass under both pydantic 2.13.4 and 2.14.0a1
(they build TapeEntry via TapeEntry.system()/.message()/.event()).
republic is 0.5.8 in every case, and republic.TapeEntry is republic.tape.entries.TapeEntry → a single class object, not a duplicate-import
problem.
- Therefore the trigger is the runtime
TapeEntry instances produced by bub
git HEAD (note the new kind='anchor' entries). They fail an isinstance
check against the republic.TapeEntry captured in the plugin's pydantic schema,
even though their repr/input_type say TapeEntry.
Why fresh installs hit HEAD
bub-tapestore-otel's pyproject.toml declares dependencies = ["bub", …]
(unpinned) and is consumed from the bub-contrib uv workspace, which sources
bub from git. A consumer doing uv tool install bub>=0.3.0a1 --with git+…/bub-tapestore-otel resolves bub to git HEAD, not the workspace's
locked commit 10fa0150a. (--prerelease allow, needed for bub's 0.3.0a1
spec, additionally pulls pydantic 2.14.0a1, but that is harmless per the table
above.)
Minimal repro
# FAILS (bub HEAD)
uv run --no-project --prerelease allow \
--with "bub>=0.3.0a1" \
--with "git+https://github.com/bubbuild/bub-contrib.git#subdirectory=packages/bub-tapestore-otel" \
bub --workspace /tmp/ws run "read data.txt" --session-id x
# → tapestore.otel.export_failed ... ValidationError: ... instance of TapeEntry
# WORKS (pin bub to the tested commit)
uv run --no-project --prerelease allow \
--with "bub @ git+https://github.com/bubbuild/bub.git@10fa0150a1168abfe67449380f714ac9456b3b7b" \
--with "git+https://github.com/bubbuild/bub-contrib.git#subdirectory=packages/bub-tapestore-otel" \
bub --workspace /tmp/ws run "read data.txt" --session-id x
# → projects + attempts OTLP export, no export_failed
Suggested fixes (upstream)
- Make the projection robust to
TapeEntry identity/shape — entries (and
the TapeEntry-typed fields) are internal-only; they don't need pydantic
validation. Type them as SkipValidation[list[TapeEntry]] (pydantic) or
list[Any], so the plugin survives any bub/republic change to how
TapeEntry is constructed. This is the real durability fix.
- Constrain
bub in pyproject.toml to the tested range, and/or publish
bub-tapestore-otel to PyPI so consumers don't resolve bub to git HEAD.
- If
bub HEAD intentionally changed TapeEntry construction/identity, treat
that as a breaking change for plugins that receive TapeEntry instances.
Related
Same failure mode (a contrib plugin breaking against current bub main) as
#43 "bub-tapestore-sqlite breaks against latest bub main after _build_llm
removal" — different plugin/symptom, same root pattern of unpinned bub drift.
Consumer workaround (until fixed)
Pin bub to the plugin's tested commit when installing:
bub @ git+https://github.com/bubbuild/bub.git@10fa0150a1168abfe67449380f714ac9456b3b7b
bub-tapestore-otel exports 0 spans:
ValidationError: ... Input should be a dictionary or an instance of TapeEntrywhen used against currentbubgit HEADSummary
bub-tapestore-otelsilently exports zero spans when installed into anenvironment that resolves
bubto git HEAD (e.g.uv tool install bub --with bub-tapestore-otel, or any fresh resolution that isn't pinned to the workspacelock). Every tape
appendthrows a pydanticValidationError, which theexporter swallows via its
try/except … warning("tapestore.otel.export_failed"),so the only visible symptom is
export_failedlog lines and no telemetry.Symptom (exact error)
Note
input_type=TapeEntry— the value is aTapeEntry, yet pydantic rejectsit. Raised from
OTelTapeExporter._append→build_tape_trace→StepTrace(**…),caught in
OTelTapeStore.appendand logged astapestore.otel.export_failed.Root cause (bisected)
It is a
bubversion drift, not pydantic and not republic. Controlled A/B,identical everything except the pinned variable:
0.3.9.dev6+g10fa0150a(the commituv.lockpins)0.3.9.dev6+g10fa0150a0.3.10.dev9+ga2b638865(git HEAD)export_failed, 10 validation errors0.3.10.dev9+ga2b638865(git HEAD)export_failed, 10 validation errors(they build
TapeEntryviaTapeEntry.system()/.message()/.event()).republicis 0.5.8 in every case, andrepublic.TapeEntry is republic.tape.entries.TapeEntry→ a single class object, not a duplicate-importproblem.
TapeEntryinstances produced bybubgit HEAD (note the new
kind='anchor'entries). They fail anisinstancecheck against the
republic.TapeEntrycaptured in the plugin's pydantic schema,even though their
repr/input_typesayTapeEntry.Why fresh installs hit HEAD
bub-tapestore-otel'spyproject.tomldeclaresdependencies = ["bub", …](unpinned) and is consumed from the
bub-contribuv workspace, which sourcesbubfrom git. A consumer doinguv tool install bub>=0.3.0a1 --with git+…/bub-tapestore-otelresolvesbubto git HEAD, not the workspace'slocked commit
10fa0150a. (--prerelease allow, needed for bub's0.3.0a1spec, additionally pulls
pydantic 2.14.0a1, but that is harmless per the tableabove.)
Minimal repro
Suggested fixes (upstream)
TapeEntryidentity/shape —entries(andthe
TapeEntry-typed fields) are internal-only; they don't need pydanticvalidation. Type them as
SkipValidation[list[TapeEntry]](pydantic) orlist[Any], so the plugin survives anybub/republicchange to howTapeEntryis constructed. This is the real durability fix.bubinpyproject.tomlto the tested range, and/or publishbub-tapestore-otelto PyPI so consumers don't resolvebubto git HEAD.bubHEAD intentionally changedTapeEntryconstruction/identity, treatthat as a breaking change for plugins that receive
TapeEntryinstances.Related
Same failure mode (a contrib plugin breaking against current
bubmain) as#43 "bub-tapestore-sqlite breaks against latest bub main after _build_llm
removal" — different plugin/symptom, same root pattern of unpinned
bubdrift.Consumer workaround (until fixed)
Pin
bubto the plugin's tested commit when installing:bub @ git+https://github.com/bubbuild/bub.git@10fa0150a1168abfe67449380f714ac9456b3b7b