Browse Source

feat(mep): Add UI to handle metrics rollout (#37341)

* feat(mep): Add UI to handle metrics rollout

This makes the UI able to handle the high-cardinality transaction names, along with checking for dynamic sampling being in effect before switching the user over to a metrics view.

A few notes:
- Added a bunch of discover queries for now until we make an endpoint for this, they should only fire behind the feature flag (for us testing it out)
- Added a very basic acceptance but a lot of indirect tests for the switching and alert logic.
- Had to make a bunch of small changes (like allowing a string to circumvent tokenization) to handle rollout for now.

* Remove acceptance test since it is likely triggering the backend flag on the PR
Kev 2 years ago
parent
commit
563ba6761f

+ 1 - 0
static/app/components/smartSearchBar/index.tsx

@@ -1598,6 +1598,7 @@ class SmartSearchBar extends Component<Props, State> {
         ref={this.containerRef}
         className={className}
         inputHasFocus={inputHasFocus}
+        data-test-id="smart-search-bar"
       >
         <SearchHotkeysListener
           visibleShortcuts={visibleShortcuts}

+ 10 - 2
static/app/utils/discover/eventView.tsx

@@ -1110,7 +1110,10 @@ class EventView {
   }
 
   // Takes an EventView instance and converts it into the format required for the events API
-  getEventsAPIPayload(location: Location): EventQuery & LocationQuery {
+  getEventsAPIPayload(
+    location: Location,
+    forceAppendRawQueryString?: string
+  ): EventQuery & LocationQuery {
     // pick only the query strings that we care about
     const picked = pickRelevantLocationQueryStrings(location);
 
@@ -1128,6 +1131,11 @@ class EventView {
     const project = this.project.map(proj => String(proj));
     const environment = this.environment as string[];
 
+    let queryString = this.getQueryWithAdditionalConditions();
+    if (forceAppendRawQueryString) {
+      queryString += ' ' + forceAppendRawQueryString;
+    }
+
     // generate event query
     const eventQuery = Object.assign(
       omit(picked, DATETIME_QUERY_STRING_KEYS),
@@ -1139,7 +1147,7 @@ class EventView {
         field: [...new Set(fields)],
         sort,
         per_page: DEFAULT_PER_PAGE,
-        query: this.getQueryWithAdditionalConditions(),
+        query: queryString,
       }
     ) as EventQuery & LocationQuery;
 

+ 9 - 1
static/app/utils/discover/genericDiscoverQuery.tsx

@@ -60,6 +60,11 @@ type BaseDiscoverQueryProps = {
    * multiple paginated results on the page.
    */
   cursor?: string;
+  /**
+   * Appends a raw string to query to be able to sidestep the tokenizer.
+   * @deprecated
+   */
+  forceAppendRawQueryString?: string;
   /**
    * Record limit to get.
    */
@@ -173,7 +178,10 @@ class _GenericDiscoverQuery<T, P> extends Component<Props<T, P>, State<T>> {
     const {cursor, limit, noPagination, referrer} = props;
     const payload = this.props.getRequestPayload
       ? this.props.getRequestPayload(props)
-      : props.eventView.getEventsAPIPayload(props.location);
+      : props.eventView.getEventsAPIPayload(
+          props.location,
+          props.forceAppendRawQueryString
+        );
 
     if (cursor) {
       payload.cursor = cursor;

+ 7 - 6
static/app/utils/performance/contexts/metricsEnhancedSetting.tsx

@@ -78,25 +78,26 @@ export const MEPSettingProvider = ({
   children,
   location,
   _hasMEPState,
+  forceTransactions,
 }: {
   children: ReactNode;
   _hasMEPState?: MEPState;
+  forceTransactions?: boolean;
   location?: Location;
 }) => {
   const organization = useOrganization();
 
   const canUseMEP = canUseMetricsData(organization);
-  const shouldDefaultToMetrics = organization.features.includes(
-    'performance-transaction-name-only-search'
-  );
 
   const allowedStates = [MEPState.auto, MEPState.metricsOnly, MEPState.transactionsOnly];
   const _metricSettingFromParam = location
     ? decodeScalar(location.query[METRIC_SETTING_PARAM])
     : MEPState.auto;
-  const defaultMetricsState = shouldDefaultToMetrics
-    ? MEPState.metricsOnly
-    : MEPState.auto;
+  let defaultMetricsState = MEPState.metricsOnly;
+
+  if (forceTransactions) {
+    defaultMetricsState = MEPState.transactionsOnly;
+  }
 
   const metricSettingFromParam =
     allowedStates.find(s => s === _metricSettingFromParam) ?? defaultMetricsState;

+ 31 - 33
static/app/views/performance/content.tsx

@@ -11,7 +11,6 @@ import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
 import {t} from 'sentry/locale';
 import {PageFilters, Project} from 'sentry/types';
 import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
-import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
 import {PerformanceEventViewProvider} from 'sentry/utils/performance/contexts/performanceEventViewContext';
 import useApi from 'sentry/utils/useApi';
 import useOrganization from 'sentry/utils/useOrganization';
@@ -38,7 +37,7 @@ type State = {
   error?: string;
 };
 
-function PerformanceContent({selection, location, demoMode}: Props) {
+function PerformanceContent({selection, location, demoMode, router}: Props) {
   const api = useApi();
   const organization = useOrganization();
   const {projects} = useProjects();
@@ -148,37 +147,36 @@ function PerformanceContent({selection, location, demoMode}: Props) {
   return (
     <SentryDocumentTitle title={t('Performance')} orgSlug={organization.slug}>
       <PerformanceEventViewProvider value={{eventView}}>
-        <MEPSettingProvider location={location}>
-          <PageFiltersContainer
-            defaultSelection={{
-              datetime: {
-                start: null,
-                end: null,
-                utc: false,
-                period: DEFAULT_STATS_PERIOD,
-              },
-            }}
-          >
-            <PerformanceLanding
-              eventView={eventView}
-              setError={setError}
-              handleSearch={handleSearch}
-              handleTrendsClick={() =>
-                handleTrendsClick({
-                  location,
-                  organization,
-                  projectPlatforms: getSelectedProjectPlatforms(location, projects),
-                })
-              }
-              onboardingProject={onboardingProject}
-              organization={organization}
-              location={location}
-              projects={projects}
-              selection={selection}
-              withStaticFilters={withStaticFilters}
-            />
-          </PageFiltersContainer>
-        </MEPSettingProvider>
+        <PageFiltersContainer
+          defaultSelection={{
+            datetime: {
+              start: null,
+              end: null,
+              utc: false,
+              period: DEFAULT_STATS_PERIOD,
+            },
+          }}
+        >
+          <PerformanceLanding
+            router={router}
+            eventView={eventView}
+            setError={setError}
+            handleSearch={handleSearch}
+            handleTrendsClick={() =>
+              handleTrendsClick({
+                location,
+                organization,
+                projectPlatforms: getSelectedProjectPlatforms(location, projects),
+              })
+            }
+            onboardingProject={onboardingProject}
+            organization={organization}
+            location={location}
+            projects={projects}
+            selection={selection}
+            withStaticFilters={withStaticFilters}
+          />
+        </PageFiltersContainer>
       </PerformanceEventViewProvider>
     </SentryDocumentTitle>
   );

+ 95 - 61
static/app/views/performance/landing/index.tsx

@@ -1,5 +1,5 @@
 import {FC, Fragment, useEffect, useRef} from 'react';
-import {browserHistory} from 'react-router';
+import {browserHistory, InjectedRouter} from 'react-router';
 import styled from '@emotion/styled';
 import {Location} from 'history';
 
@@ -10,7 +10,6 @@ import ButtonBar from 'sentry/components/buttonBar';
 import DatePageFilter from 'sentry/components/datePageFilter';
 import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
 import SearchBar from 'sentry/components/events/searchBar';
-import {GlobalSdkUpdateAlert} from 'sentry/components/globalSdkUpdateAlert';
 import * as Layout from 'sentry/components/layouts/thirds';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
@@ -27,6 +26,11 @@ import {Organization, PageFilters, Project} from 'sentry/types';
 import EventView from 'sentry/utils/discover/eventView';
 import {generateAggregateFields} from 'sentry/utils/discover/fields';
 import {GenericQueryBatcher} from 'sentry/utils/performance/contexts/genericQueryBatcher';
+import {
+  MEPConsumer,
+  MEPSettingProvider,
+  MEPState,
+} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
 import {
   PageErrorAlert,
   PageErrorProvider,
@@ -43,6 +47,8 @@ import {BackendView} from './views/backendView';
 import {FrontendOtherView} from './views/frontendOtherView';
 import {FrontendPageloadView} from './views/frontendPageloadView';
 import {MobileView} from './views/mobileView';
+import {MetricsDataSwitcher} from './metricsDataSwitcher';
+import {MetricsDataSwitcherAlert} from './metricsDataSwitcherAlert';
 import SamplingModal, {modalCss} from './samplingModal';
 import {
   getDefaultDisplayForPlatform,
@@ -60,6 +66,7 @@ type Props = {
   onboardingProject: Project | undefined;
   organization: Organization;
   projects: Project[];
+  router: InjectedRouter;
   selection: PageFilters;
   setError: (msg: string | undefined) => void;
   withStaticFilters: boolean;
@@ -106,6 +113,7 @@ export function PerformanceLanding(props: Props) {
         },
       });
     }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [eventView.project.join('.')]);
 
   useEffect(() => {
@@ -160,6 +168,10 @@ export function PerformanceLanding(props: Props) {
     ? SearchContainerWithFilterAndMetrics
     : SearchContainerWithFilter;
 
+  const shouldShowTransactionNameOnlySearch = organization.features.includes(
+    'performance-transaction-name-only-search'
+  );
+
   return (
     <StyledPageContent data-test-id="performance-landing-v3">
       <PageErrorProvider>
@@ -211,68 +223,90 @@ export function PerformanceLanding(props: Props) {
             ))}
           </Layout.HeaderNavTabs>
         </Layout.Header>
-        <Layout.Body>
+        <Layout.Body data-test-id="performance-landing-body">
           <Layout.Main fullWidth>
-            <GlobalSdkUpdateAlert />
-            <PageErrorAlert />
-            {showOnboarding ? (
-              <Fragment>
-                {pageFilters}
-                <Onboarding organization={organization} project={onboardingProject} />
-              </Fragment>
-            ) : (
-              <Fragment>
-                <SearchFilterContainer>
-                  {pageFilters}
-                  <Feature
-                    features={['organizations:performance-transaction-name-only-search']}
-                  >
-                    {({hasFeature}) =>
-                      hasFeature ? (
-                        // TODO replace `handleSearch prop` with transaction name search once
-                        // transaction name search becomes the default search bar
-                        <TransactionNameSearchBar
+            <MetricsDataSwitcher
+              organization={organization}
+              eventView={eventView}
+              location={location}
+            >
+              {metricsDataSide => (
+                <MEPSettingProvider
+                  location={location}
+                  forceTransactions={metricsDataSide.forceTransactionsOnly}
+                >
+                  <MetricsDataSwitcherAlert
+                    organization={organization}
+                    eventView={eventView}
+                    projects={projects}
+                    location={location}
+                    router={props.router}
+                    {...metricsDataSide}
+                  />
+                  <PageErrorAlert />
+                  {showOnboarding ? (
+                    <Fragment>
+                      {pageFilters}
+                      <Onboarding
+                        organization={organization}
+                        project={onboardingProject}
+                      />
+                    </Fragment>
+                  ) : (
+                    <Fragment>
+                      <SearchFilterContainer>
+                        {pageFilters}
+                        <MEPConsumer>
+                          {({metricSettingState}) =>
+                            metricSettingState === MEPState.metricsOnly &&
+                            shouldShowTransactionNameOnlySearch ? (
+                              // TODO replace `handleSearch prop` with transaction name search once
+                              // transaction name search becomes the default search bar
+                              <TransactionNameSearchBar
+                                organization={organization}
+                                location={location}
+                                eventView={eventView}
+                                onSearch={handleSearch}
+                                query={searchQuery}
+                              />
+                            ) : (
+                              <SearchBar
+                                searchSource="performance_landing"
+                                organization={organization}
+                                projectIds={eventView.project}
+                                query={searchQuery}
+                                fields={generateAggregateFields(
+                                  organization,
+                                  [...eventView.fields, {field: 'tps()'}],
+                                  ['epm()', 'eps()']
+                                )}
+                                onSearch={handleSearch}
+                                maxQueryLength={MAX_QUERY_LENGTH}
+                              />
+                            )
+                          }
+                        </MEPConsumer>
+                        <MetricsEventsDropdown />
+                      </SearchFilterContainer>
+                      {initiallyLoaded ? (
+                        <TeamKeyTransactionManager.Provider
                           organization={organization}
-                          location={location}
-                          eventView={eventView}
-                          onSearch={handleSearch}
-                          query={searchQuery}
-                        />
+                          teams={teams}
+                          selectedTeams={['myteams']}
+                          selectedProjects={eventView.project.map(String)}
+                        >
+                          <GenericQueryBatcher>
+                            <ViewComponent {...props} />
+                          </GenericQueryBatcher>
+                        </TeamKeyTransactionManager.Provider>
                       ) : (
-                        <SearchBar
-                          searchSource="performance_landing"
-                          organization={organization}
-                          projectIds={eventView.project}
-                          query={searchQuery}
-                          fields={generateAggregateFields(
-                            organization,
-                            [...eventView.fields, {field: 'tps()'}],
-                            ['epm()', 'eps()']
-                          )}
-                          onSearch={handleSearch}
-                          maxQueryLength={MAX_QUERY_LENGTH}
-                        />
-                      )
-                    }
-                  </Feature>
-                  <MetricsEventsDropdown />
-                </SearchFilterContainer>
-                {initiallyLoaded ? (
-                  <TeamKeyTransactionManager.Provider
-                    organization={organization}
-                    teams={teams}
-                    selectedTeams={['myteams']}
-                    selectedProjects={eventView.project.map(String)}
-                  >
-                    <GenericQueryBatcher>
-                      <ViewComponent {...props} />
-                    </GenericQueryBatcher>
-                  </TeamKeyTransactionManager.Provider>
-                ) : (
-                  <LoadingIndicator />
-                )}
-              </Fragment>
-            )}
+                        <LoadingIndicator />
+                      )}
+                    </Fragment>
+                  )}
+                </MEPSettingProvider>
+              )}
+            </MetricsDataSwitcher>
           </Layout.Main>
         </Layout.Body>
       </PageErrorProvider>

+ 291 - 0
static/app/views/performance/landing/metricsDataSwitcher.tsx

@@ -0,0 +1,291 @@
+import {Fragment} from 'react';
+import {Location} from 'history';
+
+import {Organization} from 'sentry/types';
+import DiscoverQuery, {TableData} from 'sentry/utils/discover/discoverQuery';
+import EventView from 'sentry/utils/discover/eventView';
+import {GenericChildrenProps} from 'sentry/utils/discover/genericDiscoverQuery';
+import {canUseMetricsData} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
+
+import {getMetricOnlyQueryParams} from './widgets/utils';
+
+export interface MetricDataSwitcherChildrenProps {
+  forceTransactionsOnly: boolean;
+  compatibleProjects?: number[];
+  shouldNotifyUnnamedTransactions?: boolean;
+  shouldWarnIncompatibleSDK?: boolean;
+}
+
+interface MetricDataSwitchProps {
+  children: (props: MetricDataSwitcherChildrenProps) => React.ReactNode;
+  eventView: EventView;
+  location: Location;
+  organization: Organization;
+}
+
+export enum LandingPageMEPDecision {
+  fallbackToTransactions = 'fallbackToTransactions',
+}
+
+interface DataCounts {
+  metricsCountData: GenericChildrenProps<TableData>;
+  nullData: GenericChildrenProps<TableData>;
+  transactionCountData: GenericChildrenProps<TableData>;
+  unparamData: GenericChildrenProps<TableData>;
+}
+
+/**
+ * This component decides based on some stats about current projects whether to show certain views of the landing page.
+ * It is primarily needed for the rollout during which time users, despite having the flag enabled,
+ * may or may not have sampling rules, compatible sdk's etc. This can be simplified post rollout.
+ */
+export function MetricsDataSwitcher(props: MetricDataSwitchProps) {
+  const isUsingMetrics = canUseMetricsData(props.organization);
+
+  if (!isUsingMetrics) {
+    return (
+      <Fragment>
+        {props.children({
+          forceTransactionsOnly: true,
+        })}
+      </Fragment>
+    );
+  }
+
+  const countView = props.eventView.withColumns([{kind: 'field', field: 'count()'}]);
+  countView.statsPeriod = '15m';
+  countView.start = undefined;
+  countView.end = undefined;
+  const unparamView = countView.clone();
+  unparamView.additionalConditions.setFilterValues('transaction', [
+    '<< unparameterized >>',
+  ]);
+  const nullView = countView.clone();
+  nullView.additionalConditions.setFilterValues('transaction', ['']);
+
+  const projectCompatibleView = countView.withColumns([
+    {kind: 'field', field: 'project.id'},
+    {kind: 'field', field: 'count()'},
+  ]);
+
+  const projectIncompatibleView = projectCompatibleView.clone();
+  projectIncompatibleView.additionalConditions.setFilterValues('transaction', ['']);
+
+  const baseDiscoverProps = {
+    location: props.location,
+    orgSlug: props.organization.slug,
+  };
+
+  const metricsDiscoverProps = {
+    ...baseDiscoverProps,
+    queryExtras: getMetricOnlyQueryParams(),
+  };
+
+  return (
+    <Fragment>
+      <DiscoverQuery eventView={countView} {...baseDiscoverProps}>
+        {transactionCountData => (
+          <DiscoverQuery eventView={countView} {...metricsDiscoverProps}>
+            {metricsCountData => (
+              <DiscoverQuery eventView={nullView} {...metricsDiscoverProps}>
+                {nullData => (
+                  <DiscoverQuery eventView={unparamView} {...metricsDiscoverProps}>
+                    {unparamData => (
+                      <DiscoverQuery
+                        eventView={projectCompatibleView}
+                        {...metricsDiscoverProps}
+                      >
+                        {projectsCompatData => (
+                          <DiscoverQuery
+                            eventView={projectIncompatibleView}
+                            {...metricsDiscoverProps}
+                          >
+                            {projectsIncompatData => {
+                              if (
+                                transactionCountData.isLoading ||
+                                unparamData.isLoading ||
+                                metricsCountData.isLoading ||
+                                projectsIncompatData.isLoading ||
+                                nullData.isLoading ||
+                                projectsCompatData.isLoading
+                              ) {
+                                return null;
+                              }
+
+                              const dataCounts: DataCounts = {
+                                transactionCountData,
+                                metricsCountData,
+                                nullData,
+                                unparamData,
+                              };
+
+                              const compatibleProjects = getCompatibleProjects({
+                                projectsCompatData,
+                                projectsIncompatData,
+                              });
+
+                              if (checkIfNotEffectivelySampling(dataCounts)) {
+                                return (
+                                  <Fragment>
+                                    {props.children({
+                                      forceTransactionsOnly: true,
+                                    })}
+                                  </Fragment>
+                                );
+                              }
+
+                              if (checkNoDataFallback(dataCounts)) {
+                                return (
+                                  <Fragment>
+                                    {props.children({
+                                      forceTransactionsOnly: true,
+                                    })}
+                                  </Fragment>
+                                );
+                              }
+
+                              if (checkIncompatibleData(dataCounts)) {
+                                return (
+                                  <Fragment>
+                                    {props.children({
+                                      shouldWarnIncompatibleSDK: true,
+                                      forceTransactionsOnly: true,
+                                      compatibleProjects,
+                                    })}
+                                  </Fragment>
+                                );
+                              }
+
+                              if (checkIfAllOtherData(dataCounts)) {
+                                return (
+                                  <Fragment>
+                                    {props.children({
+                                      shouldNotifyUnnamedTransactions: true,
+                                      forceTransactionsOnly: true,
+                                      compatibleProjects,
+                                    })}
+                                  </Fragment>
+                                );
+                              }
+
+                              if (checkIfPartialOtherData(dataCounts)) {
+                                return (
+                                  <Fragment>
+                                    {props.children({
+                                      shouldNotifyUnnamedTransactions: true,
+                                      compatibleProjects,
+
+                                      forceTransactionsOnly: false,
+                                    })}
+                                  </Fragment>
+                                );
+                              }
+
+                              return (
+                                <Fragment>
+                                  {props.children({
+                                    forceTransactionsOnly: false,
+                                  })}
+                                </Fragment>
+                              );
+                            }}
+                          </DiscoverQuery>
+                        )}
+                      </DiscoverQuery>
+                    )}
+                  </DiscoverQuery>
+                )}
+              </DiscoverQuery>
+            )}
+          </DiscoverQuery>
+        )}
+      </DiscoverQuery>
+    </Fragment>
+  );
+}
+
+/**
+ * Fallback if very similar amounts of metrics and transactions are found.
+ * Only used to rollout sampling before rules are selected. Could be replaced with project dynamic sampling check directly.
+ */
+function checkIfNotEffectivelySampling(dataCounts: DataCounts) {
+  const counts = extractCounts(dataCounts);
+  return (
+    counts.transactionsCount > 0 &&
+    counts.metricsCount > counts.transactionsCount &&
+    counts.transactionsCount >= counts.metricsCount * 0.95
+  );
+}
+
+/**
+ * Fallback if no metrics found.
+ */
+function checkNoDataFallback(dataCounts: DataCounts) {
+  const counts = extractCounts(dataCounts);
+  return !counts.metricsCount;
+}
+
+/**
+ * Fallback and warn if incompatible data found (old specific SDKs).
+ */
+function checkIncompatibleData(dataCounts: DataCounts) {
+  const counts = extractCounts(dataCounts);
+  return counts.nullCount > 0;
+}
+
+/**
+ * Fallback and warn about unnamed transactions (specific SDKs).
+ */
+function checkIfAllOtherData(dataCounts: DataCounts) {
+  const counts = extractCounts(dataCounts);
+  return counts.unparamCount >= counts.metricsCount;
+}
+
+/**
+ * Show metrics but warn about unnamed transactions.
+ */
+function checkIfPartialOtherData(dataCounts: DataCounts) {
+  const counts = extractCounts(dataCounts);
+  return counts.unparamCount > 0;
+}
+
+/**
+ * Temporary function, can be removed after API changes.
+ */
+function extractCounts({
+  metricsCountData,
+  transactionCountData,
+  unparamData,
+  nullData,
+}: DataCounts) {
+  const metricsCount = Number(metricsCountData.tableData?.data?.[0].count);
+  const transactionsCount = Number(transactionCountData.tableData?.data?.[0].count);
+  const unparamCount = Number(unparamData.tableData?.data?.[0].count);
+  const nullCount = Number(nullData.tableData?.data?.[0].count);
+
+  return {
+    metricsCount,
+    transactionsCount,
+    unparamCount,
+    nullCount,
+  };
+}
+
+/**
+ * Temporary function, can be removed after API changes.
+ */
+function getCompatibleProjects({
+  projectsCompatData,
+  projectsIncompatData,
+}: {
+  projectsCompatData: GenericChildrenProps<TableData>;
+  projectsIncompatData: GenericChildrenProps<TableData>;
+}) {
+  const baseProjectRows = projectsCompatData.tableData?.data || [];
+  const projectIdsPage = baseProjectRows.map(row => Number(row['project.id']));
+
+  const incompatProjectsRows = projectsIncompatData.tableData?.data || [];
+  const incompatProjectIds = incompatProjectsRows.map(row => Number(row['project.id']));
+
+  return projectIdsPage.filter(projectId => !incompatProjectIds.includes(projectId));
+}

+ 195 - 0
static/app/views/performance/landing/metricsDataSwitcherAlert.tsx

@@ -0,0 +1,195 @@
+import {useCallback, useMemo} from 'react';
+import {WithRouterProps} from 'react-router';
+import {Location} from 'history';
+
+import {updateProjects} from 'sentry/actionCreators/pageFilters';
+import Alert from 'sentry/components/alert';
+import {GlobalSdkUpdateAlert} from 'sentry/components/globalSdkUpdateAlert';
+import ExternalLink from 'sentry/components/links/externalLink';
+import Link from 'sentry/components/links/link';
+import {SidebarPanelKey} from 'sentry/components/sidebar/types';
+import {t, tct} from 'sentry/locale';
+import SidebarPanelStore from 'sentry/stores/sidebarPanelStore';
+import {NewQuery, Organization, Project} from 'sentry/types';
+import EventView from 'sentry/utils/discover/eventView';
+
+import {areMultipleProjectsSelected, getSelectedProjectPlatformsArray} from '../utils';
+
+import {MetricDataSwitcherChildrenProps} from './metricsDataSwitcher';
+
+interface MetricEnhancedDataAlertProps extends MetricDataSwitcherChildrenProps {
+  eventView: EventView;
+  location: Location;
+  organization: Organization;
+  projects: Project[];
+  router: WithRouterProps['router'];
+}
+
+/**
+ * From
+ * https://github.com/getsentry/sentry-docs/blob/master/src/platforms/common/enriching-events/transaction-name.mdx
+ */
+const SUPPORTED_TRANSACTION_NAME_DOCS = [
+  'javascript',
+  'node',
+  'python',
+  'ruby',
+  'native',
+  'react-native',
+  'dotnet',
+  'unity',
+  'flutter',
+  'dart',
+  'java',
+  'android',
+];
+const UNSUPPORTED_TRANSACTION_NAME_DOCS = [
+  'javascript.cordova',
+  'javascript.nextjs',
+  'native.minidumps',
+];
+
+function createUnnamedTransactionsDiscoverTarget(props: MetricEnhancedDataAlertProps) {
+  const fields = [
+    'transaction',
+    'project',
+    'transaction.source',
+    'tpm()',
+    'p50()',
+    'p95()',
+  ];
+
+  const query: NewQuery = {
+    id: undefined,
+    name: t('Performance - Unnamed Transactions '),
+    query:
+      'event.type:transaction (transaction.source:"url" OR transaction.source:"unknown")',
+    projects: [],
+    fields,
+    version: 2,
+  };
+
+  const discoverEventView = EventView.fromNewQueryWithLocation(query, props.location);
+  return discoverEventView.getResultsViewUrlTarget(props.organization.slug);
+}
+
+export function MetricsDataSwitcherAlert(
+  props: MetricEnhancedDataAlertProps
+): React.ReactElement | null {
+  const handleReviewUpdatesClick = useCallback(() => {
+    SidebarPanelStore.activatePanel(SidebarPanelKey.Broadcasts);
+  }, []);
+
+  const docsLink = useMemo(() => {
+    const platforms = getSelectedProjectPlatformsArray(props.location, props.projects);
+    if (platforms.length < 1) {
+      return null;
+    }
+
+    const platform = platforms[0];
+    if (UNSUPPORTED_TRANSACTION_NAME_DOCS.includes(platform)) {
+      return null;
+    }
+
+    const supportedPlatform = SUPPORTED_TRANSACTION_NAME_DOCS.find(platformBase =>
+      platform.includes(platformBase)
+    );
+
+    if (!supportedPlatform) {
+      return null;
+    }
+
+    return `https://docs.sentry.io/platforms/${supportedPlatform}/enriching-events/transaction-name/`;
+  }, [props.location, props.projects]);
+
+  const handleSwitchToCompatibleProjects = useCallback(() => {
+    updateProjects(props.compatibleProjects || [], props.router);
+  }, [props.compatibleProjects, props.router]);
+
+  if (!props.shouldNotifyUnnamedTransactions && !props.shouldWarnIncompatibleSDK) {
+    // Control showing generic sdk-alert here since stacking alerts is noisy.
+    return <GlobalSdkUpdateAlert />;
+  }
+
+  const discoverTarget = createUnnamedTransactionsDiscoverTarget(props);
+
+  if (props.shouldWarnIncompatibleSDK) {
+    const updateSDK = (
+      <Link to="" onClick={handleReviewUpdatesClick}>
+        {t('update your SDK version.')}
+      </Link>
+    );
+    if (areMultipleProjectsSelected(props.eventView)) {
+      return (
+        <Alert
+          type="warning"
+          showIcon
+          data-test-id="landing-mep-alert-multi-project-incompatible"
+        >
+          {tct(
+            `A few projects are incompatible with server side sampling. You can either [updateSDK] or [onlyViewCompatible]`,
+            {
+              updateSDK,
+              onlyViewCompatible: (
+                <Link to="" onClick={handleSwitchToCompatibleProjects}>
+                  {t('only view compatible projects.')}
+                </Link>
+              ),
+            }
+          )}
+        </Alert>
+      );
+    }
+
+    return (
+      <Alert
+        type="warning"
+        showIcon
+        data-test-id="landing-mep-alert-single-project-incompatible"
+      >
+        {tct(
+          `Your project has an outdated SDK which is incompatible with server side sampling. To enable this feature [updateSDK]`,
+          {
+            updateSDK,
+          }
+        )}
+      </Alert>
+    );
+  }
+
+  if (props.shouldNotifyUnnamedTransactions) {
+    const discover = <Link to={discoverTarget}>{t('open them in Discover.')}</Link>;
+    if (!docsLink) {
+      return (
+        <Alert type="warning" showIcon data-test-id="landing-mep-alert-unnamed-discover">
+          {tct(
+            `You have some unnamed transactions which are incompatible with server side sampling. You can [discover]`,
+            {
+              discover,
+            }
+          )}
+        </Alert>
+      );
+    }
+
+    return (
+      <Alert
+        type="warning"
+        showIcon
+        data-test-id="landing-mep-alert-unnamed-discover-or-set"
+      >
+        {tct(
+          `You have some unnamed transactions which are incompatible with server side sampling. You can either [setNames] or [discover]`,
+          {
+            setNames: (
+              <ExternalLink href={docsLink}>{t('set names manually')}</ExternalLink>
+            ),
+            discover,
+          }
+        )}
+      </Alert>
+    );
+  }
+
+  return null;
+}

+ 14 - 3
static/app/views/performance/landing/views/backendView.tsx

@@ -1,3 +1,7 @@
+import {
+  MetricsEnhancedSettingContext,
+  useMEPSettingContext,
+} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
 import {usePageError} from 'sentry/utils/performance/contexts/pageError';
 import {PerformanceDisplayProvider} from 'sentry/utils/performance/contexts/performanceDisplayContext';
 
@@ -10,7 +14,10 @@ import {PerformanceWidgetSetting} from '../widgets/widgetDefinitions';
 
 import {BasePerformanceViewProps} from './types';
 
-function getAllowedChartsSmall(props: BasePerformanceViewProps) {
+function getAllowedChartsSmall(
+  props: BasePerformanceViewProps,
+  mepSetting: MetricsEnhancedSettingContext
+) {
   const charts = [
     PerformanceWidgetSetting.APDEX_AREA,
     PerformanceWidgetSetting.TPM_AREA,
@@ -23,14 +30,18 @@ function getAllowedChartsSmall(props: BasePerformanceViewProps) {
     PerformanceWidgetSetting.DURATION_HISTOGRAM,
   ];
 
-  return filterAllowedChartsMetrics(props.organization, charts);
+  return filterAllowedChartsMetrics(props.organization, charts, mepSetting);
 }
 
 export function BackendView(props: BasePerformanceViewProps) {
+  const mepSetting = useMEPSettingContext();
   return (
     <PerformanceDisplayProvider value={{performanceType: PROJECT_PERFORMANCE_TYPE.ANY}}>
       <div>
-        <TripleChartRow {...props} allowedCharts={getAllowedChartsSmall(props)} />
+        <TripleChartRow
+          {...props}
+          allowedCharts={getAllowedChartsSmall(props, mepSetting)}
+        />
         <DoubleChartRow
           {...props}
           allowedCharts={[

+ 14 - 3
static/app/views/performance/landing/views/frontendOtherView.tsx

@@ -1,3 +1,7 @@
+import {
+  MetricsEnhancedSettingContext,
+  useMEPSettingContext,
+} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
 import {usePageError} from 'sentry/utils/performance/contexts/pageError';
 import {PerformanceDisplayProvider} from 'sentry/utils/performance/contexts/performanceDisplayContext';
 
@@ -10,7 +14,10 @@ import {PerformanceWidgetSetting} from '../widgets/widgetDefinitions';
 
 import {BasePerformanceViewProps} from './types';
 
-function getAllowedChartsSmall(props: BasePerformanceViewProps) {
+function getAllowedChartsSmall(
+  props: BasePerformanceViewProps,
+  mepSetting: MetricsEnhancedSettingContext
+) {
   const charts = [
     PerformanceWidgetSetting.TPM_AREA,
     PerformanceWidgetSetting.DURATION_HISTOGRAM,
@@ -20,16 +27,20 @@ function getAllowedChartsSmall(props: BasePerformanceViewProps) {
     PerformanceWidgetSetting.P99_DURATION_AREA,
   ];
 
-  return filterAllowedChartsMetrics(props.organization, charts);
+  return filterAllowedChartsMetrics(props.organization, charts, mepSetting);
 }
 
 export function FrontendOtherView(props: BasePerformanceViewProps) {
+  const mepSetting = useMEPSettingContext();
   return (
     <PerformanceDisplayProvider
       value={{performanceType: PROJECT_PERFORMANCE_TYPE.FRONTEND_OTHER}}
     >
       <div>
-        <TripleChartRow {...props} allowedCharts={getAllowedChartsSmall(props)} />
+        <TripleChartRow
+          {...props}
+          allowedCharts={getAllowedChartsSmall(props, mepSetting)}
+        />
         <DoubleChartRow
           {...props}
           allowedCharts={[

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