feat(traces): OTLP span encoding and client-side validity - #950
Conversation
posthog-python Compliance ReportDate: 2026-09-17 03:42:53 UTC ✅ All Tests Passed!111/111 tests passed Capture_V1 Tests✅ 94/94 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
|
Prompt To Fix All With AI### Issue 1
posthog/tracing/_sanitize.py:81
**Float timestamps can overflow**
A finite float such as `1e308` becomes infinite when multiplied by `1e9`, so `round()` or `int()` raises instead of returning `None`. This crashes span creation rather than falling back to the derived time. Validate the scaled value before converting it.
```suggestion
scaled = value * 1e9
if not math.isfinite(scaled):
return None
ns = int(round(scaled))
```
### Issue 2
posthog/tracing/_otlp.py:192
**Mapping traversal is unbounded**
`list(attributes.keys())` traverses and retains every key before the item and node limits are checked. A large mapping can consume excessive memory, while a custom non-terminating key iterator can hang encoding. Iterate over keys incrementally so encoding stops when its budget is exhausted.
```suggestion
for key in attributes.keys():
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "feat(traces): OTLP span encoding and cli..." | Re-trigger Greptile |
599993b to
3a26165
Compare
3a26165 to
efccf67
Compare
Prompt To Fix All With AI### Issue 1
posthog/test/tracing/test_otlp.py:373-478
**Large inline expected payload**
This test embeds the complete nested OTLP payload as a large inline expected value. That violates the repository directive to keep extensive test results in external snapshot files. Move the payload to an external snapshot; this requirement must be satisfied before merging.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (2): Last reviewed commit: "fix(traces): reject a float time whose n..." | Re-trigger Greptile |
dustinbyrne
left a comment
There was a problem hiding this comment.
Reviewed 284701863a1bca407428638086ea5ff23568df53 via source inspection and existing CI. No issues found in this staged OTLP encoding/validity slice. Approval covers this slice; #949 precedes it in the stack.
jzhu13
left a comment
There was a problem hiding this comment.
Reviewed against traces/01-ids-traceparent. Tests pass at the head, ruff and mypy are clean, and the OTLP/JSON shape checks out (casing, hex ids, decimal-string nanos, intValue as string, NaN/Infinity literals, flags bits, enum ints). No blocking issues. Approving with non-blocking notes; the first two are worth doing before this reaches users.
Non-blocking, recommended
posthog/tracing/_otlp.py:199when the 10000-node budget is exhausted, every remaining span attribute is dropped with only a debug log: no[Truncated]marker and nothing feedsdroppedAttributesCount. Arrays do get a marker, so the two container types report truncation differently. Reproduced: one large nested first attribute erasedposthogDistinctId. Suggest a marker or a returned dropped count so #955's accounting sees it.posthog/tracing/_otlp.py:162Decimal,UUID, dataclasses, andbytesfall tostr(), whileposthog.utils.cleanmakes them numeric or structured for events.span.set_attribute("order.total", Decimal("1.50"))is a string attribute; the same value on an event is numeric. Consider routing leaves through the same primitives. #955's_limits._truncatestringify fallback must change in step.posthog/metrics_capture.py:89already has an OTLPAnyValueencoder with different int rules and no cycle handling. Two encoders in one package will drift; worth sharing one.posthog/tracing/_otlp.py:335host_resource_attributesdisagrees withget_os_info: events sendMac OS Xand the macOS version, spans sendmacOSand the Darwin kernel release. Reuse_get_platform_os_info, or document why traces follow posthog-node rather than this SDK's events.- Nits: the module docstring says any input yields an acceptable payload, but ids and timestamps pass through unvalidated (#956 is what actually guarantees them);
to_resource_key_value_listre-encodes the fixed resource dict on every flush.
Reviewed with Claude Code (Claude Fable 5.1). Behaviors above were reproduced by probe against this branch head.
2847018 to
e4b74a7
Compare
|
Thanks. Done in e4b74a7: item 4 (constant renamed) and the docstring nit.
|
|
Follow-up, a6b6c95:
|
Adds the OTLP JSON encoder for spans: AnyValue encoding per the traces spec (int64 as strings, out-of-range ints and non-finite floats as strings, None values and empty keys dropped, unpaired surrogates replaced with U+FFFD, a bounded walk that terminates on cycles), the span record builder with the W3C flags byte and OTel remoteness bits, the one-resource/one-scope envelope with service.name always present, and the name and timestamp sanitizers. One bad value otherwise gets the whole batch rejected. Not reachable from the client. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TkZAsCciW4PV8ZdcCHmAbA
A finite float like 1e308 becomes infinite once scaled to nanoseconds, so round() raised instead of the value falling back to the derived time.
Keeps the ingestion-shape fixture in posthog/test/snapshots alongside the other server payload snapshots instead of a 100-line inline dict.
TRACE_FLAGS_SAMPLED was a hex string in _traceparent and an int here. The module docstring now says which inputs the encoder trusts.
… resource once When the node budget ran out, every remaining attribute vanished with a debug log and no droppedAttributesCount, so an oversized first value silently erased later keys. The cut is now counted on the span and on each event. The resource is encoded once by the caller instead of on every batch.
a6b6c95 to
aeab32a
Compare
💡 Motivation and Context
Adds the OTLP JSON encoder for spans. AnyValue encoding follows the traces spec: int64 as strings, out-of-range ints and non-finite floats as strings,
Nonevalues and empty keys dropped, unpaired surrogates replaced with U+FFFD, and a bounded walk that terminates on cycles. Also adds the span record builder (W3C flags byte and OTel remoteness bits), the one-resource/one-scope envelope withservice.namealways present, and the name and timestamp sanitizers.Why it matters: one bad value otherwise gets the whole batch rejected by ingestion. Not reachable from the client yet.
Stack (PR 2 of 9, based on
traces/01-ids-traceparent):traces/01-ids-traceparenttraces/02-otlp-encoding← this PRtraces/03-span-handlestraces/04-transporttraces/05-pipelinetraces/06-exporttraces/07-span-limitstraces/08-before-span-sendtraces/09-client-wiring💚 How did you test it?
Unit tests in
posthog/test/tracing/test_otlp.pyandtest_sanitize.pycover each encoding rule, the envelope shape and the sanitizers.📝 Checklist
If releasing new changes
sampo addto generate a changeset file🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Implemented with Claude Code (Claude Opus 5) against the traces spec, one commit per slice so each PR reviews on its own. Rebased onto main and opened as a stacked draft in a later Claude Code session (Claude Fable 5.1).
🤖 Generated with Claude Code
https://claude.ai/code/session_012o7CtHLfcypjmXL7g9ZGRC