releaseEventsChart.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import {Fragment} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import type {ToolboxComponentOption} from 'echarts';
  4. import {Client} from 'sentry/api';
  5. import EventsChart from 'sentry/components/charts/eventsChart';
  6. import EventsRequest from 'sentry/components/charts/eventsRequest';
  7. import {HeaderTitleLegend, HeaderValue} from 'sentry/components/charts/styles';
  8. import {getInterval} from 'sentry/components/charts/utils';
  9. import QuestionTooltip from 'sentry/components/questionTooltip';
  10. import {t} from 'sentry/locale';
  11. import {
  12. Organization,
  13. ReleaseComparisonChartType,
  14. ReleaseProject,
  15. ReleaseWithHealth,
  16. } from 'sentry/types';
  17. import {tooltipFormatter} from 'sentry/utils/discover/charts';
  18. import EventView from 'sentry/utils/discover/eventView';
  19. import {aggregateOutputType} from 'sentry/utils/discover/fields';
  20. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  21. import useApi from 'sentry/utils/useApi';
  22. import {useLocation} from 'sentry/utils/useLocation';
  23. import useRouter from 'sentry/utils/useRouter';
  24. import withOrganization from 'sentry/utils/withOrganization';
  25. import {getTermHelp, PerformanceTerm} from 'sentry/views/performance/data';
  26. import {
  27. generateReleaseMarkLines,
  28. releaseComparisonChartTitles,
  29. releaseMarkLinesLabels,
  30. } from '../../utils';
  31. type Props = {
  32. chartType: ReleaseComparisonChartType;
  33. diff: React.ReactNode;
  34. organization: Organization;
  35. project: ReleaseProject;
  36. release: ReleaseWithHealth;
  37. value: React.ReactNode;
  38. end?: string;
  39. period?: string | null;
  40. start?: string;
  41. utc?: boolean;
  42. };
  43. function ReleaseEventsChart({
  44. release,
  45. project,
  46. chartType,
  47. value,
  48. diff,
  49. organization,
  50. period,
  51. start,
  52. end,
  53. utc,
  54. }: Props) {
  55. const location = useLocation();
  56. const router = useRouter();
  57. const api = useApi();
  58. const theme = useTheme();
  59. function getColors() {
  60. const colors = theme.charts.getColorPalette(14);
  61. switch (chartType) {
  62. case ReleaseComparisonChartType.ERROR_COUNT:
  63. return [colors[12]];
  64. case ReleaseComparisonChartType.TRANSACTION_COUNT:
  65. return [colors[0]];
  66. case ReleaseComparisonChartType.FAILURE_RATE:
  67. return [colors[9]];
  68. default:
  69. return undefined;
  70. }
  71. }
  72. function getQuery() {
  73. const releaseFilter = `release:${release.version}`;
  74. switch (chartType) {
  75. case ReleaseComparisonChartType.ERROR_COUNT:
  76. return new MutableSearch(['event.type:error', releaseFilter]).formatString();
  77. case ReleaseComparisonChartType.TRANSACTION_COUNT:
  78. return new MutableSearch([
  79. 'event.type:transaction',
  80. releaseFilter,
  81. ]).formatString();
  82. case ReleaseComparisonChartType.FAILURE_RATE:
  83. return new MutableSearch([
  84. 'event.type:transaction',
  85. releaseFilter,
  86. ]).formatString();
  87. default:
  88. return '';
  89. }
  90. }
  91. function getField() {
  92. switch (chartType) {
  93. case ReleaseComparisonChartType.ERROR_COUNT:
  94. return ['count()'];
  95. case ReleaseComparisonChartType.TRANSACTION_COUNT:
  96. return ['count()'];
  97. case ReleaseComparisonChartType.FAILURE_RATE:
  98. return ['failure_rate()'];
  99. default:
  100. return undefined;
  101. }
  102. }
  103. function getYAxis() {
  104. switch (chartType) {
  105. case ReleaseComparisonChartType.ERROR_COUNT:
  106. return 'count()';
  107. case ReleaseComparisonChartType.TRANSACTION_COUNT:
  108. return 'count()';
  109. case ReleaseComparisonChartType.FAILURE_RATE:
  110. return 'failure_rate()';
  111. default:
  112. return '';
  113. }
  114. }
  115. function getHelp() {
  116. switch (chartType) {
  117. case ReleaseComparisonChartType.FAILURE_RATE:
  118. return getTermHelp(organization, PerformanceTerm.FAILURE_RATE);
  119. default:
  120. return null;
  121. }
  122. }
  123. const eventView = EventView.fromSavedQueryOrLocation(undefined, location);
  124. const projects = eventView.project as number[];
  125. const environments = eventView.environment as string[];
  126. const markLines = generateReleaseMarkLines(release, project, theme, location);
  127. return (
  128. /**
  129. * EventsRequest is used to fetch the second series of Failure Rate chart.
  130. * First one is "This Release" - fetched as usual inside EventsChart
  131. * component and this one is "All Releases" that's shoehorned in place
  132. * of Previous Period via previousSeriesTransformer
  133. */
  134. <EventsRequest
  135. organization={organization}
  136. api={new Client()}
  137. period={period}
  138. project={projects}
  139. environment={environments}
  140. start={start}
  141. end={end}
  142. interval={getInterval({start, end, period, utc}, 'high')}
  143. query="event.type:transaction"
  144. includePrevious={false}
  145. currentSeriesNames={[t('All Releases')]}
  146. yAxis={getYAxis()}
  147. field={getField()}
  148. confirmedQuery={chartType === ReleaseComparisonChartType.FAILURE_RATE}
  149. partial
  150. referrer="api.releases.release-details-chart"
  151. >
  152. {({timeseriesData, loading, reloading}) => (
  153. <EventsChart
  154. query={getQuery()}
  155. yAxis={getYAxis()}
  156. field={getField()}
  157. colors={getColors()}
  158. api={api}
  159. router={router}
  160. organization={organization}
  161. disableReleases
  162. disablePrevious
  163. showLegend
  164. projects={projects}
  165. environments={environments}
  166. start={start ?? null}
  167. end={end ?? null}
  168. period={period}
  169. utc={utc}
  170. currentSeriesName={t('This Release') + (loading || reloading ? ' ' : '')} // HACK: trigger echarts rerender without remounting
  171. previousSeriesName={t('All Releases')}
  172. disableableSeries={[t('This Release'), t('All Releases')]}
  173. chartHeader={
  174. <Fragment>
  175. <HeaderTitleLegend>
  176. {releaseComparisonChartTitles[chartType]}
  177. {getHelp() && (
  178. <QuestionTooltip size="sm" position="top" title={getHelp()} />
  179. )}
  180. </HeaderTitleLegend>
  181. <HeaderValue>
  182. {value} {diff}
  183. </HeaderValue>
  184. </Fragment>
  185. }
  186. legendOptions={{
  187. right: 10,
  188. top: 0,
  189. textStyle: {
  190. padding: [2, 0, 0, 0],
  191. },
  192. }}
  193. chartOptions={{
  194. grid: {left: '10px', right: '10px', top: '70px', bottom: '0px'},
  195. tooltip: {
  196. trigger: 'axis',
  197. truncate: 80,
  198. valueFormatter: (val: number, label?: string) => {
  199. if (label && Object.values(releaseMarkLinesLabels).includes(label)) {
  200. return '';
  201. }
  202. return tooltipFormatter(val, aggregateOutputType(getYAxis()));
  203. },
  204. } as ToolboxComponentOption,
  205. }}
  206. usePageZoom
  207. height={240}
  208. seriesTransformer={series => [...series, ...markLines]}
  209. previousSeriesTransformer={series => {
  210. if (chartType === ReleaseComparisonChartType.FAILURE_RATE) {
  211. return timeseriesData?.[0];
  212. }
  213. return series;
  214. }}
  215. referrer="api.releases.release-details-chart"
  216. />
  217. )}
  218. </EventsRequest>
  219. );
  220. }
  221. export default withOrganization(ReleaseEventsChart);