appStartup.tsx 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. import {useTheme} from '@emotion/react';
  2. import styled from '@emotion/styled';
  3. import Alert from 'sentry/components/alert';
  4. import SearchBar from 'sentry/components/performance/searchBar';
  5. import {t} from 'sentry/locale';
  6. import {space} from 'sentry/styles/space';
  7. import type {NewQuery} from 'sentry/types/organization';
  8. import {defined} from 'sentry/utils';
  9. import {trackAnalytics} from 'sentry/utils/analytics';
  10. import EventView from 'sentry/utils/discover/eventView';
  11. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  12. import {decodeScalar} from 'sentry/utils/queryString';
  13. import {escapeFilterValue, MutableSearch} from 'sentry/utils/tokenizeSearch';
  14. import {useLocation} from 'sentry/utils/useLocation';
  15. import useOrganization from 'sentry/utils/useOrganization';
  16. import usePageFilters from 'sentry/utils/usePageFilters';
  17. import useRouter from 'sentry/utils/useRouter';
  18. import {useReleaseSelection} from 'sentry/views/insights/common/queries/useReleases';
  19. import {appendReleaseFilters} from 'sentry/views/insights/common/utils/releaseComparison';
  20. import {AverageComparisonChart} from 'sentry/views/insights/mobile/appStarts/components/charts/averageComparisonChart';
  21. import {CountChart} from 'sentry/views/insights/mobile/appStarts/components/charts/countChart';
  22. import {COLD_START_TYPE} from 'sentry/views/insights/mobile/appStarts/components/startTypeSelector';
  23. import {AppStartScreens} from 'sentry/views/insights/mobile/appStarts/components/tables/screensTable';
  24. import useCrossPlatformProject from 'sentry/views/insights/mobile/common/queries/useCrossPlatformProject';
  25. import useTruncatedReleaseNames from 'sentry/views/insights/mobile/common/queries/useTruncatedRelease';
  26. import {TOP_SCREENS} from 'sentry/views/insights/mobile/constants';
  27. import {ScreensBarChart} from 'sentry/views/insights/mobile/screenload/components/charts/screenBarChart';
  28. import {getFreeTextFromQuery} from 'sentry/views/insights/mobile/screenload/components/screensView';
  29. import {useTableQuery} from 'sentry/views/insights/mobile/screenload/components/tables/screensTable';
  30. import {YAxis, YAXIS_COLUMNS} from 'sentry/views/insights/mobile/screenload/constants';
  31. import {transformReleaseEvents} from 'sentry/views/insights/mobile/screenload/utils';
  32. import {ModuleName, SpanMetricsField} from 'sentry/views/insights/types';
  33. import {prepareQueryForLandingPage} from 'sentry/views/performance/data';
  34. import {getTransactionSearchQuery} from 'sentry/views/performance/utils';
  35. const Y_AXES = [YAxis.COLD_START, YAxis.WARM_START];
  36. const Y_AXIS_COLS = [YAXIS_COLUMNS[YAxis.COLD_START], YAXIS_COLUMNS[YAxis.WARM_START]];
  37. type Props = {
  38. additionalFilters?: string[];
  39. chartHeight?: number;
  40. };
  41. function AppStartup({additionalFilters, chartHeight}: Props) {
  42. const theme = useTheme();
  43. const pageFilter = usePageFilters();
  44. const {selection} = pageFilter;
  45. const location = useLocation();
  46. const organization = useOrganization();
  47. const {query: locationQuery} = location;
  48. const {
  49. primaryRelease,
  50. secondaryRelease,
  51. isLoading: isReleasesLoading,
  52. } = useReleaseSelection();
  53. const {truncatedPrimaryRelease, truncatedSecondaryRelease} = useTruncatedReleaseNames();
  54. const {isProjectCrossPlatform, selectedPlatform} = useCrossPlatformProject();
  55. const router = useRouter();
  56. const appStartType =
  57. decodeScalar(location.query[SpanMetricsField.APP_START_TYPE]) ?? COLD_START_TYPE;
  58. const query = new MutableSearch([
  59. 'event.type:transaction',
  60. 'transaction.op:ui.load',
  61. `count_starts(measurements.app_start_${appStartType}):>0`,
  62. ...(additionalFilters ?? []),
  63. ]);
  64. if (isProjectCrossPlatform) {
  65. query.addFilterValue('os.name', selectedPlatform);
  66. }
  67. const searchQuery = decodeScalar(locationQuery.query, '');
  68. if (searchQuery) {
  69. query.addStringFilter(prepareQueryForLandingPage(searchQuery, false));
  70. }
  71. const queryString = appendReleaseFilters(query, primaryRelease, secondaryRelease);
  72. const sortCountField = `count_starts_measurements_app_start_${appStartType}`;
  73. const orderby = decodeScalar(locationQuery.sort, `-${sortCountField}`);
  74. const newQuery: NewQuery = {
  75. name: '',
  76. fields: [
  77. 'transaction',
  78. SpanMetricsField.PROJECT_ID,
  79. `avg_if(measurements.app_start_${appStartType},release,${primaryRelease})`,
  80. `avg_if(measurements.app_start_${appStartType},release,${secondaryRelease})`,
  81. `avg_compare(measurements.app_start_${appStartType},release,${primaryRelease},${secondaryRelease})`,
  82. 'count_starts(measurements.app_start_cold)',
  83. 'count_starts(measurements.app_start_warm)',
  84. ],
  85. query: queryString,
  86. dataset: DiscoverDatasets.METRICS,
  87. version: 2,
  88. projects: selection.projects,
  89. };
  90. newQuery.orderby = orderby;
  91. const tableEventView = EventView.fromNewQueryWithLocation(newQuery, location);
  92. const {
  93. data: topTransactionsData,
  94. isPending: topTransactionsLoading,
  95. pageLinks,
  96. } = useTableQuery({
  97. eventView: tableEventView,
  98. enabled: !isReleasesLoading,
  99. referrer: 'api.starfish.mobile-startup-screen-table',
  100. });
  101. const topTransactions =
  102. topTransactionsData?.data?.slice(0, 5).map(datum => datum.transaction as string) ??
  103. [];
  104. const topEventsQuery = new MutableSearch([
  105. 'event.type:transaction',
  106. 'transaction.op:ui.load',
  107. ...(additionalFilters ?? []),
  108. ]);
  109. if (isProjectCrossPlatform) {
  110. topEventsQuery.addFilterValue('os.name', selectedPlatform);
  111. }
  112. const topEventsQueryString = `${appendReleaseFilters(
  113. topEventsQuery,
  114. primaryRelease,
  115. secondaryRelease
  116. )} ${
  117. topTransactions.length > 0
  118. ? escapeFilterValue(
  119. `transaction:[${topTransactions.map(name => `"${name}"`).join()}]`
  120. )
  121. : ''
  122. }`.trim();
  123. const {data: releaseEvents, isPending: isReleaseEventsLoading} = useTableQuery({
  124. eventView: EventView.fromNewQueryWithPageFilters(
  125. {
  126. name: '',
  127. fields: ['transaction', 'release', ...Y_AXIS_COLS],
  128. orderby: Y_AXIS_COLS[0],
  129. yAxis: Y_AXIS_COLS,
  130. query: topEventsQueryString,
  131. dataset: DiscoverDatasets.METRICS,
  132. version: 2,
  133. },
  134. selection
  135. ),
  136. enabled: !topTransactionsLoading,
  137. referrer: 'api.starfish.mobile-startup-bar-chart',
  138. });
  139. if (!defined(primaryRelease) && !isReleasesLoading) {
  140. return (
  141. <Alert type="warning" showIcon>
  142. {t(
  143. 'No screens found on recent releases. Please try a single iOS or Android project, a single environment or a smaller date range.'
  144. )}
  145. </Alert>
  146. );
  147. }
  148. const derivedQuery = getTransactionSearchQuery(location, tableEventView.query);
  149. const tableSearchFilters = new MutableSearch([
  150. 'event.type:transaction',
  151. 'transaction.op:ui.load',
  152. ]);
  153. const transformedReleaseEvents = transformReleaseEvents({
  154. yAxes: Y_AXES,
  155. primaryRelease,
  156. secondaryRelease,
  157. colorPalette: theme.charts.getColorPalette(TOP_SCREENS - 2),
  158. releaseEvents,
  159. topTransactions,
  160. });
  161. const countTopScreens = Math.min(TOP_SCREENS, topTransactions.length);
  162. const [singularTopScreenTitle, pluralTopScreenTitle] =
  163. appStartType === COLD_START_TYPE
  164. ? [t('Top Screen Cold Start'), t('Top %s Screen Cold Starts', countTopScreens)]
  165. : [t('Top Screen Warm Start'), t('Top %s Screen Warm Starts', countTopScreens)];
  166. const yAxis =
  167. YAXIS_COLUMNS[appStartType === COLD_START_TYPE ? YAxis.COLD_START : YAxis.WARM_START];
  168. return (
  169. <div data-test-id="starfish-mobile-app-startup-view">
  170. <ChartContainer>
  171. <AverageComparisonChart chartHeight={chartHeight} />
  172. <ScreensBarChart
  173. chartOptions={[
  174. {
  175. title: countTopScreens > 1 ? pluralTopScreenTitle : singularTopScreenTitle,
  176. yAxis,
  177. xAxisLabel: topTransactions,
  178. series: Object.values(transformedReleaseEvents[yAxis]),
  179. subtitle: primaryRelease
  180. ? t(
  181. '%s v. %s',
  182. truncatedPrimaryRelease,
  183. secondaryRelease ? truncatedSecondaryRelease : ''
  184. )
  185. : '',
  186. },
  187. ]}
  188. chartHeight={chartHeight}
  189. isLoading={isReleaseEventsLoading || isReleasesLoading}
  190. chartKey={`${appStartType}Start`}
  191. />
  192. <CountChart chartHeight={chartHeight} />
  193. </ChartContainer>
  194. <StyledSearchBar
  195. eventView={tableEventView}
  196. onSearch={search => {
  197. trackAnalytics('insight.general.search', {
  198. organization,
  199. query: search,
  200. source: ModuleName.APP_START,
  201. });
  202. router.push({
  203. pathname: router.location.pathname,
  204. query: {
  205. ...location.query,
  206. cursor: undefined,
  207. query: String(search).trim() || undefined,
  208. },
  209. });
  210. }}
  211. organization={organization}
  212. query={getFreeTextFromQuery(derivedQuery)}
  213. placeholder={t('Search for Screen')}
  214. additionalConditions={
  215. new MutableSearch(
  216. appendReleaseFilters(tableSearchFilters, primaryRelease, secondaryRelease)
  217. )
  218. }
  219. />
  220. <AppStartScreens
  221. eventView={tableEventView}
  222. data={topTransactionsData}
  223. isLoading={topTransactionsLoading}
  224. pageLinks={pageLinks}
  225. />
  226. </div>
  227. );
  228. }
  229. export default AppStartup;
  230. const StyledSearchBar = styled(SearchBar)`
  231. margin-bottom: ${space(1)};
  232. `;
  233. const ChartContainer = styled('div')`
  234. display: grid;
  235. grid-template-columns: 33% 33% 33%;
  236. gap: ${space(1)};
  237. `;