Browse Source

feat(insights): keep transaction summary pages within the domain view (#79056)

If I navigate to `performance -> frontend` in the sidebar, then i click
a transaction, I end up in the transaction summary page.

This PR updates the header for that page, and ensure all of the
following tabs remain within the frontend domain view.
(i.e when clicking on these tabs, the resulting url will still be under
`/performance/frontend/summary`, currently it goes back to
`/performance/summary`)

<img width="732" alt="image"
src="https://github.com/user-attachments/assets/c911c6f2-2d30-46ac-89fb-2c4ff1af4cfa">

We have to also do the sub links as well in another PR, and update the
breadcrumbs, but doing this one step at a time.
Dominik Buszowiecki 4 months ago
parent
commit
bb6ad404c3

+ 90 - 69
static/app/routes.tsx

@@ -1610,6 +1610,75 @@ function buildRoutes() {
     </Route>
   );
 
+  const transactionSummaryRoutes = (
+    <Route path="summary/">
+      <IndexRoute
+        component={make(
+          () => import('sentry/views/performance/transactionSummary/transactionOverview')
+        )}
+      />
+      <Route
+        path="replays/"
+        component={make(
+          () => import('sentry/views/performance/transactionSummary/transactionReplays')
+        )}
+      />
+      <Route
+        path="vitals/"
+        component={make(
+          () => import('sentry/views/performance/transactionSummary/transactionVitals')
+        )}
+      />
+      <Route
+        path="tags/"
+        component={make(
+          () => import('sentry/views/performance/transactionSummary/transactionTags')
+        )}
+      />
+      <Route
+        path="events/"
+        component={make(
+          () => import('sentry/views/performance/transactionSummary/transactionEvents')
+        )}
+      />
+      <Route
+        path="anomalies/"
+        component={make(
+          () => import('sentry/views/performance/transactionSummary/transactionAnomalies')
+        )}
+      />
+      <Route
+        path="profiles/"
+        component={make(
+          () => import('sentry/views/performance/transactionSummary/transactionProfiles')
+        )}
+      />
+      <Route
+        path="aggregateWaterfall/"
+        component={make(
+          () =>
+            import('sentry/views/performance/transactionSummary/aggregateSpanWaterfall')
+        )}
+      />
+      <Route path="spans/">
+        <IndexRoute
+          component={make(
+            () => import('sentry/views/performance/transactionSummary/transactionSpans')
+          )}
+        />
+        <Route
+          path=":spanSlug/"
+          component={make(
+            () =>
+              import(
+                'sentry/views/performance/transactionSummary/transactionSpans/spanDetails'
+              )
+          )}
+        />
+      </Route>
+    </Route>
+  );
+
   const performanceRoutes = (
     <Route
       path="/performance/"
@@ -1627,6 +1696,11 @@ function buildRoutes() {
             () => import('sentry/views/insights/pages/frontend/frontendOverviewPage')
           )}
         />
+        {transactionSummaryRoutes}
+        <Route
+          path="trace/:traceSlug/"
+          component={make(() => import('sentry/views/performance/traceDetails'))}
+        />
         <Route path={`${MODULE_BASE_URLS[ModuleName.HTTP]}/`}>
           <IndexRoute
             component={make(
@@ -1682,6 +1756,11 @@ function buildRoutes() {
             () => import('sentry/views/insights/pages/backend/backendOverviewPage')
           )}
         />
+        {transactionSummaryRoutes}
+        <Route
+          path="trace/:traceSlug/"
+          component={make(() => import('sentry/views/performance/traceDetails'))}
+        />
         <Route path={`${MODULE_BASE_URLS[ModuleName.DB]}/`}>
           <IndexRoute
             component={make(
@@ -1735,6 +1814,11 @@ function buildRoutes() {
             () => import('sentry/views/insights/pages/mobile/mobileOverviewPage')
           )}
         />
+        {transactionSummaryRoutes}
+        <Route
+          path="trace/:traceSlug/"
+          component={make(() => import('sentry/views/performance/traceDetails'))}
+        />
         <Route path={`${MODULE_BASE_URLS[ModuleName.MOBILE_SCREENS]}/`}>
           <IndexRoute
             component={make(
@@ -1822,6 +1906,11 @@ function buildRoutes() {
         <IndexRoute
           component={make(() => import('sentry/views/insights/pages/ai/aiOverviewPage'))}
         />
+        {transactionSummaryRoutes}
+        <Route
+          path="trace/:traceSlug/"
+          component={make(() => import('sentry/views/performance/traceDetails'))}
+        />
         <Route path={`${MODULE_BASE_URLS[ModuleName.AI]}/`}>
           <IndexRoute
             component={make(
@@ -1842,75 +1931,7 @@ function buildRoutes() {
           />
         </Route>
       </Route>
-      <Route path="summary/">
-        <IndexRoute
-          component={make(
-            () =>
-              import('sentry/views/performance/transactionSummary/transactionOverview')
-          )}
-        />
-        <Route
-          path="replays/"
-          component={make(
-            () => import('sentry/views/performance/transactionSummary/transactionReplays')
-          )}
-        />
-        <Route
-          path="vitals/"
-          component={make(
-            () => import('sentry/views/performance/transactionSummary/transactionVitals')
-          )}
-        />
-        <Route
-          path="tags/"
-          component={make(
-            () => import('sentry/views/performance/transactionSummary/transactionTags')
-          )}
-        />
-        <Route
-          path="events/"
-          component={make(
-            () => import('sentry/views/performance/transactionSummary/transactionEvents')
-          )}
-        />
-        <Route
-          path="anomalies/"
-          component={make(
-            () =>
-              import('sentry/views/performance/transactionSummary/transactionAnomalies')
-          )}
-        />
-        <Route
-          path="profiles/"
-          component={make(
-            () =>
-              import('sentry/views/performance/transactionSummary/transactionProfiles')
-          )}
-        />
-        <Route
-          path="aggregateWaterfall/"
-          component={make(
-            () =>
-              import('sentry/views/performance/transactionSummary/aggregateSpanWaterfall')
-          )}
-        />
-        <Route path="spans/">
-          <IndexRoute
-            component={make(
-              () => import('sentry/views/performance/transactionSummary/transactionSpans')
-            )}
-          />
-          <Route
-            path=":spanSlug/"
-            component={make(
-              () =>
-                import(
-                  'sentry/views/performance/transactionSummary/transactionSpans/spanDetails'
-                )
-            )}
-          />
-        </Route>
-      </Route>
+      {transactionSummaryRoutes}
       <Route
         path="vitaldetail/"
         component={make(() => import('sentry/views/performance/vitalDetail'))}

+ 6 - 1
static/app/utils/discover/urls.tsx

@@ -3,6 +3,7 @@ import type {Location, LocationDescriptorObject} from 'history';
 
 import type {Organization, OrganizationSummary} from 'sentry/types/organization';
 import normalizeUrl from 'sentry/utils/url/normalizeUrl';
+import type {DomainView} from 'sentry/views/insights/pages/useFilters';
 import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils';
 
 import {getTimeStampFromTableDateField} from '../dates';
@@ -56,6 +57,7 @@ export function generateLinkToEventInTraceView({
   demo,
   source,
   type = 'performance',
+  view,
 }: {
   eventId: string | undefined;
   location: Location;
@@ -73,6 +75,7 @@ export function generateLinkToEventInTraceView({
   targetId?: string;
   transactionName?: string;
   type?: 'performance' | 'discover';
+  view?: DomainView;
 }) {
   const _eventView = eventView ?? EventView.fromLocation(location);
   const dateSelection = _eventView.normalizeDateSelection(location);
@@ -99,6 +102,7 @@ export function generateLinkToEventInTraceView({
       demo,
       location,
       source,
+      view,
     });
   }
 
@@ -108,7 +112,8 @@ export function generateLinkToEventInTraceView({
       eventSlug,
       transactionName,
       location.query,
-      spanId
+      spanId,
+      view
     );
   }
 

+ 5 - 2
static/app/utils/performance/urls.ts

@@ -4,6 +4,8 @@ import {spanTargetHash} from 'sentry/components/events/interfaces/spans/utils';
 import type {Organization} from 'sentry/types/organization';
 import {defined} from 'sentry/utils';
 import normalizeUrl from 'sentry/utils/url/normalizeUrl';
+import type {DomainView} from 'sentry/views/insights/pages/useFilters';
+import {getPerformanceBaseUrl} from 'sentry/views/performance/utils';
 
 /**
  * Routes to the transaction event details view.
@@ -17,7 +19,8 @@ export function getTransactionDetailsUrl(
   eventSlug: string,
   transaction?: string,
   query?: Query,
-  spanId?: string
+  spanId?: string,
+  view?: DomainView
 ): LocationDescriptor {
   const locationQuery = {
     ...(query || {}),
@@ -28,7 +31,7 @@ export function getTransactionDetailsUrl(
   }
 
   const target: LocationDescriptor = {
-    pathname: normalizeUrl(`/organizations/${orgSlug}/performance/${eventSlug}/`),
+    pathname: normalizeUrl(`${getPerformanceBaseUrl(orgSlug, view)}/${eventSlug}/`),
     query: locationQuery,
     hash: defined(spanId) ? spanTargetHash(spanId) : undefined,
   };

+ 9 - 0
static/app/views/performance/table.spec.tsx

@@ -1,3 +1,4 @@
+import {LocationFixture} from 'sentry-fixture/locationFixture';
 import {ProjectFixture} from 'sentry-fixture/project';
 
 import {initializeData as _initializeData} from 'sentry-test/performance/initializePerformanceData';
@@ -8,11 +9,16 @@ import {browserHistory} from 'sentry/utils/browserHistory';
 import EventView from 'sentry/utils/discover/eventView';
 import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
+import {useLocation} from 'sentry/utils/useLocation';
 import {OrganizationContext} from 'sentry/views/organizationContext';
 import Table from 'sentry/views/performance/table';
 
 const FEATURES = ['performance-view'];
 
+jest.mock('sentry/utils/useLocation');
+
+const mockUseLocation = jest.mocked(useLocation);
+
 const initializeData = (settings = {}, features: string[] = []) => {
   const projects = [
     ProjectFixture({id: '1', slug: '1'}),
@@ -104,6 +110,9 @@ describe('Performance > Table', function () {
   let eventsMock;
   beforeEach(function () {
     browserHistory.push = jest.fn();
+    mockUseLocation.mockReturnValue(
+      LocationFixture({pathname: '/organizations/org-slug/performance/summary'})
+    );
     MockApiClient.addMockResponse({
       url: '/organizations/org-slug/projects/',
       body: [],

+ 17 - 1
static/app/views/performance/table.tsx

@@ -31,6 +31,10 @@ import {useLocation} from 'sentry/utils/useLocation';
 import useOrganization from 'sentry/utils/useOrganization';
 import CellAction, {Actions, updateQuery} from 'sentry/views/discover/table/cellAction';
 import type {TableColumn} from 'sentry/views/discover/table/types';
+import {
+  type DomainViewFilters,
+  useDomainViewFilters,
+} from 'sentry/views/insights/pages/useFilters';
 import {getLandingDisplayFromParam} from 'sentry/views/performance/landing/utils';
 
 import {getMEPQueryParams} from './landing/widgets/utils';
@@ -59,6 +63,7 @@ type Props = {
   setError: (msg: string | undefined) => void;
   withStaticFilters: boolean;
   columnTitles?: string[];
+  domainViewFilters?: DomainViewFilters;
   summaryConditions?: string;
 };
 
@@ -276,6 +281,8 @@ class _Table extends Component<Props, State> {
         this.sendUnparameterizedAnalytic(project);
         this.unparameterizedMetricSet = true;
       }
+      const {isInDomainView, view} = this.props.domainViewFilters ?? {};
+
       const target = isUnparameterizedRow
         ? createUnnamedTransactionsDiscoverTarget({
             organization,
@@ -286,6 +293,7 @@ class _Table extends Component<Props, State> {
             transaction: String(dataRow.transaction) || '',
             query: summaryView.generateQueryStringObject(),
             projectID,
+            view: (isInDomainView && view) || undefined,
           });
 
       return (
@@ -595,7 +603,15 @@ function Table(props: Omit<Props, 'summaryConditions'> & {summaryConditions?: st
   const summaryConditions =
     props.summaryConditions ?? props.eventView.getQueryWithAdditionalConditions();
 
-  return <_Table {...props} summaryConditions={summaryConditions} />;
+  const domainViewFilters = useDomainViewFilters();
+
+  return (
+    <_Table
+      {...props}
+      summaryConditions={summaryConditions}
+      domainViewFilters={domainViewFilters}
+    />
+  );
 }
 
 // Align the contained IconStar with the IconStar buttons in individual table

+ 8 - 6
static/app/views/performance/traceDetails/utils.tsx

@@ -12,6 +12,8 @@ import type {
 } from 'sentry/utils/performance/quickTrace/types';
 import {isTraceSplitResult, reduceTrace} from 'sentry/utils/performance/quickTrace/utils';
 import normalizeUrl from 'sentry/utils/url/normalizeUrl';
+import type {DomainView} from 'sentry/views/insights/pages/useFilters';
+import {getPerformanceBaseUrl} from 'sentry/views/performance/utils';
 
 import {DEFAULT_TRACE_ROWS_LIMIT} from './limitExceededMessage';
 import type {TraceInfo} from './types';
@@ -27,6 +29,7 @@ export function getTraceDetailsUrl({
   demo,
   location,
   source,
+  view,
 }: {
   dateSelection;
   location: Location;
@@ -40,6 +43,7 @@ export function getTraceDetailsUrl({
   // to trace view are updated to use spand ids of transactions instead of event ids.
   targetId?: string;
   timestamp?: string | number;
+  view?: DomainView;
 }): LocationDescriptorObject {
   const {start, end, statsPeriod} = dateSelection;
 
@@ -50,10 +54,10 @@ export function getTraceDetailsUrl({
     [PAGE_URL_PARAM.PAGE_END]: end,
   };
 
+  const performanceBaseUrl = getPerformanceBaseUrl(organization.slug, view);
+
   const oldTraceUrl = {
-    pathname: normalizeUrl(
-      `/organizations/${organization.slug}/performance/trace/${traceSlug}/`
-    ),
+    pathname: normalizeUrl(`${performanceBaseUrl}/trace/${traceSlug}/`),
     query: queryParams,
   };
 
@@ -66,9 +70,7 @@ export function getTraceDetailsUrl({
       queryParams.node = [`span-${spanId}`, `txn-${targetId ?? eventId}`];
     }
     return {
-      pathname: normalizeUrl(
-        `/organizations/${organization.slug}/performance/trace/${traceSlug}/`
-      ),
+      pathname: normalizeUrl(`${performanceBaseUrl}/trace/${traceSlug}/`),
       query: {
         ...queryParams,
         timestamp: getTimeStampFromTableDateField(timestamp),

+ 10 - 0
static/app/views/performance/transactionEvents.spec.tsx

@@ -1,3 +1,4 @@
+import {LocationFixture} from 'sentry-fixture/locationFixture';
 import {OrganizationFixture} from 'sentry-fixture/organization';
 
 import {initializeOrg} from 'sentry-test/initializeOrg';
@@ -5,10 +6,15 @@ import {render, screen} from 'sentry-test/reactTestingLibrary';
 
 import ProjectsStore from 'sentry/stores/projectsStore';
 import {WebVital} from 'sentry/utils/fields';
+import {useLocation} from 'sentry/utils/useLocation';
 import TransactionEvents from 'sentry/views/performance/transactionSummary/transactionEvents';
 
 // XXX(epurkhiser): This appears to also be tested by ./transactionSummary/transactionEvents/index.spec.tsx
 
+jest.mock('sentry/utils/useLocation');
+
+const mockUseLocation = jest.mocked(useLocation);
+
 type Data = {
   features?: string[];
   query?: {
@@ -39,6 +45,10 @@ function initializeData({features: additionalFeatures = [], query = {}}: Data =
 
 describe('Performance > TransactionSummary', function () {
   beforeEach(function () {
+    mockUseLocation.mockReturnValue(
+      LocationFixture({pathname: '/organizations/org-slug/performance/summary'})
+    );
+
     MockApiClient.addMockResponse({
       url: '/organizations/org-slug/projects/',
       body: [],

+ 5 - 1
static/app/views/performance/transactionSummary/aggregateSpanWaterfall/utils.tsx

@@ -2,19 +2,23 @@ import type {Query} from 'history';
 
 import {decodeScalar} from 'sentry/utils/queryString';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
+import type {DomainView} from 'sentry/views/insights/pages/useFilters';
+import {getTransactionSummaryBaseUrl} from 'sentry/views/performance/transactionSummary/utils';
 
 export function aggregateWaterfallRouteWithQuery({
   orgSlug,
   transaction,
   projectID,
   query,
+  view,
 }: {
   orgSlug: string;
   query: Query;
   transaction: string;
   projectID?: string | string[];
+  view?: DomainView;
 }) {
-  const pathname = `/organizations/${orgSlug}/performance/summary/aggregateWaterfall/`;
+  const pathname = `${getTransactionSummaryBaseUrl(orgSlug, view)}/aggregateWaterfall/`;
 
   const filter = decodeScalar(query.query);
   let httpMethod: string | undefined = undefined;

+ 186 - 1
static/app/views/performance/transactionSummary/header.tsx

@@ -1,4 +1,4 @@
-import {useCallback} from 'react';
+import {Fragment, useCallback} from 'react';
 import styled from '@emotion/styled';
 import type {Location} from 'history';
 
@@ -23,7 +23,28 @@ import HasMeasurementsQuery from 'sentry/utils/performance/vitals/hasMeasurement
 import {isProfilingSupportedOrProjectHasProfiles} from 'sentry/utils/profiling/platforms';
 import useReplayCountForTransactions from 'sentry/utils/replayCount/useReplayCountForTransactions';
 import projectSupportsReplay from 'sentry/utils/replays/projectSupportsReplay';
+import normalizeUrl from 'sentry/utils/url/normalizeUrl';
+import {useNavigate} from 'sentry/utils/useNavigate';
+import {AiHeader} from 'sentry/views/insights/pages/ai/aiPageHeader';
+import {AI_LANDING_SUB_PATH} from 'sentry/views/insights/pages/ai/settings';
+import {BackendHeader} from 'sentry/views/insights/pages/backend/backendPageHeader';
+import {BACKEND_LANDING_SUB_PATH} from 'sentry/views/insights/pages/backend/settings';
+import {FrontendHeader} from 'sentry/views/insights/pages/frontend/frontendPageHeader';
+import {FRONTEND_LANDING_SUB_PATH} from 'sentry/views/insights/pages/frontend/settings';
+import {MobileHeader} from 'sentry/views/insights/pages/mobile/mobilePageHeader';
+import {MOBILE_LANDING_SUB_PATH} from 'sentry/views/insights/pages/mobile/settings';
+import {useDomainViewFilters} from 'sentry/views/insights/pages/useFilters';
 import Breadcrumb from 'sentry/views/performance/breadcrumb';
+import {aggregateWaterfallRouteWithQuery} from 'sentry/views/performance/transactionSummary/aggregateSpanWaterfall/utils';
+import {TAB_ANALYTICS} from 'sentry/views/performance/transactionSummary/pageLayout';
+import {anomaliesRouteWithQuery} from 'sentry/views/performance/transactionSummary/transactionAnomalies/utils';
+import {eventsRouteWithQuery} from 'sentry/views/performance/transactionSummary/transactionEvents/utils';
+import {profilesRouteWithQuery} from 'sentry/views/performance/transactionSummary/transactionProfiles/utils';
+import {replaysRouteWithQuery} from 'sentry/views/performance/transactionSummary/transactionReplays/utils';
+import {spansRouteWithQuery} from 'sentry/views/performance/transactionSummary/transactionSpans/utils';
+import {tagsRouteWithQuery} from 'sentry/views/performance/transactionSummary/transactionTags/utils';
+import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
+import {getSelectedProjectPlatforms} from 'sentry/views/performance/utils';
 
 import {getCurrentLandingDisplay, LandingDisplayField} from '../landing/utils';
 
@@ -57,6 +78,67 @@ function TransactionHeader({
   currentTab,
   hasWebVitals,
 }: Props) {
+  const {isInDomainView, view} = useDomainViewFilters();
+  const navigate = useNavigate();
+
+  const getNewRoute = useCallback(
+    (newTab: Tab) => {
+      if (!transactionName) {
+        return {};
+      }
+
+      const routeQuery = {
+        orgSlug: organization.slug,
+        transaction: transactionName,
+        projectID: projectId,
+        query: location.query,
+        view,
+      };
+
+      switch (newTab) {
+        case Tab.TAGS:
+          return tagsRouteWithQuery(routeQuery);
+        case Tab.EVENTS:
+          return eventsRouteWithQuery(routeQuery);
+        case Tab.SPANS:
+          return spansRouteWithQuery(routeQuery);
+        case Tab.ANOMALIES:
+          return anomaliesRouteWithQuery(routeQuery);
+        case Tab.REPLAYS:
+          return replaysRouteWithQuery(routeQuery);
+        case Tab.PROFILING: {
+          return profilesRouteWithQuery(routeQuery);
+        }
+        case Tab.AGGREGATE_WATERFALL:
+          return aggregateWaterfallRouteWithQuery(routeQuery);
+        case Tab.TRANSACTION_SUMMARY:
+        default:
+          return transactionSummaryRouteWithQuery(routeQuery);
+      }
+    },
+    [location.query, organization.slug, projectId, transactionName, view]
+  );
+
+  const onTabChange = useCallback(
+    (newTab: string) => {
+      // Prevent infinite rerenders
+      if (newTab === currentTab) {
+        return;
+      }
+
+      const analyticsKey = TAB_ANALYTICS[newTab];
+      if (analyticsKey) {
+        trackAnalytics(analyticsKey, {
+          organization,
+          project_platforms: getSelectedProjectPlatforms(location, projects),
+        });
+      }
+
+      navigate(normalizeUrl(getNewRoute(newTab as Tab)));
+    },
+    [getNewRoute, organization, location, projects, currentTab, navigate]
+  );
+
   function handleCreateAlertSuccess() {
     trackAnalytics('performance_views.summary.create_alert_clicked', {
       organization,
@@ -117,6 +199,109 @@ function TransactionHeader({
   });
   const replaysCount = getReplayCountForTransaction(transactionName);
 
+  const tabList = (
+    <HasMeasurementsQuery
+      location={location}
+      orgSlug={organization.slug}
+      eventView={eventView}
+      transaction={transactionName}
+      type="web"
+    >
+      {({hasMeasurements}) => {
+        const renderWebVitals = getWebVitals(!!hasMeasurements);
+
+        return (
+          <TabList
+            hideBorder
+            outerWrapStyles={{
+              gridColumn: '1 / -1',
+            }}
+          >
+            <TabList.Item key={Tab.TRANSACTION_SUMMARY}>{t('Overview')}</TabList.Item>
+            <TabList.Item key={Tab.EVENTS}>{t('Sampled Events')}</TabList.Item>
+            <TabList.Item key={Tab.TAGS}>{t('Tags')}</TabList.Item>
+            <TabList.Item key={Tab.SPANS}>{t('Spans')}</TabList.Item>
+            <TabList.Item
+              key={Tab.ANOMALIES}
+              textValue={t('Anomalies')}
+              hidden={!hasAnomalyDetection}
+            >
+              {t('Anomalies')}
+              <FeatureBadge type="alpha" tooltipProps={{disabled: true}} />
+            </TabList.Item>
+            <TabList.Item
+              key={Tab.WEB_VITALS}
+              textValue={t('Web Vitals')}
+              hidden={!renderWebVitals}
+            >
+              {t('Web Vitals')}
+            </TabList.Item>
+            <TabList.Item
+              key={Tab.REPLAYS}
+              textValue={t('Replays')}
+              hidden={!hasSessionReplay}
+            >
+              {t('Replays')}
+              <ReplayCountBadge count={replaysCount} />
+            </TabList.Item>
+            <TabList.Item
+              key={Tab.PROFILING}
+              textValue={t('Profiling')}
+              hidden={!hasProfiling}
+            >
+              {t('Profiles')}
+            </TabList.Item>
+            <TabList.Item
+              key={Tab.AGGREGATE_WATERFALL}
+              textValue={t('Aggregate Spans')}
+              hidden={!hasAggregateWaterfall}
+            >
+              {t('Aggregate Spans')}
+            </TabList.Item>
+          </TabList>
+        );
+      }}
+    </HasMeasurementsQuery>
+  );
+
+  if (isInDomainView) {
+    const headerProps = {
+      headerTitle: (
+        <Fragment>
+          {project && (
+            <IdBadge
+              project={project}
+              avatarSize={28}
+              hideName
+              avatarProps={{hasTooltip: true, tooltip: project.slug}}
+            />
+          )}
+          <Tooltip showOnlyOnOverflow skipWrapper title={transactionName}>
+            <TransactionName>{transactionName}</TransactionName>
+          </Tooltip>
+        </Fragment>
+      ),
+      hideDefaultTabs: true,
+      tabs: {
+        onTabChange,
+        tabList,
+        value: currentTab,
+      },
+    };
+    if (view === FRONTEND_LANDING_SUB_PATH) {
+      return <FrontendHeader {...headerProps} />;
+    }
+    if (view === BACKEND_LANDING_SUB_PATH) {
+      return <BackendHeader {...headerProps} />;
+    }
+    if (view === AI_LANDING_SUB_PATH) {
+      return <AiHeader {...headerProps} />;
+    }
+    if (view === MOBILE_LANDING_SUB_PATH) {
+      return <MobileHeader {...headerProps} />;
+    }
+  }
+
   return (
     <Layout.Header>
       <Layout.HeaderContent>

+ 1 - 1
static/app/views/performance/transactionSummary/pageLayout.tsx

@@ -49,7 +49,7 @@ type TabEvents =
   | 'performance_views.events.events_tab_clicked'
   | 'performance_views.spans.spans_tab_clicked';
 
-const TAB_ANALYTICS: Partial<Record<Tab, TabEvents>> = {
+export const TAB_ANALYTICS: Partial<Record<Tab, TabEvents>> = {
   [Tab.WEB_VITALS]: 'performance_views.vitals.vitals_tab_clicked',
   [Tab.TAGS]: 'performance_views.tags.tags_tab_clicked',
   [Tab.EVENTS]: 'performance_views.events.events_tab_clicked',

Some files were not shown because too many files changed in this diff