mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
661 lines
20 KiB
TypeScript
661 lines
20 KiB
TypeScript
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]};
|
|
|
|
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;
|
|
}
|