Skip to content

perf(dashboard): Read assistant usage fields once per event row - #2650

Merged
thomasjiangcy merged 1 commit into
mainfrom
fix/dashboard-model-usage-detoast
Nov 4, 2026
Merged

thomasjiangcy merged 1 commit into
mainfrom
fix/dashboard-model-usage-detoast

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Long conversations are slow to open in the dashboard. Each detail poll (every 2–10s) runs the model usage query, and in production it takes up most of the request: for one long web conversation, detail p50 was 814ms and p95 1.5s, and in the slowest trace the usage query ran for 2.8s of a 3.3s request.

The query reads payload->... about 100 times per assistant_message row. Postgres detoasts and decompresses the whole payload on each read, and that payload includes the full reply content. So the cost grows with the size of the conversation, not with the number of usage fields.

This change reads provider, model, and usage once per row with a lateral jsonb_to_record, and computes all aggregates from the small usage value. The output does not change.

Local benchmark on Postgres 17 with 1,500 assistant messages of about 27KB each: 415ms → 37ms. The same rows with tiny payloads ran in 27ms, so the remaining time is mostly the row scan.

via @BYK.

--

View Session in Sentry

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
@vercel

vercel Bot commented Nov 4, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
junior-docs Ready Ready Preview Nov 4, 2026 9:42am UTC

Request Review

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

4 screenshot changes — 4 changed · 0 added · 0 removed

Review screenshots in Frameshift

Conversation Attachment · Desktop
Conversation Attachment · Desktop
Changed
Conversation Share Details · Desktop
Conversation Share Details · Desktop
Changed
Conversation Subagents · Desktop
Conversation Subagents · Desktop
Changed
Settings Mcp · Desktop
Settings Mcp · Desktop
Changed

@thomasjiangcy
thomasjiangcy merged commit 38cde79 into main Nov 4, 2026
47 checks passed
thomasjiangcy pushed a commit that referenced this pull request Nov 4, 2026
…ls (#2655)

Open detail pages poll `GET /api/conversations/:id`. The old ETag
(#1954) hashed the response, so every poll still built the whole report
(p50 814ms, p95 1.5s in prod) only to return 304. This compares a cheap
version token first and skips the build when nothing changed. Follow-up
to #2650.

## Design

- New `junior_conversations.detail_version` column (migration 0075).
Writes to a table the report reads advance it:
- `juniorConversations` writes set `detailVersion: nextDetailVersion`
inline.
- Event, attachment, annotation, Brief, and archive writes use
`db.with(conversationDetailTouch(db, ids))`, a data-modifying CTE that
bumps in the same statement.
- Purge and attachment GC call `touchConversationDetail(db, ids)` after
the write, because their ids come from the write itself.
- Participant upserts do not bump. Each caller already bumps the
Conversation in the same transaction, and the root sums its subtree.
- Pin writes do not bump. A pin is a viewer mark, which the ETag reads
on each request.
- `readConversationDetailEtag` hashes the subtree `sum`/`count` of
`detail_version` with the state that is per viewer or owned by other
Conversations: access, viewer marks (archive and pin), read position,
forks, agent tree, `limit`, and `JUNIOR_VERSION`. Child agents bump only
their own row. A new child changes the count.
- The route returns 304 before `readConversationDetail` when
`If-None-Match` matches, else 200 with the ETag.
- The dashboard content-hash middleware now runs only for mock
reporting, which has no stored version.

## Lint rule

`junior/touch-conversation-detail` (oxlint JS plugin in
`tools/oxlint/junior`, on for `packages/junior/src`) reports:

- `insert`/`update`/`delete` on a detail table, or a raw `sql` write to
one, that is not built on `.with(conversationDetailTouch(...))` and has
no later `touchConversationDetail` in the same function.
- A `juniorConversations` update or upsert that does not set
`detailVersion`. Plain inserts pass, because the row count covers them.

RuleTester cases run in `pnpm oxlint-rules:check`, which is now part of
`pnpm lint`.

## Not covered by the version

Identity names/avatars, destination names, Automation titles/source
task, Slack team domains, and plugin sidebar hook output. These update
with the next detail change or a reload.

## Cost

The bump rides in the write statement, so it adds no round trip. It
costs one primary-key update of the Conversation row in Postgres.
`detail_version` has no index, so Postgres can usually update the row in
place without touching indexes. Event appends already hold the event
lock.

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:local%3Aweb%3A17b6d7f555623680001f2a90 -->

--

[View Session in
Sentry](https://sentry.sentry.io/explore/conversations/local%3Aweb%3A17b6d7f555623680001f2a90/?project=4510944073809921)

<!-- junior-session-footer:end -->

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: BYK <126780+BYK@users.noreply.github.com>
Co-authored-by: BYK <burak.kaya@sentry.io>

This branch was successfully deployed

1 active deployment
Preview – junior-docs — 53d91168 Deployed Nov 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: high PR risk score: high

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant