Skip to content

feat: Add per-series number formats#2174

Merged
kodiakhq[bot] merged 10 commits into
mainfrom
drew/per-series-number-format
May 12, 2026
Merged

feat: Add per-series number formats#2174
kodiakhq[bot] merged 10 commits into
mainfrom
drew/per-series-number-format

Conversation

@pulpdrew
Copy link
Copy Markdown
Contributor

@pulpdrew pulpdrew commented Apr 29, 2026

Summary

This PR adds per-series number formats (for chart-builder-based charts). This allows the user to specify a different number format for different table columns or series.

For series without formats defined, the existing config's format is applied (if applicable). If there's no chart-wide format, then we attempt to infer a duration type format for any series aggregating the Trace Duration field. (re-using the previous logic for inferring duration number formats).

This PR includes updates the external API to support the new per-series numberFormat field.

This PR also updates various charts on the services dashboard which previously displayed ms units for non-duration series. These charts now only show ms formatting for duration aggregations.

Screenshots or video

Screen.Recording.2026-04-29.at.10.07.45.AM.mov

Services Dashboard:

Screenshot 2026-04-29 at 10 09 31 AM Screenshot 2026-04-29 at 10 09 23 AM

How to test locally or on Vercel

Most of this can be tested in the preview environment in a dashboard or in the chart explorer. The external API updates can be tested locally by running CRUD requests against a dashboard, specifying per-series number formats.

References

  • Linear Issue: Closes HDX-3609
  • Related PRs:

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 29, 2026

🦋 Changeset detected

Latest commit: b8d3cbf

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

This PR includes changesets to release 4 packages
Name Type
@hyperdx/common-utils Minor
@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 Apr 29, 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 12, 2026 11:40am

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 29, 2026

E2E Test Results

All tests passed • 174 passed • 3 skipped • 1254s

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

Tests ran across 4 shards in parallel.

View full report →

@pulpdrew pulpdrew marked this pull request as ready for review April 29, 2026 01:31
@github-actions github-actions Bot added the review/tier-4 Critical — deep review + domain expert sign-off label Apr 29, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 29, 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
  • Cross-layer change: touches frontend (packages/app) + backend (packages/api) + shared utils (packages/common-utils)

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: 22
  • Production lines changed: 825 (+ 1060 in test files, excluded from tier calculation)
  • Branch: drew/per-series-number-format
  • Author: pulpdrew

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 Apr 29, 2026

PR Review

  • ⚠️ Metric resultSet join can break meta[i] ↔ config.select[i] alignment in packages/common-utils/src/clickhouse/index.ts:673-678. The seeding loop dedupes by valueColumn.name, so if two metric splits produce the same alias (e.g., identical aggFn+metricName) the second is silently dropped, shifting downstream indices that useChartNumberFormats relies on. The new integration test only covers distinct aliases. → Either guarantee unique value-column names per split or fall back to positional indexing rather than metaSet.has.

  • ⚠️ useChartNumberFormats index loop doesn't guard against shorter meta at packages/app/src/source.ts:629-641. If meta.length < config.select.length (which becomes possible given the collision risk above), allColumns[i] is undefined and you'll call formatByColumn.set(undefined, …). → Skip the iteration when key == null.

  • 📝 Unrelated bug fix bundled silently in packages/app/src/components/ServiceDashboardEndpointPerformanceChart.tsx: "Calls per Request" expression changed from "Number of Calls" / "Average Duration" to "Number of Calls" / "Number of Requests". Looks correct but isn't called out in the PR description — worth mentioning so reviewers know to validate it independently.

  • 🧰 Untyped Map at source.ts:604, 609, 622, 630: new Map() infers Map<any, any> while the declared return type is Map<string, NumberFormat>. → Use new Map<string, NumberFormat>() for safety.

  • 🧰 SeriesNumberFormatDrawer resets on every initialNumberFormat reference change (useEffect(resetToDefaults, [resetToDefaults])). Since initialNumberFormat comes from useWatch, unrelated form updates can blow away in-progress edits in the drawer. → Gate the reset on opened transitions instead of every prop change.

Tests, MCP/external-API schemas, and the e2e coverage look thorough. The two ⚠️ items are the only ones I'd treat as blockers; the rest are polish.

@pulpdrew pulpdrew force-pushed the drew/per-series-number-format branch from fd4f3c0 to 4113c42 Compare April 29, 2026 05:11
@pulpdrew pulpdrew requested review from a team and fleon and removed request for a team April 29, 2026 05:27
fleon
fleon previously approved these changes Apr 30, 2026
Comment thread .changeset/dirty-rings-report.md
Comment thread packages/app/tests/e2e/features/dashboard.spec.ts
Comment thread packages/app/src/__tests__/source.test.ts Outdated
fleon
fleon previously approved these changes Apr 30, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 9, 2026

Deep Review

