Skip to content

feat(app): add faceted filters to the sessions page - #2992

Open
vinzee wants to merge 1 commit into
hyperdxio:mainfrom
vinzee:add-session-filters
Open

feat(app): add faceted filters to the sessions page#2992
vinzee wants to merge 1 commit into
hyperdxio:mainfrom
vinzee:add-session-filters

Conversation

@vinzee

@vinzee vinzee commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Bring the search page's faceted filter sidebar to /sessions and wire the selected filters into the session aggregation query.

  • Add a SessionFilters wrapper around DBSearchPageFilters that hides the analysis-mode tabs, denoise, and root-spans-only toggles that don't apply to sessions.
  • Persist sidebar filters in the URL and thread them through useSessions so they constrain the trace aggregation alongside the free-text query.
  • Scope both the facet sidebar and the aggregation to RUM session spans:
  • Add unit tests covering the new sessions behavior.

Screenshots or video

The Sessions Tab now has a sidebar with filters:
image

How to test on Vercel preview

Preview routes: /sessions

Steps (/sessions):

  1. Open /sessions.
  2. In the "Data Source" dropdown (combobox labeled "Data Source"), confirm a session source (labeled "Sessions") is selected; if it is not, open the dropdown and choose the "Sessions" source.
  3. Wait for the results area (data-testid "session-card-list") to render at least one session card (each card has a data-testid beginning with "session-card-").
  4. Confirm the filters sidebar is visible to the left of the results with a "Filters" heading.
  5. Confirm the sidebar is populated: at least one facet group is listed (for example a group labeled "ServiceName") and the text "No filters available" is NOT shown.
  6. Click the "ServiceName" facet group header to expand it, then click the first value's checkbox inside its panel.
  7. Confirm an active filter pill appears above the results list and the page URL gains a "filters=" query parameter.
  8. Click "Hide filters", confirm the sidebar collapses, then click "Show filters" and confirm the sidebar reappears.
  9. Verify the results area (data-testid "session-card-list") re-rendered after the filter was applied and no red error notification was shown.

References

  • Linear Issue: n/a
  • Related PRs: n/a

@changeset-bot

changeset-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2f51ae2

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 Aug 25, 2026

Copy link
Copy Markdown

@vinzee is attempting to deploy a commit to the HyperDX Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps

greptile-apps Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds URL-persisted faceted filters to the sessions page and applies them to correlated RUM trace aggregation.

  • Adds a sessions-specific wrapper around the shared filter sidebar.
  • Builds facet queries scoped to RUM session spans.
  • Adds service-based trace-scan scoping and filter/parser unit coverage.

Confidence Score: 3/5

The PR is not yet safe to merge because service scoping can omit valid sessions and source changes can execute stale facet filters against a different trace schema.

A nonempty but incomplete session-derived service list remains a hard trace scope, while source changes retain and resubmit SQL filters created for the previous correlated trace source.

Files Needing Attention: packages/app/src/sessions.ts and packages/app/src/SessionsPage.tsx

Important Files Changed

Filename Overview
packages/app/src/SessionsPage.tsx Adds URL-backed filter state, the sidebar UI, and facet configuration, but source switches still submit filters bound to the previous trace source.
packages/app/src/sessions.ts Applies facet filters to session aggregation and adds service scoping, but a successful incomplete service enumeration still excludes valid sessions.
packages/app/src/components/DBSearchPageFilters.tsx Makes analysis controls optional and supports forcing exact facet mode for the sessions wrapper.
packages/app/src/components/SessionFilters.tsx Provides a thin sessions-specific configuration of the shared faceted-filter sidebar.
packages/app/src/tests/sessions.test.ts Covers filter URL validation and service-filter serialization, but not incomplete service enumeration or source-switch behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  U[Session query and facet filters] --> T[Correlated trace source]
  S[Session source] --> E[Enumerate service names]
  E --> I[ServiceName IN scope]
  I --> T
  T --> A[Aggregate sessions]
  A --> L[Session list]
Loading

Reviews (5): Last reviewed commit: "feat(app): add faceted filters to the se..." | Re-trigger Greptile

Comment thread packages/app/src/sessions.ts Outdated
Comment thread packages/app/src/SessionsPage.tsx
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Deep Review

