useMetricStatsChart.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import {useTheme} from '@emotion/react';
  2. import type {LineSeriesOption} from 'echarts';
  3. import type {AreaChartProps} from 'sentry/components/charts/areaChart';
  4. import {
  5. transformComparisonTimeseriesData,
  6. transformTimeseriesData,
  7. } from 'sentry/components/charts/eventsRequest';
  8. import LineSeries from 'sentry/components/charts/series/lineSeries';
  9. import type {Series} from 'sentry/types/echarts';
  10. import type {EventsStats, SessionApiResponse} from 'sentry/types/organization';
  11. import type {Project} from 'sentry/types/project';
  12. import type {UseApiQueryOptions, UseApiQueryResult} from 'sentry/utils/queryClient';
  13. import type RequestError from 'sentry/utils/requestError/requestError';
  14. import {MINUTES_THRESHOLD_TO_DISPLAY_SECONDS} from 'sentry/utils/sessions';
  15. import {capitalize} from 'sentry/utils/string/capitalize';
  16. import {COMPARISON_DELTA_OPTIONS} from 'sentry/views/alerts/rules/metric/constants';
  17. import type {TimePeriodType} from 'sentry/views/alerts/rules/metric/details/constants';
  18. import {
  19. getMetricChartTooltipFormatter,
  20. getRuleChangeSeries,
  21. } from 'sentry/views/alerts/rules/metric/details/metricChart';
  22. import {
  23. getMetricAlertChartOption,
  24. transformSessionResponseToSeries,
  25. } from 'sentry/views/alerts/rules/metric/details/metricChartOption';
  26. import {getPeriodInterval} from 'sentry/views/alerts/rules/metric/details/utils';
  27. import type {MetricRule} from 'sentry/views/alerts/rules/metric/types';
  28. import {isCrashFreeAlert} from 'sentry/views/alerts/rules/metric/utils/isCrashFreeAlert';
  29. import type {Anomaly, Incident} from 'sentry/views/alerts/types';
  30. import {useMetricEventStats} from 'sentry/views/issueDetails/metricIssues/useMetricEventStats';
  31. import {useMetricSessionStats} from 'sentry/views/issueDetails/metricIssues/useMetricSessionStats';
  32. interface MetricStatsParams {
  33. project: Project;
  34. referrer: string;
  35. rule: MetricRule;
  36. timePeriod: TimePeriodType;
  37. anomalies?: Anomaly[];
  38. incidents?: Incident[];
  39. }
  40. type MetricStatsResponse = SessionApiResponse | EventsStats | undefined;
  41. interface MetricStatsResult {
  42. chartProps: Partial<AreaChartProps>;
  43. data: UseApiQueryResult<MetricStatsResponse, RequestError>['data'];
  44. resultType: 'sessions' | 'events';
  45. queryResult?: Partial<UseApiQueryResult<MetricStatsResponse, RequestError>>;
  46. }
  47. /**
  48. * Helper hook to coerce any rule into returning a series data response, whether it is a session or event rule.
  49. * Returns a similar response to a useApiQuery hook,
  50. */
  51. export function useMetricStatsChart(
  52. {
  53. project,
  54. rule,
  55. timePeriod,
  56. referrer,
  57. anomalies = [],
  58. incidents = [],
  59. }: MetricStatsParams,
  60. options: Partial<UseApiQueryOptions<MetricStatsResponse>> = {}
  61. ): MetricStatsResult {
  62. const theme = useTheme();
  63. const shouldUseSessionsStats = isCrashFreeAlert(rule.dataset);
  64. const interval = getPeriodInterval(timePeriod, rule);
  65. const {data: sessionStats, ...sessionStatsResults} = useMetricSessionStats(
  66. {project, rule, timePeriod},
  67. {
  68. enabled: shouldUseSessionsStats,
  69. ...(options as Partial<UseApiQueryOptions<SessionApiResponse>>),
  70. }
  71. );
  72. const {data: eventStats, ...eventStatsResults} = useMetricEventStats(
  73. {project, rule, timePeriod, referrer},
  74. {
  75. enabled: !shouldUseSessionsStats,
  76. ...(options as Partial<UseApiQueryOptions<EventsStats>>),
  77. }
  78. );
  79. let stats: Series[] = [];
  80. if (shouldUseSessionsStats && sessionStats) {
  81. stats = transformSessionResponseToSeries(sessionStats, rule);
  82. } else if (eventStats) {
  83. stats = transformTimeseriesData(eventStats.data, eventStats?.meta, rule.aggregate);
  84. }
  85. let comparisonData: Series[] = [];
  86. if (rule.comparisonDelta) {
  87. comparisonData = transformComparisonTimeseriesData(eventStats?.data ?? []);
  88. }
  89. let chartProps: Partial<AreaChartProps> = {};
  90. if (stats.length > 0) {
  91. const {chartOption} = getMetricAlertChartOption({
  92. timeseriesData: stats,
  93. rule,
  94. anomalies,
  95. incidents,
  96. seriesName: rule.aggregate,
  97. });
  98. chartProps = {...chartOption};
  99. }
  100. const comparisonSeriesName = capitalize(
  101. COMPARISON_DELTA_OPTIONS.find(({value}) => value === rule.comparisonDelta)?.label ||
  102. ''
  103. );
  104. const additionalSeries: LineSeriesOption[] = [
  105. ...(comparisonData || []).map(({data: _data, ...otherSeriesProps}) =>
  106. LineSeries({
  107. name: comparisonSeriesName,
  108. data: _data.map(({name, value}) => [name, value]),
  109. lineStyle: {color: theme.gray200, type: 'dashed', width: 1},
  110. itemStyle: {color: theme.gray200},
  111. animation: false,
  112. animationThreshold: 1,
  113. animationDuration: 0,
  114. ...otherSeriesProps,
  115. })
  116. ),
  117. ...getRuleChangeSeries(rule, stats, theme),
  118. ];
  119. return {
  120. chartProps: {
  121. minutesThresholdToDisplaySeconds: shouldUseSessionsStats
  122. ? MINUTES_THRESHOLD_TO_DISPLAY_SECONDS
  123. : undefined,
  124. additionalSeries,
  125. tooltip: getMetricChartTooltipFormatter({
  126. formattedAggregate: rule.aggregate,
  127. rule,
  128. interval,
  129. comparisonSeriesName,
  130. theme,
  131. }),
  132. ...chartProps,
  133. },
  134. resultType: shouldUseSessionsStats ? 'sessions' : 'events',
  135. data: shouldUseSessionsStats ? sessionStats : eventStats,
  136. queryResult: shouldUseSessionsStats ? sessionStatsResults : eventStatsResults,
  137. };
  138. }