projectBaseSessionsChart.tsx 11 KB

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