spanSamplesPanel.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import {useCallback} from 'react';
  2. import styled from '@emotion/styled';
  3. import omit from 'lodash/omit';
  4. import * as qs from 'query-string';
  5. import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
  6. import Link from 'sentry/components/links/link';
  7. import {t} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. import {trackAnalytics} from 'sentry/utils/analytics';
  10. import {PageAlert, PageAlertProvider} from 'sentry/utils/performance/contexts/pageAlert';
  11. import {decodeScalar} from 'sentry/utils/queryString';
  12. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  13. import useLocationQuery from 'sentry/utils/url/useLocationQuery';
  14. import {useLocation} from 'sentry/utils/useLocation';
  15. import useOrganization from 'sentry/utils/useOrganization';
  16. import useRouter from 'sentry/utils/useRouter';
  17. import DetailPanel from 'sentry/views/insights/common/components/detailPanel';
  18. import {useReleaseSelection} from 'sentry/views/insights/common/queries/useReleases';
  19. import {SpanSamplesContainer} from 'sentry/views/insights/mobile/common/components/spanSamplesPanelContainer';
  20. import useCrossPlatformProject from 'sentry/views/insights/mobile/common/queries/useCrossPlatformProject';
  21. import {useDomainViewFilters} from 'sentry/views/insights/pages/useFilters';
  22. import {type ModuleName, SpanMetricsField} from 'sentry/views/insights/types';
  23. import {getTransactionSummaryBaseUrl} from 'sentry/views/performance/transactionSummary/utils';
  24. type Props = {
  25. groupId: string;
  26. moduleName: ModuleName;
  27. onClose?: () => void;
  28. transactionRoute?: string;
  29. };
  30. const PRIMARY_SPAN_QUERY_KEY = 'primarySpanSearchQuery';
  31. const SECONDARY_SPAN_QUERY_KEY = 'secondarySpanSearchQuery';
  32. export function SpanSamplesPanel({
  33. groupId,
  34. moduleName,
  35. onClose,
  36. transactionRoute,
  37. }: Props) {
  38. const router = useRouter();
  39. const organization = useOrganization();
  40. const {view} = useDomainViewFilters();
  41. const {
  42. [SpanMetricsField.APP_START_TYPE]: appStartType,
  43. [SpanMetricsField.DEVICE_CLASS]: deviceClass,
  44. transaction: transactionName,
  45. transactionMethod,
  46. spanOp,
  47. spanDescription,
  48. } = useLocationQuery({
  49. fields: {
  50. [SpanMetricsField.APP_START_TYPE]: decodeScalar,
  51. [SpanMetricsField.DEVICE_CLASS]: decodeScalar,
  52. transaction: decodeScalar,
  53. transactionMethod: decodeScalar,
  54. spanOp: decodeScalar,
  55. spanDescription: decodeScalar,
  56. },
  57. });
  58. const additionalFilters = {
  59. ...(appStartType ? {[SpanMetricsField.APP_START_TYPE]: appStartType} : {}),
  60. ...(deviceClass ? {[SpanMetricsField.DEVICE_CLASS]: deviceClass} : {}),
  61. };
  62. transactionRoute ??= getTransactionSummaryBaseUrl(organization.slug, view);
  63. const {primaryRelease, secondaryRelease} = useReleaseSelection();
  64. // A a transaction name is required to show the panel, but a transaction
  65. // method is not
  66. const detailKey = transactionName
  67. ? [groupId, transactionName, transactionMethod].filter(Boolean).join(':')
  68. : undefined;
  69. const {query} = useLocation();
  70. const {project} = useCrossPlatformProject();
  71. const onOpenDetailPanel = useCallback(() => {
  72. if (query.transaction) {
  73. trackAnalytics('performance_views.sample_spans.opened', {
  74. organization,
  75. source: moduleName,
  76. });
  77. }
  78. }, [organization, query.transaction, moduleName]);
  79. const label =
  80. transactionMethod && !transactionName.startsWith(transactionMethod)
  81. ? `${transactionMethod} ${transactionName}`
  82. : transactionName;
  83. const link = normalizeUrl(
  84. `/organizations/${organization.slug}${transactionRoute}?${qs.stringify({
  85. project: query.project,
  86. transaction: transactionName,
  87. })}`
  88. );
  89. function defaultOnClose() {
  90. router.replace({
  91. pathname: router.location.pathname,
  92. query: omit(router.location.query, 'transaction', 'transactionMethod'),
  93. });
  94. }
  95. return (
  96. <PageAlertProvider>
  97. <DetailPanel
  98. detailKey={detailKey}
  99. onClose={() => {
  100. onClose ? onClose() : defaultOnClose();
  101. }}
  102. onOpen={onOpenDetailPanel}
  103. >
  104. <HeaderContainer>
  105. {project && (
  106. <SpanSummaryProjectAvatar
  107. project={project}
  108. direction="left"
  109. size={40}
  110. hasTooltip
  111. tooltip={project.slug}
  112. />
  113. )}
  114. <TitleContainer>
  115. {spanDescription && <SpanDescription>{spanDescription}</SpanDescription>}
  116. <Title>
  117. <Link to={link}>{label}</Link>
  118. </Title>
  119. </TitleContainer>
  120. </HeaderContainer>
  121. <PageAlert />
  122. <ChartsContainer>
  123. <ChartsContainerItem key="release1">
  124. <SpanSamplesContainer
  125. groupId={groupId}
  126. moduleName={moduleName}
  127. transactionName={transactionName}
  128. transactionMethod={transactionMethod}
  129. release={primaryRelease}
  130. sectionTitle={t('Release 1')}
  131. searchQueryKey={PRIMARY_SPAN_QUERY_KEY}
  132. spanOp={spanOp}
  133. additionalFilters={additionalFilters}
  134. />
  135. </ChartsContainerItem>
  136. <ChartsContainerItem key="release2">
  137. <SpanSamplesContainer
  138. groupId={groupId}
  139. moduleName={moduleName}
  140. transactionName={transactionName}
  141. transactionMethod={transactionMethod}
  142. release={secondaryRelease}
  143. sectionTitle={t('Release 2')}
  144. searchQueryKey={SECONDARY_SPAN_QUERY_KEY}
  145. spanOp={spanOp}
  146. additionalFilters={additionalFilters}
  147. />
  148. </ChartsContainerItem>
  149. </ChartsContainer>
  150. </DetailPanel>
  151. </PageAlertProvider>
  152. );
  153. }
  154. const SpanSummaryProjectAvatar = styled(ProjectAvatar)`
  155. padding-right: ${space(1)};
  156. `;
  157. const HeaderContainer = styled('div')`
  158. width: 100%;
  159. padding-bottom: ${space(2)};
  160. padding-top: ${space(1)};
  161. display: grid;
  162. grid-template-rows: auto auto auto;
  163. @media (min-width: ${p => p.theme.breakpoints.small}) {
  164. grid-template-rows: auto;
  165. grid-template-columns: auto 1fr auto;
  166. }
  167. `;
  168. const TitleContainer = styled('div')`
  169. width: 100%;
  170. position: relative;
  171. height: 40px;
  172. `;
  173. const Title = styled('h4')`
  174. position: absolute;
  175. bottom: 0;
  176. margin-bottom: 0;
  177. `;
  178. const SpanDescription = styled('div')`
  179. display: inline-block;
  180. white-space: nowrap;
  181. overflow: hidden;
  182. text-overflow: ellipsis;
  183. max-width: 35vw;
  184. `;
  185. const ChartsContainer = styled('div')`
  186. display: flex;
  187. flex-direction: row;
  188. gap: ${space(2)};
  189. align-items: top;
  190. `;
  191. const ChartsContainerItem = styled('div')`
  192. flex: 1;
  193. `;