projectCharts.tsx 17 KB

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