Skip to content

fix: support service name expression and quick attribute filters in surrounding context#2558

Open
MikeShi42 wants to merge 10 commits into
mainfrom
cursor/fix-surrounding-context-filters-8b00
Open

fix: support service name expression and quick attribute filters in surrounding context#2558
MikeShi42 wants to merge 10 commits into
mainfrom
cursor/fix-surrounding-context-filters-8b00

Conversation

@MikeShi42

@MikeShi42 MikeShi42 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes the Surrounding Context feature for non-OTEL schemas (HDX-4665) and redesigns the filter UX with a hybrid preset + pill approach.

Problem: The old design had a segmented control (All/Service/Host/Custom) that only worked with OTel schema. Users with custom schemas saw only "All" and "Custom", losing the ability to quickly filter surrounding context.

Solution: A unified preset + pill system:

  1. "MATCH ON" presets — Segmented control (Anything/Service/Host/Pod/Node/Custom) at natural width acts as shortcuts that auto-select groups of related filter pills.
  2. Always-visible filter pills — All event attributes shown as toggleable pills with checkmark + yellow border when selected, + icon + dashed border when available.
  3. Smart mode switching — Manual pill toggles automatically flip the preset to "Custom" to indicate a non-preset selection. "Anything" means no filters.
  4. Service Name Expression support — Uses serviceNameExpression from the source config, making the service filter work with non-OTEL schemas.
  5. Review hardening — Escapes resource/event attribute values, avoids duplicate service pills, skips unsafe Lucene keys, falls back safely for non-bare Lucene service expressions, resets stale custom search text on row change, and uses the custom input's active query language.
  6. Test coverage — Adds unit/component tests for filter extraction, preset behavior, WHERE clause composition, escaping, stale row resets, and Custom-mode input visibility.

Screenshots or video

Default state — "Anything" selected, pills visible:
Default state

"Service" preset auto-selects service pills:
Service preset

Manual pill toggle flips to "Custom":
Custom mode

Demo video:
surrounding_context_polished_ux.mp4

How to test on Vercel preview

Preview routes: /search

Steps:

  1. Navigate to /search and select a time range with log data
  2. Click any log entry to open the side panel
  3. Click the "Surrounding Context" tab
  4. Verify "MATCH ON" control shows at natural width with "Anything" as first option
  5. Verify time displays as plain "±15s" text (no border)
  6. Click "Service" — verify it auto-selects service pills and shows "Matching on N"
  7. Click "Anything" — verify all pills are cleared
  8. Manually click a pill — verify the preset control flips to "Custom" and the custom search input appears
  9. Click "Clear all" — verify it resets to "Anything"

References

  • Linear Issue: HDX-4665
  • Related PRs: None

To show artifacts inline, enable in settings.

Linear Issue: HDX-4665

Open in Web Open in Cursor 

cursoragent and others added 2 commits June 30, 2026 20:51
…urrounding context

- Use serviceNameExpression from the source configuration for the Service
  filter instead of hardcoded ResourceAttributes['service.name']. This
  makes the Service filter work with non-OTEL schemas that use custom
  column names (e.g. ModuleName).
- Use resourceAttributesExpression from the source instead of hardcoded
  'ResourceAttributes' column name for Host/Pod/Node filters.
- Add quick event attribute filters: users can toggle attributes from the
  current event (resource attributes, event attributes, and top-level
  columns) to narrow down surrounding context results.