🟡 P2 -- recommended

  • packages/api/src/mcp/tools/dashboards/schemas.ts:10 -- mcpNumberFormatSchema is a hand-written copy of NumberFormatSchema that omits numericUnit, so a tile authored via REST with numericUnit set silently loses that field on any MCP read/edit round-trip and output: 'data_rate' | 'throughput' | 'byte' cannot be unit-qualified via MCP.

    • Fix: Reuse NumberFormatSchema from @hyperdx/common-utils/dist/types (optionally via .pick/.describe) instead of redefining the object shape in MCP.
    • maintainability, api-contract, agent-native, kieran-typescript, previous-comments, adversarial
  • packages/api/src/mcp/tools/dashboards/schemas.ts:161 -- Tile-level numberFormat is exposed only on mcpNumberTileSchema; mcpLineTileSchema, mcpBarTileSchema, mcpTableTileSchema, mcpPieTileSchema, and mcpSqlTileSchema omit it even though the corresponding REST external schemas (and the UI) expose it for those chart types.

    • Fix: Add numberFormat: mcpNumberFormatSchema.optional() to the config block of each tile schema that lacks it.
  • packages/app/src/source.ts:629 -- The new useChartNumberFormats index loop iterates config.select.length but reads allColumns[i] from meta; when meta.length < config.select.length (alias dedupe collapse, hidden series) the loop calls formatByColumn.set(undefined, ...) and silently swallows the per-series format.

    • Fix: Add if (key == null) continue; after const key = allColumns[i]; so unmatched select entries never write an undefined key.
    • correctness, previous-comments
  • packages/common-utils/src/clickhouse/index.ts:670 -- The new seed loop dedupes the joined metaSet on column name, but setChartSelectsAlias derives identical aliases for two splits that share aggFn/metricName and differ only in aggCondition or quantile level; the second split's value column drops from meta, its rows are silently overwritten by {...existingRow, ...row} in tsBucketMap, and useChartNumberFormats mis-binds the second series' format to a non-value column.

    • Fix: Disambiguate colliding aliases in setChartSelectsAlias/splitChartConfigs, or key the format mapping on a per-select identity computed before the join dedupe rather than on positional meta order.
    • correctness, reliability, adversarial
  • packages/app/src/components/DBEditTimeChartForm/EditTimeChartForm.tsx:233 -- autoDetectedNumberFormat now resolves to numberFormat ?? getFirstSeriesNumberFormat(select, tableSource) and is passed into ChartDisplaySettingsDrawer; when a user opens the drawer to toggle an unrelated setting with no chart-wide format set, Apply persists series[0].numberFormat as the new chart-wide value and changes how sibling series render.

    • Fix: Pre-fill the form with the auto-detected format but persist undefined when the user did not change numberFormat, or restrict autoDetectedNumberFormat to the trace-duration inference path the variable name implies.
    • adversarial, kieran-typescript
  • packages/app/src/components/ServiceDashboardEndpointPerformanceChart.tsx:152 -- The "Calls per Request" valueExpression was corrected from "Number of Calls" / "Average Duration" (semantically wrong) to "Number of Calls" / "Number of Requests", but no test snapshots or asserts the new expression; a future revert would silently restore the wrong column.

    • Fix: Add a unit/snapshot test that pins the corrected valueExpression for the "Calls per Request" column.
  • packages/app/src/HDXMultiSeriesTimeChart.tsx:82 -- HDXLineChartTooltip resolves per-series formats via numberFormatByKey.get(valueColumnName) ?? numberFormat, where valueColumnName is populated from valueColumn.name in addResponseToFormattedData; the wiring between the format map (keyed by meta column names) and the tooltip lookup is not exercised by any test.

    • Fix: Add a component test (or a Line/Bar e2e equivalent of the existing Table case) that asserts one series renders with its per-series format while a sibling uses the chart-wide fallback.
🔵 P3 nitpicks (4)
  • packages/app/src/components/DBTimeChart.tsx:728 -- MemoChart receives axisNumberFormat = chartFormat (encoding the full fallback chain) but fallbackNumberFormat = queriedConfig.numberFormat (only the raw config-level format), so tooltips and axis ticks can fall back to different formats for series missing from formatByColumn.

    • Fix: Pass chartFormat as fallbackNumberFormat so axis and tooltip fallbacks agree.
    • correctness, maintainability
  • packages/app/src/source.ts:546 -- useSingleSeriesNumberFormat only consults select[0].numberFormat even for seriesReturnType: 'ratio' configs where useChartNumberFormats also considers select[1].numberFormat; a numberFormat set on the ratio denominator alone silently disappears in DBNumberChart/DBPieChart.

    • Fix: Mirror the ratio handling: fall back to select[1].numberFormat when select[0].numberFormat is unset and the config is a ratio.
  • packages/app/src/components/SeriesNumberFormatDrawer.tsx:38 -- useEffect keyed on resetToDefaults (memoed on initialNumberFormat) re-runs whenever the parent's useWatch emits a new reference, which can clobber an open drawer's in-progress edits.

    • Fix: Gate the reset on opened transitioning to true, or remount the drawer when it opens.
    • correctness, maintainability, kieran-typescript
  • packages/api/src/routers/external-api/__tests__/dashboards.test.ts:2167 -- The new round-trip tests cover valid numberFormat payloads but no negative test pins that the external API returns 4xx for numberFormat.output: 'unknown' or other malformed shapes; the contract is enforced by Zod today but is unpinned against future schema relaxations.

    • Fix: Add one negative test posting a tile with a malformed numberFormat.output and asserting a 400.
    • testing, reliability

Reviewers (12): correctness, testing, maintainability, project-standards, agent-native, learnings-researcher, api-contract, performance, reliability, kieran-typescript, previous-comments, adversarial.

Testing gaps:

  • E2E coverage for per-series numberFormat only exercises Table charts; Line and Bar paths (which use a different axis-vs-tooltip resolution) are not exercised end-to-end.
  • SeriesNumberFormatDrawer inherit-vs-custom toggle has no unit test pinning that selecting "Inherit" clears the field to undefined.
  • queryChartConfig multi-series meta-ordering invariant (value-columns-first) is only verified by an integration test that requires a live ClickHouse server; consider extracting the seed loop into a pure helper for unit coverage.

@kodiakhq kodiakhq Bot merged commit 143f7a7 into main May 12, 2026
29 of 30 checks passed
@kodiakhq kodiakhq Bot deleted the drew/per-series-number-format branch May 12, 2026 11:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants