diff --git a/static/app/views/dashboards/datasetConfig/spans.tsx b/static/app/views/dashboards/datasetConfig/spans.tsx index cd6d8c06aa9d69..04cabc0ac19463 100644 --- a/static/app/views/dashboards/datasetConfig/spans.tsx +++ b/static/app/views/dashboards/datasetConfig/spans.tsx @@ -23,6 +23,8 @@ import { isEquation, isEquationAlias, type Aggregation, + type AggregationOutputType, + type DataUnit, type QueryFieldValue, } from 'sentry/utils/discover/fields'; import { @@ -238,6 +240,57 @@ export const SpansConfig: DatasetConfig< } return getFieldRenderer(field, meta, false, widget, dashboardFilters); }, + getSeriesResultUnit: (data, widgetQuery) => { + const resultUnits: Record = {}; + widgetQuery.fieldMeta?.forEach((meta, index) => { + if (meta && widgetQuery.fields) { + resultUnits[widgetQuery.fields[index]!] = meta.valueUnit; + } + }); + const isMultiSeriesStats = isMultiSeriesEventsStats(data); + + // if there's only one aggregate and more then one group by the series names are the name of the group, not the aggregate name + // But we can just assume the units is for all the series + // TODO: This doesn't work with multiple aggregates + const firstMeta = widgetQuery.fieldMeta?.find(meta => meta !== null); + if ( + isMultiSeriesStats && + firstMeta && + widgetQuery.aggregates?.length === 1 && + widgetQuery.columns?.length > 0 + ) { + Object.keys(data).forEach(seriesName => { + resultUnits[seriesName] = firstMeta.valueUnit; + }); + } + return resultUnits; + }, + getSeriesResultType: (data, widgetQuery) => { + const resultTypes: Record = {}; + widgetQuery.fieldMeta?.forEach((meta, index) => { + if (meta && widgetQuery.fields) { + resultTypes[widgetQuery.fields[index]!] = meta.valueType as AggregationOutputType; + } + }); + + const isMultiSeriesStats = isMultiSeriesEventsStats(data); + + // if there's only one aggregate and more then one group by the series names are the name of the group, not the aggregate name + // But we can just assume the units is for all the series + // TODO: This doesn't work with multiple aggregates + const firstMeta = widgetQuery.fieldMeta?.find(meta => meta !== null); + if ( + isMultiSeriesStats && + firstMeta && + widgetQuery.aggregates?.length === 1 && + widgetQuery.columns?.length > 0 + ) { + Object.keys(data).forEach(seriesName => { + resultTypes[seriesName] = firstMeta.valueType as AggregationOutputType; + }); + } + return resultTypes; + }, }; function getPrimaryFieldOptions( diff --git a/static/app/views/dashboards/types.tsx b/static/app/views/dashboards/types.tsx index f2f6cf882f5f48..cfd5cf787b748f 100644 --- a/static/app/views/dashboards/types.tsx +++ b/static/app/views/dashboards/types.tsx @@ -5,6 +5,7 @@ import type {Tag} from 'sentry/types/group'; import type {User} from 'sentry/types/user'; import {SavedQueryDatasets, type DatasetSource} from 'sentry/utils/discover/types'; import type {PrebuiltDashboardId} from 'sentry/views/dashboards/utils/prebuiltConfigs'; +import type {TimeSeriesMeta} from 'sentry/views/dashboards/widgets/common/types'; import type {ThresholdsConfig} from './widgetBuilder/buildSteps/thresholdsStep/thresholds'; @@ -95,6 +96,8 @@ export type WidgetQuery = { // Table column alias. // We may want to have alias for y-axis in the future too fieldAliases?: string[]; + // Used to define the units of the fields in the widget queries, currently not saved + fieldMeta?: Array | null>; // Fields is replaced with aggregates + columns. It // is currently used to track column order on table // widgets. diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/backendOverview/backendOverview.ts b/static/app/views/dashboards/utils/prebuiltConfigs/backendOverview/backendOverview.ts index f69fddb450cf74..ed8565cd66c03f 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/backendOverview/backendOverview.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/backendOverview/backendOverview.ts @@ -48,6 +48,7 @@ const FIRST_ROW_WIDGETS: Widget[] = spaceWidgetsEquallyOnRow( `count(${SpanFields.SPAN_DURATION})`, `equation|count_if(${SpanFields.TRACE_STATUS},equals,internal_error) / count(${SpanFields.SPAN_DURATION})`, ], + fieldMeta: [null, {valueType: 'percentage', valueUnit: null}], columns: [], fieldAliases: [], conditions: `${SpanFields.SPAN_OP}:http.server`, @@ -129,6 +130,7 @@ const SECOND_ROW_WIDGETS: Widget[] = spaceWidgetsEquallyOnRow( `equation|count_if(${SpanFields.TRACE_STATUS},equals,internal_error) / count(${SpanFields.SPAN_DURATION})`, ], columns: [], + fieldMeta: [null, {valueType: 'percentage', valueUnit: null}], fieldAliases: [], conditions: `${SpanFields.SPAN_OP}:queue.process`, orderby: `count(${SpanFields.SPAN_DURATION})`, @@ -171,13 +173,13 @@ const SECOND_ROW_WIDGETS: Widget[] = spaceWidgetsEquallyOnRow( { name: '', fields: [ - SpanFields.TRANSACTION, `equation|count_if(${SpanFields.CACHE_HIT},equals,false) / count(${SpanFields.SPAN_DURATION})`, ], aggregates: [ `equation|count_if(${SpanFields.CACHE_HIT},equals,false) / count(${SpanFields.SPAN_DURATION})`, ], columns: [SpanFields.TRANSACTION], + fieldMeta: [{valueType: 'percentage', valueUnit: null}], fieldAliases: [''], conditions: `${SpanFields.SPAN_OP}:[cache.get,cache.get_item]`, orderby: `-equation|count_if(${SpanFields.CACHE_HIT},equals,false) / count(${SpanFields.SPAN_DURATION})`, @@ -211,6 +213,7 @@ const THIRD_ROW_WIDGETS: Widget[] = spaceWidgetsEquallyOnRow( `equation|count_if(${SpanFields.TRACE_STATUS},equals,internal_error) / count(${SpanFields.SPAN_DURATION})`, ], columns: [], + fieldMeta: [null, {valueType: 'percentage', valueUnit: null}], fieldAliases: ['Jobs', 'Error Rate'], conditions: `${SpanFields.SPAN_OP}:queue.process`, orderby: `-count(${SpanFields.SPAN_DURATION})`, @@ -271,6 +274,7 @@ const THIRD_ROW_WIDGETS: Widget[] = spaceWidgetsEquallyOnRow( `equation|count_if(${SpanFields.CACHE_HIT},equals,false) / count(${SpanFields.SPAN_DURATION})`, ], columns: [SpanFields.TRANSACTION], + fieldMeta: [null, null, null, {valueType: 'percentage', valueUnit: null}], fieldAliases: ['', 'Cache Misses', 'Cache Calls', 'Cache Miss Rate'], conditions: `${SpanFields.SPAN_OP}:[cache.get,cache.get_item]`, orderby: '-equation[1]', @@ -329,6 +333,16 @@ const TRANSACTIONS_TABLE: Widget = { 'Users', 'Time Spent', ], + fieldMeta: [ + null, + null, + null, + null, + null, + null, + null, + {valueType: 'percentage', valueUnit: null}, + ], conditions: TABLE_QUERY.formatString(), orderby: '-sum(span.duration)', linkedDashboards: [], diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/http/domainSummary.ts b/static/app/views/dashboards/utils/prebuiltConfigs/http/domainSummary.ts index 7aa5d55a4419ed..2fad6f767886b8 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/http/domainSummary.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/http/domainSummary.ts @@ -112,6 +112,7 @@ const BIG_NUMBER_ROW_WIDGETS: Widget[] = spaceWidgetsEquallyOnRow( aggregates: [PERCENTAGE_3XX], columns: [], orderby: PERCENTAGE_3XX, + fieldMeta: [{valueType: 'percentage', valueUnit: null}], }, ], }, @@ -129,6 +130,7 @@ const BIG_NUMBER_ROW_WIDGETS: Widget[] = spaceWidgetsEquallyOnRow( aggregates: [PERCENTAGE_4XX], columns: [], orderby: PERCENTAGE_4XX, + fieldMeta: [{valueType: 'percentage', valueUnit: null}], }, ], }, @@ -146,6 +148,7 @@ const BIG_NUMBER_ROW_WIDGETS: Widget[] = spaceWidgetsEquallyOnRow( aggregates: [PERCENTAGE_5XX], columns: [], orderby: PERCENTAGE_5XX, + fieldMeta: [{valueType: 'percentage', valueUnit: null}], }, ], }, @@ -221,6 +224,7 @@ const CHART_ROW_WIDGETS: Widget[] = spaceWidgetsEquallyOnRow( aggregates: [PERCENTAGE_3XX], columns: [], orderby: PERCENTAGE_3XX, + fieldMeta: [{valueType: 'percentage', valueUnit: null}], }, { name: '4XX', @@ -229,6 +233,7 @@ const CHART_ROW_WIDGETS: Widget[] = spaceWidgetsEquallyOnRow( aggregates: [PERCENTAGE_4XX], columns: [], orderby: PERCENTAGE_4XX, + fieldMeta: [{valueType: 'percentage', valueUnit: null}], }, { name: '5XX', @@ -237,6 +242,7 @@ const CHART_ROW_WIDGETS: Widget[] = spaceWidgetsEquallyOnRow( aggregates: [PERCENTAGE_5XX], columns: [], orderby: PERCENTAGE_5XX, + fieldMeta: [{valueType: 'percentage', valueUnit: null}], }, ], }, @@ -279,6 +285,13 @@ const TRANSACTIONS_TABLE: Widget = { DataTitles.avg, DataTitles.timeSpent, ], + fieldMeta: [ + null, + null, + {valueType: 'percentage', valueUnit: null}, + {valueType: 'percentage', valueUnit: null}, + {valueType: 'percentage', valueUnit: null}, + ], conditions: FILTER_STRING, name: '', orderby: '-sum(span.self_time)', diff --git a/static/app/views/dashboards/utils/prebuiltConfigs/http/http.ts b/static/app/views/dashboards/utils/prebuiltConfigs/http/http.ts index 835977048f21b8..461983931d03a0 100644 --- a/static/app/views/dashboards/utils/prebuiltConfigs/http/http.ts +++ b/static/app/views/dashboards/utils/prebuiltConfigs/http/http.ts @@ -71,6 +71,7 @@ const FIRST_ROW_WIDGETS: Widget[] = spaceWidgetsEquallyOnRow( fields: [PERCENTAGE_3XX], aggregates: [PERCENTAGE_3XX], columns: [], + fieldMeta: [{valueType: 'percentage', valueUnit: null}], orderby: PERCENTAGE_3XX, }, { @@ -79,6 +80,7 @@ const FIRST_ROW_WIDGETS: Widget[] = spaceWidgetsEquallyOnRow( fields: [PERCENTAGE_4XX], aggregates: [PERCENTAGE_4XX], columns: [], + fieldMeta: [{valueType: 'percentage', valueUnit: null}], orderby: PERCENTAGE_4XX, }, { @@ -87,6 +89,7 @@ const FIRST_ROW_WIDGETS: Widget[] = spaceWidgetsEquallyOnRow( fields: [PERCENTAGE_5XX], aggregates: [PERCENTAGE_5XX], columns: [], + fieldMeta: [{valueType: 'percentage', valueUnit: null}], orderby: PERCENTAGE_5XX, }, ], @@ -139,6 +142,14 @@ const DOMAIN_TABLE: Widget = { staticDashboardId: 5, }, ], + fieldMeta: [ + null, + null, + null, + {valueType: 'percentage', valueUnit: null}, + {valueType: 'percentage', valueUnit: null}, + {valueType: 'percentage', valueUnit: null}, + ], conditions: FILTER_STRING, name: '', orderby: '-sum(span.self_time)', diff --git a/static/app/views/dashboards/widgetCard/genericWidgetQueries.tsx b/static/app/views/dashboards/widgetCard/genericWidgetQueries.tsx index b71a781e4a96c0..ce6ae2e4f9c317 100644 --- a/static/app/views/dashboards/widgetCard/genericWidgetQueries.tsx +++ b/static/app/views/dashboards/widgetCard/genericWidgetQueries.tsx @@ -308,6 +308,18 @@ class GenericWidgetQueries extends Component< ) as TableDataWithTitle; transformedData.title = widget.queries[i]?.name ?? ''; + const meta = transformedData.meta; + const fieldMeta = widget?.queries?.[i]?.fieldMeta; + if (fieldMeta && meta) { + fieldMeta.forEach((m, index) => { + const field = widget.queries?.[i]?.fields?.[index]; + if (m && field) { + meta.units![field] = m.valueUnit ?? ''; + meta.fields![field] = m.valueType; + } + }); + } + // Overwrite the local var to work around state being stale in tests. transformedTableResults = [...transformedTableResults, transformedData]; @@ -390,14 +402,28 @@ class GenericWidgetQueries extends Component< // to derive the types and units since they share the same aggregations and fields const timeseriesResultsTypes = responses.reduce( (acc, response) => { - acc = {...acc, ...config.getSeriesResultType?.(response[0], widget.queries[0]!)}; + let allResultTypes: Record = {}; + widget.queries.forEach(query => { + allResultTypes = { + ...allResultTypes, + ...config.getSeriesResultType?.(response[0], query), + }; + }); + acc = {...acc, ...allResultTypes}; return acc; }, {} as Record ); const timeseriesResultsUnits = responses.reduce( (acc, response) => { - acc = {...acc, ...config.getSeriesResultUnit?.(response[0], widget.queries[0]!)}; + let allResultUnits: Record = {}; + widget.queries.forEach(query => { + allResultUnits = { + ...allResultUnits, + ...config.getSeriesResultUnit?.(response[0], query), + }; + }); + acc = {...acc, ...allResultUnits}; return acc; }, {} as Record