Skip to content

[HDX-4120] feat(api): support heatmap tiles in external dashboards API#2200

Open
alex-fedotyev wants to merge 6 commits intomainfrom
alex/HDX-4120-external-api-heatmap
Open

[HDX-4120] feat(api): support heatmap tiles in external dashboards API#2200
alex-fedotyev wants to merge 6 commits intomainfrom
alex/HDX-4120-external-api-heatmap

Conversation

@alex-fedotyev
Copy link
Copy Markdown
Contributor

Summary

Heatmap was the only builder-mode display type that did not round-trip through the external dashboards API. The serializer dropped it into the "unsupported" fall-through, so creating, fetching, and updating heatmap tiles via /api/v2/dashboards lost the config.

This wires up heatmap end-to-end on the external API: a dedicated select-item schema, an explicit case in both serialization directions, OpenAPI JSDoc, and tests.

Follow-up to #2107 (review feedback from @pulpdrew, who asked whether we had a follow-up ticket to update the external API for the new visualization type).

What's in the diff

  • Zod schemas (packages/api/src/utils/zod.ts): a heatmap select-item schema that exposes the literal aggFn: "heatmap" plus valueExpression, optional countExpression, alias, heatmapScaleType; and a heatmap chart-config schema with optional groupBy and numberFormat. Heatmap is added to the builder discriminated union only.
  • Conversion utilities (packages/api/src/routers/external-api/v2/utils/dashboards.ts):
    • Builder serializer: replaces the old case DisplayType.Heatmap: fall-through with an explicit case that reads heatmap-specific fields off config.select[0] and emits aggFn: "heatmap" on the external surface.
    • Builder deserializer: new case 'heatmap': mirroring the Pie pattern; maps the external aggFn: "heatmap" back to the internal aggFn: "count" that the editor form persists, while preserving countExpression, alias, and heatmapScaleType on the select item.
    • The raw-SQL switch is intentionally left untouched: heatmap stays in the unsupported fall-through there because heatmap rendering requires isBuilderChartConfig.
  • OpenAPI JSDoc (packages/api/src/routers/external-api/v2/dashboards.ts): HeatmapSelectItem and HeatmapBuilderChartConfig components, and heatmap added to the TileConfig oneOf and discriminator mapping.
  • Tests (packages/api/src/routers/external-api/__tests__/dashboards.test.ts): heatmap added to the existing "round-trip all supported chart types" tests for both POST and PUT, plus an explicit rejection test confirming raw-SQL heatmap tiles return 400.
  • packages/api/openapi.json: regenerated.

Notes for review

  • No raw-SQL heatmap variant. PR feat: Wire heatmap chart into dashboard editor and tile rendering #2107 made heatmap builder-only and DBDashboardPage.tsx requires isBuilderChartConfig for heatmap rendering, so the raw-SQL fall-through stays.
  • heatmapScaleType and countExpression are persisted on the per-select-item level (via DerivedColumnSchema in packages/common-utils/src/types.ts), not on the chart config root. The form binds them as series.0.heatmapScaleType / series.0.countExpression. The schema and conversion utilities follow that.
  • The aggFn translation (external "heatmap" ↔ internal "count") keeps the saved Mongo document identical to what the editor form produces, so heatmap tiles created via the API render the same way as ones created via the UI.

Tier

Lands as review/tier-4 because anything under packages/api/src/routers/external-api/ is on the critical-path list. Diff is ~250 prod lines (most of it OpenAPI JSDoc and Zod boilerplate); no schema migrations or auth changes.

Test plan

  • make ci-lint (yarn lint, tsc --noEmit, OpenAPI lint)
  • make ci-unit (common-utils + app)
  • CI runs the full integration suite (heatmap round-trip POST + PUT + raw-SQL rejection); local make dev-int requires Docker BuildKit which isn't available on this host.

Heatmap is the only builder-mode display type that did not round-trip
through the external dashboards API. The serializer dropped it into the
"unsupported" fall-through, so creating, fetching, and updating heatmap
tiles via /api/v2/dashboards lost the config.

This adds:

- A heatmap select-item schema in zod.ts that exposes the literal
  aggFn 'heatmap', valueExpression, optional countExpression, alias,
  and heatmapScaleType. Heatmap is included in the builder
  discriminated union only; it is intentionally not added to the raw
  SQL union since heatmap rendering requires isBuilderChartConfig.
- A heatmap chart config schema with optional groupBy and numberFormat.
- Builder serialization that reads the heatmap-specific fields off
  config.select[0] and emits aggFn 'heatmap' on the external surface,
  passing through groupBy and numberFormat.
- Builder deserialization that maps the external aggFn 'heatmap' back
  to the internal aggFn 'count' the editor form persists, preserving
  countExpression, alias, and heatmapScaleType on the select item.
- OpenAPI JSDoc with HeatmapSelectItem and HeatmapBuilderChartConfig
  components, and heatmap added to the TileConfig discriminator.
- Integration tests: heatmap added to the existing "round-trip all
  supported chart types" tests for both POST and PUT, plus an explicit
  rejection test for raw SQL heatmap tiles.
- Regenerated openapi.json.

Follow-up to #2107 (review feedback from @pulpdrew).
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 5, 2026

🦋 Changeset detected

Latest commit: e1d1d9d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/api Minor
@hyperdx/app Minor
@hyperdx/otel-collector Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented May 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment May 5, 2026 8:11pm

Request Review

@github-actions github-actions Bot added the review/tier-4 Critical — deep review + domain expert sign-off label May 5, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

🔴 Tier 4 — Critical

Touches auth, data models, config, tasks, OTel pipeline, ClickHouse, or CI/CD.

