projectCharts.tsx 17 KB

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