index.tsx 14 KB

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