index.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import {useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {LocationDescriptor} from 'history';
  4. import omit from 'lodash/omit';
  5. import type {Crumb} from 'sentry/components/breadcrumbs';
  6. import Breadcrumbs from 'sentry/components/breadcrumbs';
  7. import ButtonBar from 'sentry/components/buttonBar';
  8. import ErrorBoundary from 'sentry/components/errorBoundary';
  9. import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton';
  10. import * as Layout from 'sentry/components/layouts/thirds';
  11. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  12. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  13. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  14. import {t} from 'sentry/locale';
  15. import {space} from 'sentry/styles/space';
  16. import {DurationUnit} from 'sentry/utils/discover/fields';
  17. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  18. import {PageAlert, PageAlertProvider} from 'sentry/utils/performance/contexts/pageAlert';
  19. import {useLocation} from 'sentry/utils/useLocation';
  20. import useOrganization from 'sentry/utils/useOrganization';
  21. import useProjects from 'sentry/utils/useProjects';
  22. import useRouter from 'sentry/utils/useRouter';
  23. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  24. import {
  25. ScreenCharts,
  26. YAxis,
  27. } from 'sentry/views/performance/mobile/screenload/screenLoadSpans/charts';
  28. import {ScreenLoadEventSamples} from 'sentry/views/performance/mobile/screenload/screenLoadSpans/eventSamples';
  29. import {MetricsRibbon} from 'sentry/views/performance/mobile/screenload/screenLoadSpans/metricsRibbon';
  30. import {ScreenLoadSpanSamples} from 'sentry/views/performance/mobile/screenload/screenLoadSpans/samples';
  31. import {ScreenLoadSpansTable} from 'sentry/views/performance/mobile/screenload/screenLoadSpans/table';
  32. import {
  33. MobileCursors,
  34. MobileSortKeys,
  35. } from 'sentry/views/performance/mobile/screenload/screens/constants';
  36. import {PlatformSelector} from 'sentry/views/performance/mobile/screenload/screens/platformSelector';
  37. import {isCrossPlatform} from 'sentry/views/performance/mobile/screenload/screens/utils';
  38. import {
  39. PRIMARY_RELEASE_ALIAS,
  40. ReleaseComparisonSelector,
  41. SECONDARY_RELEASE_ALIAS,
  42. } from 'sentry/views/starfish/components/releaseSelector';
  43. import {StarfishPageFiltersContainer} from 'sentry/views/starfish/components/starfishPageFiltersContainer';
  44. import {SpanMetricsField} from 'sentry/views/starfish/types';
  45. import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters';
  46. type Query = {
  47. primaryRelease: string;
  48. project: string;
  49. secondaryRelease: string;
  50. spanGroup: string;
  51. transaction: string;
  52. [QueryParameterNames.SPANS_SORT]: string;
  53. spanDescription?: string;
  54. };
  55. function ScreenLoadSpans() {
  56. const location = useLocation<Query>();
  57. const organization = useOrganization();
  58. const router = useRouter();
  59. const {projects} = useProjects();
  60. const project = useMemo(() => {
  61. return projects.find(p => p.id === location.query.project);
  62. }, [location.query.project, projects]);
  63. const screenLoadModule: LocationDescriptor = {
  64. pathname: `/organizations/${organization.slug}/performance/mobile/screens/`,
  65. query: {
  66. ...omit(location.query, [
  67. QueryParameterNames.SPANS_SORT,
  68. 'transaction',
  69. SpanMetricsField.SPAN_OP,
  70. ]),
  71. },
  72. };
  73. const crumbs: Crumb[] = [
  74. {
  75. label: t('Performance'),
  76. to: normalizeUrl(`/organizations/${organization.slug}/performance/`),
  77. preservePageFilters: true,
  78. },
  79. {
  80. to: screenLoadModule,
  81. label: t('Screen Loads'),
  82. preservePageFilters: true,
  83. },
  84. {
  85. to: '',
  86. label: t('Screen Summary'),
  87. },
  88. ];
  89. const {
  90. spanGroup,
  91. primaryRelease,
  92. secondaryRelease,
  93. transaction: transactionName,
  94. spanDescription,
  95. } = location.query;
  96. return (
  97. <SentryDocumentTitle title={transactionName} orgSlug={organization.slug}>
  98. <Layout.Page>
  99. <PageAlertProvider>
  100. <Layout.Header>
  101. <Layout.HeaderContent>
  102. <Breadcrumbs crumbs={crumbs} />
  103. <HeaderWrapper>
  104. <Layout.Title>{transactionName}</Layout.Title>
  105. {organization.features.includes('spans-first-ui') &&
  106. project &&
  107. isCrossPlatform(project) && <PlatformSelector />}
  108. </HeaderWrapper>
  109. </Layout.HeaderContent>
  110. <Layout.HeaderActions>
  111. <ButtonBar gap={1}>
  112. <FeedbackWidgetButton />
  113. </ButtonBar>
  114. </Layout.HeaderActions>
  115. </Layout.Header>
  116. <Layout.Body>
  117. <Layout.Main fullWidth>
  118. <PageAlert />
  119. <StarfishPageFiltersContainer>
  120. <Container>
  121. <FilterContainer>
  122. <PageFilterBar condensed>
  123. <DatePageFilter />
  124. </PageFilterBar>
  125. <ReleaseComparisonSelector />
  126. </FilterContainer>
  127. <MetricsRibbon
  128. dataset={DiscoverDatasets.METRICS}
  129. filters={[
  130. 'event.type:transaction',
  131. 'transaction.op:ui.load',
  132. `transaction:${transactionName}`,
  133. ]}
  134. fields={[
  135. `avg_if(measurements.time_to_initial_display,release,${primaryRelease})`,
  136. `avg_if(measurements.time_to_initial_display,release,${secondaryRelease})`,
  137. `avg_if(measurements.time_to_full_display,release,${primaryRelease})`,
  138. `avg_if(measurements.time_to_full_display,release,${secondaryRelease})`,
  139. 'count()',
  140. ]}
  141. blocks={[
  142. {
  143. unit: DurationUnit.MILLISECOND,
  144. dataKey: `avg_if(measurements.time_to_initial_display,release,${primaryRelease})`,
  145. title: t('TTID (%s)', PRIMARY_RELEASE_ALIAS),
  146. },
  147. {
  148. unit: DurationUnit.MILLISECOND,
  149. dataKey: `avg_if(measurements.time_to_initial_display,release,${secondaryRelease})`,
  150. title: t('TTID (%s)', SECONDARY_RELEASE_ALIAS),
  151. },
  152. {
  153. unit: DurationUnit.MILLISECOND,
  154. dataKey: `avg_if(measurements.time_to_full_display,release,${primaryRelease})`,
  155. title: t('TTFD (%s)', PRIMARY_RELEASE_ALIAS),
  156. },
  157. {
  158. unit: DurationUnit.MILLISECOND,
  159. dataKey: `avg_if(measurements.time_to_full_display,release,${secondaryRelease})`,
  160. title: t('TTFD (%s)', SECONDARY_RELEASE_ALIAS),
  161. },
  162. {
  163. unit: 'count',
  164. dataKey: 'count()',
  165. title: t('Count'),
  166. },
  167. ]}
  168. referrer="api.starfish.mobile-screen-totals"
  169. />
  170. </Container>
  171. </StarfishPageFiltersContainer>
  172. <ErrorBoundary mini>
  173. <ScreenCharts
  174. yAxes={[YAxis.TTID, YAxis.TTFD, YAxis.COUNT]}
  175. additionalFilters={[`transaction:${transactionName}`]}
  176. chartHeight={120}
  177. project={project}
  178. />
  179. <SampleContainer>
  180. <SampleContainerItem>
  181. <ScreenLoadEventSamples
  182. release={primaryRelease}
  183. sortKey={MobileSortKeys.RELEASE_1_EVENT_SAMPLE_TABLE}
  184. cursorName={MobileCursors.RELEASE_1_EVENT_SAMPLE_TABLE}
  185. transaction={transactionName}
  186. showDeviceClassSelector
  187. project={project}
  188. />
  189. </SampleContainerItem>
  190. <SampleContainerItem>
  191. <ScreenLoadEventSamples
  192. release={secondaryRelease}
  193. sortKey={MobileSortKeys.RELEASE_2_EVENT_SAMPLE_TABLE}
  194. cursorName={MobileCursors.RELEASE_2_EVENT_SAMPLE_TABLE}
  195. transaction={transactionName}
  196. project={project}
  197. />
  198. </SampleContainerItem>
  199. </SampleContainer>
  200. <ScreenLoadSpansTable
  201. transaction={transactionName}
  202. primaryRelease={primaryRelease}
  203. secondaryRelease={secondaryRelease}
  204. project={project}
  205. />
  206. {spanGroup && (
  207. <ScreenLoadSpanSamples
  208. groupId={spanGroup}
  209. transactionName={transactionName}
  210. spanDescription={spanDescription}
  211. onClose={() => {
  212. router.replace({
  213. pathname: router.location.pathname,
  214. query: omit(
  215. router.location.query,
  216. 'spanGroup',
  217. 'transactionMethod'
  218. ),
  219. });
  220. }}
  221. />
  222. )}
  223. </ErrorBoundary>
  224. </Layout.Main>
  225. </Layout.Body>
  226. </PageAlertProvider>
  227. </Layout.Page>
  228. </SentryDocumentTitle>
  229. );
  230. }
  231. export default ScreenLoadSpans;
  232. const Container = styled('div')`
  233. display: grid;
  234. grid-template-rows: 1fr 1fr;
  235. grid-template-columns: 1fr;
  236. column-gap: ${space(2)};
  237. @media (min-width: ${p => p.theme.breakpoints.large}) {
  238. grid-template-rows: auto;
  239. grid-template-columns: auto minmax(100px, max-content);
  240. }
  241. `;
  242. const FilterContainer = styled('div')`
  243. display: grid;
  244. column-gap: ${space(1)};
  245. grid-template-rows: auto;
  246. grid-template-columns: auto 1fr;
  247. `;
  248. const SampleContainer = styled('div')`
  249. display: flex;
  250. flex-direction: row;
  251. flex-wrap: wrap;
  252. gap: ${space(2)};
  253. `;
  254. const SampleContainerItem = styled('div')`
  255. flex: 1;
  256. `;
  257. const HeaderWrapper = styled('div')`
  258. display: flex;
  259. `;