[HDX-4120] feat(api): support heatmap tiles in external dashboards API#2200
Conversation
🦋 Changeset detectedLatest commit: d6f27b3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🔴 Tier 4 — CriticalTouches auth, data models, config, tasks, OTel pipeline, ClickHouse, or CI/CD. Why this tier:
Review process: Deep review from a domain expert. Synchronous walkthrough may be required. Stats
|
E2E Test Results✅ All tests passed • 175 passed • 3 skipped • 1250s
Tests ran across 4 shards in parallel. |
|
<!-- claude-code-review --> PR Review✅ No critical issues found. Implementation is clean and well-scoped:
Minor (non-blocking) observations:
|
|
Thanks for the review.
|
The external API surface in PR #2200 exposes where/whereLanguage at the chart-config level for heatmaps (UI HeatmapSeriesEditor doesn't render per-series filters; the persisted shape stores them once via SelectSQLStatementSchema). The MCP heatmap schema was dropping them, so once #2200 lands a save through hyperdx_save_dashboard would silently discard the filter on read-back. Add `where` and `whereLanguage` to mcpHeatmapTileSchema.config with the same defaults the search tile uses (where: '', whereLanguage: 'lucene'). Extend the round-trip test with a Lucene filter, add an explicit whereLanguage="sql" round-trip, and a negative test for an invalid whereLanguage. Update the example string and changeset to document the new fields.
PR ReviewHeatmap round-trip support looks solid: schemas, serializer, deserializer, OpenAPI, source-kind gate (REST + MCP), and tests for create/update/round-trip/rejection/legacy-corrupted GET all line up. Promise.all hoisting is a nice cleanup. A couple of doc-rot items only — no functional blockers.
|
Compound Engineering Review✅ No critical issues found. Security audit returned no findings (tenant scoping, IDOR, info-leak, auth all sound). Type safety, test parity, and adherence to sibling patterns (Pie/Search/Markdown) are clean. P2 — Important
|
Compound-review feedback on #2200, all P2: - Drop redundant `whereLanguage.optional()`; the underlying `SearchConditionLanguageSchema` (alias `whereLanguageSchema`) is already `.optional()` and the sibling `externalDashboardSearchChartConfigSchema` writes it without the outer wrap. - Drop the `where: externalConfig.where ?? ''` redundancy in the deserializer; the Zod schema declares `.default('')` so the field is always a string post-parse. - Rename schema `HeatmapBuilderChartConfig` -> `HeatmapChartConfig` to match the sibling `SearchChartConfig` / `MarkdownChartConfig` naming. The `Builder` suffix is reserved for cases that disambiguate from a sibling `RawSqlChartConfig`; heatmap has no raw-SQL variant. - `convertToExternalHeatmapSelectItem` now takes a non-optional item. The case-arm caller checks for missing/string/empty-valueExpression items and falls through to `defaultTileConfig` with a logger.warn instead of silently emitting an out-of-contract payload that violates the external schema's `min(1)` rule. - Extract `HEATMAP_ALLOWED_SOURCE_KINDS` and `isHeatmapCompatibleSource` into common-utils. Both `ChartEditorControls.tsx` (UI) and the API's `getHeatmapTilesWithIncompatibleSources` (renamed from `...WithNonTraceSources`) reference the same set so UI and API gates move together. - Add a parity comment on the deserializer pointing to `applyHeatmapDefaults` so the `aggCondition`/`aggConditionLanguage` defaults stay coupled to the editor's behaviour. - Consolidate three pure-Zod schema-rejection tests (raw-SQL-heatmap absent, empty valueExpression, multiple select items) into a single parametrized `it.each` block; the runtime non-Trace-source rejection stays as its own test because it exercises the new `getHeatmapTilesWithIncompatibleSources` path.
|
P2 cleanups addressed in e89989d:
|
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.
The existing realistic-payload round-trip exercises every optional field
populated. The minimal path (no countExpression / alias /
heatmapScaleType / where / whereLanguage / numberFormat) is what the
deserializer's !== undefined guards in v2/utils/dashboards.ts exist to
protect, and was uncovered until now.
The Zod schema applies where: z.string().optional().default(''), so the
round-trip surfaces an empty string for where while the other optional
fields stay undefined. The expected response keeps explicit on the
applied default rather than asserting strict equality with the request.
When the external API request omits whereLanguage on a heatmap tile, the deserializer at v2/utils/dashboards.ts:514 fills it with 'lucene' so the Mongo doc stays consistent across heatmap and non-heatmap chart types. The minimal-fields round-trip needs to expect whereLanguage on the response, not undefined. The schema-level constraint stays optional (matching the OpenAPI shape); the default lives at the persistence boundary, not the contract boundary.
Compound-review feedback on #2200, all P2: - Drop redundant `whereLanguage.optional()`; the underlying `SearchConditionLanguageSchema` (alias `whereLanguageSchema`) is already `.optional()` and the sibling `externalDashboardSearchChartConfigSchema` writes it without the outer wrap. - Drop the `where: externalConfig.where ?? ''` redundancy in the deserializer; the Zod schema declares `.default('')` so the field is always a string post-parse. - Rename schema `HeatmapBuilderChartConfig` -> `HeatmapChartConfig` to match the sibling `SearchChartConfig` / `MarkdownChartConfig` naming. The `Builder` suffix is reserved for cases that disambiguate from a sibling `RawSqlChartConfig`; heatmap has no raw-SQL variant. - `convertToExternalHeatmapSelectItem` now takes a non-optional item. The case-arm caller checks for missing/string/empty-valueExpression items and falls through to `defaultTileConfig` with a logger.warn instead of silently emitting an out-of-contract payload that violates the external schema's `min(1)` rule. - Extract `HEATMAP_ALLOWED_SOURCE_KINDS` and `isHeatmapCompatibleSource` into common-utils. Both `ChartEditorControls.tsx` (UI) and the API's `getHeatmapTilesWithIncompatibleSources` (renamed from `...WithNonTraceSources`) reference the same set so UI and API gates move together. - Add a parity comment on the deserializer pointing to `applyHeatmapDefaults` so the `aggCondition`/`aggConditionLanguage` defaults stay coupled to the editor's behaviour. - Consolidate three pure-Zod schema-rejection tests (raw-SQL-heatmap absent, empty valueExpression, multiple select items) into a single parametrized `it.each` block; the runtime non-Trace-source rejection stays as its own test because it exercises the new `getHeatmapTilesWithIncompatibleSources` path.
Deep-review on #2200 flagged the previous comment as overstating parity. The editor's `applyHeatmapDefaults` writes `numberFormat: { output: 'duration', factor: 0.001 }`, `series.0.countExpression: 'count()'`, and `aggConditionLanguage: getStoredLanguage() ?? 'lucene'`, while this converter hardcodes `'lucene'` for the language and passes `numberFormat`/`countExpression` through verbatim from the external payload (or leaves them absent). Comment now enumerates the divergences and the rendering implications: the renderer does not read `aggConditionLanguage` for heatmap tiles (heatmap has no per-select where), and an API-built tile renders without duration formatting unless the caller specifies `numberFormat`. No behavior change.
946412a to
d8f9af7
Compare
Six findings from the bot review on #2200: P0/P1 - MCP schema rejected heatmap tiles. Adds `mcpHeatmapTileSchema` (mirroring `externalDashboardHeatmapChartConfigSchema`) and includes it in the `mcpTilesParam` union so `hyperdx_save_dashboard` accepts the same heatmap shape the REST endpoint accepts. - MCP create/update did not run the heatmap source-kind gate. Both paths now call `getHeatmapTilesWithIncompatibleSources` after schema validation so an MCP-issued create cannot persist a heatmap that the REST PUT would reject with 400 on the next round-trip. The update path scopes the check to heatmap tiles whose `sourceId` or `displayType` changed in this request, matching the REST PUT. - `buildQueryGuidePrompt` did not document heatmap. Adds entries to TILE TYPE GUIDE, PER-TILE TYPE CONSTRAINTS, AGGREGATION FUNCTIONS, and COMMON MISTAKES so the LLM cannot skip the Trace-source and non-empty-`valueExpression` rules when generating tiles. P2 - `convertTileToExternalChart` returned `displayType: 'line'` via `defaultTileConfig` for any heatmap tile whose internal doc lacked a non-empty `valueExpression`. A GET / mutate / PUT round-trip would then silently overwrite the heatmap with a line chart in Mongo (data loss). The heatmap branch now emits a heatmap-shaped placeholder with empty `valueExpression`, so the response preserves `displayType` and a re-PUT surfaces the breakage as a clear schema error from the input schema's `min(1)` rule rather than dropping the heatmap. - The PUT-time `getHeatmapTilesWithIncompatibleSources` ran on every request regardless of whether heatmap tiles changed, so a source whose `kind` was changed to non-Trace after the dashboard was originally accepted wedged every PUT. The check now scopes to heatmap tiles whose `sourceId` or `displayType` changed compared to the existing dashboard via a new `filterChangedHeatmapTiles` helper. - POST and PUT each ran three separate `getSources(team)` queries (one per validation helper). The helpers now accept a pre-fetched `SourceForValidation[]`, hoisted into a single `fetchSourcesForValidation` call shared across `getMissingSources`, `getSourceConnectionMismatches`, and `getHeatmapTilesWithIncompatibleSources`. Same change applied to the MCP create/update paths. Drops two redundant DB round-trips per request. Tests cover the MCP schema accept/reject paths, the heatmap source-kind gate at the MCP layer, the GET placeholder behavior, the PUT scoping (no 400 on unrelated edits when the underlying source's `kind` was changed later), and that the prompt documents heatmap in all four sections. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Pushed a fix-pack addressing the deep-review findings in commit a217900. Mapping: P0/P1
P2
Tests cover the MCP schema accept/reject paths, the heatmap source-kind gate at the MCP layer, the GET placeholder behavior, the PUT scoping (no 400 on unrelated edits when the underlying source's Skipping the P2/P3 items the task spec marked as deferred (renames, cosmetic TS, |
pulpdrew
left a comment
There was a problem hiding this comment.
Two small thoughts on the API spec, otherwise LGTM
| aggFn: z.literal('heatmap'), | ||
| valueExpression: z.string().min(1).max(10000), | ||
| countExpression: z.string().max(10000).optional(), | ||
| alias: z.string().max(10000).optional(), |
There was a problem hiding this comment.
Alias isn't relevant to heatmaps, is it?
There was a problem hiding this comment.
Good catch. Dropped from the external schema, the OpenAPI JSDoc, the MCP companion, and the converter (both directions). The per-feature code map at notes/repo-conventions/hyperdx/heatmap.md already flagged alias as "not in heatmap UI" and I kept it anyway for a hypothetical round-trip preservation case that never fires, since the UI never sets it for heatmap. Fixed in 476fa99.
| // asks for it. | ||
| select: [ | ||
| { | ||
| aggFn: 'count', |
There was a problem hiding this comment.
If we're just always saving count here, could we just remove the aggFn property from the OpenAPI spec / zod schema for the heatmap select item? Why make the user provide it if we ignore it?
There was a problem hiding this comment.
Agreed. Dropped aggFn from the zod schema, the OpenAPI JSDoc, the MCP companion schema, the MCP prompt examples, and the placeholder-on-read path. The chart-level displayType: "heatmap" is the discriminator; the per-select-item literal was pure noise. Fixed in 476fa99.
…ontainer/tab refs (#2201) ## Summary PR #2015 added a dashboard organization layer (containers with optional tabs, plus per-tile `containerId` and `tabId`) but the v2 external API was not updated to round-trip the new fields. External integrations that build dashboards programmatically had no way to use the new layer. This wires the full set of fields through `CREATE` / `GET` / `LIST` / `UPDATE` on `/api/v2/dashboards`. Dashboards saved without containers round-trip unchanged. Closes #2150. Follow-up to #2015 (commit 7665fbe). ## What's in scope - Dashboard body Zod schema gains `containers: DashboardContainer[]?` (imported from `@hyperdx/common-utils`) and the tile schema gains `containerId?` and `tabId?`. - `convertToExternalDashboard` now emits `containers` (only when at least one is present, so dashboards without the layer round-trip with the field absent). - `convertTileToExternalChart` and `convertToInternalTileConfig` propagate `containerId` and `tabId`. The legacy `series`-format translator in `externalApi.ts` also propagates them so both code paths preserve the fields. - The `containers: 1` projection is added to the Mongoose `find` and `findOne` calls. - New cross-field validation on the body schema: - container ids unique within a dashboard - tab ids unique within a container - tile `containerId` resolves to a real container - tile `tabId` resolves to a tab inside that container - tile `tabId` requires `containerId` to be set - OpenAPI JSDoc additions for `DashboardContainer`, `DashboardContainerTab`, the new tile fields, and the new dashboard field on `Dashboard` / `CreateDashboardRequest` / `UpdateDashboardRequest`. `openapi.json` regenerated. - A changeset entry. ## Out of scope Each item below has a tracking issue so the gap is visible after merge. - The legacy `type: 'section'` discriminator removed in #2015 (commit 7665fbe). Not emitted, not validated against. Legacy documents parse cleanly via Zod's strip-unknown. No follow-up needed; documenting for clarity. - Container-level `repeat` field (future work per #2015 review notes). Tracked in #2213. - The MCP `hyperdx_save_dashboard` tool is unchanged in this PR. Its inline `inputSchema` does not yet expose `containers`, and adding it there is its own follow-up; doing both at once would tangle two surfaces. Tracked in #2212. - Heatmap chart-type round-trip lands separately in #2200. - Per-tile `onClick` drill-down round-trip is tracked in #2214. ## Tier The triage classifier marks `packages/api/src/routers/external-api/v2/*` as critical-path, so this lands as **Tier 4** by directory rule, even though the diff is small (~284 prod lines) and additive. Splitting further would separate the body schema, the conversion utilities, and the route wiring from each other and not actually reduce review burden. Happy to break this up if there's a preferred way to slice it. ## Test plan - [x] `yarn ci:lint` (lint + tsc + spectral) on `@hyperdx/common-utils`, `@hyperdx/api`, `@hyperdx/app` - [x] `yarn knip` (no new unused exports) - [x] Integration: `yarn jest dashboards.test.ts -t "Containers and tabs"`, all 8 new tests pass - [x] Integration: full `yarn jest dashboards.test.ts`, 86/86 tests pass (no regressions in old or new format suites) - [x] Integration: `yarn jest src/mcp/__tests__/dashboards.test.ts`, 19/19 MCP dashboard tests pass (the MCP body schema shares with the external API body schema, so this confirms the new validations don't break the MCP path) - [x] `openapi.json` regenerated and committed; spectral lint passes
|
This PR currently has a merge conflict. Please resolve this and then re-add the |
Review feedback from pulpdrew on #2200: - "Alias isn't relevant to heatmaps, is it?" (zod.ts:310) - "If we're just always saving `count` here, could we just remove the `aggFn` property from the OpenAPI spec / zod schema for the heatmap select item? Why make the user provide it if we ignore it?" (utils/dashboards.ts:561) Both call out the same root failure: the external heatmap select item schema carried fields the heatmap UI and the converter do not honor. - The chart-level discriminator is `displayType: "heatmap"`. The per-select-item `aggFn: "heatmap"` literal was pure noise: the converter always wrote `aggFn: "count"` internally regardless. Drop it from the zod schema, the OpenAPI JSDoc, the MCP companion schema, the MCP prompt examples, and the placeholder-on-read path. - `HeatmapSeriesEditor.tsx` does not render an alias input (per-feature code map at notes/repo-conventions/hyperdx/heatmap.md line 70 says: "not in heatmap UI"). The previous schema kept alias for a hypothetical "preserved through round-trip" case that never triggers because the UI never sets it. Drop it from the schema, the OpenAPI JSDoc, the converter (both directions), the MCP companion schema, and the tests that pass it. Tests updated to use the new minimal shape `{ valueExpression: "..." }`. The minimal-round-trip test, the rejection-cases parametrized test, the non-Trace-source test, the multi-tile round-trip test, the PUT round-trip test, and the PUT-scoping test all drop `aggFn` and `alias` from their fixtures. openapi.json regenerated. No behaviour change for callers who weren't sending these fields (serialization path strips them on read so existing dashboards with internal `alias` set will not surface it through the external API; the UI never sets it for heatmap, so this is a no-op for UI-created tiles). External-API callers that were previously sending `aggFn: "heatmap"` or `alias: "..."` will now see those fields ignored by the schema's default unknown-key-pass-through behavior; the converter no longer reads them.
Resolves conflicts with the containers/tabs PR #2201 (41395ca) which landed while this branch was iterating on deep-review feedback. Conflicts and resolution: - packages/api/src/routers/external-api/v2/dashboards.ts - Imports: keep IDashboard (used by Partial<IDashboard> setPayload), drop getSources (replaced by fetchSourcesForValidation helper from the deep-review refactor). - getSourceConnectionMismatches signature: keep the sync (sources, tiles) shape from this branch; the async (team, tiles) shape on main re-fetched sources, which the deep-review explicitly flagged as redundant. - POST endpoint: combine main's tileRefIssues container/tab check AHEAD of this branch's hoisted-fetch + 4-validator block. Both additions are orthogonal; the tile-ref check runs first because it short-circuits on a bad request body and avoids any DB calls. - PUT endpoint: drop main's standalone Dashboard.findOne; the hoisted Promise.all in this branch already fetches existingDashboard once and reuses it. Bumped the projection from { tiles: 1, filters: 1 } to { tiles: 1, filters: 1, containers: 1 } so the new tile-ref check sees the existing container set. - packages/api/src/routers/external-api/v2/utils/dashboards.ts - Imports: combine main's validateDashboardContainersStructure / validateDashboardTileContainerRefs from dashboardValidation with this branch's isHeatmapCompatibleSource from guards. Verified yarn workspace @hyperdx/api ci:lint clean after rebuilding common-utils; the 19 unit tests in utils/__tests__/dashboards.test.ts pass. Integration tests (__tests__/dashboards.test.ts, mcp/__tests__) need the make dev-int harness.
|
Rebased onto main to clear the kodiakhq conflict. The merge commit (
Your two API-spec thoughts are addressed in
Mind converting the COMMENTED review to an approval if it still reads LGTM with the thoughts addressed? |
…l-api-heatmap # Conflicts: # packages/api/src/mcp/__tests__/dashboards.test.ts # packages/api/src/mcp/tools/dashboards/saveDashboard.ts # packages/api/src/mcp/tools/dashboards/schemas.ts # packages/api/src/routers/external-api/v2/dashboards.ts # packages/api/src/routers/external-api/v2/utils/dashboards.ts
Brandon flagged on #2199 that the schema unit tests only assert that zod accepts the shape and don't prove that an agent-issued hyperdx_save_dashboard call actually persists a heatmap with every MCP-specific field. The bare-minimum heatmap test that lands with the #2200 heatmap external-API work covers only the required valueExpression; this test drives the full round-trip with heatmapScaleType, countExpression, numberFormat, where, and whereLanguage all set, then mutates them on PUT and re-reads to confirm the new values stick. Drive-by: drops an em-dash from a sibling test comment to satisfy the prose-lint hook on this file.
|
Rebased on top of the recent batch of merges (#2156, #2174, #2201, #2250, #2260, #2262) in merge commit Net change vs. your last look (commit
Drew's two nits from the 2026-05-09 review (drop |
Brandon flagged on #2199 that the schema unit tests only assert that zod accepts the shape and don't prove that an agent-issued hyperdx_save_dashboard call actually persists a heatmap with every MCP-specific field. The bare-minimum heatmap test that lands with the #2200 heatmap external-API work covers only the required valueExpression; this test drives the full round-trip with heatmapScaleType, countExpression, numberFormat, where, and whereLanguage all set, then mutates them on PUT and re-reads to confirm the new values stick. Drive-by: drops an em-dash from a sibling test comment to satisfy the prose-lint hook on this file.
## Summary Adds a thorough save → get → update → get round-trip test for the heatmap MCP path. Brandon flagged on this PR that the schema unit tests at `mcp/tools/dashboards/__tests__/schemas.test.ts` only assert that zod accepts the shape and don't prove that an agent-issued `hyperdx_save_dashboard` call actually persists a heatmap with every MCP-specific field. The bare-minimum heatmap test that landed with #2200 covers only the required `valueExpression`; this test exercises `heatmapScaleType`, `countExpression`, `numberFormat`, `where`, and `whereLanguage` all at once, then mutates them on PUT and re-reads to confirm the new values stick. ## Backstory The MCP heatmap schema work originally drafted on this branch (`mcpHeatmapSelectItemSchema`, `mcpHeatmapTileSchema`, the few-shot example, the prompt guides) was absorbed into #2200 during the convergence on the deep-review feedback. #2200 has merged on `main`, so the only delta this PR adds on top is the integration test. ## What's in this PR - One new test in `packages/api/src/mcp/__tests__/dashboards.test.ts`: `should round-trip a fully-specified heatmap tile through save, get, and update` - Drive-by: drops one em-dash from a sibling test comment in the same file so the prose-lint hook passes when this file is touched ## Test plan - [x] `yarn workspace @hyperdx/api lint` clean - [x] `yarn workspace @hyperdx/api tsc --noEmit` clean - [x] `yarn workspace @hyperdx/api docgen` produces no openapi.json diff - [ ] CI runs the integration suite. Local `make dev-int` needs Docker BuildKit which isn't available on this host.
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/dashboardslost 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
packages/api/src/utils/zod.ts): a heatmap select-item schema that exposes the literalaggFn: "heatmap"plusvalueExpression, optionalcountExpression,alias,heatmapScaleType; and a heatmap chart-config schema with optionalgroupByandnumberFormat. Heatmap is added to the builder discriminated union only.packages/api/src/routers/external-api/v2/utils/dashboards.ts):case DisplayType.Heatmap:fall-through with an explicit case that reads heatmap-specific fields offconfig.select[0]and emitsaggFn: "heatmap"on the external surface.case 'heatmap':mirroring the Pie pattern; maps the externalaggFn: "heatmap"back to the internalaggFn: "count"that the editor form persists, while preservingcountExpression,alias, andheatmapScaleTypeon the select item.isBuilderChartConfig.packages/api/src/routers/external-api/v2/dashboards.ts):HeatmapSelectItemandHeatmapBuilderChartConfigcomponents, andheatmapadded to theTileConfigoneOfand discriminator mapping.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
DBDashboardPage.tsxrequiresisBuilderChartConfigfor heatmap rendering, so the raw-SQL fall-through stays.heatmapScaleTypeandcountExpressionare persisted on the per-select-item level (viaDerivedColumnSchemainpackages/common-utils/src/types.ts), not on the chart config root. The form binds them asseries.0.heatmapScaleType/series.0.countExpression. The schema and conversion utilities follow that."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-4because anything underpackages/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)make dev-intrequires Docker BuildKit which isn't available on this host.Deep-review carryover (2026-05-07)
convertToInternalTileConfigcomment vsapplyHeatmapDefaultsreality, see commit946412ad).mcpTilesParam) already shipped; P0/P1 feat: introduce usage-stats service + init changeset #3 (heatmap entry inbuildQueryGuidePrompt+buildCreateDashboardPrompt) added in commite583fc68.createDashboard/updateDashboardcallinggetHeatmapTilesWithIncompatibleSources), since the helper is added in this PR and not yet importable from test(api/mcp): cover full heatmap save/get/update round-trip #2199's branch.