widgetQueries.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import {useContext} from 'react';
  2. import omit from 'lodash/omit';
  3. import {Client} from 'sentry/api';
  4. import {isMultiSeriesStats} from 'sentry/components/charts/utils';
  5. import {
  6. EventsStats,
  7. MultiSeriesEventsStats,
  8. Organization,
  9. PageFilters,
  10. } from 'sentry/types';
  11. import {Series} from 'sentry/types/echarts';
  12. import {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 {useMEPSettingContext} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  16. import {OnDemandControlConsumer} from 'sentry/utils/performance/contexts/onDemandControl';
  17. import {ErrorsAndTransactionsConfig} from '../datasetConfig/errorsAndTransactions';
  18. import {DashboardFilters, Widget} from '../types';
  19. import {DashboardsMEPContext} from './dashboardsMEPContext';
  20. import GenericWidgetQueries, {
  21. GenericWidgetQueriesChildrenProps,
  22. OnDataFetchedProps,
  23. } from './genericWidgetQueries';
  24. type SeriesResult = EventsStats | MultiSeriesEventsStats;
  25. type TableResult = TableData | EventsTableData;
  26. type SeriesWithOrdering = [order: number, series: Series];
  27. export function transformSeries(
  28. stats: EventsStats,
  29. seriesName: string,
  30. field: string
  31. ): Series {
  32. const unit = stats.meta?.units?.[getAggregateAlias(field)];
  33. // Scale series values to milliseconds or bytes depending on units from meta
  34. const scale = (unit && (DURATION_UNITS[unit] ?? SIZE_UNITS[unit])) ?? 1;
  35. return {
  36. seriesName,
  37. data:
  38. stats?.data?.map(([timestamp, counts]) => {
  39. return {
  40. name: timestamp * 1000,
  41. value: counts.reduce((acc, {count}) => acc + count, 0) * scale,
  42. };
  43. }) ?? [],
  44. };
  45. }
  46. /**
  47. * Multiseries data with a grouping needs to be "flattened" because the aggregate data
  48. * are stored under the group names. These names need to be combined with the aggregate
  49. * names to show a series.
  50. *
  51. * e.g. count() and count_unique() grouped by environment
  52. * {
  53. * "local": {
  54. * "count()": {...},
  55. * "count_unique()": {...}
  56. * },
  57. * "prod": {
  58. * "count()": {...},
  59. * "count_unique()": {...}
  60. * }
  61. * }
  62. */
  63. export function flattenMultiSeriesDataWithGrouping(
  64. result: SeriesResult,
  65. queryAlias: string
  66. ): SeriesWithOrdering[] {
  67. const seriesWithOrdering: SeriesWithOrdering[] = [];
  68. const groupNames = Object.keys(result);
  69. groupNames.forEach(groupName => {
  70. // Each group contains an order key which we should ignore
  71. const aggregateNames = Object.keys(omit(result[groupName], 'order'));
  72. aggregateNames.forEach(aggregate => {
  73. const seriesName = `${groupName} : ${aggregate}`;
  74. const prefixedName = queryAlias ? `${queryAlias} > ${seriesName}` : seriesName;
  75. const seriesData: EventsStats = result[groupName][aggregate];
  76. seriesWithOrdering.push([
  77. result[groupName].order || 0,
  78. transformSeries(seriesData, prefixedName, seriesName),
  79. ]);
  80. });
  81. });
  82. return seriesWithOrdering;
  83. }
  84. export function getIsMetricsDataFromSeriesResponse(
  85. result: SeriesResult
  86. ): boolean | undefined {
  87. const multiIsMetricsData = Object.values(result)
  88. .map(({isMetricsData}) => isMetricsData)
  89. // One non-metrics series will cause all of them to be marked as such
  90. .reduce((acc, value) => (acc === false ? false : value), undefined);
  91. return isMultiSeriesStats(result) ? multiIsMetricsData : result.isMetricsData;
  92. }
  93. type Props = {
  94. api: Client;
  95. children: (props: GenericWidgetQueriesChildrenProps) => JSX.Element;
  96. organization: Organization;
  97. selection: PageFilters;
  98. widget: Widget;
  99. cursor?: string;
  100. dashboardFilters?: DashboardFilters;
  101. limit?: number;
  102. onDataFetched?: (results: OnDataFetchedProps) => void;
  103. };
  104. function WidgetQueries({
  105. api,
  106. children,
  107. organization,
  108. selection,
  109. widget,
  110. dashboardFilters,
  111. cursor,
  112. limit,
  113. onDataFetched,
  114. }: Props) {
  115. const config = ErrorsAndTransactionsConfig;
  116. const context = useContext(DashboardsMEPContext);
  117. const mepSettingContext = useMEPSettingContext();
  118. let setIsMetricsData: undefined | ((value?: boolean) => void);
  119. if (context) {
  120. setIsMetricsData = context.setIsMetricsData;
  121. }
  122. const isSeriesMetricsDataResults: boolean[] = [];
  123. const afterFetchSeriesData = (rawResults: SeriesResult) => {
  124. if (rawResults.data) {
  125. rawResults = rawResults as EventsStats;
  126. if (rawResults.isMetricsData !== undefined) {
  127. isSeriesMetricsDataResults.push(rawResults.isMetricsData);
  128. }
  129. } else {
  130. Object.keys(rawResults).forEach(key => {
  131. const rawResult: EventsStats = rawResults[key];
  132. if (rawResult.isMetricsData !== undefined) {
  133. isSeriesMetricsDataResults.push(rawResult.isMetricsData);
  134. }
  135. });
  136. }
  137. // If one of the queries is sampled, then mark the whole thing as sampled
  138. setIsMetricsData?.(!isSeriesMetricsDataResults.includes(false));
  139. };
  140. const isTableMetricsDataResults: boolean[] = [];
  141. const afterFetchTableData = (rawResults: TableResult) => {
  142. if (rawResults.meta?.isMetricsData !== undefined) {
  143. isTableMetricsDataResults.push(rawResults.meta.isMetricsData);
  144. }
  145. // If one of the queries is sampled, then mark the whole thing as sampled
  146. setIsMetricsData?.(!isTableMetricsDataResults.includes(false));
  147. };
  148. return (
  149. <OnDemandControlConsumer>
  150. {OnDemandControlContext => (
  151. <GenericWidgetQueries<SeriesResult, TableResult>
  152. config={config}
  153. api={api}
  154. organization={organization}
  155. selection={selection}
  156. widget={widget}
  157. cursor={cursor}
  158. limit={limit}
  159. dashboardFilters={dashboardFilters}
  160. onDataFetched={onDataFetched}
  161. afterFetchSeriesData={afterFetchSeriesData}
  162. afterFetchTableData={afterFetchTableData}
  163. mepSetting={mepSettingContext.metricSettingState}
  164. onDemandControlContext={OnDemandControlContext}
  165. {...OnDemandControlContext}
  166. >
  167. {children}
  168. </GenericWidgetQueries>
  169. )}
  170. </OnDemandControlConsumer>
  171. );
  172. }
  173. export default WidgetQueries;