projectCharts.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  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';
  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 {trackAnalyticsEvent} 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 {
  35. getSessionTermDescription,
  36. SessionTerm,
  37. } from 'sentry/views/releases/utils/sessionTerm';
  38. import {getTermHelp, PERFORMANCE_TERM} from '../performance/data';
  39. import ProjectBaseEventsChart from './charts/projectBaseEventsChart';
  40. import ProjectBaseSessionsChart from './charts/projectBaseSessionsChart';
  41. import ProjectErrorsBasicChart from './charts/projectErrorsBasicChart';
  42. export enum DisplayModes {
  43. APDEX = 'apdex',
  44. FAILURE_RATE = 'failure_rate',
  45. TPM = 'tpm',
  46. ERRORS = 'errors',
  47. TRANSACTIONS = 'transactions',
  48. STABILITY = 'crash_free',
  49. STABILITY_USERS = 'crash_free_users',
  50. ANR_RATE = 'anr_rate',
  51. FOREGROUND_ANR_RATE = 'foreground_anr_rate',
  52. SESSIONS = 'sessions',
  53. }
  54. type Props = {
  55. api: Client;
  56. chartId: string;
  57. chartIndex: number;
  58. hasSessions: boolean | null;
  59. hasTransactions: boolean;
  60. location: Location;
  61. organization: Organization;
  62. router: InjectedRouter;
  63. theme: Theme;
  64. visibleCharts: string[];
  65. project?: Project;
  66. projectId?: string;
  67. query?: string;
  68. };
  69. type State = {
  70. totalValues: number | null;
  71. };
  72. class ProjectCharts extends Component<Props, State> {
  73. state: State = {
  74. totalValues: null,
  75. };
  76. get defaultDisplayModes() {
  77. const {hasSessions, hasTransactions, organization, project} = this.props;
  78. if (!hasSessions && !hasTransactions) {
  79. return [DisplayModes.ERRORS];
  80. }
  81. if (hasSessions && !hasTransactions) {
  82. if (organization.features.includes('anr-rate') && project?.platform === 'android') {
  83. return [DisplayModes.STABILITY, DisplayModes.FOREGROUND_ANR_RATE];
  84. }
  85. return [DisplayModes.STABILITY, DisplayModes.ERRORS];
  86. }
  87. if (!hasSessions && hasTransactions) {
  88. return [DisplayModes.FAILURE_RATE, DisplayModes.APDEX];
  89. }
  90. if (organization.features.includes('anr-rate') && project?.platform === 'android') {
  91. return [DisplayModes.STABILITY, DisplayModes.FOREGROUND_ANR_RATE];
  92. }
  93. return [DisplayModes.STABILITY, DisplayModes.APDEX];
  94. }
  95. get otherActiveDisplayModes() {
  96. const {location, visibleCharts, chartId} = this.props;
  97. return visibleCharts
  98. .filter(visibleChartId => visibleChartId !== chartId)
  99. .map(urlKey => {
  100. return decodeScalar(
  101. location.query[urlKey],
  102. this.defaultDisplayModes[visibleCharts.findIndex(value => value === urlKey)]
  103. );
  104. });
  105. }
  106. get displayMode() {
  107. const {location, chartId, chartIndex} = this.props;
  108. const displayMode =
  109. decodeScalar(location.query[chartId]) || this.defaultDisplayModes[chartIndex];
  110. if (!Object.values(DisplayModes).includes(displayMode as DisplayModes)) {
  111. return this.defaultDisplayModes[chartIndex];
  112. }
  113. return displayMode;
  114. }
  115. get displayModes(): SelectValue<string>[] {
  116. const {organization, hasSessions, hasTransactions, project} = this.props;
  117. const hasPerformance = organization.features.includes('performance-view');
  118. const noPerformanceTooltip = NOT_AVAILABLE_MESSAGES.performance;
  119. const noHealthTooltip = NOT_AVAILABLE_MESSAGES.releaseHealth;
  120. const options = [
  121. {
  122. value: DisplayModes.STABILITY,
  123. label: t('Crash Free Sessions'),
  124. disabled:
  125. this.otherActiveDisplayModes.includes(DisplayModes.STABILITY) || !hasSessions,
  126. tooltip: !hasSessions ? noHealthTooltip : undefined,
  127. },
  128. {
  129. value: DisplayModes.STABILITY_USERS,
  130. label: t('Crash Free Users'),
  131. disabled:
  132. this.otherActiveDisplayModes.includes(DisplayModes.STABILITY_USERS) ||
  133. !hasSessions,
  134. tooltip: !hasSessions ? noHealthTooltip : undefined,
  135. },
  136. {
  137. value: DisplayModes.APDEX,
  138. label: t('Apdex'),
  139. disabled:
  140. this.otherActiveDisplayModes.includes(DisplayModes.APDEX) ||
  141. !hasPerformance ||
  142. !hasTransactions,
  143. tooltip:
  144. hasPerformance && hasTransactions
  145. ? getTermHelp(organization, PERFORMANCE_TERM.APDEX)
  146. : noPerformanceTooltip,
  147. },
  148. {
  149. value: DisplayModes.FAILURE_RATE,
  150. label: t('Failure Rate'),
  151. disabled:
  152. this.otherActiveDisplayModes.includes(DisplayModes.FAILURE_RATE) ||
  153. !hasPerformance ||
  154. !hasTransactions,
  155. tooltip:
  156. hasPerformance && hasTransactions
  157. ? getTermHelp(organization, PERFORMANCE_TERM.FAILURE_RATE)
  158. : noPerformanceTooltip,
  159. },
  160. {
  161. value: DisplayModes.TPM,
  162. label: t('Transactions Per Minute'),
  163. disabled:
  164. this.otherActiveDisplayModes.includes(DisplayModes.TPM) ||
  165. !hasPerformance ||
  166. !hasTransactions,
  167. tooltip:
  168. hasPerformance && hasTransactions
  169. ? getTermHelp(organization, PERFORMANCE_TERM.TPM)
  170. : noPerformanceTooltip,
  171. },
  172. {
  173. value: DisplayModes.ERRORS,
  174. label: t('Number of Errors'),
  175. disabled: this.otherActiveDisplayModes.includes(DisplayModes.ERRORS),
  176. },
  177. {
  178. value: DisplayModes.SESSIONS,
  179. label: t('Number of Sessions'),
  180. disabled:
  181. this.otherActiveDisplayModes.includes(DisplayModes.SESSIONS) || !hasSessions,
  182. tooltip: !hasSessions ? noHealthTooltip : undefined,
  183. },
  184. {
  185. value: DisplayModes.TRANSACTIONS,
  186. label: t('Number of Transactions'),
  187. disabled:
  188. this.otherActiveDisplayModes.includes(DisplayModes.TRANSACTIONS) ||
  189. !hasPerformance ||
  190. !hasTransactions,
  191. tooltip: hasPerformance && hasTransactions ? undefined : noPerformanceTooltip,
  192. },
  193. ];
  194. if (organization.features.includes('anr-rate') && project?.platform === 'android') {
  195. return [
  196. {
  197. value: DisplayModes.ANR_RATE,
  198. label: t('ANR Rate'),
  199. disabled:
  200. this.otherActiveDisplayModes.includes(DisplayModes.ANR_RATE) || !hasSessions,
  201. tooltip: !hasSessions ? noHealthTooltip : undefined,
  202. },
  203. {
  204. value: DisplayModes.FOREGROUND_ANR_RATE,
  205. label: t('Foreground ANR Rate'),
  206. disabled:
  207. this.otherActiveDisplayModes.includes(DisplayModes.FOREGROUND_ANR_RATE) ||
  208. !hasSessions,
  209. tooltip: !hasSessions ? noHealthTooltip : undefined,
  210. },
  211. ...options,
  212. ];
  213. }
  214. return options;
  215. }
  216. get summaryHeading() {
  217. switch (this.displayMode) {
  218. case DisplayModes.ERRORS:
  219. return t('Total Errors');
  220. case DisplayModes.STABILITY:
  221. case DisplayModes.SESSIONS:
  222. return t('Total Sessions');
  223. case DisplayModes.STABILITY_USERS:
  224. case DisplayModes.ANR_RATE:
  225. case DisplayModes.FOREGROUND_ANR_RATE:
  226. return t('Total Users');
  227. case DisplayModes.APDEX:
  228. case DisplayModes.FAILURE_RATE:
  229. case DisplayModes.TPM:
  230. case DisplayModes.TRANSACTIONS:
  231. default:
  232. return t('Total Transactions');
  233. }
  234. }
  235. get barChartInterval() {
  236. const {query} = this.props.location;
  237. const diffInMinutes = getDiffInMinutes({
  238. ...query,
  239. period: decodeScalar(query.statsPeriod),
  240. });
  241. if (diffInMinutes >= TWO_WEEKS) {
  242. return '1d';
  243. }
  244. if (diffInMinutes >= ONE_WEEK) {
  245. return '12h';
  246. }
  247. if (diffInMinutes > TWENTY_FOUR_HOURS) {
  248. return '6h';
  249. }
  250. if (diffInMinutes === TWENTY_FOUR_HOURS) {
  251. return '1h';
  252. }
  253. if (diffInMinutes <= ONE_HOUR) {
  254. return '1m';
  255. }
  256. return '15m';
  257. }
  258. handleDisplayModeChange = (value: string) => {
  259. const {location, chartId, chartIndex, organization} = this.props;
  260. trackAnalyticsEvent({
  261. eventKey: `project_detail.change_chart${chartIndex + 1}`,
  262. eventName: `Project Detail: Change Chart #${chartIndex + 1}`,
  263. organization_id: parseInt(organization.id, 10),
  264. metric: value,
  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 =
  292. organization.features.includes('anr-rate') && project?.platform === 'android';
  293. return (
  294. <Panel>
  295. <ChartContainer>
  296. {!defined(hasSessions) ? (
  297. <LoadingPanel />
  298. ) : (
  299. <Fragment>
  300. {displayMode === DisplayModes.APDEX && (
  301. <ProjectBaseEventsChart
  302. title={t('Apdex')}
  303. help={getTermHelp(organization, PERFORMANCE_TERM.APDEX)}
  304. query={new MutableSearch([
  305. 'event.type:transaction',
  306. query ?? '',
  307. ]).formatString()}
  308. yAxis="apdex()"
  309. field={['apdex()']}
  310. api={api}
  311. router={router}
  312. organization={organization}
  313. onTotalValuesChange={this.handleTotalValuesChange}
  314. colors={[CHART_PALETTE[0][0], theme.purple200]}
  315. />
  316. )}
  317. {displayMode === DisplayModes.FAILURE_RATE && (
  318. <ProjectBaseEventsChart
  319. title={t('Failure Rate')}
  320. help={getTermHelp(organization, PERFORMANCE_TERM.FAILURE_RATE)}
  321. query={new MutableSearch([
  322. 'event.type:transaction',
  323. query ?? '',
  324. ]).formatString()}
  325. yAxis="failure_rate()"
  326. field={[`failure_rate()`]}
  327. api={api}
  328. router={router}
  329. organization={organization}
  330. onTotalValuesChange={this.handleTotalValuesChange}
  331. colors={[theme.red300, theme.purple200]}
  332. />
  333. )}
  334. {displayMode === DisplayModes.TPM && (
  335. <ProjectBaseEventsChart
  336. title={t('Transactions Per Minute')}
  337. help={getTermHelp(organization, PERFORMANCE_TERM.TPM)}
  338. query={new MutableSearch([
  339. 'event.type:transaction',
  340. query ?? '',
  341. ]).formatString()}
  342. yAxis="tpm()"
  343. field={[`tpm()`]}
  344. api={api}
  345. router={router}
  346. organization={organization}
  347. onTotalValuesChange={this.handleTotalValuesChange}
  348. colors={[theme.yellow300, theme.purple200]}
  349. disablePrevious
  350. />
  351. )}
  352. {displayMode === DisplayModes.ERRORS &&
  353. (hasDiscover ? (
  354. <ProjectBaseEventsChart
  355. title={t('Number of Errors')}
  356. query={new MutableSearch([
  357. '!event.type:transaction',
  358. query ?? '',
  359. ]).formatString()}
  360. yAxis="count()"
  361. field={[`count()`]}
  362. api={api}
  363. router={router}
  364. organization={organization}
  365. onTotalValuesChange={this.handleTotalValuesChange}
  366. colors={[theme.purple300, theme.purple200]}
  367. interval={this.barChartInterval}
  368. chartComponent={BarChart}
  369. disableReleases
  370. />
  371. ) : (
  372. <ProjectErrorsBasicChart
  373. organization={organization}
  374. projectId={projectId}
  375. location={location}
  376. onTotalValuesChange={this.handleTotalValuesChange}
  377. />
  378. ))}
  379. {displayMode === DisplayModes.TRANSACTIONS && (
  380. <ProjectBaseEventsChart
  381. title={t('Number of Transactions')}
  382. query={new MutableSearch([
  383. 'event.type:transaction',
  384. query ?? '',
  385. ]).formatString()}
  386. yAxis="count()"
  387. field={[`count()`]}
  388. api={api}
  389. router={router}
  390. organization={organization}
  391. onTotalValuesChange={this.handleTotalValuesChange}
  392. colors={[theme.gray200, theme.purple200]}
  393. interval={this.barChartInterval}
  394. chartComponent={BarChart}
  395. disableReleases
  396. />
  397. )}
  398. {displayMode === DisplayModes.STABILITY && (
  399. <ProjectBaseSessionsChart
  400. title={t('Crash Free Sessions')}
  401. help={getSessionTermDescription(SessionTerm.STABILITY, null)}
  402. router={router}
  403. api={api}
  404. organization={organization}
  405. onTotalValuesChange={this.handleTotalValuesChange}
  406. displayMode={displayMode}
  407. query={query}
  408. />
  409. )}
  410. {hasAnrRateFeature && displayMode === DisplayModes.ANR_RATE && (
  411. <ProjectBaseSessionsChart
  412. title={t('ANR Rate')}
  413. help={getSessionTermDescription(SessionTerm.ANR_RATE, null)}
  414. router={router}
  415. api={api}
  416. organization={organization}
  417. onTotalValuesChange={this.handleTotalValuesChange}
  418. displayMode={displayMode}
  419. query={query}
  420. />
  421. )}
  422. {hasAnrRateFeature && displayMode === DisplayModes.FOREGROUND_ANR_RATE && (
  423. <ProjectBaseSessionsChart
  424. title={t('Foreground ANR Rate')}
  425. help={getSessionTermDescription(SessionTerm.FOREGROUND_ANR_RATE, null)}
  426. router={router}
  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. router={router}
  439. api={api}
  440. organization={organization}
  441. onTotalValuesChange={this.handleTotalValuesChange}
  442. displayMode={displayMode}
  443. query={query}
  444. />
  445. )}
  446. {displayMode === DisplayModes.SESSIONS && (
  447. <ProjectBaseSessionsChart
  448. title={t('Number of Sessions')}
  449. router={router}
  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));