Skip to content

feat(api): accept exemplar settings on API- and agent-authored tiles - #2809

Open
jordan-simonovski wants to merge 6 commits into
jordansimonovski/exemplars-overlayfrom
jordansimonovski/exemplars-agent-surface
Open

feat(api): accept exemplar settings on API- and agent-authored tiles#2809
jordan-simonovski wants to merge 6 commits into
jordansimonovski/exemplars-overlayfrom
jordansimonovski/exemplars-agent-surface

Conversation

@jordan-simonovski

Copy link
Copy Markdown
Contributor

Last of the PRs replacing #2536. Stacked on #2808 — these fields do nothing without the overlay.

What it does

enableExemplars and exemplarTraceSourceId are accepted on line and stacked-bar tiles through the external v2 API and the MCP dashboard tools.

The part that was actually broken

The fields have to survive three places, and the original change only cleared the first:

  1. the input schema,
  2. convertToInternalTileConfig's pick list on the way in,
  3. convertToExternalTileChartConfig on the way out.

They validated on write and were then dropped before persistence. So the feature could only ever be switched on by a human in the chart editor — an agent could set the field, get a 200, and have it silently vanish. The round trip is what makes them usable.

Also documented in the OpenAPI spec, which is hand-maintained JSDoc rather than generated from the zod schemas — so adding schema fields alone left the published contract silent about them.

MCP describe strings

They follow the file's own convention for source ids: say to call clickstack_list_sources and pick a trace-kind source, and note that the flag only renders on an exemplar-eligible tile (single non-ratio histogram series, no groupBy). Without that an agent gets a 200 and no markers, with nothing explaining why.

Verification

make ci-lint and make ci-unit (5,350 tests) pass. Merged cleanly with main's seriesLimit addition, which landed in the same conversion functions — both sets of fields are kept.

@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview Aug 6, 2026 9:59pm
hyperdx-storybook Ready Ready Preview Aug 6, 2026 9:59pm

Request Review

@changeset-bot

changeset-bot Bot commented Aug 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: be4166f

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

@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR completes external API and MCP support for exemplar settings and addresses all three previously reported validation defects.

  • Preserves enableExemplars and exemplarTraceSourceId through inbound and outbound tile conversion.
  • Validates explicit exemplar source IDs for format, team-scoped existence, and Trace source kind.
  • Exempts unchanged legacy references during unrelated updates while revalidating newly activated exemplars.
  • Documents the fields in MCP schemas and OpenAPI and adds focused round-trip and integration coverage.

Confidence Score: 5/5

The PR appears safe to merge.

The previously reported malformed-ID, unchanged-tile update, and activation-validation failures are all addressed, and no blocking failure remains.

Important Files Changed

Filename Overview
packages/api/src/routers/external-api/v2/utils/dashboards.ts Preserves exemplar fields in both conversion directions and correctly scopes source validation for new, changed, and newly activated references.
packages/api/src/mcp/tools/dashboards/schemas.ts Adds optional exemplar settings to line and stacked-bar MCP tile schemas with shared ObjectId validation and agent-facing eligibility guidance.
packages/api/src/utils/zod.ts Extends the shared external time-chart schema with optional exemplar settings.
packages/api/src/routers/external-api/tests/dashboards.int.test.ts Covers source existence and kind checks, unchanged-tile update exemptions, and revalidation when exemplars are enabled.
packages/api/src/mcp/tools/dashboards/tests/exemplarSettings.test.ts Verifies MCP schema acceptance and rejection behavior for both supported tile types.
packages/api/openapi.json Documents exemplar fields and their rendering eligibility and source requirements for line and stacked-bar tiles.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Client[External API or MCP client] --> Schema[Validate tile schema]
  Schema --> SourceGate[Validate changed exemplar source]
  SourceGate --> ConvertIn[Convert to internal tile config]
  ConvertIn --> Mongo[(Persist dashboard)]
  Mongo --> ConvertOut[Convert to external tile config]
  ConvertOut --> Client
Loading

Reviews (10): Last reviewed commit: "fix(api): re-validate the exemplar trace..." | Re-trigger Greptile

Comment thread packages/api/src/mcp/tools/dashboards/schemas.ts
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

<!-- deep-review -->

Deep Review

No critical issues found. The three-stage round trip this change set out to fix (input schema → convertToInternalTileConfig pick list → convertToExternalTileChartConfig) is complete and covered by a test with exact-value assertions on both tile types in both directions, so a regression of the original drop-the-field bug would fail CI.

⚠️ Degraded run — read the Coverage note before treating this as a full review. Bash, Grep, Glob, and WebFetch were all unavailable in this environment, so no textual git diff could be produced and the reviewer fan-out was crippled. 10 reviewers were dispatched; 2 returned before output was required. Findings below are the ones verified by direct file reads.

