useMetricSamples.tsx 5.1 KB

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