projectCharts.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. import {Component, Fragment} from 'react';
  2. import * as ReactRouter from 'react-router';
  3. import {browserHistory} from 'react-router';
  4. import {withTheme} from '@emotion/react';
  5. import {Location} from 'history';
  6. import {Client} from 'app/api';
  7. import BarChart from 'app/components/charts/barChart';
  8. import LoadingPanel from 'app/components/charts/loadingPanel';
  9. import OptionSelector from 'app/components/charts/optionSelector';
  10. import {
  11. ChartContainer,
  12. ChartControls,
  13. InlineContainer,
  14. SectionHeading,
  15. SectionValue,
  16. } from 'app/components/charts/styles';
  17. import {
  18. getDiffInMinutes,
  19. ONE_HOUR,
  20. ONE_WEEK,
  21. TWENTY_FOUR_HOURS,
  22. TWO_WEEKS,
  23. } from 'app/components/charts/utils';
  24. import {Panel} from 'app/components/panels';
  25. import Placeholder from 'app/components/placeholder';
  26. import CHART_PALETTE from 'app/constants/chartPalette';
  27. import NOT_AVAILABLE_MESSAGES from 'app/constants/notAvailableMessages';
  28. import {t} from 'app/locale';
  29. import {Organization, SelectValue} from 'app/types';
  30. import {defined} from 'app/utils';
  31. import {trackAnalyticsEvent} from 'app/utils/analytics';
  32. import {decodeScalar} from 'app/utils/queryString';
  33. import {Theme} from 'app/utils/theme';
  34. import withApi from 'app/utils/withApi';
  35. import {
  36. getSessionTermDescription,
  37. SessionTerm,
  38. } from 'app/views/releases/utils/sessionTerm';
  39. import {getTermHelp, PERFORMANCE_TERM} from '../performance/data';
  40. import ProjectBaseEventsChart from './charts/projectBaseEventsChart';
  41. import ProjectBaseSessionsChart from './charts/projectBaseSessionsChart';
  42. import ProjectErrorsBasicChart from './charts/projectErrorsBasicChart';
  43. export enum DisplayModes {
  44. APDEX = 'apdex',
  45. FAILURE_RATE = 'failure_rate',
  46. TPM = 'tpm',
  47. ERRORS = 'errors',
  48. TRANSACTIONS = 'transactions',
  49. STABILITY = 'crash_free',
  50. SESSIONS = 'sessions',
  51. }
  52. type Props = {
  53. api: Client;
  54. location: Location;
  55. organization: Organization;
  56. router: ReactRouter.InjectedRouter;
  57. chartId: string;
  58. chartIndex: number;
  59. theme: Theme;
  60. hasSessions: boolean | null;
  61. hasTransactions: boolean;
  62. visibleCharts: string[];
  63. projectId?: string;
  64. };
  65. type State = {
  66. totalValues: number | null;
  67. };
  68. class ProjectCharts extends Component<Props, State> {
  69. state: State = {
  70. totalValues: null,
  71. };
  72. get defaultDisplayModes() {
  73. const {hasSessions, hasTransactions} = this.props;
  74. if (!hasSessions && !hasTransactions) {
  75. return [DisplayModes.ERRORS];
  76. }
  77. if (hasSessions && !hasTransactions) {
  78. return [DisplayModes.STABILITY, DisplayModes.ERRORS];
  79. }
  80. if (!hasSessions && hasTransactions) {
  81. return [DisplayModes.FAILURE_RATE, DisplayModes.APDEX];
  82. }
  83. return [DisplayModes.STABILITY, DisplayModes.APDEX];
  84. }
  85. get otherActiveDisplayModes() {
  86. const {location, visibleCharts, chartId} = this.props;
  87. return visibleCharts
  88. .filter(visibleChartId => visibleChartId !== chartId)
  89. .map(urlKey => {
  90. return decodeScalar(
  91. location.query[urlKey],
  92. this.defaultDisplayModes[visibleCharts.findIndex(value => value === urlKey)]
  93. );
  94. });
  95. }
  96. get displayMode() {
  97. const {location, chartId, chartIndex} = this.props;
  98. const displayMode =
  99. decodeScalar(location.query[chartId]) || this.defaultDisplayModes[chartIndex];
  100. if (!Object.values(DisplayModes).includes(displayMode as DisplayModes)) {
  101. return this.defaultDisplayModes[chartIndex];
  102. }
  103. return displayMode;
  104. }
  105. get displayModes(): SelectValue<string>[] {
  106. const {organization, hasSessions, hasTransactions} = this.props;
  107. const hasPerformance = organization.features.includes('performance-view');
  108. const noPerformanceTooltip = NOT_AVAILABLE_MESSAGES.performance;
  109. const noHealthTooltip = NOT_AVAILABLE_MESSAGES.releaseHealth;
  110. return [
  111. {
  112. value: DisplayModes.STABILITY,
  113. label: t('Crash Free Sessions'),
  114. disabled:
  115. this.otherActiveDisplayModes.includes(DisplayModes.STABILITY) || !hasSessions,
  116. tooltip: !hasSessions ? noHealthTooltip : undefined,
  117. },
  118. {
  119. value: DisplayModes.APDEX,
  120. label: t('Apdex'),
  121. disabled:
  122. this.otherActiveDisplayModes.includes(DisplayModes.APDEX) ||
  123. !hasPerformance ||
  124. !hasTransactions,
  125. tooltip:
  126. hasPerformance && hasTransactions
  127. ? getTermHelp(organization, PERFORMANCE_TERM.APDEX)
  128. : noPerformanceTooltip,
  129. },
  130. {
  131. value: DisplayModes.FAILURE_RATE,
  132. label: t('Failure Rate'),
  133. disabled:
  134. this.otherActiveDisplayModes.includes(DisplayModes.FAILURE_RATE) ||
  135. !hasPerformance ||
  136. !hasTransactions,
  137. tooltip:
  138. hasPerformance && hasTransactions
  139. ? getTermHelp(organization, PERFORMANCE_TERM.FAILURE_RATE)
  140. : noPerformanceTooltip,
  141. },
  142. {
  143. value: DisplayModes.TPM,
  144. label: t('Transactions Per Minute'),
  145. disabled:
  146. this.otherActiveDisplayModes.includes(DisplayModes.TPM) ||
  147. !hasPerformance ||
  148. !hasTransactions,
  149. tooltip:
  150. hasPerformance && hasTransactions
  151. ? getTermHelp(organization, PERFORMANCE_TERM.TPM)
  152. : noPerformanceTooltip,
  153. },
  154. {
  155. value: DisplayModes.ERRORS,
  156. label: t('Number of Errors'),
  157. disabled: this.otherActiveDisplayModes.includes(DisplayModes.ERRORS),
  158. },
  159. {
  160. value: DisplayModes.SESSIONS,
  161. label: t('Number of Sessions'),
  162. disabled:
  163. this.otherActiveDisplayModes.includes(DisplayModes.SESSIONS) || !hasSessions,
  164. tooltip: !hasSessions ? noHealthTooltip : undefined,
  165. },
  166. {
  167. value: DisplayModes.TRANSACTIONS,
  168. label: t('Number of Transactions'),
  169. disabled:
  170. this.otherActiveDisplayModes.includes(DisplayModes.TRANSACTIONS) ||
  171. !hasPerformance ||
  172. !hasTransactions,
  173. tooltip: hasPerformance && hasTransactions ? undefined : noPerformanceTooltip,
  174. },
  175. ];
  176. }
  177. get summaryHeading() {
  178. switch (this.displayMode) {
  179. case DisplayModes.ERRORS:
  180. return t('Total Errors');
  181. case DisplayModes.STABILITY:
  182. case DisplayModes.SESSIONS:
  183. return t('Total Sessions');
  184. case DisplayModes.APDEX:
  185. case DisplayModes.FAILURE_RATE:
  186. case DisplayModes.TPM:
  187. case DisplayModes.TRANSACTIONS:
  188. default:
  189. return t('Total Transactions');
  190. }
  191. }
  192. get barChartInterval() {
  193. const {query} = this.props.location;
  194. const diffInMinutes = getDiffInMinutes({
  195. ...query,
  196. period: decodeScalar(query.statsPeriod),
  197. });
  198. if (diffInMinutes >= TWO_WEEKS) {
  199. return '1d';
  200. }
  201. if (diffInMinutes >= ONE_WEEK) {
  202. return '12h';
  203. }
  204. if (diffInMinutes > TWENTY_FOUR_HOURS) {
  205. return '6h';
  206. }
  207. if (diffInMinutes === TWENTY_FOUR_HOURS) {
  208. return '1h';
  209. }
  210. if (diffInMinutes <= ONE_HOUR) {
  211. return '1m';
  212. }
  213. return '15m';
  214. }
  215. handleDisplayModeChange = (value: string) => {
  216. const {location, chartId, chartIndex, organization} = this.props;
  217. trackAnalyticsEvent({
  218. eventKey: `project_detail.change_chart${chartIndex + 1}`,
  219. eventName: `Project Detail: Change Chart #${chartIndex + 1}`,
  220. organization_id: parseInt(organization.id, 10),
  221. metric: value,
  222. });
  223. browserHistory.push({
  224. pathname: location.pathname,
  225. query: {...location.query, [chartId]: value},
  226. });
  227. };
  228. handleTotalValuesChange = (value: number | null) => {
  229. if (value !== this.state.totalValues) {
  230. this.setState({totalValues: value});
  231. }
  232. };
  233. render() {
  234. const {
  235. api,
  236. router,
  237. location,
  238. organization,
  239. theme,
  240. projectId,
  241. hasSessions,
  242. } = this.props;
  243. const {totalValues} = this.state;
  244. const hasDiscover = organization.features.includes('discover-basic');
  245. const displayMode = this.displayMode;
  246. let apdexYAxis: string;
  247. let apdexPerformanceTerm: PERFORMANCE_TERM;
  248. if (organization.features.includes('project-transaction-threshold')) {
  249. apdexPerformanceTerm = PERFORMANCE_TERM.APDEX_NEW;
  250. apdexYAxis = 'apdex()';
  251. } else {
  252. apdexPerformanceTerm = PERFORMANCE_TERM.APDEX;
  253. apdexYAxis = `apdex(${organization.apdexThreshold})`;
  254. }
  255. return (
  256. <Panel>
  257. <ChartContainer>
  258. {!defined(hasSessions) ? (
  259. <LoadingPanel />
  260. ) : (
  261. <Fragment>
  262. {displayMode === DisplayModes.APDEX && (
  263. <ProjectBaseEventsChart
  264. title={t('Apdex')}
  265. help={getTermHelp(organization, apdexPerformanceTerm)}
  266. query="event.type:transaction"
  267. yAxis={apdexYAxis}
  268. field={[apdexYAxis]}
  269. api={api}
  270. router={router}
  271. organization={organization}
  272. onTotalValuesChange={this.handleTotalValuesChange}
  273. colors={[CHART_PALETTE[0][0], theme.purple200]}
  274. />
  275. )}
  276. {displayMode === DisplayModes.FAILURE_RATE && (
  277. <ProjectBaseEventsChart
  278. title={t('Failure Rate')}
  279. help={getTermHelp(organization, PERFORMANCE_TERM.FAILURE_RATE)}
  280. query="event.type:transaction"
  281. yAxis="failure_rate()"
  282. field={[`failure_rate()`]}
  283. api={api}
  284. router={router}
  285. organization={organization}
  286. onTotalValuesChange={this.handleTotalValuesChange}
  287. colors={[theme.red300, theme.purple200]}
  288. />
  289. )}
  290. {displayMode === DisplayModes.TPM && (
  291. <ProjectBaseEventsChart
  292. title={t('Transactions Per Minute')}
  293. help={getTermHelp(organization, PERFORMANCE_TERM.TPM)}
  294. query="event.type:transaction"
  295. yAxis="tpm()"
  296. field={[`tpm()`]}
  297. api={api}
  298. router={router}
  299. organization={organization}
  300. onTotalValuesChange={this.handleTotalValuesChange}
  301. colors={[theme.yellow300, theme.purple200]}
  302. disablePrevious
  303. />
  304. )}
  305. {displayMode === DisplayModes.ERRORS &&
  306. (hasDiscover ? (
  307. <ProjectBaseEventsChart
  308. title={t('Number of Errors')}
  309. query="!event.type:transaction"
  310. yAxis="count()"
  311. field={[`count()`]}
  312. api={api}
  313. router={router}
  314. organization={organization}
  315. onTotalValuesChange={this.handleTotalValuesChange}
  316. colors={[theme.purple300, theme.purple200]}
  317. interval={this.barChartInterval}
  318. chartComponent={BarChart}
  319. disableReleases
  320. />
  321. ) : (
  322. <ProjectErrorsBasicChart
  323. organization={organization}
  324. projectId={projectId}
  325. location={location}
  326. onTotalValuesChange={this.handleTotalValuesChange}
  327. />
  328. ))}
  329. {displayMode === DisplayModes.TRANSACTIONS && (
  330. <ProjectBaseEventsChart
  331. title={t('Number of Transactions')}
  332. query="event.type:transaction"
  333. yAxis="count()"
  334. field={[`count()`]}
  335. api={api}
  336. router={router}
  337. organization={organization}
  338. onTotalValuesChange={this.handleTotalValuesChange}
  339. colors={[theme.gray200, theme.purple200]}
  340. interval={this.barChartInterval}
  341. chartComponent={BarChart}
  342. disableReleases
  343. />
  344. )}
  345. {displayMode === DisplayModes.STABILITY && (
  346. <ProjectBaseSessionsChart
  347. title={t('Crash Free Sessions')}
  348. help={getSessionTermDescription(SessionTerm.STABILITY, null)}
  349. router={router}
  350. api={api}
  351. organization={organization}
  352. onTotalValuesChange={this.handleTotalValuesChange}
  353. displayMode={displayMode}
  354. />
  355. )}
  356. {displayMode === DisplayModes.SESSIONS && (
  357. <ProjectBaseSessionsChart
  358. title={t('Number of Sessions')}
  359. router={router}
  360. api={api}
  361. organization={organization}
  362. onTotalValuesChange={this.handleTotalValuesChange}
  363. displayMode={displayMode}
  364. disablePrevious
  365. />
  366. )}
  367. </Fragment>
  368. )}
  369. </ChartContainer>
  370. <ChartControls>
  371. {/* if hasSessions is not yet defined, it means that request is still in progress and we can't decide what default chart to show */}
  372. {defined(hasSessions) ? (
  373. <Fragment>
  374. <InlineContainer>
  375. <SectionHeading>{this.summaryHeading}</SectionHeading>
  376. <SectionValue>
  377. {typeof totalValues === 'number'
  378. ? totalValues.toLocaleString()
  379. : '\u2014'}
  380. </SectionValue>
  381. </InlineContainer>
  382. <InlineContainer>
  383. <OptionSelector
  384. title={t('Display')}
  385. selected={displayMode}
  386. options={this.displayModes}
  387. onChange={this.handleDisplayModeChange}
  388. />
  389. </InlineContainer>
  390. </Fragment>
  391. ) : (
  392. <Placeholder height="34px" />
  393. )}
  394. </ChartControls>
  395. </Panel>
  396. );
  397. }
  398. }
  399. export default withApi(withTheme(ProjectCharts));