errors.tsx 8.9 KB

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