🟡 P2 — recommended

  • packages/api/src/routers/external-api/v2/utils/dashboards.ts:1359filterChangedExemplarTiles scopes only the Trace-kind gate to changed tiles, while the existence check in getMissingSources still runs against the full unscoped tile list and now includes exemplarTraceSourceId (lines 949-954), so deleting a referenced trace source makes every later save of that dashboard fail with Could not find the following source IDs: <id> — the exact lockout the new function's docstring at 1086-1094 claims to prevent.
    • Fix: Either scope the exemplarTraceSourceId contribution in getMissingSources to the same changed-tile set the kind gate uses, or narrow the docstring to claim only the kind-change exemption it actually delivers.
  • packages/api/src/routers/external-api/v2/utils/dashboards.ts:1009 — The two new validation helpers getInvalidExemplarTraceSources (1009-1039) and filterChangedExemplarTiles (1095-1119) plus the getMissingSources addition (949-954) have no test coverage; the added suite at __tests__/dashboards.test.ts:411-470 exercises only the converters, never validateDashboardTiles.
    • Fix: Add tests driving the exported validateDashboardTiles with a stubbed source list covering trace, log, and metric kinds, asserting both rejection messages and that an unchanged existing tile is skipped by the kind gate.
    • testing
🔵 P3 nitpicks (4)
  • packages/api/src/routers/external-api/v2/utils/__tests__/dashboards.test.ts:461 — The suite covers enableExemplars: true and the fields-absent case, but never enableExemplars: false, so nothing pins the _.omitBy(_.isNil) strip at dashboards.ts:872 to null/undefined-only semantics.
    • Fix: Add a case asserting an explicit false survives conversion as false.
    • testing
  • packages/api/src/mcp/tools/dashboards/schemas.ts:546 — The enableExemplars and exemplarTraceSourceId .describe() strings are byte-identical between mcpLineTileSchema (546-564) and mcpBarTileSchema (583-601), while this file's own convention hoists shared descriptions into named constants such as timeChartSeriesLimitDescription (line 50).
    • Fix: Hoist both strings into module-level constants and reference them from each tile schema.
  • packages/api/src/routers/external-api/v2/dashboards.ts:652 — The OpenAPI text and the matching MCP describe string at mcp/tools/dashboards/schemas.ts:562 both promise exemplarTraceSourceId "defaults to the chart source's linked trace source when omitted", but no server-side code in the write path resolves that default, so a caller reading the tile back cannot tell which trace source will be used.
    • Fix: Confirm whether the default is resolved in the renderer and, if it is client-side only, say so in both descriptions rather than implying a persisted server default.
  • packages/api/package.json:95 — The global --max-warnings ceiling was raised to 358 to admit one new warning from the added test rather than suppressing it at the offending line, which leaves the warning unattributed and ratchets the budget for unrelated files.
    • Fix: Restore the previous ceiling and add a targeted eslint-disable-next-line with a reason at the triggering line.
    • testing

Reviewers (2 of 10 returned): testing, learnings-researcher. Dispatched but not returned before output was required: correctness, adversarial, security, api-contract, maintainability, kieran-typescript, agent-native, project-standards.

