123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- import omit from 'lodash/omit';
- import type {Client, ResponseMeta} from 'sentry/api';
- import {t} from 'sentry/locale';
- import type {
- MetricsQueryApiResponse,
- Organization,
- PageFilters,
- TagCollection,
- } from 'sentry/types';
- import type {Series} from 'sentry/types/echarts';
- import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements';
- import type {TableData} from 'sentry/utils/discover/discoverQuery';
- import type {EventData} from 'sentry/utils/discover/eventView';
- import {NumberContainer} from 'sentry/utils/discover/styles';
- import {getMetricsSeriesName, groupByOp} from 'sentry/utils/metrics';
- import {formatMetricUsingUnit} from 'sentry/utils/metrics/formatters';
- import {
- formatMRIField,
- getMRI,
- getUseCaseFromMRI,
- isMRIField,
- parseField,
- parseMRI,
- } from 'sentry/utils/metrics/mri';
- import {getMetricsQueryApiRequestPayload} from 'sentry/utils/metrics/useMetricsQuery';
- import type {OnDemandControlContext} from 'sentry/utils/performance/contexts/onDemandControl';
- import {MetricSearchBar} from 'sentry/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/metricSearchBar';
- import type {FieldValueOption} from 'sentry/views/discover/table/queryField';
- import {FieldValueKind} from 'sentry/views/discover/table/types';
- import type {Widget, WidgetQuery} from '../types';
- import {DisplayType} from '../types';
- import type {DatasetConfig} from './base';
- import {handleOrderByReset} from './base';
- const DEFAULT_WIDGET_QUERY: WidgetQuery = {
- name: '',
- fields: [''],
- columns: [''],
- fieldAliases: [],
- aggregates: [''],
- conditions: '',
- orderby: '',
- };
- export const MetricsConfig: DatasetConfig<
- MetricsQueryApiResponse,
- MetricsQueryApiResponse
- > = {
- defaultWidgetQuery: DEFAULT_WIDGET_QUERY,
- enableEquations: false,
- getTableRequest: (
- api: Client,
- _: Widget,
- query: WidgetQuery,
- organization: Organization,
- pageFilters: PageFilters,
- __?: OnDemandControlContext,
- limit?: number
- ) => getMetricRequest(api, query, organization, pageFilters, limit),
- getSeriesRequest: getMetricSeriesRequest,
- getCustomFieldRenderer: field => (data: EventData) =>
- renderMetricField(field, data[field]),
- SearchBar: MetricSearchBar,
- handleOrderByReset: handleMetricTableOrderByReset,
- supportedDisplayTypes: [
- DisplayType.AREA,
- DisplayType.BAR,
- DisplayType.BIG_NUMBER,
- DisplayType.LINE,
- DisplayType.TABLE,
- DisplayType.TOP_N,
- ],
- transformSeries: transformMetricsResponseToSeries,
- transformTable: transformMetricsResponseToTable,
- getTableFieldOptions: getFields,
- getTimeseriesSortOptions: getMetricTimeseriesSortOptions,
- getTableSortOptions: getMetricTableSortOptions,
- filterTableOptions: filterMetricOperations,
- filterYAxisOptions: () => option => filterMetricOperations(option),
- filterAggregateParams: filterMetricMRIs,
- filterYAxisAggregateParams: () => option => filterMetricMRIs(option),
- getGroupByFieldOptions: getTagsForMetric,
- getFieldHeaderMap: getFormattedMRIHeaders,
- };
- export function renderMetricField(field: string, value: any) {
- const parsedField = parseField(field);
- if (parsedField) {
- const unit = parseMRI(parsedField.mri)?.unit ?? '';
- return <NumberContainer>{formatMetricUsingUnit(value, unit)}</NumberContainer>;
- }
- return value;
- }
- export function formatMetricAxisValue(field: string, value: number) {
- const unit = parseMRI(parseField(field)?.mri)?.unit ?? '';
- return formatMetricUsingUnit(value, unit);
- }
- function getFormattedMRIHeaders(query?: WidgetQuery) {
- if (!query) {
- return {};
- }
- return (query.fields || []).reduce((acc, field, index) => {
- const fieldAlias = query.fieldAliases?.[index];
- acc[field] = fieldAlias || formatMRIField(field);
- return acc;
- }, {});
- }
- function getMetricTimeseriesSortOptions(_, widgetQuery: WidgetQuery) {
- if (!widgetQuery.fields?.[0]) {
- return [];
- }
- return widgetQuery.fields.reduce((acc, field) => {
- if (!isMRIField(field)) {
- return acc;
- }
- return {
- ...acc,
- [`field:${field}`]: {
- label: formatMRIField(field),
- value: {
- kind: FieldValueKind.FIELD,
- value: field,
- meta: {
- name: field,
- dataType: 'number',
- },
- },
- },
- };
- }, {});
- }
- function getMetricTableSortOptions(_, widgetQuery: WidgetQuery) {
- if (!widgetQuery.fields?.[0]) {
- return [];
- }
- return widgetQuery.fields
- .map((field, i) => {
- const alias = widgetQuery.fieldAliases?.[i];
- return {
- label: alias || formatMRIField(field),
- value: field,
- };
- })
- .filter(option => isMRIField(option.value));
- }
- function getFields(
- organization: Organization,
- _?: TagCollection | undefined,
- __?: CustomMeasurementCollection,
- api?: Client
- ) {
- if (!api) {
- return {};
- }
- return api
- .requestPromise(`/organizations/${organization.slug}/metrics/meta/`, {
- query: {useCase: 'custom'},
- })
- .then(metaReponse => {
- const groupedByOp = groupByOp(metaReponse);
- const typesByOp: Record<string, Set<string>> = Object.entries(groupedByOp).reduce(
- (acc, [operation, fields]) => {
- const types = new Set();
- fields.forEach(field => types.add(field.type));
- acc[operation] = types;
- return acc;
- },
- {}
- );
- const fieldOptions: Record<string, any> = {};
- Object.entries(groupedByOp).forEach(([operation, fields]) => {
- fieldOptions[`function:${operation}`] = {
- label: `${operation}(${'\u2026'})`,
- value: {
- kind: FieldValueKind.FUNCTION,
- meta: {
- name: operation,
- parameters: [
- {
- kind: 'column',
- columnTypes: [...typesByOp[operation]],
- defaultValue: fields[0].mri,
- required: true,
- },
- ],
- },
- },
- };
- });
- metaReponse
- .sort((a, b) => a.name.localeCompare(b.name))
- .forEach(field => {
- fieldOptions[`field:${field.mri}`] = {
- label: field.name,
- value: {
- kind: FieldValueKind.METRICS,
- meta: {
- name: field.mri,
- dataType: field.type,
- },
- },
- };
- });
- return fieldOptions;
- });
- }
- function filterMetricOperations(option: FieldValueOption) {
- return option.value.kind === FieldValueKind.FUNCTION;
- }
- function filterMetricMRIs(option: FieldValueOption) {
- return option.value.kind === FieldValueKind.METRICS;
- }
- function getTagsForMetric(
- organization: Organization,
- _?: TagCollection,
- __?: CustomMeasurementCollection,
- api?: Client,
- queries?: WidgetQuery[]
- ) {
- const fieldOptions = {};
- if (!api) {
- return fieldOptions;
- }
- const field = queries?.[0].aggregates[0] ?? '';
- const mri = getMRI(field);
- const useCase = getUseCaseFromMRI(mri);
- return api
- .requestPromise(`/organizations/${organization.slug}/metrics/tags/`, {
- query: {metric: mri, useCase},
- })
- .then(tagsResponse => {
- tagsResponse.forEach(tag => {
- fieldOptions[`field:${tag.key}`] = {
- label: tag.key,
- value: {
- kind: FieldValueKind.TAG,
- meta: {name: tag.key, dataType: 'string'},
- },
- };
- });
- return fieldOptions;
- });
- }
- function getMetricSeriesRequest(
- api: Client,
- widget: Widget,
- queryIndex: number,
- organization: Organization,
- pageFilters: PageFilters
- ) {
- const query = widget.queries[queryIndex];
- return getMetricRequest(
- api,
- query,
- organization,
- pageFilters,
- widget.limit,
- widget.displayType
- );
- }
- function handleMetricTableOrderByReset(widgetQuery: WidgetQuery, newFields: string[]) {
- const disableSortBy = widgetQuery.columns.includes('session.status');
- if (disableSortBy) {
- widgetQuery.orderby = '';
- }
- return handleOrderByReset(widgetQuery, newFields);
- }
- export function transformMetricsResponseToTable(
- data: MetricsQueryApiResponse,
- widgetQuery: WidgetQuery
- ): TableData {
- const field = widgetQuery.aggregates[0];
- const rows = data.data.flatMap((group, index) =>
- group.map(entry => {
- const groupColumn = mapMetricGroupsToFields(entry.by);
- const value = {
- [field]: entry.totals,
- };
- return {
- id: String(index),
- ...groupColumn,
- ...value,
- };
- })
- );
- const singleRow = rows[0];
- const meta = {
- ...changeObjectValuesToTypes(omit(singleRow, 'id')),
- };
- return {meta, data: rows};
- }
- function mapMetricGroupsToFields(
- results: Record<string, number | string | null> | undefined
- ) {
- if (!results) {
- return {};
- }
- const mappedResults: typeof results = {};
- for (const [key, value] of Object.entries(results)) {
- mappedResults[key] = value;
- }
- return mappedResults;
- }
- function changeObjectValuesToTypes(
- obj: Record<string, number | string | null> | undefined
- ) {
- return Object.entries(obj ?? {}).reduce((acc, [key, value]) => {
- acc[key] = typeof value;
- return acc;
- }, {});
- }
- export function transformMetricsResponseToSeries(
- data: MetricsQueryApiResponse,
- widgetQuery: WidgetQuery
- ) {
- if (data === null) {
- return [];
- }
- const field = widgetQuery.aggregates[0];
- const results: Series[] = [];
- const queryAlias = widgetQuery.name;
- if (!data.data.length) {
- return [
- {
- seriesName: `(${t('no results')})`,
- data: data.intervals.map(interval => ({
- name: interval,
- value: 0,
- })),
- },
- ];
- }
- data.data.forEach(group =>
- group.forEach(entry => {
- results.push({
- seriesName: getMetricsSeriesName(queryAlias || field, entry.by),
- data: data.intervals.map((interval, index) => ({
- name: interval,
- value: entry.series[field][index] ?? 0,
- })),
- });
- })
- );
- return results;
- }
- function getMetricRequest(
- api: Client,
- query: WidgetQuery,
- organization: Organization,
- pageFilters: PageFilters,
- limit?: number,
- displayType?: DisplayType
- ): Promise<[MetricsQueryApiResponse, string | undefined, ResponseMeta | undefined]> {
- if (!query.aggregates[0]) {
- // No aggregate selected, return empty response
- return Promise.resolve([
- {
- intervals: [],
- groups: [],
- meta: [],
- },
- 'OK',
- {
- getResponseHeader: () => '',
- },
- ] as any);
- }
- const payload = getMetricsQueryApiRequestPayload(
- [
- {
- field: query.aggregates[0],
- query: query.conditions || undefined,
- groupBy: query.columns || undefined,
- orderBy: query.orderby
- ? query.orderby.indexOf('-') === 0
- ? 'desc'
- : 'asc'
- : undefined,
- limit: limit || undefined,
- },
- ],
- pageFilters,
- {
- intervalLadder: displayType === DisplayType.BAR ? 'bar' : 'dashboard',
- }
- );
- const pathname = `/organizations/${organization.slug}/metrics/query/`;
- return api.requestPromise(pathname, {
- method: 'POST',
- query: payload.query,
- data: payload.body,
- includeAllArgs: true,
- });
- }
|