Repository navigation
fix(security): rebase the gascity SSO patch onto @hyperdx/app@2.42.0; stop the proxy secret leaking to logs and the ClickHouse proxy - #4
Conversation
## Summary This PR is part 1 of 3 in introducing alert-level name and tags. **Context**: We require alert-level name and tags so that the alerts page can sort, filter, and paginate alert documents by name and tags without joining the full alerts collection with the dashboard and saved search collections. In this PR: 1. Add `displayName` and `tags` to the alert model, types, and schemas. `displayName` is chosen because the existing `name` can be a handlebars template (confirmed that production data includes handlebars templates) and we don't want to show an unrendered handlebars template as an alert's name on the alerts page. 2. Functions for deriving an alert's name and tags from the dashboard tile or saved search it references, more or less matching how these values are derived today for the alerts page, and more or less matching this [backfill PR.](ClickHouse#3029) 3. When writing alerts through internal endpoints (create/update dashboard, create/update alert), if an explicit displayName or tags is provided in the request, use that value. If none is provided, write the name and tags derived from the referenced dashboard or saved search. 4. When reading /alerts or /alert/:id, return a stored displayName and tags if available, otherwise derive the displayName and tags from the referenced dashboard or saved search In future PRs: 1. Part 2: External API and MCP support. Notification titles use displayName if available (and not overridden by `name`) 2. Part 3: UI updated to allow the user to specify alert's name and tags directly. Alerts page + alerts details page updated to show displayName and tags 3. Part 4: Backfill displayName and tags for existing alerts 4. Part 5+: paginate the alerts page ### Screenshots or video <details> <summary>Create dashboard alert</summary> Request is sent without alert-level displayName or tags, the value saved in Mongo is derived from the dashboard+tile names, and the dashboard's tags <img width="800" height="363" alt="Screenshot 2026-09-03 at 8 30 53 AM" src="https://github.com/user-attachments/assets/350fc85c-7f71-4d5b-9963-c8360845349d" /> <img width="381" height="199" alt="Screenshot 2026-09-03 at 8 30 00 AM" src="https://github.com/user-attachments/assets/4e435321-7d4d-4654-9e37-48de77c3b87c" /> If displayName or tags are included in the request, they are persisted, instead of a derived displayName and tags: <img width="360" height="93" alt="Screenshot 2026-09-03 at 8 36 05 AM" src="https://github.com/user-attachments/assets/f0bea253-4453-4dfb-9787-7e1d00da66f4" /> </details> <details> <summary>Update dashboard alert</summary> If an existing tile alert has no persisted displayName or tags, and the request doesn't include one, then the derived values will be saved <img width="332" height="65" alt="Screenshot 2026-09-03 at 8 33 56 AM" src="https://github.com/user-attachments/assets/7ee69581-6eb4-47a0-88ee-2503ef429bc1" /> If an existing tile is updated and a displayName or tags are included in the request, they are persisted, instead of a derived displayName and tags: <img width="360" height="93" alt="Screenshot 2026-09-03 at 8 36 05 AM" src="https://github.com/user-attachments/assets/f0bea253-4453-4dfb-9787-7e1d00da66f4" /> </details> <details> <summary>Create saved search alert</summary> Derived values are saved in Mongo <img width="294" height="76" alt="Screenshot 2026-09-03 at 8 44 56 AM" src="https://github.com/user-attachments/assets/295aa546-05d2-4ef7-9969-b19dbc0d2fd7" /> <img width="553" height="269" alt="Screenshot 2026-09-03 at 8 45 20 AM" src="https://github.com/user-attachments/assets/4b904ca3-309e-4a6a-b32a-5fb52c7cf821" /> </details> <details> <summary>Update saved search alert</summary> Derived values are saved in Mongo, if no displayName/tags are provided in the request <img width="553" height="269" alt="Screenshot 2026-09-03 at 8 45 20 AM" src="https://github.com/user-attachments/assets/71c53bbd-673b-4ff7-b9b7-45488187f829" /> <img width="259" height="58" alt="Screenshot 2026-09-03 at 8 47 00 AM" src="https://github.com/user-attachments/assets/a63487da-bb2b-4cae-8090-2e8ccf41c0a4" /> </details> <details> <summary>GET alerts</summary> Derived values are returned for displayName / tags if not present in the DB <img width="915" height="911" alt="Screenshot 2026-09-03 at 8 48 20 AM" src="https://github.com/user-attachments/assets/076460e3-de1f-4a69-9f43-a6f8a92c9422" /> Persisted values are returned, if present <img width="884" height="362" alt="Screenshot 2026-09-03 at 8 49 45 AM" src="https://github.com/user-attachments/assets/894032b8-cee9-405f-93da-b757d6dac023" /> </details> <details> <summary>GET alert/:id</summary> Derived values are returned if no persistent values exist <img width="732" height="444" alt="Screenshot 2026-09-03 at 8 50 26 AM" src="https://github.com/user-attachments/assets/622b12e6-de79-4710-99be-65aad62d7fec" /> Persisted values are returned if present <img width="759" height="444" alt="Screenshot 2026-09-03 at 8 50 40 AM" src="https://github.com/user-attachments/assets/69ed40a7-d8fd-40d8-bc99-546646a40bd7" /> </details> ### How to test locally Create and update alerts on dashboard tiles and saved searches, observe the displayName and tags values persisted in mongodb. Observe the response from the `/alerts` and `/alerts/:id` internal endpoints, they should include a derived displayName and tags, or the stored value if it's available. ### References - Linear Issue: Related to HDX-5116 - Related PRs:
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @hyperdx/cli@0.6.3 ### Patch Changes - 53336f7: fix(charts): show decimals on Y-axis ticks under 10 in magnitude The Y-axis of a time series chart (and the CLI's termchart equivalent) always rounded tick labels to 0 decimal places, regardless of the chart's Number Format settings. Charts whose values live under 1 (fractional gauges, ratios, sub-1 rates) rendered every axis tick as `0` even though the tooltip and legend showed the correct value. A tick under 10 in magnitude (as displayed - a percent tick's magnitude is checked against its ×100 value, not its raw 0-1 ratio) now honors the chart's configured decimals, capped at 2 to keep the label within the axis's fixed width. A tick of 10 or more, and a tick of exactly 0, stay integers exactly as before, whatever the chart's Number Format configures - so ordinary counts and the byte/percent tiles in the bundled dashboard templates are unaffected. ## @hyperdx/api@2.38.0 ### Minor Changes - 74c28e7: feat: Alerts now persist their own `displayName` and `tags` - 9c4f94f: Add enriched template variables to Generic and incident.io webhook bodies: `{{alertId}}`, `{{status}}`, `{{alertType}}`, `{{comparator}}`, `{{threshold}}`, `{{value}}`, `{{groupKey}}`, `{{sourceQuery}}`, `{{teamId}}`, `{{note}}`, and ISO-8601 `{{startTimeISO}}` / `{{endTimeISO}}` alongside the existing Unix-ms `{{startTime}}` / `{{endTime}}`. Receivers can now route, filter and dedupe on an alert's identity and condition without parsing the rendered message body. Existing templates are unaffected — every new variable is additive and renders empty when an alert doesn't carry it. - f0d1cef: Support inline chart alerts (source `inline` + `chartConfig`) in the external API v2 and the MCP `clickstack_save_alert` / `clickstack_get_alert` tools. Inline alerts can now be created, updated, listed, and deleted through `/api/v2/alerts` using the same tile-config dialect as v2 dashboards, with the same validation rules as the internal API: display-type allowlist, metric formula validation, the formula source-kind gate, raw SQL template validation, and team-scoped source/connection ownership. Passing a `chartConfig` to a `tile` or `saved_search` alert is now rejected instead of silently dropped, and reading an inline alert whose config carries internal-only fields omits `chartConfig` rather than returning a lossy approximation. Also fixes three pre-existing issues on the external v2 dashboards path: a gauge tile saved through `clickstack_save_dashboard` with `isDelta: true` was persisted as a non-delta (the flag was dropped converting MCP tiles to the internal shape); a tile config carrying an unrecognized `configType` (e.g. `promql`) was silently stored as a builder config and skipped the formula and number-select rules, and is now rejected; and validation errors on alert bodies no longer collapse to `Invalid input` — the schema reports the failing branch's own message. - d9c5c45: fix: preserve a Connection host path prefix when proxying PromQL. `proxyToPrometheus` joined absolute Prometheus paths (`/api/v1/query_range`, `/api/v1/query`, `/api/v1/query_exemplars`, `/api/v1/label/.../values`) with `new URL(path, host)`, which replaces the host pathname instead of appending to it. VictoriaMetrics cluster `vmselect` URLs such as `http://vmselect:8481/select/0/prometheus` were rewritten to `/api/v1/query_range` and rejected. The join now keeps the existing pathname. This is a behavior change for Connections whose host already included a path that was never meant as a Prometheus API prefix — for example `http://prom:9090/graph` copied from the Prometheus UI. That previously happened to work because the absolute API path replaced `/graph`; requests now go to `/graph/api/v1/query_range` and will 404. Trim stray paths from existing Connection hosts before upgrading. Root-mounted hosts (`http://prom:9090` or `http://prom:9090/`) are unchanged. Query parameters on the Connection host are now only a fallback for a fixed set of real Prometheus API params (`query`, `time`, `start`, `end`, `step`, `match`/`match[]`, `limit`, `timeout`, `stats`): a request value for one of these (including repeatable ones such as `match[]`) always wins and replaces a same-named host value outright, rather than being dropped. Any other host query key the request never mentions -- for example `?extra_label=namespace%3Dprod` pinning a VictoriaMetrics tenant scope -- is left as-is and is never overridable by the request, since a param name outside that fixed set is not forwarded at all regardless of what the host carries. This also means a host copied with a stray query string (not just a stray path) now forwards its non-Prometheus keys upstream as a fallback on every request -- trim those too if they weren't intended as Prometheus API params. This is also a behavior change for a direct API caller (e.g. curl or Terraform) that previously relied on sending an arbitrary, non-Prometheus query param through this endpoint: that param is now silently dropped rather than forwarded, regardless of whether the Connection host carries anything under the same name. ### Patch Changes - 25a3b01: Fix `{{sourceQuery}}` returning empty for inline-query and dashboard-tile alerts. It read only the saved search's filter, so alerts backed by a chart config — where the query lives on the alert or the tile — advertised a variable that never rendered. It now resolves the query from whichever config backs the alert: the builder `where` or the raw `sqlTemplate`. Add `{{thresholdMax}}`, the upper bound of a `between` / `outside` condition. Receivers previously saw only the lower bound and could not reconstruct the range that fired. It renders empty for every other comparator. Test Webhook now sends a sample value for every template variable. It carried only the original seven, so a body using an enriched variable rendered it empty — and because `threshold`, `thresholdMax` and `value` are emitted unquoted, a body like `{"value": {{value}}}` was sent as `{"value": }` and rejected, failing the test for a template that works on a real firing. - 808b345: Accept `Bearer `-prefixed Authorization header values on the OTel ingest endpoint in OpAMP-managed mode. The collector's bearer-token authenticator matches the full header value exactly and was configured with only the bare API key, so RFC 6750 clients that send `Authorization: Bearer <token>` were rejected. The generated collector config now also accepts `Bearer`, `bearer`, and `BEARER` prefixed forms of each ingestion API key. - bf4443d: feat: Support autocomplete for PromQL label filters - 55db91f: feat: Support static filters in MCP - 89a897e: Fixed single-series histogram charts failing with "Unknown expression or function identifier" when sorted by a group-by column or expression. The histogram translation packs group values into a single `group` Array, so the table default ORDER BY (the raw group-by text) referenced source columns that no longer exist in scope; matched sort items now address the packed array positionally. - f1062a7: Fixed multi-series metric charts failing with "Unknown expression or function identifier" when sorted by an expression group-by (e.g. `ResourceAttributes['service.name']`). Table tiles default their ORDER BY to the group-by text, so any multi-series metric table grouped by a resource/attribute-derived expression failed to render. Such sort expressions are now evaluated inside each per-series branch through internal companion columns instead of being re-evaluated in the composed outer query, where the source columns no longer exist. - 4184a89: feat: Support dashboard filters based on Prometheus label values - 2736003: feat: Support series filter (matcher) in PromQL label dashboard filters - 7ed8dc8: feat: Evaluate Prometheus `match[]` series selectors on the labels and label values endpoints for ClickHouse TimeSeries connections - bcf0257: feat: Accept optional time bounds on Prometheus label values endpoint - cff6388: feat: Add static filters to schemas and APIs - Updated dependencies [74c28e7] - Updated dependencies [b917308] - Updated dependencies [55db91f] - Updated dependencies [89a897e] - Updated dependencies [f1062a7] - Updated dependencies [4184a89] - Updated dependencies [2736003] - Updated dependencies [c796592] - Updated dependencies [cff6388] - @hyperdx/common-utils@0.28.1 ## @hyperdx/app@2.38.0 ### Minor Changes - 503aac8: Show GPU utilization and GPU memory utilization charts in the log/span side panel Infrastructure section when `hw.gpu.*` metrics (OTel hardware semconv) exist for the correlated host/node. Multiple GPUs on a host render as separate series grouped by `hw.id`, and utilization is split per GPU engine by `hw.gpu.task` (general/encoder/decoder) so a node saturated on video encode is still visible; a missing task is reported as `general`. The section is fully hidden when no GPU metrics are present and partially rendered when only one metric is available. The Infrastructure tab now also treats a Kubernetes resource attribute that is present but empty (for example `k8s.node.name: ""`) as absent. Such rows previously surfaced an Infrastructure tab that could render nothing. Fix GPU chart availability leaking across rows: switching the side panel to a row on a different host briefly rendered the previous host's set of GPU charts, because the availability query keeps the prior result readable while the new one runs. - 38e99a3: Add a read-time, schema-agnostic LLM observability dashboard at `/llm` (beta, linked from the dashboards list) — no ingestion changes required, and it works retroactively on already-ingested telemetry. Traces instrumented with the OTel GenAI semantic conventions, OpenLLMetry, OpenInference, or the Vercel AI SDK are interpreted at query time to chart traffic, token split (uncached/cached/cache-write/output/reasoning), estimated cost by model (bundled price catalog with cache-read discounts and cache-write premiums; an instrumentation-provided cost attribute always wins), latency/TTFT, tool analytics, per-session timelines with lazy-loaded conversation views, and side-by-side LLM span/log search. Aggregations elect an app's own cost-reporting spans as the authoritative per-call reporters so apps emitting several instrumentation dialects in parallel (e.g. opencode emitting OpenInference and Vercel AI spans for each call) are counted once — verified to match opencode's self-reported session cost exactly. ### Patch Changes - 3f7934a: chore: Add isSourceAllowed prop to SourceSelectControlledComponent - b917308: fix: metric names in the chart editor are now listed deterministically instead of sampled. The dropdown discovered names with `groupUniqArray(3000)(MetricName)`, which keeps an arbitrary subset once a metrics table holds more than 3000 distinct names — the survivors follow hash order, not name order — so metrics that exist and are actively reporting could be unselectable, with no warning and no way to search for what had been dropped. Names are now fetched with an ordered, paginated query and matched server-side, ranked so an exact match is always on the first page, and the dropdown says when the list is incomplete. Also fixes the metric list ignoring the chart's selected time range, which pinned it to the last 24 hours. - bf4443d: feat: Support autocomplete for PromQL label filters - 55db91f: feat: Support static filters in MCP - 89a897e: Fixed single-series histogram charts failing with "Unknown expression or function identifier" when sorted by a group-by column or expression. The histogram translation packs group values into a single `group` Array, so the table default ORDER BY (the raw group-by text) referenced source columns that no longer exist in scope; matched sort items now address the packed array positionally. - fbb1e20: Preserve conditional color rules for SQL and PromQl chart configs - 020e85f: Fix two problems with the chart editor's metric name picker. The truncation and load-failure notices now render below the input instead of above it, so they no longer push the field down out of alignment with the browse-metrics button beside it, and both were shortened to fit the tile editor's column without wrapping. A search that matches nothing now says so rather than silently hiding the dropdown, and offers the searched name as a selectable option — the catalog only covers the most recent three days of the chart's time range, so a metric that stopped reporting was previously impossible to chart by name. - f1062a7: Fixed multi-series metric charts failing with "Unknown expression or function identifier" when sorted by an expression group-by (e.g. `ResourceAttributes['service.name']`). Table tiles default their ORDER BY to the group-by text, so any multi-series metric table grouped by a resource/attribute-derived expression failed to render. Such sort expressions are now evaluated inside each per-series branch through internal companion columns instead of being re-evaluated in the composed outer query, where the source columns no longer exist. - cdcb023: fix: Reliably show service name and level in pattern sample drawer - 0ebb689: fix: De-dupe PromQL labels and values to prevent Mantine crash - 4184a89: feat: Support dashboard filters based on Prometheus label values - 2736003: feat: Support series filter (matcher) in PromQL label dashboard filters - bcf0257: feat: Accept optional time bounds on Prometheus label values endpoint - c796592: feat: Support a custom template for PromQL series legends - 83c1b57: feat: Display static value filters - 66f6cf0: Add a `window.hdx` browser-console debug handle and a "Copy debug info" action in the Help menu, so you can confirm which build is deployed and grab the context worth attaching when filing an issue. `window.hdx.report()` (and the Help menu action) produces a pasteable summary: frontend version (from package.json), backend/API version (from `/api/health` — the two deploy separately), deployment mode, user/team ids, enabled env-configurable feature flags, current URL, screen/viewport/OS/browser info, and the RUM session id. The handle is installed once and reads its async fields (server version, identity, features, session id) live via getters. - cff6388: feat: Add static filters to schemas and APIs - 53336f7: fix(charts): show decimals on Y-axis ticks under 10 in magnitude The Y-axis of a time series chart (and the CLI's termchart equivalent) always rounded tick labels to 0 decimal places, regardless of the chart's Number Format settings. Charts whose values live under 1 (fractional gauges, ratios, sub-1 rates) rendered every axis tick as `0` even though the tooltip and legend showed the correct value. A tick under 10 in magnitude (as displayed - a percent tick's magnitude is checked against its ×100 value, not its raw 0-1 ratio) now honors the chart's configured decimals, capped at 2 to keep the label within the axis's fixed width. A tick of 10 or more, and a tick of exactly 0, stay integers exactly as before, whatever the chart's Number Format configures - so ordinary counts and the byte/percent tiles in the bundled dashboard templates are unaffected. - 25a3b01: List every supported template variable in the webhook form, including the enriched set added to Generic and incident.io bodies (`{{alertId}}`, `{{status}}`, `{{alertType}}`, `{{comparator}}`, `{{threshold}}`, `{{thresholdMax}}`, `{{value}}`, `{{groupKey}}`, `{{sourceQuery}}`, `{{teamId}}`, `{{note}}` and ISO-8601 `{{startTimeISO}}` / `{{endTimeISO}}`). Each variable now carries a one-line description, so a webhook body can be written without leaving the form. - 0a18745: feat: Allow creation and editing of static list filters in the UI - Updated dependencies [74c28e7] - Updated dependencies [25a3b01] - Updated dependencies [9c4f94f] - Updated dependencies [808b345] - Updated dependencies [b917308] - Updated dependencies [bf4443d] - Updated dependencies [55db91f] - Updated dependencies [89a897e] - Updated dependencies [f0d1cef] - Updated dependencies [f1062a7] - Updated dependencies [4184a89] - Updated dependencies [2736003] - Updated dependencies [7ed8dc8] - Updated dependencies [bcf0257] - Updated dependencies [c796592] - Updated dependencies [d9c5c45] - Updated dependencies [cff6388] - @hyperdx/api@2.38.0 - @hyperdx/common-utils@0.28.1 ## @hyperdx/common-utils@0.28.1 ### Patch Changes - 74c28e7: feat: Alerts now persist their own `displayName` and `tags` - b917308: fix: metric names in the chart editor are now listed deterministically instead of sampled. The dropdown discovered names with `groupUniqArray(3000)(MetricName)`, which keeps an arbitrary subset once a metrics table holds more than 3000 distinct names — the survivors follow hash order, not name order — so metrics that exist and are actively reporting could be unselectable, with no warning and no way to search for what had been dropped. Names are now fetched with an ordered, paginated query and matched server-side, ranked so an exact match is always on the first page, and the dropdown says when the list is incomplete. Also fixes the metric list ignoring the chart's selected time range, which pinned it to the last 24 hours. - 55db91f: feat: Support static filters in MCP - 89a897e: Fixed single-series histogram charts failing with "Unknown expression or function identifier" when sorted by a group-by column or expression. The histogram translation packs group values into a single `group` Array, so the table default ORDER BY (the raw group-by text) referenced source columns that no longer exist in scope; matched sort items now address the packed array positionally. - f1062a7: Fixed multi-series metric charts failing with "Unknown expression or function identifier" when sorted by an expression group-by (e.g. `ResourceAttributes['service.name']`). Table tiles default their ORDER BY to the group-by text, so any multi-series metric table grouped by a resource/attribute-derived expression failed to render. Such sort expressions are now evaluated inside each per-series branch through internal companion columns instead of being re-evaluated in the composed outer query, where the source columns no longer exist. - 4184a89: feat: Support dashboard filters based on Prometheus label values - 2736003: feat: Support series filter (matcher) in PromQL label dashboard filters - c796592: feat: Support a custom template for PromQL series legends - cff6388: feat: Add static filters to schemas and APIs ## @hyperdx/otel-collector@2.38.0 ### Patch Changes - 808b345: Accept `Bearer `-prefixed Authorization header values on the OTLP receiver in standalone mode (`OTLP_AUTH_TOKEN`). Previously only the bare-token form of the header was accepted, rejecting RFC 6750 clients that send `Authorization: Bearer <token>`. The `Bearer`, `bearer`, and `BEARER` prefixed forms are now accepted alongside the bare token.
…use#3044) ## Summary - Adds the OpenTelemetry `spanmetricsconnector` to HyperDX's custom collector build. - Enables users to derive span-based RED metrics — request count, errors, and latency — from ingested traces (most useful for Datadog Agent traces, where these metrics aren't otherwise available in the current HyperDX ingestion path). - Supports exponential histograms, which HyperDX already stores natively. - The change is additive and opt-in; it does not modify any default collector pipelines or runtime behavior. - Documents how to wire the connector in both standalone and OpAMP-managed deployments. - Includes the required changeset for `@hyperdx/otel-collector`. ## Test plan - Built the collector using the repo's documented Docker build flow. - Verified `spanmetricsconnector v0.155.0` is present in the resulting binary. - Validated the documented standalone and OpAMP configurations against the compiled collector. <details> <summary>Implementation notes / OpAMP wiring details</summary> In OpAMP mode, `receivers`/`exporters` on the default `traces`/`metrics` pipelines are supplied dynamically and overwrite the corresponding values from `CUSTOM_OTELCOL_CONFIG_FILE`. Because of that, span-metrics wiring must use separate pipeline names rather than modifying the default pipelines. The documented config sets a few fields beyond the bare minimum: `namespace` (pins the connector's own default for explicitness), `histogram.exponential.max_size` (opts into the exponential histogram this PR is motivated by, vs. the connector's default explicit-bucket histogram), `aggregation_cardinality_limit` + `series_expiration` (bounds the connector's own in-memory state, which `memory_limiter` doesn't cover), and `aggregation_temporality: DELTA` (cumulative temporality plus expiration can reset a low-traffic series' counter in a way HyperDX's rate SQL doesn't detect; delta avoids the issue). Two specific claims came up more than once across review and were checked directly against the pinned v0.155.0 source and the compiled binary — both turned out to be incorrect, noted here so they aren't re-litigated: the connector's canonical config key is `span_metrics` (`spanmetrics` is a working deprecated alias, not the other way around), and `series_expiration` is a real, distinct field from `metrics_expiration`. </details> ## Follow-up An end-to-end smoke test that emits a span and asserts generated `calls`/`duration` metrics reach ClickHouse would provide additional coverage beyond collector config validation.
) ## Summary This PR introduces support for marking dashboard filters (variables) as required, indicating that some or all of the dashboard should not load until values are selected for the filters. Two new fields across each filter type: 1. `minSelections`: a value of 1 indicates that the filter is required. I chose a number so that (a) we can expose a consistent `maxSelections` in the future for capping selections and (b) we can express a minimum >1 in the future if we want to 2. `isGlobalRequirement`: true indicates that the filter is required for any/every tile on the dashboard to load. The default (false) implies that only tiles that reference the required filter as a variable or receive a broadcasted value from the filter are blocked by the requirement. Note: dependent filters are not yet blocked by required filters that they depend on. ### Screenshots or video https://github.com/user-attachments/assets/fba44693-7677-450e-a196-fa2b4c19b297 ### How to test on Vercel preview 1. Create a dashboard 2. Add some tiles and some filters 3. Play around with the new required settings, variable references, and broadcasting settings ### References - Linear Issue: Closes HDX-5292 - Related PRs:
## Summary This PR updates the team/tags endpoint to return tags from Alert documents, in addition to Dashboards and Saved Searches (existing behavior). Now that Alerts can be tagged independently of dashboards and saved searches, this is necessary so that if a tag is use exclusively by an alert, that tag appears in the suggested tags for other alerts. There is no existing index on tags in Dashboards or Saved Searches, so this update is consistent with that for alerts. ### Screenshots or video <img width="289" height="364" alt="Screenshot 2026-09-08 at 9 50 51 AM" src="https://github.com/user-attachments/assets/211e67aa-6c04-431d-ba32-abdbb36dcc82" /> ### How to test locally 1. Create an alert and add a new tag to it 2. Create another alert and notice that the tag added to the first alert is present in the tag dropdown ### References - Linear Issue: Closes HDX-5287 - Related PRs:
…kHouse#3093) ## Summary This PR adds an option in the dashboard tile editor that propagates the current dashboard filter and variable selections into the chart preview. This is toggled on by default, and disabled when the user toggles it off or when there is an alert configured on the tile (since alerts evaluate without any filters). Motivation: 1. Variables and core filters (EE-only, currently) were already propagated but broadcast filters weren't; this was an inconsistency 2. Now that we have required filters (ClickHouse#3078), it may be desirable for performance reasons to encourage the user to apply required filters when editing the tile. This will not fully block the user from making unfiltered queries, as that requires further modifications which are the domain of core filters in EE. ### Screenshots or video https://github.com/user-attachments/assets/451613ba-0e8a-4d9f-93ad-23056228e78b ### How to test on Vercel preview 1. Create a dashboard, add a filter 2. Open the tile editor under different combos of filter-required modes, variable/broadcast modes, sources, configured alerts, display types, and play around with the toggle. ### References - Linear Issue: Closes HDX-5325 - Related PRs:
## Summary Updates the trace schema to use text indexes instead of bloom filters or tokenbf_v1. ### References - Linear Issue: Closes HDX-5337
…d copy (ClickHouse#3098) ## Summary Addresses two comments from #3903: 1. Update the copy in the tile editor when the preview is blocked by required variables to point the user towards turning off Apply Filters as an option to bypass the block 2. Extract a shared getBlockingRequiredFilterNames function wrapping two similar calls to `getBlockingRequiredFilters` ### Screenshots or video New Copy: <img width="2069" height="243" alt="Screenshot 2026-09-09 at 7 54 32 AM" src="https://github.com/user-attachments/assets/2269ff9e-1e56-4480-afcd-10a605560231" /> ### How to test on Vercel preview 1. Create a dashboard, add a filter and set it to required 2. Create a tile that uses that filter, check that the preview is disabled with the new message ### References - Linear Issue: - Related PRs:
…s has no nested _id (ClickHouse#3099) ## Summary Fixes ClickHouse#3094 Fixes a fetch / mount loop when viewing the trace waterfall panel on sources with metadata MVs. The issue was that Mongo would add a freshly generated `_id` to each metadataMaterializedViews object, when the persisted source had none. This _id is part of the object that exists in the react query key for various queries, so when it changes, old queries are canceled and new ones start. This led to the UI switching from loading to loading state, unmounting and remounting components as the loading state toggled back and forth. The fix removes the _id from the metadataMaterializedViews sub-schema so that a fresh one is not generated on each read. Reproduction: 1. Create a traces source and add metadata MVs. Save 2. Change the source's type to Log. Save. 3. Change the source's type back to Trace. Fill in any fields that were removed. Save. 4. Navigate to the waterfall for the source. ### Screenshots or video **Before:** Fetch/mount loop: https://github.com/user-attachments/assets/299c8eeb-7cc0-43fa-8cc5-e8be84419e99 **After:** Waterfall loads: <img width="2558" height="1235" alt="Screenshot 2026-09-09 at 11 52 16 AM" src="https://github.com/user-attachments/assets/dd5c22a9-afe4-494f-829c-342e2f1aa748" /> ### References - Linear Issue: - Related PRs:
…e#3069) ## Summary [HDX-5090](https://linear.app/clickhouse/issue/HDX-5090/support-creating-alerts-without-saved-searches-or-dashboard-tiles)'s backend ([ClickHouse#3010](ClickHouse#3010)) added `AlertSource.INLINE` — alerts that persist their own `chartConfig` and evaluate with no saved search or dashboard tile behind them. The internal `/alerts` API accepts and returns them, but no UI could create or edit one. This is that UI. The motivating case is Epidemic Sound migrating 1000+ Grafana rules: today every alert needs a saved search (logs) or a dashboard tile (metrics) created first, which does not scale. **Chart explorer** (`/chart`) - The alert affordances are gated on a new `enableAlerts` prop rather than on being inside a dashboard (previously `dashboardId != null`), and a **Create alert** action in the action bar POSTs `{ source: 'inline', chartConfig, ...alertFields }`. Works for builder configs (log/trace/metric) and raw SQL, on Line / Stacked bar / Number — the display types the evaluator supports. - Naming comes from the shared `AlertDisplayFields` (name + tags) that ClickHouse#3063 added to the alert editor, so an inline alert is labelled the same way as any other. Left blank, the server derives the name from the chart config — it already handles `AlertSource.INLINE` in `deriveAlertDisplayFields`. - On success the alert is dropped from the explorer's URL config — it now lives on the alert document, and leaving it there would offer to create a second copy. **Alerts list** - Inline alerts render their name with a chart icon and link to the chart explorer as `/chart?alertId=<id>`, which the explorer resolves into the persisted query. By id rather than by an inlined config because `GET /alerts` omits `chartConfig` — a config-carrying link would leave every list row without one whenever the alert detail page is disabled. It also opens the query as it stands now rather than a snapshot. (The notification link still inlines the config: it is built server-side, with no session to fetch with, and lands on the same chart.) - The row name renders as plain text when no destination resolves instead of a link to an empty `href`. That also covers a pre-existing case: a tile alert whose dashboard was deleted, with the details page disabled. - Banner and empty-state copy now mention the chart explorer. **Alert detail page** - The chart renders from the persisted `chartConfig`. The tile variant's config assembly is extracted into a shared `buildAlertChartConfig`, so the two previews cannot drift. - Editing opens the full chart editor seeded from the alert (`EditInlineAlertModal`), so the query and the alert's threshold/channels/schedule are changed in one place, then split apart again on save. The remove-alert control is hidden there, and "Save to dashboard" is suppressed — saving that chart as a tile would copy its alert onto the tile, leaving two alerts on one query. - `GET /alerts` deliberately omits `chartConfig`, so an alert opened from a list row fetches its detail response before seeding the editor. - `EditAlertModal` keeps handling saved-search and tile alerts. An inline alert routed there would previously have fallen through to the saved-search branch and been rewritten as a saved-search alert with no saved search; it now round-trips its config instead. **Not in scope** (matching the backend PR): External API v2, MCP write support, and Terraform for inline alerts. ### Screenshots or video | Before | After | | :----- | :---- | | | | ### How to test on Vercel preview N/A — the preview runs in `LOCAL_MODE`, which has no API server behind it, and alert creation is gated off there (`!IS_LOCAL_MODE`). Exercise this against a full-stack dev server: 1. Open `/chart`, pick a source, and build a Time series or Number chart. 2. Click **Add Alert**, give it a name and a webhook, then click **Create alert**. 3. Open `/alerts` — the alert is listed with a chart icon, and its name links back to the explorer with the same query. Works with `NEXT_PUBLIC_ENABLE_ALERT_DETAILS` both on and off; with it on, the detail page header and the row menu's **Open chart** use the same link. 4. Open the alert's details page (requires `NEXT_PUBLIC_ENABLE_ALERT_DETAILS`) — the query chart renders with the threshold line. **Edit alert** opens the chart editor; change the threshold and the query, save, and confirm both persist. ### References - Linear Issue: [HDX-5192](https://linear.app/clickhouse/issue/HDX-5192/ui-for-creating-and-editing-chart-alerts-chart-explorer-alert-details) - Related PRs: [ClickHouse#3010](ClickHouse#3010) (backend) --- **Testing done:** `make ci-lint`, `make ci-unit` — pass (3637 app tests). New unit tests cover the inline-alert helpers, the explorer's alert gating / name field / save payload, the `?alertId=` seeding hook, the detail-chart config builder, and the inline edit modal's seeding, PUT payload, and lazy fetch. *Note:* the ticket calls the source `AlertSource.CHART`; the merged backend named it `AlertSource.INLINE`, which is what this follows.
…x-eligible (ClickHouse#3104) ## Summary Two changes to the LLM dashboard, both touching how its `WHERE` clauses are built. **Performance.** Every tile carries a `WHERE` testing whether attribute keys are present, expressed as `SpanAttributes['key'] != ''` — the one shape that can't be indexed. `!= ''` normalizes to `notEmpty()`, which no skip index supports, so every granule is scanned. Map subscripts are also subcolumn references, so on ClickHouse 26.3+ each distinct key adds a per-part size lookup during PREWHERE planning (HDX-5348). These filters now lead with `mapContains`, which the `mapKeys()` index serves and which costs no per-part lookups. **Feature.** Adds a user filter beside the existing session filter, so the dashboard can be scoped to one end user. <details> <summary>Index and function references</summary> ClickHouse's text index condition ([`MergeTreeIndexConditionText::isSupportedFunction`](https://github.com/ClickHouse/ClickHouse/blob/master/src/Storages/MergeTree/MergeTreeIndexConditionText.cpp)) serves `mapContainsKey` and `has()` against a `mapKeys()` index; a `bloom_filter` over `mapKeys()` answers the same shape. ClickHouse#3096 moved `idx_span_attr_key` to `text(tokenizer='array')`, which makes this pointed: the index is there and these queries couldn't use it. `mapContains` is a case-sensitive alias of `mapContainsKey` ([FunctionsMapMiscellaneous.cpp:766](https://github.com/ClickHouse/ClickHouse/blob/master/src/Functions/array/FunctionsMapMiscellaneous.cpp)). </details> ## Measured on staging SharedMergeTree, 26.6.1, 162 active parts. `EXPLAIN` only — no rows read. **Planning cost**, 10-key detection predicate (`isLLMSpan`), subcolumn sizes forced on: | | duration | `S3GetObject` | outcome | | :--- | ---: | ---: | :--- | | `!= ''` | 36,111 ms | 1,480 | `TIMEOUT_EXCEEDED` before start | | `mapContains` | **7 ms** | **0** | ok | **Granule pruning**, 2-key predicate over 6h (`EXPLAIN indexes = 1`): | | parts | granules | skip index | | :--- | ---: | ---: | :--- | | `!= ''` | 19 | 1,306 | *no `Skip` section* | | `mapContains` | 3 | **3** | `idx_span_attr_key` | **Equivalence**: both forms return `66` over 7 days (248M rows scanned). ## How the filters are shaped Review moved this on from the first version, which was bare `mapContains` everywhere. Presence alone admits a key explicitly set to `''`, and most of these gates are paired with a value expression the dashboard *groups by* — so an empty `user.email` became a blank "Top Users" row whose drill-in link went nowhere. Gates that feed a group-by therefore keep their value check, and the presence term is pruning only: ```sql indexHint(mapContains(a,'k1') OR mapContains(a,'k2')) AND coalesce(nullif(a['k1'],''), …) != '' ``` The value term defines the result; the presence term reaches skip-index analysis without being re-evaluated per surviving row. `indexHint` is the existing repo idiom for this (`mapKeyIndexExpression`, queryParser.ts:1728). It costs no extra per-part lookups, because the value term reads the same keys the group-by already reads. Three positional rules fell out of this, each with a test pinning it: - **`isLLMSpan` is bare presence, not hinted.** `indexHint` returns true at execution, so hinting a filter that *is* the presence check would match every span. This gate is where the planning win comes from. - **Select-list gates are unhinted.** Skip-index analysis never reaches the select list, so a hint there folds to a constant while lengthening every query. `hasReportedTokens` is unhinted for this reason; `isToolSpan` is used in both positions and is exposed in both forms. - **Hints sit above an `OR`, never inside one.** A skip index can only drop a granule when every arm of an `OR` is decidable by it, so `isToolSpan`'s hint is hoisted to cover the span-kind key too — sound because both arms imply one of the hinted keys. ## User filter Follows the session filter exactly: a dropdown of the distinct values seen in the searched range, applied centrally in `baseLLMChartConfig` so every tab picks it up, and held in the URL rather than the search form so it can be set programmatically without two-way sync. Users resolve through the same cross-dialect expression "Top Users" groups by (`user.email`, `enduser.id`, `user.id`, `ai.telemetry.metadata.userId`), so a dropdown value always matches the rows that produced it. The Errors tab's correlated **log** query is scoped too, matched against the log source's own attribute column. `SessionSelect` and `UserSelect` are now thin wrappers over a shared `DistinctValueSelect`. That extraction also fixed a bug pre-existing in `SessionSelect`: an applied value absent from the fetched options fell back to the placeholder, so the control read "All users" while every chart stayed filtered. Absence isn't only staleness — it also happens before the query resolves (deep-linking a filtered URL) and when the value falls off the row limit — so the applied value is kept selectable rather than cleared. `buildSessionCondition` is now `buildScopeCondition`: it was already generic, and there are two scopes. ## Behavior changes **LLM span detection is presence-based**: a span carrying `gen_ai.system` at all is an LLM span whatever the value. Nothing groups by that predicate, so it can't produce a blank row. On staging this made no difference (66 = 66 over 248M rows) — OTel SDKs omit attributes rather than emitting empty strings — but it is a real semantic change and is in the changeset. Every other gate preserves its exact previous semantics. ## Deliberately unchanged - **Value expressions** (`model`, token counts, `costUsd`, `ttftMs`, …) still read subscripts — they need the values. They remain subject to the planning fan-out, which the client-level setting in ClickHouse#3102 covers. - **`isToolSpan`'s `openinference.span.kind = 'TOOL'`** stays an equality: it discriminates one span kind from others, and the query builder rewrites equality onto the attribute-items index. - **`hasTtft`** keeps `> 0` — it gates a p50/p95 and zero-valued rows would drag the percentiles down. - **JSON attribute columns** are untouched. Their paths are real subcolumns, there is no key index to prune with, and a presence term would only duplicate reads — so the JSON branch emits the original expression, pinned by `toBe` assertions. - **`buildScopeCondition`'s `coalesce(...) = 'value'`.** The indexable form is `has(SpanAttributeItems, 'key=value')`, but the items column name lives in `renderChartConfig`'s `textIndexInfoLookup` and isn't reachable from the app package. Follow-up. ## Caveat: materialized columns `fastifySQL` rewrites a `Col['key']` subscript onto a materialized column when an operator has created one, and it matches on the subscript AST node — so it cannot see `mapContains`. Such tables never had the planning problem either, since a rewritten subscript is no longer a subcolumn reference. This trades that rewrite for skip-index pruning, which is the better deal only where those columns don't exist (the default, and what the numbers above were measured on). Teaching `fastifySQL` to match the `mapContains` shape would fix this and benefit queryParser's existing `indexHint(mapContains(...))` emissions too, but it changes shared query planning for every source. Not in this PR. ## How to test on Vercel preview **Preview route:** `/llm` 1. Open `/llm`; confirm the Overview tab renders tiles with data, and that values match what the page showed before this change. 2. Latency tab: "Time to First Token" still plots median and p95. 3. Sessions tab: the session dropdown populates and selecting one filters every tile. 4. **User filter**: pick a user from the new dropdown and confirm every tab rescopes, including the Errors tab's log rows. Copy the URL, open it in a new tab, and confirm the dropdown still shows the selected user rather than "All users". 5. Move the time picker to a window with no data for the selected user; the dropdown must keep showing that user (with a clear "×") rather than reading "All users" over empty charts. ## Verification - `yarn ci:unit` (app): 233 suites, 3,827 tests, pass - `yarn ci:unit` (common-utils): 36 suites, 2,526 tests, pass - `make ci-lint`: 0 errors · `tsc --noEmit` clean · `knip` clean - Staging `EXPLAIN` A/B above; result equivalence confirmed over 7 days of data ## Notes for reviewers - **The staging numbers were taken on the bare `mapContains` shape**, before review moved most gates to `indexHint(presence) AND value`. `indexHint` feeds the same expression to index analysis, and the `isLLMSpan` headline (36s → 7ms) is directly valid since that gate is still bare presence — but the post-review shape was not re-measured on staging. It is covered by a `renderChartConfig` test asserting the kv-items rewrite still fires through an `indexHint`-bearing condition. - Staging still runs the **bloom-filter** `idx_span_attr_key`, since ClickHouse#3096 isn't deployed there. `mapContains` is served by both bloom-filter and text indexes on `mapKeys`, so the pruning result holds either way — but the measurements are from the bloom-filter index. - The two `EXPLAIN indexes = 1` runs report different part counts (16 vs 19) because ingest continued between them; it doesn't affect the granule comparison. - Two changesets: `patch` for the perf fix, `minor` for the user filter. ## References - Related: [HDX-5348](https://linear.app/clickhouse/issue/HDX-5348/ch-263-allow-calculating-subcolumns-sizes-for-merge-tree-reading-makes) — the 26.3 planning regression; ClickHouse#3102 handles it at the client level - Builds on: ClickHouse#3096 (trace schema text indexes)
…il to load (ClickHouse#3105) ## Summary Follow-up to ClickHouse#3104, addressing a detail raised in review there that landed after the merge. ClickHouse#3104 fixed the scope dropdowns (session, user) falling back to their placeholder when the applied value wasn't in the fetched options — the control read "All users" while every chart stayed filtered. Pinning the value made it visible, but the control is still `disabled` whenever the distinct-value query is loading or has failed: ```tsx disabled={isLoading || isError} ``` So with a filter applied and a failing query, the user can see the scope but can't remove it — and `isError` doesn't clear on its own, so that state is permanent. Mantine also only renders the `clearable` × on an enabled input, so there is no escape from the control itself. Now only disabled when nothing is applied: ```tsx disabled={(isLoading || isError) && !value} ``` Loading with no filter still disables, which is the case the guard was for. ## Tests Two cases added to `DistinctValueSelect.test.tsx`: - applied value + failing query → input is enabled and still shows the value - loading with nothing applied → input is disabled (pins the behaviour that's kept) The first fails against the old `disabled={isLoading || isError}`. ## Verification - `yarn ci:unit` (app): 233 suites, 3,829 tests, pass - `make ci-lint`: 0 errors · `tsc --noEmit` clean · `knip` clean ## References - Follows: ClickHouse#3104
## Summary **Next.js goes 16.2.11 to 16.3.4 in `packages/app`**, picking up what shipped in 16.3.3 and 16.3.4. No application code moved. `packages/app` is Pages Router only, so the 16.3.0 App Router deprecations (edge runtime, `experimental.useCache`/`dynamicIO`, `partialFallbacks`) never come into play, and a grep confirmed none of those flags were set anywhere. Two ESLint packages ride along: `@next/eslint-plugin-next` and `eslint-config-next`, both 16.0.10 to 16.3.4. Both sat two minors behind the framework. Lint attributes zero warnings to `@next/next` rules, so the bump costs nothing. **Expect a big `yarn.lock` diff.** Next's own pins moved with it and nothing else did: `sharp` 0.34.5 to 0.35.4 plus its whole `@img/*` platform-binary set, `postcss` 8.4.31 to 8.5.23, `@swc/helpers` 0.5.15 to 0.5.23. `packages/app/AGENTS.md` is new, and Next wrote it. 16.3 added a `next dev` step that drops an agent-rules block into the project directory whenever it detects an AI coding agent. Committing the file satisfies its check, which kills the recurring uncommitted change and stops it creating a `packages/app/CLAUDE.md` alongside. Everything ran green. `make ci-lint` (0 errors, 559 warnings against a 564 cap), `yarn ci:unit` (3829 tests), the production Webpack build, the ClickStack static export, a Turbopack dev boot, and the navigation E2E suite (10 passed). ### How to test on Vercel preview **Preview routes:** /search, /dashboards, /chart **Steps:** 1. Open /search and verify the page renders with the search input and a results table. 2. Open /dashboards and verify the dashboard list renders. 3. Open /chart and verify the chart builder renders. ### References - Linear Issue: - Related PRs:
## Summary Disabled sources still triggered ClickHouse queries when a page loaded. The reported symptom was a `SELECT name, value FROM system.settings FORMAT JSON` query firing for a source that was turned off. The root cause is that several load-time helpers build their query inputs from the full source list (or every source on a connection) without checking `source.disabled`. The `system.settings` fetch is a per-connection side effect of `processClickhouseSettings`, so any metadata/field/value query issued for a disabled source's connection surfaces as that observed query. This PR excludes disabled sources from the load-time builders that fan out queries across sources: - **`RawSqlChartEditor`** — the SQL editor autocomplete built `tableConnections` from *every* source on the connection, then ran `getAllFields`/`getColumns` per table on mount. - **`DBDashboardPage`** — the dashboard WHERE-bar autocomplete built `tableConnections` from tile sources without skipping disabled ones. - **`useQueriedDashboardFilterValues`** — dashboard filter dropdowns ran a key-values lookup per filter source, keeping filters whose source is disabled. - **`OnboardingChecklist`** — the "has data" probe OR-combined every source on the first connection into a `system.tables` query. Disabled sources produce no user-selectable autocomplete/filter values anyway, so filtering them out has no visible downside; it only removes wasted queries. Enabled sources continue to load their fields/settings normally. Out of scope: a dashboard *tile* that directly references a source which was later disabled still runs its chart query. Blanking such a tile is a product/UX decision (it should probably show a "source disabled" state), so it's left for a follow-up. ### Screenshots or video N/A — non-UI change (removes background queries only; no visual change). ### How to test on Vercel preview N/A — non-UI change. ### References - Linear Issue: HDX-5355 - Related PRs: Linear Issue: [HDX-5355](https://linear.app/clickhouse/issue/HDX-5355/prevent-queries-for-disabled-sources-on-load) <div><a href="https://cursor.com/agents/bc-70a0990f-9561-4816-a0fb-957e7df78166?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> <a href="https://cursor.com/background-agent?bcId=bc-70a0990f-9561-4816-a0fb-957e7df78166&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> </div> Co-authored-by: Cursor Agent <199161495+cursoragent@users.noreply.github.com>
…on on CH 26.3+ (ClickHouse#3102) ## Summary ClickHouse 26.3 flipped `allow_calculating_subcolumns_sizes_for_merge_tree_reading` to on by default. `optimizePrewhere` then calls `getColumnSizes` with subcolumn sizes enabled, which walks every **(active part × referenced map key)** to read per-part sizes. On SharedMergeTree each of those is an S3 GET. It happens during **planning**, before a single row is read, and neither `max_execution_time` nor client cancellation interrupts it. Measured on a 26.6 SharedMergeTree cluster with 162 active parts. `EXPLAIN PLAN` only, so 0 rows read — this is pure planning cost: | distinct map keys | setting | duration | `S3GetObject` | outcome | | :--- | :--- | :--- | :--- | :--- | | 1 | `0` | 5 ms | 0 | ok | | 1 | `1` | 4,670 ms | 148 | ok | | 10 | `1` | 36,111 ms | 1,480 | `TIMEOUT_EXCEEDED` as `ExceptionBeforeStart` — overran a 30s `max_execution_time` by 6s | Cost is linear in referenced keys, at ~one GET per active part each (148/key here). The LLM dashboard reads ~64 keys per tile. On a 645-part cluster running 26.4 that worked out to 64k GETs and **27 minutes of planning for zero rows** — and because 26.4 still held the `DataParts` lock across the loop, a queued writer starved every other reader on the replica for ~25 min. One page load froze it. This sends the setting as `0`. ### Why in the shared client The fix goes in `processClickhouseSettings` (`packages/common-utils/src/clickhouse/index.ts`) rather than at each call site, for two reasons: - **One change covers every caller** — charts, search, alerts, the API, and MCP all go through this method. Patching `useChartConfig` alone would leave alert evaluation exposed, and it runs unattended. - **`applySettingIfAvailable` is already the version guard.** It only emits settings present in that connection's `system.settings` (cached per `connectionId`), so pre-26.3 servers never receive it and can't fail on an unknown setting. No version parsing needed. Caller-supplied `clickhouse_settings` are still spread last, so anyone who explicitly wants the calculation on can override. The sizes are only used to *reorder* PREWHERE conditions. The `= 0` path approximates each subcolumn with its parent column size, which is exactly what every version before 26.3 did. ### Also included `EfficiencyCharts` and `LatencyCharts` bound the ~70 KiB model-price cost expression as a WITH alias and never selected it. Both now pass `withCostAlias: false`, matching what `ErrorsTab` / `LatencyTab` / `LLMEmptyStateBanner` already do. Smaller queries, less parse time, no behavior change. ### How to test on Vercel preview N/A — non-UI change. No visual difference; the change is in the settings sent alongside each query. To verify against a ≥26.3 server, run any dashboard query and check it in `system.query_log`: ```sql SELECT Settings['allow_calculating_subcolumns_sizes_for_merge_tree_reading'] FROM system.query_log WHERE event_date = today() AND type != 'QueryStart' ORDER BY event_time DESC LIMIT 5; ``` Before this change the column is empty; after, it reads `0`. ### Verification - `make ci-unit` — 5 projects, 7,756 tests, exit 0 - `make ci-lint` — 5 projects, 0 errors, exit 0 - `tsc --noEmit` clean in `common-utils` and `app` - New unit coverage: the setting is applied when the server advertises it, and omitted when it does not (the pre-26.3 path) ### Notes for reviewers - **Not verified end-to-end against a real ≥26.3 SharedMergeTree cluster through the app.** The staging cluster I probed already has `compatibility = '26.2'` set, which independently pins this setting to `0`, so a live run there would only prove the setting is transmitted, not that it changes anything. The measurements above come from forcing the flag on per-query via `SETTINGS` on that same cluster. - **This does not help queries already stuck in planning for other reasons** — `max_execution_time` still doesn't bound the planner. It removes this particular fan-out, not the general exposure. - Follow-ups tracked on the ticket: setting the profile on the affected cluster, auditing other CHC ClickStack services on ≥26.3, an upstream ClickHouse issue, and materialized `__hdx_llm_*` columns for the execution-time (rather than planning-time) cost. ### References - Linear Issue: [HDX-5348](https://linear.app/clickhouse/issue/HDX-5348/ch-263-allow-calculating-subcolumns-sizes-for-merge-tree-reading-makes) - Related PRs: hyperdx-ee#3261 (alert `max_execution_time`) — its "does not bound planning" caveat is this issue
> **Stacked on ClickHouse#2747.** Base is `claude/deterministic-metric-name-listing`, so this diff is only the streaming layer. Merge ClickHouse#2747 first. ## Problem The metric name select waited on a `groupUniqArray` aggregation over the data before showing anything. On a source reporting ~4,900 gauge metrics that is ~770ms of empty dropdown, paid on every chart editor mount — and the control is mounted per series, on every dashboard tile editor. ## Fix Read `MetricName` from the table's sparse primary index via the `mergeTreeIndex` table function instead of scanning the data: one row per granule mark. Measured on a 121M-row `otel_metrics_gauge` with 4,914 gauge names: | | cold latency | names returned | |---|---|---| | index read | **7ms** | 1,986 | | exhaustive `GROUP BY` | 767ms | 4,914 | `DISTINCT` is a streaming transform in ClickHouse, so results also arrive progressively rather than in one block — first options on screen ~30ms after the query starts, and a small spinner replaces the chevron while more land. `ORDER BY` is deliberately omitted, since the sort would have to complete before the first row; the client sorts. ## The index returns a subset, and that is the design The index only records the column's value at each granule boundary, so a metric confined to one granule never appears. On the test dataset that is 1,986 of 4,914 names — and the split is not arbitrary: | | metric names | median datapoints each | |---|---|---| | visible in index | 1,991 | **32,060** | | invisible | 2,923 | **14** | The hidden ~2,900 are almost entirely metrics with a dozen datapoints. So browsing shows the metrics that actually carry data, and **typing switches to the exhaustive relevance-ranked search from ClickHouse#2747**, which reaches anything the index omitted. The placeholder reads `Search metrics...` to invite that. Verified end-to-end: a metric absent from the index is found by typing its name, ranked first. Browsing also falls back to that same exhaustive query when the index cannot be read at all — a server older than 24.2, a Distributed or non-MergeTree metric table, or a primary key without `MetricName` — so no deployment loses the picker. ## Two behaviours worth reviewing **The options never blank mid-keystroke.** `keepPreviousData` serves the *previous* pattern's page when the search key changes, and results for `http` cannot match a search for `billing` — the client-side filter reduces them to nothing and the dropdown flashes empty for ~700ms. This ignores placeholder pages (`isPlaceholderData`) and holds the unfiltered browse list instead, which still filters to relevant matches immediately. Measured before/after with a `MutationObserver` on the option list. **Render cap raised 100 → 500** to match `DEFAULT_METRIC_NAMES_LIMIT`, so a search not reported as `truncated` is fully renderable. Mantine renders options as plain DOM, so this costs ~150ms of keystroke blocking at ~4.9k names that a cap of 100 avoided — an accepted trade, not an oversight. ## Known limitation, not addressed here `getMetricOptions` concatenates the four kind lists gauge-first, so kind rather than relevance decides what survives the render cap. Searching `http` on the test dataset returns 0 of 26 matching Sum metrics behind 325 gauge matches. This predates both PRs (called out in ClickHouse#2747 as tracked separately) and the fix is a relevance-ordered merge across kinds; raising the cap does **not** fix it, since gauge alone exceeds 500 while browsing. ## Testing - `Metadata.streamDistinctIndexValues`: 13 unit tests over a faked `BaseResultSet` — chunk-per-chunk yielding, the emitted SQL, parts pruning, and each guard (old server, pointer table, non-MergeTree engine, column outside the primary key, including a column that only appears nested in a key expression). - `useStreamingQuery`: 5 tests — partial publication before completion, throttle-away, error discards partials, disabled, empty stream. - `useMetricNames`: 10 tests — per-kind streaming and sorting, mode switch, the hold, the stale-placeholder guard, the fallback. - `MetricNameSelectSearch`: ClickHouse#2747's 11 cases retargeted to the two-mode behaviour, all passing. - E2E (`metric-name-streaming.spec.ts`): browses from the index, then finds an index-invisible metric by typing. Asserts the seed still *has* such a metric, so the test cannot pass vacuously. - `make ci-lint` clean, `make ci-unit` 3400 passed. Verified by hand against a 121M-row local instance. ## Notes `streamToAsyncIterator` moves from `packages/app/src/sessions.ts` (marked `TO BE DEPRECATED`) into `common-utils` beside the ClickHouse client. `Metadata.streamDistinctIndexValues` is generic over table and column, so `ServiceName` and other primary-key columns can be listed the same way. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Triggered by [comment](ClickHouse#3106 (comment)) from @pulpdrew. Vouch: @Tyagiquamar
Fixes ClickHouse#3059. ## Why A saved search scoped through the filter bar evaluated correctly, but the sample events rendered into the notification came from an unfiltered query, so alerts showed unrelated log lines as evidence next to a correct count. ## What changed The sample fetch in renderAlertTemplate hand-built its chart config from where/whereLanguage only. It now also passes savedSearch.filters, the same input the evaluation path feeds buildSearchChartConfig, so renderChartConfig narrows both queries to the same row set. TILE and INLINE paths are untouched. Includes a changeset (patch, @hyperdx/api). ## Tests - New integration case in renderAlertTemplate.int.test.ts renders a saved-search alert whose search carries a pinned filter and asserts the generated sample SQL contains the filter condition. It fails on unpatched main and passes with the fix. - Targeted run against local ClickHouse/Mongo services: 1 passed. - ESLint on touched files: 0 errors (8 pre-existing warnings elsewhere in the files); Prettier clean. Co-authored-by: Warren Lee <5959690+wrn14897@users.noreply.github.com> Co-authored-by: Tom Alexander <3245235+teeohhem@users.noreply.github.com>
## Summary Backfills alert `name` and `tags` from the referenced saved search, dashboard tile, or inline chart config. - Adds `runStartupMigrations()` called from `Server.start()`. Idempotent and re-runs on every start, alerts with an existing name or tags are skipped. - Adds optional `tags` field to the Alert model. - Since `alert.name` is also used as the notification title template, a name that isn't valid Handlebars now renders as is to avoid breaking the notification. Tested with `make dev-int FILE=src/__tests__/migrations.int.test.ts` and `FILE=renderAlertTemplate.int.test.ts`, plus unit tests for the name/tags derivation. ### References - Linear Issue: HDX-5149
…use#3084) ## What Adds a second phase to the sidebar onboarding checklist. After the existing setup steps (connect ClickHouse, create sources, add data) complete, a product-usage phase tracks four "getting started" milestones per user: - **Explore your data** — run a search with a filter or query condition - **Build a dashboard** — add a chart tile to a dashboard - **Set up an alert** — create or edit an alert - **Connect the MCP server** — make a successful MCP tool call Each task links to where to do it, the card can be dismissed, and a brief celebration shows when the final task is completed in-session. ## Why The existing checklist stops once a team is technically set up. New users still need a nudge toward the actions that deliver value (searching, charting, alerting, querying via an agent). This extends the same surface to cover first real usage without adding a separate onboarding UI. ## How it works - **Single source of truth**: the task registry (`ONBOARDING_TASK_IDS`) lives in `common-utils`. It's typed so adding a task is a compile error until both the API validation enum and the frontend UI (copy + link) are updated — a new task can't be silently untracked. - **Surface-agnostic completion**: alert and dashboard tasks complete whether the action came from the UI, external REST API v2, or an MCP tool (all authenticate with the user's personal access key). Recorded server-side and fire-and-forget, so onboarding bookkeeping never blocks or fails the triggering write. - **"Dashboard" means a chart, not a shell**: the dashboard task completes only once a dashboard has at least one tile — including tiles added to a temporary (unsaved, URL-state) dashboard, which the frontend records directly since it never touches the backend. - **Explore data**: completes on any non-trivial search (a non-empty where clause in Lucene or SQL, or an applied filter); a blank default search doesn't count. - **MCP**: recorded in the tool-tracing chokepoint — a successful tool call is the only reliable signal the user exercised the server. - **Cache hygiene**: after a UI action the frontend patches only `onboardingData` in the cached `me` object — no `me` refetch — so the many `useMe` consumers (metadata, ClickHouse settings, nav) are untouched. - **Derived "done"**: the checklist's completed state is derived from whether every current task is complete rather than a persisted flag, so adding or changing a task later automatically reopens the checklist for users who finished the old set. The persisted `isDismissed` is only for the manual X (opting out early). - Read-tolerant / write-strict schema: the read path drops persisted task ids no longer in `ONBOARDING_TASK_IDS` (so removing a task can't 500 `GET /me`), while the write boundary stays a strict enum. `onboardingData` is defaulted for users created before the field existed. ## Demo https://github.com/user-attachments/assets/0e693c55-0f54-4177-86d8-239ac6403fca ## Persistence New optional `user.onboardingData` subdocument (`completedTasks`, `isDismissed`). Two new routes: `POST /me/onboarding/task` and `PATCH /me/onboarding/dismiss`. ## Testing - `make ci-lint` — lint + tsc + styles + escape-hatch ratchet: green - `make ci-unit` scope — app (onboarding suites + DBSearchPage + dashboard, 274 tests), common-utils (2424), api (825): all pass - Integration tests updated (`me.int.test.ts`, external `dashboards.int.test.ts`); they require the Docker stack (`make dev-int`) and were not run in this session.
…House#3125) ## Why Service map nodes were always some shade of red, and you couldn't tell a service with zero errors from one with a trace of them. Two causes, both in `getNodeColors`: 1. **The ramp was max-normalized.** `intensity = value / max` meant the darkest node was always whichever service had the highest error rate *on the current graph* — whether that was 0.3% or 60%. A perfectly healthy map painted its best-of-a-good-bunch service full dark red. 2. **Zero sat at the bottom of the red ramp,** rendering `hsl(0 35% 90%)` — a pale pink. A service at 5% of the graph max landed at `hsl(0 37% 88%)`, a 2-point lightness difference on a 32px circle. Not perceptible. Worst case: when no service has errors at all, `metricMax.errorRate === 0`, so every node renders identically pale pink — indistinguishable from "everyone has a trace of errors." This is out of step with how other tools handle it. Datadog and New Relic leave healthy nodes neutral and only color on an absolute threshold breach (New Relic explicitly won't draw green for an operational node). AWS X-Ray and Grafana's Tempo service graph use a proportional donut. Nobody asks users to read error rate off a lightness ramp. ## What changed Error rate now maps to four **absolute** states: | Error rate | Fill | |---|---| | `0` | `hsl(220 8% 82%)` — neutral cool grey | | `0 – 1%` | `hsl(0 48% 73%)` | | `1 – 5%` | `hsl(0 61% 56%)` | | `≥ 5%` | `hsl(0 72% 42%)` | - The neutral is a **hue shift, not desaturation**. At 32px a cool grey reads as a different kind of thing; a washed-out red just reads as a little red. - The three red stops come from the existing ramp, so no new hexes enter the palette. - 5% is the threshold `ServiceMapTooltip` already used for its alarm color, now exported as `ERROR_RATE_HIGH` and shared, so the fill and the hover text can't drift apart. - **Latency and throughput are unchanged** — they keep the max-normalized ramp. Neither has a universal threshold to anchor to. - The legend gradient emits hard stops for error rate (four discrete steps rather than a blend, since a continuous bar would imply precision the bucketing doesn't have), and labels the top of the scale `≥5%` instead of the observed max. No changes to `ServiceMapNode`, `ServiceMap.tsx`, or the SCSS — the node already just consumes `{ backgroundColor, borderColor }`. ## Trade-off With hard buckets a service at 4.9% and one at 60% paint identically. A proportional donut would keep that distinction, but it's a much larger change (and a four-way success/4xx/5xx/throttle split isn't available to us — the source schema only has `statusCodeExpression`, and OTel's `StatusCode` is just `Ok`/`Error`/`Unset`). The tooltip shows the exact percentage on hover, so the detail is recoverable. Thresholds are hardcoded as exported constants. A service on a 99.9% SLO and a batch job have different notions of "elevated" — if that comes up, per-source overrides are the natural extension. ## Testing `packages/app/src/components/ServiceMap/__tests__/utils.test.ts` — existing ramp tests re-pointed at `throughput` (still max-normalized), plus a new block covering the absolute buckets: zero renders neutral, buckets ignore `max`, thresholds are inclusive lower bounds, and the neutral state still draws a darker border. Full app suite green (223 suites / 3664 tests), `make ci-lint` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…House#3115) ## Summary This PR fixes a few bugs causing failures when opening the row side panel, when not using the _block_number/ _block_offset based lookup. To reproduce, disable _block_number/ _block_offset based lookup with `ALTER TABLE <source_table> MODIFY SETTING enable_block_offset_column = 0, enable_block_number_column = 0;` ### Screenshots or video #### Select containing hyphenated column: Before <img width="2313" height="839" alt="Screenshot 2026-09-11 at 2 34 39 PM" src="https://github.com/user-attachments/assets/8f44e47e-4866-4da5-89bc-230b6f943f0d" /> <img width="1707" height="774" alt="Screenshot 2026-09-11 at 2 35 05 PM" src="https://github.com/user-attachments/assets/5b8dff75-1b5f-4318-b323-bcd71cff6599" /> After <img width="2312" height="928" alt="Screenshot 2026-09-11 at 2 30 30 PM" src="https://github.com/user-attachments/assets/1521877a-b661-4e4d-a81f-3657c4b3b593" /> #### Select containing a backtick-quoted alias, with other aliases Before <img width="1714" height="861" alt="Screenshot 2026-09-11 at 2 33 55 PM" src="https://github.com/user-attachments/assets/d3e96c2d-cb75-46eb-916c-2be36797a64f" /> After <img width="2310" height="1193" alt="Screenshot 2026-09-11 at 2 32 11 PM" src="https://github.com/user-attachments/assets/789c4237-9671-476a-849c-26e064cbc65d" /> ### How to test locally Add a hyphenated column to your logs table ``` alter table otel_logs add column `test-hyphen` String ``` Disable the block_number filtering: ``` alter table otel_logs modify setting enable_block_offset_column = 0, enable_block_number_column = 0; ``` ### References - Linear Issue: Closes HDX-5363 - Related PRs:
…se#3450) ## Summary This PR moves some external API source conversion functions into common-utils, so that they can be re-used in non-api contexts. This is a pure refactor, no behavior changes are expected.
Bumps [hono](https://github.com/honojs/hono) from 4.13.9 to 4.13.13. e notes</summary> <p><em>Sourced from <a href="https://github.com/honojs/hono/releases">hono's releases</a>.</em></p> <blockquote> <h2>v4.13.13</h2> <h2>Mount Middleware</h2> <p><code>app.mount()</code> is now available as the Mount Middleware, <code>hono/mount</code>. It is just a handler, so you register it with <code>app.all()</code>:</p> <pre lang="ts"><code>import { Router as IttyRouter } from 'itty-router' import { Hono } from 'hono' import { mount } from 'hono/mount' <p>const ittyRouter = IttyRouter()<br /> ittyRouter.get('/hello', () => new Response('Hello from itty-router'))</p> <p>const app = new Hono()<br /> app.all('/itty-router/*', mount(ittyRouter.handle))<br /> </code></pre></p> <p><code>app.mount()</code> still works in v4 but is deprecated and will be removed in v5. Migrating is a one-line change:</p> <pre lang="diff"><code>- app.mount('/itty-router', ittyRouter.handle) + app.all('/itty-router/*', mount(ittyRouter.handle)) </code></pre> <h2>What's Changed</h2> <ul> <li>test(client): simulate network error for undefined route in parseResponse test in <a href="https://redirect.github.com/honojs/hono/pull/5439">honojs/hono#5439</a></li> <li>docs(request): fix jsdoc comments for some getters in <a href="https://redirect.github.com/honojs/hono/pull/5445">honojs/hono#5445</a></li> <li>fix(jsx): allow JSXNode function component results in <a href="https://redirect.github.com/honojs/hono/pull/5476">honojs/hono#5476</a></li> <li>feat(mount): introduce Mount Middleware and deprecate <code>app.mount</code> in <a href="https://redirect.github.com/honojs/hono/pull/5221">honojs/hono#5221</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/honojs/hono/compare/v4.13.12...v4.13.13">https://github.com/honojs/hono/compare/v4.13.12...v4.13.13</a></p> <h2>v4.13.12</h2> <h2>What's Changed</h2> <ul> <li>fix(build): keep internal types private in bundled d.ts and avoid a self-referencing JSX.IntrinsicElements in <a href="https://redirect.github.com/honojs/hono/pull/5485">honojs/hono#5485</a></li> <li>test(build): type-check the bundled declarations from a consumer project in <a href="https://redirect.github.com/honojs/hono/pull/5486">honojs/hono#5486</a></li> <li>fix(etag): correctly match mixed-case header name in retainedHeader option in <a href="https://redirect.github.com/honojs/hono/pull/5475">honojs/hono#5475</a></li> <li>fix(jsx): add px to numeric gridGap, gridRowGap and gridColumnGap in <a href="https://redirect.github.com/honojs/hono/pull/5487">honojs/hono#5487</a></li> <li>fix(combine): return a Response from a short-circuiting middleware in some() in <a href="https://redirect.github.com/honojs/hono/pull/5391">honojs/hono#5391</a></li> <li>chore(deps): upgrade vite-plus to 1.0.0 in <a href="https://redirect.github.com/honojs/hono/pull/5464">honojs/hono#5464</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/honojs/hono/compare/v4.13.11...v4.13.12">https://github.com/honojs/hono/compare/v4.13.11...v4.13.12</a></p> <h2>v4.13.11</h2> <h2>Security fixes</h2> <h3><code>serveStatic</code> decodes the request path a second time, leading to bypass of middleware on static paths</h3> <p>Affects: <code>hono/serve-static</code> and the adapters built on it (<code>hono/bun</code>, <code>hono/deno</code>, <code>hono/cloudflare-workers</code>, <code>@hono/bun</code>, <code>@hono/deno</code>, <code>@hono/cloudflare-workers</code>). Fixes <code>serveStatic</code> decoding an already-decoded path, where a crafted request could be routed as one path and served as another, skipping middleware mounted on a static prefix. GHSA-5r4p-p66f-jhc7</p> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/honojs/hono/commit/08a023cbfde55b434fb0fb30fae35d42e4bf20aa"><code>08a023c</code></a> 4.13.13</li> <li><a href="https://github.com/honojs/hono/commit/ae595de8ae05ea548560da93140ee1d53e5a8202"><code>ae595de</code></a> feat(mount): introduce Mount Middleware and deprecate <code>app.mount</code> (<a href="https://redirect.github.com/honojs/hono/issues/5221">#5221</a>)</li> <li><a href="https://github.com/honojs/hono/commit/f23b146afcec63606144cde50b5fbd360dd60238"><code>f23b146</code></a> fix(jsx): allow JSXNode function component results (<a href="https://redirect.github.com/honojs/hono/issues/5476">#5476</a>)</li> <li><a href="https://github.com/honojs/hono/commit/6d73a74f9757ed65d6103c0e8375097fa20873c4"><code>6d73a74</code></a> docs(request): fix jsdoc comments for some getters (<a href="https://redirect.github.com/honojs/hono/issues/5445">#5445</a>)</li> <li><a href="https://github.com/honojs/hono/commit/deff529656cff775b35956f7f9071388d4eb55ae"><code>deff529</code></a> test(client): simulate network error for undefined route in parseResponse tes...</li> <li><a href="https://github.com/honojs/hono/commit/6abd35b0a5f35f67b6417627d5b0a6c2d266ac04"><code>6abd35b</code></a> 4.13.12</li> <li><a href="https://github.com/honojs/hono/commit/95eb860473d26bd303a2a29716e61a77c8c5e5f5"><code>95eb860</code></a> chore(deps): upgrade vite-plus to 1.0.0 (<a href="https://redirect.github.com/honojs/hono/issues/5464">#5464</a>)</li> <li><a href="https://github.com/honojs/hono/commit/afb2068c1c53a530528ac5f237e245793dd03c07"><code>afb2068</code></a> fix(combine): return a Response from a short-circuiting middleware in some() ...</li> <li><a href="https://github.com/honojs/hono/commit/e5bb2062a3ddd645e3a16d1300bd76d6c9af33a5"><code>e5bb206</code></a> fix(jsx): add px to numeric gridGap, gridRowGap and gridColumnGap (<a href="https://redirect.github.com/honojs/hono/issues/5487">#5487</a>)</li> <li><a href="https://github.com/honojs/hono/commit/c3053ccf14f1c4c70a0c2888eed6121b9f6c8f95"><code>c3053cc</code></a> fix(etag): correctly match mixed-case header name in retainedHeader option (#...</li> <li>Additional commits viewable in <a href="https://github.com/honojs/hono/compare/v4.13.9...v4.13.13">compare view</a></li> </ul> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/hyperdxio/hyperdx/network/alerts). </details>
Triggered by [comment](ClickHouse#3409 (comment)) from @MikeShi42. Vouch: @hercynium
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @hyperdx/cli@0.8.0 ### Minor Changes - 0b1e078: Add dashboard and saved-search authoring commands to the CLI: - `hdx dashboards list` (default subcommand) gains `--query` name filtering and `--format table|json|csv` - `hdx dashboards export --id <id-or-name>` prints a dashboard definition as JSON, round-trip compatible with import - `hdx dashboards create` accepts either a full definition (`--file <json>`) or inline charts (`--name` + repeatable `--chart` / `--chart-file`, with automatic grid layout), plus `--if-not-exists` - `hdx dashboards import --file <json>` recreates an exported dashboard, with `--if-not-exists` and `--name-override` - `hdx saved-searches` command group: `list` and `create` (`--name/--source/--where/--where-language/--select/--order-by/--tags`) Definitions are validated locally against the shared schema before being sent, and missing tile ids are generated automatically. Also fixes `getSavedSearches()` to call the correct `/saved-search` API path (the previous `/saved-searches` path always returned 404). ### Patch Changes - 5253b86: fix: Stop abbreviating values between 500 and 999 as "0.5k"/"0.8k" on chart axes and in large number format ## @hyperdx/api@2.42.0 ### Minor Changes - 9c2c703: feat: Support PromQL dashboard tiles in the external API `/api/v2/dashboards` now returns PromQL tiles and accepts them on create and update, as `configType: "promql"` with a `sourceId` and a list of `expressions`. Previously PromQL tiles were left out of responses, so saving a dashboard back through the API deleted them. MCP dashboard tools still hide PromQL tiles until they support editing them. - eb72cfa: feat(mcp): add `clickstack_service_health`, a one-call per-service health report for a trace source (throughput, error rate, p50/p95/p99, top error groups), compared with yesterday and ranked worst first - 95aef70: Add an optional `eventNameExpression` to log sources, pointing at the OpenTelemetry log EventName. Source auto-detection sets it when an OTel log table has a string `EventName` column. The field is stored and editable only: nothing reads it yet. HDX-5787 - bc47e48: Add an optional `severityNumberExpression` to log sources, pointing at the numeric OpenTelemetry SeverityNumber (1-24). Source auto-detection sets it when an OTel log table has an integer `SeverityNumber` column. The field is stored and editable only: log levels still come from the severity text. - cfa865b: feat: expose group-by values to webhook body templates as `{{var.<column>}}`, and keep them on resolve notifications ### Patch Changes - 9891a79: feat: Add ad hoc dashboard filter schemas and types - 27cccf7: fix: Reject creating or updating PromQL sources through the external API when PromQL is not enabled `POST` and `PUT /api/v2/sources` now return a 400 for `kind: "promql"` unless `ENABLE_PROMQL` is set, since PromQL sources cannot be queried without it. - 8d064df: fix: Reject external API PromQL tiles that reference a non-PromQL source, and keep PromQL tiles when an MCP agent updates a dashboard `/api/v2/dashboards` now returns a 400 when a PromQL tile's `sourceId` is not a PromQL source. MCP `clickstack_save_dashboard` updates no longer delete the PromQL tiles that MCP hides from agents. - 0000fa4: feat: Add a line interpolation display setting (smooth, linear, step) - ee9f84b: feat(mcp): record the response size of each MCP tool call - 24d324c: fix(mcp): store whereLanguage as lucene when clickstack_save_saved_search creates a search without one, so its alerts no longer parse a Lucene filter as SQL - 748315d: feat(mcp): advertise a title, description, website URL, and SVG icon in the MCP server's initialize response - cdfae45: fix(mcp): `clickstack_sql` now binds the bucket-size parameters its description advertises. `{intervalSeconds:Int64}`, `{intervalMilliseconds:Int64}`, `$__interval_s`, `$__timeInterval` and `$__timeInterval_ms` used to fail with "Substitution intervalSeconds is not set", including the tool's own time-series example. - b00d410: feat(mcp): `clickstack_sql` accepts an optional `sourceId`, so `$__sourceTable` expands to that source's table. Unknown sources, other teams' sources, and a source on a different connection than `connectionId` are rejected, as is `$__sourceTable` without a `sourceId`. - a06b17f: fix(mcp): stop `clickstack_sql` callers from guessing table and timestamp column names. `clickstack_list_sources` now returns each source's database-qualified `table` (and `database` for metric sources), the `sql` examples use `<table>` and `<timestampColumn>` placeholders instead of `otel_logs` and `TimestampTime`, and unknown-table errors from `clickstack_sql` point at `clickstack_list_sources`. - e494a46: feat(mcp): declare readOnlyHint, destructiveHint, idempotentHint, and openWorldHint on every MCP tool - 03f9fd5: fix(mcp): describe every MCP tool input, drop descriptions that restate schema constraints, and keep tool descriptions under Claude Code's 2 KB limit - 5cabc11: feat: Support distribution heatmaps calculated from promql data - a4b6395: fix: render percentile aggregations (`p50`, `p90`, `p95`, `p99`) as `quantile` at the matching level. A chart config with `aggFn: 'p95'` rendered as the nonexistent ClickHouse function `p95If`. The AI assistant now returns percentiles with a level instead of a bare `quantile` (median), and v1 metric chart conversion keeps the percentile level. - ca8e3fd: fix: support https for the PromQL remote-write endpoint and ClickHouse scraping `CLICKHOUSE_PROMETHEUS_METRICS_ENDPOINT` now accepts an `http://` or `https://` URL as well as a bare `host:port`. With `https://`, the collector's `prometheusremotewrite` exporter sends metrics over TLS and verifies the server certificate against the system CAs. Previously the exporter always used plain HTTP, so an HTTPS-only endpoint could not receive metrics. This applies to both standalone and OpAMP-managed collectors. A bare `host:port` keeps plain HTTP, so existing setups are unchanged. The value must not include a path (`/write` is still appended automatically); the collector now exits at startup with a clear error if it does, or if the scheme is anything other than `http` or `https`. The OpAMP-managed collector scraped the endpoint over plain HTTP regardless of its scheme. ClickHouse is now scraped in its own `clickhouse` job (previously the `otelcol` job) using the endpoint's scheme, with TLS verification on. The collector's own metrics endpoint stays on plain HTTP. A bare `host:port` or `http://` endpoint behaves as before. Scraped ClickHouse metrics now carry `job="clickhouse"` instead of `job="otelcol"`. - c94d12c: fix: Resolve PromQL queries through the source instead of a caller-supplied connection and table The `/api/v1/prometheus/*` routes now take `sourceId` instead of `connectionId`, and read the connection and TimeSeries table from that PromQL source. Caller-supplied `database`/`table` params are ignored for ClickHouse and no longer forwarded to external Prometheus endpoints. Saved PromQL chart configs now require `source` and no longer persist `connection`, so provisioned dashboards with PromQL tiles must set `source`. - be5304d: Forward the bytes consumed by the ClickHouse proxy's body parsers without changing JSON values, form fields or text encodings. Keep request framing in sync with inflated bytes and preserve unparsed multipart streams. - af6e7f6: Add a description field to the tile editor. The description is now stored on the tile's chart config next to its name, and is kept for series tiles in the external Dashboards API. - 02080f2: Add an optional tile description, accepted by the external Dashboards API and shown as an info tooltip next to the tile name. - ce6654d: refactor: Store the time display setting as a time zone instead of a UTC switch, converting existing settings, and remove the `isUTC` option from `formatDate` - 28168ae: fix: Floor the time filter start to the precision of a coarse timestamp column (DateTime, DateTime64 below ms) when a finer column also filters the range, so a sub-second start no longer drops the rest of that tick; make the external API v2 search `maxResults` cap configurable via `EXTERNAL_API_SEARCH_MAX_RESULTS` (defaults to 2000, the previous hardcoded value) - ecddea2: fix: reject generic and incident.io webhook body templates that don't compile, or that call a helper delivery doesn't register (e.g. a typo for `eq`), with a 400 on create, update, and test (internal API, External API v2, and MCP `clickstack_save_webhook`). Add `POST /api/v2/webhooks/validate` to check a webhook config without saving it, e.g. at Terraform plan time. - Updated dependencies [9205823] - Updated dependencies [3e9185d] - Updated dependencies [180a1f1] - Updated dependencies [a682ce1] - Updated dependencies [9891a79] - Updated dependencies [43b09a7] - Updated dependencies [0000fa4] - Updated dependencies [eb72cfa] - Updated dependencies [03f9fd5] - Updated dependencies [5cabc11] - Updated dependencies [a4b6395] - Updated dependencies [3bedfab] - Updated dependencies [c94d12c] - Updated dependencies [95aef70] - Updated dependencies [bc47e48] - Updated dependencies [85675fc] - Updated dependencies [af6e7f6] - Updated dependencies [02080f2] - Updated dependencies [dab6eb8] - Updated dependencies [ce6654d] - Updated dependencies [28168ae] - Updated dependencies [807cd9a] - Updated dependencies [cfa865b] - @hyperdx/common-utils@0.32.0 ## @hyperdx/app@2.42.0 ### Minor Changes - fe59feb: feat: Edit chart alerts in a side panel docked beside the chart editor, so the preview chart stays in view while the alert is tuned. "Add alert" / "Edit alert" opens the panel; a new alert is kept with "Add alert" and an existing one with "Update alert", while closing the panel discards the changes. The panel fills the editor's visible height and can be resized from its left edge. Save and Cancel move to the chart name row. - 1773c11: feat: add a content font size preference Small text in the log/trace table was not adjustable without browser zoom, which also shrinks the surrounding chrome. User preferences now carry a **Content font size** setting (small / medium / large) that scales the search results table, data tables, expanded row JSON, and chart axes, legends, and tooltips. Small is the default and matches the previous sizes, so nothing changes until you opt in. - 95aef70: Add an optional `eventNameExpression` to log sources, pointing at the OpenTelemetry log EventName. Source auto-detection sets it when an OTel log table has a string `EventName` column. The field is stored and editable only: nothing reads it yet. HDX-5787 - bc47e48: Add an optional `severityNumberExpression` to log sources, pointing at the numeric OpenTelemetry SeverityNumber (1-24). Source auto-detection sets it when an OTel log table has an integer `SeverityNumber` column. The field is stored and editable only: log levels still come from the severity text. - 7eea0fd: feat: Pick any time zone in User Preferences, replacing the "Use UTC time" switch. Local and UTC stay pinned at the top of the list - cfa865b: feat: expose group-by values to webhook body templates as `{{var.<column>}}`, and keep them on resolve notifications ### Patch Changes - 9205823: feat: Broadcast adhoc dashboard filter conditions - 3e9185d: feat: Filter search tile cells into adhoc dashboard filters - 180a1f1: feat: Filter table tile group-by cells into adhoc dashboard filters - 7db73cf: feat: Filter time chart series into adhoc dashboard filters - a682ce1: feat: Render adhoc dashboard filter inputs - 9891a79: feat: Add ad hoc dashboard filter schemas and types - 43b09a7: feat: Add UI for configuring adhoc dashboard filters - f56d760: feat: use the click-ui `Badge` for the sidebar "Beta", "UTC", and instance label badges, and for the "Latest", "New", and "Breaking" badges in What's new. - 5253b86: fix: Stop abbreviating values between 500 and 999 as "0.5k"/"0.8k" on chart axes and in large number format - 89ad276: When a removed column is one field in a multi-field sort, drop only that field - 4d74bd5: fix: saved search is usable on a phone. Below 900px the filters open in a drawer, SELECT and ORDER BY stay behind "Columns", and the log table keeps the remaining screen. - 72ed406: fix: the time picker no longer resets a selected date range to the current year. The range text now includes the year when the range isn't in the current year (or reaches into the future), so it survives being re-parsed. - c0af58b: fix: Let the Services > Database query drawer use the span attribute skip index instead of scanning the whole trace window - 0444e3e: fix: point the "Connect to Demo Server" connection at the new demo ClickHouse - f33fc83: Count spans recorded with the legacy `STATUS_CODE_ERROR` status as errors on the Services and LLM dashboards. The errored-span definition now lives in one place and matches both that value and the short `Error` form, so those dashboards no longer undercount errors from collectors that emit the long form. - a89b324: fix: the search page filter key limit (Filter Keys Fetch Limit, default 100) now applies to each Map column, so keys from `LogAttributes` no longer crowd out `ResourceAttributes` and other Map columns - 7d77d27: fix: Fix table sort state and support order by without asc/desc - f1bdd8f: Fix expanding a row on tables with a string `id` column: row details now keep the internal WHERE-clause id instead of being overwritten by the raw column value (ClickHouse#3370) - f078e6b: feat: Add group-by to the search page histogram - 0840b8b: feat: Support source remapping during ad hoc filter import - 2c2295f: feat: use the click-ui `Badge` for all status badges on the Kubernetes dashboard and its node, namespace, and pod drawers. - 0000fa4: feat: Add a line interpolation display setting (smooth, linear, step) - 3db148a: feat: use the click-ui `Badge` for the model, provider, tool, agent, and chat role badges in the LLM span panel, session drawer, and dashboard. - eb72cfa: feat(mcp): add `clickstack_service_health`, a one-call per-service health report for a trace source (throughput, error rate, p50/p95/p99, top error groups), compared with yesterday and ranked worst first - 938494d: feat: use the click-ui `Badge` in the metric explorer, the chart series and formula editors, the tile alert editor, the materialized view indicator and modal, and the span kind in the row side panel. - bc0a1e4: refactor: Look up every source in useMultipleGetKeyValues and keep partial results when one fails - f21bdb8: fix: keys inside Map and JSON filter groups (e.g. `LogAttributes`) in the search page filter sidebar stay expanded or collapsed after scrolling them out of view and back. - b000bbb: fix: add a key search to Map and JSON filter groups (e.g. `ResourceAttributes`, `LogAttributes`) in the search page filter sidebar, matching the value search on regular filters. - 8b92b7a: fix: keys inside Map and JSON filter groups (e.g. `LogAttributes`) in the search page filter sidebar are sorted alphabetically, after keys with a selected value and pinned keys. - 5cabc11: feat: Support distribution heatmaps calculated from promql data - e7b38c2: fix: Show the Exception panel for logs and spans that carry `exception.type`, `exception.message` or `exception.stacktrace` attributes, not only for `exception` span events; show correlated logs in the trace waterfall even when their span was never exported, and color error logs and `exception` span events red there; and set `spanEventsValueExpression` and `spanLinksValueExpression` on the default trace sources so span exceptions and links load out of the box. The default sources also gain the summary and exponential histogram metric tables and drop key rollup tables the schema no longer creates. - 097cb7c: fix: Read current OpenTelemetry HTTP and RPC semantic conventions (with deprecated names as fallbacks) in the span side panel and trace waterfall: redesign the HTTP panel around a request line and details, add a gRPC/RPC call panel, and flag correlated log errors and warnings by SeverityNumber or case-insensitive SeverityText - 07de613: refactor: useMultipleAllFields returns each table connection's fields and columns. - a4b6395: fix: render percentile aggregations (`p50`, `p90`, `p95`, `p99`) as `quantile` at the matching level. A chart config with `aggFn: 'p95'` rendered as the nonexistent ClickHouse function `p95If`. The AI assistant now returns percentiles with a level instead of a bare `quantile` (median), and v1 metric chart conversion keeps the percentile level. - 3bedfab: feat: Support histogram heatmaps from PromQL bucket metrics - c94d12c: fix: Resolve PromQL queries through the source instead of a caller-supplied connection and table The `/api/v1/prometheus/*` routes now take `sourceId` instead of `connectionId`, and read the connection and TimeSeries table from that PromQL source. Caller-supplied `database`/`table` params are ignored for ClickHouse and no longer forwarded to external Prometheus endpoints. Saved PromQL chart configs now require `source` and no longer persist `connection`, so provisioned dashboards with PromQL tiles must set `source`. - 30debf9: speed up the sessions query by scoping the trace scan to RUM services - b9f6170: chore: upgrade @tanstack/react-query to 5.90 and keep a cancelled filter values stream's partial results out of the cache - 685f465: fix: Wrap long values in release marker tooltips instead of overflowing - f41027f: feat: use the click-ui `Badge` for the exception breadcrumb status and level chips, the AI Assistant "Experimental" label, and the materialized view config pills. Remove the unused stacktrace, breadcrumb, and source map code from `LogSidePanelElements`, and the unused `StacktraceFrame` component. - e3f24b9: fix: Preserve multi-line SQL in search replay links - d341ba0: fix: resolve row details with the primary key plus whichever of `_block_number` / `_block_offset` the table persists, instead of falling back to the slow full-row lookup when only one of `enable_block_number_column` / `enable_block_offset_column` is set - 3ff761b: feat: Remember the service map sampling rate in local storage; a `samplingFactor` URL param still takes precedence - 96a0861: Show a Group by field count as a number on a narrow screen - 85675fc: perf: stream filter values into the UI as each query finishes instead of waiting for all of them - d3e6f96: feat: use the click-ui `Badge` for dashboard and alert tags, and for the "Experimental", "Beta", "Disabled", "Dev only", team member, and pending invite badges in settings. - e8f0f94: fix: Keep the dashboard tile menu on screen when a dashboard has many groups, by moving the group list into a scrollable "Move to group" submenu - af6e7f6: Add a description field to the tile editor. The description is now stored on the tile's chart config next to its name, and is kept for series tiles in the external Dashboards API. - 02080f2: Add an optional tile description, accepted by the external Dashboards API and shown as an info tooltip next to the tile name. - dab6eb8: fix: Read and show time picker ranges in the display time zone, so applying a range with "Use UTC time" on no longer shifts it for users west of UTC - ce6654d: refactor: Store the time display setting as a time zone instead of a UTC switch, converting existing settings, and remove the `isUTC` option from `formatDate` - d1cecf8: Fix the trace waterfall sticking on the previously opened span when another span of the same trace is opened, including from a session replay. Closing the result clears the selected span. Opening a different result selects that result's span, including when the waterfall stays mounted on cached data. - 807cd9a: fix: the webhook form now compiles the body template as you edit it and shows the Handlebars error with its line and column. A broken template used to give no feedback until an alert failed to deliver. - Updated dependencies [9205823] - Updated dependencies [3e9185d] - Updated dependencies [180a1f1] - Updated dependencies [a682ce1] - Updated dependencies [9891a79] - Updated dependencies [43b09a7] - Updated dependencies [27cccf7] - Updated dependencies [8d064df] - Updated dependencies [9c2c703] - Updated dependencies [0000fa4] - Updated dependencies [ee9f84b] - Updated dependencies [24d324c] - Updated dependencies [748315d] - Updated dependencies [eb72cfa] - Updated dependencies [cdfae45] - Updated dependencies [b00d410] - Updated dependencies [a06b17f] - Updated dependencies [e494a46] - Updated dependencies [03f9fd5] - Updated dependencies [5cabc11] - Updated dependencies [a4b6395] - Updated dependencies [3bedfab] - Updated dependencies [ca8e3fd] - Updated dependencies [c94d12c] - Updated dependencies [be5304d] - Updated dependencies [95aef70] - Updated dependencies [bc47e48] - Updated dependencies [85675fc] - Updated dependencies [af6e7f6] - Updated dependencies [02080f2] - Updated dependencies [dab6eb8] - Updated dependencies [ce6654d] - Updated dependencies [28168ae] - Updated dependencies [ecddea2] - Updated dependencies [807cd9a] - Updated dependencies [cfa865b] - @hyperdx/common-utils@0.32.0 - @hyperdx/api@2.42.0 ## @hyperdx/common-utils@0.32.0 ### Minor Changes - 95aef70: Add an optional `eventNameExpression` to log sources, pointing at the OpenTelemetry log EventName. Source auto-detection sets it when an OTel log table has a string `EventName` column. The field is stored and editable only: nothing reads it yet. HDX-5787 - bc47e48: Add an optional `severityNumberExpression` to log sources, pointing at the numeric OpenTelemetry SeverityNumber (1-24). Source auto-detection sets it when an OTel log table has an integer `SeverityNumber` column. The field is stored and editable only: log levels still come from the severity text. - cfa865b: feat: expose group-by values to webhook body templates as `{{var.<column>}}`, and keep them on resolve notifications ### Patch Changes - 9205823: feat: Broadcast adhoc dashboard filter conditions - 3e9185d: feat: Filter search tile cells into adhoc dashboard filters - 180a1f1: feat: Filter table tile group-by cells into adhoc dashboard filters - a682ce1: feat: Render adhoc dashboard filter inputs - 9891a79: feat: Add ad hoc dashboard filter schemas and types - 43b09a7: feat: Add UI for configuring adhoc dashboard filters - 0000fa4: feat: Add a line interpolation display setting (smooth, linear, step) - eb72cfa: feat(mcp): add `clickstack_service_health`, a one-call per-service health report for a trace source (throughput, error rate, p50/p95/p99, top error groups), compared with yesterday and ranked worst first - 03f9fd5: fix(mcp): describe every MCP tool input, drop descriptions that restate schema constraints, and keep tool descriptions under Claude Code's 2 KB limit - 5cabc11: feat: Support distribution heatmaps calculated from promql data - a4b6395: fix: render percentile aggregations (`p50`, `p90`, `p95`, `p99`) as `quantile` at the matching level. A chart config with `aggFn: 'p95'` rendered as the nonexistent ClickHouse function `p95If`. The AI assistant now returns percentiles with a level instead of a bare `quantile` (median), and v1 metric chart conversion keeps the percentile level. - 3bedfab: feat: Support histogram heatmaps from PromQL bucket metrics - c94d12c: fix: Resolve PromQL queries through the source instead of a caller-supplied connection and table The `/api/v1/prometheus/*` routes now take `sourceId` instead of `connectionId`, and read the connection and TimeSeries table from that PromQL source. Caller-supplied `database`/`table` params are ignored for ClickHouse and no longer forwarded to external Prometheus endpoints. Saved PromQL chart configs now require `source` and no longer persist `connection`, so provisioned dashboards with PromQL tiles must set `source`. - 85675fc: perf: stream filter values into the UI as each query finishes instead of waiting for all of them - af6e7f6: Add a description field to the tile editor. The description is now stored on the tile's chart config next to its name, and is kept for series tiles in the external Dashboards API. - 02080f2: Add an optional tile description, accepted by the external Dashboards API and shown as an info tooltip next to the tile name. - dab6eb8: fix: Read and show time picker ranges in the display time zone, so applying a range with "Use UTC time" on no longer shifts it for users west of UTC - ce6654d: refactor: Store the time display setting as a time zone instead of a UTC switch, converting existing settings, and remove the `isUTC` option from `formatDate` - 28168ae: fix: Floor the time filter start to the precision of a coarse timestamp column (DateTime, DateTime64 below ms) when a finer column also filters the range, so a sub-second start no longer drops the rest of that tick; make the external API v2 search `maxResults` cap configurable via `EXTERNAL_API_SEARCH_MAX_RESULTS` (defaults to 2000, the previous hardcoded value) - 807cd9a: fix: the webhook form now compiles the body template as you edit it and shows the Handlebars error with its line and column. A broken template used to give no feedback until an alert failed to deliver. ## @hyperdx/otel-collector@2.42.0 ### Patch Changes - ab563d2: Stop the collector logging a `ParseJSON` syntax error for every log body that contains braces but isn't JSON (e.g. Go struct or Python dict dumps). JSON extraction now only matches objects that start with a quoted key, and remaining parse failures in that statement group are silenced. - ca8e3fd: fix: support https for the PromQL remote-write endpoint and ClickHouse scraping `CLICKHOUSE_PROMETHEUS_METRICS_ENDPOINT` now accepts an `http://` or `https://` URL as well as a bare `host:port`. With `https://`, the collector's `prometheusremotewrite` exporter sends metrics over TLS and verifies the server certificate against the system CAs. Previously the exporter always used plain HTTP, so an HTTPS-only endpoint could not receive metrics. This applies to both standalone and OpAMP-managed collectors. A bare `host:port` keeps plain HTTP, so existing setups are unchanged. The value must not include a path (`/write` is still appended automatically); the collector now exits at startup with a clear error if it does, or if the scheme is anything other than `http` or `https`. The OpAMP-managed collector scraped the endpoint over plain HTTP regardless of its scheme. ClickHouse is now scraped in its own `clickhouse` job (previously the `otelcol` job) using the endpoint's scheme, with TLS verification on. The collector's own metrics endpoint stays on plain HTTP. A bare `host:port` or `http://` endpoint behaves as before. Scraped ClickHouse metrics now carry `job="clickhouse"` instead of `job="otelcol"`.
Opt-in proxy-header SSO so Authentik is the only login. New self-contained middleware/proxyHeaderAuth.ts + one hook in isUserAuthenticated + additive PROXY_AUTH_* config (fails closed without a shared secret; never honors a bare session in proxy mode) + gate sign-out on logout. Stock behavior when disabled. Adversarially reviewed; typecheck + 15 tests + lint green. Edge controls (strip inbound headers + NetworkPolicy + shared secret) are mandatory — see GASCITY-FORK.md.
…city/hyperdx On push to gascity-sso (+ dispatch): verify (lint/typecheck/unit tests) and build the docker/hyperdx 'prod' target, assert the proxy-auth symbols are in the image (no silent stock ship), and push :gascity-sso + :2.28.0-gascity-<sha>. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- EMAIL_RE drops the \x00-\x1f\x7f ranges (the prettier wrap had moved the regex off the eslint-disable-next-line); control chars are now checked in code via hasControlChar(), so no lint carve-out is needed. - CI test step uses an anchored --testPathPattern so it runs only the two DB-free unit suites (a bare 'config' also matched renderChartConfig.test.ts → needs DBs). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Accept a distinct previous value during attended rotations while keeping the current value mandatory and the middleware fail-closed. Tighten the production artifact guard and prevent image publication before verification succeeds.
Run auth verification for ordinary pull requests without secrets or write credentials. Replace inherited pull_request_target AI reviewers with visibly disabled manual stubs until a separately reviewed isolation design exists.
…e logged or forwarded The gate injects the shared secret on every request. Stock HyperDX copies req.headers verbatim in two places: the pino-http request log (stdout, shipped to the log store) and the ClickHouse proxy (forwarded to the connection host, which any HyperDX user can configure). consumeProxyAuthSecret is mounted first in api-app.ts, parks the presented value in a WeakMap keyed by the request, and deletes it from headers/rawHeaders; sharedSecretOk reads the parked value. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- derive the image/CODE_VERSION tag from packages/app/package.json instead of the hard-coded 2.28.0 - exact-file symbol guard: the 2.42 API build is flat (build/*.js, rootDir=src), not build/src/*.js; also guard consumeProxyAuthSecret and the auth.ts hook - run the fork-owned proxyAuth* suites Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… hosts
The ClickHouse proxy copies every incoming request header (httpxy
setupOutgoing: outgoing.headers = { ...req.headers }) to the connection's
host, which any authenticated user can set to an arbitrary URL. That hands the
browser's session cookie, the SSO gate's cookie (_oauth2_proxy) and any bearer
token to that host. ClickHouse authenticates with the X-ClickHouse-* headers the
proxy sets, so drop cookie and authorization from the outgoing request.
Upstreamable as-is.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Once gascity-sso is the default branch, schedule / issues / issue_comment / pull_request_target events read their workflow files from it, so upstream's Release Nightly, Vulnerability Alerts, Claude/Deep review, vouch, etc. would run on the public fork (and fail, or act with the fork's GITHUB_TOKEN). Keep only gascity-image.yml and gascity-pr.yml, and make both fail if a rebase re-adds an inherited workflow file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ke harness - gascity-sso is the default branch; main is frozen - rebase recipe: keep inherited workflows deleted, verify before pushing (a textually clean rebase broke three ways on 2.28 -> 2.42) - document the hook shape, consumeProxyAuthSecret and the ClickHouse proxy credential strip; secret-hygiene and self-telemetry requirements - gascity/smoke: local auth-matrix + leak checks against FerretDB Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
CodeQL (js/polynomial-redos) flagged EMAIL_RE: its domain part,
[^\s@,]+\.[^\s@,]+, is ambiguous because the label class also matches '.',
so a long dotted header value backtracks quadratically (about 5 s for 50k
characters). It was only reachable after the shared secret matched, but it is
fork-owned code on the auth path.
Validate structurally instead: at most 254 characters (RFC 5321), no
whitespace or comma, exactly one '@' with a non-empty local part, and two or
more non-empty dot-separated domain labels. This is stricter only for empty
labels ("a@b..c", "a@.b.c", "a@b.c."), which no gate-asserted address has.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
f19ea53 to
c160553
Compare
|
Force-pushed this PR's own branch, Re-verified on
The body is updated to match. 🤖 Generated with Claude Code |
… capture consumeProxyAuthSecret takes the shared secret off the request before any Express middleware can log or forward it, but the bundled OpenTelemetry SDK's HTTP instrumentation runs earlier, at the Node http server. With HDX_NODE_ADVANCED_NETWORK_CAPTURE on it records every incoming request header on the server span as the request arrives. The smoke harness, run with it on, found http.request.header.x_hdx_proxy_auth_secret (plus the session cookie, the gate cookie and any bearer token) in the exported traces. The setting is opt-in and is not used by our deployment, but it is the documented way to debug request headers, so turn that posture into a startup error instead of a silent leak, like the missing-secret check. Truthy spellings mirror the SDK's own parsing (true/yes/1, case-insensitive, trimmed). The error names the setting, never the secret. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and the proxy strip - gascity-image: packages: write moves from the workflow to the build job, and both checkouts stop persisting the token. The verify job runs `yarn install`, which runs dependency lifecycle scripts; with the persisted credential and a workflow-wide packages: write, a compromised dependency could have read a token able to push to the image repository. - Both symbol guards now also require the advanced-network-capture refusal in config.js and the credential strip (removeHeader) in routers/api/clickhouseProxy.js, so a rebase that drops either fails CI instead of shipping. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…etry and rotation New smoke cases (fake values only): - T5b canonical-case gate headers; T5c trailing-dot domain (401); T5d a plus-address is its own identity, never linked to the base address - T9b an identity switch issues a new session id (no fixation) - T13c a bearer token is not forwarded to the connection host - T14c/T14d the gate cookie and a bearer token never reach stdout; T14e/T14f exercise the error-handler and console.error paths with the secret present - T15b no browser credential in the OTLP export - T16b/T16c advanced network capture refuses to start, without echoing the secret - `telemetry`: the production posture (no HYPERDX_API_KEY, no OTLP endpoint) starts no SDK, hands out no apiKey and exports nothing - `rotation`: the current/previous overlap window, which GASCITY-FORK.md already claimed the harness covered - SMOKE_LOG_LEVEL (e.g. trace) for a verbose-logging run GASCITY-FORK.md: document the capture refusal, that the gate cookie must be SameSite=Lax or Strict (it is the effective credential in proxy mode), that the email is the identity, and that connection hosts are user-editable. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…into gascity-sso The first parent is the reviewed patch stack on @hyperdx/app@2.42.0; the second is the previous gascity-sso (2.28.0 base, 7b24ad4; to be tagged gascity-sso/2.28.0), kept reachable so the deployed 2.28.0-gascity-7b24ad4fa2f9 image stays traceable to its source. Tree == the first parent (merge -s ours). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
c160553 to
2ae4ced
Compare
Security and correctness review (head
|
| # | Commit | Why |
|---|---|---|
| 13 | 3f2370eb fix(auth): refuse to start proxy auth with the SDK's advanced network capture |
See F1b below. |
| 14 | c3b763d7 ci(gascity): least-privilege image workflow; guard the capture check and the proxy strip |
Two changes: packages: write is now granted only to the publishing job, and neither checkout persists the token. |
| 15 | 8855373e test(gascity): smoke attack cases for credentials, error paths, telemetry and rotation |
New harness cases, plus doc updates. |
Findings
| Sev | Where | Finding | Status |
|---|---|---|---|
| Medium (latent) | packages/api/src/config.ts (fork block) |
The bundled OpenTelemetry SDK's HTTP instrumentation runs at the Node http server, before consumeProxyAuthSecret. With HDX_NODE_ADVANCED_NETWORK_CAPTURE on, it records every request header on the server span. The smoke run with it on exported http.request.header.x_hdx_proxy_auth_secret, the session cookie, the gate cookie and a bearer token. It is opt-in, and our deployment does not set it. |
Fixed in 13: the API refuses to start with this setting and proxy auth both on. Covered by 10 unit tests and smoke T16b/T16c. The new T16b fails against c160553b. |
| Low | .github/workflows/gascity-image.yml |
packages: write applied to the whole workflow, and both checkouts persisted the token. Meanwhile verify runs yarn install, which runs dependency lifecycle scripts. |
Fixed in 14 |
| Low | both workflows | The symbol guard did not cover the ClickHouse-proxy credential strip, so a rebase that dropped it would still pass CI. | Fixed in 14: routers/api/clickhouseProxy.js:removeHeader is now guarded. |
| Info | GASCITY-FORK.md |
The doc said the smoke harness covered the rotation window. It did not. | Fixed in 15 with a rotation subcommand. The doc now also covers three more points: the gate cookie must be SameSite=Lax or Strict (in proxy mode it is the credential that counts), the email is the identity, and connection hosts are user-editable. |
| Medium (pre-existing, upstream design) | connection hosts | Any user can point a connection at any host the API can reach. The ClickHouse proxy then sends that user's requests there (any path, the user's body and added headers) and returns the response. This PR stops the proxy from carrying browser credentials and the shared secret, but the request itself is upstream behavior and unchanged since 2.28. | Not fixed here; follow-up. Restrict egress in the deployment, and report it upstream. This does not block the merge. |
| Low | proxyHeaderAuth.ts hasProxyAuthSession |
The email-domain allowlist is checked when a session is established, but not on later requests whose session already matches. This only matters if the gate itself asserts a foreign domain. | Not changed |
Verified
1. Header trust is gated on the shared secret.
- The identity header is honored only after
timingSafeEqualmatches the current or previous secret. - Both comparisons are always computed. Length is checked first, an empty configured slot never matches, and startup refuses both a missing secret and previous == current.
getProxyAuthEmailis the only reader of the identity header.- Paths that skip
isUserAuthenticatednever read it:- root router:
/health,/ready,/installation,/login/password,/register/password,/team/setup/:token,/logout - Bearer-only:
/mcpand/api/v2 - OpAMP, on its own port
- Next.js:
/api/configand static assets
- root router:
- There are no websocket or SSE routes outside the guarded routers.
2. Sessions bind to the verified identity.
req.login()regenerates the session (passport 0.6). Smoke T9b confirms a new session id after an identity switch.- Every request re-checks the session user's email against the vouched identity, and bare sessions get 401.
3. Email edge cases.
- Values are trimmed and lowercased.
- The check requires exactly one
@, no whitespace, comma or control characters, non-empty labels (so a trailing dot is rejected) and at most 254 characters. The domain allowlist is an exact match. - Plus-addresses and lookalike local parts become distinct accounts, never links (smoke T5c/T5d).
- The user lookup is an exact match on the lowercased value, with no regex.
4. The secret is stripped before every logger and the proxy.
consumeProxyAuthSecretis the first app middleware; only Express's own init runs before it.- I ran the smoke harness with
SMOKE_LOG_LEVEL=traceandOTEL_LOG_LEVEL=debug. That produced several thousand stdout lines, including the OTLP exporter's debug dump of every record. None contained the secret, the session cookie, the gate cookie or a bearer token, on the 400 error-handler path and theconsole.errorpath as well. - The only exception was the opt-in capture above.
5. The ClickHouse proxy strips credentials.
cookieandauthorizationare removed, and the secret is already consumed. The capture host received none of them (T13–T13c).X-Auth-Request-EmailandX-Forwarded-*still reach the connection host.
6. Nothing in the 2.28 patch was lost.
- The hook's decision table is the same as in 2.28. The success path now runs through upstream's
setBusinessContextand query attribution. - The email check is stricter only for empty labels and values over 254 characters.
7. Migrations are additive.
- 2.42 adds no new migrate-mongo scripts.
- The startup backfill catches its own errors.
- The new indexes are additive, and
accessKeyhas a UUID default, so its unique index can build. seed/verify-seedread back cleanly on 2.42.
8. Gates.
- Lint: 0 errors, 301 warnings (budget 302, none on fork lines).
tsc: clean.- Jest: 4 suites, 82/82.
- API build plus the PR symbol guard: 10/10.
- Docker
--target prodbuilds a fresh image; the exact in-image guard passes 10/10.
9. Smoke against the fresh image.
- Setup: FerretDB 2.5.0, fake secrets,
1000:1000,--cap-drop ALL. authpasses 31/31 three times: at the default log level, at trace level, and in the production posture (noHYPERDX_API_KEY, no OTLP endpoint).telemetrypasses 4/4: the SDK is skipped in the API and the alert task,/api/configreturns noapiKey, and nothing is exported.rotationpasses 7/7 andfailclosed3/3.
10. CodeQL. 2ae4cede has the same 70 alerts as c160553b, matched by path and rule, and all of them are on upstream lines; the polynomial-regex alert now points at upstream common-utils. The new commits add none. CodeQL is not a required check. auth quality gate is green on 2ae4cede.
Verdict: OK to merge now, with Create a merge commit. The image digest for the repin must come from the gascity-image run for this merge.
🤖 Generated with Claude Code
|
Addendum: I re-ran the rollback rehearsal against this head's code, with the previous fork image and this PR's image taking turns on one fresh FerretDB 2.5.0 database: 2.28 → 2.42 → 2.28.
One operational caveat for anyone rolling back: the 2.28 image answers 404 on 🤖 Generated with Claude Code |
Rebases the gascity reverse-proxy (forward-auth) SSO patch from
@hyperdx/app@2.28.0onto@hyperdx/app@2.42.0(cf00ae53, released 2026-10-23). It folds in #2 (rotating the secret with an overlap window, plus CI hardening) and adds two security fixes.Merge with "Create a merge commit". Do not squash or rebase-merge. This branch is the 15-commit stack on 2.42.0 plus a
merge -s oursof the currentgascity-sso(7b24ad4f). The tree equals the stack, and the old tip stays reachable as the second parent. That keeps the deployed2.28.0-gascity-7b24ad4fa2f9image traceable to its source without a force-push or any branch-protection change.Security findings
No secret values appear in this PR, its commits or its test fixtures. The smoke harness uses fake, test-only values.
req.headersverbatim, so the secret ended up in every API request log line, along with session and gate cookies. Stock 2.42 still logs this header: upstream redacts onlyauthorization,cookie,set-cookieandlocation. Those logs are shipped to the log store, where HyperDX users can read them, and were also exported over OTLP.97dd676f.consumeProxyAuthSecretis mounted first inapi-app.ts, ahead of pino-http. It moves the presented secret into a module-privateWeakMapand deletes it fromreq.headersandrawHeaders. Rotating the secret after deploy is still required.outgoing.headers = { ...req.headers }) forwarded the secret, the gate cookie and any bearer token to the connection host, and any HyperDX user can point that host anywhere.97dd676fremoves the secret, and53376697stripscookieandauthorizationfrom the proxied request. ClickHouse authenticates with theX-ClickHouse-*headers the proxy sets. The strip can go upstream as-is.HDX_NODE_ADVANCED_NETWORK_CAPTUREon, it records every request header on the server span, so the secret, cookies and bearer tokens would be exported in traces (reproduced with the smoke harness). Opt-in; our deployment does not set it.3f2370eb: the API refuses to start with proxy auth and advanced network capture both on.HYPERDX_API_KEYwithout an OTLP endpoint. The bundled SDK therefore exported the app's own logs, traces and metrics (including F1's secret) to HyperDX's hosted ingest, and/api/configreturned the key to unauthenticated callers.The trust logic itself checked out correct:
timingSafeEqualafter a length check.Commits (
git range-diff @hyperdx/app@2.28.0..7b24ad4f @hyperdx/app@2.42.0..2ae4cede^1)8ad46914feat(auth): reverse-proxy (forward-auth) SSOauth.ts4139c2a7,47412501,28efccfcci / lint / jest fixes=)f567cb14feat(auth): support overlapping proxy secret rotation87dde18a), conflict-resolved (R4)a38ca3bcci: add safe Gas City PR quality gatef09be641), conflict-resolved (R5, R6)97dd676ffix(auth): take the proxy-auth secret off the request before it can be logged or forwardedc5b2f4a2ci(gascity): adapt image + PR workflows to the 2.42 base53376697fix(api): do not forward browser credentials to ClickHouse connection hosts07aa2451ci(gascity): drop the inherited upstream workflows from the build branch7fdaee3bdocs(gascity): refresh GASCITY-FORK.md for the 2.42 base; add the smoke harnessa7c6d7cffix(auth): validate the gate email header in linear time3f2370ebfix(auth): refuse to start proxy auth with the SDK's advanced network capturec3b763d7ci(gascity): least-privilege image workflow; guard the capture check and the proxy strip8855373etest(gascity): smoke attack cases for credentials, error paths, telemetry and rotation2ae4cedemerge -s ours ofgascity-ssoThe PR diff shows the whole upstream release (1,798 files). To review only the fork delta (32 files, +2,038/−5,253; the deletions are the 18 inherited workflows), run:
Upstream-owned files changed:
api-app.ts+4,middleware/auth.ts+15,config.ts+72 (one additive block),routers/api/root.ts+7,routers/api/clickhouseProxy.ts+6. Everything else is fork-owned.The rebase broke silently in three places, all fixed
The 4 original commits applied with no textual conflicts, but the result was still broken:
middleware/auth.ts: upstream removedsetTraceAttributesand moved the success path tosetBusinessContext+nextWithQueryAttribution. The old hook no longer type-checked and skipped query attribution. The hook is now 4 lines: in proxy mode, unless the session already belongs to the gate-vouched identity,establishProxyAuthSession401s or logs the user in and re-entersisUserAuthenticated, ending on upstream's own success path. It cannot loop. The decision table is unchanged; the newproxyAuthGuard.test.ts(12 cases) proves it, and disabling the hook fails 8 of 12.no-unsafe-type-assertionwith--max-warnings 302, and the patch's tests pushed the count to 309. The two fork test files now use file-level disables, and the fork's production code adds 0 warnings. Total: 301, equal to upstream's baseline.build/*.js, notbuild/src/*.js), so the old guard paths would have failed or passed vacuously. The guard now checks 8 exact file:symbol pairs, includingconsumeProxyAuthSecretandestablishProxyAuthSession, and fails closed if a path moves.Also: #2's tests moved to the fork-owned
__tests__/proxyAuthConfig.test.ts, so upstream'sconfig.test.tsis byte-identical again (R4). All inherited upstream workflows are deleted, and both fork workflows fail if a rebase re-adds one (R5). The image tag andCODE_VERSIONnow derive frompackages/app/package.json, so this build is tagged2.42.0-gascity-<sha12>(R7).Verification (on
2ae4cede, Node 22.23)yarn install --immutable, then common-utilsdev:build: OKyarn workspace @hyperdx/api run lint: 0 errors, 301 warnings (budget 302; none on fork lines)tsc --noEmit: cleanproxyHeaderAuth,proxyAuthGuard,proxyAuthConfig, upstreamconfig): 4 suites, 82/82 tests passyarn workspace @hyperdx/api run build, then the PR-gate symbol guard: 10/10buildx --target prod, using the same contexts and args asgascity-image.yml(CODE_VERSION=2.42.0-gascity-9ba6586f593a;9ba6586fdiffers from2ae4cedeonly ingascity/smoke/smoke.sh): builds, node 22.22.3,USER nodegascity-image.ymlscript: 10/10 present, now including the advanced-capture refusal inconfig.jsand the credential strip inclickhouseProxy.js.gascity/smokeagainst the built image: FerretDB 2.5.0 and postgres-documentdb at pinned digests, capture servers standing in for the ClickHouse connection host and the OTLP endpoint, fake secrets, run as 1000:1000 with--cap-drop ALL.auth31/31, three times: at the default log level, atSMOKE_LOG_LEVEL=tracewithOTEL_LOG_LEVEL=debug(several thousand stdout lines, including the OTLP exporter's debug dump of every record), and in the production posture (noHYPERDX_API_KEY, no OTLP endpoint).telemetry4/4: in that posture the SDK skips initialization in the API and the alert task,/api/confighands out noapiKey, and nothing is exported.rotation7/7 (current/previous overlap, then previous cleared; previous == current refuses to start without printing the value) andfailclosed3/3 (T16, T16b, T16c).seed/verify-seed: the dashboard, saved search and alert read back (the alert shows the backfilled display name)./readyreturns 200 and there are 0 restarts.c160553b, as expected: that build starts with advanced network capture on.The smoke matrix covers:
console.errorpathsRun on the prototype (this stack before review commits 13–15; from the design doc, not repeated here):
DEFAULT_SOURCES, via/api/v2/search,/api/v2/charts/seriesand the browser/api/clickhouse-proxyCodeQL on this PR
The CodeQL check reports 71 "new" alerts because the entire 2.42 release appears as changed code in this PR. 70 of them are on upstream code. The one on fork code was
js/polynomial-redoson the gate email check inproxyHeaderAuth.ts.[^\s@,]+\.[^\s@,]+is ambiguous because the label class also matches., so a long dotted header value took about 5 s per request for 50k characters. That check was only reachable after the shared secret matched, and it was already in 2.28. Commit 12 replaces it with a linear structural check: at most 254 characters, no whitespace or comma, exactly one@, and two or more non-empty domain labels. 4 new tests cover it, and the pathological case drops from 4.7 s to 3 ms. Re-analysis onc160553bconfirms the result: 70 alerts remain, none on fork-owned files or fork hunks. The remaining upstream alerts, mostly missing rate limiting and "query built from user-controlled sources", are unchanged by the fork and belong to upstream.Dependency audit (osv-scanner on
yarn.lock; the lockfile here is identical to the audited prototype's)gascity-sso)Remaining runtime criticals. Both are
next16.3.8 advisories published 2026-10-22 and fixed in 16.4.1. Neither applies here:use cache): HyperDX is Pages Router only.Decision: wait for upstream 2.43 rather than override
nexthere.trivy's third critical is Go stdlib 1.23.7 inside the build-only
esbuildbinary, which is never executed. Most of the remaining highs are DoS-class and belong to upstream.Migration and rollback safety
This was rehearsed on FerretDB 2.5.0 by running 2.28, then 2.42, then 2.28 again on the same database (see the prototype design notes).
migrate-mongoscripts. The only migration is an additive startup backfill of alertdisplayName/tags(loggedBackfilled alert display names and tags), plus the new uniqueusers.accessKeyindex and newalertsindexes. All of these built cleanly on FerretDB. The seeded dashboard, saved search, webhook and alert stayed readable, and the auth matrix passed 21/21.sha256:ded30d1c…(2.28.0-gascity-7b24ad4fa2f9), never stock upstream, which ignoresPROXY_AUTH_*. Rolling back brings F1 and F2 back, so a lasting rollback must be followed by another secret rotation.After merge
gascity-ssorunsgascity-image(verify → build → symbol guard → publish). Record the digest ofghcr.io/gascity/hyperdx:2.42.0-gascity-<sha12>from the job's### published ###output. If the Blacksmith runner jobs sit queued, switch them toubuntu-latestin a follow-up PR.gascity-sso/2.28.0→7b24ad4f. The upgrade recipe inGASCITY-FORK.mduses it.gascity-sso. The 9 inherited upstream workflows are already disabled in Actions settings; two more (Claude Code,Deep Review Resolver) were disabled after GitHub registered them on the firstissue_commentevent. Onlygascity-imageandgascity-prremain active, plus GitHub's dynamic CodeQL and Dependency Graph./readyreadiness probe,HYPERDX_API_KEYremoved), merge it, and verify.f567cb14, and the gate's cookie secret. Optionally purge the old log rows.53376697) with hyperdxio/hyperdx.Supersedes #2. Its two commits are carried here as
f567cb14anda38ca3bc.🤖 Generated with Claude Code