projectCharts.tsx 17 KB

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