errorsAndTransactions.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. import * as Sentry from '@sentry/react';
  2. import trimStart from 'lodash/trimStart';
  3. import {doEventsRequest} from 'sentry/actionCreators/events';
  4. import type {Client, ResponseMeta} from 'sentry/api';
  5. import {isMultiSeriesStats} from 'sentry/components/charts/utils';
  6. import Link from 'sentry/components/links/link';
  7. import {Tooltip} from 'sentry/components/tooltip';
  8. import {t} from 'sentry/locale';
  9. import type {PageFilters, SelectValue} from 'sentry/types/core';
  10. import type {Series} from 'sentry/types/echarts';
  11. import type {TagCollection} from 'sentry/types/group';
  12. import type {
  13. EventsStats,
  14. GroupedMultiSeriesEventsStats,
  15. MultiSeriesEventsStats,
  16. Organization,
  17. } from 'sentry/types/organization';
  18. import {defined} from 'sentry/utils';
  19. import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements';
  20. import {getTimeStampFromTableDateField} from 'sentry/utils/dates';
  21. import type {EventsTableData, TableData} from 'sentry/utils/discover/discoverQuery';
  22. import type {MetaType} from 'sentry/utils/discover/eventView';
  23. import type {RenderFunctionBaggage} from 'sentry/utils/discover/fieldRenderers';
  24. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  25. import type {AggregationOutputType, QueryFieldValue} from 'sentry/utils/discover/fields';
  26. import {
  27. errorsAndTransactionsAggregateFunctionOutputType,
  28. getAggregateAlias,
  29. isEquation,
  30. isEquationAlias,
  31. isLegalYAxisType,
  32. SPAN_OP_BREAKDOWN_FIELDS,
  33. stripEquationPrefix,
  34. } from 'sentry/utils/discover/fields';
  35. import type {
  36. DiscoverQueryExtras,
  37. DiscoverQueryRequestParams,
  38. } from 'sentry/utils/discover/genericDiscoverQuery';
  39. import {doDiscoverQuery} from 'sentry/utils/discover/genericDiscoverQuery';
  40. import {Container} from 'sentry/utils/discover/styles';
  41. import {TOP_N} from 'sentry/utils/discover/types';
  42. import {
  43. eventDetailsRouteWithEventView,
  44. generateEventSlug,
  45. } from 'sentry/utils/discover/urls';
  46. import {getShortEventId} from 'sentry/utils/events';
  47. import {FieldKey} from 'sentry/utils/fields';
  48. import {getMeasurements} from 'sentry/utils/measurements/measurements';
  49. import {MEPState} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  50. import type {OnDemandControlContext} from 'sentry/utils/performance/contexts/onDemandControl';
  51. import {shouldUseOnDemandMetrics} from 'sentry/utils/performance/contexts/onDemandControl';
  52. import type {FieldValueOption} from 'sentry/views/discover/table/queryField';
  53. import type {FieldValue} from 'sentry/views/discover/table/types';
  54. import {FieldValueKind} from 'sentry/views/discover/table/types';
  55. import {generateFieldOptions} from 'sentry/views/discover/utils';
  56. import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs';
  57. import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';
  58. import {
  59. createUnnamedTransactionsDiscoverTarget,
  60. DiscoverQueryPageSource,
  61. UNPARAMETERIZED_TRANSACTION,
  62. } from 'sentry/views/performance/utils';
  63. import type {Widget, WidgetQuery} from '../types';
  64. import {DisplayType, WidgetType} from '../types';
  65. import {
  66. eventViewFromWidget,
  67. getDashboardsMEPQueryParams,
  68. getNumEquations,
  69. getWidgetInterval,
  70. hasDatasetSelector,
  71. } from '../utils';
  72. import {transformEventsResponseToSeries} from '../utils/transformEventsResponseToSeries';
  73. import {EventsSearchBar} from '../widgetBuilder/buildSteps/filterResultsStep/eventsSearchBar';
  74. import {CUSTOM_EQUATION_VALUE} from '../widgetBuilder/buildSteps/sortByStep';
  75. import type {DatasetConfig} from './base';
  76. import {handleOrderByReset} from './base';
  77. const DEFAULT_WIDGET_QUERY: WidgetQuery = {
  78. name: '',
  79. fields: ['count()'],
  80. columns: [],
  81. fieldAliases: [],
  82. aggregates: ['count()'],
  83. conditions: '',
  84. orderby: '-count()',
  85. };
  86. const DEFAULT_FIELD: QueryFieldValue = {
  87. function: ['count', '', undefined, undefined],
  88. kind: FieldValueKind.FUNCTION,
  89. };
  90. export type SeriesWithOrdering = [order: number, series: Series];
  91. export const ErrorsAndTransactionsConfig: DatasetConfig<
  92. EventsStats | MultiSeriesEventsStats | GroupedMultiSeriesEventsStats,
  93. TableData | EventsTableData
  94. > = {
  95. defaultField: DEFAULT_FIELD,
  96. defaultWidgetQuery: DEFAULT_WIDGET_QUERY,
  97. enableEquations: true,
  98. getCustomFieldRenderer: getCustomEventsFieldRenderer,
  99. SearchBar: EventsSearchBar,
  100. filterSeriesSortOptions,
  101. filterYAxisAggregateParams,
  102. filterYAxisOptions,
  103. getTableFieldOptions: getEventsTableFieldOptions,
  104. getTimeseriesSortOptions: (organization, widgetQuery, tags) =>
  105. getTimeseriesSortOptions(organization, widgetQuery, tags, getEventsTableFieldOptions),
  106. getTableSortOptions,
  107. getGroupByFieldOptions: getEventsTableFieldOptions,
  108. handleOrderByReset,
  109. supportedDisplayTypes: [
  110. DisplayType.AREA,
  111. DisplayType.BAR,
  112. DisplayType.BIG_NUMBER,
  113. DisplayType.LINE,
  114. DisplayType.TABLE,
  115. DisplayType.TOP_N,
  116. ],
  117. getTableRequest: (
  118. api: Client,
  119. widget: Widget,
  120. query: WidgetQuery,
  121. organization: Organization,
  122. pageFilters: PageFilters,
  123. onDemandControlContext?: OnDemandControlContext,
  124. limit?: number,
  125. cursor?: string,
  126. referrer?: string,
  127. mepSetting?: MEPState | null
  128. ) => {
  129. const url = `/organizations/${organization.slug}/events/`;
  130. const useOnDemandMetrics = shouldUseOnDemandMetrics(
  131. organization,
  132. widget,
  133. onDemandControlContext
  134. );
  135. const queryExtras = {
  136. useOnDemandMetrics,
  137. ...getQueryExtraForSplittingDiscover(widget, organization, !!useOnDemandMetrics),
  138. onDemandType: 'dynamic_query',
  139. };
  140. return getEventsRequest(
  141. url,
  142. api,
  143. query,
  144. organization,
  145. pageFilters,
  146. limit,
  147. cursor,
  148. referrer,
  149. mepSetting,
  150. queryExtras
  151. );
  152. },
  153. getSeriesRequest: getEventsSeriesRequest,
  154. transformSeries: transformEventsResponseToSeries,
  155. transformTable: transformEventsResponseToTable,
  156. filterAggregateParams,
  157. getSeriesResultType,
  158. };
  159. export function getTableSortOptions(
  160. _organization: Organization,
  161. widgetQuery: WidgetQuery
  162. ) {
  163. const {columns, aggregates} = widgetQuery;
  164. const options: Array<SelectValue<string>> = [];
  165. let equations = 0;
  166. [...aggregates, ...columns]
  167. .filter(field => !!field)
  168. .forEach(field => {
  169. let alias: any;
  170. const label = stripEquationPrefix(field);
  171. // Equations are referenced via a standard alias following this pattern
  172. if (isEquation(field)) {
  173. alias = `equation[${equations}]`;
  174. equations += 1;
  175. }
  176. options.push({label, value: alias ?? field});
  177. });
  178. return options;
  179. }
  180. export function filterSeriesSortOptions(columns: Set<string>) {
  181. return (option: FieldValueOption) => {
  182. if (
  183. option.value.kind === FieldValueKind.FUNCTION ||
  184. option.value.kind === FieldValueKind.EQUATION
  185. ) {
  186. return true;
  187. }
  188. return (
  189. columns.has(option.value.meta.name) ||
  190. option.value.meta.name === CUSTOM_EQUATION_VALUE
  191. );
  192. };
  193. }
  194. export function getTimeseriesSortOptions(
  195. organization: Organization,
  196. widgetQuery: WidgetQuery,
  197. tags?: TagCollection,
  198. getFieldOptions: typeof getEventsTableFieldOptions = getEventsTableFieldOptions
  199. ) {
  200. const options: Record<string, SelectValue<FieldValue>> = {};
  201. options[`field:${CUSTOM_EQUATION_VALUE}`] = {
  202. label: 'Custom Equation',
  203. value: {
  204. kind: FieldValueKind.EQUATION,
  205. meta: {name: CUSTOM_EQUATION_VALUE},
  206. },
  207. };
  208. let equations = 0;
  209. [...widgetQuery.aggregates, ...widgetQuery.columns]
  210. .filter(field => !!field)
  211. .forEach(field => {
  212. let alias: any;
  213. const label = stripEquationPrefix(field);
  214. // Equations are referenced via a standard alias following this pattern
  215. if (isEquation(field)) {
  216. alias = `equation[${equations}]`;
  217. equations += 1;
  218. options[`equation:${alias}`] = {
  219. label,
  220. value: {
  221. kind: FieldValueKind.EQUATION,
  222. meta: {
  223. name: alias ?? field,
  224. },
  225. },
  226. };
  227. }
  228. });
  229. const fieldOptions = getFieldOptions(organization, tags);
  230. return {...options, ...fieldOptions};
  231. }
  232. function getEventsTableFieldOptions(
  233. organization: Organization,
  234. tags?: TagCollection,
  235. customMeasurements?: CustomMeasurementCollection
  236. ) {
  237. const measurements = getMeasurements();
  238. return generateFieldOptions({
  239. organization,
  240. tagKeys: Object.values(tags ?? {}).map(({key}) => key),
  241. measurementKeys: Object.values(measurements).map(({key}) => key),
  242. spanOperationBreakdownKeys: SPAN_OP_BREAKDOWN_FIELDS,
  243. customMeasurements: Object.values(customMeasurements ?? {}).map(
  244. ({key, functions}) => ({
  245. key,
  246. functions,
  247. })
  248. ),
  249. });
  250. }
  251. export function transformEventsResponseToTable(
  252. data: TableData | EventsTableData,
  253. _widgetQuery: WidgetQuery
  254. ): TableData {
  255. let tableData = data;
  256. // events api uses a different response format so we need to construct tableData differently
  257. const {fields, ...otherMeta} = (data as EventsTableData).meta ?? {};
  258. tableData = {
  259. ...data,
  260. meta: {...fields, ...otherMeta, fields},
  261. } as TableData;
  262. return tableData;
  263. }
  264. export function filterYAxisAggregateParams(
  265. fieldValue: QueryFieldValue,
  266. displayType: DisplayType
  267. ) {
  268. return (option: FieldValueOption) => {
  269. // Only validate function parameters for timeseries widgets and
  270. // world map widgets.
  271. if (displayType === DisplayType.BIG_NUMBER) {
  272. return true;
  273. }
  274. if (fieldValue.kind !== FieldValueKind.FUNCTION) {
  275. return true;
  276. }
  277. const functionName = fieldValue.function[0];
  278. const primaryOutput = errorsAndTransactionsAggregateFunctionOutputType(
  279. functionName as string,
  280. option.value.meta.name
  281. );
  282. if (primaryOutput) {
  283. return isLegalYAxisType(primaryOutput);
  284. }
  285. if (
  286. option.value.kind === FieldValueKind.FUNCTION ||
  287. option.value.kind === FieldValueKind.EQUATION
  288. ) {
  289. // Functions and equations are not legal options as an aggregate/function parameter.
  290. return false;
  291. }
  292. return isLegalYAxisType(option.value.meta.dataType);
  293. };
  294. }
  295. export function filterYAxisOptions(displayType: DisplayType) {
  296. return (option: FieldValueOption) => {
  297. // Only validate function names for timeseries widgets and
  298. // world map widgets.
  299. if (
  300. !(displayType === DisplayType.BIG_NUMBER) &&
  301. option.value.kind === FieldValueKind.FUNCTION
  302. ) {
  303. const primaryOutput = errorsAndTransactionsAggregateFunctionOutputType(
  304. option.value.meta.name,
  305. undefined
  306. );
  307. if (primaryOutput) {
  308. // If a function returns a specific type, then validate it.
  309. return isLegalYAxisType(primaryOutput);
  310. }
  311. }
  312. return option.value.kind === FieldValueKind.FUNCTION;
  313. };
  314. }
  315. // Get the series result type from the EventsStats meta
  316. function getSeriesResultType(
  317. data: EventsStats | MultiSeriesEventsStats | GroupedMultiSeriesEventsStats,
  318. widgetQuery: WidgetQuery
  319. ): Record<string, AggregationOutputType> {
  320. const field = widgetQuery.aggregates[0]!;
  321. const resultTypes = {};
  322. // Need to use getAggregateAlias since events-stats still uses aggregate alias format
  323. if (isMultiSeriesStats(data)) {
  324. Object.keys(data).forEach(
  325. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  326. key => (resultTypes[key] = data[key]!.meta?.fields[getAggregateAlias(key)])
  327. );
  328. } else {
  329. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  330. resultTypes[field] = data.meta?.fields[getAggregateAlias(field)];
  331. }
  332. return resultTypes;
  333. }
  334. export function renderEventIdAsLinkable(
  335. data: any,
  336. {eventView, organization}: RenderFunctionBaggage
  337. ) {
  338. const id: string | unknown = data?.id;
  339. if (!eventView || typeof id !== 'string') {
  340. return null;
  341. }
  342. const eventSlug = generateEventSlug(data);
  343. const target = eventDetailsRouteWithEventView({
  344. orgSlug: organization.slug,
  345. eventSlug,
  346. eventView,
  347. });
  348. return (
  349. <Tooltip title={t('View Event')}>
  350. <Link data-test-id="view-event" to={target}>
  351. <Container>{getShortEventId(id)}</Container>
  352. </Link>
  353. </Tooltip>
  354. );
  355. }
  356. export function renderTraceAsLinkable(widget?: Widget) {
  357. return function (
  358. data: any,
  359. {eventView, organization, location}: RenderFunctionBaggage
  360. ) {
  361. const id: string | unknown = data?.trace;
  362. if (!eventView || typeof id !== 'string') {
  363. return null;
  364. }
  365. const dateSelection = eventView.normalizeDateSelection(location);
  366. const target = getTraceDetailsUrl({
  367. organization,
  368. traceSlug: String(data.trace),
  369. dateSelection,
  370. timestamp: getTimeStampFromTableDateField(data['max(timestamp)'] ?? data.timestamp),
  371. location: widget
  372. ? {
  373. ...location,
  374. query: {
  375. ...location.query,
  376. widgetId: widget.id,
  377. dashboardId: widget.dashboardId,
  378. },
  379. }
  380. : location,
  381. source: TraceViewSources.DASHBOARDS,
  382. });
  383. return (
  384. <Tooltip title={t('View Trace')}>
  385. <Link data-test-id="view-trace" to={target}>
  386. <Container>{getShortEventId(id)}</Container>
  387. </Link>
  388. </Tooltip>
  389. );
  390. };
  391. }
  392. export function getCustomEventsFieldRenderer(
  393. field: string,
  394. meta: MetaType,
  395. widget?: Widget
  396. ) {
  397. if (field === 'id') {
  398. return renderEventIdAsLinkable;
  399. }
  400. if (field === 'trace') {
  401. return renderTraceAsLinkable(widget);
  402. }
  403. // When title or transaction are << unparameterized >>, link out to discover showing unparameterized transactions
  404. if (['title', 'transaction'].includes(field)) {
  405. return function (data: any, baggage: any) {
  406. if (data[field] === UNPARAMETERIZED_TRANSACTION) {
  407. return (
  408. <Container>
  409. <Link
  410. to={createUnnamedTransactionsDiscoverTarget({
  411. location: baggage.location,
  412. organization: baggage.organization,
  413. source: DiscoverQueryPageSource.DISCOVER,
  414. })}
  415. >
  416. {data[field]}
  417. </Link>
  418. </Container>
  419. );
  420. }
  421. return getFieldRenderer(field, meta, false)(data, baggage);
  422. };
  423. }
  424. return getFieldRenderer(field, meta, false);
  425. }
  426. export function getEventsRequest(
  427. url: string,
  428. api: Client,
  429. query: WidgetQuery,
  430. _organization: Organization,
  431. pageFilters: PageFilters,
  432. limit?: number,
  433. cursor?: string,
  434. referrer?: string,
  435. mepSetting?: MEPState | null,
  436. queryExtras?: DiscoverQueryExtras
  437. ) {
  438. const isMEPEnabled = defined(mepSetting) && mepSetting !== MEPState.TRANSACTIONS_ONLY;
  439. const eventView = eventViewFromWidget('', query, pageFilters);
  440. const params: DiscoverQueryRequestParams = {
  441. per_page: limit,
  442. cursor,
  443. referrer,
  444. ...getDashboardsMEPQueryParams(isMEPEnabled),
  445. ...queryExtras,
  446. };
  447. if (query.orderby) {
  448. params.sort = typeof query.orderby === 'string' ? [query.orderby] : query.orderby;
  449. }
  450. // TODO: eventually need to replace this with just EventsTableData as we deprecate eventsv2
  451. return doDiscoverQuery<TableData | EventsTableData>(
  452. api,
  453. url,
  454. {
  455. ...eventView.generateQueryStringObject(),
  456. ...params,
  457. },
  458. // Tries events request up to 3 times on rate limit
  459. {
  460. retry: {
  461. statusCodes: [429],
  462. tries: 3,
  463. },
  464. }
  465. );
  466. }
  467. function getEventsSeriesRequest(
  468. api: Client,
  469. widget: Widget,
  470. queryIndex: number,
  471. organization: Organization,
  472. pageFilters: PageFilters,
  473. onDemandControlContext?: OnDemandControlContext,
  474. referrer?: string,
  475. mepSetting?: MEPState | null
  476. ) {
  477. const widgetQuery = widget.queries[queryIndex]!;
  478. const {displayType, limit} = widget;
  479. const {environments, projects} = pageFilters;
  480. const {start, end, period: statsPeriod} = pageFilters.datetime;
  481. const interval = getWidgetInterval(
  482. displayType,
  483. {start, end, period: statsPeriod},
  484. '1m'
  485. );
  486. const isMEPEnabled = defined(mepSetting) && mepSetting !== MEPState.TRANSACTIONS_ONLY;
  487. let requestData: any;
  488. if (displayType === DisplayType.TOP_N) {
  489. requestData = {
  490. organization,
  491. interval,
  492. start,
  493. end,
  494. project: projects,
  495. environment: environments,
  496. period: statsPeriod,
  497. query: widgetQuery.conditions,
  498. yAxis: widgetQuery.aggregates[widgetQuery.aggregates.length - 1],
  499. includePrevious: false,
  500. referrer,
  501. partial: true,
  502. field: [...widgetQuery.columns, ...widgetQuery.aggregates],
  503. queryExtras: getDashboardsMEPQueryParams(isMEPEnabled),
  504. includeAllArgs: true,
  505. topEvents: TOP_N,
  506. };
  507. if (widgetQuery.orderby) {
  508. requestData.orderby = widgetQuery.orderby;
  509. }
  510. } else {
  511. requestData = {
  512. organization,
  513. interval,
  514. start,
  515. end,
  516. project: projects,
  517. environment: environments,
  518. period: statsPeriod,
  519. query: widgetQuery.conditions,
  520. yAxis: widgetQuery.aggregates,
  521. orderby: widgetQuery.orderby,
  522. includePrevious: false,
  523. referrer,
  524. partial: true,
  525. queryExtras: getDashboardsMEPQueryParams(isMEPEnabled),
  526. includeAllArgs: true,
  527. };
  528. if (widgetQuery.columns?.length !== 0) {
  529. requestData.topEvents = limit ?? TOP_N;
  530. requestData.field = [...widgetQuery.columns, ...widgetQuery.aggregates];
  531. // Compare field and orderby as aliases to ensure requestData has
  532. // the orderby selected
  533. // If the orderby is an equation alias, do not inject it
  534. const orderby = trimStart(widgetQuery.orderby, '-');
  535. if (
  536. widgetQuery.orderby &&
  537. !isEquationAlias(orderby) &&
  538. !requestData.field.includes(orderby)
  539. ) {
  540. requestData.field.push(orderby);
  541. }
  542. // The "Other" series is only included when there is one
  543. // y-axis and one widgetQuery
  544. requestData.excludeOther =
  545. widgetQuery.aggregates.length !== 1 || widget.queries.length !== 1;
  546. if (isEquation(trimStart(widgetQuery.orderby, '-'))) {
  547. const nextEquationIndex = getNumEquations(widgetQuery.aggregates);
  548. const isDescending = widgetQuery.orderby.startsWith('-');
  549. const prefix = isDescending ? '-' : '';
  550. // Construct the alias form of the equation and inject it into the request
  551. requestData.orderby = `${prefix}equation[${nextEquationIndex}]`;
  552. requestData.field = [
  553. ...widgetQuery.columns,
  554. ...widgetQuery.aggregates,
  555. trimStart(widgetQuery.orderby, '-'),
  556. ];
  557. }
  558. }
  559. }
  560. if (shouldUseOnDemandMetrics(organization, widget, onDemandControlContext)) {
  561. requestData.queryExtras = {
  562. ...requestData.queryExtras,
  563. ...getQueryExtraForSplittingDiscover(widget, organization, true),
  564. };
  565. return doOnDemandMetricsRequest(api, requestData, widget.widgetType);
  566. }
  567. if (organization.features.includes('performance-discover-dataset-selector')) {
  568. requestData.queryExtras = {
  569. ...requestData.queryExtras,
  570. ...getQueryExtraForSplittingDiscover(widget, organization, false),
  571. };
  572. }
  573. return doEventsRequest<true>(api, requestData);
  574. }
  575. export async function doOnDemandMetricsRequest(
  576. api: any,
  577. requestData: any,
  578. widgetType: any
  579. ): Promise<
  580. [EventsStats | MultiSeriesEventsStats, string | undefined, ResponseMeta | undefined]
  581. > {
  582. try {
  583. const isEditing = location.pathname.endsWith('/edit/');
  584. const fetchEstimatedStats = () =>
  585. `/organizations/${requestData.organization.slug}/metrics-estimation-stats/`;
  586. const response = await doEventsRequest<false>(api, {
  587. ...requestData,
  588. queryExtras: {
  589. ...requestData.queryExtras,
  590. useOnDemandMetrics: true,
  591. onDemandType: 'dynamic_query',
  592. },
  593. dataset: 'metricsEnhanced',
  594. generatePathname: isEditing ? fetchEstimatedStats : undefined,
  595. });
  596. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  597. response[0] = {...response[0]};
  598. if (
  599. hasDatasetSelector(requestData.organization) &&
  600. widgetType === WidgetType.DISCOVER
  601. ) {
  602. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  603. const meta = response[0].meta ?? {};
  604. meta.discoverSplitDecision = 'transaction-like';
  605. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  606. response[0] = {...response[0], ...{meta}};
  607. }
  608. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  609. return [response[0], response[1], response[2]];
  610. } catch (err) {
  611. Sentry.captureMessage('Failed to fetch metrics estimation stats', {extra: err});
  612. return doEventsRequest<true>(api, requestData);
  613. }
  614. }
  615. // Checks fieldValue to see what function is being used and only allow supported custom measurements
  616. export function filterAggregateParams(
  617. option: FieldValueOption,
  618. fieldValue?: QueryFieldValue
  619. ) {
  620. if (
  621. (option.value.kind === FieldValueKind.CUSTOM_MEASUREMENT &&
  622. fieldValue?.kind === 'function' &&
  623. fieldValue?.function &&
  624. !option.value.meta.functions.includes(fieldValue.function[0])) ||
  625. option.value.meta.name === FieldKey.TOTAL_COUNT
  626. ) {
  627. return false;
  628. }
  629. return true;
  630. }
  631. const getQueryExtraForSplittingDiscover = (
  632. widget: Widget,
  633. organization: Organization,
  634. useOnDemandMetrics: boolean
  635. ) => {
  636. // We want to send the dashboardWidgetId on the request if we're in the Widget
  637. // Builder with the selector feature flag
  638. const isEditing = location.pathname.endsWith('/edit/');
  639. const hasDiscoverSelector = organization.features.includes(
  640. 'performance-discover-dataset-selector'
  641. );
  642. if (!hasDiscoverSelector) {
  643. if (
  644. !useOnDemandMetrics ||
  645. !organization.features.includes('performance-discover-widget-split-ui')
  646. ) {
  647. return {};
  648. }
  649. if (widget.id) {
  650. return {dashboardWidgetId: widget.id};
  651. }
  652. return {};
  653. }
  654. if (isEditing && widget.id) {
  655. return {dashboardWidgetId: widget.id};
  656. }
  657. return {};
  658. };