feat(eap): semver sort key for sentry.release ORDER BY in EAP#8103
feat(eap): semver sort key for sentry.release ORDER BY in EAP#8103phacops wants to merge 27 commits into
Conversation
Fixes lexicographic ordering of release strings by wrapping `sentry.release` in a `tuple(arrayResize(arrayMap(...), 4), is_stable)` sort key when used in ORDER BY / GROUP BY in EAP trace item table queries. This ensures: - 1.2.9 sorts before 1.2.10 (numeric, not lexicographic) - 1.2.3-beta.1 sorts before 1.2.3 (prerelease before stable) - 1.2.3-beta.1 sorts after 1.2.2 (prerelease of newer > older stable) - 1.2 and 1.2.0 are equal (arrayResize pads to 4 components) - package@version strips the prefix before comparison Uses only ClickHouse functions available on Altinity 25.3/25.8 (no naturalSortKey which requires upstream 26.3+). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
Two fixes for the semver sort key implementation: 1. Replace f.tuple(..., alias=alias) with FunctionCall(alias, "tuple", ...) to fix mypy "Optional[str] not compatible with str" error on the alias argument. 2. Only apply semver_sort_key in the ORDER BY path, not GROUP BY. GROUP BY sentry.release must use the raw attribute expression so that the SELECT (also the raw string) is a function of the GROUP BY key and ClickHouse accepts the aggregation query. ORDER BY on a function of a GROUP BY key is valid in ClickHouse, so semver ordering still works. This also fixes the 500 error in Sentry's test_semver_build test where the spans events API auto-groups by release. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
…rison The flextime pagination filter was comparing the raw release string lexicographically, while ORDER BY now uses the semver tuple key. This caused rows to be skipped or repeated on follow-up pages when `sentry.release` was in the order clause (e.g. "1.2.3-beta.1" would be missed because it sorts after "1.2.3" lexicographically but before it semantically). Fix: move `SEMVER_SORT_ATTRIBUTES` to common.py so pagination.py can import it. In `get_filters`, wrap both the column reference and the stored literal value in `semver_sort_key(...)` for any semver attribute so the page-boundary `<` comparison uses the same ordering as ORDER BY. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
sentry.release is coalesced from multiple attribute columns (attributes_string_25['sentry.release'] + attributes_string_30['release']), which makes the expression Nullable(String). ClickHouse forbids Nullable(Array(...)), so splitByChar on the raw expression would throw: Code: 43 – Nested type Array(String) cannot be inside Nullable type Fix: apply ifNull(expr, '') at the start of semver_sort_key to convert Nullable(String) to String before any string→array operations. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
"1.2" and "1.2.0" normalise to the same sort key ([1,2,0,0], 1). ClickHouse may return them in either relative order within the tied pair, making the strict `asc == list(reversed(desc))` assertion non-deterministic. Collapse contiguous tied elements into a frozenset before comparing so the test is order-insensitive within ties while still verifying that every other element is in exact reverse order. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
Resolve conflict in resolver_trace_item_table.py: - Keep master's updated _groupby_order_by_expression docstring (TYPE_ARRAY note) plus the semver paragraph. - Keep master's inlined _convert_order_by structure and use_array_map_columns aggregation arg while preserving for_order_by=True so SEMVER_SORT_ATTRIBUTES still get the semver tuple sort key in ORDER BY. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
Master's ruff config enables flake8-bugbear; B905 requires an explicit strict= argument on zip(). column_names and column_values are built in lockstep so strict=True is correct (they always have equal length). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
Resolve conflict in test_endpoint_trace_item_table.py: both branches appended independent test code at end of file. Keep both the TestSemverSorting class and master's test_uniq_with_default_value_double_casts_to_float64. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
|
|
||
| _SEMVER_COMPONENT_COUNT = 4 # major.minor.patch.build | ||
|
|
||
| SEMVER_SORT_ATTRIBUTES: frozenset[str] = frozenset({"sentry.release"}) |
There was a problem hiding this comment.
Can we include sentry.sdk.version here as well?
There was a problem hiding this comment.
No, I just hardcoded for tests, I’m going to merge this instead and let you choose getsentry/sentry-protos#334
There was a problem hiding this comment.
Reverted sentry.sdk.version in b989fe9 — SEMVER_SORT_ATTRIBUTES is back to just sentry.release. Good to merge as-is; the broader attribute selection can come through sentry-protos#334.
Generated by Claude Code
SDK versions follow the same semantic-versioning format as releases, so add sentry.sdk.version to SEMVER_SORT_ATTRIBUTES. It is a TYPE_STRING attribute, so ORDER BY and flextime pagination pick up the semver tuple key automatically. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
Per maintainer direction on the PR: keep the semver-sort attribute list hardcoded to sentry.release only for now. Broader/configurable attribute selection will be handled separately via sentry-protos. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
Flextime pagination compares sentry.release with semver_sort_key under a strict `less` tuple predicate. Distinct strings sharing the same numeric key + stability flag (e.g. "1.2" and "1.2.0") compared equal, so rows tied with the page cursor but not on the previous page could be skipped on follow-up pages (Cursor Bugbot, Medium). Append the raw (non-null) release string as a third tuple element in semver_sort_key. This gives distinct release strings a deterministic total order and makes the page-boundary comparison exact. Both ORDER BY and pagination call semver_sort_key, so they stay consistent automatically. - test_common: assert the 3-element tuple shape (raw-string tiebreaker). - test_endpoint_trace_item_table: ties are now deterministic, so test_desc_is_reverse_of_asc reverts to exact-reverse and test_length_normalisation asserts "1.2" immediately precedes "1.2.0". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
Resolve conflict in test_endpoint_trace_item_table.py: contextual conflict around the end of the file. Keep both the TestSemverSorting class and test_uniq_with_default_value_double_casts_to_float64 (each defined once, no duplication). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
Master reverted the "Cast aggregation to Float64 in coalesce" change (#7908), which deleted this regression test and the FunctionCall import from the test module. The merge incorrectly kept the test, leaving it referencing an undefined FunctionCall (ruff F821 / mypy) and asserting reverted behavior. Drop it to match master. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
Resolve conflict in tests/web/rpc/test_common.py: both branches appended independent test classes at the end of the file. Keep both TestSemverSortKey and master's TestAnyAttributeFilterOption. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
Update sentry-protos to 0.39.0, which adds the OrderBy `sort` field (SORT_UNSPECIFIED/SORT_DEFAULT/SORT_NATURAL) to the attribute names and values requests (sentry-protos#334), and honor it in the attribute values endpoint. - Add natural_sort_key() to common.py: a general natural-order key that tokenizes into digit / non-digit runs and zero-pads digit runs so a lexicographic comparison matches natural order (e.g. "1.2.9" before "1.2.10", "item2" before "item10"). Built from primitives since naturalSortKey() is unavailable on Altinity 25.3/25.8. - trace_item_attribute_values: when order_by.sort == SORT_NATURAL, order the value column by natural_sort_key; unset/SORT_DEFAULT keeps the historical lexicographic order. count() remains the primary ordering. - Unit tests for natural_sort_key structure and integration tests contrasting natural vs lexicographic value ordering. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
The 3 version-valued items added to the autouse fixture defaulted to sentry.is_segment=true, inflating test_boolean_case's count from 8 to 11. Move them into the two natural-sort tests via a helper so they don't perturb count-based assertions in sibling tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
SORT_NATURAL now selects the appropriate sort key per attribute: release attributes (SEMVER_SORT_ATTRIBUTES, e.g. sentry.release) use the semver key (prerelease before stable, "pkg@" prefix stripped), while every other attribute uses the general natural-sort key (digit runs compared numerically). This gives both a general natural sort and a release-aware sort under the single SORT_NATURAL option the proto exposes today. A dedicated proto value (e.g. SORT_RELEASE) would let clients pick semver ordering explicitly for any attribute; that needs a sentry-protos change and can be wired here once it lands. Add an integration test asserting the semver ordering for sentry.release values (prerelease-before-stable, prefix stripped). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
gen_item_message stamps a default sentry.release on every fixture item, so that value shows up (with a higher count) alongside the releases the test writes. Assert only the relative semver order of the written releases rather than the full value list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
is_stable previously only flagged SemVer '-' prereleases, so PEP 440 dot-style dev builds like "24.7.0.dev0+<sha>" (Sentry's own sentry.release format) were classified stable and sorted after their GA release. Define stable as a plain dotted-numeric version instead, so both "1.2.3-beta.1" and "24.7.0.dev0+<sha>" sort before their matching stable release. Add a table integration test asserting the dev build sorts before GA. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 75edf1d. Configure here.
The is_stable expression changed from equals(position(...)) to match(version, ...) in 75edf1d; update the unit test's structural assertion accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
Update sentry-protos to 0.40.0, which adds the OrderBy `sort` field to the trace-item-table request as well as the attribute names/values requests. Make SORT_NATURAL the client-driven, semver-aware ordering everywhere and remove the hardcoded attribute list. - common: remove SEMVER_SORT_ATTRIBUTES and the general natural_sort_key; semver_sort_key is now the single implementation of SORT_NATURAL. - table resolver: apply semver_sort_key in ORDER BY when a string column's OrderBy.sort == SORT_NATURAL (no attribute gating); GROUP BY keeps the raw expression. - flextime pagination: mark SORT_NATURAL page-boundary columns and apply the semver key on both sides so pages stay consistent with ORDER BY. - attribute values + names endpoints: order by the semver key when SORT_NATURAL is requested; the names endpoint mirrors the ordering in its Python merge/re-sort of synthetic keys. - tests: drive semver via SORT_NATURAL, assert default (unset) stays lexicographic, and cover the names endpoint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
str.isdigit() is True for characters like "²" that int() cannot parse, which would crash the names endpoint under SORT_NATURAL. Require c.isascii() too, matching ClickHouse toUInt32OrZero (ASCII decimal only, else 0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
The re-sort key callback used a bare `dict` param (missing type args) and an `object` return, which mypy rejects for list.sort's key. Accept Mapping[str, Any] (covariant, fits both the synthetic dicts and Row) and return Any. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN

Summary
Fixes incorrect lexicographic ordering of
sentry.releasestrings when used inORDER BY(andGROUP BY) in EAP trace item table queries.Bugs fixed:
1.2.10was sorting before1.2.9(classic lex bug)1.2.3-beta.1was sorting after1.2.3(pre-release should come before stable)1.2and1.2.0were not equal (length mismatch)Approach: Wrap
sentry.releasein atuple(arrayResize(arrayMap(x -> toUInt32OrZero(x), splitByChar('.', release_part)), 4), is_stable)sort key when it appears inORDER BY/GROUP BY. Uses only ClickHouse functions available on Altinity 25.3/25.8 — nonaturalSortKey(requires upstream 26.3+) and noCOLLATE(fails pre-release ordering, needsDangerousRawSQL).Key correctness properties:
1.2.9before1.2.10(numeric component comparison) ✓1.2.3-beta.1before1.2.3(pre-release sorts before stable) ✓1.2.3-beta.1after1.2.2(pre-release of newer > older stable) ✓1.2==1.2.0(arrayResize pads to 4 components) ✓my-pkg@2.0.0strips the@-prefix before comparison ✓25.3.8.999before25.3.8.10041✓Changes
snuba/web/rpc/common/common.pysemver_sort_key()helper and_SEMVER_COMPONENT_COUNTconstantsnuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py_SEMVER_SORT_ATTRIBUTES; extend_groupby_order_by_expression()to wrapsentry.releaseinsemver_sort_key()tests/web/rpc/test_common.pysemver_sort_key()expression structuretests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table.pyTest plan
TestSemverSortKey— checks expression shape (no DB required)TestSemverSorting— writes spans with differentsentry.releasevalues, queries withORDER BY sentry.releaseASC/DESC and asserts:1.2.9before1.2.101.2.3-beta.1before1.2.31.2.3-beta.1after1.2.2my-pkg@2.0.0sorts as version 2.0.01.2and1.2.0are adjacent (equal sort key)🤖 Generated with Claude Code
https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN
Generated by Claude Code