releaseEventsChart.tsx 7.3 KB

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