Repository navigation
feat(api): accept exemplar settings on API- and agent-authored tiles #2809
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
77857a1
5bddb58
877d781
30e6dbf
ad36645
2260246
8e36612
dc4e06a
e63a155
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| --- | ||
| '@hyperdx/api': minor | ||
| --- | ||
|
|
||
| feat: accept exemplar settings on API- and agent-authored dashboard tiles | ||
|
|
||
| `enableExemplars` and `exemplarTraceSourceId` are now accepted on line and | ||
| stacked-bar tiles through the external v2 API and the MCP dashboard tools, | ||
| survive the tile conversion in both directions, and are documented in the | ||
| OpenAPI spec. Previously they validated on write and were dropped before | ||
| persistence, so they could not be set through any surface. | ||
|
|
||
| `exemplarTraceSourceId` is checked three ways, because a marker's "view trace" | ||
| link is only as good as the source behind it: an ObjectId on both surfaces, the | ||
| source must exist for the team, and it must actually be a Trace source. The | ||
| existence and kind checks mirror the heatmap gate. Without them a well-formed id | ||
| for a metric source saved cleanly and left every marker linking nowhere. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import { Types } from 'mongoose'; | ||
|
|
||
| import { mcpTilesParam } from '@/mcp/tools/dashboards/schemas'; | ||
|
|
||
| /** | ||
| * `enableExemplars` and `exemplarTraceSourceId` are the two exemplar settings an | ||
| * MCP agent can put on a tile. The trace source id has to survive as something | ||
| * the source lookup can actually resolve — it is read back as a Mongo ObjectId — | ||
| * so accepting any string here would persist a tile whose markers silently never | ||
| * link to a trace. | ||
| */ | ||
| const tile = | ||
| (displayType: 'line' | 'stacked_bar') => | ||
| (config: Record<string, unknown>) => [ | ||
| { | ||
| name: 'Latency', | ||
| config: { | ||
| displayType, | ||
| sourceId: new Types.ObjectId().toString(), | ||
| select: [{ aggFn: 'count', alias: 'Requests' }], | ||
| ...config, | ||
| }, | ||
| }, | ||
| ]; | ||
|
|
||
| // The two tile types declare the exemplar fields in separate schema blocks, so | ||
| // an edit that only updates one would otherwise go unnoticed. | ||
| const lineTile = tile('line'); | ||
| const barTile = tile('stacked_bar'); | ||
|
|
||
| describe('MCP tile exemplar settings', () => { | ||
| it('accepts a valid ObjectId as the exemplar trace source', () => { | ||
| const traceSourceId = new Types.ObjectId().toString(); | ||
| const parsed = mcpTilesParam.parse( | ||
| lineTile({ enableExemplars: true, exemplarTraceSourceId: traceSourceId }), | ||
| ); | ||
| expect(parsed[0].config).toMatchObject({ | ||
| enableExemplars: true, | ||
| exemplarTraceSourceId: traceSourceId, | ||
| }); | ||
| }); | ||
|
|
||
| it.each([ | ||
| ['a source name rather than an id', 'Traces'], | ||
| ['a truncated id', '507f1f77bcf86cd7994390'], | ||
| ['an over-long id', '507f1f77bcf86cd799439011ff'], | ||
| ['an empty string', ''], | ||
| ['a non-hex id of the right length', 'zzzzzzzzzzzzzzzzzzzzzzzz'], | ||
| ])('rejects %s as the exemplar trace source', (_label, value) => { | ||
| expect(() => | ||
| mcpTilesParam.parse(lineTile({ exemplarTraceSourceId: value })), | ||
| ).toThrow(/Invalid ObjectId/); | ||
| }); | ||
|
|
||
| // Worth recording because it is surprising: Mongo also accepts a 12-character | ||
| // string as 12 raw bytes, so `Types.ObjectId.isValid` — and therefore every | ||
| // objectIdSchema field in this codebase, not just this one — lets one through. | ||
| // Such an id simply resolves to no source, the same outcome as an id that | ||
| // does not exist, so it is not worth diverging from the shared validator | ||
| // here. | ||
| it('lets a 12-character string through, as every other id field does', () => { | ||
| expect(() => | ||
| mcpTilesParam.parse(lineTile({ exemplarTraceSourceId: 'trace-source' })), | ||
| ).not.toThrow(); | ||
| }); | ||
|
|
||
| it('leaves both settings optional', () => { | ||
| const parsed = mcpTilesParam.parse(lineTile({})); | ||
| expect(parsed[0].config).not.toHaveProperty('enableExemplars'); | ||
| expect(parsed[0].config).not.toHaveProperty('exemplarTraceSourceId'); | ||
| }); | ||
|
|
||
| it.each([ | ||
| ['line', lineTile], | ||
| ['stacked_bar', barTile], | ||
| ])('validates the trace source on a %s tile', (_label, build) => { | ||
| expect(() => | ||
| mcpTilesParam.parse(build({ exemplarTraceSourceId: 'Traces' })), | ||
| ).toThrow(/Invalid ObjectId/); | ||
| const id = new Types.ObjectId().toString(); | ||
| expect( | ||
| mcpTilesParam.parse(build({ exemplarTraceSourceId: id }))[0].config, | ||
| ).toMatchObject({ exemplarTraceSourceId: id }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -753,6 +753,25 @@ export const mcpLineTileSchema = mcpTileLayoutSchema.extend({ | |
| lineInterpolation: LineInterpolationSchema.optional().describe( | ||
| 'How points are connected: "smooth" (default), "linear", or "step".', | ||
| ), | ||
| enableExemplars: z | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 minor — Exemplar field definitions and describe strings copied verbatim into two MCP tile schemas Define one shared Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond |
||
| .boolean() | ||
| .optional() | ||
| .describe( | ||
| 'Overlay exemplars: markers for individual trace-linked data points. ' + | ||
| 'Only renders on an exemplar-eligible tile — a single non-ratio ' + | ||
| 'histogram metric series with no groupBy, aggregated with avg, min, ' + | ||
| 'max, quantile or last_value. Not count or sum: a counted point is ' + | ||
| 'not attributable to any one trace, so the overlay stays inert with ' + | ||
| 'no error.', | ||
| ), | ||
| exemplarTraceSourceId: objectIdSchema | ||
| .optional() | ||
| .describe( | ||
| 'Trace source an exemplar marker links to. Must be an existing Trace ' + | ||
| 'source — use clickstack_list_sources and pick one whose kind is ' + | ||
| '"trace"; anything else is rejected on save. Defaults to the chart ' + | ||
| "source's linked trace source when omitted.", | ||
| ), | ||
| }) | ||
| .describe('Line chart query and display settings.'), | ||
| }); | ||
|
|
@@ -786,6 +805,25 @@ export const mcpBarTileSchema = mcpTileLayoutSchema.extend({ | |
| .literal('stacked_bar') | ||
| .describe('Stacked bar chart over time'), | ||
| ...mcpStackedTimeChartConfigFields, | ||
| enableExemplars: z | ||
| .boolean() | ||
| .optional() | ||
| .describe( | ||
| 'Overlay exemplars: markers for individual trace-linked data points. ' + | ||
| 'Only renders on an exemplar-eligible tile — a single non-ratio ' + | ||
| 'histogram metric series with no groupBy, aggregated with avg, min, ' + | ||
| 'max, quantile or last_value. Not count or sum: a counted point is ' + | ||
| 'not attributable to any one trace, so the overlay stays inert with ' + | ||
| 'no error.', | ||
| ), | ||
| exemplarTraceSourceId: objectIdSchema | ||
| .optional() | ||
| .describe( | ||
| 'Trace source an exemplar marker links to. Must be an existing Trace ' + | ||
| 'source — use clickstack_list_sources and pick one whose kind is ' + | ||
| '"trace"; anything else is rejected on save. Defaults to the chart ' + | ||
| "source's linked trace source when omitted.", | ||
| ), | ||
| }) | ||
| .describe('Stacked bar chart query and display settings.'), | ||
| }); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔵 minor — Lint warning cap raised instead of fixing the new warnings
The two new warnings appear to come from the new tests' casts (
as ConfigTile['config'],as unknown as DashboardDocumentin v2/utils/tests/dashboards.test.ts), which break the repo's "avoidascasts" rule. Type the fixtures withsatisfiesor a typed builder and keep the cap at 302.Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond