widgetQueries.tsx 5.1 KB

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