transactions.tsx 9.0 KB

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