Coverage caveats:

  • No git diff was obtainable (Bash fails at sandbox init with bwrap: Can't create file at /home/.mcp.json; Grep/Glob are not registered tools; WebFetch was not permitted). The change surface was reconstructed by reading files at HEAD and locating every enableExemplars / exemplarTraceSourceId site, so a changed line unrelated to those identifiers could have been missed.
  • ce-learnings-researcher found no docs/solutions/ in this repo — it uses agent_docs/ instead — so no prior-learning check was possible.

Testing gaps:

  • Whether the committed generated packages/api/openapi.json was regenerated to include the two new fields is unverified; the root lint-staged hook (lines 56-60) requires it for edits under packages/api/src/routers/external-api/**.
  • No test was found exercising the MCP save or patch handlers end-to-end with the exemplar fields, despite mcpPatchTileSchema (schemas.ts:985-1009) reusing the line/bar config shapes.
  • Zod negative cases are uncovered: a malformed exemplarTraceSourceId, and the fields supplied on a raw-SQL line/stacked_bar config where the base schema does not declare them.

karl-power
karl-power previously approved these changes Aug 5, 2026
@jordan-simonovski
jordan-simonovski dismissed karl-power’s stale review August 5, 2026 13:02

The merge-base changed after approval.

@jordan-simonovski
jordan-simonovski force-pushed the jordansimonovski/exemplars-agent-surface branch from e5337c3 to 929dfdb Compare August 5, 2026 21:52
@github-actions github-actions Bot added the review/tier-4 Critical — deep review + domain expert sign-off label Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🔴 Tier 4 — Critical

Touches authentication, tenancy data models, the public API or shipped database config — or substantially changes background tasks, the OTel pipeline, image build, or release CI.

Why this tier:

  • Critical-path files (2) — tenancy, public API, or shipped database config:
    • 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: 6
  • Production lines changed: 210 (+ 481 in test files, excluded from tier calculation)
  • Critical-path lines changed: 145
  • Branch: jordansimonovski/exemplars-agent-surface
  • Author: jordan-simonovski

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

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 275 passed • 1 skipped • 1006s

Status Count
✅ Passed 275
❌ Failed 0
⚠️ Flaky 0
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

Comment thread packages/api/src/routers/external-api/v2/utils/dashboards.ts Outdated
Comment thread packages/api/src/routers/external-api/v2/utils/dashboards.ts Outdated
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Complete — see structured output.

enableExemplars and exemplarTraceSourceId are accepted on line and stacked-bar
tiles through the external v2 API and the MCP dashboard tools.

The fields have to survive three places, and originally only cleared the first:
the input schema, convertToInternalTileConfig's pick list on the way in, and
convertToExternalTileChartConfig on the way out. They validated on write and were
dropped before persistence, so the overlay could only ever be enabled by a human
in the chart editor — the round trip is what makes them usable by an agent.

Also documented in the OpenAPI spec, which is hand-maintained JSDoc rather than
generated from the zod schemas, so adding the schema fields alone left the
published contract silent about them.

The MCP describe strings follow the file's own convention for source ids and say
to call clickstack_list_sources and pick a trace-kind source, and note that the
flag only renders on an exemplar-eligible tile.

Stacked on the overlay: these fields do nothing without it.
Addresses review findings on the agent-facing exemplar settings.

`exemplarTraceSourceId` was a bare `z.string()` on the MCP surface, so an agent
passing a source *name* had the tile saved and every marker's trace link
silently dead. It now uses the shared objectIdSchema, matching the external API.

Format alone was not enough though: a well-formed id for a source that does not
exist, or for a metric or log source, failed the same way. It now goes through
the same existence check as every other tile source reference, plus a Trace-kind
gate modelled on the heatmap one — which is what the field's own description has
been promising all along.

The eligibility text in both descriptions omitted the aggregation constraint, so
an agent following it with `count` — which the neighbouring aggFn description
recommends for histograms — got an inert overlay and no error. Both now name the
compatible functions and say why count and sum are excluded.

Tests: both tile types through the MCP schema, both directions of the tile
conversion (the changeset's headline claim had no coverage, and the two tile
types have separate blocks in both converters), and the existence and kind gates
end to end.
The round-trip test needs one type assertion: its tile builder takes the display
type as a parameter, so `displayType` is the union rather than a literal and TS
cannot pick a branch of the external tile config union without it. Writing the
two configs out separately to avoid the assertion loses the it.each that makes
the line and stacked_bar cases symmetrical — which is the point, since both
converters handle them in separate blocks.
…check

Addresses a review finding on the previous fix.

The kind gate ran over every tile in the request, so an unchanged tile whose
trace source had since been deleted or changed kind would fail it on every
subsequent save — making an unrelated edit elsewhere on the dashboard impossible
to persist. Scoped to new and changed tiles on update, mirroring
filterChangedHeatmapTiles, which exists for exactly this reason.

The existence check in getMissingSources is deliberately left unscoped: it treats
every source reference on the dashboard the same way and reports them with one
clear message, and singling this field out would be the inconsistency rather
than the fix.
…heck too

Addresses a review finding on the previous fix, which only went half way.

The kind gate was scoped to changed tiles but the existence check was not, so
deleting an exemplar trace source still made every later save of that dashboard
fail. The two checks now live together and share one exemption: a deleted source
and a source whose kind changed are the same situation to whoever hits them, and
nobody could predict that one blocks an edit and the other does not.

My earlier reasoning for leaving the existence check unscoped — that
getMissingSources should treat every source reference alike — weighted mechanical
consistency over what actually happens. A missing `sourceId` means the tile
cannot render; a missing `exemplarTraceSourceId` means a marker's trace link is
dead on a chart that is otherwise fine. That is not worth refusing an unrelated
edit over.
…itched on

Addresses a review finding on the previous fix.

The unchanged-tile exemption keyed on the trace source id, so a tile carrying a
source id while exemplars were disabled kept the exemption when an update turned
them on. Nothing was following that reference in the meantime, so the source
could have been deleted or stopped being a Trace source with no visible effect —
and switching markers on then persisted them pointing at nothing.

An id nobody was following is not the same as one about to draw markers, so the
off-to-on transition is now checked as if the reference were new. A tile whose
exemplars were already on keeps the exemption, which is the case that exemption
exists for.
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.

2 participants