errors.tsx 8.7 KB

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