Skip to content

perf(dashboard): Keep the detail version when the reader marks a Conversation read - #2720

Merged
BYK merged 4 commits into
mainfrom
perf/detail-etag-skip-read-position
Nov 5, 2026
Merged

BYK merged 4 commits into
mainfrom
perf/detail-etag-skip-read-position

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Opening a conversation built the whole detail report twice. readConversationDetailEtag, added in #2655, hashed the viewer's read position, and the transcript sends PUT /read-position soon after it opens and again as the reader scrolls. Each write changed the version token. So the next poll got a 200 with the full report, not a 304. That new token also made conversationDetailQueryOptions refresh agents, memories, watches and Review items.

A prod browser capture of one conversation open showed it: pending-messages → detail (200, 77.8 kB, 1.41s) → read-position → pending-messages → detail (200 again, 77.9 kB, 1.44s) → memories and spend again.

Change:

  • The read position is no longer part of the detail ETag. The report still shows it: since perf(api): Open conversation detail at the last 3 Turns in one pass #2722 the ETag and the report share one detail state read. useMarkConversationRead already puts the stored position into the cached detail, so the reader's own client stays correct.
  • Polls now answer 304 after a read, so the cache keeps the unreadCursor from the last full build. useMarkConversationRead now drops unreadCursor and unreadPage when the new position reaches the latest page. It uses the same rule as the server (lastReadSeq + 1 < firstSeq). Without this, a second visit in the same session would open at rows the reader already read. The current visit is not affected, because useReadPositionAtOpen keeps the values from when the conversation opened.
  • A poll that started before a read and finishes after it no longer drops the unread cursor. keepLatestReadPosition keeps the cached cursor for the newer position, because no later poll would bring it back.
  • The read-position integration scenario now checks that a poll after a read returns 304.

Trade-off: another tab or device does not see the new read position until the detail changes or the page reloads. A new page load has no cache, so it always gets the current position. If the reader is still behind the latest page after a write, the cached cursor stays at the older position until the next full build. A reopen in that tab then loads from slightly earlier rows.

Not checked: the dashboard Playwright read-position spec, because Chrome is not installed in my sandbox. CI runs it. Mock reporting uses a content-hash ETag, so the e2e path still rebuilds after a read either way. tests/dashboard-output.test.ts also fails on main in my sandbox.

Overlaps: #2716 changes the first detail read (mailbox in the same request) in queries.ts and the detail route, but not the ETag. #2657 (unread catch-up) touches read-position e2e and the unread flow. Whichever merges second may need a small rebase.

--

View Session [Sentry]

…ersation read

Co-Authored-By: Thomas Jiang <20501235+thomasjiangcy@users.noreply.github.com>
@vercel

vercel Bot commented Nov 5, 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 5, 2026 11:13am UTC

Request Review

@sentry-junior
sentry-junior Bot requested a review from BYK November 5, 2026 07:28
@github-actions github-actions Bot added the risk: medium PR risk score: medium label Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

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

Review screenshots in Frameshift

Code · Desktop
Code · Desktop
Changed
Conversation Attachment · Desktop
Conversation Attachment · Desktop
Changed
Conversation Attachment · Mobile
Conversation Attachment · Mobile
Changed
Conversation Attachment Modal · Desktop
Conversation Attachment Modal · Desktop
Changed
Conversation Memory List Jump · Desktop
Conversation Memory List Jump · Desktop
Changed
Conversation Memory List Jump · Mobile
Conversation Memory List Jump · Mobile
Changed
Show 3 more changed screenshots
Conversation Share Details · Desktop
Conversation Share Details · Desktop
Changed
Conversation Subagents · Desktop
Conversation Subagents · Desktop
Changed
Settings Mcp · Desktop
Settings Mcp · Desktop
Changed

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

Stale Bugbot comment from a previous run.

BYK pushed a commit that referenced this pull request Nov 5, 2026
…#2731)

The dashboard entry (`client-[hash].js`) is 2.85 MB raw. An esbuild
metafile showed about 400 KB of it comes from code the browser never
runs.

