Skip to content

feat(charts): make the series limit opt-in and consistent across chunks#2449

Merged
kodiakhq[bot] merged 10 commits into
mainfrom
warren/fix-series-limit-empty-group
Jun 15, 2026
Merged

feat(charts): make the series limit opt-in and consistent across chunks#2449
kodiakhq[bot] merged 10 commits into
mainfrom
warren/fix-series-limit-empty-group

Conversation

@wrn14897

@wrn14897 wrn14897 commented Jun 11, 2026

Copy link
Copy Markdown
Member

Summary

Reworks the top-N series cap from #2429. The limit moves from a workspace-wide team setting to a per-chart control in the Display Settings drawer, and now holds correctly across chunked queries.

Per-chart, opt-in (default: disabled). A chart's seriesLimit is set in its Display Settings drawer — it already persisted on the chart config, so no new persistence was needed. The control only renders for builder line/bar charts. When unset, charts fetch every series and no __hdx_series_limit CTE is emitted. The old workspace-wide team seriesLimit setting is removed (team settings UI, the seriesLimit ClickHouse setting schema, and the mongoose field).

Value sourced from the chart, not the team. convertToTimeChartConfig no longer takes a team limit; it reads config.seriesLimit (clamped to ≥ 1). DBTimeChart, SearchTotalCountChart, and the "Generated SQL" preview (buildChartConfigForExplanations) all drop the me.team.seriesLimit plumbing, so the preview matches the executed query — including no CTE when the limit is disabled.

Consistent across chunks. Time charts fetch in time-windowed chunks, and each chunk previously ranked its top-N within its own window — so the union across chunks could exceed the limit and adjacent windows disagreed. Chunked queries now pin the ranking to the newest chunk window via a runtime-only seriesLimitDateRange, and renderSeriesLimitCte renders the CTE's WHERE/GROUP BY against that pinned range (boundary inclusivity normalized) while the outer query still fetches only its window. The CTE is byte-identical across chunks, so every chunk keeps the same top-N set and the union equals N. Trade-off: a group with no events in the newest window is dropped even if it was active earlier. Unchunked consumers never set the field and are unchanged.

Tests

  • ChartUtils / buildChartConfigForExplanations: per-chart limit applied / omitted when disabled.
  • ChartDisplaySettingsDrawer: control shown for builder line/bar charts only; entered value sets seriesLimit; emptying clears it to disabled.
  • useChartConfig: serial and parallel chunked fetches pin the ranking to the newest window; unchunked fetches don't set it.
  • renderChartConfig: CTE pinned to seriesLimitDateRange and byte-identical across two windowed renders; no CTE without a limit.
  • ClickHouse integration: two windows with different local winners both return the newest window's top-1.

References

Screenshot

image

Each time-window chunk's __hdx_series_limit CTE ranked the top N within
its own window, so the union across chunks could exceed seriesLimit and
adjacent windows disagreed on which series to keep. Chunked queries now
carry the full chart range as seriesLimitDateRange and the CTE ranks
over that pinned range, so every chunk keeps the identical top-N set.
Follow-up to HDX-4499.
@changeset-bot

changeset-bot Bot commented Jun 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 137c338

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

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

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 Jun 11, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment Jun 15, 2026 9:52pm
hyperdx-storybook Ready Ready Preview, Comment Jun 15, 2026 9:52pm

Request Review

@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label Jun 11, 2026
@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

🔴 Tier 4 — Critical

Touches auth, data models, config, tasks, OTel pipeline, ClickHouse, or CI/CD.

Why this tier:

  • Critical-path files (1):
    • packages/api/src/models/team.ts
  • Cross-layer change: touches frontend (packages/app) + backend (packages/api) + shared utils (packages/common-utils)

Review process: Deep review from a domain expert. Synchronous walkthrough may be required.
SLA: Schedule synchronous review within 2 business days.

Stats
  • Production files changed: 10
  • Production lines changed: 146 (+ 418 in test files, excluded from tier calculation)
  • Branch: warren/fix-series-limit-empty-group
  • Author: wrn14897

To override this classification, remove the review/tier-4 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the workspace-wide team seriesLimit setting with a per-chart opt-in control in the Display Settings drawer, and fixes a longstanding bug where each chunked time-window independently computed its own top-N series — causing the union across chunks to exceed the limit and adjacent windows to disagree on which series to show.

  • Per-chart opt-in: seriesLimit is now read from the chart config (not me.team); the team-level Mongoose field, Zod schema entries, and UI control are all removed. convertToTimeChartConfig normalizes nullundefined (disabled) and clamps to ≥1 when set.
  • Chunk consistency fix: fetchDataInChunks pins seriesLimitDateRange to windows[0].dateRange (the newest chunk window) and passes it to every chunk config; renderSeriesLimitCte re-renders its WHERE and GROUP BY against that pinned range so the CTE is byte-identical across all chunks.
  • Test coverage: Unit tests for serial/parallel chunking (useChartConfig), CTE identity across windows (renderChartConfig), per-chart limit in explanations (buildChartConfigForExplanations), and a ClickHouse integration test that verifies both windows return the newest window's top-1 winner.

