-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
fix(tracemetrics): Validate aggregate sort bys for metrics #103555
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
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 |
|---|---|---|
|
|
@@ -4,21 +4,29 @@ import {defined} from 'sentry/utils'; | |
| import type {Sort} from 'sentry/utils/discover/fields'; | ||
| import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode'; | ||
| import type {AggregateField} from 'sentry/views/explore/queryParams/aggregateField'; | ||
| import {validateAggregateSort} from 'sentry/views/explore/queryParams/aggregateSortBy'; | ||
| import {isGroupBy, type GroupBy} from 'sentry/views/explore/queryParams/groupBy'; | ||
| import {ReadableQueryParams} from 'sentry/views/explore/queryParams/readableQueryParams'; | ||
| import { | ||
| isBaseVisualize, | ||
| isVisualize, | ||
| Visualize, | ||
| VisualizeFunction, | ||
| type BaseVisualize, | ||
| } from 'sentry/views/explore/queryParams/visualize'; | ||
|
|
||
| export interface TraceMetric { | ||
| name: string; | ||
| type: string; | ||
| } | ||
|
|
||
| function isTraceMetric(value: unknown): value is TraceMetric { | ||
| if (value === null || !defined(value) || typeof value !== 'object') { | ||
| return false; | ||
| } | ||
|
|
||
| return 'name' in value && !!value.name && 'type' in value && !!value.type; | ||
| } | ||
|
|
||
| export interface BaseMetricQuery { | ||
| metric: TraceMetric; | ||
| queryParams: ReadableQueryParams; | ||
|
|
@@ -38,37 +46,21 @@ export function decodeMetricsQueryParams(value: string): BaseMetricQuery | null | |
| return null; | ||
| } | ||
|
|
||
| const metric = json.metric; | ||
| if (defined(metric) && typeof metric !== 'object') { | ||
| return null; | ||
| } | ||
|
|
||
| const query = json.query; | ||
| if (typeof query !== 'string') { | ||
| if (!isTraceMetric(json.metric)) { | ||
| return null; | ||
| } | ||
|
|
||
| const rawAggregateFields = json.aggregateFields; | ||
| if (!Array.isArray(rawAggregateFields)) { | ||
| return null; | ||
| } | ||
|
|
||
| const visualizes = rawAggregateFields | ||
| .filter<BaseVisualize>(isBaseVisualize) | ||
| .flatMap(vis => Visualize.fromJSON(vis)); | ||
| const metric = json.metric; | ||
| const query = parseQuery(json.query); | ||
| const visualizes = parseVisualizes(json.aggregateFields); | ||
|
|
||
| if (!visualizes.length) { | ||
| return null; | ||
| } | ||
|
|
||
| const groupBys = rawAggregateFields.filter<GroupBy>(isGroupBy); | ||
|
|
||
| const groupBys = parseGroupBys(json.aggregateFields); | ||
| const aggregateFields = [...visualizes, ...groupBys]; | ||
|
|
||
| const aggregateSortBys = json.aggregateSortBys; | ||
| if (!Array.isArray(aggregateSortBys)) { | ||
| return null; | ||
| } | ||
| const aggregateSortBys = parseAggregateSortBys(json.aggregateSortBys, aggregateFields); | ||
|
|
||
| return { | ||
| metric, | ||
|
|
@@ -154,13 +146,24 @@ export function defaultSortBys(fields: string[]): Sort[] { | |
| return []; | ||
| } | ||
|
|
||
| function defaultVisualize(): Visualize { | ||
| return new VisualizeFunction('per_second(value)'); | ||
| } | ||
|
|
||
| function defaultGroupBys(): GroupBy[] { | ||
| return []; | ||
| } | ||
|
|
||
| export function defaultAggregateFields(): AggregateField[] { | ||
| return [new VisualizeFunction('per_second(value)')]; | ||
| return [defaultVisualize(), ...defaultGroupBys()]; | ||
| } | ||
|
|
||
| export function defaultAggregateSortBys(_: AggregateField[]): Sort[] { | ||
| // TODO: Handle aggregate fields for compound sort-by. | ||
| return [{field: 'per_second(value)', kind: 'desc'}]; | ||
| export function defaultAggregateSortBys(aggregateFields: AggregateField[]): Sort[] { | ||
| const visualize = aggregateFields.find(isVisualize); | ||
| if (!defined(visualize)) { | ||
| return []; | ||
| } | ||
| return [{field: visualize.yAxis, kind: 'desc'}]; | ||
| } | ||
|
|
||
| export function stripMetricParamsFromLocation(location: Location): Location { | ||
|
|
@@ -173,3 +176,41 @@ export function stripMetricParamsFromLocation(location: Location): Location { | |
|
|
||
| return target; | ||
| } | ||
|
|
||
| function parseQuery(value: unknown): string { | ||
| return typeof value === 'string' ? value : defaultQuery(); | ||
| } | ||
|
|
||
| function parseVisualizes(value: unknown): Visualize[] { | ||
| if (!Array.isArray(value)) { | ||
| return []; | ||
| } | ||
|
|
||
| const baseVisualize = value.find(isBaseVisualize); | ||
| return baseVisualize ? Visualize.fromJSON(baseVisualize) : []; | ||
|
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. Bug: Support for Multiple Visualizations BrokenThe |
||
| } | ||
|
|
||
| function parseGroupBys(value: unknown): GroupBy[] { | ||
| if (!Array.isArray(value)) { | ||
| return defaultGroupBys(); | ||
| } | ||
|
|
||
| return value.filter<GroupBy>(isGroupBy); | ||
| } | ||
|
|
||
| function parseAggregateSortBys( | ||
| value: unknown, | ||
| aggregateFields: AggregateField[] | ||
| ): Sort[] { | ||
| if (!Array.isArray(value) || value.length === 0) { | ||
| return defaultAggregateSortBys(aggregateFields); | ||
| } | ||
|
|
||
| if (value.length > 0) { | ||
| if (value.some(v => !validateAggregateSort(v, aggregateFields))) { | ||
| return defaultAggregateSortBys(aggregateFields); | ||
| } | ||
| } | ||
|
|
||
| return value; | ||
| } | ||
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.
Bug: Refine
isTraceMetricValidation for Null and TypesThe
isTraceMetricfunction crashes whenvalueisnullbecausetypeof null === 'object'in JavaScript, causing line 27 to attempt property access onnullwhich throws a TypeError. Additionally, the function incorrectly rejects validTraceMetricobjects with empty strings (like{name: '', type: ''}used indefaultMetricQuery) because!!''evaluates tofalse. The checks should verify property types instead of truthiness and explicitly excludenull.