profilesChartWidget.tsx 3.6 KB

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