Confidence Score: 5/5

Safe to merge — all changed paths are well-tested, the chunking logic is verified by both unit and integration tests, and the removed team-level setting had a clear per-chart replacement added in the same PR.

The core chunking fix is tightly scoped: only the CTE's WHERE/GROUP BY are re-rendered against the pinned range; the outer query and aggregate expressions are untouched. The null/undefined normalization for seriesLimit is handled consistently at every layer (Zod schema, convertToTimeChartConfig, form reset, URL round-trip). The byte-identical CTE assertion in tests is a strong correctness signal. No open logic gaps were found.

No files require special attention.

Important Files Changed

Filename Overview
packages/common-utils/src/core/renderChartConfig.ts Adds seriesLimitDateRange support to renderSeriesLimitCte — re-renders WHERE and GROUP BY against the pinned range while keeping the outer query windowed; inclusivity is normalized so the CTE is byte-identical across chunks.
packages/app/src/hooks/useChartConfig.tsx Introduces windowedConfigFor helper that injects seriesLimitDateRange = windows[0].dateRange (newest window) into every chunk when seriesLimit is active; unchunked paths are untouched.
packages/app/src/ChartUtils.tsx Removes the teamSeriesLimit parameter from convertToTimeChartConfig; reads per-chart seriesLimit from config, normalizing null to undefined (disabled) and clamping to ≥1 when set.
packages/app/src/components/ChartDisplaySettingsDrawer.tsx Adds NumberInput for per-chart Series Limit, shown only for builder line/bar charts; clears to null (not undefined) for JSON round-trip safety; wired through applyDefaultSettings and form reset.
packages/common-utils/src/types.ts Makes seriesLimit nullish in SelectSQLStatementSchema; adds runtime-only seriesLimitDateRange to DateRange type; removes seriesLimit from both TeamClickHouseSettings schemas.
packages/common-utils/src/tests/queryChartConfig.int.test.ts Integration test inserts data where older window and newer window have different top-1 groups; asserts both chunks return the newer window's winner when seriesLimitDateRange is pinned.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Chart config with seriesLimit] --> B{enableQueryChunking\n& shouldUseChunking?}
    B -- No --> C[windows = undefined\nno seriesLimitDateRange]
    B -- Yes --> D[getGranularityAlignedTimeWindows\nwindows newest→oldest]
    D --> E[rankingDateRange = windows 0 .dateRange\nnewest window]
    E --> F{seriesLimit != null\n& rankingDateRange != null?}
    F -- No --> G[windowedConfig = chunk window only]
    F -- Yes --> H[windowedConfig = chunk window\n+ seriesLimitDateRange = rankingDateRange]
    H --> I[renderSeriesLimitCte\nre-renders WHERE & GROUP BY\nagainst seriesLimitDateRange]
    I --> J[CTE byte-identical\nacross all chunks]
    J --> K[Union of chunks = exactly N series]
    C --> L[CTE uses outer query dateRange\nunchunked path unchanged]
Loading

Reviews (9): Last reviewed commit: "Merge branch 'main' into warren/fix-seri..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 199 passed • 3 skipped • 1343s

Status Count
✅ Passed 199
❌ Failed 0
⚠️ Flaky 4
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

buildChartConfigForExplanations called convertToTimeChartConfig without
the team's seriesLimit, so the Generated SQL section always rendered the
default LIMIT 100 even when the team setting was lower. Pass the same
value DBTimeChart uses so the preview matches the executed query.
Pinning the __hdx_series_limit ranking to the full chart range made
every chunk re-scan the whole range. Rank over the newest window
instead: still one fixed range shared by all chunks (so the top-N set
stays consistent), but the scan is bounded by the smallest window.
Trade-off: series are picked by recent activity, so groups with no
events in the newest window are dropped from the chart.
wrn14897 added 2 commits June 11, 2026 14:40
The team seriesLimit setting no longer falls back to 100: when unset,
charts fetch every series and no __hdx_series_limit CTE is emitted.
The team settings page shows the setting as "Disabled" by default and
adds a Disable button to clear a configured value back to undefined.
@wrn14897 wrn14897 changed the title fix(charts): keep a consistent top-N series set across chunked queries feat(charts): make the series limit opt-in and consistent across chunks Jun 11, 2026

@pulpdrew pulpdrew left a comment

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.

Change itself LGTM in isolation. I have some thoughts though:

  1. I think opt-in is a good idea, in the future I could see it being useful to opt-in at the chart level instead of team-wide, so that charts without too many series don't pay the performance penalty of this new approach, while charts that do need series limiting can opt-in individually. This could be another "display setting"
  2. Also I am wondering if you've considered a two-query solution: One query to fetch the top-N series over the whole time range; then the main query, without a CTE, just an additional WHERE <series> IN (...) condition?
    • This would address "Series are selected by activity in the newest window, so a group with no events there is dropped from the chart even if it was active earlier in the range."
    • I would imagine this could also make the query building a bit simpler too, but there are probably some complexities around making sure that the tuples identifying the top N series are rendered correctly, so maybe not.
    • This could also be used to fix the complaint that the chart seems to change drastically as new chunks load. The "pre flight" query could be used to get the max series value that will be rendered, and the chart y-axis could be scaled according to that value, instead of being re-scaled as each new chunk is loaded in.
  3. Not related to this change but something I notice about original implementation which is still present: When using multiple series, or when using ratio mode, only the first series is aggregated to choose the "top" series, instead of the max of the series or the ratio value.

@wrn14897

Copy link
Copy Markdown
Member Author

Change itself LGTM in isolation. I have some thoughts though:

  1. I think opt-in is a good idea, in the future I could see it being useful to opt-in at the chart level instead of team-wide, so that charts without too many series don't pay the performance penalty of this new approach, while charts that do need series limiting can opt-in individually. This could be another "display setting"

  2. Also I am wondering if you've considered a two-query solution: One query to fetch the top-N series over the whole time range; then the main query, without a CTE, just an additional WHERE <series> IN (...) condition?

    • This would address "Series are selected by activity in the newest window, so a group with no events there is dropped from the chart even if it was active earlier in the range."
    • I would imagine this could also make the query building a bit simpler too, but there are probably some complexities around making sure that the tuples identifying the top N series are rendered correctly, so maybe not.
    • This could also be used to fix the complaint that the chart seems to change drastically as new chunks load. The "pre flight" query could be used to get the max series value that will be rendered, and the chart y-axis could be scaled according to that value, instead of being re-scaled as each new chunk is loaded in.
  3. Not related to this change but something I notice about original implementation which is still present: When using multiple series, or when using ratio mode, only the first series is aggregated to choose the "top" series, instead of the max of the series or the ratio value.

  1. I thought about moving the series limit configuration to the Display Settings. One concern I have is the migration path, since in Cloud v1 the limit is enforced by default. We can defer that discussion for now, though. I agree that from a UX perspective, it makes more sense there.
  2. Good call on this. Selecting groups from the local maximum can be problematic because those groups may not always exist. My main concern is the performance impact of scanning groups across the entire time range, which could defeat the purpose of chunking. I'll put more thought into separating the queries and come back with some ideas.
  3. I think this is exactly the issue this PR is trying to address. The groups need to remain fixed.

pulpdrew
pulpdrew previously approved these changes Jun 13, 2026
…ile Display Settings

The series limit is now configured per chart in the Display Settings
drawer instead of as a workspace-wide team setting. The value already
persisted on the chart config, so convertToTimeChartConfig now reads
config.seriesLimit directly; the team seriesLimit field, its ClickHouse
settings schema, and the Team Settings UI control are removed. The
chunk-consistency behavior (seriesLimitDateRange pinned to the newest
window) is unchanged and now driven per tile.

HDX-4499
@github-actions github-actions Bot added review/tier-4 Critical — deep review + domain expert sign-off and removed review/tier-3 Standard — full human review required labels Jun 15, 2026
Comment thread packages/app/src/components/ChartDisplaySettingsDrawer.tsx
Clearing the per-chart Series Limit in Display Settings updated the chart
query but reappeared when the drawer was reopened. In Chart Explorer the
config round-trips through the URL query state (JSON), which drops keys
with undefined values; react-hook-form's `values` prop then leaves the
stale field in place because the key is absent. Clear to null instead,
which survives JSON serialization and forces RHF to reset the field.
seriesLimit is now nullish in the schema, and convertToTimeChartConfig
normalizes a cleared null back to undefined (no CTE).

HDX-4499
@kodiakhq kodiakhq Bot merged commit bf6e1f2 into main Jun 15, 2026
19 checks passed
@kodiakhq kodiakhq Bot deleted the warren/fix-series-limit-empty-group branch June 15, 2026 21:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge review/tier-4 Critical — deep review + domain expert sign-off

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants