projectCharts.tsx 15 KB

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