Skip to content

feat(app): show live ClickHouse query progress during a search - #3103

Draft
wrn14897 wants to merge 2 commits into
mainfrom
warren/leverage-clickhouse-progress-headear
Draft

feat(app): show live ClickHouse query progress during a search#3103
wrn14897 wants to merge 2 commits into
mainfrom
warren/leverage-clickhouse-progress-headear

Conversation

@wrn14897

@wrn14897 wrn14897 commented Sep 9, 2026

Copy link
Copy Markdown
Member

Why

A search over a long range shows an unqualified spinner for its whole duration.
The user cannot tell whether ClickHouse is 5% or 95% of the way through, or
whether it is doing anything at all. This surfaces ClickHouse's own progress
counters in two places: the results-table footer and the histogram's
"Scanned Rows | Elapsed Time" strip.

How

Transport. Progress comes from JSONEachRowWithProgress, which interleaves
{"progress":...} lines with rows in the response body. Headers were a dead
end: ClickHouse stops emitting X-ClickHouse-Progress once the body starts
(WriteBufferFromHTTPServerResponse::onProgress checks headers_finished_sending),
and fetch() cannot surface headers incrementally anyway. The format needs
ClickHouse >= 25.1 (PR #74181 added the meta/exception events); it is
chosen per connection via metadata.getServerVersion, and older or unknown
servers silently keep the existing non-streaming path.

Proxy. The API's clickhouse-proxy now flushes each upstream chunk through
compression() instead of letting zlib buffer the tiny progress lines until
enough bytes accumulate. It also no longer tries to write a 500 after the body
has started — that threw ERR_HTTP_HEADERS_SENT from inside the error
listener, an uncaught exception that takes down the process in dev.

Percent is time coverage, not rows. The only row denominator available is
an EXPLAIN estimate that ignores skip indexes and early LIMIT termination, and
is wrong outright when a chart is rewritten onto a materialized view — a
row-based bar would routinely stall short of the end. Instead each time window
contributes its share of the total range, weighted by how far ClickHouse has
scanned it. This also matches what the surrounding UI already says
("Searched ... across about 1 month").

Surviving window boundaries. Search pagination fetches one time window at a
time (15m, 6h, 6h, 12h, 24h, ...), so isFetching dips between them.
Progress deliberately survives those dips — clearing on that edge made the bar
vanish and the elapsed timer restart at every window, dozens of times over a
month-long range — and idle gaps are excluded from elapsed. A fresh run resets
it from the queryFn, which is keyed to an actual new query rather than racing
a timeout. Windows also register themselves as in-flight at 0% before reading,
so a just-opened window's placeholder page is not credited its whole range.

Hidden during live tail, where the query refetches every few seconds and a bar
would only flicker.

Testing

  • make ci-lint, make ci-unit (7,855 tests), make dev-int FILE=clickhouseProxy — all green
  • Negative controls run for the three new behaviours (proxy per-chunk flush,
    proxy headersSent guard, progress surviving window gaps): each test fails
    without its fix
  • Not yet done: manual browser verification on a clean yarn dev restart.
    Marked draft for that reason.

Known caveats

  • With the 1-minute first window, a search that fills its LIMIT immediately
    shows ~0% for a moment before the bar disappears. Accurate, but may look odd.
  • The footer's "Loading results..." text itself unmounts for one frame between
    windows (DBRowTable.tsx:1255). Pre-existing; not addressed here.

The results table footer and the histogram's "Scanned Rows | Elapsed Time"
strip now report rows read and percent complete while a search runs, instead
of an unqualified spinner.

Progress is read from ClickHouse's JSONEachRowWithProgress events, streamed
in the response body. Progress headers are not an option in the browser:
ClickHouse stops emitting X-ClickHouse-Progress once the body starts, and
fetch() cannot surface headers incrementally anyway. The format needs >= 25.1
for its meta/exception events, so the format is chosen per connection and
older servers silently keep the previous non-streaming path.

The percentage is time coverage, not rows. The only row denominator available
is an EXPLAIN estimate that ignores skip indexes and early LIMIT termination,
and is wrong outright when a chart is rewritten onto a materialized view, so a
row-based bar would routinely stall short of the end. Coverage also matches
what the surrounding UI already says ("Searched ... across about 1 month").

Search results paginate one time window at a time, each its own fetch, so
isFetching dips between windows. Progress deliberately survives those dips —
clearing on that edge made the bar vanish and the elapsed timer restart at
every window boundary — and idle gaps are excluded from elapsed. A fresh run
resets it from the queryFn, which is keyed to an actual new query rather than
racing a timeout. Windows also register themselves as in-flight at 0% before
reading, so the placeholder page for a just-opened window is not credited its
whole range.

Hidden during live tail, where the query refetches every few seconds and a bar
would only flicker.
@changeset-bot

changeset-bot Bot commented Sep 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 09280ea

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@hyperdx/app Minor
@hyperdx/common-utils Minor
@hyperdx/api Minor
@hyperdx/otel-collector Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Sep 9, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
hyperdx-oss Ready Ready Preview Sep 9, 2026 11:34pm UTC
hyperdx-storybook Ready Ready Preview Sep 9, 2026 11:34pm UTC

Request Review

@greptile-apps

greptile-apps Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds live ClickHouse query progress to search results and histogram status displays while preserving compatibility with older ClickHouse servers.

  • Streams and parses JSONEachRowWithProgress responses on supported servers.
  • Aggregates progress across query chunks, windows, and offset pages.
  • Flushes compressed proxy chunks and safely handles failures after response headers are sent.
  • Adds UI indicators and coverage for streaming, aggregation, compatibility, and proxy behavior.

Confidence Score: 4/5

The functional progress fixes appear sound, but the explicit semantic-color requirement must be satisfied before merging; live-tail progress collection should also be disabled to avoid unused recurring work.

The previous offset-pagination issue is fixed by withholding full-window completion until an empty page, the shared histogram observers now receive matching execution options, and the prohibited test any has been replaced with a concrete response type. Two lower-impact issues remain: live-tail polling still performs hidden progress tracking, and the new progress component violates the repository requirement to use semantic color tokens.

Files Needing Attention: packages/app/src/DBSearchPage.tsx, packages/app/src/components/QueryProgressIndicator.tsx

Important Files Changed

Filename Overview
packages/app/src/hooks/useOffsetPaginatedQuery.tsx Streams supported queries and now preserves counters without marking offset-paginated windows complete prematurely.
packages/app/src/hooks/useQueryProgress.ts Aggregates active and settled progress across requests sharing a time-window chunk.
packages/app/src/DBSearchPage.tsx Enables histogram progress throughout search views, but leaves collection enabled during live tail when its display is suppressed.
packages/app/src/components/QueryProgressIndicator.tsx Adds inline and block progress displays, with raw palette colors that should use semantic tokens.
packages/api/src/routers/api/clickhouseProxy.ts Flushes each compressed upstream chunk and destroys started responses safely on upstream failure.
packages/app/src/components/SearchTotalCountChart.tsx Aligns progress and parallel-query options across observers sharing the histogram query key.

Sequence Diagram

sequenceDiagram
    participant UI as Search UI
    participant Query as Query hooks
    participant Proxy as ClickHouse proxy
    participant CH as ClickHouse

    UI->>Query: Start search with progress enabled
    Query->>Proxy: JSONEachRowWithProgress query
    Proxy->>CH: Forward query
    loop While query runs
        CH-->>Proxy: Progress or row chunk
        Proxy-->>Query: Flush chunk immediately
        Query-->>Query: Aggregate window/page progress
        Query-->>UI: Update rows, elapsed time, and coverage
    end
    CH-->>Proxy: Metadata and completion
    Proxy-->>Query: Final chunks
    Query-->>UI: Render completed results
Loading

Fix all with Greploop Fix All in Claude Code Fix All in Conductor Fix All in Cursor Fix All in Codex

Reviews (2): Last reviewed commit: "fix(app): correct progress accounting fo..." | Re-trigger Greptile

Comment thread packages/app/src/hooks/useOffsetPaginatedQuery.tsx Outdated
Comment thread packages/app/src/components/SearchTotalCountChart.tsx
Comment thread packages/app/src/hooks/__tests__/useChartConfig.test.tsx Outdated
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 357 passed • 1 skipped • 1402s

Status Count
✅ Passed 357
❌ Failed 0
⚠️ Flaky 0
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

…ervers

Three issues from review.

A window serves its results over several offset pages, each its own
ClickHouse query. Marking the window complete after every page credited its
whole range while pages were still running, and let each page's counters
replace the previous page's instead of adding to them, so the reported row
count could drop. A window is now credited its full range only once a page
comes back empty, which is the same condition getNextPageParam uses to move
on, and settled counters are banked on the progress entry so several requests
can share one chunk. coverageFromPages applies the same rule to pages restored
from cache.

reportProgress is not part of the query key, so whichever observer wins the
deduplicated fetch decides whether progress is emitted at all. The count text
shares the histogram key and renders before the strip, so it was usually the
one to own the request — with progress reporting off. It now takes and
forwards the flag like the other two observers.

Also types a test's promise resolver instead of using any.
}
histogramTimeChartConfig={histogramTimeChartConfig}
enableParallelQueries
reportProgress

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Hidden live-tail progress work

During live tail, the visible progress indicator is suppressed, but the mounted histogram observers still receive reportProgress. Every poll therefore continues collecting and storing progress that cannot be displayed, adding avoidable streaming and client-state overhead. Disable progress reporting when live tail is active instead of only hiding the indicator. The same issue occurs for the time-chart observer at line 2624.

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

animated={isIndeterminate}
size="xs"
w={72}
color="gray.6"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Raw palette color used

This progress bar uses the raw Mantine palette value color="gray.6", and the block variant repeats it at line 71. The repository UI directive requires semantic color tokens for application content when one exists so components remain consistent across themes and palette changes. This repository requirement must be satisfied before merging.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

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