spans.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import pickBy from 'lodash/pickBy';
  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 {TagCollection} from 'sentry/types/group';
  6. import type {
  7. EventsStats,
  8. MultiSeriesEventsStats,
  9. Organization,
  10. } from 'sentry/types/organization';
  11. import toArray from 'sentry/utils/array/toArray';
  12. import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements';
  13. import type {EventsTableData, TableData} from 'sentry/utils/discover/discoverQuery';
  14. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  15. import {
  16. type DiscoverQueryExtras,
  17. type DiscoverQueryRequestParams,
  18. doDiscoverQuery,
  19. } from 'sentry/utils/discover/genericDiscoverQuery';
  20. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  21. import {ALLOWED_EXPLORE_VISUALIZE_AGGREGATES} from 'sentry/utils/fields';
  22. import type {MEPState} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  23. import type {OnDemandControlContext} from 'sentry/utils/performance/contexts/onDemandControl';
  24. import {
  25. type DatasetConfig,
  26. handleOrderByReset,
  27. } from 'sentry/views/dashboards/datasetConfig/base';
  28. import {
  29. getTableSortOptions,
  30. transformEventsResponseToSeries,
  31. transformEventsResponseToTable,
  32. } from 'sentry/views/dashboards/datasetConfig/errorsAndTransactions';
  33. import {getSeriesRequestData} from 'sentry/views/dashboards/datasetConfig/utils/getSeriesRequestData';
  34. import {DisplayType, type Widget, type WidgetQuery} from 'sentry/views/dashboards/types';
  35. import {eventViewFromWidget} from 'sentry/views/dashboards/utils';
  36. import SpansSearchBar from 'sentry/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/spansSearchBar';
  37. import type {FieldValueOption} from 'sentry/views/discover/table/queryField';
  38. import {FieldValueKind} from 'sentry/views/discover/table/types';
  39. import {generateFieldOptions} from 'sentry/views/discover/utils';
  40. const DEFAULT_WIDGET_QUERY: WidgetQuery = {
  41. name: '',
  42. fields: ['count(span.duration)'],
  43. columns: [],
  44. fieldAliases: [],
  45. aggregates: ['count(span.duration)'],
  46. conditions: '',
  47. orderby: '',
  48. };
  49. const EAP_AGGREGATIONS = ALLOWED_EXPLORE_VISUALIZE_AGGREGATES.reduce((acc, aggregate) => {
  50. acc[aggregate] = {
  51. isSortable: true,
  52. outputType: null,
  53. parameters: [
  54. {
  55. kind: 'column',
  56. columnTypes: ['number', 'string'], // Need to keep the string type for unknown values before tags are resolved
  57. defaultValue: 'span.duration',
  58. required: true,
  59. },
  60. ],
  61. };
  62. return acc;
  63. }, {});
  64. // getTimeseriesSortOptions is undefined because we want to restrict the
  65. // sort options to the same behaviour as tables. i.e. we are only able
  66. // to sort by fields that have already been selected
  67. export const SpansConfig: DatasetConfig<
  68. EventsStats | MultiSeriesEventsStats,
  69. TableData | EventsTableData
  70. > = {
  71. defaultWidgetQuery: DEFAULT_WIDGET_QUERY,
  72. enableEquations: false,
  73. SearchBar: SpansSearchBar,
  74. filterYAxisAggregateParams: () => filterAggregateParams,
  75. filterYAxisOptions,
  76. getTableFieldOptions: getPrimaryFieldOptions,
  77. getTableSortOptions: getTableSortOptions,
  78. getGroupByFieldOptions,
  79. handleOrderByReset,
  80. supportedDisplayTypes: [
  81. DisplayType.AREA,
  82. DisplayType.BAR,
  83. DisplayType.BIG_NUMBER,
  84. DisplayType.LINE,
  85. DisplayType.TABLE,
  86. DisplayType.TOP_N,
  87. ],
  88. getTableRequest: (
  89. api: Client,
  90. _widget: Widget,
  91. query: WidgetQuery,
  92. organization: Organization,
  93. pageFilters: PageFilters,
  94. _onDemandControlContext?: OnDemandControlContext,
  95. limit?: number,
  96. cursor?: string,
  97. referrer?: string,
  98. _mepSetting?: MEPState | null
  99. ) => {
  100. return getEventsRequest(
  101. api,
  102. query,
  103. organization,
  104. pageFilters,
  105. limit,
  106. cursor,
  107. referrer
  108. );
  109. },
  110. getSeriesRequest,
  111. transformTable: transformEventsResponseToTable,
  112. transformSeries: transformEventsResponseToSeries,
  113. filterTableOptions,
  114. filterAggregateParams,
  115. getCustomFieldRenderer: (field, meta, _organization) => {
  116. return getFieldRenderer(field, meta, false);
  117. },
  118. };
  119. function getPrimaryFieldOptions(
  120. organization: Organization,
  121. tags?: TagCollection,
  122. _customMeasurements?: CustomMeasurementCollection
  123. ): Record<string, FieldValueOption> {
  124. const baseFieldOptions = generateFieldOptions({
  125. organization,
  126. tagKeys: [],
  127. fieldKeys: [],
  128. aggregations: EAP_AGGREGATIONS,
  129. });
  130. const spanTags = Object.values(tags ?? {}).reduce(
  131. (acc, tag) => ({
  132. ...acc,
  133. [`${tag.kind}:${tag.key}`]: {
  134. label: tag.name,
  135. value: {
  136. kind: FieldValueKind.TAG,
  137. // We have numeric and string tags which have the same
  138. // display name, but one is used for aggregates and the other
  139. // is used for grouping.
  140. meta: {name: tag.key, dataType: tag.kind === 'tag' ? 'string' : 'number'},
  141. },
  142. },
  143. }),
  144. {}
  145. );
  146. return {...baseFieldOptions, ...spanTags};
  147. }
  148. function filterTableOptions(option: FieldValueOption) {
  149. // Filter out numeric tags from primary options, they only show up in
  150. // the parameter fields for aggregate functions
  151. if ('dataType' in option.value.meta) {
  152. return option.value.meta.dataType !== 'number';
  153. }
  154. return true;
  155. }
  156. function filterAggregateParams(option: FieldValueOption) {
  157. // Allow for unknown values to be used for aggregate functions
  158. // This supports showing the tag value even if it's not in the current
  159. // set of tags.
  160. if ('unknown' in option.value.meta && option.value.meta.unknown) {
  161. return true;
  162. }
  163. if ('dataType' in option.value.meta) {
  164. return option.value.meta.dataType === 'number';
  165. }
  166. return true;
  167. }
  168. function filterYAxisOptions() {
  169. return function (option: FieldValueOption) {
  170. return option.value.kind === FieldValueKind.FUNCTION;
  171. };
  172. }
  173. function getEventsRequest(
  174. api: Client,
  175. query: WidgetQuery,
  176. organization: Organization,
  177. pageFilters: PageFilters,
  178. limit?: number,
  179. cursor?: string,
  180. referrer?: string,
  181. _mepSetting?: MEPState | null,
  182. queryExtras?: DiscoverQueryExtras
  183. ) {
  184. const url = `/organizations/${organization.slug}/events/`;
  185. const eventView = eventViewFromWidget('', query, pageFilters);
  186. const params: DiscoverQueryRequestParams = {
  187. per_page: limit,
  188. cursor,
  189. referrer,
  190. dataset: DiscoverDatasets.SPANS_EAP,
  191. ...queryExtras,
  192. };
  193. if (query.orderby) {
  194. params.sort = toArray(query.orderby);
  195. }
  196. return doDiscoverQuery<EventsTableData>(
  197. api,
  198. url,
  199. {
  200. ...eventView.generateQueryStringObject(),
  201. ...params,
  202. },
  203. // Tries events request up to 3 times on rate limit
  204. {
  205. retry: {
  206. statusCodes: [429],
  207. tries: 3,
  208. },
  209. }
  210. );
  211. }
  212. function getGroupByFieldOptions(
  213. organization: Organization,
  214. tags?: TagCollection,
  215. customMeasurements?: CustomMeasurementCollection
  216. ) {
  217. const primaryFieldOptions = getPrimaryFieldOptions(
  218. organization,
  219. tags,
  220. customMeasurements
  221. );
  222. const yAxisFilter = filterYAxisOptions();
  223. // The only options that should be returned as valid group by options
  224. // are string tags
  225. const filterGroupByOptions = (option: FieldValueOption) =>
  226. filterTableOptions(option) && !yAxisFilter(option);
  227. return pickBy(primaryFieldOptions, filterGroupByOptions);
  228. }
  229. function getSeriesRequest(
  230. api: Client,
  231. widget: Widget,
  232. queryIndex: number,
  233. organization: Organization,
  234. pageFilters: PageFilters,
  235. _onDemandControlContext?: OnDemandControlContext,
  236. referrer?: string,
  237. _mepSetting?: MEPState | null
  238. ) {
  239. const requestData = getSeriesRequestData(
  240. widget,
  241. queryIndex,
  242. organization,
  243. pageFilters,
  244. DiscoverDatasets.SPANS_EAP,
  245. referrer
  246. );
  247. return doEventsRequest<true>(api, requestData);
  248. }