- Quick filters are additive (AND'd) with the selected context filter.

Co-authored-by: Mike Shi <mike@hyperdx.io>
Co-authored-by: Mike Shi <mike@hyperdx.io>
@changeset-bot

changeset-bot Bot commented Jun 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c97c9ca

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 Jun 30, 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 Jul 2, 2026 6:48pm
hyperdx-storybook Ready Ready Preview, Comment Jul 2, 2026 6:48pm

Request Review

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR redesigns the Surrounding Context filter UX from a segmented control that only worked with OTEL schemas to a preset + pill system that supports any schema's serviceNameExpression. It also fixes quote injection in formatAttributeClause/formatColumnEquals and properly resets filter state when switching between log rows.

  • New ContextFilterPills module — extracts available filter pills from rowData/source, exposes buildContextWhereClause, and renders interactive FilterPill components with a preset segmented control (Anything / Service / Host / Pod / Node / Custom).
  • ContextSidePanel refactor — replaces the old ContextBy enum and CONTEXT_MAPPING with the new pill system; adds a useEffect keyed on rowId to reset selected filters when the user opens a different event.
  • utils.ts hardeningformatAttributeClause and the new formatColumnEquals now properly escape single-quoted (SQL) and double-quoted (Lucene) values, with tests covering names like O'Brien.

Confidence Score: 4/5

Safe to merge for OTEL schemas; the one gap — a silent empty WHERE clause for non-OTEL sources with complex Lucene service expressions — affects a narrow edge case and does not corrupt data, only misleads the filter count display.

The core of the fix (state reset, preset+pill logic, quote escaping) is correct and well-tested. The only defect is in ContextFilterPills.tsx lines 78–91: when serviceNameExpression is a complex bracket-notation string that cannot be used as a Lucene field path and the row's ResourceAttributes also lacks service.name, the svc filter pill is shown and selectable but generateWhere(false) returns empty string. The Matching on N attributes header counts it, but the actual query receives no service filter. All OTEL schemas use 'ServiceName' which passes the safe-field check, so they are unaffected.

packages/app/src/components/ContextFilterPills.tsx — specifically the generateWhere closure inside extractQuickFilters for the svc pill (lines 78–91).

Important Files Changed

Filename Overview
packages/app/src/components/ContextFilterPills.tsx New file implementing filter pill extraction and preset logic. Contains a logic gap where the svc pill can generate an empty Lucene WHERE clause, silently misrepresenting query behavior to users on non-OTEL schemas in Lucene mode.
packages/app/src/components/ContextSidePanel.tsx Major redesign of the ContextSubpanel UI: replaces the old OTEL-only segmented control with a preset+pill system, adds useEffect to reset state on row change, and properly derives showCustomSearch from activePreset.
packages/app/src/utils.ts Adds formatColumnEquals and private SQL/Lucene escape helpers; retrofits proper quote escaping into formatAttributeClause. Field/column names are not escaped but are safe because callers validate them through isSafeAttributeKey or isSafeLuceneFieldExpression first.
packages/app/src/tests/utils.test.ts Adds quote-escaping tests for formatAttributeClause and new formatColumnEquals; coverage is thorough for the happy path and injection cases.
packages/app/src/components/tests/ContextFilterPills.test.ts Comprehensive unit tests covering dedup, escaping, unsafe keys, and fallback paths. Missing a test for the case where generateWhere(false) returns empty string (unsafe expression + no resourceServiceName).
packages/app/src/components/tests/ContextSidePanel.test.tsx Adds integration tests covering state reset on row change and custom-search visibility. Tests are correct given intended behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Row selected] --> B[extractQuickFilters]
    B --> C{serviceNameExpression set?}
    C -- Yes + value is string --> D[Add 'svc' pill]
    C -- No, but resourceAttrs.service.name exists --> E[Add 'ra:service.name' pill]
    D --> F[Add promoted RA keys: host, pod, namespace, node]
    E --> F
    F --> G[Add remaining RA keys]
    G --> H[Add event attribute keys]
    H --> I[Add top-level column keys]
    I --> J[availableFilters rendered as pills]

    J --> K{User action}
    K -- Clicks preset --> L[setActivePreset + getPresetFilterIds]
    K -- Clicks pill --> M[toggleFilter → setActivePreset 'custom']
    K -- Clicks Clear all --> N[setSelectedFilterIds empty + setActivePreset 'all']

    L --> O[buildContextWhereClause]
    M --> O
    N --> O

    O --> P{selectedFilterIds non-empty?}
    P -- Yes --> Q[generateWhere per filter]
    P -- No --> R[empty WHERE clause]
    Q --> S{Lucene + unsafe expr + no resourceServiceName?}
    S -- Yes --> T[generateWhere returns '' — silent no-op]
    S -- No --> U[Valid WHERE clause ANDed together]
    T --> V[DBSqlRowTable query]
    U --> V
    R --> V
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Row selected] --> B[extractQuickFilters]
    B --> C{serviceNameExpression set?}
    C -- Yes + value is string --> D[Add 'svc' pill]
    C -- No, but resourceAttrs.service.name exists --> E[Add 'ra:service.name' pill]
    D --> F[Add promoted RA keys: host, pod, namespace, node]
    E --> F
    F --> G[Add remaining RA keys]
    G --> H[Add event attribute keys]
    H --> I[Add top-level column keys]
    I --> J[availableFilters rendered as pills]

    J --> K{User action}
    K -- Clicks preset --> L[setActivePreset + getPresetFilterIds]
    K -- Clicks pill --> M[toggleFilter → setActivePreset 'custom']
    K -- Clicks Clear all --> N[setSelectedFilterIds empty + setActivePreset 'all']

    L --> O[buildContextWhereClause]
    M --> O
    N --> O

    O --> P{selectedFilterIds non-empty?}
    P -- Yes --> Q[generateWhere per filter]
    P -- No --> R[empty WHERE clause]
    Q --> S{Lucene + unsafe expr + no resourceServiceName?}
    S -- Yes --> T[generateWhere returns '' — silent no-op]
    S -- No --> U[Valid WHERE clause ANDed together]
    T --> V[DBSqlRowTable query]
    U --> V
    R --> V
Loading

Fix All in Claude Code Fix All in Conductor Fix All in Cursor Fix All in Codex

Reviews (7): Last reviewed commit: "fix: address surrounding context review ..." | Re-trigger Greptile

Comment thread packages/app/src/components/ContextSidePanel.tsx
Comment thread packages/app/src/components/ContextSidePanel.tsx Outdated
@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 224 passed • 3 skipped • 1515s

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

Tests ran across 4 shards in parallel.

View full report →

Address UX feedback:
- Remove confusing two-layer system (ContextBy segmented control +
  separate event filters). All filtering is now done via a single set
  of always-visible filter pills.
- Filter pills are always visible (no toggle/expand needed).
- Service, Host, Pod, Node are promoted as the first pills so they're
  easy to find.
- Selected pills show a clear X icon for easy removal.
- Property names no longer truncated (wider labels, larger pill maxWidth).
- Custom search is available via a search icon toggle.
- Multiple pills can be selected simultaneously (AND'd together).

Co-authored-by: Mike Shi <mike@hyperdx.io>
Split filter pill logic (extractQuickFilters, FilterPill component, and
helper functions) into a separate file to keep ContextSidePanel.tsx
under the 300-line limit.

Co-authored-by: Mike Shi <mike@hyperdx.io>
- Add useEffect to reset selectedFilterIds when rowId changes,
  preventing stale filters from carrying over between events.
- Remove export from formatColumnEquals (only used internally)
  to fix Knip unused-export CI check.

Co-authored-by: Mike Shi <mike@hyperdx.io>
Comment thread packages/app/src/components/ContextFilterPills.tsx Outdated
Implement the hybrid preset + pill design:
- MATCH ON segmented control (All/Service/Host/Pod/Node/Custom) acts
  as preset shortcuts that auto-select groups of related filter pills.
- Service preset selects the service pill; Pod selects service + pod +
  namespace; Host selects service + host; Node selects service + node.
- All available attribute pills are always visible below the presets.
- Users can manually toggle individual pills on top of or instead of
  presets for fine-grained control.
- 'Matching on N attributes' header with Clear all.
- Legend distinguishes matching (solid yellow border) from available
  (dashed border).
- Pills show checkmark when selected, plus when available.
- k8s.namespace.name added to promoted resource attributes for Pod
  preset support.

Co-authored-by: Mike Shi <mike@hyperdx.io>
Comment thread packages/app/src/components/ContextSidePanel.tsx Outdated
- Rename 'All' preset to 'Anything' for clarity.
- MATCH ON segmented control uses fit-content width instead of
  stretching 100%.
- Manual pill toggles flip the preset to 'Custom' to indicate a
  non-preset selection.
- Remove Badge border from the ±time display for visual consistency;
  show as plain text.
- Remove unused Badge import.

Co-authored-by: Mike Shi <mike@hyperdx.io>
Co-authored-by: Mike Shi <mike@hyperdx.io>
…, add tests

- Remove showCustomSearch state; derive from activePreset === 'custom'.
- Move formatColumnEquals to @/utils alongside formatAttributeClause
  (DRY: was the only local utility not shared).
- Add ErrorBoundary around filter pills section to limit blast radius.
- Add 19 unit tests for ContextFilterPills (extractQuickFilters,
  getPresetFilterIds, getAvailablePresets) covering OTEL/non-OTEL
  schemas, value escaping, promoted attributes, and preset logic.
- Add 2 unit tests for formatColumnEquals in utils.test.ts.

Co-authored-by: Mike Shi <mike@hyperdx.io>
@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label Jul 1, 2026
@github-actions

github-actions Bot commented Jul 1, 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: 647 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: 3
  • Production lines changed: 647 (+ 627 in test files, excluded from tier calculation)
  • Branch: cursor/fix-surrounding-context-filters-8b00
  • Author: MikeShi42

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 Jul 1, 2026

Copy link
Copy Markdown
Contributor

Deep Review

Scope: packages/app/src/{utils.ts, components/ContextFilterPills.tsx, components/ContextSidePanel.tsx} + tests, vs base 89f34db. Surrounding-context filter redesign (preset + attribute pills) and non-OTEL service filtering via serviceNameExpression.

Intent: Make surrounding-context filtering work on non-OTEL schemas and let users toggle event attributes as WHERE-clause filters. The new pill system builds ClickHouse SQL / Lucene predicates from the event's attribute keys and values.

🔴 P0/P1 — must fix

  • packages/app/src/utils.ts:1227escapeSqlValueSingleQuoted only doubles single quotes and never escapes backslashes, but ClickHouse honors C-style backslash escapes in string literals, so a value like \' OR 1=1-- renders as Column = '\'' OR 1=1--' and breaks out of the literal; this value path is now reachable from arbitrary event attributes via the new pills, widening the surface.
    • Fix: Escape backslashes before quotes (value.replace(/\\/g, '\\\\').replace(/'/g, "''")), reusing the existing canonical escapeSqlSingleQuoted (utils.ts:1166) / escapeString (packages/common-utils/src/filters.ts:16) rather than a weaker new helper.
    • ce-security-reviewer, orchestrator

🟡 P2 — recommended

  • packages/app/src/utils.ts:1231escapeLuceneDoubleQuoted has the same gap: it escapes " but not a preceding backslash, so \" OR field:secret closes the phrase early and injects Lucene logic that alters which rows return; downstream impact depends on the Lucene→SQL translator's own backslash handling.
    • Fix: Escape backslash first, then the double quote, in the Lucene escaper.
    • ce-security-reviewer, orchestrator
  • packages/app/src/components/ContextFilterPills.tsx:90 — for a non-OTEL Lucene source whose serviceNameExpression is a complex expression (fails isSafeLuceneFieldExpression) and whose row has no ResourceAttributes['service.name'], the svc filter's generateWhere returns '', so selecting the "Service" preset silently applies no predicate and surrounding context shows every event — a silent miss of the feature's stated goal.
    • Fix: When the service predicate cannot be built for the active language, disable/hide the Service preset or surface a visible "unavailable for this source" state instead of returning an empty clause.
  • packages/app/src/utils.ts:1227 — no test exercises the escaping helpers or formatColumnEquals with backslash / backslash-plus-quote values, so the injection gap above is uncovered despite new quote-escaping tests being added.
    • Fix: Add cases for values such as \, \', and \" OR 1=1-- to formatColumnEquals and formatAttributeClause tests.
    • ce-security-reviewer, orchestrator
🔵 P3 nitpicks (4)
  • packages/app/src/utils.ts:1227 — the added escapeSqlValueSingleQuoted duplicates the pre-existing escapeSqlSingleQuoted (utils.ts:1166) and escapeString (common-utils/filters.ts:16) but drops their backslash handling, creating a weaker third escaper in the same file.
    • Fix: Delete the new helper and route SQL value escaping through the existing canonical function.
  • packages/app/src/components/ContextFilterPills.tsx:191 — preset and filter IDs ('svc', 'ra:service.name', 'ra:k8s.pod.name', etc.) are string literals duplicated across MATCH_PRESET_IDS, getAvailablePresets, extractQuickFilters, and ContextSidePanel, so a rename in one place silently desyncs the presets.
    • Fix: Centralize the IDs as shared constants and reference them everywhere.
  • packages/app/src/components/ContextSidePanel.tsx:96activePreset and selectedFilterIds are typed as bare string / string[] rather than a preset/id union, allowing invalid values to type-check.
    • Fix: Introduce a ContextPreset union type and use it for the preset state and handlers.
  • packages/app/src/components/ContextSidePanel.tsx:151 — after toggling off the last selected pill, activePreset remains 'custom', so showCustomSearch stays true and the free-text box lingers even though zero attributes are selected.
    • Fix: Reset activePreset to 'all' when the last pill is deselected.

Reviewers (6): ce-security-reviewer, ce-correctness-reviewer, ce-testing-reviewer, ce-maintainability-reviewer, ce-adversarial-reviewer, ce-kieran-typescript-reviewer.

Coverage note: The primary finding was returned by the security reviewer and independently confirmed against the codebase (two existing correct escapers, utils.ts:1166 and common-utils/filters.ts:16, escape backslashes; the new helper does not). Findings from the correctness, testing, maintainability, adversarial, and typescript reviewers were still completing at synthesis time; the P2/P3 items above are orchestrator-verified against the diff.

Testing gaps:

  • No unit coverage for backslash-containing values in the escaping helpers (security-relevant).
  • No test for the empty-clause "Service" preset path on non-OTEL Lucene sources.

- Escape attribute filter values in formatAttributeClause for SQL and Lucene.
- Reset custom search form state when the selected row changes.
- Use the active custom where language when generating surrounding context queries.
- Avoid duplicate service.name pills when serviceNameExpression is available.
- Avoid generating Lucene clauses from non-bare serviceNameExpression values.
- Extract and test buildContextWhereClause for 0/1/many/custom clause paths.
- Add ContextSidePanel regression tests for custom input visibility and row-change reset.

Co-authored-by: Mike Shi <mike@hyperdx.io>
Comment on lines +78 to +91
generateWhere: isSql => {
if (isSql || isSafeLuceneFieldExpression(serviceNameExpr)) {
return formatColumnEquals(serviceNameExpr, serviceNameValue, isSql);
}
if (resourceAttrExpr && resourceServiceName) {
return formatAttributeClause(
resourceAttrExpr,
'service.name',
resourceServiceName,
isSql,
);
}
return '';
},

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.

P1 Service pill silently emits empty WHERE clause in Lucene mode

When serviceNameExpression fails isSafeLuceneFieldExpression (e.g., a bracket-notation expression like ResourceAttributes['service.name']) and resourceAttrs['service.name'] is absent, generateWhere(false) returns ''. buildContextWhereClause silently drops empty clauses, so selecting this pill records it in selectedFilterIds, shows "Matching on 1 attributes" in the header, but generates no actual WHERE condition — the query returns all logs exactly as if "Anything" were selected. Consider either omitting the svc pill when no safe Lucene clause can be produced, or surfacing a tooltip/warning when the selected filter is a no-op.

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

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

Labels

review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants