projectBaseSessionsChart.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. import {Component, Fragment} from 'react';
  2. import type {InjectedRouter} from 'react-router';
  3. import type {Theme} from '@emotion/react';
  4. import {useTheme} from '@emotion/react';
  5. import type {LegendComponentOption, LineSeriesOption} from 'echarts';
  6. import isEqual from 'lodash/isEqual';
  7. import type {Client} from 'sentry/api';
  8. import {BarChart} from 'sentry/components/charts/barChart';
  9. import type {ZoomRenderProps} from 'sentry/components/charts/chartZoom';
  10. import ChartZoom from 'sentry/components/charts/chartZoom';
  11. import ErrorPanel from 'sentry/components/charts/errorPanel';
  12. import type {LineChartProps} from 'sentry/components/charts/lineChart';
  13. import {LineChart} from 'sentry/components/charts/lineChart';
  14. import ReleaseSeries from 'sentry/components/charts/releaseSeries';
  15. import StackedAreaChart from 'sentry/components/charts/stackedAreaChart';
  16. import {HeaderTitleLegend} from 'sentry/components/charts/styles';
  17. import TransitionChart from 'sentry/components/charts/transitionChart';
  18. import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask';
  19. import {RELEASE_LINES_THRESHOLD} from 'sentry/components/charts/utils';
  20. import QuestionTooltip from 'sentry/components/questionTooltip';
  21. import {IconWarning} from 'sentry/icons';
  22. import {t} from 'sentry/locale';
  23. import type {PageFilters} from 'sentry/types/core';
  24. import type {EChartEventHandler, Series} from 'sentry/types/echarts';
  25. import type {Organization} from 'sentry/types/organization';
  26. import getDynamicText from 'sentry/utils/getDynamicText';
  27. import {MINUTES_THRESHOLD_TO_DISPLAY_SECONDS} from 'sentry/utils/sessions';
  28. import withPageFilters from 'sentry/utils/withPageFilters';
  29. import {displayCrashFreePercent} from 'sentry/views/releases/utils';
  30. import {sessionTerm} from 'sentry/views/releases/utils/sessionTerm';
  31. import {DisplayModes} from '../projectCharts';
  32. import ProjectSessionsAnrRequest from './projectSessionsAnrRequest';
  33. import ProjectSessionsChartRequest from './projectSessionsChartRequest';
  34. type Props = {
  35. api: Client;
  36. displayMode:
  37. | DisplayModes.SESSIONS
  38. | DisplayModes.STABILITY_USERS
  39. | DisplayModes.ANR_RATE
  40. | DisplayModes.FOREGROUND_ANR_RATE
  41. | DisplayModes.STABILITY;
  42. onTotalValuesChange: (value: number | null) => void;
  43. organization: Organization;
  44. router: InjectedRouter;
  45. selection: PageFilters;
  46. title: string;
  47. disablePrevious?: boolean;
  48. help?: string;
  49. query?: string;
  50. };
  51. function ProjectBaseSessionsChart({
  52. title,
  53. organization,
  54. router,
  55. selection,
  56. api,
  57. onTotalValuesChange,
  58. displayMode,
  59. help,
  60. disablePrevious,
  61. query,
  62. }: Props) {
  63. const theme = useTheme();
  64. const {projects, environments, datetime} = selection;
  65. const {start, end, period, utc} = datetime;
  66. const Request = [DisplayModes.ANR_RATE, DisplayModes.FOREGROUND_ANR_RATE].includes(
  67. displayMode
  68. )
  69. ? ProjectSessionsAnrRequest
  70. : ProjectSessionsChartRequest;
  71. return (
  72. <Fragment>
  73. {getDynamicText({
  74. value: (
  75. <ChartZoom router={router} period={period} start={start} end={end} utc={utc}>
  76. {zoomRenderProps => (
  77. <Request
  78. api={api}
  79. selection={selection}
  80. organization={organization}
  81. onTotalValuesChange={onTotalValuesChange}
  82. displayMode={displayMode}
  83. disablePrevious={disablePrevious}
  84. query={query}
  85. >
  86. {({
  87. errored,
  88. loading,
  89. reloading,
  90. timeseriesData,
  91. previousTimeseriesData,
  92. additionalSeries,
  93. }) => (
  94. <ReleaseSeries
  95. utc={utc}
  96. period={period}
  97. start={start}
  98. end={end}
  99. projects={projects}
  100. environments={environments}
  101. query={query}
  102. >
  103. {({releaseSeries}) => {
  104. if (errored) {
  105. return (
  106. <ErrorPanel>
  107. <IconWarning color="gray300" size="lg" />
  108. </ErrorPanel>
  109. );
  110. }
  111. return (
  112. <TransitionChart loading={loading} reloading={reloading}>
  113. <TransparentLoadingMask visible={reloading} />
  114. <HeaderTitleLegend>
  115. {title}
  116. {help && (
  117. <QuestionTooltip size="sm" position="top" title={help} />
  118. )}
  119. </HeaderTitleLegend>
  120. <Chart
  121. theme={theme}
  122. zoomRenderProps={zoomRenderProps}
  123. reloading={reloading}
  124. timeSeries={timeseriesData}
  125. previousTimeSeries={
  126. previousTimeseriesData
  127. ? [previousTimeseriesData]
  128. : undefined
  129. }
  130. releaseSeries={releaseSeries}
  131. displayMode={displayMode}
  132. additionalSeries={additionalSeries}
  133. />
  134. </TransitionChart>
  135. );
  136. }}
  137. </ReleaseSeries>
  138. )}
  139. </Request>
  140. )}
  141. </ChartZoom>
  142. ),
  143. fixed: `${title} Chart`,
  144. })}
  145. </Fragment>
  146. );
  147. }
  148. type ChartProps = {
  149. displayMode:
  150. | DisplayModes.SESSIONS
  151. | DisplayModes.STABILITY
  152. | DisplayModes.STABILITY_USERS
  153. | DisplayModes.ANR_RATE
  154. | DisplayModes.FOREGROUND_ANR_RATE;
  155. releaseSeries: Series[];
  156. reloading: boolean;
  157. theme: Theme;
  158. timeSeries: Series[];
  159. zoomRenderProps: ZoomRenderProps;
  160. additionalSeries?: LineSeriesOption[];
  161. previousTimeSeries?: Series[];
  162. };
  163. type ChartState = {
  164. forceUpdate: boolean;
  165. seriesSelection: Record<string, boolean>;
  166. };
  167. class Chart extends Component<ChartProps, ChartState> {
  168. state: ChartState = {
  169. seriesSelection: {},
  170. forceUpdate: false,
  171. };
  172. shouldComponentUpdate(nextProps: ChartProps, nextState: ChartState) {
  173. if (nextState.forceUpdate) {
  174. return true;
  175. }
  176. if (!isEqual(this.state.seriesSelection, nextState.seriesSelection)) {
  177. return true;
  178. }
  179. if (
  180. nextProps.releaseSeries !== this.props.releaseSeries &&
  181. !nextProps.reloading &&
  182. !this.props.reloading
  183. ) {
  184. return true;
  185. }
  186. if (this.props.reloading && !nextProps.reloading) {
  187. return true;
  188. }
  189. if (nextProps.timeSeries !== this.props.timeSeries) {
  190. return true;
  191. }
  192. return false;
  193. }
  194. // inspired by app/components/charts/eventsChart.tsx@handleLegendSelectChanged
  195. handleLegendSelectChanged: EChartEventHandler<{
  196. name: string;
  197. selected: Record<string, boolean>;
  198. type: 'legendselectchanged';
  199. }> = ({selected}) => {
  200. const seriesSelection = Object.keys(selected).reduce((state, key) => {
  201. state[key] = selected[key];
  202. return state;
  203. }, {});
  204. // we have to force an update here otherwise ECharts will
  205. // update its internal state and disable the series
  206. this.setState({seriesSelection, forceUpdate: true}, () =>
  207. this.setState({forceUpdate: false})
  208. );
  209. };
  210. get isCrashFree() {
  211. const {displayMode} = this.props;
  212. return [DisplayModes.STABILITY, DisplayModes.STABILITY_USERS].includes(displayMode);
  213. }
  214. get isAnr() {
  215. const {displayMode} = this.props;
  216. return [DisplayModes.ANR_RATE, DisplayModes.FOREGROUND_ANR_RATE].includes(
  217. displayMode
  218. );
  219. }
  220. get legend(): LegendComponentOption {
  221. const {theme, timeSeries, previousTimeSeries, releaseSeries, additionalSeries} =
  222. this.props;
  223. const {seriesSelection} = this.state;
  224. const hideReleasesByDefault =
  225. (releaseSeries[0] as any)?.markLine?.data.length >= RELEASE_LINES_THRESHOLD;
  226. const hideHealthyByDefault = timeSeries
  227. .filter(s => sessionTerm.healthy !== s.seriesName)
  228. .some(s => s.data.some(d => d.value > 0));
  229. const selected =
  230. Object.keys(seriesSelection).length === 0 &&
  231. (hideReleasesByDefault || hideHealthyByDefault)
  232. ? {
  233. [t('Releases')]: !hideReleasesByDefault,
  234. [sessionTerm.healthy]: !hideHealthyByDefault,
  235. }
  236. : seriesSelection;
  237. return {
  238. right: 10,
  239. top: 0,
  240. icon: 'circle',
  241. itemHeight: 8,
  242. itemWidth: 8,
  243. itemGap: 12,
  244. align: 'left' as const,
  245. textStyle: {
  246. color: theme.textColor,
  247. verticalAlign: 'top',
  248. fontSize: 11,
  249. fontFamily: theme.text.family,
  250. },
  251. data: [
  252. ...timeSeries.map(s => s.seriesName),
  253. ...(previousTimeSeries ?? []).map(s => s.seriesName),
  254. ...(additionalSeries ?? []).map(s => s.name?.toString() ?? ''),
  255. ...releaseSeries.map(s => s.seriesName),
  256. ],
  257. selected,
  258. };
  259. }
  260. get chartOptions(): Omit<LineChartProps, 'series'> {
  261. return {
  262. grid: {left: '10px', right: '10px', top: '40px', bottom: '0px'},
  263. seriesOptions: {
  264. showSymbol: false,
  265. },
  266. tooltip: {
  267. trigger: 'axis',
  268. truncate: 80,
  269. valueFormatter: (value: number | null) => {
  270. if (value === null) {
  271. return '\u2014';
  272. }
  273. if (this.isCrashFree) {
  274. return displayCrashFreePercent(value, 0, 3);
  275. }
  276. if (this.isAnr) {
  277. return displayCrashFreePercent(value, 0, 3, false);
  278. }
  279. return typeof value === 'number' ? value.toLocaleString() : value;
  280. },
  281. },
  282. yAxis: this.isCrashFree
  283. ? {
  284. axisLabel: {
  285. formatter: (value: number) => displayCrashFreePercent(value),
  286. },
  287. scale: true,
  288. max: 100,
  289. }
  290. : this.isAnr
  291. ? {
  292. axisLabel: {
  293. formatter: (value: number) => displayCrashFreePercent(value, 0, 3, false),
  294. },
  295. scale: true,
  296. }
  297. : {min: 0},
  298. };
  299. }
  300. render() {
  301. const {
  302. zoomRenderProps,
  303. timeSeries,
  304. previousTimeSeries,
  305. releaseSeries,
  306. additionalSeries,
  307. } = this.props;
  308. const ChartComponent = this.isCrashFree
  309. ? LineChart
  310. : this.isAnr
  311. ? BarChart
  312. : StackedAreaChart;
  313. return (
  314. <ChartComponent
  315. {...zoomRenderProps}
  316. {...this.chartOptions}
  317. legend={this.legend}
  318. series={
  319. Array.isArray(releaseSeries) && !this.isAnr
  320. ? [...timeSeries, ...releaseSeries]
  321. : timeSeries
  322. }
  323. additionalSeries={additionalSeries}
  324. previousPeriod={previousTimeSeries}
  325. onLegendSelectChanged={this.handleLegendSelectChanged}
  326. minutesThresholdToDisplaySeconds={MINUTES_THRESHOLD_TO_DISPLAY_SECONDS}
  327. transformSinglePointToBar
  328. />
  329. );
  330. }
  331. }
  332. export default withPageFilters(ProjectBaseSessionsChart);