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