Skip to content
27 changes: 17 additions & 10 deletions static/app/utils/timeSeries/transformLegacySeriesToPlottables.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import {
} from 'sentry/utils/discover/fields';
import type {Widget} from 'sentry/views/dashboards/types';
import {DisplayType} from 'sentry/views/dashboards/types';
import type {TimeSeries} from 'sentry/views/dashboards/widgets/common/types';
import type {
AttributeValueType,
AttributeValueUnit,
TimeSeries,
} from 'sentry/views/dashboards/widgets/common/types';
import {Area} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/area';
import {Bars} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/bars';
import {Line} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/line';
Expand All @@ -19,7 +23,9 @@ import {convertEventsStatsToTimeSeriesData} from 'sentry/views/insights/common/q
*/
export function transformLegacySeriesToPlottables(
timeseriesResults: Series[] | undefined,
timeseriesResultsTypes: Record<string, AggregationOutputType> | undefined,
valueUnitResultTypes:
| Record<string, {valueType: AttributeValueType; valueUnit: AttributeValueUnit}>
| undefined,
widget: Widget
): Plottable[] {
if (!timeseriesResults || timeseriesResults.length === 0) {
Expand All @@ -30,13 +36,14 @@ export function transformLegacySeriesToPlottables(
.map(series => {
const unaliasedSeriesName =
series.seriesName?.split(' : ').at(-1)?.trim() ?? series.seriesName;
const fieldType =
timeseriesResultsTypes?.[unaliasedSeriesName] ??
aggregateOutputType(unaliasedSeriesName);
const {valueType, valueUnit} = mapAggregationTypeToValueTypeAndUnit(
fieldType,
unaliasedSeriesName
);

const {valueType, valueUnit} =
valueUnitResultTypes?.[unaliasedSeriesName] ??
mapAggregationTypeToValueTypeAndUnit(
aggregateOutputType(unaliasedSeriesName),
unaliasedSeriesName
);

const timeSeries = convertEventsStatsToTimeSeriesData(
series.seriesName,
createEventsStatsFromSeries(series, valueType as AggregationOutputType, valueUnit)
Expand Down Expand Up @@ -91,7 +98,7 @@ function createPlottableFromTimeSeries(
}
}

function mapAggregationTypeToValueTypeAndUnit(
export function mapAggregationTypeToValueTypeAndUnit(
aggregationType: AggregationOutputType,
fieldName: string
): {
Expand Down
11 changes: 11 additions & 0 deletions static/app/views/dashboards/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ 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 {
AttributeValueType,
AttributeValueUnit,
} from 'sentry/views/dashboards/widgets/common/types';

import type {ThresholdsConfig} from './widgetBuilder/buildSteps/thresholdsStep/thresholds';

Expand Down Expand Up @@ -82,6 +86,11 @@ export type LinkedDashboard = {
staticDashboardId?: PrebuiltDashboardId;
};

export type Unit = {
valueType: AttributeValueType;
valueUnit: AttributeValueUnit;
};

/**
* A widget query is one or more aggregates and a single filter string (conditions.)
* Widgets can have multiple widget queries, and they all combine into a unified timeseries view (for example)
Expand Down Expand Up @@ -109,6 +118,8 @@ export type WidgetQuery = {
// TODO: currently not stored in the backend, only used
// by prebuilt dashboards in the frontend.
slideOutId?: SlideoutId;
// Used to define the units of the fields in the widget queries, currently not saved
units?: Array<Unit | null>;
};

type WidgetChangedReason = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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})`,
],
units: [null, {valueType: 'percentage', valueUnit: null}],
columns: [],
fieldAliases: [],
conditions: `${SpanFields.SPAN_OP}:http.server`,
Expand Down Expand Up @@ -129,6 +130,7 @@ const SECOND_ROW_WIDGETS: Widget[] = spaceWidgetsEquallyOnRow(
`equation|count_if(${SpanFields.TRACE_STATUS},equals,internal_error) / count(${SpanFields.SPAN_DURATION})`,
],
columns: [],
units: [null, {valueType: 'percentage', valueUnit: null}],
fieldAliases: [],
conditions: `${SpanFields.SPAN_OP}:queue.process`,
orderby: `count(${SpanFields.SPAN_DURATION})`,
Expand Down Expand Up @@ -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],
units: [{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})`,
Expand Down Expand Up @@ -211,6 +213,7 @@ const THIRD_ROW_WIDGETS: Widget[] = spaceWidgetsEquallyOnRow(
`equation|count_if(${SpanFields.TRACE_STATUS},equals,internal_error) / count(${SpanFields.SPAN_DURATION})`,
],
columns: [],
units: [null, {valueType: 'percentage', valueUnit: null}],
fieldAliases: ['Jobs', 'Error Rate'],
conditions: `${SpanFields.SPAN_OP}:queue.process`,
orderby: `-count(${SpanFields.SPAN_DURATION})`,
Expand Down Expand Up @@ -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],
units: [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]',
Expand Down Expand Up @@ -329,6 +333,16 @@ const TRANSACTIONS_TABLE: Widget = {
'Users',
'Time Spent',
],
units: [
null,
null,
null,
null,
null,
null,
null,
{valueType: 'percentage', valueUnit: null},
],
conditions: TABLE_QUERY.formatString(),
orderby: '-sum(span.duration)',
linkedDashboards: [],
Expand Down
12 changes: 12 additions & 0 deletions static/app/views/dashboards/widgetCard/genericWidgetQueries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,18 @@ class GenericWidgetQueries<SeriesResponse, TableResponse> extends Component<
// Overwrite the local var to work around state being stale in tests.
transformedTableResults = [...transformedTableResults, transformedData];

const meta = transformedTableResults?.[0]?.meta;
const widgetUnits = widget?.queries?.[i]?.units;
if (widgetUnits && meta) {
widgetUnits.forEach((unit, index) => {
const field = widget.queries?.[i]?.fields?.[index];
if (unit && field) {
meta.units![field] = unit.valueUnit ?? '';
meta.fields![field] = unit.valueType;
}
});
}

// There is some inconsistency with the capitalization of "link" in response headers
responsePageLinks =
(resp?.getResponseHeader('Link') || resp?.getResponseHeader('link')) ?? undefined;
Expand Down
49 changes: 45 additions & 4 deletions static/app/views/dashboards/widgetCard/visualizationWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import type {PageFilters} from 'sentry/types/core';
import type {Series} from 'sentry/types/echarts';
import type {AggregationOutputType, Sort} from 'sentry/utils/discover/fields';
import {transformLegacySeriesToPlottables} from 'sentry/utils/timeSeries/transformLegacySeriesToPlottables';
import {
mapAggregationTypeToValueTypeAndUnit,
transformLegacySeriesToPlottables,
} from 'sentry/utils/timeSeries/transformLegacySeriesToPlottables';
import {useReleaseStats} from 'sentry/utils/useReleaseStats';
import type {DashboardFilters, Widget} from 'sentry/views/dashboards/types';
import type {TabularColumn} from 'sentry/views/dashboards/widgets/common/types';
import type {
AttributeValueType,
AttributeValueUnit,
TabularColumn,
} from 'sentry/views/dashboards/widgets/common/types';
import {TimeSeriesWidgetVisualization} from 'sentry/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization';
import type {LoadableChartWidgetProps} from 'sentry/views/insights/common/components/widgets/types';

Expand Down Expand Up @@ -58,10 +65,44 @@ export function VisualizationWidget({
onDataFetchStart={onDataFetchStart}
tableItemLimit={tableItemLimit}
>
{({timeseriesResults, timeseriesResultsTypes, errorMessage, loading}) => {
{({timeseriesResults, timeseriesResultsTypes = {}, errorMessage, loading}) => {
const valueUnitResultTypes: Record<
string,
{valueType: AttributeValueType; valueUnit: AttributeValueUnit}
> = {};
Object.entries(timeseriesResultsTypes).forEach(([key, outputType]) => {
valueUnitResultTypes[key] = mapAggregationTypeToValueTypeAndUnit(
outputType,
key
);
});

widget.queries.forEach(query => {
query.units?.forEach((unit, index) => {
if (unit && query.fields) {
valueUnitResultTypes[query.fields[index]!] = unit;
}
});
});

const firstUnit = widget.queries[0]?.units?.[0];

if (
firstUnit &&
widget.queries?.[0]?.aggregates?.length === 1 &&
widget.queries?.[0]?.columns?.length > 0
) {
// 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 group bys
timeseriesResults?.forEach(series => {
valueUnitResultTypes[series.seriesName] = firstUnit;
});
}

const plottables = transformLegacySeriesToPlottables(
timeseriesResults,
timeseriesResultsTypes,
valueUnitResultTypes,
widget
);

Expand Down
4 changes: 2 additions & 2 deletions static/app/views/dashboards/widgets/common/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {Confidence} from 'sentry/types/organization';
import type {DataUnit} from 'sentry/utils/discover/fields';
import type {ThresholdsConfig} from 'sentry/views/dashboards/widgetBuilder/buildSteps/thresholdsStep/thresholds';

type AttributeValueType =
export type AttributeValueType =
| 'number'
| 'integer'
| 'date'
Expand All @@ -16,7 +16,7 @@ type AttributeValueType =
| 'score'
| 'currency';

type AttributeValueUnit = DataUnit | null;
export type AttributeValueUnit = DataUnit | null;

type TimeSeriesValueType = AttributeValueType;
export type TimeSeriesValueUnit = AttributeValueUnit;
Expand Down
Loading