useMetricSamples.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import {useCallback, useEffect, useMemo, useState} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import moment from 'moment';
  4. import {Series} from 'sentry/types/echarts';
  5. import {getDuration} from 'sentry/utils/formatters';
  6. import {isCumulativeOp, MetricCorrelation} from 'sentry/utils/metrics';
  7. import {fitToValueRect, getValueRect} from 'sentry/views/ddm/rect';
  8. import {Sample} from 'sentry/views/ddm/widget';
  9. type UseMetricSamplesProps = {
  10. timeseries: Series[];
  11. chartRef?: any;
  12. correlations?: MetricCorrelation[];
  13. highlightedSampleId?: string;
  14. onClick?: (sample: Sample) => void;
  15. onMouseOut?: (sample: Sample) => void;
  16. onMouseOver?: (sample: Sample) => void;
  17. operation?: string;
  18. };
  19. function getDateRange(timeseries: Series[]) {
  20. if (!timeseries?.length) {
  21. return {min: -Infinity, max: Infinity};
  22. }
  23. const min = timeseries[0].data[0].name as number;
  24. const max = timeseries[0].data[timeseries[0].data.length - 1].name as number;
  25. return {min, max};
  26. }
  27. export function useMetricSamples({
  28. correlations,
  29. onClick,
  30. highlightedSampleId,
  31. chartRef,
  32. operation,
  33. timeseries,
  34. }: UseMetricSamplesProps) {
  35. const theme = useTheme();
  36. const [valueRect, setValueRect] = useState(getValueRect(chartRef));
  37. const samples: Record<string, any> = useMemo(() => {
  38. return (correlations ?? [])
  39. ?.flatMap(correlation => [
  40. ...correlation.metricSummaries.map(summaries => ({...summaries, ...correlation})),
  41. ])
  42. .reduce((acc, sample) => {
  43. acc[sample.transactionId] = sample;
  44. return acc;
  45. }, {});
  46. }, [correlations]);
  47. useEffect(() => {
  48. // Changes in timeseries change the valueRect since the timeseries yAxis auto scales
  49. // and scatter yAxis needs to match the scale
  50. setValueRect(getValueRect(chartRef));
  51. }, [chartRef, timeseries]);
  52. const xAxis = useMemo(() => {
  53. const {min, max} = getDateRange(timeseries);
  54. return {
  55. id: 'xAxisScatter',
  56. scale: false,
  57. show: false,
  58. axisLabel: {
  59. formatter: () => {
  60. return '';
  61. },
  62. },
  63. min: Math.max(valueRect.xMin, min),
  64. max: Math.min(valueRect.xMax, max),
  65. };
  66. }, [valueRect.xMin, valueRect.xMax, timeseries]);
  67. const yAxis = useMemo(() => {
  68. return {
  69. id: 'yAxisScatter',
  70. scale: false,
  71. show: false,
  72. axisLabel: {
  73. formatter: () => {
  74. return '';
  75. },
  76. },
  77. min: valueRect.yMin,
  78. max: valueRect.yMax,
  79. };
  80. }, [valueRect.yMin, valueRect.yMax]);
  81. const getSample = useCallback(
  82. event => {
  83. return samples?.[event.seriesName] as Sample;
  84. },
  85. [samples]
  86. );
  87. const handleClick = useCallback(
  88. event => {
  89. if (!onClick) {
  90. return;
  91. }
  92. const sample = getSample(event);
  93. if (!sample) {
  94. return;
  95. }
  96. onClick(sample);
  97. },
  98. [getSample, onClick]
  99. );
  100. const series = useMemo(() => {
  101. if (isCumulativeOp(operation)) {
  102. // TODO: for now we do not show samples for cumulative operations,
  103. // we will implement them as marklines
  104. return [];
  105. }
  106. return Object.values(samples).map(sample => {
  107. const isHighlighted = highlightedSampleId === sample.transactionId;
  108. const xValue = moment(sample.timestamp).valueOf();
  109. const yValue = (sample.min + sample.max) / 2;
  110. const [xPosition, yPosition] = fitToValueRect(xValue, yValue, valueRect);
  111. const symbol = yPosition === yValue ? 'circle' : 'arrow';
  112. const symbolRotate = yPosition > yValue ? 180 : 0;
  113. return {
  114. seriesName: sample.transactionId,
  115. unit: '',
  116. symbolSize: isHighlighted ? 20 : 10,
  117. animation: false,
  118. symbol,
  119. symbolRotate,
  120. color: theme.purple400,
  121. // TODO: for now we just pass these ids through, but we should probably index
  122. // samples by an id and then just pass that reference
  123. transactionId: sample.transactionId,
  124. spanId: sample.spanId,
  125. projectId: sample.projectId,
  126. itemStyle: {
  127. color: theme.purple400,
  128. opacity: isHighlighted ? 0.9 : 0.75,
  129. },
  130. yAxisIndex: 1,
  131. xAxisIndex: 1,
  132. xValue,
  133. yValue,
  134. data: [
  135. {
  136. name: xPosition,
  137. value: yPosition,
  138. },
  139. ],
  140. z: 10,
  141. };
  142. });
  143. }, [samples, highlightedSampleId, theme.purple400, valueRect, operation]);
  144. const formatters = useMemo(() => {
  145. return {
  146. isGroupedByDate: true,
  147. limit: 1,
  148. showTimeInTooltip: true,
  149. addSecondsToTimeFormat: true,
  150. nameFormatter: (name: string) => {
  151. return name.substring(0, 8);
  152. },
  153. valueFormatter: (_, label?: string) => {
  154. const sample = samples[label ?? ''];
  155. return getDuration(sample.duration / 1000, 2, true);
  156. },
  157. };
  158. }, [samples]);
  159. return {
  160. handleClick,
  161. series,
  162. xAxis,
  163. yAxis,
  164. formatters,
  165. };
  166. }