Skip to content

fix(heatmap): persist drag-select rectangle across re-renders#2189

Open
alex-fedotyev wants to merge 5 commits intomainfrom
alex/HDX-4147-heatmap-select-persist
Open

fix(heatmap): persist drag-select rectangle across re-renders#2189
alex-fedotyev wants to merge 5 commits intomainfrom
alex/HDX-4147-heatmap-select-persist

Conversation

@alex-fedotyev
Copy link
Copy Markdown
Contributor

@alex-fedotyev alex-fedotyev commented May 5, 2026

Summary

The dashed selection rectangle on the Event Deltas heatmap collapses to a 2x2 px box the instant the user releases the mouse. The filter itself is applied correctly (URL gets xMin/xMax/yMin/yMax, the comparison legend shows "Selection" vs "Background", and the bar charts below split into two colors), but visually the user has no idea what slice of the heatmap they picked.

Repro on play-clickstack: open the Demo Traces source, switch to the Event Deltas tab, drag any region. Inspect the .u-select element after mouse-up: width=2 px, height=2 px, position pinned to top-left of the canvas.

Same component is used in the Search heatmap (DBSearchHeatmapChart), so this fix applies there too. Dashboard tile heatmaps don't pass onFilter, so they're not affected.

Linked: HDX-4147.

Root cause

HeatmapContainer rebuilt the [time, bucket, count] arrays in its render body each render. After a drag:

  1. setSelect hook fires onFilter(...) -> setFields({xMin, xMax, yMin, yMax}) (nuqs URL update)
  2. URL change re-renders the parent
  3. New [time, bucket, count] arrays are constructed with identical values but fresh refs
  4. data prop ref changes, so uplot-react's data effect runs (propDataRef.current !== data)
  5. uPlot's setData(data, true) path is taken and the rendered u.select rectangle ends up wiped (the visible 2x2 px residue)

Stable data refs short-circuit step 4 before any setData call, leaving u.select untouched.

Fix

Memoize the [time, bucket, count] data array in HeatmapContainer so the inner references stay stable across re-renders that don't change the underlying data.

Also memoized generatedTsBuckets (it's a fresh Date[] from timeBucketByGranularity each render); without that, including it in the heatmapData useMemo deps would defeat the memoization.

Test plan

  • Bug reproduced on play-clickstack (drag-select on Demo Traces -> Event Deltas, observe .u-select collapse to 2x2 px)
  • yarn workspace @hyperdx/app run tsc --noEmit passes for the modified file
  • yarn workspace @hyperdx/app jest --testPathPatterns heatmap passes (1537 tests)
  • No new lint warnings in the diff
  • End-to-end browser verification: drag-select shows a persistent red dashed rectangle, click anywhere clears it, time-range / display-settings change clears it. Local dev stack hit a Docker BuildKit issue while bringing up the OTel collector image. Vercel preview is up but its login flow is currently broken for these previews, so this checkbox is left for the reviewer's environment.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 5, 2026

🦋 Changeset detected

Latest commit: c7ee6cd

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

This PR includes changesets to release 3 packages
Name Type
@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
Copy link
Copy Markdown

vercel Bot commented May 5, 2026

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

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment May 6, 2026 2:19am

Request Review

@github-actions github-actions Bot added the review/tier-2 Low risk — AI review + quick human skim label May 5, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

🔵 Tier 2 — Low Risk

Small, isolated change with no API route or data model modifications.

Why this tier:

  • Standard feature/fix — introduces new logic or modifies core functionality

Review process: AI review + quick human skim (target: 5–15 min). Reviewer validates AI assessment and checks for domain-specific concerns.
SLA: Resolve within 4 business hours.

Stats
  • Production files changed: 1
  • Production lines changed: 78
  • Branch: alex/HDX-4147-heatmap-select-persist
  • Author: alex-fedotyev

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

PR Review

fix(heatmap): persist drag-select rectangle across re-renders — the root cause analysis is accurate and the memoization fix is correct.

  • ⚠️ E2E not verified → PR explicitly notes browser-level verification was skipped due to a Docker issue. Reviewer should drag-select on Event Deltas / Search heatmaps and confirm the dashed rectangle persists after mouseup before merging.

  • ⚠️ eslint-disable-next-line react-hooks/exhaustive-deps on generatedTsBuckets → The primitive-ms pattern is valid, but if dateRange[0] or dateRange[1] is null/undefined at render time, fromMs/toMs silently fall back to 0 while the memo body still receives the real (possibly non-null) dateRange[i] objects. If the parent can pass a null dateRange, the memoized buckets would be stale. This is pre-existing behavior, but worth confirming the parent always provides non-null dates before this component mounts.

  • ✅ Dependency array [data, generatedTsBuckets, scaleType, effectiveMin, max, nBuckets] is complete — no silent stale-closure risk.

  • const time = heatmapData[0] extraction for the time.length < 2 guard is clean; bucket/count are no longer leaked to the outer scope.

  • ✅ Changeset present and scoped correctly as patch.

No blocking issues beyond the missing browser smoke-test. If a reviewer can confirm the visual fix in-browser, this is good to merge.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

E2E Test Results

All tests passed • 158 passed • 3 skipped • 1137s

Status Count
✅ Passed 158
❌ Failed 0
⚠️ Flaky 5
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

The dashed selection rectangle on the Event Deltas heatmap collapses to
a 2x2 px box the instant the user releases the mouse. The filter is
applied correctly (URL params, comparison legend, bar charts all
update), but visually you can no longer see what region you picked.

Root cause: uplot-react's dataMatch deep-compares the data prop and
calls chart.setData(data, true) when refs differ. setData resets
uPlot's internal u.select rectangle. HeatmapContainer rebuilt the
[time, bucket, count] arrays in its render body, so on every parent
re-render those entries had new references with identical values, and
dataMatch returned false.

Memoize the data array with stable refs so dataMatch returns true,
setData is skipped, and u.select survives the re-render that follows
a drag (the URL param write triggers).

Also memoize generatedTsBuckets so its fresh-Date-array each render
doesn't propagate into the heatmapData useMemo deps.

HDX-4147
Lint feedback from CI:
- timestampColumn?.name dep is wrong since the closure uses the full
  timestampColumn object; pass the object instead. The ref is stable
  when data is stable, so this doesn't reintroduce the original bug.
- Stop destructuring bucket/count when only time is needed for the
  not-enough-data short-circuit.

Add changeset (patch on @hyperdx/app).
Bot review feedback: timestampColumn was a non-primitive useMemo dep
derived entirely from data?.meta. Move the call inside the useMemo so
data is the only relevant dep, which is the cleanest way to satisfy
both exhaustive-deps and the ref-stability the fix relies on.

Also drop the // x values / // y value series 1 / // y value series 2
labels — they describe what the line is rather than why.
prose-lint flags U+2014 anywhere; tighten the comment instead.
Drop the dataMatch description (it was element-wise === per series,
not top-level === as written) and consolidate to the actionable
mechanism: fresh data ref drives uplot-react to call setData(data, true)
which wipes u.select. Add HDX-4147 ticket reference.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge review/tier-2 Low risk — AI review + quick human skim

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant