profileCharts.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import {useMemo} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {AreaChart, AreaChartProps} from 'sentry/components/charts/areaChart';
  5. import ChartZoom from 'sentry/components/charts/chartZoom';
  6. import {LineChartProps} from 'sentry/components/charts/lineChart';
  7. import {HeaderTitle} from 'sentry/components/charts/styles';
  8. import Panel from 'sentry/components/panels/panel';
  9. import {t} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import {PageFilters} from 'sentry/types';
  12. import {Series} from 'sentry/types/echarts';
  13. import {axisLabelFormatter, tooltipFormatter} from 'sentry/utils/discover/charts';
  14. import {aggregateOutputType} from 'sentry/utils/discover/fields';
  15. import {useProfileEventsStats} from 'sentry/utils/profiling/hooks/useProfileEventsStats';
  16. import useRouter from 'sentry/utils/useRouter';
  17. // We want p99 to be before p75 because echarts renders the series in order.
  18. // So if p75 is before p99, p99 will be rendered on top of p75 which will
  19. // cover it up.
  20. const SERIES_ORDER = ['count()', 'p99()', 'p95()', 'p75()'] as const;
  21. interface ProfilesChartProps {
  22. query: string;
  23. referrer: string;
  24. compact?: boolean;
  25. hideCount?: boolean;
  26. selection?: PageFilters;
  27. }
  28. export function ProfilesChart({
  29. query,
  30. referrer,
  31. selection,
  32. hideCount,
  33. compact = false,
  34. }: ProfilesChartProps) {
  35. const router = useRouter();
  36. const theme = useTheme();
  37. const seriesOrder = useMemo(() => {
  38. if (hideCount) {
  39. return SERIES_ORDER.filter(s => s !== 'count()');
  40. }
  41. return SERIES_ORDER;
  42. }, [hideCount]);
  43. const profileStats = useProfileEventsStats({
  44. dataset: 'profiles',
  45. query,
  46. referrer,
  47. yAxes: seriesOrder,
  48. });
  49. const series: Series[] = useMemo(() => {
  50. if (profileStats.status !== 'success') {
  51. return [];
  52. }
  53. // the timestamps in the response is in seconds but echarts expects
  54. // a timestamp in milliseconds, so multiply by 1e3 to do the conversion
  55. const timestamps = profileStats.data.timestamps.map(ts => ts * 1e3);
  56. const allSeries = profileStats.data.data
  57. .filter(rawData => seriesOrder.includes(rawData.axis))
  58. .map(rawData => {
  59. if (timestamps.length !== rawData.values.length) {
  60. throw new Error('Invalid stats response');
  61. }
  62. if (rawData.axis === 'count()') {
  63. return {
  64. data: rawData.values.map((value, i) => ({
  65. name: timestamps[i]!,
  66. // the response value contains nulls when no data is
  67. // available, use 0 to represent it
  68. value: value ?? 0,
  69. })),
  70. seriesName: rawData.axis,
  71. xAxisIndex: 0,
  72. yAxisIndex: 0,
  73. };
  74. }
  75. return {
  76. data: rawData.values.map((value, i) => ({
  77. name: timestamps[i]!,
  78. // the response value contains nulls when no data
  79. // is available, use 0 to represent it
  80. value: value ?? 0,
  81. })),
  82. seriesName: rawData.axis,
  83. xAxisIndex: 1,
  84. yAxisIndex: 1,
  85. };
  86. });
  87. allSeries.sort((a, b) => {
  88. const idxA = seriesOrder.indexOf(a.seriesName as any);
  89. const idxB = seriesOrder.indexOf(b.seriesName as any);
  90. return idxA - idxB;
  91. });
  92. return allSeries;
  93. }, [profileStats, seriesOrder]);
  94. const chartProps: LineChartProps | AreaChartProps = useMemo(() => {
  95. const baseProps: LineChartProps | AreaChartProps = {
  96. height: compact ? 150 : 300,
  97. series,
  98. grid: [
  99. {
  100. top: '32px',
  101. left: '24px',
  102. right: '52%',
  103. bottom: '16px',
  104. },
  105. {
  106. top: '32px',
  107. left: hideCount ? '24px' : '52%',
  108. right: '24px',
  109. bottom: '16px',
  110. },
  111. ],
  112. legend: {
  113. right: 16,
  114. top: 12,
  115. data: seriesOrder.slice(),
  116. },
  117. tooltip: {
  118. valueFormatter: (value, label) =>
  119. tooltipFormatter(value, aggregateOutputType(label)),
  120. },
  121. axisPointer: {
  122. link: [
  123. {
  124. xAxisIndex: [0, 1],
  125. },
  126. ],
  127. },
  128. xAxes: [
  129. {
  130. show: !hideCount,
  131. gridIndex: 0,
  132. type: 'time' as const,
  133. },
  134. {
  135. gridIndex: 1,
  136. type: 'time' as const,
  137. },
  138. ],
  139. yAxes: [
  140. {
  141. gridIndex: 0,
  142. scale: true,
  143. axisLabel: {
  144. color: theme.chartLabel,
  145. formatter(value: number) {
  146. return axisLabelFormatter(value, 'integer');
  147. },
  148. },
  149. },
  150. {
  151. gridIndex: 1,
  152. scale: true,
  153. axisLabel: {
  154. color: theme.chartLabel,
  155. formatter(value: number) {
  156. return axisLabelFormatter(value, 'duration');
  157. },
  158. },
  159. },
  160. ],
  161. };
  162. return baseProps;
  163. }, [compact, hideCount, series, seriesOrder, theme.chartLabel]);
  164. return (
  165. <ChartZoom router={router} {...selection?.datetime}>
  166. {zoomRenderProps => (
  167. <StyledPanel>
  168. <TitleContainer>
  169. {!hideCount && (
  170. <StyledHeaderTitle compact>{t('Profiles by Count')}</StyledHeaderTitle>
  171. )}
  172. <StyledHeaderTitle compact>{t('Profiles Duration')}</StyledHeaderTitle>
  173. </TitleContainer>
  174. <AreaChart
  175. {...chartProps}
  176. isGroupedByDate
  177. showTimeInTooltip
  178. {...zoomRenderProps}
  179. />
  180. </StyledPanel>
  181. )}
  182. </ChartZoom>
  183. );
  184. }
  185. const StyledPanel = styled(Panel)`
  186. padding-top: ${space(2)};
  187. `;
  188. const TitleContainer = styled('div')`
  189. width: 100%;
  190. display: flex;
  191. flex-direction: row;
  192. `;
  193. const StyledHeaderTitle = styled(HeaderTitle)<{compact?: boolean}>`
  194. flex-grow: 1;
  195. margin-left: ${space(2)};
  196. font-size: ${p => (p.compact ? p.theme.fontSizeSmall : undefined)};
  197. `;