index.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. import {Fragment, useMemo} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import Alert from 'sentry/components/alert';
  5. import LoadingContainer from 'sentry/components/loading/loadingContainer';
  6. import LoadingIndicator from 'sentry/components/loadingIndicator';
  7. import type {CursorHandler} from 'sentry/components/pagination';
  8. import SearchBar from 'sentry/components/performance/searchBar';
  9. import {t} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import type {NewQuery} from 'sentry/types/organization';
  12. import {defined} from 'sentry/utils';
  13. import {browserHistory} from 'sentry/utils/browserHistory';
  14. import EventView from 'sentry/utils/discover/eventView';
  15. import type {AggregationOutputType} from 'sentry/utils/discover/fields';
  16. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  17. import {decodeScalar} from 'sentry/utils/queryString';
  18. import {escapeFilterValue, MutableSearch} from 'sentry/utils/tokenizeSearch';
  19. import {useLocation} from 'sentry/utils/useLocation';
  20. import useOrganization from 'sentry/utils/useOrganization';
  21. import usePageFilters from 'sentry/utils/usePageFilters';
  22. import useRouter from 'sentry/utils/useRouter';
  23. import {prepareQueryForLandingPage} from 'sentry/views/performance/data';
  24. import {TOP_SCREENS} from 'sentry/views/performance/mobile/constants';
  25. import {MobileCursors} from 'sentry/views/performance/mobile/screenload/screens/constants';
  26. import {ScreensBarChart} from 'sentry/views/performance/mobile/screenload/screens/screenBarChart';
  27. import {
  28. ScreensTable,
  29. useTableQuery,
  30. } from 'sentry/views/performance/mobile/screenload/screens/screensTable';
  31. import {SETUP_CONTENT} from 'sentry/views/performance/mobile/screenload/screens/setupContent';
  32. import {TabbedCodeSnippet} from 'sentry/views/performance/mobile/screenload/screens/tabbedCodeSnippets';
  33. import {transformReleaseEvents} from 'sentry/views/performance/mobile/screenload/screens/utils';
  34. import useCrossPlatformProject from 'sentry/views/performance/mobile/useCrossPlatformProject';
  35. import useTruncatedReleaseNames from 'sentry/views/performance/mobile/useTruncatedRelease';
  36. import {getTransactionSearchQuery} from 'sentry/views/performance/utils';
  37. import ChartPanel from 'sentry/views/starfish/components/chartPanel';
  38. import {useTTFDConfigured} from 'sentry/views/starfish/queries/useHasTtfdConfigured';
  39. import {useReleaseSelection} from 'sentry/views/starfish/queries/useReleases';
  40. import {SpanMetricsField} from 'sentry/views/starfish/types';
  41. import {appendReleaseFilters} from 'sentry/views/starfish/utils/releaseComparison';
  42. export enum YAxis {
  43. WARM_START = 0,
  44. COLD_START = 1,
  45. TTID = 2,
  46. TTFD = 3,
  47. SLOW_FRAME_RATE = 4,
  48. FROZEN_FRAME_RATE = 5,
  49. THROUGHPUT = 6,
  50. COUNT = 7,
  51. SLOW_FRAMES = 8,
  52. FROZEN_FRAMES = 9,
  53. FRAMES_DELAY = 10,
  54. }
  55. export const YAXIS_COLUMNS: Readonly<Record<YAxis, string>> = {
  56. [YAxis.WARM_START]: 'avg(measurements.app_start_warm)',
  57. [YAxis.COLD_START]: 'avg(measurements.app_start_cold)',
  58. [YAxis.TTID]: 'avg(measurements.time_to_initial_display)',
  59. [YAxis.TTFD]: 'avg(measurements.time_to_full_display)',
  60. [YAxis.SLOW_FRAME_RATE]: 'avg(measurements.frames_slow_rate)',
  61. [YAxis.FROZEN_FRAME_RATE]: 'avg(measurements.frames_frozen_rate)',
  62. [YAxis.THROUGHPUT]: 'tpm()',
  63. [YAxis.COUNT]: 'count()',
  64. // Using span metrics
  65. [YAxis.SLOW_FRAMES]: 'avg(mobile.slow_frames)',
  66. [YAxis.FROZEN_FRAMES]: 'avg(mobile.frozen_frames)',
  67. [YAxis.FRAMES_DELAY]: 'avg(mobile.frames_delay)',
  68. };
  69. export const READABLE_YAXIS_LABELS: Readonly<Record<YAxis, string>> = {
  70. [YAxis.WARM_START]: 'avg(app_start_warm)',
  71. [YAxis.COLD_START]: 'avg(app_start_cold)',
  72. [YAxis.TTID]: 'avg(time_to_initial_display)',
  73. [YAxis.TTFD]: 'avg(time_to_full_display)',
  74. [YAxis.SLOW_FRAME_RATE]: 'avg(frames_slow_rate)',
  75. [YAxis.FROZEN_FRAME_RATE]: 'avg(frames_frozen_rate)',
  76. [YAxis.THROUGHPUT]: 'tpm()',
  77. [YAxis.COUNT]: 'count()',
  78. [YAxis.SLOW_FRAMES]: 'avg(mobile.slow_frames)',
  79. [YAxis.FROZEN_FRAMES]: 'avg(mobile.frozen_frames)',
  80. [YAxis.FRAMES_DELAY]: 'avg(mobile.frames_delay)',
  81. };
  82. export const CHART_TITLES: Readonly<Record<YAxis, string>> = {
  83. [YAxis.WARM_START]: t('Warm Start'),
  84. [YAxis.COLD_START]: t('Cold Start'),
  85. [YAxis.TTID]: t('Time To Initial Display'),
  86. [YAxis.TTFD]: t('Time To Full Display'),
  87. [YAxis.SLOW_FRAME_RATE]: t('Slow Frame Rate'),
  88. [YAxis.FROZEN_FRAME_RATE]: t('Frozen Frame Rate'),
  89. [YAxis.THROUGHPUT]: t('Throughput'),
  90. [YAxis.COUNT]: t('Total Count'),
  91. [YAxis.SLOW_FRAMES]: t('Slow Frames'),
  92. [YAxis.FROZEN_FRAMES]: t('Frozen Frames'),
  93. [YAxis.FRAMES_DELAY]: t('Frames Delay'),
  94. };
  95. export const OUTPUT_TYPE: Readonly<Record<YAxis, AggregationOutputType>> = {
  96. [YAxis.WARM_START]: 'duration',
  97. [YAxis.COLD_START]: 'duration',
  98. [YAxis.TTID]: 'duration',
  99. [YAxis.TTFD]: 'duration',
  100. [YAxis.SLOW_FRAME_RATE]: 'percentage',
  101. [YAxis.FROZEN_FRAME_RATE]: 'percentage',
  102. [YAxis.THROUGHPUT]: 'number',
  103. [YAxis.COUNT]: 'number',
  104. [YAxis.SLOW_FRAMES]: 'number',
  105. [YAxis.FROZEN_FRAMES]: 'number',
  106. [YAxis.FRAMES_DELAY]: 'duration',
  107. };
  108. type Props = {
  109. yAxes: YAxis[];
  110. additionalFilters?: string[];
  111. chartHeight?: number;
  112. };
  113. export function ScreensView({yAxes, additionalFilters, chartHeight}: Props) {
  114. const pageFilter = usePageFilters();
  115. const {selection} = pageFilter;
  116. const location = useLocation();
  117. const theme = useTheme();
  118. const organization = useOrganization();
  119. const {isProjectCrossPlatform, selectedPlatform} = useCrossPlatformProject();
  120. const {query: locationQuery} = location;
  121. const cursor = decodeScalar(location.query?.[MobileCursors.SCREENS_TABLE]);
  122. const yAxisCols = yAxes.map(val => YAXIS_COLUMNS[val]);
  123. const {
  124. primaryRelease,
  125. secondaryRelease,
  126. isLoading: isReleasesLoading,
  127. } = useReleaseSelection();
  128. const {truncatedPrimaryRelease, truncatedSecondaryRelease} = useTruncatedReleaseNames();
  129. const router = useRouter();
  130. const {hasTTFD} = useTTFDConfigured(additionalFilters);
  131. const queryString = useMemo(() => {
  132. const query = new MutableSearch([
  133. 'event.type:transaction',
  134. 'transaction.op:ui.load',
  135. ...(additionalFilters ?? []),
  136. ]);
  137. if (isProjectCrossPlatform) {
  138. query.addFilterValue('os.name', selectedPlatform);
  139. }
  140. const searchQuery = decodeScalar(locationQuery.query, '');
  141. if (searchQuery) {
  142. query.addStringFilter(prepareQueryForLandingPage(searchQuery, false));
  143. }
  144. return appendReleaseFilters(query, primaryRelease, secondaryRelease);
  145. }, [
  146. additionalFilters,
  147. isProjectCrossPlatform,
  148. locationQuery.query,
  149. primaryRelease,
  150. secondaryRelease,
  151. selectedPlatform,
  152. ]);
  153. const orderby = decodeScalar(locationQuery.sort, `-count`);
  154. const newQuery: NewQuery = {
  155. name: '',
  156. fields: [
  157. 'transaction',
  158. SpanMetricsField.PROJECT_ID,
  159. `avg_if(measurements.time_to_initial_display,release,${primaryRelease})`,
  160. `avg_if(measurements.time_to_initial_display,release,${secondaryRelease})`,
  161. `avg_if(measurements.time_to_full_display,release,${primaryRelease})`,
  162. `avg_if(measurements.time_to_full_display,release,${secondaryRelease})`,
  163. 'count()',
  164. ],
  165. query: queryString,
  166. dataset: DiscoverDatasets.METRICS,
  167. version: 2,
  168. projects: selection.projects,
  169. };
  170. newQuery.orderby = orderby;
  171. const tableEventView = EventView.fromNewQueryWithLocation(newQuery, location);
  172. const {
  173. data: topTransactionsData,
  174. isLoading: topTransactionsLoading,
  175. pageLinks,
  176. } = useTableQuery({
  177. eventView: tableEventView,
  178. enabled: !isReleasesLoading,
  179. referrer: 'api.starfish.mobile-screen-table',
  180. cursor,
  181. });
  182. const topTransactions = useMemo(() => {
  183. return (
  184. topTransactionsData?.data?.slice(0, 5).map(datum => datum.transaction as string) ??
  185. []
  186. );
  187. }, [topTransactionsData?.data]);
  188. const topEventsQueryString = useMemo(() => {
  189. const topEventsQuery = new MutableSearch([
  190. 'event.type:transaction',
  191. 'transaction.op:ui.load',
  192. ...(additionalFilters ?? []),
  193. ]);
  194. if (isProjectCrossPlatform) {
  195. topEventsQuery.addFilterValue('os.name', selectedPlatform);
  196. }
  197. return `${appendReleaseFilters(topEventsQuery, primaryRelease, secondaryRelease)} ${
  198. topTransactions.length > 0
  199. ? escapeFilterValue(
  200. `transaction:[${topTransactions.map(name => `"${name}"`).join()}]`
  201. )
  202. : ''
  203. }`.trim();
  204. }, [
  205. additionalFilters,
  206. isProjectCrossPlatform,
  207. primaryRelease,
  208. secondaryRelease,
  209. topTransactions,
  210. selectedPlatform,
  211. ]);
  212. const {data: releaseEvents, isLoading: isReleaseEventsLoading} = useTableQuery({
  213. eventView: EventView.fromNewQueryWithLocation(
  214. {
  215. name: '',
  216. fields: ['transaction', 'release', ...yAxisCols],
  217. orderby: yAxisCols[0],
  218. yAxis: yAxisCols,
  219. query: topEventsQueryString,
  220. dataset: DiscoverDatasets.METRICS,
  221. version: 2,
  222. },
  223. location
  224. ),
  225. enabled: !topTransactionsLoading,
  226. referrer: 'api.starfish.mobile-screen-bar-chart',
  227. });
  228. if (isReleasesLoading) {
  229. return (
  230. <LoadingContainer>
  231. <LoadingIndicator />
  232. </LoadingContainer>
  233. );
  234. }
  235. if (!defined(primaryRelease) && !isReleasesLoading) {
  236. return (
  237. <Alert type="warning" showIcon>
  238. {t(
  239. 'No screens found on recent releases. Please try a single iOS or Android project, a single environment or a smaller date range.'
  240. )}
  241. </Alert>
  242. );
  243. }
  244. const transformedReleaseEvents = transformReleaseEvents({
  245. yAxes,
  246. primaryRelease,
  247. secondaryRelease,
  248. colorPalette: theme.charts.getColorPalette(TOP_SCREENS - 2),
  249. releaseEvents,
  250. topTransactions,
  251. });
  252. const derivedQuery = getTransactionSearchQuery(location, tableEventView.query);
  253. const tableSearchFilters = new MutableSearch(['transaction.op:ui.load']);
  254. const handleCursor: CursorHandler = (newCursor, pathname, query_) => {
  255. browserHistory.push({
  256. pathname,
  257. query: {...query_, [MobileCursors.SCREENS_TABLE]: newCursor},
  258. });
  259. };
  260. return (
  261. <div data-test-id="starfish-mobile-view">
  262. <ChartsContainer>
  263. <Fragment>
  264. <ChartsContainerItem key="ttid">
  265. <ScreensBarChart
  266. chartOptions={[
  267. {
  268. title: t('%s by Top Screen', CHART_TITLES[yAxes[0]]),
  269. yAxis: YAXIS_COLUMNS[yAxes[0]],
  270. xAxisLabel: topTransactions,
  271. series: Object.values(
  272. transformedReleaseEvents[YAXIS_COLUMNS[yAxes[0]]]
  273. ),
  274. subtitle: primaryRelease
  275. ? t(
  276. '%s v. %s',
  277. truncatedPrimaryRelease,
  278. secondaryRelease ? truncatedSecondaryRelease : ''
  279. )
  280. : '',
  281. },
  282. ]}
  283. chartHeight={chartHeight ?? 180}
  284. isLoading={isReleaseEventsLoading}
  285. chartKey="screensChart1"
  286. />
  287. </ChartsContainerItem>
  288. {defined(hasTTFD) && !hasTTFD && yAxes[1] === YAxis.TTFD ? (
  289. <ChartsContainerWithHiddenOverflow>
  290. <ChartPanel title={CHART_TITLES[yAxes[1]]}>
  291. <TabbedCodeSnippet tabs={SETUP_CONTENT} />
  292. </ChartPanel>
  293. </ChartsContainerWithHiddenOverflow>
  294. ) : (
  295. <ChartsContainerItem key="ttfd">
  296. <ScreensBarChart
  297. chartOptions={[
  298. {
  299. title: t('%s by Top Screen', CHART_TITLES[yAxes[1]]),
  300. yAxis: YAXIS_COLUMNS[yAxes[1]],
  301. xAxisLabel: topTransactions,
  302. series: Object.values(
  303. transformedReleaseEvents[YAXIS_COLUMNS[yAxes[1]]]
  304. ),
  305. subtitle: primaryRelease
  306. ? t(
  307. '%s v. %s',
  308. truncatedPrimaryRelease,
  309. secondaryRelease ? truncatedSecondaryRelease : ''
  310. )
  311. : '',
  312. },
  313. ]}
  314. chartHeight={chartHeight ?? 180}
  315. isLoading={isReleaseEventsLoading}
  316. chartKey="screensChart1"
  317. />
  318. </ChartsContainerItem>
  319. )}
  320. </Fragment>
  321. </ChartsContainer>
  322. <StyledSearchBar
  323. eventView={tableEventView}
  324. onSearch={search => {
  325. router.push({
  326. pathname: router.location.pathname,
  327. query: {
  328. ...location.query,
  329. cursor: undefined,
  330. query: String(search).trim() || undefined,
  331. },
  332. });
  333. }}
  334. organization={organization}
  335. query={getFreeTextFromQuery(derivedQuery)}
  336. placeholder={t('Search for Screens')}
  337. additionalConditions={
  338. new MutableSearch(
  339. appendReleaseFilters(tableSearchFilters, primaryRelease, secondaryRelease)
  340. )
  341. }
  342. />
  343. <ScreensTable
  344. eventView={tableEventView}
  345. data={topTransactionsData}
  346. isLoading={topTransactionsLoading}
  347. pageLinks={pageLinks}
  348. onCursor={handleCursor}
  349. />
  350. </div>
  351. );
  352. }
  353. export function getFreeTextFromQuery(query: string) {
  354. const conditions = new MutableSearch(query);
  355. const transactionValues = conditions.getFilterValues('transaction');
  356. if (transactionValues.length) {
  357. return transactionValues[0];
  358. }
  359. if (conditions.freeText.length > 0) {
  360. // raw text query will be wrapped in wildcards in generatePerformanceEventView
  361. // so no need to wrap it here
  362. return conditions.freeText.join(' ');
  363. }
  364. return '';
  365. }
  366. const ChartsContainer = styled('div')`
  367. display: flex;
  368. flex-direction: row;
  369. flex-wrap: wrap;
  370. gap: ${space(2)};
  371. `;
  372. const ChartsContainerWithHiddenOverflow = styled('div')`
  373. flex: 1;
  374. overflow: hidden;
  375. `;
  376. const ChartsContainerItem = styled('div')`
  377. flex: 1;
  378. `;
  379. export const Spacer = styled('div')`
  380. margin-top: ${space(3)};
  381. `;
  382. const StyledSearchBar = styled(SearchBar)`
  383. margin-bottom: ${space(1)};
  384. `;