index.tsx 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. import {Component, Fragment} from 'react';
  2. import {InjectedRouter} from 'react-router';
  3. import {withTheme} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import {Location} from 'history';
  6. import {Client} from 'app/api';
  7. import GuideAnchor from 'app/components/assistant/guideAnchor';
  8. import EventsChart from 'app/components/charts/eventsChart';
  9. import {ChartContainer, HeaderTitleLegend} from 'app/components/charts/styles';
  10. import {Panel} from 'app/components/panels';
  11. import QuestionTooltip from 'app/components/questionTooltip';
  12. import {PlatformKey} from 'app/data/platformCategories';
  13. import {t} from 'app/locale';
  14. import {GlobalSelection, Organization, ReleaseMeta} from 'app/types';
  15. import {Series} from 'app/types/echarts';
  16. import {trackAnalyticsEvent} from 'app/utils/analytics';
  17. import {WebVital} from 'app/utils/discover/fields';
  18. import {decodeScalar} from 'app/utils/queryString';
  19. import {Theme} from 'app/utils/theme';
  20. import {getTermHelp, PERFORMANCE_TERM} from 'app/views/performance/data';
  21. import ReleaseStatsRequest from '../releaseStatsRequest';
  22. import HealthChartContainer from './healthChartContainer';
  23. import ReleaseChartControls, {
  24. EventType,
  25. PERFORMANCE_AXIS,
  26. YAxis,
  27. } from './releaseChartControls';
  28. import {getReleaseEventView} from './utils';
  29. type Props = {
  30. releaseMeta: ReleaseMeta;
  31. selection: GlobalSelection;
  32. platform: PlatformKey;
  33. yAxis: YAxis;
  34. eventType: EventType;
  35. vitalType: WebVital;
  36. onYAxisChange: (yAxis: YAxis) => void;
  37. onEventTypeChange: (eventType: EventType) => void;
  38. onVitalTypeChange: (vitalType: WebVital) => void;
  39. router: InjectedRouter;
  40. organization: Organization;
  41. hasHealthData: boolean;
  42. location: Location;
  43. api: Client;
  44. version: string;
  45. hasDiscover: boolean;
  46. hasPerformance: boolean;
  47. theme: Theme;
  48. defaultStatsPeriod: string;
  49. projectSlug: string;
  50. };
  51. class ReleaseChartContainer extends Component<Props> {
  52. componentDidMount() {
  53. const {organization, yAxis, platform} = this.props;
  54. trackAnalyticsEvent({
  55. eventKey: `release_detail.display_chart`,
  56. eventName: `Release Detail: Display Chart`,
  57. organization_id: parseInt(organization.id, 10),
  58. display: yAxis,
  59. platform,
  60. });
  61. }
  62. /**
  63. * This returns an array with 3 colors, one for each of
  64. * 1. This Release
  65. * 2. Other Releases
  66. * 3. Releases (the markers)
  67. */
  68. getTransactionsChartColors(): [string, string, string] {
  69. const {yAxis, theme} = this.props;
  70. switch (yAxis) {
  71. case YAxis.FAILED_TRANSACTIONS:
  72. return [theme.red300, theme.red100, theme.purple300];
  73. default:
  74. return [theme.purple300, theme.purple100, theme.purple300];
  75. }
  76. }
  77. getChartTitle() {
  78. const {yAxis, organization} = this.props;
  79. switch (yAxis) {
  80. case YAxis.SESSIONS:
  81. return {
  82. title: t('Session Count'),
  83. help: t('The number of sessions in a given period.'),
  84. };
  85. case YAxis.USERS:
  86. return {
  87. title: t('User Count'),
  88. help: t('The number of users in a given period.'),
  89. };
  90. case YAxis.SESSION_DURATION:
  91. return {title: t('Session Duration')};
  92. case YAxis.CRASH_FREE:
  93. return {title: t('Crash Free Rate')};
  94. case YAxis.FAILED_TRANSACTIONS:
  95. return {
  96. title: t('Failure Count'),
  97. help: getTermHelp(organization, PERFORMANCE_TERM.FAILURE_RATE),
  98. };
  99. case YAxis.COUNT_DURATION:
  100. return {title: t('Slow Duration Count')};
  101. case YAxis.COUNT_VITAL:
  102. return {title: t('Slow Vital Count')};
  103. case YAxis.EVENTS:
  104. default:
  105. return {title: t('Event Count')};
  106. }
  107. }
  108. cloneSeriesAsZero(series: Series): Series {
  109. return {
  110. ...series,
  111. data: series.data.map(point => ({
  112. ...point,
  113. value: 0,
  114. })),
  115. };
  116. }
  117. /**
  118. * The top events endpoint used to generate these series is not guaranteed to return a series
  119. * for both the current release and the other releases. This happens when there is insufficient
  120. * data. In these cases, the endpoint will return a single zerofilled series for the current
  121. * release.
  122. *
  123. * This is problematic as we want to show both series even if one is empty. To deal with this,
  124. * we clone the non empty series (to preserve the timestamps) with value 0 (to represent the
  125. * lack of data).
  126. */
  127. seriesTransformer = (series: Series[]): Series[] => {
  128. let current: Series | null = null;
  129. let others: Series | null = null;
  130. const allSeries: Series[] = [];
  131. series.forEach(s => {
  132. if (s.seriesName === 'current' || s.seriesName === t('This Release')) {
  133. current = s;
  134. } else if (s.seriesName === 'others' || s.seriesName === t('Other Releases')) {
  135. others = s;
  136. } else {
  137. allSeries.push(s);
  138. }
  139. });
  140. if (current !== null && others === null) {
  141. others = this.cloneSeriesAsZero(current);
  142. } else if (current === null && others !== null) {
  143. current = this.cloneSeriesAsZero(others);
  144. }
  145. if (others !== null) {
  146. others.seriesName = t('Other Releases');
  147. allSeries.unshift(others);
  148. }
  149. if (current !== null) {
  150. current.seriesName = t('This Release');
  151. allSeries.unshift(current);
  152. }
  153. return allSeries;
  154. };
  155. renderStackedChart() {
  156. const {
  157. location,
  158. router,
  159. organization,
  160. api,
  161. releaseMeta,
  162. yAxis,
  163. eventType,
  164. vitalType,
  165. selection,
  166. version,
  167. } = this.props;
  168. const {projects, environments, datetime} = selection;
  169. const {start, end, period, utc} = datetime;
  170. const eventView = getReleaseEventView(
  171. selection,
  172. version,
  173. yAxis,
  174. eventType,
  175. vitalType,
  176. organization
  177. );
  178. const apiPayload = eventView.getEventsAPIPayload(location);
  179. const colors = this.getTransactionsChartColors();
  180. const {title, help} = this.getChartTitle();
  181. const releaseQueryExtra = {
  182. showTransactions: location.query.showTransactions,
  183. eventType,
  184. vitalType,
  185. yAxis,
  186. };
  187. return (
  188. <EventsChart
  189. router={router}
  190. organization={organization}
  191. showLegend
  192. yAxis={eventView.getYAxis()}
  193. query={apiPayload.query}
  194. api={api}
  195. projects={projects}
  196. environments={environments}
  197. start={start}
  198. end={end}
  199. period={period}
  200. utc={utc}
  201. disablePrevious
  202. emphasizeReleases={[releaseMeta.version]}
  203. field={eventView.getFields()}
  204. topEvents={2}
  205. orderby={decodeScalar(apiPayload.sort)}
  206. currentSeriesName={t('This Release')}
  207. // This seems a little strange but is intentional as EventsChart
  208. // uses the previousSeriesName as the secondary series name
  209. previousSeriesName={t('Other Releases')}
  210. seriesTransformer={this.seriesTransformer}
  211. disableableSeries={[t('This Release'), t('Other Releases')]}
  212. colors={colors}
  213. preserveReleaseQueryParams
  214. releaseQueryExtra={releaseQueryExtra}
  215. chartHeader={
  216. <HeaderTitleLegend>
  217. {title}
  218. {help && <QuestionTooltip size="sm" position="top" title={help} />}
  219. </HeaderTitleLegend>
  220. }
  221. legendOptions={{right: 10, top: 0}}
  222. chartOptions={{grid: {left: '10px', right: '10px', top: '40px', bottom: '0px'}}}
  223. />
  224. );
  225. }
  226. renderHealthChart(
  227. loading: boolean,
  228. reloading: boolean,
  229. errored: boolean,
  230. chartData: Series[]
  231. ) {
  232. const {selection, yAxis, router, platform} = this.props;
  233. const {title, help} = this.getChartTitle();
  234. return (
  235. <HealthChartContainer
  236. platform={platform}
  237. loading={loading}
  238. errored={errored}
  239. reloading={reloading}
  240. chartData={chartData}
  241. selection={selection}
  242. yAxis={yAxis}
  243. router={router}
  244. title={title}
  245. help={help}
  246. />
  247. );
  248. }
  249. render() {
  250. const {
  251. yAxis,
  252. eventType,
  253. vitalType,
  254. hasDiscover,
  255. hasHealthData,
  256. hasPerformance,
  257. onYAxisChange,
  258. onEventTypeChange,
  259. onVitalTypeChange,
  260. organization,
  261. defaultStatsPeriod,
  262. api,
  263. version,
  264. selection,
  265. location,
  266. projectSlug,
  267. } = this.props;
  268. return (
  269. <ReleaseStatsRequest
  270. api={api}
  271. organization={organization}
  272. projectSlug={projectSlug}
  273. version={version}
  274. selection={selection}
  275. location={location}
  276. yAxis={yAxis}
  277. eventType={eventType}
  278. vitalType={vitalType}
  279. hasHealthData={hasHealthData}
  280. hasDiscover={hasDiscover}
  281. hasPerformance={hasPerformance}
  282. defaultStatsPeriod={defaultStatsPeriod}
  283. >
  284. {({loading, reloading, errored, chartData, chartSummary}) => (
  285. <Panel>
  286. <ChartContainer>
  287. {((hasDiscover || hasPerformance) && yAxis === YAxis.EVENTS) ||
  288. (hasPerformance && PERFORMANCE_AXIS.includes(yAxis))
  289. ? this.renderStackedChart()
  290. : this.renderHealthChart(loading, reloading, errored, chartData)}
  291. </ChartContainer>
  292. <AnchorWrapper>
  293. <GuideAnchor target="release_chart" position="bottom" offset="-80px">
  294. <Fragment />
  295. </GuideAnchor>
  296. </AnchorWrapper>
  297. <ReleaseChartControls
  298. summary={chartSummary}
  299. yAxis={yAxis}
  300. onYAxisChange={onYAxisChange}
  301. eventType={eventType}
  302. onEventTypeChange={onEventTypeChange}
  303. vitalType={vitalType}
  304. onVitalTypeChange={onVitalTypeChange}
  305. organization={organization}
  306. hasDiscover={hasDiscover}
  307. hasHealthData={hasHealthData}
  308. hasPerformance={hasPerformance}
  309. />
  310. </Panel>
  311. )}
  312. </ReleaseStatsRequest>
  313. );
  314. }
  315. }
  316. export default withTheme(ReleaseChartContainer);
  317. const AnchorWrapper = styled('div')`
  318. height: 0;
  319. width: 0;
  320. margin-left: 50%;
  321. `;