Skip to content

feat(eap): semver sort key for sentry.release ORDER BY in EAP#8103

Open
phacops wants to merge 27 commits into
masterfrom
claude/semver-sorting-eap-clickhouse-lpg3gs
Open

feat(eap): semver sort key for sentry.release ORDER BY in EAP#8103
phacops wants to merge 27 commits into
masterfrom
claude/semver-sorting-eap-clickhouse-lpg3gs

Conversation

@phacops

@phacops phacops commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes incorrect lexicographic ordering of sentry.release strings when used in ORDER BY (and GROUP BY) in EAP trace item table queries.

Bugs fixed:

  • 1.2.10 was sorting before 1.2.9 (classic lex bug)
  • 1.2.3-beta.1 was sorting after 1.2.3 (pre-release should come before stable)
  • 1.2 and 1.2.0 were not equal (length mismatch)

Approach: Wrap sentry.release in a tuple(arrayResize(arrayMap(x -> toUInt32OrZero(x), splitByChar('.', release_part)), 4), is_stable) sort key when it appears in ORDER BY / GROUP BY. Uses only ClickHouse functions available on Altinity 25.3/25.8 — no naturalSortKey (requires upstream 26.3+) and no COLLATE (fails pre-release ordering, needs DangerousRawSQL).

Key correctness properties:

  • 1.2.9 before 1.2.10 (numeric component comparison) ✓
  • 1.2.3-beta.1 before 1.2.3 (pre-release sorts before stable) ✓
  • 1.2.3-beta.1 after 1.2.2 (pre-release of newer > older stable) ✓
  • 1.2 == 1.2.0 (arrayResize pads to 4 components) ✓
  • my-pkg@2.0.0 strips the @-prefix before comparison ✓
  • 4-component versions like 25.3.8.999 before 25.3.8.10041

Changes

File Change
snuba/web/rpc/common/common.py Add semver_sort_key() helper and _SEMVER_COMPONENT_COUNT constant
snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py Add _SEMVER_SORT_ATTRIBUTES; extend _groupby_order_by_expression() to wrap sentry.release in semver_sort_key()
tests/web/rpc/test_common.py Unit tests for semver_sort_key() expression structure
tests/web/rpc/v1/test_endpoint_trace_item_table/test_endpoint_trace_item_table.py Integration tests verifying sort order with ClickHouse

Test plan

  • Unit tests: TestSemverSortKey — checks expression shape (no DB required)
  • Integration tests: TestSemverSorting — writes spans with different sentry.release values, queries with ORDER BY sentry.release ASC/DESC and asserts:
    • 1.2.9 before 1.2.10
    • 1.2.3-beta.1 before 1.2.3
    • 1.2.3-beta.1 after 1.2.2
    • my-pkg@2.0.0 sorts as version 2.0.0
    • 1.2 and 1.2.0 are adjacent (equal sort key)
    • DESC is exact reverse of ASC

🤖 Generated with Claude Code

https://claude.ai/code/session_013TCwndq43WTDRf6jsERQHN


Generated by Claude Code

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
@phacops phacops requested review from a team as code owners June 25, 2026 03:07
Comment thread snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py Outdated
Comment thread snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py Outdated
claude and others added 2 commits June 25, 2026 03:23
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
Comment thread snuba/web/rpc/v1/resolvers/R_eap_items/resolver_trace_item_table.py Outdated
claude added 3 commits June 25, 2026 03:32
…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
@phacops phacops marked this pull request as draft June 25, 2026 23:18
claude and others added 4 commits June 26, 2026 20:46
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
@phacops phacops marked this pull request as ready for review June 28, 2026 21:10
Comment thread snuba/web/rpc/common/common.py Outdated

_SEMVER_COMPONENT_COUNT = 4 # major.minor.patch.build

SEMVER_SORT_ATTRIBUTES: frozenset[str] = frozenset({"sentry.release"})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we include sentry.sdk.version here as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I just hardcoded for tests, I’m going to merge this instead and let you choose getsentry/sentry-protos#334

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted sentry.sdk.version in b989fe9SEMVER_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

claude and others added 3 commits June 29, 2026 13:48
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
Comment thread snuba/web/rpc/common/pagination.py
claude added 4 commits June 29, 2026 15:53
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
@phacops phacops requested a review from alexjillard July 3, 2026 03:38
claude added 3 commits July 3, 2026 03:46
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
Comment thread snuba/web/rpc/common/common.py
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

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

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

Comment thread tests/web/rpc/test_common.py Outdated
claude added 2 commits July 3, 2026 17:35
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
Comment thread snuba/web/rpc/v1/endpoint_trace_item_attribute_names.py
claude and others added 3 commits July 3, 2026 18:21
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
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.

3 participants