profilesChartWidget.tsx 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  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. Subtitle,
  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. 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[0].timestamps.map(ts => ts * 1e3);
  49. return profileStats.data[0].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: 12,
  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. <Subtitle>{t('P50(), P75(), P95(), P99() over time')}</Subtitle>
  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. }