header.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. import {Fragment, useCallback} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import Feature from 'sentry/components/acl/feature';
  5. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  6. import ButtonBar from 'sentry/components/buttonBar';
  7. import {CreateAlertFromViewButton} from 'sentry/components/createAlertButton';
  8. import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton';
  9. import IdBadge from 'sentry/components/idBadge';
  10. import * as Layout from 'sentry/components/layouts/thirds';
  11. import ReplayCountBadge from 'sentry/components/replays/replayCountBadge';
  12. import {TabList} from 'sentry/components/tabs';
  13. import {Tooltip} from 'sentry/components/tooltip';
  14. import {t} from 'sentry/locale';
  15. import type {Organization} from 'sentry/types/organization';
  16. import type {Project} from 'sentry/types/project';
  17. import {trackAnalytics} from 'sentry/utils/analytics';
  18. import type EventView from 'sentry/utils/discover/eventView';
  19. import type {MetricsCardinalityContext} from 'sentry/utils/performance/contexts/metricsCardinality';
  20. import HasMeasurementsQuery from 'sentry/utils/performance/vitals/hasMeasurementsQuery';
  21. import {isProfilingSupportedOrProjectHasProfiles} from 'sentry/utils/profiling/platforms';
  22. import useReplayCountForTransactions from 'sentry/utils/replayCount/useReplayCountForTransactions';
  23. import projectSupportsReplay from 'sentry/utils/replays/projectSupportsReplay';
  24. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  25. import {useNavigate} from 'sentry/utils/useNavigate';
  26. import {AiHeader} from 'sentry/views/insights/pages/ai/aiPageHeader';
  27. import {AI_LANDING_SUB_PATH} from 'sentry/views/insights/pages/ai/settings';
  28. import {BackendHeader} from 'sentry/views/insights/pages/backend/backendPageHeader';
  29. import {BACKEND_LANDING_SUB_PATH} from 'sentry/views/insights/pages/backend/settings';
  30. import {FrontendHeader} from 'sentry/views/insights/pages/frontend/frontendPageHeader';
  31. import {FRONTEND_LANDING_SUB_PATH} from 'sentry/views/insights/pages/frontend/settings';
  32. import {MobileHeader} from 'sentry/views/insights/pages/mobile/mobilePageHeader';
  33. import {MOBILE_LANDING_SUB_PATH} from 'sentry/views/insights/pages/mobile/settings';
  34. import {useDomainViewFilters} from 'sentry/views/insights/pages/useFilters';
  35. import Breadcrumb, {getTabCrumbs} from 'sentry/views/performance/breadcrumb';
  36. import {aggregateWaterfallRouteWithQuery} from 'sentry/views/performance/transactionSummary/aggregateSpanWaterfall/utils';
  37. import {TAB_ANALYTICS} from 'sentry/views/performance/transactionSummary/pageLayout';
  38. import {eventsRouteWithQuery} from 'sentry/views/performance/transactionSummary/transactionEvents/utils';
  39. import {profilesRouteWithQuery} from 'sentry/views/performance/transactionSummary/transactionProfiles/utils';
  40. import {replaysRouteWithQuery} from 'sentry/views/performance/transactionSummary/transactionReplays/utils';
  41. import {spansRouteWithQuery} from 'sentry/views/performance/transactionSummary/transactionSpans/utils';
  42. import {tagsRouteWithQuery} from 'sentry/views/performance/transactionSummary/transactionTags/utils';
  43. import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
  44. import {getSelectedProjectPlatforms} from 'sentry/views/performance/utils';
  45. import {getCurrentLandingDisplay, LandingDisplayField} from '../landing/utils';
  46. import Tab from './tabs';
  47. import TeamKeyTransactionButton from './teamKeyTransactionButton';
  48. import TransactionThresholdButton from './transactionThresholdButton';
  49. import type {TransactionThresholdMetric} from './transactionThresholdModal';
  50. export type Props = {
  51. currentTab: Tab;
  52. eventView: EventView;
  53. hasWebVitals: 'maybe' | 'yes' | 'no';
  54. location: Location;
  55. organization: Organization;
  56. projectId: string;
  57. projects: Project[];
  58. transactionName: string;
  59. metricsCardinality?: MetricsCardinalityContext;
  60. onChangeThreshold?: (threshold: number, metric: TransactionThresholdMetric) => void;
  61. };
  62. function TransactionHeader({
  63. eventView,
  64. organization,
  65. projects,
  66. projectId,
  67. metricsCardinality,
  68. location,
  69. transactionName,
  70. onChangeThreshold,
  71. currentTab,
  72. hasWebVitals,
  73. }: Props) {
  74. const {isInDomainView, view} = useDomainViewFilters();
  75. const navigate = useNavigate();
  76. const getNewRoute = useCallback(
  77. (newTab: Tab) => {
  78. if (!transactionName) {
  79. return {};
  80. }
  81. const routeQuery = {
  82. organization,
  83. transaction: transactionName,
  84. projectID: projectId,
  85. query: location.query,
  86. view,
  87. };
  88. switch (newTab) {
  89. case Tab.TAGS:
  90. return tagsRouteWithQuery(routeQuery);
  91. case Tab.EVENTS:
  92. return eventsRouteWithQuery(routeQuery);
  93. case Tab.SPANS:
  94. return spansRouteWithQuery(routeQuery);
  95. case Tab.REPLAYS:
  96. return replaysRouteWithQuery(routeQuery);
  97. case Tab.PROFILING: {
  98. return profilesRouteWithQuery(routeQuery);
  99. }
  100. case Tab.AGGREGATE_WATERFALL:
  101. return aggregateWaterfallRouteWithQuery(routeQuery);
  102. case Tab.TRANSACTION_SUMMARY:
  103. default:
  104. return transactionSummaryRouteWithQuery(routeQuery);
  105. }
  106. },
  107. [location.query, organization, projectId, transactionName, view]
  108. );
  109. const onTabChange = useCallback(
  110. (newTab: string) => {
  111. // Prevent infinite rerenders
  112. if (newTab === currentTab) {
  113. return;
  114. }
  115. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  116. const analyticsKey = TAB_ANALYTICS[newTab];
  117. if (analyticsKey) {
  118. trackAnalytics(analyticsKey, {
  119. organization,
  120. project_platforms: getSelectedProjectPlatforms(location, projects),
  121. });
  122. }
  123. navigate(normalizeUrl(getNewRoute(newTab as Tab)));
  124. },
  125. [getNewRoute, organization, location, projects, currentTab, navigate]
  126. );
  127. function handleCreateAlertSuccess() {
  128. trackAnalytics('performance_views.summary.create_alert_clicked', {
  129. organization,
  130. });
  131. }
  132. const project = projects.find(p => p.id === projectId);
  133. const hasSessionReplay =
  134. organization.features.includes('session-replay') &&
  135. project &&
  136. projectSupportsReplay(project);
  137. const hasProfiling =
  138. project &&
  139. organization.features.includes('profiling') &&
  140. isProfilingSupportedOrProjectHasProfiles(project);
  141. const hasAggregateWaterfall = organization.features.includes(
  142. 'insights-initial-modules'
  143. );
  144. const getWebVitals = useCallback(
  145. (hasMeasurements: boolean) => {
  146. switch (hasWebVitals) {
  147. case 'maybe':
  148. // need to check if the web vitals tab should be shown
  149. // frontend projects should always show the web vitals tab
  150. if (
  151. getCurrentLandingDisplay(location, projects, eventView).field ===
  152. LandingDisplayField.FRONTEND_OTHER
  153. ) {
  154. return true;
  155. }
  156. // if it is not a frontend project, then we check to see if there
  157. // are any web vitals associated with the transaction recently
  158. return hasMeasurements;
  159. case 'yes':
  160. // always show the web vitals tab
  161. return true;
  162. case 'no':
  163. default:
  164. // never show the web vitals tab
  165. return false;
  166. }
  167. },
  168. [hasWebVitals, location, projects, eventView]
  169. );
  170. const {getReplayCountForTransaction} = useReplayCountForTransactions({
  171. statsPeriod: '90d',
  172. });
  173. const replaysCount = getReplayCountForTransaction(transactionName);
  174. const tabList = (
  175. <HasMeasurementsQuery
  176. location={location}
  177. orgSlug={organization.slug}
  178. eventView={eventView}
  179. transaction={transactionName}
  180. type="web"
  181. >
  182. {({hasMeasurements}) => {
  183. const renderWebVitals = getWebVitals(!!hasMeasurements);
  184. return (
  185. <TabList
  186. hideBorder
  187. outerWrapStyles={{
  188. gridColumn: '1 / -1',
  189. }}
  190. >
  191. <TabList.Item key={Tab.TRANSACTION_SUMMARY}>{t('Overview')}</TabList.Item>
  192. <TabList.Item key={Tab.EVENTS}>{t('Sampled Events')}</TabList.Item>
  193. <TabList.Item key={Tab.TAGS}>{t('Tags')}</TabList.Item>
  194. <TabList.Item key={Tab.SPANS}>{t('Spans')}</TabList.Item>
  195. <TabList.Item
  196. key={Tab.WEB_VITALS}
  197. textValue={t('Web Vitals')}
  198. hidden={!renderWebVitals}
  199. >
  200. {t('Web Vitals')}
  201. </TabList.Item>
  202. <TabList.Item
  203. key={Tab.REPLAYS}
  204. textValue={t('Replays')}
  205. hidden={!hasSessionReplay}
  206. >
  207. {t('Replays')}
  208. <ReplayCountBadge count={replaysCount} />
  209. </TabList.Item>
  210. <TabList.Item
  211. key={Tab.PROFILING}
  212. textValue={t('Profiling')}
  213. hidden={!hasProfiling}
  214. >
  215. {t('Profiles')}
  216. </TabList.Item>
  217. <TabList.Item
  218. key={Tab.AGGREGATE_WATERFALL}
  219. textValue={t('Aggregate Spans')}
  220. hidden={!hasAggregateWaterfall}
  221. >
  222. {t('Aggregate Spans')}
  223. </TabList.Item>
  224. </TabList>
  225. );
  226. }}
  227. </HasMeasurementsQuery>
  228. );
  229. if (isInDomainView) {
  230. const headerProps = {
  231. headerTitle: (
  232. <Fragment>
  233. {project && (
  234. <IdBadge
  235. project={project}
  236. avatarSize={28}
  237. hideName
  238. avatarProps={{hasTooltip: true, tooltip: project.slug}}
  239. />
  240. )}
  241. <Tooltip showOnlyOnOverflow skipWrapper title={transactionName}>
  242. <TransactionName>{transactionName}</TransactionName>
  243. </Tooltip>
  244. </Fragment>
  245. ),
  246. hideDefaultTabs: true,
  247. tabs: {
  248. onTabChange,
  249. tabList,
  250. value: currentTab,
  251. },
  252. breadcrumbs: getTabCrumbs({
  253. location,
  254. organization,
  255. transaction: {
  256. name: transactionName,
  257. project: projectId,
  258. },
  259. view,
  260. }),
  261. headerActions: (
  262. <Fragment>
  263. <Feature organization={organization} features="incidents">
  264. {({hasFeature}) =>
  265. hasFeature && !metricsCardinality?.isLoading ? (
  266. <CreateAlertFromViewButton
  267. size="sm"
  268. eventView={eventView}
  269. organization={organization}
  270. projects={projects}
  271. onClick={handleCreateAlertSuccess}
  272. referrer="performance"
  273. alertType="trans_duration"
  274. aria-label={t('Create Alert')}
  275. disableMetricDataset={
  276. metricsCardinality?.outcome?.forceTransactionsOnly
  277. }
  278. />
  279. ) : null
  280. }
  281. </Feature>
  282. <TeamKeyTransactionButton
  283. eventView={eventView}
  284. organization={organization}
  285. transactionName={transactionName}
  286. />
  287. <GuideAnchor target="project_transaction_threshold_override" position="bottom">
  288. <TransactionThresholdButton
  289. organization={organization}
  290. transactionName={transactionName}
  291. eventView={eventView}
  292. onChangeThreshold={onChangeThreshold}
  293. />
  294. </GuideAnchor>
  295. </Fragment>
  296. ),
  297. };
  298. if (view === FRONTEND_LANDING_SUB_PATH) {
  299. return <FrontendHeader {...headerProps} />;
  300. }
  301. if (view === BACKEND_LANDING_SUB_PATH) {
  302. return <BackendHeader {...headerProps} />;
  303. }
  304. if (view === AI_LANDING_SUB_PATH) {
  305. return <AiHeader {...headerProps} />;
  306. }
  307. if (view === MOBILE_LANDING_SUB_PATH) {
  308. return <MobileHeader {...headerProps} />;
  309. }
  310. }
  311. return (
  312. <Layout.Header>
  313. <Layout.HeaderContent>
  314. <Breadcrumb
  315. organization={organization}
  316. location={location}
  317. transaction={{
  318. project: projectId,
  319. name: transactionName,
  320. }}
  321. />
  322. <Layout.Title>
  323. {project && (
  324. <IdBadge
  325. project={project}
  326. avatarSize={28}
  327. hideName
  328. avatarProps={{hasTooltip: true, tooltip: project.slug}}
  329. />
  330. )}
  331. <Tooltip showOnlyOnOverflow skipWrapper title={transactionName}>
  332. <TransactionName>{transactionName}</TransactionName>
  333. </Tooltip>
  334. </Layout.Title>
  335. </Layout.HeaderContent>
  336. <Layout.HeaderActions>
  337. <ButtonBar gap={1}>
  338. <Feature organization={organization} features="incidents">
  339. {({hasFeature}) =>
  340. hasFeature && !metricsCardinality?.isLoading ? (
  341. <CreateAlertFromViewButton
  342. size="sm"
  343. eventView={eventView}
  344. organization={organization}
  345. projects={projects}
  346. onClick={handleCreateAlertSuccess}
  347. referrer="performance"
  348. alertType="trans_duration"
  349. aria-label={t('Create Alert')}
  350. disableMetricDataset={
  351. metricsCardinality?.outcome?.forceTransactionsOnly
  352. }
  353. />
  354. ) : null
  355. }
  356. </Feature>
  357. <TeamKeyTransactionButton
  358. transactionName={transactionName}
  359. eventView={eventView}
  360. organization={organization}
  361. />
  362. <GuideAnchor target="project_transaction_threshold_override" position="bottom">
  363. <TransactionThresholdButton
  364. organization={organization}
  365. transactionName={transactionName}
  366. eventView={eventView}
  367. onChangeThreshold={onChangeThreshold}
  368. />
  369. </GuideAnchor>
  370. <FeedbackWidgetButton />
  371. </ButtonBar>
  372. </Layout.HeaderActions>
  373. <HasMeasurementsQuery
  374. location={location}
  375. orgSlug={organization.slug}
  376. eventView={eventView}
  377. transaction={transactionName}
  378. type="web"
  379. >
  380. {({hasMeasurements}) => {
  381. const renderWebVitals = getWebVitals(!!hasMeasurements);
  382. return (
  383. <TabList
  384. hideBorder
  385. outerWrapStyles={{
  386. gridColumn: '1 / -1',
  387. }}
  388. >
  389. <TabList.Item key={Tab.TRANSACTION_SUMMARY}>{t('Overview')}</TabList.Item>
  390. <TabList.Item key={Tab.EVENTS}>{t('Sampled Events')}</TabList.Item>
  391. <TabList.Item key={Tab.TAGS}>{t('Tags')}</TabList.Item>
  392. <TabList.Item key={Tab.SPANS}>{t('Spans')}</TabList.Item>
  393. <TabList.Item
  394. key={Tab.WEB_VITALS}
  395. textValue={t('Web Vitals')}
  396. hidden={!renderWebVitals}
  397. >
  398. {t('Web Vitals')}
  399. </TabList.Item>
  400. <TabList.Item
  401. key={Tab.REPLAYS}
  402. textValue={t('Replays')}
  403. hidden={!hasSessionReplay}
  404. >
  405. {t('Replays')}
  406. <ReplayCountBadge count={replaysCount} />
  407. </TabList.Item>
  408. <TabList.Item
  409. key={Tab.PROFILING}
  410. textValue={t('Profiling')}
  411. hidden={!hasProfiling}
  412. >
  413. {t('Profiles')}
  414. </TabList.Item>
  415. <TabList.Item
  416. key={Tab.AGGREGATE_WATERFALL}
  417. textValue={t('Aggregate Spans')}
  418. hidden={!hasAggregateWaterfall}
  419. >
  420. {t('Aggregate Spans')}
  421. </TabList.Item>
  422. </TabList>
  423. );
  424. }}
  425. </HasMeasurementsQuery>
  426. </Layout.Header>
  427. );
  428. }
  429. const TransactionName = styled('div')`
  430. ${p => p.theme.overflowEllipsis}
  431. `;
  432. export default TransactionHeader;