Skip to content

explore (errors dataset): duplicate rows and a non-reproducible total while hasMore is false #1519

Closed

Description

@Windsland52

Summary

Multi-dimension sentry explore --dataset errors returns duplicate rows, and the sum of count() over the returned rows does not reconcile with the scalar count() for the same filter. Repeating the identical command produces a different set of duplicates and a different sum each time, while every response reports hasMore: false.

Environment

sentry 0.40.0
node v24.18.0
Windows 10.0.26200

Steps to reproduce

# ground truth for the filter
sentry api "/organizations/<org>/events/?field=count()&query=release%3A%22<release>%22\
&statsPeriod=14d&project=<project-id>&dataset=errors" --json
# -> count() = 7283

# same filter, grouped by two tags — run this three times
sentry explore <org>/<project> --dataset errors --period 14d \
  --query 'release:"<release>"' \
  --field 'task.name' --field 'failure.node' --field 'count()' \
  --limit 1000 --fresh --json

Actual

Three consecutive identical invocations (ground truth for the filter: 7283 events):

run rows returned distinct task.name|failure.node keys sum of count() hasMore
1 351 264 7721 false
2 351 280 6809 false
3 351 267 7006 false

So the same query yields 71–87 duplicated rows, and a total that lands anywhere from ~7% below to ~6% above the true total. Row count is stable at 351 while the content varies, which looks like a page-merge / cursor overlap rather than a server-side sampling difference.

Adding more --field dimensions makes the divergence larger (a 5-field variant of the same query summed to 6101 against a true total of 7052).

Verified with the shim bypassed (node .../sentry/dist/bin.cjs explore ...), so it is not a shell/argument artifact.

Expected

  • No duplicate rows for the same dimension tuple.
  • The sum over returned rows reconciles with the scalar count() for the same filter, or hasMore: true indicates the result is partial.
  • Repeated identical queries return the same aggregate.

Impact

Aggregates are the primary way to answer "what is broken in this release". Because the duplicates and gaps are nondeterministic, they fabricate findings that look exactly like real signal:

  • rows present in release B and absent in release A read as "new in this release" — I hit six such false positives in one investigation, all disproved by re-querying each node individually;
  • the same tuple appearing twice inflates a task or node that is actually flat.

There is no in-band way to detect this: hasMore says the result is complete.

Workaround

Deduplicate client-side, reconcile the sum against a scalar count(), and re-query every row before citing it — which defeats the purpose of the aggregate.

Activity

  1. Windsland52 commented on Sep 1, 2026

    @Windsland52
    Author

    Related: getsentry/toolkit#1477 — even with correct, reconciling aggregates, the errors dataset cannot be ordered through explore, so ranking still requires pulling the full result set. The two together are what make release triage on errors unreliable.

  2. added
    jaredTrigger the Jared agent to work on stuff
    on Sep 1, 2026
  3. self-assigned this
    on Sep 1, 2026
  4. BYK commented on Sep 1, 2026

    @BYK
    Member

    Jared, make sure this is not an API-side issue first

  5. jared-outpost commented on Sep 1, 2026

    @jared-outpost
    Contributor

    traced this end to end — the nondeterministic duplicates/gaps are API-side, and the CLI turns them into a result that looks complete. quick writeup so we can decide where the fix belongs.

    mechanism

    for sentry explore --dataset errors --limit 1000 with group-by fields:

    1. limit (1000) > API_MAX_PER_PAGE (100), so queryEvents auto-paginates — it follows the cursor from each Link header and concatenates data across pages until a page returns results="false".
    2. on the errors dataset the CLI sends no sort. resolveDatasetConfig only forwards a sort for spans; everything else is sort: undefined (the test "omits sort for non-spans dataset" pins this). it even computes -count() and then throws it away.
    3. the /organizations/{org}/events/ cursor for grouped aggregates is offset-based (cursor="0:100:0" = value:offset:is_prev). offset paging is only correct if the server returns the grouped rows in a stable total order across the separate page requests. with no ORDER BY, the group ordering isn't guaranteed stable between independent executions, so page 2's offset overlaps and skips vs page 1 — which is exactly the reported signature: 264–280 distinct of 351 rows, a sum drifting either side of the scalar count(), different content each run, row count stable.
    4. hasMore is taken from the last page's Link only. the terminal page is results="false", so the CLI reports hasMore: false even though the merged aggregate is internally inconsistent.

    so: the instability is generated server-side (unstable ordering of grouped-events cursor pagination); the CLI amplifies it by silently merging pages and asserting completeness.

    the fix hinges on one API question

    the clean CLI fix is to send a deterministic sort (e.g. -count() / first aggregate) on errors/discover so the cursor is stable and the whole bug disappears. but the CLI currently forbids sort on non-spans datasets because /events/ returned 400 for it in the past — see the matching comments in api/dashboards.ts and commands/explore.ts and the existing test. i can't hit a live org from the sandbox to confirm.

    @BYK — can you confirm whether /organizations/{org}/events/?dataset=errors&field=count()&sort=-count() is accepted (no 400) on a current SaaS org?

    • if yes → i'll thread a stable sort through for errors/discover and pagination becomes deterministic (plus fix hasMore to reflect the auto-paginated tail). fully CLI-side.
    • if no → the CLI genuinely can't reliably paginate grouped aggregates and the durable fix is in sentry (stable grouped-events pagination). the CLI change then shrinks to not lying: stop silently merging + reporting hasMore: false for multi-page grouped aggregate queries (cap to one page / surface hasMore honestly / dedupe by tuple with a reliability warning).

    lean is (A) if the endpoint takes the sort. let me know and i'll ship it.

  6. Windsland52 commented on Sep 2, 2026

    @Windsland52
    Author

    Answering @BYK's question with a live-org probe (SaaS, dataset=errors grouped aggregate, fixed filter, 14d window, per_page=12, field=failure.node + field=count()):

    request result
    sort=-count() (run 1) ReturnMain=1828, (empty)=1763, TargetCountStartReplay=584, controller_initialization_failed=508, HomeFlag=357, ... — strictly descending
    sort=-count() (run 2) identical
    sort=count() (run 1) SwitchLastAccountComplete=1, SOSBuyItems=1, LimboStarsMainFlag=1, CollectMailAwards=1, ... — strictly ascending
    sort=count() (run 2) identical
    no sort (run 1) (empty)=1763, AwardsMainFlag=68, Sub_MailBadge=2, AutoSelectLeast=2, TranceEnd=2, ... — a third, non-sorted order
    no sort (run 2) same as no-sort run 1 (stable within the same short window, but clearly not sorted)

    The endpoint accepts sort on grouped errors aggregates — no 400 — and honors it: -count() and count() each produce a strictly monotone order that repeats identically, i.e. the stable total order the offset-based cursor needs for correct pagination. That is the precondition for your path A.

    Both probes above ran against SaaS via sentry api (CLI 0.40.0), so the API-side behavior this issue was waiting on is confirmed from a real org.

  7. Windsland52 commented on Sep 2, 2026

    @Windsland52
    Author

    Reproduced against a fresh install of sentry 0.44.1 (npm latest as of today), same SaaS org, same filter, --limit 1000, group-by task.name + failure.node:

    run rows distinct sum(count()) hasMore
    1 370 274 8657 false
    2 370 370 8595 false
    3 370 370 8595 false
    4 370 274 8533 false
    5 370 302 8620 false

    Scalar count() for the same filter: 7283. So the nondeterministic duplicates/gaps are all present on 0.44.1, and --sort '-count()' still emits the same Ignoring sort for errors warning.

    Separately, the API block was probed with sentry api (CLI 0.40.0): /organizations/{org}/events/?dataset=errors&field=failure.node&field=count()&sort=-count()&... is accepted (no 400) and deterministic — two identical descending results; sort=count() gives the ascending mirror; no-sort gives a third, unsorted order. If useful, happy to post the full table.

  8. jared-outpost commented on Sep 2, 2026

    @jared-outpost
    Contributor

    thanks for the live-org probe — that confirmed path A. opened #1520.

    the fix threads a deterministic sort (-<firstAggregate>, e.g. -count()) through for the errors/discover datasets instead of dropping it. with a stable total order the offset-based cursor no longer overlaps/skips between pages, so the merged multi-page aggregate is deterministic and hasMore reflects reality. metrics/logs still 400 on sort, so they stay unsorted, and the Ignoring sort warning now only fires when you explicitly pass --sort on one of those.

    draft PR while CI runs: #1520

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

jaredTrigger the Jared agent to work on stuff

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions