diff --git a/packages/cli/src/types/dashboard.ts b/packages/cli/src/types/dashboard.ts index 07b83188e..c6f8c5981 100644 --- a/packages/cli/src/types/dashboard.ts +++ b/packages/cli/src/types/dashboard.ts @@ -388,6 +388,10 @@ function extractFunctionName(aggregate: string): string { * Example: `p50(value,completion.duration_ms,distribution,none)` */ function isTracemetricsAggregate(aggregate: string): boolean { + // equation| prefix denotes a formula combining multiple tracemetrics aggregates + if (aggregate.startsWith("equation|")) { + return true; + } const parenIdx = aggregate.indexOf("("); if (parenIdx < 0) { return false; diff --git a/packages/cli/test/types/dashboard.test.ts b/packages/cli/test/types/dashboard.test.ts index 17702440b..93f427fcc 100644 --- a/packages/cli/test/types/dashboard.test.ts +++ b/packages/cli/test/types/dashboard.test.ts @@ -33,6 +33,7 @@ import { TABLE_DISPLAY_TYPES, type TextResult, TIMESERIES_DISPLAY_TYPES, + validateAggregateNames, validateWidgetLayout, WIDGET_TYPES, type WidgetDataResult, @@ -963,6 +964,34 @@ describe("mapWidgetTypeToDataset", () => { }); }); +describe("validateAggregateNames", () => { + test("accepts comma-separated tracemetrics aggregates", () => { + expect(() => + validateAggregateNames( + ["p50(value,completion.duration_ms,distribution,none)"], + "tracemetrics" + ) + ).not.toThrow(); + }); + + test("accepts equation| formula aggregates for tracemetrics", () => { + expect(() => + validateAggregateNames( + [ + "equation|p50(value,a,distribution,none) / p50(value,b,distribution,none)", + ], + "tracemetrics" + ) + ).not.toThrow(); + }); + + test("rejects span-style aggregates for tracemetrics", () => { + expect(() => validateAggregateNames(["count()"], "tracemetrics")).toThrow( + ValidationError + ); + }); +}); + // --------------------------------------------------------------------------- // validateWidgetLayout // ---------------------------------------------------------------------------