profilesChartWidget.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. import {ReactNode, useMemo} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import {AreaChart} from 'sentry/components/charts/areaChart';
  4. import ChartZoom from 'sentry/components/charts/chartZoom';
  5. import {t} from 'sentry/locale';
  6. import {PageFilters} from 'sentry/types';
  7. import {Series} from 'sentry/types/echarts';
  8. import {axisLabelFormatter, tooltipFormatter} from 'sentry/utils/discover/charts';
  9. import {useProfileEventsStats} from 'sentry/utils/profiling/hooks/useProfileEventsStats';
  10. import useRouter from 'sentry/utils/useRouter';
  11. import {
  12. ContentContainer,
  13. HeaderContainer,
  14. HeaderTitleLegend,
  15. WidgetContainer,
  16. } from './styles';
  17. interface ProfilesChartWidgetProps {
  18. chartHeight: number;
  19. referrer: string;
  20. header?: ReactNode;
  21. selection?: PageFilters;
  22. userQuery?: string;
  23. widgetHeight?: string;
  24. }
  25. const SERIES_ORDER = ['p99()', 'p95()', 'p75()', 'p50()'] as const;
  26. export function ProfilesChartWidget({
  27. chartHeight,
  28. header,
  29. referrer,
  30. selection,
  31. userQuery,
  32. widgetHeight,
  33. }: ProfilesChartWidgetProps) {
  34. const router = useRouter();
  35. const theme = useTheme();
  36. const profileStats = useProfileEventsStats({
  37. dataset: 'profiles',
  38. query: userQuery,
  39. referrer,
  40. yAxes: SERIES_ORDER,
  41. });
  42. const series: Series[] = useMemo(() => {
  43. if (profileStats.status !== 'success') {
  44. return [];
  45. }
  46. // the timestamps in the response is in seconds but echarts expects
  47. // a timestamp in milliseconds, so multiply by 1e3 to do the conversion
  48. const timestamps = profileStats.data.timestamps.map(ts => ts * 1e3);
  49. return profileStats.data.data
  50. .map(rawData => {
  51. if (timestamps.length !== rawData.values.length) {
  52. throw new Error('Invalid stats response');
  53. }
  54. return {
  55. data: rawData.values.map((value, i) => ({
  56. name: timestamps[i]!,
  57. // the response value contains nulls when no data
  58. // is available, use 0 to represent it
  59. value: value ?? 0,
  60. })),
  61. seriesName: rawData.axis,
  62. };
  63. })
  64. .sort((a, b) => {
  65. const idxA = SERIES_ORDER.indexOf(a.seriesName as any);
  66. const idxB = SERIES_ORDER.indexOf(b.seriesName as any);
  67. return idxA - idxB;
  68. });
  69. }, [profileStats]);
  70. const chartOptions = useMemo(() => {
  71. return {
  72. height: chartHeight,
  73. grid: {
  74. top: '16px',
  75. left: '24px',
  76. right: '24px',
  77. bottom: '16px',
  78. },
  79. xAxis: {
  80. type: 'time' as const,
  81. },
  82. yAxis: {
  83. scale: true,
  84. axisLabel: {
  85. color: theme.chartLabel,
  86. formatter(value: number) {
  87. return axisLabelFormatter(value, 'duration');
  88. },
  89. },
  90. },
  91. tooltip: {
  92. valueFormatter: value => tooltipFormatter(value, 'duration'),
  93. },
  94. legend: {
  95. right: 16,
  96. top: 0,
  97. data: SERIES_ORDER.slice(),
  98. },
  99. };
  100. }, [chartHeight, theme.chartLabel]);
  101. return (
  102. <WidgetContainer height={widgetHeight}>
  103. <HeaderContainer>
  104. {header ?? <HeaderTitleLegend>{t('Profiles by Percentiles')}</HeaderTitleLegend>}
  105. </HeaderContainer>
  106. <ContentContainer>
  107. <ChartZoom router={router} {...selection?.datetime}>
  108. {zoomRenderProps => (
  109. <AreaChart
  110. {...zoomRenderProps}
  111. {...chartOptions}
  112. series={series}
  113. isGroupedByDate
  114. showTimeInTooltip
  115. />
  116. )}
  117. </ChartZoom>
  118. </ContentContainer>
  119. </WidgetContainer>
  120. );
  121. }