import trimStart from 'lodash/trimStart'; import {doEventsRequest} from 'sentry/actionCreators/events'; import type {Client} from 'sentry/api'; import type {PageFilters} from 'sentry/types/core'; import type {Series} from 'sentry/types/echarts'; import type {TagCollection} from 'sentry/types/group'; import type { EventsStats, MultiSeriesEventsStats, Organization, } from 'sentry/types/organization'; import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements'; import type {EventsTableData, TableData} from 'sentry/utils/discover/discoverQuery'; import type {MetaType} from 'sentry/utils/discover/eventView'; import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers'; import { ERROR_FIELDS, ERRORS_AGGREGATION_FUNCTIONS, getAggregations, isEquation, isEquationAlias, } from 'sentry/utils/discover/fields'; import type {DiscoverQueryRequestParams} from 'sentry/utils/discover/genericDiscoverQuery'; import {doDiscoverQuery} from 'sentry/utils/discover/genericDiscoverQuery'; import {DiscoverDatasets, TOP_N} from 'sentry/utils/discover/types'; import type {AggregationKey} from 'sentry/utils/fields'; import type {MEPState} from 'sentry/utils/performance/contexts/metricsEnhancedSetting'; import type {OnDemandControlContext} from 'sentry/utils/performance/contexts/onDemandControl'; import type {FieldValueOption} from 'sentry/views/discover/table/queryField'; import {generateFieldOptions} from 'sentry/views/discover/utils'; import type {Widget, WidgetQuery} from '../types'; import {DisplayType} from '../types'; import {eventViewFromWidget, getNumEquations, getWidgetInterval} from '../utils'; import {EventsSearchBar} from '../widgetBuilder/buildSteps/filterResultsStep/eventsSearchBar'; import {type DatasetConfig, handleOrderByReset} from './base'; import { filterAggregateParams, filterSeriesSortOptions, filterYAxisAggregateParams, // TODO: Does this need to be overridden? getTableSortOptions, getTimeseriesSortOptions, renderEventIdAsLinkable, renderTraceAsLinkable, transformEventsResponseToSeries, transformEventsResponseToTable, } from './errorsAndTransactions'; const DEFAULT_WIDGET_QUERY: WidgetQuery = { name: '', fields: ['count()'], columns: [], fieldAliases: [], aggregates: ['count()'], conditions: '', orderby: '-count()', }; export type SeriesWithOrdering = [order: number, series: Series]; export const ErrorsConfig: DatasetConfig< EventsStats | MultiSeriesEventsStats, TableData | EventsTableData > = { defaultWidgetQuery: DEFAULT_WIDGET_QUERY, enableEquations: true, getCustomFieldRenderer: getCustomEventsFieldRenderer, SearchBar: EventsSearchBar, filterSeriesSortOptions, filterYAxisAggregateParams, filterYAxisOptions, getTableFieldOptions: getEventsTableFieldOptions, getTimeseriesSortOptions: (organization, widgetQuery, tags) => getTimeseriesSortOptions(organization, widgetQuery, tags, getEventsTableFieldOptions), getTableSortOptions, getGroupByFieldOptions: getEventsTableFieldOptions, handleOrderByReset, supportedDisplayTypes: [ DisplayType.AREA, DisplayType.BAR, DisplayType.BIG_NUMBER, DisplayType.LINE, DisplayType.TABLE, DisplayType.TOP_N, ], getTableRequest: ( api: Client, _widget: Widget, query: WidgetQuery, organization: Organization, pageFilters: PageFilters, _onDemandControlContext?: OnDemandControlContext, limit?: number, cursor?: string, referrer?: string, _mepSetting?: MEPState | null ) => { return getEventsRequest( api, query, organization, pageFilters, limit, cursor, referrer ); }, getSeriesRequest: getErrorsSeriesRequest, transformTable: transformEventsResponseToTable, transformSeries: transformEventsResponseToSeries, filterAggregateParams, }; function getEventsTableFieldOptions( organization: Organization, tags?: TagCollection, _customMeasurements?: CustomMeasurementCollection ) { const aggregates = getAggregations(DiscoverDatasets.ERRORS); return generateFieldOptions({ organization, tagKeys: Object.values(tags ?? {}).map(({key}) => key), aggregations: Object.keys(aggregates) .filter(key => ERRORS_AGGREGATION_FUNCTIONS.includes(key as AggregationKey)) .reduce((obj, key) => { obj[key] = aggregates[key]; return obj; }, {}), fieldKeys: ERROR_FIELDS, }); } export function getCustomEventsFieldRenderer(field: string, meta: MetaType) { if (field === 'id') { return renderEventIdAsLinkable; } if (field === 'trace') { return renderTraceAsLinkable; } return getFieldRenderer(field, meta, false); } export function getEventsRequest( api: Client, query: WidgetQuery, organization: Organization, pageFilters: PageFilters, limit?: number, cursor?: string, referrer?: string ) { const url = `/organizations/${organization.slug}/events/`; const eventView = eventViewFromWidget('', query, pageFilters); const params: DiscoverQueryRequestParams = { per_page: limit, cursor, referrer, dataset: DiscoverDatasets.ERRORS, }; if (query.orderby) { params.sort = typeof query.orderby === 'string' ? [query.orderby] : query.orderby; } return doDiscoverQuery( api, url, { ...eventView.generateQueryStringObject(), ...params, }, // Tries events request up to 3 times on rate limit { retry: { statusCodes: [429], tries: 3, }, } ); } // The y-axis options are a strict set of available aggregates export function filterYAxisOptions(_displayType: DisplayType) { return (option: FieldValueOption) => { return ERRORS_AGGREGATION_FUNCTIONS.includes( option.value.meta.name as AggregationKey ); }; } function getErrorsSeriesRequest( api: Client, widget: Widget, queryIndex: number, organization: Organization, pageFilters: PageFilters, _onDemandControlContext?: OnDemandControlContext, referrer?: string, _mepSetting?: MEPState | null ) { const widgetQuery = widget.queries[queryIndex]; const {displayType, limit} = widget; const {environments, projects} = pageFilters; const {start, end, period: statsPeriod} = pageFilters.datetime; const interval = getWidgetInterval( displayType, {start, end, period: statsPeriod}, '1m' ); let requestData; if (displayType === DisplayType.TOP_N) { requestData = { organization, interval, start, end, project: projects, environment: environments, period: statsPeriod, query: widgetQuery.conditions, yAxis: widgetQuery.aggregates[widgetQuery.aggregates.length - 1], includePrevious: false, referrer, partial: true, field: [...widgetQuery.columns, ...widgetQuery.aggregates], includeAllArgs: true, topEvents: TOP_N, dataset: DiscoverDatasets.ERRORS, }; if (widgetQuery.orderby) { requestData.orderby = widgetQuery.orderby; } } else { requestData = { organization, interval, start, end, project: projects, environment: environments, period: statsPeriod, query: widgetQuery.conditions, yAxis: widgetQuery.aggregates, orderby: widgetQuery.orderby, includePrevious: false, referrer, partial: true, includeAllArgs: true, dataset: DiscoverDatasets.ERRORS, }; if (widgetQuery.columns?.length !== 0) { requestData.topEvents = limit ?? TOP_N; requestData.field = [...widgetQuery.columns, ...widgetQuery.aggregates]; // Compare field and orderby as aliases to ensure requestData has // the orderby selected // If the orderby is an equation alias, do not inject it const orderby = trimStart(widgetQuery.orderby, '-'); if ( widgetQuery.orderby && !isEquationAlias(orderby) && !requestData.field.includes(orderby) ) { requestData.field.push(orderby); } // The "Other" series is only included when there is one // y-axis and one widgetQuery requestData.excludeOther = widgetQuery.aggregates.length !== 1 || widget.queries.length !== 1; if (isEquation(trimStart(widgetQuery.orderby, '-'))) { const nextEquationIndex = getNumEquations(widgetQuery.aggregates); const isDescending = widgetQuery.orderby.startsWith('-'); const prefix = isDescending ? '-' : ''; // Construct the alias form of the equation and inject it into the request requestData.orderby = `${prefix}equation[${nextEquationIndex}]`; requestData.field = [ ...widgetQuery.columns, ...widgetQuery.aggregates, trimStart(widgetQuery.orderby, '-'), ]; } } } return doEventsRequest(api, requestData); }