123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660 |
- import * as Sentry from '@sentry/react';
- import trimStart from 'lodash/trimStart';
- import {doEventsRequest} from 'sentry/actionCreators/events';
- import type {Client, ResponseMeta} from 'sentry/api';
- import {isMultiSeriesStats} from 'sentry/components/charts/utils';
- import Link from 'sentry/components/links/link';
- import {Tooltip} from 'sentry/components/tooltip';
- import {t} from 'sentry/locale';
- import type {
- EventsStats,
- MultiSeriesEventsStats,
- Organization,
- PageFilters,
- SelectValue,
- TagCollection,
- } from 'sentry/types';
- import type {Series} from 'sentry/types/echarts';
- import {defined} from 'sentry/utils';
- 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 type {RenderFunctionBaggage} from 'sentry/utils/discover/fieldRenderers';
- import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
- import type {AggregationOutputType, QueryFieldValue} from 'sentry/utils/discover/fields';
- import {
- errorsAndTransactionsAggregateFunctionOutputType,
- getAggregateAlias,
- isEquation,
- isEquationAlias,
- isLegalYAxisType,
- SPAN_OP_BREAKDOWN_FIELDS,
- stripEquationPrefix,
- } from 'sentry/utils/discover/fields';
- import type {
- DiscoverQueryExtras,
- DiscoverQueryRequestParams,
- } from 'sentry/utils/discover/genericDiscoverQuery';
- import {doDiscoverQuery} from 'sentry/utils/discover/genericDiscoverQuery';
- import {Container} from 'sentry/utils/discover/styles';
- import {TOP_N} from 'sentry/utils/discover/types';
- import {
- eventDetailsRouteWithEventView,
- generateEventSlug,
- } from 'sentry/utils/discover/urls';
- import {getShortEventId} from 'sentry/utils/events';
- import {FieldKey} from 'sentry/utils/fields';
- import {getMeasurements} from 'sentry/utils/measurements/measurements';
- import {MEPState} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
- import type {OnDemandControlContext} from 'sentry/utils/performance/contexts/onDemandControl';
- import {shouldUseOnDemandMetrics} from 'sentry/utils/performance/contexts/onDemandControl';
- import type {FieldValueOption} from 'sentry/views/discover/table/queryField';
- import type {FieldValue} from 'sentry/views/discover/table/types';
- import {FieldValueKind} from 'sentry/views/discover/table/types';
- import {generateFieldOptions} from 'sentry/views/discover/utils';
- import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';
- import {
- createUnnamedTransactionsDiscoverTarget,
- DiscoverQueryPageSource,
- UNPARAMETERIZED_TRANSACTION,
- } from 'sentry/views/performance/utils';
- import type {Widget, WidgetQuery} from '../types';
- import {DisplayType} from '../types';
- import {
- eventViewFromWidget,
- getDashboardsMEPQueryParams,
- getNumEquations,
- getWidgetInterval,
- } from '../utils';
- import {EventsSearchBar} from '../widgetBuilder/buildSteps/filterResultsStep/eventsSearchBar';
- import {CUSTOM_EQUATION_VALUE} from '../widgetBuilder/buildSteps/sortByStep';
- import {
- flattenMultiSeriesDataWithGrouping,
- transformSeries,
- } from '../widgetCard/widgetQueries';
- import type {DatasetConfig} from './base';
- import {handleOrderByReset} from './base';
- const DEFAULT_WIDGET_QUERY: WidgetQuery = {
- name: '',
- fields: ['count()'],
- columns: [],
- fieldAliases: [],
- aggregates: ['count()'],
- conditions: '',
- orderby: '-count()',
- };
- export type SeriesWithOrdering = [order: number, series: Series];
- export const ErrorsAndTransactionsConfig: DatasetConfig<
- EventsStats | MultiSeriesEventsStats,
- TableData | EventsTableData
- > = {
- defaultWidgetQuery: DEFAULT_WIDGET_QUERY,
- enableEquations: true,
- getCustomFieldRenderer: getCustomEventsFieldRenderer,
- SearchBar: EventsSearchBar,
- 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 url = `/organizations/${organization.slug}/events/`;
- const queryExtras = {
- useOnDemandMetrics: shouldUseOnDemandMetrics(
- organization,
- widget,
- onDemandControlContext
- ),
- onDemandType: 'dynamic_query',
- };
- return getEventsRequest(
- url,
- api,
- query,
- organization,
- pageFilters,
- limit,
- cursor,
- referrer,
- mepSetting,
- queryExtras
- );
- },
- getSeriesRequest: getEventsSeriesRequest,
- transformSeries: transformEventsResponseToSeries,
- transformTable: transformEventsResponseToTable,
- filterAggregateParams,
- getSeriesResultType,
- };
- function getTableSortOptions(_organization: Organization, widgetQuery: WidgetQuery) {
- const {columns, aggregates} = widgetQuery;
- const options: SelectValue<string>[] = [];
- let equations = 0;
- [...aggregates, ...columns]
- .filter(field => !!field)
- .forEach(field => {
- let alias;
- const label = stripEquationPrefix(field);
- // Equations are referenced via a standard alias following this pattern
- if (isEquation(field)) {
- alias = `equation[${equations}]`;
- equations += 1;
- }
- options.push({label, value: alias ?? field});
- });
- return options;
- }
- function filterSeriesSortOptions(columns: Set<string>) {
- return (option: FieldValueOption) => {
- if (
- option.value.kind === FieldValueKind.FUNCTION ||
- option.value.kind === FieldValueKind.EQUATION
- ) {
- return true;
- }
- return (
- columns.has(option.value.meta.name) ||
- option.value.meta.name === CUSTOM_EQUATION_VALUE
- );
- };
- }
- function getTimeseriesSortOptions(
- organization: Organization,
- widgetQuery: WidgetQuery,
- tags?: TagCollection
- ) {
- const options: Record<string, SelectValue<FieldValue>> = {};
- options[`field:${CUSTOM_EQUATION_VALUE}`] = {
- label: 'Custom Equation',
- value: {
- kind: FieldValueKind.EQUATION,
- meta: {name: CUSTOM_EQUATION_VALUE},
- },
- };
- let equations = 0;
- [...widgetQuery.aggregates, ...widgetQuery.columns]
- .filter(field => !!field)
- .forEach(field => {
- let alias;
- const label = stripEquationPrefix(field);
- // Equations are referenced via a standard alias following this pattern
- if (isEquation(field)) {
- alias = `equation[${equations}]`;
- equations += 1;
- options[`equation:${alias}`] = {
- label,
- value: {
- kind: FieldValueKind.EQUATION,
- meta: {
- name: alias ?? field,
- },
- },
- };
- }
- });
- const fieldOptions = getEventsTableFieldOptions(organization, tags);
- return {...options, ...fieldOptions};
- }
- 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,
- })
- ),
- });
- }
- function transformEventsResponseToTable(
- data: TableData | EventsTableData,
- _widgetQuery: WidgetQuery
- ): TableData {
- let tableData = data;
- // events api uses a different response format so we need to construct tableData differently
- const {fields, ...otherMeta} = (data as EventsTableData).meta ?? {};
- tableData = {
- ...data,
- meta: {...fields, ...otherMeta},
- } as TableData;
- return tableData as TableData;
- }
- function filterYAxisAggregateParams(
- fieldValue: QueryFieldValue,
- displayType: DisplayType
- ) {
- return (option: FieldValueOption) => {
- // Only validate function parameters for timeseries widgets and
- // world map widgets.
- if (displayType === DisplayType.BIG_NUMBER) {
- return true;
- }
- if (fieldValue.kind !== FieldValueKind.FUNCTION) {
- return true;
- }
- const functionName = fieldValue.function[0];
- const primaryOutput = errorsAndTransactionsAggregateFunctionOutputType(
- functionName as string,
- option.value.meta.name
- );
- if (primaryOutput) {
- return isLegalYAxisType(primaryOutput);
- }
- if (
- option.value.kind === FieldValueKind.FUNCTION ||
- option.value.kind === FieldValueKind.EQUATION
- ) {
- // Functions and equations are not legal options as an aggregate/function parameter.
- return false;
- }
- return isLegalYAxisType(option.value.meta.dataType);
- };
- }
- function filterYAxisOptions(displayType: DisplayType) {
- return (option: FieldValueOption) => {
- // Only validate function names for timeseries widgets and
- // world map widgets.
- if (
- !(displayType === DisplayType.BIG_NUMBER) &&
- option.value.kind === FieldValueKind.FUNCTION
- ) {
- const primaryOutput = errorsAndTransactionsAggregateFunctionOutputType(
- option.value.meta.name,
- undefined
- );
- if (primaryOutput) {
- // If a function returns a specific type, then validate it.
- return isLegalYAxisType(primaryOutput);
- }
- }
- return option.value.kind === FieldValueKind.FUNCTION;
- };
- }
- function transformEventsResponseToSeries(
- data: EventsStats | MultiSeriesEventsStats,
- widgetQuery: WidgetQuery
- ): Series[] {
- let output: Series[] = [];
- const queryAlias = widgetQuery.name;
- if (isMultiSeriesStats(data)) {
- let seriesWithOrdering: SeriesWithOrdering[] = [];
- const isMultiSeriesDataWithGrouping =
- widgetQuery.aggregates.length > 1 && widgetQuery.columns.length;
- // Convert multi-series results into chartable series. Multi series results
- // are created when multiple yAxis are used. Convert the timeseries
- // data into a multi-series data set. As the server will have
- // replied with a map like: {[titleString: string]: EventsStats}
- if (isMultiSeriesDataWithGrouping) {
- seriesWithOrdering = flattenMultiSeriesDataWithGrouping(data, queryAlias);
- } else {
- seriesWithOrdering = Object.keys(data).map((seriesName: string) => {
- const prefixedName = queryAlias ? `${queryAlias} : ${seriesName}` : seriesName;
- const seriesData: EventsStats = data[seriesName];
- return [
- seriesData.order || 0,
- transformSeries(seriesData, prefixedName, seriesName),
- ];
- });
- }
- output = [
- ...seriesWithOrdering
- .sort((itemA, itemB) => itemA[0] - itemB[0])
- .map(item => item[1]),
- ];
- } else {
- const field = widgetQuery.aggregates[0];
- const prefixedName = queryAlias ? `${queryAlias} : ${field}` : field;
- const transformed = transformSeries(data, prefixedName, field);
- output.push(transformed);
- }
- return output;
- }
- // Get the series result type from the EventsStats meta
- function getSeriesResultType(
- data: EventsStats | MultiSeriesEventsStats,
- widgetQuery: WidgetQuery
- ): Record<string, AggregationOutputType> {
- const field = widgetQuery.aggregates[0];
- const resultTypes = {};
- // Need to use getAggregateAlias since events-stats still uses aggregate alias format
- if (isMultiSeriesStats(data)) {
- Object.keys(data).forEach(
- key => (resultTypes[key] = data[key].meta?.fields[getAggregateAlias(key)])
- );
- } else {
- resultTypes[field] = data.meta?.fields[getAggregateAlias(field)];
- }
- return resultTypes;
- }
- function renderEventIdAsLinkable(data, {eventView, organization}: RenderFunctionBaggage) {
- const id: string | unknown = data?.id;
- if (!eventView || typeof id !== 'string') {
- return null;
- }
- const eventSlug = generateEventSlug(data);
- const target = eventDetailsRouteWithEventView({
- orgSlug: organization.slug,
- eventSlug,
- eventView,
- });
- return (
- <Tooltip title={t('View Event')}>
- <Link data-test-id="view-event" to={target}>
- <Container>{getShortEventId(id)}</Container>
- </Link>
- </Tooltip>
- );
- }
- function renderTraceAsLinkable(
- data,
- {eventView, organization, location}: RenderFunctionBaggage
- ) {
- const id: string | unknown = data?.trace;
- if (!eventView || typeof id !== 'string') {
- return null;
- }
- const dateSelection = eventView.normalizeDateSelection(location);
- const target = getTraceDetailsUrl(organization, String(data.trace), dateSelection, {});
- return (
- <Tooltip title={t('View Trace')}>
- <Link data-test-id="view-trace" to={target}>
- <Container>{getShortEventId(id)}</Container>
- </Link>
- </Tooltip>
- );
- }
- export function getCustomEventsFieldRenderer(field: string, meta: MetaType) {
- if (field === 'id') {
- return renderEventIdAsLinkable;
- }
- if (field === 'trace') {
- return renderTraceAsLinkable;
- }
- // When title or transaction are << unparameterized >>, link out to discover showing unparameterized transactions
- if (['title', 'transaction'].includes(field)) {
- return function (data, baggage) {
- if (data[field] === UNPARAMETERIZED_TRANSACTION) {
- return (
- <Container>
- <Link
- to={createUnnamedTransactionsDiscoverTarget({
- location: baggage.location,
- organization: baggage.organization,
- source: DiscoverQueryPageSource.DISCOVER,
- })}
- >
- {data[field]}
- </Link>
- </Container>
- );
- }
- return getFieldRenderer(field, meta, false)(data, baggage);
- };
- }
- return getFieldRenderer(field, meta, false);
- }
- function getEventsRequest(
- url: string,
- 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 eventView = eventViewFromWidget('', query, pageFilters);
- const params: DiscoverQueryRequestParams = {
- per_page: limit,
- cursor,
- referrer,
- ...getDashboardsMEPQueryParams(isMEPEnabled),
- ...queryExtras,
- };
- if (query.orderby) {
- params.sort = typeof query.orderby === 'string' ? [query.orderby] : query.orderby;
- }
- // TODO: eventually need to replace this with just EventsTableData as we deprecate eventsv2
- return doDiscoverQuery<TableData | EventsTableData>(
- 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 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});
- const isMEPEnabled = defined(mepSetting) && mepSetting !== MEPState.TRANSACTIONS_ONLY;
- 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],
- queryExtras: getDashboardsMEPQueryParams(isMEPEnabled),
- includeAllArgs: true,
- topEvents: TOP_N,
- };
- 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,
- queryExtras: getDashboardsMEPQueryParams(isMEPEnabled),
- includeAllArgs: true,
- };
- 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)) {
- return doOnDemandMetricsRequest(api, requestData);
- }
- return doEventsRequest<true>(api, requestData);
- }
- async function doOnDemandMetricsRequest(
- api,
- requestData
- ): Promise<
- [EventsStats | MultiSeriesEventsStats, string | undefined, ResponseMeta | undefined]
- > {
- try {
- const isEditing = location.pathname.endsWith('/edit/');
- const fetchEstimatedStats = () =>
- `/organizations/${requestData.organization.slug}/metrics-estimation-stats/`;
- const response = await doEventsRequest<false>(api, {
- ...requestData,
- queryExtras: {
- ...requestData.queryExtras,
- useOnDemandMetrics: true,
- onDemandType: 'dynamic_query',
- },
- dataset: 'metricsEnhanced',
- generatePathname: isEditing ? fetchEstimatedStats : undefined,
- });
- response[0] = {...response[0], isMetricsData: true, isExtrapolatedData: isEditing};
- return [response[0], response[1], response[2]];
- } catch (err) {
- Sentry.captureMessage('Failed to fetch metrics estimation stats', {extra: err});
- return doEventsRequest<true>(api, requestData);
- }
- }
- // Checks fieldValue to see what function is being used and only allow supported custom measurements
- function filterAggregateParams(option: FieldValueOption, fieldValue?: QueryFieldValue) {
- if (
- (option.value.kind === FieldValueKind.CUSTOM_MEASUREMENT &&
- fieldValue?.kind === 'function' &&
- fieldValue?.function &&
- !option.value.meta.functions.includes(fieldValue.function[0])) ||
- option.value.meta.name === FieldKey.TOTAL_COUNT
- ) {
- return false;
- }
- return true;
- }
|