transactions.tsx 9.2 KB

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