charts.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. import {Fragment, useEffect, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import Alert from 'sentry/components/alert';
  5. import _EventsRequest from 'sentry/components/charts/eventsRequest';
  6. import {getInterval} from 'sentry/components/charts/utils';
  7. import LoadingContainer from 'sentry/components/loading/loadingContainer';
  8. import {CHART_PALETTE} from 'sentry/constants/chartPalette';
  9. import {t} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import type {Series, SeriesDataUnit} from 'sentry/types/echarts';
  12. import type {Project} from 'sentry/types/project';
  13. import {defined} from 'sentry/utils';
  14. import {tooltipFormatterUsingAggregateOutputType} from 'sentry/utils/discover/charts';
  15. import EventView from 'sentry/utils/discover/eventView';
  16. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  17. import {formatVersion} from 'sentry/utils/formatters';
  18. import {decodeScalar} from 'sentry/utils/queryString';
  19. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  20. import {useLocation} from 'sentry/utils/useLocation';
  21. import useOrganization from 'sentry/utils/useOrganization';
  22. import usePageFilters from 'sentry/utils/usePageFilters';
  23. import {
  24. CHART_TITLES,
  25. OUTPUT_TYPE,
  26. YAXIS_COLUMNS,
  27. } from 'sentry/views/performance/mobile/screenload/screens';
  28. import {
  29. DEFAULT_PLATFORM,
  30. PLATFORM_LOCAL_STORAGE_KEY,
  31. PLATFORM_QUERY_PARAM,
  32. } from 'sentry/views/performance/mobile/screenload/screens/platformSelector';
  33. import {ScreensBarChart} from 'sentry/views/performance/mobile/screenload/screens/screenBarChart';
  34. import {useTableQuery} from 'sentry/views/performance/mobile/screenload/screens/screensTable';
  35. import {
  36. isCrossPlatform,
  37. transformDeviceClassEvents,
  38. } from 'sentry/views/performance/mobile/screenload/screens/utils';
  39. import Chart, {ChartType} from 'sentry/views/starfish/components/chart';
  40. import MiniChartPanel from 'sentry/views/starfish/components/miniChartPanel';
  41. import {useReleaseSelection} from 'sentry/views/starfish/queries/useReleases';
  42. import {formatVersionAndCenterTruncate} from 'sentry/views/starfish/utils/centerTruncate';
  43. import {STARFISH_CHART_INTERVAL_FIDELITY} from 'sentry/views/starfish/utils/constants';
  44. import {appendReleaseFilters} from 'sentry/views/starfish/utils/releaseComparison';
  45. import {useEventsStatsQuery} from 'sentry/views/starfish/utils/useEventsStatsQuery';
  46. export enum YAxis {
  47. WARM_START = 0,
  48. COLD_START = 1,
  49. TTID = 2,
  50. TTFD = 3,
  51. SLOW_FRAME_RATE = 4,
  52. FROZEN_FRAME_RATE = 5,
  53. THROUGHPUT = 6,
  54. COUNT = 7,
  55. }
  56. type Props = {
  57. yAxes: YAxis[];
  58. additionalFilters?: string[];
  59. chartHeight?: number;
  60. project?: Project | null;
  61. };
  62. export function ScreenCharts({yAxes, additionalFilters, project}: Props) {
  63. const pageFilter = usePageFilters();
  64. const location = useLocation();
  65. const organization = useOrganization();
  66. const yAxisCols = yAxes.map(val => YAXIS_COLUMNS[val]);
  67. const {
  68. primaryRelease,
  69. secondaryRelease,
  70. isLoading: isReleasesLoading,
  71. } = useReleaseSelection();
  72. const hasPlatformSelectFeature = organization.features.includes('spans-first-ui');
  73. const platform =
  74. decodeScalar(location.query[PLATFORM_QUERY_PARAM]) ??
  75. localStorage.getItem(PLATFORM_LOCAL_STORAGE_KEY) ??
  76. DEFAULT_PLATFORM;
  77. const queryString = useMemo(() => {
  78. const query = new MutableSearch([
  79. 'event.type:transaction',
  80. 'transaction.op:ui.load',
  81. ...(additionalFilters ?? []),
  82. ]);
  83. if (project && isCrossPlatform(project) && hasPlatformSelectFeature) {
  84. query.addFilterValue('os.name', platform);
  85. }
  86. return appendReleaseFilters(query, primaryRelease, secondaryRelease);
  87. }, [
  88. additionalFilters,
  89. hasPlatformSelectFeature,
  90. platform,
  91. primaryRelease,
  92. project,
  93. secondaryRelease,
  94. ]);
  95. const {
  96. data: series,
  97. isLoading: isSeriesLoading,
  98. error: seriesError,
  99. } = useEventsStatsQuery({
  100. eventView: EventView.fromNewQueryWithPageFilters(
  101. {
  102. name: '',
  103. fields: ['release', ...yAxisCols],
  104. topEvents: '2',
  105. yAxis: [...yAxisCols],
  106. query: queryString,
  107. dataset: DiscoverDatasets.METRICS,
  108. version: 2,
  109. interval: getInterval(
  110. pageFilter.selection.datetime,
  111. STARFISH_CHART_INTERVAL_FIDELITY
  112. ),
  113. },
  114. pageFilter.selection
  115. ),
  116. enabled: !isReleasesLoading,
  117. // TODO: Change referrer
  118. referrer: 'api.starfish.mobile-screen-series',
  119. initialData: {},
  120. });
  121. useEffect(() => {
  122. if (defined(primaryRelease) || isReleasesLoading) {
  123. return;
  124. }
  125. Sentry.captureException(new Error('Screen summary missing releases'));
  126. }, [primaryRelease, isReleasesLoading]);
  127. const transformedReleaseSeries: {
  128. [yAxisName: string]: {
  129. [releaseVersion: string]: Series;
  130. };
  131. } = {};
  132. yAxes.forEach(val => {
  133. transformedReleaseSeries[YAXIS_COLUMNS[val]] = {};
  134. });
  135. if (defined(series)) {
  136. Object.keys(series).forEach(release => {
  137. const isPrimary = release === primaryRelease;
  138. Object.keys(series[release]).forEach(yAxis => {
  139. const label = release;
  140. if (yAxis in transformedReleaseSeries) {
  141. const data =
  142. series[release][yAxis]?.data.map(datum => {
  143. return {
  144. name: datum[0] * 1000,
  145. value: datum[1][0].count,
  146. } as SeriesDataUnit;
  147. }) ?? [];
  148. const color = isPrimary ? CHART_PALETTE[3][0] : CHART_PALETTE[3][1];
  149. transformedReleaseSeries[yAxis][release] = {
  150. seriesName: formatVersion(label),
  151. color,
  152. data,
  153. };
  154. }
  155. });
  156. });
  157. }
  158. const {data: deviceClassEvents, isLoading: isDeviceClassEventsLoading} = useTableQuery({
  159. eventView: EventView.fromNewQueryWithLocation(
  160. {
  161. name: '',
  162. fields: ['device.class', 'release', ...yAxisCols],
  163. orderby: yAxisCols[0],
  164. yAxis: yAxisCols,
  165. query: queryString,
  166. dataset: DiscoverDatasets.METRICS,
  167. version: 2,
  168. },
  169. location
  170. ),
  171. enabled: !isReleasesLoading,
  172. referrer: 'api.starfish.mobile-device-breakdown',
  173. });
  174. if (isReleasesLoading) {
  175. return <LoadingContainer />;
  176. }
  177. if (!defined(primaryRelease) && !isReleasesLoading) {
  178. return (
  179. <Alert type="warning" showIcon>
  180. {t('Invalid selection. Try a different release or date range.')}
  181. </Alert>
  182. );
  183. }
  184. const transformedEvents = transformDeviceClassEvents({
  185. yAxes,
  186. primaryRelease,
  187. secondaryRelease,
  188. data: deviceClassEvents,
  189. });
  190. function renderCharts() {
  191. return (
  192. <Fragment>
  193. <Container>
  194. <div>
  195. <StyledRow>
  196. <ChartsContainerItem key="deviceClass">
  197. <ScreensBarChart
  198. chartOptions={[
  199. {
  200. title: t('TTID by Device Class'),
  201. yAxis: YAXIS_COLUMNS[yAxes[0]],
  202. series: Object.values(transformedEvents[YAXIS_COLUMNS[yAxes[0]]]),
  203. xAxisLabel: ['high', 'medium', 'low', 'Unknown'],
  204. subtitle: primaryRelease
  205. ? t(
  206. '%s v. %s',
  207. formatVersionAndCenterTruncate(primaryRelease, 12),
  208. secondaryRelease
  209. ? formatVersionAndCenterTruncate(secondaryRelease, 12)
  210. : ''
  211. )
  212. : '',
  213. },
  214. ]}
  215. chartKey="spansChart"
  216. chartHeight={80}
  217. isLoading={isDeviceClassEventsLoading}
  218. />
  219. </ChartsContainerItem>
  220. <ChartsContainerItem key="xyz">
  221. <MiniChartPanel
  222. title={t('Average TTID')}
  223. subtitle={
  224. primaryRelease
  225. ? t(
  226. '%s v. %s',
  227. formatVersionAndCenterTruncate(primaryRelease, 12),
  228. secondaryRelease
  229. ? formatVersionAndCenterTruncate(secondaryRelease, 12)
  230. : ''
  231. )
  232. : ''
  233. }
  234. >
  235. <Chart
  236. height={80}
  237. data={Object.values(
  238. transformedReleaseSeries[YAXIS_COLUMNS[yAxes[0]]]
  239. )}
  240. loading={isSeriesLoading}
  241. grid={{
  242. left: '0',
  243. right: '0',
  244. top: '8px',
  245. bottom: '0',
  246. }}
  247. showLegend
  248. definedAxisTicks={2}
  249. type={ChartType.LINE}
  250. aggregateOutputFormat={OUTPUT_TYPE[YAxis.TTID]}
  251. tooltipFormatterOptions={{
  252. valueFormatter: value =>
  253. tooltipFormatterUsingAggregateOutputType(
  254. value,
  255. OUTPUT_TYPE[YAxis.TTID]
  256. ),
  257. }}
  258. error={seriesError}
  259. />
  260. </MiniChartPanel>
  261. </ChartsContainerItem>
  262. </StyledRow>
  263. <StyledRow>
  264. <ChartsContainerItem key="deviceClass">
  265. <ScreensBarChart
  266. chartOptions={[
  267. {
  268. title: t('TTFD by Device Class'),
  269. yAxis: YAXIS_COLUMNS[yAxes[1]],
  270. series: Object.values(transformedEvents[YAXIS_COLUMNS[yAxes[1]]]),
  271. xAxisLabel: ['high', 'medium', 'low', 'Unknown'],
  272. subtitle: primaryRelease
  273. ? t(
  274. '%s v. %s',
  275. formatVersionAndCenterTruncate(primaryRelease, 12),
  276. secondaryRelease
  277. ? formatVersionAndCenterTruncate(secondaryRelease, 12)
  278. : ''
  279. )
  280. : '',
  281. },
  282. ]}
  283. chartKey="spansChart"
  284. chartHeight={80}
  285. isLoading={isDeviceClassEventsLoading}
  286. />
  287. </ChartsContainerItem>
  288. <ChartsContainerItem key="xyz">
  289. <MiniChartPanel
  290. title={t('Average TTFD')}
  291. subtitle={
  292. primaryRelease
  293. ? t(
  294. '%s v. %s',
  295. formatVersionAndCenterTruncate(primaryRelease, 12),
  296. secondaryRelease
  297. ? formatVersionAndCenterTruncate(secondaryRelease, 12)
  298. : ''
  299. )
  300. : ''
  301. }
  302. >
  303. <Chart
  304. height={80}
  305. data={Object.values(
  306. transformedReleaseSeries[YAXIS_COLUMNS[yAxes[1]]]
  307. )}
  308. loading={isSeriesLoading}
  309. grid={{
  310. left: '0',
  311. right: '0',
  312. top: '8px',
  313. bottom: '0',
  314. }}
  315. showLegend
  316. definedAxisTicks={2}
  317. type={ChartType.LINE}
  318. aggregateOutputFormat={OUTPUT_TYPE[YAxis.TTFD]}
  319. tooltipFormatterOptions={{
  320. valueFormatter: value =>
  321. tooltipFormatterUsingAggregateOutputType(
  322. value,
  323. OUTPUT_TYPE[YAxis.TTFD]
  324. ),
  325. }}
  326. error={seriesError}
  327. />
  328. </MiniChartPanel>
  329. </ChartsContainerItem>
  330. </StyledRow>
  331. </div>
  332. <ChartsContainerItem key="xyz">
  333. <MiniChartPanel
  334. title={CHART_TITLES[YAxis.COUNT]}
  335. subtitle={
  336. primaryRelease
  337. ? t(
  338. '%s v. %s',
  339. formatVersionAndCenterTruncate(primaryRelease, 12),
  340. secondaryRelease
  341. ? formatVersionAndCenterTruncate(secondaryRelease, 12)
  342. : ''
  343. )
  344. : ''
  345. }
  346. >
  347. <Chart
  348. data={Object.values(transformedReleaseSeries[YAXIS_COLUMNS[yAxes[2]]])}
  349. height={245}
  350. loading={isSeriesLoading}
  351. grid={{
  352. left: '0',
  353. right: '0',
  354. top: '8px',
  355. bottom: '0',
  356. }}
  357. showLegend
  358. definedAxisTicks={2}
  359. type={ChartType.LINE}
  360. aggregateOutputFormat={OUTPUT_TYPE[YAxis.COUNT]}
  361. tooltipFormatterOptions={{
  362. valueFormatter: value =>
  363. tooltipFormatterUsingAggregateOutputType(
  364. value,
  365. OUTPUT_TYPE[YAxis.COUNT]
  366. ),
  367. }}
  368. error={seriesError}
  369. />
  370. </MiniChartPanel>
  371. </ChartsContainerItem>
  372. </Container>
  373. </Fragment>
  374. );
  375. }
  376. return <div data-test-id="starfish-mobile-view">{renderCharts()}</div>;
  377. }
  378. const StyledRow = styled('div')`
  379. display: grid;
  380. grid-template-columns: repeat(2, 1fr);
  381. grid-column-gap: ${space(2)};
  382. `;
  383. const ChartsContainerItem = styled('div')`
  384. flex: 1;
  385. `;
  386. export const Spacer = styled('div')`
  387. margin-top: ${space(3)};
  388. `;
  389. const Container = styled('div')`
  390. display: grid;
  391. grid-template-columns: 2fr 1fr;
  392. grid-column-gap: ${space(2)};
  393. `;