profilesChartWidget.tsx 3.7 KB

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