index.tsx 14 KB

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