projectBaseSessionsChart.tsx 11 KB

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