Browse Source

feat(perf): Most Time-Consuming Domains landing page widget (#69433)

A new widget to match the "Requests" module. Available on all landing
pages, since every stack makes network requests!
George Gritsouk 10 months ago
parent
commit
74f09c81db

+ 3 - 0
static/app/views/performance/data.tsx

@@ -71,6 +71,7 @@ export enum PerformanceTerm {
   TIME_TO_INITIAL_DISPLAY = 'timeToInitialDisplay',
   MOST_TIME_SPENT_DB_QUERIES = 'mostTimeSpentDbQueries',
   MOST_TIME_CONSUMING_RESOURCES = 'mostTimeConsumingResources',
+  MOST_TIME_CONSUMING_DOMAINS = 'mostTimeConsumingDomains',
 }
 
 export type TooltipOption = SelectValue<string> & {
@@ -383,6 +384,8 @@ export const PERFORMANCE_TERMS: Record<PerformanceTerm, TermFormatter> = {
     t('Database spans on which the application spent most of its total time.'),
   mostTimeConsumingResources: () =>
     t('Render blocking resources on which the application spent most of its total time.'),
+  mostTimeConsumingDomains: () =>
+    t('Outgoing HTTP domains on which the application spent most of its total time.'),
   slowHTTPSpans: () => t('The transactions with the slowest spans of a certain type.'),
   stallPercentage: () =>
     t(

+ 5 - 0
static/app/views/performance/landing/views/allTransactionsView.tsx

@@ -48,6 +48,11 @@ export function AllTransactionsView(props: BasePerformanceViewProps) {
     if (props.organization.features.includes('spans-first-ui')) {
       doubleChartRowCharts.unshift(PerformanceWidgetSetting.MOST_RELATED_ISSUES);
       doubleChartRowCharts.unshift(PerformanceWidgetSetting.MOST_CHANGED);
+    }
+    if (props.organization.features.includes('performance-http-view')) {
+      doubleChartRowCharts.unshift(PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS);
+    }
+    if (props.organization.features.includes('spans-first-ui')) {
       doubleChartRowCharts.unshift(PerformanceWidgetSetting.MOST_TIME_SPENT_DB_QUERIES);
     } else {
       doubleChartRowCharts.unshift(PerformanceWidgetSetting.MOST_CHANGED);

+ 4 - 0
static/app/views/performance/landing/views/backendView.tsx

@@ -69,6 +69,10 @@ export function BackendView(props: BasePerformanceViewProps) {
       doubleChartRowCharts.push(PerformanceWidgetSetting.MOST_CHANGED);
     }
 
+    if (props.organization.features.includes('performance-http-view')) {
+      doubleChartRowCharts.unshift(PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS);
+    }
+
     if (props.organization.features.includes('spans-first-ui')) {
       doubleChartRowCharts.unshift(PerformanceWidgetSetting.MOST_TIME_SPENT_DB_QUERIES);
     }

+ 4 - 0
static/app/views/performance/landing/views/frontendOtherView.tsx

@@ -37,6 +37,10 @@ export function FrontendOtherView(props: BasePerformanceViewProps) {
     PerformanceWidgetSetting.SLOW_RESOURCE_OPS,
   ];
 
+  if (props.organization.features.includes('performance-http-view')) {
+    doubleChartRowCharts.unshift(PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS);
+  }
+
   if (props.organization.features.includes('spans-first-ui')) {
     doubleChartRowCharts.unshift(PerformanceWidgetSetting.MOST_TIME_CONSUMING_RESOURCES);
   }

+ 4 - 0
static/app/views/performance/landing/views/mobileView.tsx

@@ -60,6 +60,10 @@ export function MobileView(props: BasePerformanceViewProps) {
       ...[PerformanceWidgetSetting.MOST_IMPROVED, PerformanceWidgetSetting.MOST_REGRESSED]
     );
   }
+
+  if (props.organization.features.includes('performance-http-view')) {
+    doubleRowAllowedCharts.push(PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS);
+  }
   return (
     <PerformanceDisplayProvider value={{performanceType: ProjectPerformanceType.ANY}}>
       <div>

+ 20 - 0
static/app/views/performance/landing/widgets/components/selectableList.tsx

@@ -103,6 +103,26 @@ export function TimeSpentInDatabaseWidgetEmptyStateWarning() {
   );
 }
 
+export function TimeConsumingDomainsWidgetEmptyStateWarning() {
+  return (
+    <StyledEmptyStateWarning>
+      <PrimaryMessage>{t('No results found')}</PrimaryMessage>
+      <SecondaryMessage>
+        {tct(
+          'Domains may be missing due to the filters above, a low sampling rate, or an error with instrumentation. Please see the [link] for more information.',
+          {
+            link: (
+              <ExternalLink href="https://docs.sentry.io/product/performance/requests/">
+                {t('Requests module documentation')}
+              </ExternalLink>
+            ),
+          }
+        )}
+      </SecondaryMessage>
+    </StyledEmptyStateWarning>
+  );
+}
+
 export function WidgetAddInstrumentationWarning({type}: {type: 'db' | 'http'}) {
   const pageFilters = usePageFilters();
   const fullProjects = useProjects();

+ 40 - 0
static/app/views/performance/landing/widgets/components/widgetContainer.spec.tsx

@@ -894,6 +894,46 @@ describe('Performance > Widgets > WidgetContainer', function () {
     );
   });
 
+  it('Most time consuming domains widget', async function () {
+    const data = initializeData();
+
+    wrapper = render(
+      <MEPSettingProvider forceTransactions>
+        <WrappedComponent
+          data={data}
+          defaultChartSetting={PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS}
+        />
+      </MEPSettingProvider>
+    );
+
+    expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
+      'Most Time-Consuming Domains'
+    );
+    expect(eventsMock).toHaveBeenCalledTimes(1);
+    expect(eventsMock).toHaveBeenNthCalledWith(
+      1,
+      expect.anything(),
+      expect.objectContaining({
+        query: expect.objectContaining({
+          dataset: 'spansMetrics',
+          environment: ['prod'],
+          field: [
+            'project.id',
+            'span.domain',
+            'sum(span.self_time)',
+            'avg(span.self_time)',
+            'time_spent_percentage()',
+          ],
+          per_page: QUERY_LIMIT_PARAM,
+          project: ['-42'],
+          query: 'span.module:http',
+          sort: '-time_spent_percentage()',
+          statsPeriod: '7d',
+        }),
+      })
+    );
+  });
+
   it('Most time consuming resources widget', async function () {
     const data = initializeData();
 

+ 6 - 1
static/app/views/performance/landing/widgets/components/widgetHeader.tsx

@@ -27,8 +27,13 @@ export function WidgetHeader<T extends WidgetDataConstraint>(
   const isResourcesWidget =
     chartSetting === PerformanceWidgetSetting.MOST_TIME_CONSUMING_RESOURCES;
 
+  const isRequestsWidget =
+    chartSetting === PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS;
+
   const featureBadge =
-    isWebVitalsWidget || isResourcesWidget ? <FeatureBadge type="new" /> : null;
+    isWebVitalsWidget || isResourcesWidget || isRequestsWidget ? (
+      <FeatureBadge type="new" />
+    ) : null;
 
   return (
     <WidgetHeaderContainer>

+ 9 - 0
static/app/views/performance/landing/widgets/widgetDefinitions.tsx

@@ -66,6 +66,7 @@ export enum PerformanceWidgetSetting {
   TIME_TO_FULL_DISPLAY = 'time_to_full_display',
   OVERALL_PERFORMANCE_SCORE = 'overall_performance_score',
   MOST_TIME_CONSUMING_RESOURCES = 'most_time_consuming_resources',
+  MOST_TIME_CONSUMING_DOMAINS = 'most_time_consuming_domains',
   SLOW_SCREENS_BY_TTID = 'slow_screens_by_ttid',
   SLOW_SCREENS_BY_COLD_START = 'slow_screens_by_cold_start',
   SLOW_SCREENS_BY_WARM_START = 'slow_screens_by_warm_start',
@@ -286,6 +287,14 @@ export const WIDGET_DEFINITIONS: ({
     dataType: GenericPerformanceWidgetDataType.LINE_LIST,
     chartColor: WIDGET_PALETTE[0],
   },
+  [PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS]: {
+    title: t('Most Time-Consuming Domains'),
+    subTitle: t('Top outgoing HTTP request domains by time spent'),
+    titleTooltip: getTermHelp(organization, PerformanceTerm.MOST_TIME_CONSUMING_DOMAINS),
+    fields: [`time_spent_percentage()`],
+    dataType: GenericPerformanceWidgetDataType.LINE_LIST,
+    chartColor: WIDGET_PALETTE[0],
+  },
   [PerformanceWidgetSetting.HIGHEST_OPPORTUNITY_PAGES]: {
     title: t('Best Page Opportunities'),
     subTitle: t('Pages to improve your performance score'),

+ 107 - 15
static/app/views/performance/landing/widgets/widgets/lineChartListWidget.tsx

@@ -25,6 +25,7 @@ import withApi from 'sentry/utils/withApi';
 import {DEFAULT_RESOURCE_TYPES} from 'sentry/views/performance/browser/resources/resourceView';
 import {getResourcesEventViewQuery} from 'sentry/views/performance/browser/resources/utils/useResourcesQuery';
 import DurationChart from 'sentry/views/performance/charts/chart';
+import {DomainCell} from 'sentry/views/performance/http/tables/domainCell';
 import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
 import {
   createUnnamedTransactionsDiscoverTarget,
@@ -44,6 +45,7 @@ import SelectableList, {
   ListClose,
   RightAlignedCell,
   Subtitle,
+  TimeConsumingDomainsWidgetEmptyStateWarning,
   TimeSpentInDatabaseWidgetEmptyStateWarning,
   WidgetAddInstrumentationWarning,
   WidgetEmptyStateWarning,
@@ -95,6 +97,10 @@ export function LineChartListWidget(props: PerformanceWidgetProps) {
   let emptyComponent;
   if (props.chartSetting === PerformanceWidgetSetting.MOST_TIME_SPENT_DB_QUERIES) {
     emptyComponent = TimeSpentInDatabaseWidgetEmptyStateWarning;
+  } else if (
+    props.chartSetting === PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS
+  ) {
+    emptyComponent = TimeConsumingDomainsWidgetEmptyStateWarning;
   } else {
     emptyComponent = canHaveIntegrationEmptyState
       ? () => (
@@ -174,6 +180,34 @@ export function LineChartListWidget(props: PerformanceWidgetProps) {
           mutableSearch.addFilterValue('has', 'span.description');
           mutableSearch.addFilterValue('span.module', 'db');
           eventView.query = mutableSearch.formatString();
+        } else if (
+          props.chartSetting === PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS
+        ) {
+          // Set fields
+          eventView.fields = [
+            {field: SpanMetricsField.PROJECT_ID},
+            {field: SpanMetricsField.SPAN_DOMAIN},
+            {field: `sum(${SpanMetricsField.SPAN_SELF_TIME})`},
+            {field: `avg(${SpanMetricsField.SPAN_SELF_TIME})`},
+            {field},
+          ];
+
+          // Change data set to spansMetrics
+          eventView.dataset = DiscoverDatasets.SPANS_METRICS;
+          extraQueryParams = {
+            ...extraQueryParams,
+            dataset: DiscoverDatasets.SPANS_METRICS,
+          };
+
+          // Update query
+          const mutableSearch = new MutableSearch(eventView.query);
+          mutableSearch.removeFilter('event.type');
+          mutableSearch.removeFilter('transaction.op');
+          eventView.additionalConditions.removeFilter('event.type');
+          eventView.additionalConditions.removeFilter('transaction.op');
+          eventView.additionalConditions.removeFilter('time_spent_percentage()');
+          mutableSearch.addFilterValue('span.module', 'http');
+          eventView.query = mutableSearch.formatString();
         } else if (
           props.chartSetting === PerformanceWidgetSetting.MOST_TIME_CONSUMING_RESOURCES
         ) {
@@ -222,6 +256,7 @@ export function LineChartListWidget(props: PerformanceWidgetProps) {
           ![
             PerformanceWidgetSetting.MOST_TIME_SPENT_DB_QUERIES,
             PerformanceWidgetSetting.MOST_TIME_CONSUMING_RESOURCES,
+            PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS,
           ].includes(props.chartSetting)
         ) {
           eventView.additionalConditions.setFilterValues(field, ['>0']);
@@ -269,11 +304,13 @@ export function LineChartListWidget(props: PerformanceWidgetProps) {
           let partialDataParam = true;
 
           if (
-            !provided.widgetData.list.data[selectedListIndex] ||
-            (!provided.widgetData.list.data[selectedListIndex]?.transaction &&
-              !provided.widgetData.list.data[selectedListIndex][
-                SpanMetricsField.SPAN_DESCRIPTION
-              ])
+            !provided.widgetData.list.data[selectedListIndex]?.transaction &&
+            !provided.widgetData.list.data[selectedListIndex][
+              SpanMetricsField.SPAN_DESCRIPTION
+            ] &&
+            !provided.widgetData.list.data[selectedListIndex][
+              SpanMetricsField.SPAN_DOMAIN
+            ]
           ) {
             return null;
           }
@@ -308,7 +345,9 @@ export function LineChartListWidget(props: PerformanceWidgetProps) {
             eventView.query = mutableSearch.formatString();
           } else if (
             props.chartSetting === PerformanceWidgetSetting.MOST_TIME_SPENT_DB_QUERIES ||
-            props.chartSetting === PerformanceWidgetSetting.MOST_TIME_CONSUMING_RESOURCES
+            props.chartSetting ===
+              PerformanceWidgetSetting.MOST_TIME_CONSUMING_RESOURCES ||
+            props.chartSetting === PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS
           ) {
             // Update request params
             eventView.dataset = DiscoverDatasets.SPANS_METRICS;
@@ -330,18 +369,33 @@ export function LineChartListWidget(props: PerformanceWidgetProps) {
             // Update search query
             eventView.additionalConditions.removeFilter('event.type');
             eventView.additionalConditions.removeFilter('transaction');
-            eventView.additionalConditions.addFilterValue(
-              SpanMetricsField.SPAN_GROUP,
-              provided.widgetData.list.data[selectedListIndex][
-                SpanMetricsField.SPAN_GROUP
-              ].toString()
-            );
+
+            if (
+              props.chartSetting === PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS
+            ) {
+              eventView.additionalConditions.addFilterValue(
+                SpanMetricsField.SPAN_DOMAIN,
+                provided.widgetData.list.data[selectedListIndex][
+                  SpanMetricsField.SPAN_DOMAIN
+                ].toString(),
+                false
+              );
+            } else {
+              eventView.additionalConditions.addFilterValue(
+                SpanMetricsField.SPAN_GROUP,
+                provided.widgetData.list.data[selectedListIndex][
+                  SpanMetricsField.SPAN_GROUP
+                ].toString()
+              );
+            }
+
             const mutableSearch = new MutableSearch(eventView.query);
             mutableSearch.removeFilter('transaction');
             eventView.query = mutableSearch.formatString();
           } else {
             eventView.fields = [{field: 'transaction'}, {field}];
           }
+
           return (
             <EventsRequest
               {...pick(provided, eventsRequestQueryProps)}
@@ -488,6 +542,39 @@ export function LineChartListWidget(props: PerformanceWidgetProps) {
                   )}
                 </Fragment>
               );
+            case PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS:
+              return (
+                <RoutingContextProvider value={{baseURL: '/performance/http'}}>
+                  <Fragment>
+                    <StyledTextOverflow>
+                      <DomainCell
+                        projectId={listItem[SpanMetricsField.PROJECT_ID].toString()}
+                        domain={listItem[SpanMetricsField.SPAN_DOMAIN]}
+                      />
+                    </StyledTextOverflow>
+
+                    <RightAlignedCell>
+                      <TimeSpentCell
+                        percentage={listItem[fieldString]}
+                        total={listItem[`sum(${SpanMetricsField.SPAN_SELF_TIME})`]}
+                        op={'http.client'}
+                      />
+                    </RightAlignedCell>
+
+                    {!props.withStaticFilters && (
+                      <ListClose
+                        setSelectListIndex={setSelectListIndex}
+                        onClick={() =>
+                          excludeTransaction(listItem.transaction, {
+                            eventView: props.eventView,
+                            location,
+                          })
+                        }
+                      />
+                    )}
+                  </Fragment>
+                </RoutingContextProvider>
+              );
             case PerformanceWidgetSetting.MOST_TIME_SPENT_DB_QUERIES:
             case PerformanceWidgetSetting.MOST_TIME_CONSUMING_RESOURCES:
               const description: string = listItem[SpanMetricsField.SPAN_DESCRIPTION];
@@ -628,12 +715,17 @@ export function LineChartListWidget(props: PerformanceWidgetProps) {
 
   const getContainerActions = provided => {
     const route =
-      props.chartSetting === PerformanceWidgetSetting.MOST_TIME_SPENT_DB_QUERIES
-        ? 'performance/database/'
-        : 'performance/browser/resources/';
+      {
+        [PerformanceWidgetSetting.MOST_TIME_SPENT_DB_QUERIES]: 'performance/database/',
+        [PerformanceWidgetSetting.MOST_TIME_CONSUMING_RESOURCES]:
+          'performance/browser/resources/',
+        [PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS]: 'performance/http/',
+      }[props.chartSetting] ?? '';
+
     return [
       PerformanceWidgetSetting.MOST_TIME_SPENT_DB_QUERIES,
       PerformanceWidgetSetting.MOST_TIME_CONSUMING_RESOURCES,
+      PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS,
     ].includes(props.chartSetting) ? (
       <Fragment>
         <div>