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 {SavedSearchType, type TagCollection} from 'sentry/types/group';
import type {
EventsStats,
MultiSeriesEventsStats,
Organization,
} from 'sentry/types/organization';
import {defined} from 'sentry/utils';
import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements';
import type {EventsTableData, TableData} from 'sentry/utils/discover/discoverQuery';
import {
isEquation,
isEquationAlias,
SPAN_OP_BREAKDOWN_FIELDS,
TRANSACTION_FIELDS,
} from 'sentry/utils/discover/fields';
import type {
DiscoverQueryExtras,
DiscoverQueryRequestParams,
} from 'sentry/utils/discover/genericDiscoverQuery';
import {doDiscoverQuery} from 'sentry/utils/discover/genericDiscoverQuery';
import {DiscoverDatasets, TOP_N} from 'sentry/utils/discover/types';
import {getMeasurements} from 'sentry/utils/measurements/measurements';
import {MEPState} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
import {
type OnDemandControlContext,
shouldUseOnDemandMetrics,
} from 'sentry/utils/performance/contexts/onDemandControl';
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 {
doOnDemandMetricsRequest,
filterAggregateParams,
filterSeriesSortOptions,
filterYAxisAggregateParams,
filterYAxisOptions,
getCustomEventsFieldRenderer,
getTableSortOptions,
getTimeseriesSortOptions,
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];
// TODO: Commented out functions will be given implementations
// to be able to make events-stats requests
export const TransactionsConfig: DatasetConfig<
EventsStats | MultiSeriesEventsStats,
TableData | EventsTableData
> = {
defaultWidgetQuery: DEFAULT_WIDGET_QUERY,
enableEquations: true,
getCustomFieldRenderer: getCustomEventsFieldRenderer,
SearchBar: props => (
),
filterSeriesSortOptions,
filterYAxisAggregateParams,
filterYAxisOptions,
getTableFieldOptions: getEventsTableFieldOptions,
getTimeseriesSortOptions,
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
) => {
const useOnDemandMetrics = shouldUseOnDemandMetrics(
organization,
widget,
onDemandControlContext
);
const queryExtras = {
useOnDemandMetrics,
onDemandType: 'dynamic_query',
};
return getEventsRequest(
api,
query,
organization,
pageFilters,
limit,
cursor,
referrer,
mepSetting,
queryExtras
);
},
getSeriesRequest: getEventsSeriesRequest,
transformSeries: transformEventsResponseToSeries,
transformTable: transformEventsResponseToTable,
filterAggregateParams,
};
function getEventsTableFieldOptions(
organization: Organization,
tags?: TagCollection,
customMeasurements?: CustomMeasurementCollection
) {
const measurements = getMeasurements();
return generateFieldOptions({
organization,
tagKeys: Object.values(tags ?? {}).map(({key}) => key),
measurementKeys: Object.values(measurements).map(({key}) => key),
spanOperationBreakdownKeys: SPAN_OP_BREAKDOWN_FIELDS,
customMeasurements: Object.values(customMeasurements ?? {}).map(
({key, functions}) => ({
key,
functions,
})
),
fieldKeys: TRANSACTION_FIELDS,
});
}
function getEventsRequest(
api: Client,
query: WidgetQuery,
organization: Organization,
pageFilters: PageFilters,
limit?: number,
cursor?: string,
referrer?: string,
mepSetting?: MEPState | null,
queryExtras?: DiscoverQueryExtras
) {
const isMEPEnabled = defined(mepSetting) && mepSetting !== MEPState.TRANSACTIONS_ONLY;
const url = `/organizations/${organization.slug}/events/`;
const eventView = eventViewFromWidget('', query, pageFilters);
const params: DiscoverQueryRequestParams = {
per_page: limit,
cursor,
referrer,
dataset: isMEPEnabled
? DiscoverDatasets.METRICS_ENHANCED
: DiscoverDatasets.TRANSACTIONS,
...queryExtras,
};
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,
},
}
);
}
function getEventsSeriesRequest(
api: Client,
widget: Widget,
queryIndex: number,
organization: Organization,
pageFilters: PageFilters,
onDemandControlContext?: OnDemandControlContext,
referrer?: string,
mepSetting?: MEPState | null
) {
const isMEPEnabled = defined(mepSetting) && mepSetting !== MEPState.TRANSACTIONS_ONLY;
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: isMEPEnabled
? DiscoverDatasets.METRICS_ENHANCED
: DiscoverDatasets.TRANSACTIONS,
};
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: isMEPEnabled
? DiscoverDatasets.METRICS_ENHANCED
: DiscoverDatasets.TRANSACTIONS,
};
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, '-'),
];
}
}
}
if (shouldUseOnDemandMetrics(organization, widget, onDemandControlContext)) {
requestData.queryExtras = {
...requestData.queryExtras,
...{dataset: DiscoverDatasets.METRICS_ENHANCED},
};
return doOnDemandMetricsRequest(api, requestData, widget.widgetType);
}
return doEventsRequest(api, requestData);
}