transactions.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. import trimStart from 'lodash/trimStart';
  2. import {doEventsRequest} from 'sentry/actionCreators/events';
  3. import type {Client} from 'sentry/api';
  4. import type {PageFilters} from 'sentry/types/core';
  5. import type {Series} from 'sentry/types/echarts';
  6. import type {TagCollection} from 'sentry/types/group';
  7. import type {
  8. EventsStats,
  9. MultiSeriesEventsStats,
  10. Organization,
  11. } from 'sentry/types/organization';
  12. import {defined} from 'sentry/utils';
  13. import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements';
  14. import type {EventsTableData, TableData} from 'sentry/utils/discover/discoverQuery';
  15. import {
  16. isEquation,
  17. isEquationAlias,
  18. SPAN_OP_BREAKDOWN_FIELDS,
  19. TRANSACTION_FIELDS,
  20. } from 'sentry/utils/discover/fields';
  21. import type {
  22. DiscoverQueryExtras,
  23. DiscoverQueryRequestParams,
  24. } from 'sentry/utils/discover/genericDiscoverQuery';
  25. import {doDiscoverQuery} from 'sentry/utils/discover/genericDiscoverQuery';
  26. import {DiscoverDatasets, TOP_N} from 'sentry/utils/discover/types';
  27. import {getMeasurements} from 'sentry/utils/measurements/measurements';
  28. import {MEPState} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  29. import {
  30. type OnDemandControlContext,
  31. shouldUseOnDemandMetrics,
  32. } from 'sentry/utils/performance/contexts/onDemandControl';
  33. import {generateFieldOptions} from 'sentry/views/discover/utils';
  34. import type {Widget, WidgetQuery} from '../types';
  35. import {DisplayType} from '../types';
  36. import {eventViewFromWidget, getNumEquations, getWidgetInterval} from '../utils';
  37. import {EventsSearchBar} from '../widgetBuilder/buildSteps/filterResultsStep/eventsSearchBar';
  38. import {type DatasetConfig, handleOrderByReset} from './base';
  39. import {
  40. doOnDemandMetricsRequest,
  41. filterAggregateParams,
  42. filterSeriesSortOptions,
  43. filterYAxisAggregateParams,
  44. filterYAxisOptions,
  45. getCustomEventsFieldRenderer,
  46. getTableSortOptions,
  47. getTimeseriesSortOptions,
  48. transformEventsResponseToSeries,
  49. transformEventsResponseToTable,
  50. } from './errorsAndTransactions';
  51. const DEFAULT_WIDGET_QUERY: WidgetQuery = {
  52. name: '',
  53. fields: ['count()'],
  54. columns: [],
  55. fieldAliases: [],
  56. aggregates: ['count()'],
  57. conditions: '',
  58. orderby: '-count()',
  59. };
  60. export type SeriesWithOrdering = [order: number, series: Series];
  61. export const TransactionsConfig: DatasetConfig<
  62. EventsStats | MultiSeriesEventsStats,
  63. TableData | EventsTableData
  64. > = {
  65. defaultWidgetQuery: DEFAULT_WIDGET_QUERY,
  66. enableEquations: true,
  67. getCustomFieldRenderer: getCustomEventsFieldRenderer,
  68. SearchBar: EventsSearchBar,
  69. filterSeriesSortOptions,
  70. filterYAxisAggregateParams,
  71. filterYAxisOptions,
  72. getTableFieldOptions: getEventsTableFieldOptions,
  73. getTimeseriesSortOptions,
  74. getTableSortOptions,
  75. getGroupByFieldOptions: getEventsTableFieldOptions,
  76. handleOrderByReset,
  77. supportedDisplayTypes: [
  78. DisplayType.AREA,
  79. DisplayType.BAR,
  80. DisplayType.BIG_NUMBER,
  81. DisplayType.LINE,
  82. DisplayType.TABLE,
  83. DisplayType.TOP_N,
  84. ],
  85. getTableRequest: (
  86. api: Client,
  87. widget: Widget,
  88. query: WidgetQuery,
  89. organization: Organization,
  90. pageFilters: PageFilters,
  91. onDemandControlContext?: OnDemandControlContext,
  92. limit?: number,
  93. cursor?: string,
  94. referrer?: string,
  95. mepSetting?: MEPState | null
  96. ) => {
  97. const useOnDemandMetrics = shouldUseOnDemandMetrics(
  98. organization,
  99. widget,
  100. onDemandControlContext
  101. );
  102. const queryExtras = {
  103. useOnDemandMetrics,
  104. onDemandType: 'dynamic_query',
  105. };
  106. return getEventsRequest(
  107. api,
  108. query,
  109. organization,
  110. pageFilters,
  111. limit,
  112. cursor,
  113. referrer,
  114. mepSetting,
  115. queryExtras
  116. );
  117. },
  118. getSeriesRequest: getEventsSeriesRequest,
  119. transformSeries: transformEventsResponseToSeries,
  120. transformTable: transformEventsResponseToTable,
  121. filterAggregateParams,
  122. };
  123. function getEventsTableFieldOptions(
  124. organization: Organization,
  125. tags?: TagCollection,
  126. customMeasurements?: CustomMeasurementCollection
  127. ) {
  128. const measurements = getMeasurements();
  129. return generateFieldOptions({
  130. organization,
  131. tagKeys: Object.values(tags ?? {}).map(({key}) => key),
  132. measurementKeys: Object.values(measurements).map(({key}) => key),
  133. spanOperationBreakdownKeys: SPAN_OP_BREAKDOWN_FIELDS,
  134. customMeasurements: Object.values(customMeasurements ?? {}).map(
  135. ({key, functions}) => ({
  136. key,
  137. functions,
  138. })
  139. ),
  140. fieldKeys: TRANSACTION_FIELDS,
  141. });
  142. }
  143. function getEventsRequest(
  144. api: Client,
  145. query: WidgetQuery,
  146. organization: Organization,
  147. pageFilters: PageFilters,
  148. limit?: number,
  149. cursor?: string,
  150. referrer?: string,
  151. mepSetting?: MEPState | null,
  152. queryExtras?: DiscoverQueryExtras
  153. ) {
  154. const isMEPEnabled = defined(mepSetting) && mepSetting !== MEPState.TRANSACTIONS_ONLY;
  155. const url = `/organizations/${organization.slug}/events/`;
  156. const eventView = eventViewFromWidget('', query, pageFilters);
  157. const params: DiscoverQueryRequestParams = {
  158. per_page: limit,
  159. cursor,
  160. referrer,
  161. dataset: isMEPEnabled
  162. ? DiscoverDatasets.METRICS_ENHANCED
  163. : DiscoverDatasets.TRANSACTIONS,
  164. ...queryExtras,
  165. };
  166. if (query.orderby) {
  167. params.sort = typeof query.orderby === 'string' ? [query.orderby] : query.orderby;
  168. }
  169. return doDiscoverQuery<EventsTableData>(
  170. api,
  171. url,
  172. {
  173. ...eventView.generateQueryStringObject(),
  174. ...params,
  175. },
  176. // Tries events request up to 3 times on rate limit
  177. {
  178. retry: {
  179. statusCodes: [429],
  180. tries: 3,
  181. },
  182. }
  183. );
  184. }
  185. function getEventsSeriesRequest(
  186. api: Client,
  187. widget: Widget,
  188. queryIndex: number,
  189. organization: Organization,
  190. pageFilters: PageFilters,
  191. onDemandControlContext?: OnDemandControlContext,
  192. referrer?: string,
  193. mepSetting?: MEPState | null
  194. ) {
  195. const isMEPEnabled = defined(mepSetting) && mepSetting !== MEPState.TRANSACTIONS_ONLY;
  196. const widgetQuery = widget.queries[queryIndex];
  197. const {displayType, limit} = widget;
  198. const {environments, projects} = pageFilters;
  199. const {start, end, period: statsPeriod} = pageFilters.datetime;
  200. const interval = getWidgetInterval(
  201. displayType,
  202. {start, end, period: statsPeriod},
  203. '1m'
  204. );
  205. let requestData;
  206. if (displayType === DisplayType.TOP_N) {
  207. requestData = {
  208. organization,
  209. interval,
  210. start,
  211. end,
  212. project: projects,
  213. environment: environments,
  214. period: statsPeriod,
  215. query: widgetQuery.conditions,
  216. yAxis: widgetQuery.aggregates[widgetQuery.aggregates.length - 1],
  217. includePrevious: false,
  218. referrer,
  219. partial: true,
  220. field: [...widgetQuery.columns, ...widgetQuery.aggregates],
  221. includeAllArgs: true,
  222. topEvents: TOP_N,
  223. dataset: isMEPEnabled
  224. ? DiscoverDatasets.METRICS_ENHANCED
  225. : DiscoverDatasets.TRANSACTIONS,
  226. };
  227. if (widgetQuery.orderby) {
  228. requestData.orderby = widgetQuery.orderby;
  229. }
  230. } else {
  231. requestData = {
  232. organization,
  233. interval,
  234. start,
  235. end,
  236. project: projects,
  237. environment: environments,
  238. period: statsPeriod,
  239. query: widgetQuery.conditions,
  240. yAxis: widgetQuery.aggregates,
  241. orderby: widgetQuery.orderby,
  242. includePrevious: false,
  243. referrer,
  244. partial: true,
  245. includeAllArgs: true,
  246. dataset: isMEPEnabled
  247. ? DiscoverDatasets.METRICS_ENHANCED
  248. : DiscoverDatasets.TRANSACTIONS,
  249. };
  250. if (widgetQuery.columns?.length !== 0) {
  251. requestData.topEvents = limit ?? TOP_N;
  252. requestData.field = [...widgetQuery.columns, ...widgetQuery.aggregates];
  253. // Compare field and orderby as aliases to ensure requestData has
  254. // the orderby selected
  255. // If the orderby is an equation alias, do not inject it
  256. const orderby = trimStart(widgetQuery.orderby, '-');
  257. if (
  258. widgetQuery.orderby &&
  259. !isEquationAlias(orderby) &&
  260. !requestData.field.includes(orderby)
  261. ) {
  262. requestData.field.push(orderby);
  263. }
  264. // The "Other" series is only included when there is one
  265. // y-axis and one widgetQuery
  266. requestData.excludeOther =
  267. widgetQuery.aggregates.length !== 1 || widget.queries.length !== 1;
  268. if (isEquation(trimStart(widgetQuery.orderby, '-'))) {
  269. const nextEquationIndex = getNumEquations(widgetQuery.aggregates);
  270. const isDescending = widgetQuery.orderby.startsWith('-');
  271. const prefix = isDescending ? '-' : '';
  272. // Construct the alias form of the equation and inject it into the request
  273. requestData.orderby = `${prefix}equation[${nextEquationIndex}]`;
  274. requestData.field = [
  275. ...widgetQuery.columns,
  276. ...widgetQuery.aggregates,
  277. trimStart(widgetQuery.orderby, '-'),
  278. ];
  279. }
  280. }
  281. }
  282. if (shouldUseOnDemandMetrics(organization, widget, onDemandControlContext)) {
  283. requestData.queryExtras = {
  284. ...requestData.queryExtras,
  285. ...{dataset: DiscoverDatasets.METRICS_ENHANCED},
  286. };
  287. return doOnDemandMetricsRequest(api, requestData, widget.widgetType);
  288. }
  289. return doEventsRequest<true>(api, requestData);
  290. }