profilesChartWidget.tsx 3.6 KB

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