projectCharts.tsx 17 KB

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