metricsBaselineContainer.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. import {useEffect, useState} from 'react';
  2. import {InjectedRouter} from 'react-router';
  3. import {LineSeriesOption} from 'echarts';
  4. import {Location} from 'history';
  5. import {doEventsRequest} from 'sentry/actionCreators/events';
  6. import {Client} from 'sentry/api';
  7. import LineSeries from 'sentry/components/charts/series/lineSeries';
  8. import {isMultiSeriesStats, lightenHexToRgb} from 'sentry/components/charts/utils';
  9. import {EventsStats, Organization} from 'sentry/types';
  10. import {defined} from 'sentry/utils';
  11. import {getUtcToLocalDateObject} from 'sentry/utils/dates';
  12. import {EventsTableData} from 'sentry/utils/discover/discoverQuery';
  13. import EventView from 'sentry/utils/discover/eventView';
  14. import {aggregateMultiPlotType, isEquation} from 'sentry/utils/discover/fields';
  15. import {doDiscoverQuery} from 'sentry/utils/discover/genericDiscoverQuery';
  16. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  17. import localStorage from 'sentry/utils/localStorage';
  18. import {useMetricsCardinalityContext} from 'sentry/utils/performance/contexts/metricsCardinality';
  19. import theme from 'sentry/utils/theme';
  20. import {transformSeries} from 'sentry/views/dashboardsV2/widgetCard/widgetQueries';
  21. import {SeriesWithOrdering} from '../dashboardsV2/datasetConfig/errorsAndTransactions';
  22. import {PROCESSED_BASELINE_TOGGLE_KEY} from './chartFooter';
  23. import ResultsChart from './resultsChart';
  24. import {usesTransactionsDataset} from './utils';
  25. type MetricsBaselineContainerProps = {
  26. api: Client;
  27. confirmedQuery: boolean;
  28. eventView: EventView;
  29. location: Location;
  30. onAxisChange: (value: string[]) => void;
  31. onDisplayChange: (value: string) => void;
  32. onIntervalChange: (value: string | undefined) => void;
  33. onTopEventsChange: (value: string) => void;
  34. organization: Organization;
  35. router: InjectedRouter;
  36. // chart footer props
  37. total: number | null;
  38. yAxis: string[];
  39. };
  40. export function MetricsBaselineContainer({
  41. eventView,
  42. yAxis,
  43. location,
  44. organization,
  45. api,
  46. router,
  47. ...props
  48. }: MetricsBaselineContainerProps) {
  49. const metricsCardinality = useMetricsCardinalityContext();
  50. const displayMode = eventView.getDisplayMode();
  51. const isRollingOut =
  52. organization.features.includes('server-side-sampling') &&
  53. organization.features.includes('mep-rollout-flag') &&
  54. organization.features.includes('discover-metrics-baseline');
  55. const disableProcessedBaselineToggle =
  56. metricsCardinality.outcome?.forceTransactionsOnly ||
  57. displayMode !== 'default' ||
  58. !usesTransactionsDataset(eventView, yAxis) ||
  59. yAxis.some(isEquation);
  60. const apiPayload = eventView.getEventsAPIPayload(location);
  61. apiPayload.query = '';
  62. const pageFilters = eventView.getPageFilters();
  63. const start = pageFilters.datetime.start
  64. ? getUtcToLocalDateObject(pageFilters.datetime.start)
  65. : null;
  66. const end = pageFilters.datetime.end
  67. ? getUtcToLocalDateObject(pageFilters.datetime.end)
  68. : null;
  69. const showBaseline =
  70. (location.query.baseline ?? localStorage.getItem(PROCESSED_BASELINE_TOGGLE_KEY)) ===
  71. '0'
  72. ? false
  73. : true;
  74. const [metricsCompatible, setMetricsCompatible] = useState<boolean>(true);
  75. const [processedLineSeries, setProcessedLineSeries] = useState<
  76. LineSeriesOption[] | undefined
  77. >(undefined);
  78. const [processedTotal, setProcessedTotal] = useState<number | undefined>(undefined);
  79. const [loadingTotals, setLoadingTotals] = useState<boolean>(true);
  80. const [loadingSeries, setLoadingSeries] = useState<boolean>(true);
  81. useEffect(() => {
  82. let shouldCancelRequest = false;
  83. if (!isRollingOut || disableProcessedBaselineToggle || !showBaseline) {
  84. setProcessedTotal(undefined);
  85. setLoadingTotals(false);
  86. return undefined;
  87. }
  88. setLoadingTotals(true);
  89. doDiscoverQuery<EventsTableData>(api, `/organizations/${organization.slug}/events/`, {
  90. ...eventView.generateQueryStringObject(),
  91. field: ['count()'],
  92. query: '',
  93. sort: [],
  94. referrer: 'api.discover.processed-baseline-total',
  95. ...{dataset: DiscoverDatasets.METRICS},
  96. })
  97. .then(response => {
  98. if (shouldCancelRequest) {
  99. return;
  100. }
  101. const [data] = response;
  102. const total = data.data[0]?.['count()'];
  103. if (defined(total)) {
  104. setProcessedTotal(parseInt(total as string, 10));
  105. setLoadingTotals(false);
  106. } else {
  107. setProcessedTotal(undefined);
  108. setLoadingTotals(false);
  109. }
  110. })
  111. .catch(() => {
  112. if (shouldCancelRequest) {
  113. return;
  114. }
  115. setMetricsCompatible(false);
  116. setLoadingTotals(false);
  117. setProcessedTotal(undefined);
  118. });
  119. return () => {
  120. shouldCancelRequest = true;
  121. };
  122. }, [
  123. disableProcessedBaselineToggle,
  124. api,
  125. organization,
  126. eventView,
  127. showBaseline,
  128. isRollingOut,
  129. ]);
  130. useEffect(() => {
  131. let shouldCancelRequest = false;
  132. if (!isRollingOut || disableProcessedBaselineToggle || !showBaseline) {
  133. setLoadingSeries(false);
  134. setProcessedLineSeries(undefined);
  135. return undefined;
  136. }
  137. setLoadingSeries(true);
  138. setProcessedLineSeries(undefined);
  139. doEventsRequest(api, {
  140. organization,
  141. partial: true,
  142. start,
  143. end,
  144. yAxis,
  145. environment: pageFilters.environments,
  146. period: pageFilters.datetime.period,
  147. interval: eventView.interval,
  148. project: pageFilters.projects,
  149. query: '',
  150. queryExtras: {dataset: DiscoverDatasets.METRICS},
  151. })
  152. .then(response => {
  153. if (shouldCancelRequest) {
  154. return;
  155. }
  156. const additionalSeries: LineSeriesOption[] = [];
  157. if (isMultiSeriesStats(response)) {
  158. let seriesWithOrdering: SeriesWithOrdering[] = [];
  159. seriesWithOrdering = Object.keys(response).map((seriesName: string) => {
  160. const prefixedName = `processed events: ${seriesName}`;
  161. const seriesData: EventsStats = response[seriesName];
  162. return [
  163. seriesData.order || 0,
  164. transformSeries(seriesData, prefixedName, seriesName),
  165. ];
  166. });
  167. const color = theme.charts.getColorPalette(seriesWithOrdering.length - 2);
  168. const additionalSeriesColor = lightenHexToRgb(color);
  169. seriesWithOrdering.forEach(([order, series]) =>
  170. additionalSeries.push(
  171. LineSeries({
  172. name: series.seriesName,
  173. data: series.data.map(({name, value}) => [name, value]),
  174. stack:
  175. aggregateMultiPlotType(yAxis[order]) === 'area'
  176. ? 'processed'
  177. : undefined,
  178. lineStyle: {
  179. color: additionalSeriesColor[order],
  180. type: 'dashed',
  181. width: 1.5,
  182. opacity: 0.5,
  183. },
  184. itemStyle: {color: additionalSeriesColor[order]},
  185. animation: false,
  186. animationThreshold: 1,
  187. animationDuration: 0,
  188. })
  189. )
  190. );
  191. } else {
  192. const field = yAxis[0];
  193. const prefixedName = `processed events: ${field}`;
  194. const transformed = transformSeries(response, prefixedName, field);
  195. additionalSeries.push(
  196. LineSeries({
  197. name: transformed.seriesName,
  198. data: transformed.data.map(({name, value}) => [name, value]),
  199. lineStyle: {color: theme.gray300, type: 'dashed', width: 1.5},
  200. animation: false,
  201. animationThreshold: 1,
  202. animationDuration: 0,
  203. })
  204. );
  205. }
  206. setLoadingSeries(false);
  207. setMetricsCompatible(true);
  208. setProcessedLineSeries(additionalSeries);
  209. })
  210. .catch(() => {
  211. if (shouldCancelRequest) {
  212. return;
  213. }
  214. setLoadingSeries(false);
  215. setMetricsCompatible(false);
  216. setProcessedLineSeries(undefined);
  217. });
  218. return () => {
  219. shouldCancelRequest = true;
  220. };
  221. }, [
  222. disableProcessedBaselineToggle,
  223. api,
  224. organization,
  225. start,
  226. end,
  227. yAxis,
  228. pageFilters.environments,
  229. pageFilters.datetime.period,
  230. pageFilters.projects,
  231. eventView.interval,
  232. showBaseline,
  233. isRollingOut,
  234. ]);
  235. return (
  236. <ResultsChart
  237. organization={organization}
  238. eventView={eventView}
  239. location={location}
  240. yAxis={yAxis}
  241. router={router}
  242. processedLineSeries={showBaseline ? processedLineSeries : undefined}
  243. disableProcessedBaselineToggle={
  244. disableProcessedBaselineToggle || !metricsCompatible
  245. }
  246. processedTotal={processedTotal}
  247. loadingProcessedTotals={loadingTotals}
  248. showBaseline={showBaseline}
  249. loadingProcessedEventsBaseline={loadingSeries}
  250. reloadingProcessedEventsBaseline={processedLineSeries !== null && loadingSeries}
  251. setShowBaseline={(value: boolean) => {
  252. router.push({
  253. pathname: location.pathname,
  254. query: {
  255. ...location.query,
  256. baseline: value === false ? '0' : '1',
  257. },
  258. });
  259. }}
  260. {...props}
  261. />
  262. );
  263. }