widgetQueries.tsx 9.1 KB

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