Faceted-filter sidebar added to /sessions, URL-persisted filters threaded into the RUM session trace aggregation, plus a ServiceName IN (...) scope optimization (RUM services enumerated from the session source over a 30-day lookback, used to constrain the trace query; unscoped fallback only on empty/error). Ten reviewers ran. Security cleared the SQL surface: escapeSqlString correctly neutralizes backslash-then-quote for ClickHouse literals and the ?filters= raw-SQL param is pre-existing product behavior already exposed on the search page — no new injection.

🔴 P0/P1 — must fix

  • packages/app/src/sessions.ts:234 — A non-empty-but-incomplete session-source service list still constrains the trace aggregation to ServiceName IN (...), silently omitting sessions whose service has in-range RUM trace spans but no session-source row within the 30-day window (session retention shorter than trace retention, ingestion lag, or a churned service); the fallback fires only on an empty/failed result, never a partial one, so this is a silent correctness regression from the previously unscoped scan.
    • Fix: Treat a scoped-but-empty final result as a signal to retry unscoped, or gate the IN scope behind a completeness guarantee rather than assuming the session-source enumeration is exhaustive.
    • correctness, adversarial, previous-comments, testing
  • packages/app/src/SessionsPage.tsx:344 — On source change, submitOnSourceChangeonSubmit spreads the existing form filters (built against the previous trace source's columns/map-keys) into the new source's applied config with no reset or reconciliation, so switching the Data Source applies stale facet filters to a schema that may lack those columns — producing a query error or silently wrong results.
    • Fix: Clear filters and the ?filters= param when sourceId diverges from the resolved param, or re-validate persisted filters against the new trace source's columns before applying them.
    • adversarial, previous-comments, frontend-races, correctness, testing

🟡 P2 — recommended

  • packages/app/src/sessions.ts:198 — The DISTINCT enumeration interpolates traceSource.serviceNameExpression against sessionSource.from; if the session source stores service name under a different column, the enumerated values never match the trace ServiceName predicate, yielding a non-empty IN list that matches zero rows and drops every session with no error.
    • Fix: Read the distinct list using sessionSource.serviceNameExpression, and apply the IN predicate only when the two service-name domains are known to align.
    • adversarial, correctness
  • packages/app/src/sessions.ts:224.filter((name): name is string => !!name) drops the empty string along with null, so sessions whose ServiceName is '' are excluded from the IN list and vanish from results whenever the scope predicate is active.
    • Fix: Distinguish null (unmatchable) from '' (a real value) and keep '' in the IN list, or fall back to unscoped when any scanned row has an empty service name.
    • adversarial
  • packages/app/src/SessionsPage.tsx:485useSessions is destructured without its error field, so a filter that produces broken SQL surfaces as an empty "no matches" state (hasActiveSearch true, zero rows) rather than an error, hiding the failure from the user.
    • Fix: Read the query error and render a distinct error state instead of the empty-results state when the sessions query rejects.
    • adversarial
  • packages/app/src/sessions.ts:194 — The scope-enumeration query is awaited sequentially before the main Promise.all, adding a full ClickHouse round-trip (a 30-day DISTINCT scan) to every session load — including first paint and single-service deployments where the scope can never prune anything.
    • Fix: Lift the enumeration into its own long-staleTime query keyed on the session source so it is cached across loads and off the critical path, and skip emitting the predicate when it cannot help.
    • performance, adversarial
  • packages/app/src/sessions.ts:193 — The new useSessions branches (service-scope enumeration and its error fallback, and the filters-only hasSearchQuery → HAVING/CTE selection) have no integration coverage; only the pure buildServiceScopeFilters helper and the mirrored URL parser are tested.
    • Fix: Add a renderHook test with a mocked ClickHouse client asserting the success/empty/throw fallback paths and the three HAVING/finalQuery states.
    • testing, correctness, adversarial
🔵 P3 nitpicks (5)
  • packages/app/src/components/DBSearchPageFilters.tsx:1066 — Widening analysisMode/setAnalysisMode/showDelta/denoiseResults/setDenoiseResults from required to optional with voidFunc defaults trades a compile error for a silent no-op if a future full-UI caller forgets a setter.
    • Fix: Gate the mode props behind a discriminated union on hideAnalysisMode so they are required exactly when the analysis UI is shown.
  • packages/app/src/SessionsPage.tsx:120appliedConfig.filters is non-nullable via parseAsJsonEncoded(...).withDefault([]), yet it is guarded three inconsistent ways (?? [], ?? undefined, ?.length ?? 0), implying a null case that cannot occur.
    • Fix: Drop the coalescing or standardize on one form.
  • packages/app/src/__tests__/sessions.test.ts:123 — The ?filters= test re-declares the parser inline instead of importing the production appliedConfigMap.filters, so parser drift will not be caught.
    • Fix: Export the parser from a shared module and have both the page and test import it.
  • packages/app/src/sessions.ts:145 — The whereLanguage === 'promql' ? 'lucene' : whereLanguage branch is unreachable (the param is a sql | lucene enum) and would map promql to lucene incorrectly if ever reused.
    • Fix: Remove the dead branch or handle promql explicitly if the hook is meant to be reusable.
  • packages/app/src/SessionsPage.tsx:492hasActiveSearch duplicates the hasSearchQuery predicate inside sessions.ts; the two must stay in sync across files to keep the setup-vs-results gating consistent.
    • Fix: Export the predicate from sessions.ts and share it.

Reviewers (10): correctness, security, adversarial, performance, frontend-races, typescript, testing, maintainability, api-contract, previous-comments.

Testing gaps:

  • No test for a RUM service present in traces but absent from the session-source distinct list (partial-list under-inclusion), nor for the scoped-but-zero-match case.
  • No test for empty-string ServiceName being dropped from the scope list.
  • No test for switching session source with filters applied (carry-over vs reset).
  • No test asserting the useSessions error state is surfaced when a filter produces invalid SQL.
  • No test for the session-source serviceNameExpression diverging from the trace source's.

@vinzee
vinzee force-pushed the add-session-filters branch from fa84b72 to d81257b Compare August 25, 2026 01:22
Comment thread packages/app/src/sessions.ts
@vinzee
vinzee force-pushed the add-session-filters branch 2 times, most recently from 2b5bdd1 to 1901232 Compare August 25, 2026 14:43
kodiakhq Bot pushed a commit that referenced this pull request Aug 29, 2026
…gate (#3024)

## Summary

`claude-code-review.yml` has been disabled since **2026-05-20**, so this is a rebuild rather than a tweak. It replaces the prompt, moves it into a versioned file, and changes how findings reach the PR.

**The prompt was selected by measurement, not taste.** I built an offline harness that scores reviewer prompts against a frozen dataset of **122 hyperdx PRs where a human left a substantive inline review comment**, using those comments as ground truth. On the held-out test split (Opus, 49 PRs / 87 gold items):

| prompt | recall | cost/PR | agents |
| --- | --- | --- | --- |
| lifted-budget variant of the old prompt | 31% | $2.86 | 1 |
| **this prompt** | **40%** | **$3.11** | **1** |
| `deep-review` (currently in production) | ~22–33% | — | 6–13 |

On the larger shared slice this prompt and `deep-review` are within noise of each other, so **the claim is parity-or-better at one agent instead of six-to-thirteen — not a 4× improvement.** I'd rather understate that than have it quoted back later.

### What changes for reviewers

- **Findings post as inline comments on the changed lines.** 82% of findings anchor to a diff line (measured); the rest reference files the diff never touches and fall back to the sticky summary rather than being dropped — that class is a large share of the useful output.
- **One sticky summary**, updated in place, with counts and anything unanchored.
- **Repeat comments are suppressed across pushes** via a hidden per-finding fingerprint keyed on path + normalized title, so a reworded body doesn't repost.
- **Minor findings are folded behind `<details>`, not filtered.** On the dataset, minor-severity findings carry **29–57% of everything a human independently flagged** — filtering them would cost roughly half the recall. Severity is used for ordering only.

### Cost and correctness controls

- **Review gate.** Hashes the effective diff (`merge-base..HEAD`) and the prompt; skips when both are unchanged. `synchronize` fires on every "Update branch" merge, which advances the merge-base while leaving the diff byte-identical — without this, a large share of runs are pure waste at ~$3–5/PR. Same idea as `deep-review.yml`.
- **Fail-open.** The state marker is only stamped when the run produced parseable output; otherwise the next push retries instead of the gate caching a zero-finding review against that diff forever.
- **`concurrency` + cancel-in-progress**, so two quick pushes can't race the sticky comment or pay twice.
- **Model pinned to `opus`.** This prompt gained ~13 points from Opus where the old one gained ~5 — the extra passes only pay off at that tier, and an action-default change shouldn't silently swap the reviewer.

### Tool grants

Follows the pattern already set in `deep-review.yml`: a read-only `git`/`gh` prefix allowlist. Withheld deliberately, since this job checks out fork-authored code and holds secrets:

- **`gh api`** — accepts `--method POST`, and prefix allowlists can't constrain flags (your own note in `deep-review.yml`). Prior review threads are materialized in trusted shell instead, fenced and capped, and the prompt instructs the reviewer not to re-report them.
- **bare `Bash`** — would be arbitrary code execution on PR-author-controlled build scripts next to `ANTHROPIC_API_KEY`.
- **`WebFetch`/`WebSearch`/`curl`/`wget`** — data egress from a job holding secrets.

### Testing

`.github/scripts/review-comments.cjs` holds the comment-routing logic so it's testable without triggering a PR event. 11 tests, run by this workflow before the review step (same pattern as `pr-triage.yml`). They pin the two silent failure modes — a wrong line map, and a fingerprint that reposts — plus the fail-open contract, so nobody "tidies" the marker format and quietly disables the gate.

### Dry run on live PRs

Ran against #2992, #2981, #2980 (none in the eval set) without posting: 17 findings, 14 inline, 3 in summary, $10.86. Full output on request. The standout was a `critical` on #2981 that neither `greptile` nor `deep-review` reported — quoting every builder column makes `ORDER BY "count()"` unresolvable for unaliased projections.

## Merging this does not turn it on

The workflow is `disabled_manually`; merging the file doesn't change that. Enabling is a separate, deliberate step:

```
gh api -X PUT repos/hyperdxio/hyperdx/actions/workflows/claude-code-review.yml/enable
```

I'd suggest enabling it **alongside** `deep-review` for a couple of weeks (two comments per PR, distinct markers) and comparing on live PRs before deciding whether this replaces it. If it does, expect a net spend *decrease* — one agent instead of 6–13.

## Known gaps, stated plainly

- **Two changes here are unevaluated.** The harness measured the prompt with the diff *inlined* and with `Read/Grep/Glob` only. CI reads the diff from a file, and grants read-only `git`/`gh` plus materialized prior comments. Those should help, but the 40% figure doesn't cover them. Prior comments in particular are *unevaluable* in the harness — on the eval PRs those comments **are** the gold set.
- **Cost may run above $3.11/PR.** The dry run averaged $3.62 on mid-sized PRs. If the bill looks wrong, the natural lever is gating on the existing `review/tier-N` labels and only running this on tier-3/4.
- **One measured gap.** On the dry-run PRs, both `greptile` and `deep-review` independently caught failures involving *data persisted before the change* (stale saved sort strings) and this prompt caught neither. It has no pass for that class. That's the next thing I'd add.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Bring the search page's faceted filter sidebar to /sessions and wire the
selected filters into the session aggregation query.

- Add a SessionFilters wrapper around DBSearchPageFilters that hides the
  analysis-mode tabs, denoise, and root-spans-only toggles that don't
  apply to sessions.
- Persist sidebar filters in the URL and thread them through useSessions
  so they constrain the trace aggregation alongside the free-text query.
  Validate the ?filters= param against FilterSchema with a [] default so a
  stale/hand-edited param resolves to the default instead of white-screening.
- Speed up the aggregation: resolve the RUM service name(s) from the
  session source and constrain the otel_traces scan by ServiceName,
  turning a ~100k-mark full scan into a small primary-key range. Enumerate
  those services over a window wider than the selected range so sessions
  that started just before the range aren't dropped, and on any failure or
  when no services resolve, fall back to the unscoped (still correct) scan.
- Harden the ServiceName scope: escape ingested names with escapeSqlString
  (backslash then quote) in a dedicated buildServiceScopeFilters helper and
  build it inside the try/catch, so a crafted/backslash name can't inject
  into or break the aggregation.
- Fix the empty filter sidebar: scope facet queries to RUM session spans
  via a rum.sessionId indexHint and force exact facet mode, so the
  'show all values' path can't strip the scope and sample the whole trace
  table (which timed out and left the sidebar empty).
- Portal the source-select dropdown so it renders above the filter
  sidebar instead of being clipped by its stacking context.
- Add unit tests: SessionFilters prop forwarding, buildServiceScopeFilters
  escaping/fallback, and the ?filters= param validator.

Co-authored-by: Cursor <cursoragent@cursor.com>
@pulpdrew
pulpdrew force-pushed the add-session-filters branch from 1901232 to 2f51ae2 Compare September 1, 2026 19:18

// Applying a facet filter re-runs the query immediately (against the current
// time range), mirroring the search page's debounced auto-submit.
const handleSetFilters = useCallback(

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 — Clicking a sidebar facet resets the form and discards a typed-but-unsubmitted query

useForm({ values: {...} }) at SessionsPage.tsx:272 has no resetOptions, so RHF does a full reset whenever the values object changes. handleSetFilters calls setAppliedConfig({ filters }), which changes appliedConfig → changes values → resets where back to appliedConfig.where. Concretely: type error in the search box (don't press Run), click a ServiceName checkbox — the search box visibly reverts to empty and the query never runs. The comment claims this mirrors the search page, but DBSearchPage.tsx:1122 sets resetOptions: { keepDirtyValues: true } and its handleSetFilters (DBSearchPage.tsx:1296) calls debouncedSubmit() so the whole form is applied. Do the same here: add resetOptions: { keepDirtyValues: true } to the useForm call and make handleSetFilters call onSubmit() instead of writing only filters.

];
const hasSearchQuery = searchFilters.length > 0;

// Scope the trace aggregation to just the service(s) that emit RUM

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 — ServiceName scope derived from the session source can silently drop whole services' sessions

The scope list is built from a DISTINCT ServiceName scan of sessionSource.from (the rrweb table), then applied as a hard ServiceName IN (...) predicate on the trace aggregation. A service that emits RUM spans (rum.sessionId, component:"user-interaction") but writes no rrweb rows — session recording disabled in the browser SDK, recording not yet started, or a shorter TTL on the sessions table — contributes zero rows to that DISTINCT, so its sessions disappear from /sessions entirely, with no error and no UI signal. The try/catch only covers query failure, not a successfully-returned-but-incomplete list. Either derive the scope from the trace source itself (a cheap DISTINCT serviceNameExpression with the rum.sessionId predicate over the same range), or gate the whole optimization behind an explicit source/feature flag so correctness isn't traded away by default.

// which times out and leaves the sidebar empty. The `indexHint` lets the
// `rum.sessionId` skip index prune to just the session rows. This is applied
// as an always-on SQL filter alongside the user's search + facet selections.
const rumSessionIdKey = `${traceTrace.resourceAttributesExpression}['rum.sessionId']`;

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 — rumScopeFilter hardcodes Map access and mapContains, breaking JSON-typed ResourceAttributes

${traceTrace.resourceAttributesExpression}['rum.sessionId'] and mapContains(...) assume the column is a Map. The repo already has useFieldExpressionGenerator (packages/app/src/hooks/useFieldExpressionGenerator.tsx) for exactly this — sessions.ts:132 uses it to build traceSessionIdExpression for the same key, emitting toString(col.key) for JSON columns. On a JSON-typed ResourceAttributes the sidebar's facet query is invalid SQL, so every facet fetch fails and the user gets a red notification plus an empty sidebar. resourceAttributesExpression is also z.string().optional() (common-utils/src/types.ts:2279), so it can render as the literal undefined. Build the expression with useFieldExpressionGenerator(traceTrace) and apply the ?? 'ResourceAttributes' fallback used elsewhere in sessions.ts.

</>
)}
</>
<ActiveFilterPills

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 — In-pill value picker strips the RUM scope that forceExactFacetMode exists to preserve

ActiveFilterPills is handed filtersChartConfig, and FilterPill builds its value-picker config as { ...chartConfig, where: '', filters: [] } (packages/app/src/components/ActiveFilterPills.tsx:160). That clears rumScopeFilter, so clicking a pill on /sessions runs groupUniqArray over the entire trace table for the selected range — the exact unscoped query the PR added forceExactFacetMode to avoid. Carry the always-on scope outside filters (e.g. fold it into the config's where, or add a prop for a non-strippable base filter honoured by both useFacets' mode === 'all' branch and valueChartConfig) so the pill picker stays scoped.

sourceId: sourceId ?? null,
dateRange,
mode: showAllValues ? 'all' : 'exact',
mode: forceExactFacetMode || !showAllValues ? 'exact' : 'all',

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 — "Show all values" setting is inert on /sessions but still writes the shared localStorage key

With forceExactFacetMode, mode is always 'exact', yet FilterSettingsPanel (line 1661) still renders the showAllValues switch, wired to the shared hdx-show-all-filter-values key. On /sessions the toggle does nothing locally while silently changing the search page's facet behaviour. Add a prop to hide (or disable with an explanatory tooltip) the "Show all values" control when forceExactFacetMode is set.

@@ -1622,9 +1633,11 @@ const DBSearchPageFiltersComponent = ({
>
<Stack gap="sm" p="xs">
<Flex align="center" justify="space-between">

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 — Sidebar toolbar icons jump to the left edge when hideAnalysisMode is set

The header <Flex align="center" justify="space-between"> relies on the "Analysis Mode" Text as its first item. With hideAnalysisMode the Group holding the settings gear and the "Hide filters" collapse button is the only flex child, so space-between places it at flex-start — the controls render at the top-left of the sessions sidebar instead of the top-right where they sit on the search page. Use justify={hideAnalysisMode ? 'flex-end' : 'space-between'}.

style={{ flex: 1, minWidth: 0, minHeight: 0 }}
>
<Group gap={4} align="center" wrap="nowrap">
{isFilterSidebarCollapsed && (

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 — Show-filters button duplicates DBSearchPage's ExpandFiltersButton

The Tooltip + subtle ActionIcon + IconArrowBarToRight + aria-label="Show filters" block is a copy of ExpandFiltersButton in DBSearchPage.tsx:312 (differing only in icon/ActionIcon size). Export that component (or move it next to DBSearchPageFilters) and use it here so the two collapse affordances can't drift.

// failure — query or serialization — falls back to the unscoped (correct)
// scan rather than emitting broken/injectable SQL into the aggregation.
let serviceScopeFilters: Filter[] = [];
try {

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 — Service-name enumeration re-runs serially inside every sessions fetch

The DISTINCT ServiceName query is issued inside queryFn and awaited before the main Promise.all, so every time range change, facet toggle, or manual refresh pays an extra serialized round trip for a 30-day scan whose answer changes very rarely. Lift it into its own useQuery (keyed on sessionSource.id + a coarse window) with a long staleTime, and pass the resolved names into useSessions; the fallback-to-unscoped behaviour on error is unchanged.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

PR Review

10 finding(s): 🔴 0 critical · 🟠 4 major · 🔵 6 minor

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

Findings outside the changed lines

2 minor
  • 🔵 packages/app/src/SessionsPage.tsx:433Filters are not reconciled when the session source changessubmitOnSourceChange re-submits with the previous source's filters intact. Switching to a session source backed by a different trace table leaves filters keyed on columns that don't exist there, so the aggregation errors out. DBSearchPage handles this at DBSearchPage.tsx:1427-1449 by calling searchFilters.retainFiltersByColumns(...) once the new source's columns load and notifying about dropped filters; ActiveFilterPills also accepts invalidFields/invalidFieldReason for the ones it keeps. Wire up the same reconcile against traceColumns.
  • 🔵 packages/app/src/__tests__/sessions.test.ts:411New tests re-implement the production parser and mock away the behaviour under test → The ?filters= suite constructs its own parseAsJsonEncoded<Filter[]>(v => z.array(FilterSchema).parse(v)) instead of importing the one in appliedConfigMap (SessionsPage.tsx:258), so dropping the validator from SessionsPage would leave every test green; export the parser and assert against it. SessionFilters.test.tsx mocks DBSearchPageFilters entirely, so it only re-states the three literals the 3-line wrapper passes — it cannot catch the thing the wrapper exists for (facets actually being fetched in exact mode). Meanwhile the riskiest new logic — hasSearchQuery flipping the HAVING/CTE shape and the service-scope filter reaching the trace aggregation — has no test at all; a renderChartConfig-level assertion on the emitted SQL for the filters-only case would cover it.

Severity is the reviewer's own estimate and is used for ordering, not filtering.

Comment on lines +639 to +644
<ActiveFilterPills
searchFilters={searchFilters}
chartConfig={filtersChartConfig}
dateTimeColumns={dateTimeColumns}
style={{ flex: 1 }}
/>

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.

I think we could use some bottom margin here, and maybe some alignment of the left margin between these pills and the session card list

Image

{...props}
isLive={false}
hideAnalysisMode
forceExactFacetMode

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.

If we're forcing exact mode, we should avoid showing the option to toggle between exact and all mode

Image

Comment on lines +353 to +357
{
databaseName: traceTrace?.from?.databaseName ?? '',
tableName: traceTrace?.from?.tableName ?? '',
connectionId: traceTrace?.connection ?? '',
},

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.

Suggested change
{
databaseName: traceTrace?.from?.databaseName ?? '',
tableName: traceTrace?.from?.tableName ?? '',
connectionId: traceTrace?.connection ?? '',
},
tcFromSource(traceTrace)

Comment on lines +406 to +409
const rumScopeFilter: Filter = {
type: 'sql',
condition: `notEmpty(${rumSessionIdKey}) AND indexHint(mapContains(${traceTrace.resourceAttributesExpression}, 'rum.sessionId'))`,
};

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.

I believe this will probably break when the trace source is the JSON schema (where resource attributes is not a map but rather a JSON column) due to the mapContains.

Same thing above for the rumSessionIdKey using bracket syntax

.query({
query: serviceNamesQuery.sql,
query_params: serviceNamesQuery.params,
connectionId: sessionSource.connection,

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.

Suggested change
connectionId: sessionSource.connection,
connectionId: sessionSource.connection,
format: 'JSON',

{...props}
isLive={false}
hideAnalysisMode
forceExactFacetMode

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.

Two other reachable paths wipe chartConfig.filters too: DBSearchPageFilters/hooks.ts:225 (loadMoreFacetsForKey) and ActiveFilterPills.tsx:161 (in-pill value picker). "Show more values" and pill-editing therefore scan the whole trace table unscoped

Comment on lines +155 to +186
// Scope the trace aggregation to just the service(s) that emit RUM
// sessions. `otel_traces` is sorted by (ServiceName, SpanName, Timestamp),
// so with no ServiceName predicate the time filter can't prune via the
// primary index — the query scans the whole table's marks (~100k) to
// evaluate the `rum.sessionId` skip index. RUM traffic comes from a tiny
// set of services, which we read cheaply from the session source (sorted
// by time), turning the trace scan into a primary-key range (~50 marks,
// ~20x faster cold).
//
// Correctness assumption: the session and trace sources report the same
// `ServiceName` for RUM spans (the session source is the cheap,
// time-sorted projection of the same RUM traffic, which is why we read the
// list from there instead of re-scanning the trace table). If they diverge
// — a service present in trace RUM spans but absent from the session-source
// scan, or a differing service-name column — that service's sessions would
// be dropped. On any query failure or an empty result we fall back to the
// unscoped (correct) scan.
const serviceNameExpression =
traceSource.serviceNameExpression || 'ServiceName';
// Enumerate RUM services over a window wider than the selected range. A
// session's row in the session source is timestamped at (near) its start,
// which can fall *before* `dateRange` even though its trace spans land
// inside it. Scoping to only services seen within the exact range would
// then drop those sessions. Over-inclusion is safe — an extra ServiceName
// only widens the primary-key range scanned; the `rum.sessionId`
// predicate + HAVING still filter the rows — so we look back a generous
// margin. Under-inclusion is the correctness bug we must avoid.
const SERVICE_SCOPE_LOOKBACK_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
const serviceScopeDateRange: [Date, Date] = [
new Date(dateRange[0].getTime() - SERVICE_SCOPE_LOOKBACK_MS),
dateRange[1],
];

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.

This optimization seems like a separate change, with separate risks - could we split it out into it's own PR for discussion?

// only widens the primary-key range scanned; the `rum.sessionId`
// predicate + HAVING still filter the rows — so we look back a generous
// margin. Under-inclusion is the correctness bug we must avoid.
const SERVICE_SCOPE_LOOKBACK_MS = 30 * 24 * 60 * 60 * 1000; // 30 days

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.

I'm curious why this would be such a long lookback - it seems excessive to scan 30 days back before each sessions query. And since this is tied to the selected date range, if won't necessarily be cached when changing date ranges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants