widgetQueries.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import omit from 'lodash/omit';
  2. import type {Client} from 'sentry/api';
  3. import {isMultiSeriesStats} from 'sentry/components/charts/utils';
  4. import type {PageFilters} from 'sentry/types/core';
  5. import type {Series} from 'sentry/types/echarts';
  6. import type {
  7. EventsStats,
  8. MultiSeriesEventsStats,
  9. Organization,
  10. } from 'sentry/types/organization';
  11. import type {EventsTableData, TableData} from 'sentry/utils/discover/discoverQuery';
  12. import {DURATION_UNITS, SIZE_UNITS} from 'sentry/utils/discover/fieldRenderers';
  13. import {getAggregateAlias} from 'sentry/utils/discover/fields';
  14. import type {MetricsResultsMetaMapKey} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext';
  15. import {useMetricsResultsMeta} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext';
  16. import {useMEPSettingContext} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  17. import {OnDemandControlConsumer} from 'sentry/utils/performance/contexts/onDemandControl';
  18. import {getDatasetConfig} from 'sentry/views/dashboards/datasetConfig/base';
  19. import {type DashboardFilters, type Widget, WidgetType} from '../types';
  20. import {useDashboardsMEPContext} from './dashboardsMEPContext';
  21. import type {
  22. GenericWidgetQueriesChildrenProps,
  23. OnDataFetchedProps,
  24. } from './genericWidgetQueries';
  25. import GenericWidgetQueries from './genericWidgetQueries';
  26. type SeriesResult = EventsStats | MultiSeriesEventsStats;
  27. type TableResult = TableData | EventsTableData;
  28. type SeriesWithOrdering = [order: number, series: Series];
  29. export function transformSeries(
  30. stats: EventsStats,
  31. seriesName: string,
  32. field: string
  33. ): Series {
  34. const unit = stats.meta?.units?.[getAggregateAlias(field)];
  35. // Scale series values to milliseconds or bytes depending on units from meta
  36. const scale = (unit && (DURATION_UNITS[unit] ?? SIZE_UNITS[unit])) ?? 1;
  37. return {
  38. seriesName,
  39. data:
  40. stats?.data?.map(([timestamp, counts]) => {
  41. return {
  42. name: timestamp * 1000,
  43. value: counts.reduce((acc, {count}) => acc + count, 0) * scale,
  44. };
  45. }) ?? [],
  46. };
  47. }
  48. /**
  49. * Multiseries data with a grouping needs to be "flattened" because the aggregate data
  50. * are stored under the group names. These names need to be combined with the aggregate
  51. * names to show a series.
  52. *
  53. * e.g. count() and count_unique() grouped by environment
  54. * {
  55. * "local": {
  56. * "count()": {...},
  57. * "count_unique()": {...}
  58. * },
  59. * "prod": {
  60. * "count()": {...},
  61. * "count_unique()": {...}
  62. * }
  63. * }
  64. */
  65. export function flattenMultiSeriesDataWithGrouping(
  66. result: SeriesResult,
  67. queryAlias: string
  68. ): SeriesWithOrdering[] {
  69. const seriesWithOrdering: SeriesWithOrdering[] = [];
  70. const groupNames = Object.keys(result);
  71. groupNames.forEach(groupName => {
  72. // Each group contains an order key which we should ignore
  73. const aggregateNames = Object.keys(
  74. omit(result[groupName], ['order', 'isMetricsExtractedData'])
  75. );
  76. aggregateNames.forEach(aggregate => {
  77. const seriesName = `${groupName} : ${aggregate}`;
  78. const prefixedName = queryAlias ? `${queryAlias} > ${seriesName}` : seriesName;
  79. const seriesData: EventsStats = result[groupName][aggregate];
  80. seriesWithOrdering.push([
  81. result[groupName].order || 0,
  82. transformSeries(seriesData, prefixedName, seriesName),
  83. ]);
  84. });
  85. });
  86. return seriesWithOrdering;
  87. }
  88. export function getIsMetricsDataFromSeriesResponse(
  89. result: SeriesResult
  90. ): boolean | undefined {
  91. const multiIsMetricsData = Object.values(result)
  92. .map(({isMetricsData}) => isMetricsData)
  93. // One non-metrics series will cause all of them to be marked as such
  94. .reduce((acc, value) => (acc === false ? false : value), undefined);
  95. return isMultiSeriesStats(result) ? multiIsMetricsData : result.isMetricsData;
  96. }
  97. type Props = {
  98. api: Client;
  99. children: (props: GenericWidgetQueriesChildrenProps) => JSX.Element;
  100. organization: Organization;
  101. selection: PageFilters;
  102. widget: Widget;
  103. cursor?: string;
  104. dashboardFilters?: DashboardFilters;
  105. limit?: number;
  106. onDataFetched?: (results: OnDataFetchedProps) => void;
  107. onWidgetSplitDecision?: (splitDecision: WidgetType) => void;
  108. };
  109. function WidgetQueries({
  110. api,
  111. children,
  112. organization,
  113. selection,
  114. widget,
  115. dashboardFilters,
  116. cursor,
  117. limit,
  118. onDataFetched,
  119. onWidgetSplitDecision,
  120. }: Props) {
  121. // Discover and Errors datasets are the only datasets processed in this component
  122. const config = getDatasetConfig(
  123. widget.widgetType as WidgetType.DISCOVER | WidgetType.ERRORS | WidgetType.TRANSACTIONS
  124. );
  125. const context = useDashboardsMEPContext();
  126. const metricsMeta = useMetricsResultsMeta();
  127. const mepSettingContext = useMEPSettingContext();
  128. let setIsMetricsData: undefined | ((value?: boolean) => void);
  129. let setIsMetricsExtractedData:
  130. | undefined
  131. | ((mapKey: MetricsResultsMetaMapKey, value?: boolean) => void);
  132. if (context) {
  133. setIsMetricsData = context.setIsMetricsData;
  134. }
  135. if (metricsMeta) {
  136. setIsMetricsExtractedData = metricsMeta.setIsMetricsExtractedData;
  137. }
  138. const isSeriesMetricsDataResults: boolean[] = [];
  139. const isSeriesMetricsExtractedDataResults: (boolean | undefined)[] = [];
  140. const afterFetchSeriesData = (rawResults: SeriesResult) => {
  141. if (rawResults.data) {
  142. rawResults = rawResults as EventsStats;
  143. if (rawResults.isMetricsData !== undefined) {
  144. isSeriesMetricsDataResults.push(rawResults.isMetricsData);
  145. }
  146. if (rawResults.isMetricsExtractedData !== undefined) {
  147. isSeriesMetricsExtractedDataResults.push(rawResults.isMetricsExtractedData);
  148. }
  149. isSeriesMetricsExtractedDataResults.push(
  150. rawResults.isMetricsExtractedData || rawResults.meta?.isMetricsExtractedData
  151. );
  152. } else {
  153. Object.keys(rawResults).forEach(key => {
  154. const rawResult: EventsStats = rawResults[key];
  155. if (rawResult.isMetricsData !== undefined) {
  156. isSeriesMetricsDataResults.push(rawResult.isMetricsData);
  157. }
  158. if (
  159. (rawResult.isMetricsExtractedData || rawResult.meta?.isMetricsExtractedData) !==
  160. undefined
  161. ) {
  162. isSeriesMetricsExtractedDataResults.push(
  163. rawResult.isMetricsExtractedData || rawResult.meta?.isMetricsExtractedData
  164. );
  165. }
  166. });
  167. }
  168. // If one of the queries is sampled, then mark the whole thing as sampled
  169. setIsMetricsData?.(!isSeriesMetricsDataResults.includes(false));
  170. setIsMetricsExtractedData?.(
  171. widget,
  172. isSeriesMetricsExtractedDataResults.every(Boolean) &&
  173. isSeriesMetricsExtractedDataResults.some(Boolean)
  174. );
  175. const resultValues = Object.values(rawResults);
  176. if (organization.features.includes('performance-discover-dataset-selector')) {
  177. let splitDecision: WidgetType | undefined = undefined;
  178. if (rawResults.meta) {
  179. splitDecision = (rawResults.meta as EventsStats['meta'])?.discoverSplitDecision;
  180. } else if (Object.values(rawResults).length > 0) {
  181. // Multi-series queries will have a meta key on each series
  182. // We can just read the decision from one.
  183. splitDecision = resultValues[0]?.meta?.discoverSplitDecision;
  184. }
  185. if (splitDecision) {
  186. // Update the dashboard state with the split decision
  187. onWidgetSplitDecision?.(splitDecision);
  188. }
  189. }
  190. };
  191. const isTableMetricsDataResults: boolean[] = [];
  192. const isTableMetricsExtractedDataResults: boolean[] = [];
  193. const afterFetchTableData = (rawResults: TableResult) => {
  194. if (rawResults.meta?.isMetricsData !== undefined) {
  195. isTableMetricsDataResults.push(rawResults.meta.isMetricsData);
  196. }
  197. if (rawResults.meta?.isMetricsExtractedData !== undefined) {
  198. isTableMetricsExtractedDataResults.push(rawResults.meta.isMetricsExtractedData);
  199. }
  200. // If one of the queries is sampled, then mark the whole thing as sampled
  201. setIsMetricsData?.(!isTableMetricsDataResults.includes(false));
  202. setIsMetricsExtractedData?.(
  203. widget,
  204. isTableMetricsExtractedDataResults.every(Boolean) &&
  205. isTableMetricsExtractedDataResults.some(Boolean)
  206. );
  207. if (
  208. organization.features.includes('performance-discover-dataset-selector') &&
  209. [WidgetType.ERRORS, WidgetType.TRANSACTIONS].includes(
  210. rawResults?.meta?.discoverSplitDecision
  211. )
  212. ) {
  213. // Update the dashboard state with the split decision
  214. onWidgetSplitDecision?.(rawResults?.meta?.discoverSplitDecision);
  215. }
  216. };
  217. return (
  218. <OnDemandControlConsumer>
  219. {OnDemandControlContext => (
  220. <GenericWidgetQueries<SeriesResult, TableResult>
  221. config={config}
  222. api={api}
  223. organization={organization}
  224. selection={selection}
  225. widget={widget}
  226. cursor={cursor}
  227. limit={limit}
  228. dashboardFilters={dashboardFilters}
  229. onDataFetched={onDataFetched}
  230. afterFetchSeriesData={afterFetchSeriesData}
  231. afterFetchTableData={afterFetchTableData}
  232. mepSetting={mepSettingContext.metricSettingState}
  233. onDemandControlContext={OnDemandControlContext}
  234. {...OnDemandControlContext}
  235. >
  236. {children}
  237. </GenericWidgetQueries>
  238. )}
  239. </OnDemandControlConsumer>
  240. );
  241. }
  242. export default WidgetQueries;