Why this tier:

  • Critical-path files (2):
    • packages/api/src/routers/external-api/v2/dashboards.ts
    • packages/api/src/routers/external-api/v2/utils/dashboards.ts

Review process: Deep review from a domain expert. Synchronous walkthrough may be required.
SLA: Schedule synchronous review within 2 business days.

Stats
  • Production files changed: 4
  • Production lines changed: 369 (+ 182 in test files, excluded from tier calculation)
  • Branch: alex/HDX-4120-external-api-heatmap
  • Author: alex-fedotyev

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

E2E Test Results

All tests passed • 159 passed • 3 skipped • 1192s

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

Tests ran across 4 shards in parallel.

View full report →

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

PR Review

PR #2200 — feat(api): support heatmap tiles in external dashboards API

Overall this is a well-structured PR that closely follows existing patterns (mirrors the pie chart implementation). Test coverage is thorough and the OpenAPI docs are complete.

  • ⚠️ Redundant getSources call: getHeatmapTilesWithNonTraceSources calls getSources(team) independently, even though getMissingSources already calls it in the same Promise.all. Both queries hit the DB in parallel for the same dataset. Consider passing the result of getSources in from the call site, or combining the two functions → Extract and share the source lookup between getMissingSources and getHeatmapTilesWithNonTraceSources.

  • ⚠️ Serializer produces invalid valueExpression on degenerate data: In convertToExternalHeatmapSelectItem, item?.valueExpression ?? '' will output an empty string if item is undefined (e.g. a saved heatmap tile with an empty select array from bad DB data). This violates the schema's min(1) constraint and would silently emit an invalid external representation that would 400 if immediately round-tripped. → Guard: throw/log an error or return early if item is undefined, rather than defaulting to ''.

  • ℹ️ Raw SQL heatmap "rejection" relies on missing select field, not configType: The test at line 208 (rejects raw SQL heatmap tile because heatmap is builder-only) passes because select is absent, not because the schema explicitly blocks configType: 'sql'. A raw SQL body that includes a valid select array would silently pass the builder schema (Zod strips unknown configType without .strict()). This is consistent with the rest of the builder schemas but worth noting — no .strict() means configType: 'sql' is silently dropped rather than explicitly rejected for builder-mode types.

✅ No critical security issues. Validation logic, aggFn translation (heatmapcount), and the heatmap-only builder constraint are all correctly implemented and well-tested.

Alex Fedotyev and others added 2 commits May 5, 2026 19:47
knip flagged the exported type as unused. Extracted the inline heatmap
select-item construction into a small convertToExternalHeatmapSelectItem
helper that mirrors the existing convertToExternalSelectItem pattern and
returns the typed shape, kills the knip warning, and removes the
duplicated nullish-spread bookkeeping at the call site.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reviewer flagged that convertToExternalHeatmapSelectItem used truthy
checks (countExpression ? ...) while convertToInternalTileConfig used
!== undefined. An empty-string countExpression/alias round-trip would
silently drop the field. Use !== undefined consistently in both
directions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@alex-fedotyev
Copy link
Copy Markdown
Contributor Author

Thanks for the review.

  • Optional field consistency (point 1): Good catch. Fixed in 9e09229convertToExternalHeatmapSelectItem now uses !== undefined to match convertToInternalTileConfig, so empty-string round-trips don't silently drop fields.

  • numberFormat: config.numberFormat (point 2): This is actually the prevailing pattern in the file (10+ uses across Line, StackedBar, Table, Number, Pie, and the raw-SQL variants). Leaving it for consistency with the surrounding code; happy to swap to the spread form here AND across the file in a follow-up if we want a sweep.

  • knip: previously addressed in c232564 (extracted convertToExternalHeatmapSelectItem helper that types its return as ExternalDashboardHeatmapSelectItem).

The HeatmapSeriesEditor in the UI binds a single SearchWhereInput to the
top-level `where` / `whereLanguage` and does not render any groupBy
input. The previous schema had it backwards: it carried groupBy (not
exposed in the UI, never set by the form) and was missing
where / whereLanguage (the only filter heatmap actually uses).

- zod: drop groupBy, add where + whereLanguage to
  externalDashboardHeatmapChartConfigSchema
- conversion utils: serializer emits chart-level where + whereLanguage;
  deserializer maps them onto BuilderSavedChartConfig.where /
  whereLanguage instead of stuffing them into select-item aggCondition
- OpenAPI: HeatmapBuilderChartConfig has where + whereLanguage instead
  of groupBy; description notes the row-level filter is at chart level
- tests: POST round-trip exercises sql where ("ServiceName = 'api'")
  with heatmapScaleType: 'log'; PUT round-trip exercises lucene where
  ("service:api") with heatmapScaleType: 'linear'. Both languages and
  both scale types are now covered end-to-end.
- regenerated openapi.json
…ression

Two more gaps found while auditing the schema against the heatmap UI surface:

1. The heatmap UI's source picker is restricted to SourceKind.Trace
   (ChartEditorControls.tsx:103-107: allowedSourceKinds={[SourceKind.Trace]}).
   The external API previously accepted any source kind, so a metric or log
   source would round-trip cleanly through /api/v2/dashboards and produce a
   tile that does not render in the UI. Adds getHeatmapTilesWithNonTraceSources
   and wires it into both POST and PUT alongside the existing source /
   connection validators, returning 400 with a descriptive message.

2. validateChartForm in the editor rejects empty valueExpression on heatmap
   ("Value expression is required for heatmap charts"). The Zod schema
   accepted empty string. Tightens to z.string().min(1).max(10000) and
   updates the OpenAPI minLength to match.

Tests added: rejection on non-Trace source (with assertion on the error
message), rejection on empty valueExpression, rejection on multi-item
select array.

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

Labels

review/tier-4 Critical — deep review + domain expert sign-off

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant