index.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. import {useCallback, useMemo} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {CompactSelect} from 'sentry/components/compactSelect';
  5. import {Tooltip} from 'sentry/components/tooltip';
  6. import {IconClock, IconGraph} from 'sentry/icons';
  7. import {t} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. import type {Confidence, NewQuery} from 'sentry/types/organization';
  10. import {defined} from 'sentry/utils';
  11. import {dedupeArray} from 'sentry/utils/dedupeArray';
  12. import EventView from 'sentry/utils/discover/eventView';
  13. import {parseFunction, prettifyParsedFunction} from 'sentry/utils/discover/fields';
  14. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  15. import {isTimeSeriesOther} from 'sentry/utils/timeSeries/isTimeSeriesOther';
  16. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  17. import usePageFilters from 'sentry/utils/usePageFilters';
  18. import usePrevious from 'sentry/utils/usePrevious';
  19. import {determineSeriesSampleCountAndIsSampled} from 'sentry/views/alerts/rules/metric/utils/determineSeriesSampleCount';
  20. import {WidgetSyncContextProvider} from 'sentry/views/dashboards/contexts/widgetSyncContext';
  21. import {Area} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/area';
  22. import {Bars} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/bars';
  23. import {Line} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/line';
  24. import {TimeSeriesWidgetVisualization} from 'sentry/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization';
  25. import {Widget} from 'sentry/views/dashboards/widgets/widget/widget';
  26. import {ConfidenceFooter} from 'sentry/views/explore/charts/confidenceFooter';
  27. import ChartContextMenu from 'sentry/views/explore/components/chartContextMenu';
  28. import {
  29. useExploreDataset,
  30. useExploreVisualizes,
  31. useSetExploreVisualizes,
  32. } from 'sentry/views/explore/contexts/pageParamsContext';
  33. import {useChartInterval} from 'sentry/views/explore/hooks/useChartInterval';
  34. import {useTopEvents} from 'sentry/views/explore/hooks/useTopEvents';
  35. import {showConfidence} from 'sentry/views/explore/utils';
  36. import {
  37. ChartType,
  38. useSynchronizeCharts,
  39. } from 'sentry/views/insights/common/components/chart';
  40. import type {useSortedTimeSeries} from 'sentry/views/insights/common/queries/useSortedTimeSeries';
  41. import {useSpansQuery} from 'sentry/views/insights/common/queries/useSpansQuery';
  42. import {CHART_HEIGHT, INGESTION_DELAY} from '../settings';
  43. interface ExploreChartsProps {
  44. canUsePreviousResults: boolean;
  45. confidences: Confidence[];
  46. query: string;
  47. timeseriesResult: ReturnType<typeof useSortedTimeSeries>;
  48. }
  49. export const EXPLORE_CHART_TYPE_OPTIONS = [
  50. {
  51. value: ChartType.LINE,
  52. label: t('Line'),
  53. },
  54. {
  55. value: ChartType.AREA,
  56. label: t('Area'),
  57. },
  58. {
  59. value: ChartType.BAR,
  60. label: t('Bar'),
  61. },
  62. ];
  63. export const EXPLORE_CHART_GROUP = 'explore-charts_group';
  64. export function ExploreCharts({
  65. canUsePreviousResults,
  66. confidences,
  67. query,
  68. timeseriesResult,
  69. }: ExploreChartsProps) {
  70. const theme = useTheme();
  71. const dataset = useExploreDataset();
  72. const visualizes = useExploreVisualizes();
  73. const setVisualizes = useSetExploreVisualizes();
  74. const [interval, setInterval, intervalOptions] = useChartInterval();
  75. const topEvents = useTopEvents();
  76. const isTopN = defined(topEvents) && topEvents > 0;
  77. const previousTimeseriesResult = usePrevious(timeseriesResult);
  78. const getSeries = useCallback(
  79. (dedupedYAxes: string[], formattedYAxes: Array<string | undefined>) => {
  80. const shouldUsePreviousResults =
  81. timeseriesResult.isPending &&
  82. canUsePreviousResults &&
  83. dedupedYAxes.every(yAxis => previousTimeseriesResult.data.hasOwnProperty(yAxis));
  84. const data = dedupedYAxes.flatMap((yAxis, i) => {
  85. const series = shouldUsePreviousResults
  86. ? previousTimeseriesResult.data[yAxis]
  87. : timeseriesResult.data[yAxis];
  88. return (series ?? []).map(s => {
  89. // We replace the series name with the formatted series name here
  90. // when possible as it's cleaner to read.
  91. //
  92. // We can't do this in top N mode as the series name uses the row
  93. // values instead of the aggregate function.
  94. if (s.field === yAxis) {
  95. return {
  96. ...s,
  97. seriesName: formattedYAxes[i] ?? yAxis,
  98. };
  99. }
  100. return s;
  101. });
  102. });
  103. return {
  104. data,
  105. error: shouldUsePreviousResults
  106. ? previousTimeseriesResult.error
  107. : timeseriesResult.error,
  108. loading: shouldUsePreviousResults
  109. ? previousTimeseriesResult.isPending
  110. : timeseriesResult.isPending,
  111. };
  112. },
  113. [canUsePreviousResults, timeseriesResult, previousTimeseriesResult]
  114. );
  115. const chartInfos = useMemo(() => {
  116. return visualizes.map((visualize, index) => {
  117. const dedupedYAxes = dedupeArray(visualize.yAxes);
  118. const formattedYAxes = dedupedYAxes.map(yaxis => {
  119. const func = parseFunction(yaxis);
  120. return func ? prettifyParsedFunction(func) : undefined;
  121. });
  122. const chartIcon =
  123. visualize.chartType === ChartType.LINE
  124. ? 'line'
  125. : visualize.chartType === ChartType.AREA
  126. ? 'area'
  127. : 'bar';
  128. const {data, error, loading} = getSeries(dedupedYAxes, formattedYAxes);
  129. const {sampleCount, isSampled} = determineSeriesSampleCountAndIsSampled(
  130. data,
  131. isTopN
  132. );
  133. return {
  134. chartIcon: <IconGraph type={chartIcon} />,
  135. chartType: visualize.chartType,
  136. label: visualize.label,
  137. yAxes: visualize.yAxes,
  138. formattedYAxes,
  139. data,
  140. error,
  141. loading,
  142. confidence: confidences[index],
  143. sampleCount,
  144. isSampled,
  145. };
  146. });
  147. }, [confidences, getSeries, visualizes, isTopN]);
  148. const handleChartTypeChange = useCallback(
  149. (chartType: ChartType, index: number) => {
  150. const newVisualizes = visualizes.slice();
  151. newVisualizes[index] = {...newVisualizes[index]!, chartType};
  152. setVisualizes(newVisualizes);
  153. },
  154. [visualizes, setVisualizes]
  155. );
  156. useSynchronizeCharts(
  157. visualizes.length,
  158. !timeseriesResult.isPending,
  159. EXPLORE_CHART_GROUP
  160. );
  161. const shouldRenderLabel = visualizes.length > 1;
  162. return (
  163. <ChartList>
  164. <WidgetSyncContextProvider>
  165. {chartInfos.map((chartInfo, index) => {
  166. const Title = (
  167. <ChartTitle>
  168. {shouldRenderLabel && <ChartLabel>{chartInfo.label}</ChartLabel>}
  169. <Widget.WidgetTitle
  170. title={chartInfo.formattedYAxes.filter(Boolean).join(', ')}
  171. />
  172. </ChartTitle>
  173. );
  174. if (chartInfo.loading) {
  175. return (
  176. <Widget
  177. key={index}
  178. height={CHART_HEIGHT}
  179. Title={Title}
  180. Visualization={<TimeSeriesWidgetVisualization.LoadingPlaceholder />}
  181. revealActions="always"
  182. />
  183. );
  184. }
  185. if (chartInfo.error) {
  186. return (
  187. <Widget
  188. key={index}
  189. height={CHART_HEIGHT}
  190. Title={Title}
  191. Visualization={<Widget.WidgetError error={chartInfo.error} />}
  192. revealActions="always"
  193. />
  194. );
  195. }
  196. if (chartInfo.data.length === 0) {
  197. // This happens when the `/events-stats/` endpoint returns a blank
  198. // response. This is a rare error condition that happens when
  199. // proxying to RPC. Adding explicit handling with a "better" message
  200. return (
  201. <Widget
  202. key={index}
  203. height={CHART_HEIGHT}
  204. Title={Title}
  205. Visualization={<Widget.WidgetError error={t('No data')} />}
  206. revealActions="always"
  207. />
  208. );
  209. }
  210. const DataPlottableConstructor =
  211. chartInfo.chartType === ChartType.LINE
  212. ? Line
  213. : chartInfo.chartType === ChartType.AREA
  214. ? Area
  215. : Bars;
  216. return (
  217. <Widget
  218. key={index}
  219. height={CHART_HEIGHT}
  220. Title={Title}
  221. Actions={[
  222. <Tooltip
  223. key="visualization"
  224. title={t('Type of chart displayed in this visualization (ex. line)')}
  225. >
  226. <CompactSelect
  227. triggerProps={{
  228. icon: chartInfo.chartIcon,
  229. borderless: true,
  230. showChevron: false,
  231. size: 'xs',
  232. }}
  233. value={chartInfo.chartType}
  234. menuTitle="Type"
  235. options={EXPLORE_CHART_TYPE_OPTIONS}
  236. onChange={option => handleChartTypeChange(option.value, index)}
  237. />
  238. </Tooltip>,
  239. <Tooltip
  240. key="interval"
  241. title={t('Time interval displayed in this visualization (ex. 5m)')}
  242. >
  243. <CompactSelect
  244. value={interval}
  245. onChange={({value}) => setInterval(value)}
  246. triggerProps={{
  247. icon: <IconClock />,
  248. borderless: true,
  249. showChevron: false,
  250. size: 'xs',
  251. }}
  252. menuTitle="Interval"
  253. options={intervalOptions}
  254. />
  255. </Tooltip>,
  256. <ChartContextMenu
  257. key="context"
  258. visualizeYAxes={chartInfo.yAxes}
  259. query={query}
  260. interval={interval}
  261. visualizeIndex={index}
  262. />,
  263. ]}
  264. revealActions="always"
  265. Visualization={
  266. <TimeSeriesWidgetVisualization
  267. plottables={chartInfo.data.map(timeSeries => {
  268. return new DataPlottableConstructor(timeSeries, {
  269. delay: INGESTION_DELAY,
  270. color: isTimeSeriesOther(timeSeries) ? theme.chartOther : undefined,
  271. stack: 'all',
  272. });
  273. })}
  274. />
  275. }
  276. Footer={
  277. dataset === DiscoverDatasets.SPANS_EAP_RPC &&
  278. showConfidence(chartInfo.isSampled) && (
  279. <ConfidenceFooter
  280. sampleCount={chartInfo.sampleCount}
  281. confidence={chartInfo.confidence}
  282. topEvents={
  283. topEvents ? Math.min(topEvents, chartInfo.data.length) : undefined
  284. }
  285. />
  286. )
  287. }
  288. />
  289. );
  290. })}
  291. </WidgetSyncContextProvider>
  292. </ChartList>
  293. );
  294. }
  295. export function useExtrapolationMeta({
  296. dataset,
  297. query,
  298. isAllowedSelection,
  299. }: {
  300. dataset: DiscoverDatasets;
  301. query: string;
  302. isAllowedSelection?: boolean;
  303. }) {
  304. const {selection} = usePageFilters();
  305. const extrapolationMetaEventView = useMemo(() => {
  306. const search = new MutableSearch(query);
  307. // Filtering out all spans with op like 'ui.interaction*' which aren't
  308. // embedded under transactions. The trace view does not support rendering
  309. // such spans yet.
  310. search.addFilterValues('!transaction.span_id', ['00']);
  311. const discoverQuery: NewQuery = {
  312. id: undefined,
  313. name: 'Explore - Extrapolation Meta',
  314. fields: ['count_sample()', 'min(sampling_rate)'],
  315. query: search.formatString(),
  316. version: 2,
  317. dataset,
  318. };
  319. return EventView.fromNewQueryWithPageFilters(discoverQuery, selection);
  320. }, [dataset, query, selection]);
  321. return useSpansQuery({
  322. eventView: extrapolationMetaEventView,
  323. initialData: [],
  324. referrer: 'api.explore.spans-extrapolation-meta',
  325. enabled:
  326. (defined(isAllowedSelection) ? isAllowedSelection : true) &&
  327. dataset === DiscoverDatasets.SPANS_EAP_RPC,
  328. trackResponseAnalytics: false,
  329. });
  330. }
  331. const ChartList = styled('div')`
  332. display: grid;
  333. row-gap: ${space(2)};
  334. margin-bottom: ${space(2)};
  335. `;
  336. const ChartLabel = styled('div')`
  337. background-color: ${p => p.theme.purple100};
  338. border-radius: ${p => p.theme.borderRadius};
  339. text-align: center;
  340. min-width: 32px;
  341. color: ${p => p.theme.purple400};
  342. white-space: nowrap;
  343. font-weight: ${p => p.theme.fontWeightBold};
  344. align-content: center;
  345. margin-right: ${space(1)};
  346. `;
  347. const ChartTitle = styled('div')`
  348. display: flex;
  349. margin-left: ${space(2)};
  350. `;