Skip to content
27 changes: 20 additions & 7 deletions src/sentry/replays/usecases/query/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,11 @@ def _query_using_scalar_strategy(

try:
where = handle_search_filters(scalar_search_config, search_filters)
orderby = handle_ordering(agg_sort_config, sort or "-" + DEFAULT_SORT_FIELD)
orderby = handle_ordering(
agg_sort_config,
sort or "-" + DEFAULT_SORT_FIELD,
tiebreaker="replay_id", # Ensure stable sort when ordering by column with duplicates
)
except RetryAggregated:
return _query_using_aggregated_strategy(
search_filters,
Expand Down Expand Up @@ -378,7 +382,11 @@ def _query_using_aggregated_strategy(
period_start: datetime,
period_stop: datetime,
):
orderby = handle_ordering(agg_sort_config, sort or "-" + DEFAULT_SORT_FIELD)
orderby = handle_ordering(
agg_sort_config,
sort or "-" + DEFAULT_SORT_FIELD,
tiebreaker="replay_id", # Ensure stable sort when ordering by column with duplicates
)

having: list[Condition] = handle_search_filters(agg_search_config, search_filters)
having.append(Condition(Function("min", parameters=[Column("segment_id")]), Op.EQ, 0))
Expand Down Expand Up @@ -459,11 +467,16 @@ def execute_query(query: Query, tenant_id: dict[str, int], referrer: str) -> Map
raise


def handle_ordering(config: dict[str, Expression], sort: str) -> list[OrderBy]:
if sort.startswith("-"):
return [OrderBy(_get_sort_column(config, sort[1:]), Direction.DESC)]
else:
return [OrderBy(_get_sort_column(config, sort), Direction.ASC)]
def handle_ordering(
config: dict[str, Expression], sort: str, tiebreaker: str | None = None
) -> list[OrderBy]:
direction = Direction.DESC if sort.startswith("-") else Direction.ASC
bare_sort = sort[1:] if sort.startswith("-") else sort
Copy link
Member Author

Choose a reason for hiding this comment

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

could also use sort.lstrip("-")


orderby = [OrderBy(_get_sort_column(config, bare_sort), direction)]
if tiebreaker:
orderby.append(OrderBy(Column(tiebreaker), direction))
return orderby


def _get_sort_column(config: dict[str, Expression], column_name: str) -> Function:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,30 @@ def test_get_replays_duration_sorted(self) -> None:
assert response_data["data"][0]["id"] == replay2_id
assert response_data["data"][1]["id"] == replay1_id

def test_get_replays_duration_sorted_tiebreaker(self) -> None:
"""Test that replays with identical durations have deterministic order when ordering by duration."""
project = self.create_project(teams=[self.team])
replay_ids = [uuid.uuid4().hex for _ in range(5)]

timestamp_start = datetime.datetime.now() - datetime.timedelta(seconds=10)
timestamp_end = datetime.datetime.now() - datetime.timedelta(seconds=5)

for replay_id in replay_ids:
self.store_replays(mock_replay(timestamp_start, project.id, replay_id, segment_id=0))
self.store_replays(mock_replay(timestamp_end, project.id, replay_id, segment_id=1))

with self.feature(self.features):
response = self.client.get(self.url + "?orderBy=duration")
assert response.status_code == 200, response
asc_order = [r["id"] for r in response.json()["data"]]

response = self.client.get(self.url + "?orderBy=-duration")
assert response.status_code == 200, response
desc_order = [r["id"] for r in response.json()["data"]]

assert desc_order == list(reversed(asc_order))
assert set(asc_order) == set(replay_ids)

def test_get_replays_pagination(self) -> None:
"""Test replays can be paginated."""
project = self.create_project(teams=[self.team])
Expand Down
Loading