releaseEventsChart.tsx 7.3 KB

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