header.tsx 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. import * as React from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import Feature from 'sentry/components/acl/feature';
  6. import {GuideAnchor} from 'sentry/components/assistant/guideAnchor';
  7. import ButtonBar from 'sentry/components/buttonBar';
  8. import {CreateAlertFromViewButton} from 'sentry/components/createAlertButton';
  9. import FeatureBadge from 'sentry/components/featureBadge';
  10. import * as Layout from 'sentry/components/layouts/thirds';
  11. import ListLink from 'sentry/components/links/listLink';
  12. import NavTabs from 'sentry/components/navTabs';
  13. import {t} from 'sentry/locale';
  14. import {Organization, Project} from 'sentry/types';
  15. import {trackAnalyticsEvent} from 'sentry/utils/analytics';
  16. import EventView from 'sentry/utils/discover/eventView';
  17. import HasMeasurementsQuery from 'sentry/utils/performance/vitals/hasMeasurementsQuery';
  18. import {decodeScalar} from 'sentry/utils/queryString';
  19. import Breadcrumb from 'sentry/views/performance/breadcrumb';
  20. import {getCurrentLandingDisplay, LandingDisplayField} from '../landing/utils';
  21. import {MetricsSwitch} from '../metricsSwitch';
  22. import {eventsRouteWithQuery} from './transactionEvents/utils';
  23. import {spansRouteWithQuery} from './transactionSpans/utils';
  24. import {tagsRouteWithQuery} from './transactionTags/utils';
  25. import {vitalsRouteWithQuery} from './transactionVitals/utils';
  26. import Tab from './tabs';
  27. import TeamKeyTransactionButton from './teamKeyTransactionButton';
  28. import TransactionThresholdButton from './transactionThresholdButton';
  29. import {TransactionThresholdMetric} from './transactionThresholdModal';
  30. import {transactionSummaryRouteWithQuery} from './utils';
  31. type AnalyticInfo = {
  32. eventKey: string;
  33. eventName: string;
  34. };
  35. const TAB_ANALYTICS: Partial<Record<Tab, AnalyticInfo>> = {
  36. [Tab.WebVitals]: {
  37. eventKey: 'performance_views.vitals.vitals_tab_clicked',
  38. eventName: 'Performance Views: Vitals tab clicked',
  39. },
  40. [Tab.Tags]: {
  41. eventKey: 'performance_views.tags.tags_tab_clicked',
  42. eventName: 'Performance Views: Tags tab clicked',
  43. },
  44. [Tab.Events]: {
  45. eventKey: 'performance_views.events.events_tab_clicked',
  46. eventName: 'Performance Views: Events tab clicked',
  47. },
  48. [Tab.Spans]: {
  49. eventKey: 'performance_views.spans.spans_tab_clicked',
  50. eventName: 'Performance Views: Spans tab clicked',
  51. },
  52. };
  53. type Props = {
  54. eventView: EventView;
  55. location: Location;
  56. organization: Organization;
  57. projects: Project[];
  58. projectId: string;
  59. transactionName: string;
  60. currentTab: Tab;
  61. hasWebVitals: 'maybe' | 'yes' | 'no';
  62. onChangeThreshold?: (threshold: number, metric: TransactionThresholdMetric) => void;
  63. handleIncompatibleQuery: React.ComponentProps<
  64. typeof CreateAlertFromViewButton
  65. >['onIncompatibleQuery'];
  66. };
  67. class TransactionHeader extends React.Component<Props> {
  68. trackAlertClick(errors?: Record<string, boolean>) {
  69. const {organization} = this.props;
  70. trackAnalyticsEvent({
  71. eventKey: 'performance_views.summary.create_alert_clicked',
  72. eventName: 'Performance Views: Create alert clicked',
  73. organization_id: organization.id,
  74. status: errors ? 'error' : 'success',
  75. errors,
  76. url: window.location.href,
  77. });
  78. }
  79. trackTabClick = (tab: Tab) => () => {
  80. const analyticKeys = TAB_ANALYTICS[tab];
  81. if (!analyticKeys) {
  82. return;
  83. }
  84. trackAnalyticsEvent({
  85. ...analyticKeys,
  86. organization_id: this.props.organization.id,
  87. });
  88. };
  89. handleIncompatibleQuery: React.ComponentProps<
  90. typeof CreateAlertFromViewButton
  91. >['onIncompatibleQuery'] = (incompatibleAlertNoticeFn, errors) => {
  92. this.trackAlertClick(errors);
  93. this.props.handleIncompatibleQuery?.(incompatibleAlertNoticeFn, errors);
  94. };
  95. handleCreateAlertSuccess = () => {
  96. this.trackAlertClick();
  97. };
  98. renderCreateAlertButton() {
  99. const {eventView, organization, projects} = this.props;
  100. return (
  101. <CreateAlertFromViewButton
  102. eventView={eventView}
  103. organization={organization}
  104. projects={projects}
  105. onIncompatibleQuery={this.handleIncompatibleQuery}
  106. onSuccess={this.handleCreateAlertSuccess}
  107. referrer="performance"
  108. />
  109. );
  110. }
  111. renderKeyTransactionButton() {
  112. const {eventView, organization, transactionName} = this.props;
  113. return (
  114. <TeamKeyTransactionButton
  115. transactionName={transactionName}
  116. eventView={eventView}
  117. organization={organization}
  118. />
  119. );
  120. }
  121. renderSettingsButton() {
  122. const {organization, transactionName, eventView, onChangeThreshold} = this.props;
  123. return (
  124. <GuideAnchor target="project_transaction_threshold_override" position="bottom">
  125. <TransactionThresholdButton
  126. organization={organization}
  127. transactionName={transactionName}
  128. eventView={eventView}
  129. onChangeThreshold={onChangeThreshold}
  130. />
  131. </GuideAnchor>
  132. );
  133. }
  134. renderWebVitalsTab() {
  135. const {
  136. organization,
  137. eventView,
  138. location,
  139. projects,
  140. transactionName,
  141. currentTab,
  142. hasWebVitals,
  143. } = this.props;
  144. const vitalsTarget = vitalsRouteWithQuery({
  145. orgSlug: organization.slug,
  146. transaction: transactionName,
  147. projectID: decodeScalar(location.query.project),
  148. query: location.query,
  149. });
  150. const tab = (
  151. <ListLink
  152. data-test-id="web-vitals-tab"
  153. to={vitalsTarget}
  154. isActive={() => currentTab === Tab.WebVitals}
  155. onClick={this.trackTabClick(Tab.WebVitals)}
  156. >
  157. {t('Web Vitals')}
  158. </ListLink>
  159. );
  160. switch (hasWebVitals) {
  161. case 'maybe':
  162. // need to check if the web vitals tab should be shown
  163. // frontend projects should always show the web vitals tab
  164. if (
  165. getCurrentLandingDisplay(location, projects, eventView).field ===
  166. LandingDisplayField.FRONTEND_PAGELOAD
  167. ) {
  168. return tab;
  169. }
  170. // if it is not a frontend project, then we check to see if there
  171. // are any web vitals associated with the transaction recently
  172. return (
  173. <HasMeasurementsQuery
  174. location={location}
  175. orgSlug={organization.slug}
  176. eventView={eventView}
  177. transaction={transactionName}
  178. type="web"
  179. >
  180. {({hasMeasurements}) => (hasMeasurements ? tab : null)}
  181. </HasMeasurementsQuery>
  182. );
  183. case 'yes':
  184. // always show the web vitals tab
  185. return tab;
  186. case 'no':
  187. default:
  188. // never show the web vitals tab
  189. return null;
  190. }
  191. }
  192. handleSwitchMetrics = () => {
  193. const {location} = this.props;
  194. browserHistory.push({
  195. pathname: location.pathname,
  196. query: {
  197. ...location.query,
  198. query: undefined,
  199. },
  200. });
  201. };
  202. render() {
  203. const {organization, location, projectId, transactionName, currentTab} = this.props;
  204. const routeQuery = {
  205. orgSlug: organization.slug,
  206. transaction: transactionName,
  207. projectID: projectId,
  208. query: location.query,
  209. };
  210. const summaryTarget = transactionSummaryRouteWithQuery(routeQuery);
  211. const tagsTarget = tagsRouteWithQuery(routeQuery);
  212. const eventsTarget = eventsRouteWithQuery(routeQuery);
  213. const spansTarget = spansRouteWithQuery(routeQuery);
  214. return (
  215. <Layout.Header>
  216. <Layout.HeaderContent>
  217. <Breadcrumb
  218. organization={organization}
  219. location={location}
  220. transaction={{
  221. project: projectId,
  222. name: transactionName,
  223. }}
  224. tab={currentTab}
  225. />
  226. <Layout.Title>{transactionName}</Layout.Title>
  227. </Layout.HeaderContent>
  228. <Layout.HeaderActions>
  229. <ButtonBar gap={1}>
  230. <MetricsSwitch onSwitch={this.handleSwitchMetrics} />
  231. <Feature organization={organization} features={['incidents']}>
  232. {({hasFeature}) => hasFeature && this.renderCreateAlertButton()}
  233. </Feature>
  234. {this.renderKeyTransactionButton()}
  235. {this.renderSettingsButton()}
  236. </ButtonBar>
  237. </Layout.HeaderActions>
  238. <React.Fragment>
  239. <StyledNavTabs>
  240. <ListLink
  241. to={summaryTarget}
  242. isActive={() => currentTab === Tab.TransactionSummary}
  243. >
  244. {t('Overview')}
  245. </ListLink>
  246. {this.renderWebVitalsTab()}
  247. <ListLink
  248. to={tagsTarget}
  249. isActive={() => currentTab === Tab.Tags}
  250. onClick={this.trackTabClick(Tab.Tags)}
  251. >
  252. {t('Tags')}
  253. </ListLink>
  254. <Feature features={['organizations:performance-events-page']}>
  255. <ListLink
  256. to={eventsTarget}
  257. isActive={() => currentTab === Tab.Events}
  258. onClick={this.trackTabClick(Tab.Events)}
  259. >
  260. {t('All Events')}
  261. </ListLink>
  262. </Feature>
  263. <Feature
  264. organization={organization}
  265. features={['organizations:performance-suspect-spans-view']}
  266. >
  267. <ListLink
  268. data-test-id="spans-tab"
  269. to={spansTarget}
  270. isActive={() => currentTab === Tab.Spans}
  271. onClick={this.trackTabClick(Tab.Spans)}
  272. >
  273. {t('Spans')}
  274. <FeatureBadge type="alpha" noTooltip />
  275. </ListLink>
  276. </Feature>
  277. </StyledNavTabs>
  278. </React.Fragment>
  279. </Layout.Header>
  280. );
  281. }
  282. }
  283. const StyledNavTabs = styled(NavTabs)`
  284. margin-bottom: 0;
  285. /* Makes sure the tabs are pushed into another row */
  286. width: 100%;
  287. `;
  288. export default TransactionHeader;