| entry | raw | gzip-9 | br-11 |
|---|---|---|---|
| main | 2,847,273 | 814,316 | 661,472 |
| this PR | 2,443,134 | 739,683 | 605,140 |
| change | −404 KB (−14%) | −75 KB | −56 KB |

The saving is bigger raw than compressed because locale tables compress
well. The browser still has to parse and compile less JS, which matters
most on phones.

## Changes

- **zod locales: 426 KB → 103 KB.** `import { z } from "zod"` reads the
`export * as z` namespace object. esbuild can't tree-shake that, so
every zod locale (he, ru, ta, …, 248 KB) and the JSON-schema code stayed
in the bundle. `import * as z from "zod"` gives the same API (zod 4 docs
use it) and shakes correctly. On a one-line test bundle, the named
import gives 433 KB and the namespace import 81 KB. This is a repo-wide
codemod (325 files, import lines only), because one leftover named
import anywhere in the browser graph brings everything back.
- New oxlint rule `junior/zod-namespace-import` (in
`tools/oxlint/junior`) keeps it that way. It is on in the junior config,
which the plugin packages and `junior-plugin-api` share, and in the
dashboard config. Root `pnpm lint` now also lints `junior-plugin-api`,
because its code ships in the bundle. Type-only imports are allowed. The
built-in `no-restricted-imports` can't do this: with `importNames:
["z"]` it also rejects `import * as z`.
- **drizzle-orm: 34 KB → 0.** `@sentry/junior/api/schema` imported
`AGENT_INVOCATION_STATUSES` and `juniorDestinationKindSchema` from the
Drizzle table modules. Those modules call `pgTable(...)` at the top
level, so the tables and drizzle-orm came along. The values now live in
`chat/agent-invocations/statuses.ts` and
`db/schema/destination-kind.ts`, and the table modules import them.
`db:generate` reports no schema changes.
- **One copy of the plugin API: −17 KB.** With
`injectWorkspacePackages`, `@sentry/junior` has its own injected copy of
`@sentry/junior-plugin-api`, so the browser loaded both. The client
build now aliases the package to one resolved path.

## Not in this PR

Splitting routes comes next, as a separate PR with screenshots. The
entry still eagerly includes recharts plus d3 and redux (~348 KB, used
only by charts), every page under `src/client/pages` (225 KB), and the
emoji shortcode table (47 KB).

## Checks

- `pnpm typecheck`, `pnpm lint` (including the new rule test in
`oxlint-rules:check`), and `db:generate` (no changes).
- Dashboard e2e: 89/89 passed, run locally with the CI env unset.
- Dashboard unit tests: 368/369. The one failure is
`dashboard-output.test.ts` (nitro build: `nitroIntegration` is not
exported by a stale `@sentry/server-utils@11.6.0` in this sandbox). It's
environmental and not caused by this diff; #2720 reported the same
failure on main.
- Integration tests: `child-agents`, `locations/*`, conversation
`detail`, and `list-usage` (15/15).

No UI change, so no screenshots.

<!-- 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>
@BYK
BYK enabled auto-merge (squash) November 5, 2026 11:07
…-read-position

# Conflicts:
#	packages/junior/src/api/conversations/detail-etag.ts
@github-actions github-actions Bot added risk: high PR risk score: high and removed risk: medium PR risk score: medium labels Nov 5, 2026
@github-actions github-actions Bot added risk: medium PR risk score: medium and removed risk: high PR risk score: high labels Nov 5, 2026
@BYK
BYK merged commit 5abef39 into main Nov 5, 2026
44 checks passed
@BYK
BYK deleted the perf/detail-etag-skip-read-position branch November 5, 2026 11:18

@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 4913b70. Configure here.

...(previous.unreadCursor
? { unreadCursor: previous.unreadCursor, unreadPage: previous.unreadPage }
: undefined),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Race drops unread cursor

Medium Severity

An in-flight detail poll that finishes after a read can bring a later latest page while keepLatestReadPosition still trusts the cached unreadCursor decision. That decision was made against the older events, so the cursor is gone and later 304s never restore it. A later visit in the same session then opens on the latest page instead of the unread rows.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4913b70. Configure here.

This branch was successfully deployed

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

Labels

risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants