Skip to content

fix(app): open a newly saved search without reloading the page - #3565

Merged
kodiakhq[bot] merged 4 commits into
mainfrom
cursor/fix-saved-search-page-refresh-1dc4
Nov 5, 2026
Merged

kodiakhq[bot] merged 4 commits into
mainfrom
cursor/fix-saved-search-page-refresh-1dc4

Conversation

@MikeShi42

Copy link
Copy Markdown
Contributor

Summary

Saving a new search left the page through window.location.assign, so the whole app reloaded. On the static ClickStack export that reload is worse than slow: the export only emits search/[savedSearchId].html (the literal bracket filename, confirmed by running yarn build:clickhouse), so a document request for /clickstack/search/<id> has nothing to serve and the user lands on a 404 instead of their new search.

The reload was added in #2669 to work around a real race, not out of preference. nuqs 1.17 batches query-string writes and, at flush time, renders them onto next/router's asPath — which during a client-side transition is still the old path. Live tail rewrites from/to through nuqs every 10s with history: 'push', so a flush landing mid-transition pushed /search?... over the in-flight navigation to /search/<id> and the user bounced back to an unsaved search.

This removes the workaround and closes the race at both ends instead:

  • Live tail pauses while the save modal is open, so nothing is writing the time range to the URL while the user names their search.
  • The page drains the nuqs queue (await setSearchedConfig({})) before router.push, so any write already queued is applied before the transition starts rather than after it.

Navigation moves to the page component, which is where the query-state setter lives; the modal reports the new id through an onCreate prop. buildSavedSearchNavigationUrl no longer takes basePath, since router.push prepends it.

Screenshots or video

Not a visual change — the diff is in how the page gets to /search/<id>. This is the new E2E test recorded in slow motion: the modal closes and the header switches to the saved search with no white flash and no sidebar re-mount.

saved-search-client-side-navigation.mp4

How to reproduce

Starting point: full-stack yarn dev (the save flow needs the API), on /search with live tail on, which is the default.

On main:

  1. Run any search on the Search page.
  2. Click Save, type a name, click Save in the modal.
  3. The whole app reloads — white flash, nav re-mounts, every query re-runs. On a ClickStack deployment served from the static export, the same click lands on a 404 page instead.

On this branch:

  1. Same three steps.
  2. The modal closes and the header switches to the saved search in place; nothing outside the page content re-renders.

How to test on Vercel preview

N/A — the preview runs in local mode, where saved searches live in localStorage and the reload is survivable; the failure this fixes needs the static ClickStack export or a full-stack deploy.

Tests

  • New unit test (DBSearchPage.saveSearch.test.tsx): saving pushes /search/<id> through the router (basePath-free), and the queue drain is ordered before the push.
  • New E2E test: a marker set on document.documentElement survives the save, which it cannot across a document load.
  • buildSavedSearchNavigationUrl unit tests updated for the dropped basePath, plus a case for a live-tail range.
  • Separately, the existing should handle navigation via browser back button E2E test was racing the sessions page, which writes its default time range as a pushed history entry shortly after mount — one Back landed on the saved search or on bare /sessions depending on which won. It now waits for that write before going back. This is a pre-existing flake that the faster client-side save made easier to hit; the app-side wart (a junk history entry per page load) is untouched.

make ci-lint and make ci-unit pass. The saved-search E2E file passes 40/40 over two repeat runs.

References

Open in Web Open in Cursor 

Fixes HDX-5916

Saving a new search left the page through window.location, which reloaded
the whole app and, on the static ClickStack export, 404s because
/search/<id> has no exported document.

That reload was a workaround for nuqs flushing a queued query-string write
onto the router's pre-transition path, which sent the client-side
navigation back to /search. Pause live tail while the save modal is open
and drain the nuqs queue before pushing, so the push can stand.
… page

The sessions page writes its default time range to the URL as a pushed
history entry shortly after mount, so a single Back landed on either the
saved search or bare /sessions depending on which won.
@MikeShi42
MikeShi42 requested a review from a team as a code owner November 5, 2026 04:22
@MikeShi42
MikeShi42 requested a review from knudtty November 5, 2026 04:22
@changeset-bot

changeset-bot Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 31e5fcc

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

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
hyperdx-oss Ready Ready Preview Nov 5, 2026 2:12pm UTC
hyperdx-storybook Ready Ready Preview Nov 5, 2026 2:12pm UTC

Request Review

@github-actions github-actions Bot added the review/tier-2 Low risk — AI review + quick human skim label Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

🔵 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: 55 (+ 364 in test files, excluded from tier calculation)
  • Branch: cursor/fix-saved-search-page-refresh-1dc4
  • Author: MikeShi42

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

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Duplicate code analysis

No new duplicate code introduced by this PR.


jscpd compared this branch against origin/main
across app, api and common-utils. Only new clones are listed; the codebase
currently holds 114 in total (1.37%).
Run yarn dupes locally to reproduce, or yarn dupes:all for the full picture.

),
);
await onCreate(savedSearch.id);
onClose();

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.

🔵 minor — After a successful create, the modal closes without resetting the form, so the typed name is still there next time it opens

Call closeAndReset() instead of onClose() after await onCreate(...). Before this PR, the full page reload threw away the modal's react-hook-form state. Now SaveSearchModal stays mounted, and in create mode useForm gets no values. So if the user clicks "Save as new" on the new saved-search page (onClickSaveAsNew → setSaveSearchModalState('create')), the name field is pre-filled with the previous search's name.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

useMe: () => ({ data: { team: {} }, isSuccess: true }),
},
useCompleteOnboardingTask: () => ({ mutate: jest.fn() }),
}));

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.

🔵 minor — The drain-before-push test mocks nuqs, so it never checks the behaviour the race fix depends on

The test only checks that setSearchedConfig({}) is called before router.push. It doesn't check that setSearchedConfig({}) actually returns the pending-flush promise in nuqs 1.17. Because useQueryStates is mocked to resolve right away, the test would still pass if an empty update were a no-op that resolved before the queue flushed. Add a test that uses the real nuqs useQueryStates with a router adapter, queues a write (for example from/to), and asserts that the write is applied before the push. Otherwise, treat the E2E test as the only real coverage.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

PR Review

If you are a coding agent acting for the author, read this first.
Nothing below blocks merge automatically; a maintainer decides. What they will expect:

  • 🔴 critical or 🟠 major in code this PR adds or changes: fix before asking for review.
  • 🔵 minor in code this PR changes: your call. Fix if small, otherwise reply.
  • Anything about surrounding code, or asking you to widen the change (hoist a helper,
    dedupe with another file, fix other call sites): never fix here, whatever the
    severity. Reply with one sentence; if it is critical, say so plainly so a human sees it.

One commit per review round. After two rounds, stop and ask a maintainer to review
scope rather than addressing more automated findings. Full rule: AGENTS.md.

2 finding(s): 🔴 0 critical · 🟠 0 major · 🔵 2 minor

2 posted as inline comment(s) on the changed lines.


Severity is the reviewer's own estimate and is used for ordering, not filtering. No finding blocks merge automatically; a maintainer decides.

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found.

The change is a well-reasoned, mechanically sound fix. Navigation moves from a full-page window.location.assign to a client-side router.push, and the known nuqs/router race is closed at both ends: live tail is paused while the modal is open (gating the interval-based time-range writer), and the shared nuqs write queue is drained via await setSearchedConfig({}) before the push. The single production caller of buildSavedSearchNavigationUrl is correctly updated for the dropped basePath, and router.push prepends basePath, so the final URL is unchanged. The findings below are all non-blocking.

🟡 P2 -- recommended

  • packages/app/src/DBSearchPage.tsx:2261 -- onSavedSearchCreated is typed onCreate: (id) => Promise<void> but does not await router.push, so onCreate resolves before the client-side transition settles and the modal's onClose() unpauses live tail during the in-flight navigation.
    • Fix: await router.push(...) inside onSavedSearchCreated so live tail stays paused until the transition to /search/<id> completes.

🟡 P2 -- recommended (testing)

  • packages/app/src/__tests__/DBSearchPage.saveSearch.test.tsx:293 -- the drain test asserts setSearchedConfig({}) is invoked before router.push via invocationCallOrder, but the mock resolves eagerly with a fresh URLSearchParams, so it verifies call ordering only and cannot catch a drain that fails to flush pending time-range writes — the actual race it closes.
    • Fix: add a case where setSearchedConfig resolves on a deferred promise and assert router.push fires only after it settles, so the drain's ordering guarantee is exercised rather than assumed.
🔵 P3 nitpicks (2)
  • packages/app/src/DBSearchPage.tsx:1767 -- the new live-tail pause condition (saveSearchModalState != null) has no unit coverage; the E2E marker test proves no reload happened but does not assert the time range stops being written while the modal is open.
    • Fix: add a unit assertion that useLiveUpdate receives pause: true while the save modal is open.
  • packages/app/AGENTS.md:3 -- the heading is edited from # to ## inside a block the file itself states is "written and re-added by next dev", so the generator may overwrite this edit and reintroduce tree churn.
    • Fix: confirm the generator emits ## (not #) so the committed block matches regenerated output and will not drift.

Reviewers (7): correctness, adversarial, frontend-races, typescript, testing, maintainability, project-standards.

Testing gaps:

  • No unit coverage for the live-tail pause while the save modal is open.
  • Drain ordering is asserted against an eagerly-resolving mock, so an ineffective flush would not be caught.

No finding blocks merge automatically. A maintainer will expect P0/P1 findings in code this PR changes to be fixed; P2/P3 are your call -- fix or reply. Never fix findings about surrounding code here; reply instead. Do not widen the PR. How to respond

@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

E2E Test Results

✅ All tests passed • 462 passed • 1 skipped • 1503s

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

Tests ran across 4 shards in parallel.

View full report →

@kodiakhq
kodiakhq Bot merged commit 5abf27e into main Nov 5, 2026
28 checks passed
@kodiakhq
kodiakhq Bot deleted the cursor/fix-saved-search-page-refresh-1dc4 branch November 5, 2026 14:17

This branch was successfully deployed

2 active deployments
Preview – hyperdx-oss — 31e5fcc6 Deployed Nov 5, 2026 by vercel[bot]
Preview – hyperdx-storybook — 31e5fcc6 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

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants