Skip to content

fix(leaderboard): match period rank tiebreakers across profile and embed surfaces - #1192

Merged
junhoyeo merged 1 commit into
mainfrom
fix/period-rank-tiebreakers
Aug 25, 2026
Merged

fix(leaderboard): match period rank tiebreakers across profile and embed surfaces#1192
junhoyeo merged 1 commit into
mainfrom
fix/period-rank-tiebreakers

Conversation

@junhoyeo

@junhoyeo junhoyeo commented Aug 24, 2026

Copy link
Copy Markdown
Owner

What

Three surfaces show a user's rank for a finite period — the leaderboard's week/month/custom tab, the public profile's 7d/30d view, and the embed card's week/month view — and until now they did not agree on what a tie means.

getLeaderboard.ts builds its rank expression from a sequentialRanks flag. The period path passes true and emits ROW_NUMBER() OVER (ORDER BY total_tokens DESC, total_cost DESC, LOWER(username) ASC, user_id ASC) (a cost sort swaps the two metrics), which gives every user a distinct position and gives pagination a stable order. The all-time path passes false and emits RANK() OVER (ORDER BY total_tokens DESC), so tied users share a position.

publicProfileData.ts and getUserEmbedStats.ts each rolled their own period rank with a bare RANK() OVER (ORDER BY total_tokens DESC). So two users on the same period total read #1 and #2 on the leaderboard's week tab, but #1 and #1 — followed by a gap — on their own profile pages and on their embed cards, from the same data over the same window at the same moment.

Change

Both finite-period rank queries now use the leaderboard's period expression and its tiebreak columns, matched to each surface's own ranking metric (the embed's cost sort orders total_cost DESC, total_tokens DESC, the tokens sort the other way around, and the profile ranks by tokens only).

The tiebreak needs username and user_id in scope, so each period CTE now also selects u.username (and, on the profile side, SUM(CAST(d.cost AS DECIMAL(18,4))) AS total_cost for the secondary term) and groups by s.user_id, u.username. users.id is the primary key the join runs through, so the added grouping column produces the same groups and the embed's SELECT COUNT(*)::int FROM rankable denominator is unchanged. The u.leaderboard_hidden = false filter and the date-range bounds are untouched — the same rows are ranked, and only the order within a tie moves. The Number(...) coercion of the returned bigint, the unstable_cache keys and tags, and the profile's empty-window skip all stay as they were.

The all-time paths deliberately stay on shared RANK(). They already agree with the leaderboard's all-time tab, and moving them to ROW_NUMBER would introduce exactly the same divergence in the other direction. Both files now carry a comment saying so, and the tests below pin it.

Tests

__tests__/api/usersProfile.test.ts and __tests__/lib/getUserEmbedStats.test.ts mock next/cache and drizzle-orm's sql tag, so they can assert on the SQL each path emits. Added there:

  • ranks the profile period window the way the leaderboard's period tab does — asserts the period rank emits the full ROW_NUMBER() OVER (ORDER BY total_tokens DESC, total_cost DESC, LOWER(username) ASC, user_id ASC) clause, that no RANK() OVER is emitted on that path, that the hidden-user filter and both date bounds are unchanged, and that the cache key still carries the anchored window.
  • ranks the finite embed window the way the leaderboard's period tab ranks $sortBy — the same check for the embed card, once per sort, pinning the metric order for each and the of N denominator.
  • keeps the lifetime profile rank on shared RANK and the renamed keeps the lifetime embed rank on shared RANK with no tie-breakers — assert the all-time paths still emit RANK() and emit no ROW_NUMBER at all, so the mirror-image bug fails the suite too.

Four existing assertions that matched the old period SQL shape were updated from RANK() OVER to ROW_NUMBER() OVER. The embed test's SQL serializer now recurses into nested sql fragments — a conditional ORDER BY is interpolated as a value, and the old serializer flattened it to [object Object], which hid the clause these tests are about.

Verified by falsification in both directions: reverting the source change fails 7 tests (the 3 new period tests plus the 4 updated ones) and leaves the two all-time tests passing; flipping the all-time queries to ROW_NUMBER instead fails exactly those two and nothing else.

Verification

From packages/frontend, on the branch head:

  • npx tsc --noEmit — exit 0
  • bun run test — exit 0, 925 passed | 6 skipped (931) across 87 passing / 1 skipped test files
  • bun run lint — exit 0, 17 pre-existing warnings, none in the changed files

No database integration test: the repo has no harness for one on this path, so these tests pin the emitted SQL rather than Postgres' ordering of a real tie.


Summary by cubic

Aligns finite-period ranks on profile and embed with the leaderboard’s period tab. Previously, profile/embed used shared RANK on period totals (ties showed the same position); now they use sequential ROW_NUMBER with the same tie-breakers as the leaderboard, so tied users read distinct positions consistently. All-time ranks remain on shared RANK.

  • Period queries now rank by ROW_NUMBER over tokens/cost plus tie-breakers (tokens sort: total_tokens DESC, total_cost DESC, LOWER(username) ASC, user_id ASC; cost sort swaps the first two). CTEs include u.username (and profile includes summed total_cost) and group by s.user_id, u.username. Filters, date bounds, cache keys/tags, and the embed “of N” denominator are unchanged; only tie ordering moves. Tests assert emitted SQL for each path and update the SQL serializer to recurse nested fragments.

Written for commit ddf66e6. Summary will update on new commits.

Review in cubic

…bed surfaces

The leaderboard ranks a finite period with a sequential ROW_NUMBER over
`total_tokens DESC, total_cost DESC, LOWER(username) ASC, user_id ASC`
(cost sort swaps the two metrics), and ranks all-time with a shared
`RANK() OVER (ORDER BY <metric> DESC)`.

The public profile's period rank and the embed card's period rank each
used a bare `RANK() OVER (ORDER BY total_tokens DESC)` instead, so two
users on the same period total read #1 and #2 on the leaderboard's week
or month tab but #1 and #1 on their own profile and embed card.

Move both finite-period queries onto the leaderboard's expression and
tiebreak columns. The CTEs now carry `u.username` and, on the profile
side, `SUM(CAST(d.cost AS DECIMAL(18,4)))` so the tiebreak has its
columns in scope; the `leaderboard_hidden = false` filter, the date
bounds, and the embed's `of N` denominator are unchanged, so the same
rows are ranked and only the order within a tie moves.

Both all-time queries stay on shared RANK, which is what the
leaderboard's all-time tab does — switching them would be the same
divergence mirrored.

Constraint: profile period rank must keep its unstable_cache key and the empty-window skip
Rejected: sequential ranks everywhere | all-time surfaces would then disagree with the leaderboard's all-time tab
Rejected: shared RANK everywhere | loses the leaderboard's deterministic pagination order
Confidence: high
Scope-risk: narrow
Directive: the finite and lifetime windows rank differently on purpose; each mirrors the leaderboard tab it sits beside
Not-tested: no database integration test — the regression tests assert the emitted SQL, not Postgres' ordering of a real tie
@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
tokscale Ready Ready Preview Aug 24, 2026 10:48pm

Request Review

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.

1 participant