header.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  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. orgSlug: organization.slug,
  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.slug, projectId, transactionName, view]
  108. );
  109. const onTabChange = useCallback(
  110. (newTab: string) => {
  111. // Prevent infinite rerenders
  112. if (newTab === currentTab) {
  113. return;
  114. }
  115. const analyticsKey = TAB_ANALYTICS[newTab];
  116. if (analyticsKey) {
  117. trackAnalytics(analyticsKey, {
  118. organization,
  119. project_platforms: getSelectedProjectPlatforms(location, projects),
  120. });
  121. }
  122. navigate(normalizeUrl(getNewRoute(newTab as Tab)));
  123. },
  124. [getNewRoute, organization, location, projects, currentTab, navigate]
  125. );
  126. function handleCreateAlertSuccess() {
  127. trackAnalytics('performance_views.summary.create_alert_clicked', {
  128. organization,
  129. });
  130. }
  131. const project = projects.find(p => p.id === projectId);
  132. const hasSessionReplay =
  133. organization.features.includes('session-replay') &&
  134. project &&
  135. projectSupportsReplay(project);
  136. const hasProfiling =
  137. project &&
  138. organization.features.includes('profiling') &&
  139. isProfilingSupportedOrProjectHasProfiles(project);
  140. const hasAggregateWaterfall = organization.features.includes(
  141. 'insights-initial-modules'
  142. );
  143. const getWebVitals = useCallback(
  144. (hasMeasurements: boolean) => {
  145. switch (hasWebVitals) {
  146. case 'maybe':
  147. // need to check if the web vitals tab should be shown
  148. // frontend projects should always show the web vitals tab
  149. if (
  150. getCurrentLandingDisplay(location, projects, eventView).field ===
  151. LandingDisplayField.FRONTEND_OTHER
  152. ) {
  153. return true;
  154. }
  155. // if it is not a frontend project, then we check to see if there
  156. // are any web vitals associated with the transaction recently
  157. return hasMeasurements;
  158. case 'yes':
  159. // always show the web vitals tab
  160. return true;
  161. case 'no':
  162. default:
  163. // never show the web vitals tab
  164. return false;
  165. }
  166. },
  167. [hasWebVitals, location, projects, eventView]
  168. );
  169. const {getReplayCountForTransaction} = useReplayCountForTransactions({
  170. statsPeriod: '90d',
  171. });
  172. const replaysCount = getReplayCountForTransaction(transactionName);
  173. const tabList = (
  174. <HasMeasurementsQuery
  175. location={location}
  176. orgSlug={organization.slug}
  177. eventView={eventView}
  178. transaction={transactionName}
  179. type="web"
  180. >
  181. {({hasMeasurements}) => {
  182. const renderWebVitals = getWebVitals(!!hasMeasurements);
  183. return (
  184. <TabList
  185. hideBorder
  186. outerWrapStyles={{
  187. gridColumn: '1 / -1',
  188. }}
  189. >
  190. <TabList.Item key={Tab.TRANSACTION_SUMMARY}>{t('Overview')}</TabList.Item>
  191. <TabList.Item key={Tab.EVENTS}>{t('Sampled Events')}</TabList.Item>
  192. <TabList.Item key={Tab.TAGS}>{t('Tags')}</TabList.Item>
  193. <TabList.Item key={Tab.SPANS}>{t('Spans')}</TabList.Item>
  194. <TabList.Item
  195. key={Tab.WEB_VITALS}
  196. textValue={t('Web Vitals')}
  197. hidden={!renderWebVitals}
  198. >
  199. {t('Web Vitals')}
  200. </TabList.Item>
  201. <TabList.Item
  202. key={Tab.REPLAYS}
  203. textValue={t('Replays')}
  204. hidden={!hasSessionReplay}
  205. >
  206. {t('Replays')}
  207. <ReplayCountBadge count={replaysCount} />
  208. </TabList.Item>
  209. <TabList.Item
  210. key={Tab.PROFILING}
  211. textValue={t('Profiling')}
  212. hidden={!hasProfiling}
  213. >
  214. {t('Profiles')}
  215. </TabList.Item>
  216. <TabList.Item
  217. key={Tab.AGGREGATE_WATERFALL}
  218. textValue={t('Aggregate Spans')}
  219. hidden={!hasAggregateWaterfall}
  220. >
  221. {t('Aggregate Spans')}
  222. </TabList.Item>
  223. </TabList>
  224. );
  225. }}
  226. </HasMeasurementsQuery>
  227. );
  228. if (isInDomainView) {
  229. const headerProps = {
  230. headerTitle: (
  231. <Fragment>
  232. {project && (
  233. <IdBadge
  234. project={project}
  235. avatarSize={28}
  236. hideName
  237. avatarProps={{hasTooltip: true, tooltip: project.slug}}
  238. />
  239. )}
  240. <Tooltip showOnlyOnOverflow skipWrapper title={transactionName}>
  241. <TransactionName>{transactionName}</TransactionName>
  242. </Tooltip>
  243. </Fragment>
  244. ),
  245. hideDefaultTabs: true,
  246. tabs: {
  247. onTabChange,
  248. tabList,
  249. value: currentTab,
  250. },
  251. breadcrumbs: getTabCrumbs({
  252. location,
  253. organization,
  254. tab: currentTab,
  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. tab={currentTab}
  322. />
  323. <Layout.Title>
  324. {project && (
  325. <IdBadge
  326. project={project}
  327. avatarSize={28}
  328. hideName
  329. avatarProps={{hasTooltip: true, tooltip: project.slug}}
  330. />
  331. )}
  332. <Tooltip showOnlyOnOverflow skipWrapper title={transactionName}>
  333. <TransactionName>{transactionName}</TransactionName>
  334. </Tooltip>
  335. </Layout.Title>
  336. </Layout.HeaderContent>
  337. <Layout.HeaderActions>
  338. <ButtonBar gap={1}>
  339. <Feature organization={organization} features="incidents">
  340. {({hasFeature}) =>
  341. hasFeature && !metricsCardinality?.isLoading ? (
  342. <CreateAlertFromViewButton
  343. size="sm"
  344. eventView={eventView}
  345. organization={organization}
  346. projects={projects}
  347. onClick={handleCreateAlertSuccess}
  348. referrer="performance"
  349. alertType="trans_duration"
  350. aria-label={t('Create Alert')}
  351. disableMetricDataset={
  352. metricsCardinality?.outcome?.forceTransactionsOnly
  353. }
  354. />
  355. ) : null
  356. }
  357. </Feature>
  358. <TeamKeyTransactionButton
  359. transactionName={transactionName}
  360. eventView={eventView}
  361. organization={organization}
  362. />
  363. <GuideAnchor target="project_transaction_threshold_override" position="bottom">
  364. <TransactionThresholdButton
  365. organization={organization}
  366. transactionName={transactionName}
  367. eventView={eventView}
  368. onChangeThreshold={onChangeThreshold}
  369. />
  370. </GuideAnchor>
  371. <FeedbackWidgetButton />
  372. </ButtonBar>
  373. </Layout.HeaderActions>
  374. <HasMeasurementsQuery
  375. location={location}
  376. orgSlug={organization.slug}
  377. eventView={eventView}
  378. transaction={transactionName}
  379. type="web"
  380. >
  381. {({hasMeasurements}) => {
  382. const renderWebVitals = getWebVitals(!!hasMeasurements);
  383. return (
  384. <TabList
  385. hideBorder
  386. outerWrapStyles={{
  387. gridColumn: '1 / -1',
  388. }}
  389. >
  390. <TabList.Item key={Tab.TRANSACTION_SUMMARY}>{t('Overview')}</TabList.Item>
  391. <TabList.Item key={Tab.EVENTS}>{t('Sampled Events')}</TabList.Item>
  392. <TabList.Item key={Tab.TAGS}>{t('Tags')}</TabList.Item>
  393. <TabList.Item key={Tab.SPANS}>{t('Spans')}</TabList.Item>
  394. <TabList.Item
  395. key={Tab.WEB_VITALS}
  396. textValue={t('Web Vitals')}
  397. hidden={!renderWebVitals}
  398. >
  399. {t('Web Vitals')}
  400. </TabList.Item>
  401. <TabList.Item
  402. key={Tab.REPLAYS}
  403. textValue={t('Replays')}
  404. hidden={!hasSessionReplay}
  405. >
  406. {t('Replays')}
  407. <ReplayCountBadge count={replaysCount} />
  408. </TabList.Item>
  409. <TabList.Item
  410. key={Tab.PROFILING}
  411. textValue={t('Profiling')}
  412. hidden={!hasProfiling}
  413. >
  414. {t('Profiles')}
  415. </TabList.Item>
  416. <TabList.Item
  417. key={Tab.AGGREGATE_WATERFALL}
  418. textValue={t('Aggregate Spans')}
  419. hidden={!hasAggregateWaterfall}
  420. >
  421. {t('Aggregate Spans')}
  422. </TabList.Item>
  423. </TabList>
  424. );
  425. }}
  426. </HasMeasurementsQuery>
  427. </Layout.Header>
  428. );
  429. }
  430. const TransactionName = styled('div')`
  431. ${p => p.theme.overflowEllipsis}
  432. `;
  433. export default TransactionHeader;