Browse Source

feat(cache): add cache widget (#70382)

Dominik Buszowiecki 10 months ago
parent
commit
72645b2df5

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

@@ -72,6 +72,7 @@ export enum PerformanceTerm {
   MOST_TIME_SPENT_DB_QUERIES = 'mostTimeSpentDbQueries',
   MOST_TIME_CONSUMING_RESOURCES = 'mostTimeConsumingResources',
   MOST_TIME_CONSUMING_DOMAINS = 'mostTimeConsumingDomains',
+  HIGHEST_CACHE_MISS_RATE_TRANSACTIONS = 'highestCacheMissRateTransactions',
 }
 
 export type TooltipOption = SelectValue<string> & {
@@ -386,6 +387,8 @@ export const PERFORMANCE_TERMS: Record<PerformanceTerm, TermFormatter> = {
     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.'),
+  highestCacheMissRateTransactions: () =>
+    t('Transactions with the highest cache miss rate.'),
   slowHTTPSpans: () => t('The transactions with the slowest spans of a certain type.'),
   stallPercentage: () =>
     t(

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

@@ -72,6 +72,12 @@ export function BackendView(props: BasePerformanceViewProps) {
     if (props.organization.features.includes('spans-first-ui')) {
       doubleChartRowCharts.unshift(PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS);
       doubleChartRowCharts.unshift(PerformanceWidgetSetting.MOST_TIME_SPENT_DB_QUERIES);
+
+      if (props.organization.features.includes('performance-cache-view')) {
+        doubleChartRowCharts.unshift(
+          PerformanceWidgetSetting.HIGHEST_CACHE_MISS_RATE_TRANSACTIONS
+        );
+      }
     }
   } else {
     doubleChartRowCharts.push(

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

@@ -123,6 +123,26 @@ export function TimeConsumingDomainsWidgetEmptyStateWarning() {
   );
 }
 
+export function HighestCacheMissRateTransactionsWidgetEmptyStateWarning() {
+  return (
+    <StyledEmptyStateWarning>
+      <PrimaryMessage>{t('No results found')}</PrimaryMessage>
+      <SecondaryMessage>
+        {tct(
+          'Transactions 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/cache/">
+                {t('Cache 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

@@ -977,6 +977,46 @@ describe('Performance > Widgets > WidgetContainer', function () {
     );
   });
 
+  it('Highest cache miss rate transactions widget', async function () {
+    const data = initializeData();
+
+    wrapper = render(
+      <MEPSettingProvider forceTransactions>
+        <WrappedComponent
+          data={data}
+          defaultChartSetting={
+            PerformanceWidgetSetting.HIGHEST_CACHE_MISS_RATE_TRANSACTIONS
+          }
+        />
+      </MEPSettingProvider>
+    );
+
+    expect(await screen.findByTestId('performance-widget-title')).toHaveTextContent(
+      'Highest Cache Miss Rates'
+    );
+    expect(eventsMock).toHaveBeenCalledTimes(1);
+    expect(eventsMock).toHaveBeenNthCalledWith(
+      1,
+      expect.anything(),
+      expect.objectContaining({
+        query: expect.objectContaining({
+          cursor: '0:0:1',
+          dataset: 'spansMetrics',
+          environment: ['prod'],
+          field: ['transaction', 'project.id', 'cache_miss_rate()'],
+          noPagination: true,
+          per_page: QUERY_LIMIT_PARAM,
+          project: ['-42'],
+          query: 'span.op:cache.get_item',
+          statsPeriod: '7d',
+          referrer:
+            'api.performance.generic-widget-chart.highest-cache--miss-rate-transactions',
+          sort: '-cache_miss_rate()',
+        }),
+      })
+    );
+  });
+
   it('Best Page Opportunities widget', async function () {
     const data = initializeData();
 

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

@@ -30,8 +30,11 @@ export function WidgetHeader<T extends WidgetDataConstraint>(
   const isRequestsWidget =
     chartSetting === PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS;
 
+  const isCacheWidget =
+    chartSetting === PerformanceWidgetSetting.HIGHEST_CACHE_MISS_RATE_TRANSACTIONS;
+
   const featureBadge =
-    isWebVitalsWidget || isResourcesWidget || isRequestsWidget ? (
+    isWebVitalsWidget || isResourcesWidget || isRequestsWidget || isCacheWidget ? (
       <FeatureBadge type="new" />
     ) : null;
 

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

@@ -70,6 +70,7 @@ export enum PerformanceWidgetSetting {
   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',
+  HIGHEST_CACHE_MISS_RATE_TRANSACTIONS = 'highest_cache__miss_rate_transactions',
 }
 
 const WIDGET_PALETTE = CHART_PALETTE[5];
@@ -287,6 +288,17 @@ export const WIDGET_DEFINITIONS: ({
     dataType: GenericPerformanceWidgetDataType.LINE_LIST,
     chartColor: WIDGET_PALETTE[0],
   },
+  [PerformanceWidgetSetting.HIGHEST_CACHE_MISS_RATE_TRANSACTIONS]: {
+    title: t('Highest Cache Miss Rates'),
+    subTitle: t('Suggested Transactions'),
+    titleTooltip: getTermHelp(
+      organization,
+      PerformanceTerm.HIGHEST_CACHE_MISS_RATE_TRANSACTIONS
+    ),
+    fields: [`cache_miss_rate()`],
+    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'),

+ 85 - 1
static/app/views/performance/landing/widgets/widgets/lineChartListWidget.tsx

@@ -1,6 +1,7 @@
 import {Fragment, useMemo, useState} from 'react';
 import styled from '@emotion/styled';
 import pick from 'lodash/pick';
+import * as qs from 'query-string';
 
 import Accordion from 'sentry/components/accordion/accordion';
 import {LinkButton} from 'sentry/components/button';
@@ -14,6 +15,7 @@ import Truncate from 'sentry/components/truncate';
 import {t, tct} from 'sentry/locale';
 import DiscoverQuery from 'sentry/utils/discover/discoverQuery';
 import {DiscoverDatasets} from 'sentry/utils/discover/types';
+import {formatPercentage} from 'sentry/utils/formatters';
 import {
   canUseMetricsData,
   useMEPSettingContext,
@@ -22,8 +24,10 @@ import {usePageAlert} from 'sentry/utils/performance/contexts/pageAlert';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
 import {useLocation} from 'sentry/utils/useLocation';
 import withApi from 'sentry/utils/withApi';
+import {normalizeUrl} from 'sentry/utils/withDomainRequired';
 import {DEFAULT_RESOURCE_TYPES} from 'sentry/views/performance/browser/resources/resourceView';
 import {getResourcesEventViewQuery} from 'sentry/views/performance/browser/resources/utils/useResourcesQuery';
+import {BASE_FILTERS, CACHE_BASE_URL} from 'sentry/views/performance/cache/settings';
 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';
@@ -34,7 +38,7 @@ import {
 import {getPerformanceDuration} from 'sentry/views/performance/utils/getPerformanceDuration';
 import {SpanDescriptionCell} from 'sentry/views/starfish/components/tableCells/spanDescriptionCell';
 import {TimeSpentCell} from 'sentry/views/starfish/components/tableCells/timeSpentCell';
-import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types';
+import {ModuleName, SpanFunction, SpanMetricsField} from 'sentry/views/starfish/types';
 import {STARFISH_CHART_INTERVAL_FIDELITY} from 'sentry/views/starfish/utils/constants';
 import {RoutingContextProvider} from 'sentry/views/starfish/utils/routingContext';
 
@@ -42,6 +46,7 @@ import {excludeTransaction} from '../../utils';
 import {GenericPerformanceWidget} from '../components/performanceWidget';
 import SelectableList, {
   GrowLink,
+  HighestCacheMissRateTransactionsWidgetEmptyStateWarning,
   ListClose,
   RightAlignedCell,
   Subtitle,
@@ -101,6 +106,10 @@ export function LineChartListWidget(props: PerformanceWidgetProps) {
     props.chartSetting === PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS
   ) {
     emptyComponent = TimeConsumingDomainsWidgetEmptyStateWarning;
+  } else if (
+    props.chartSetting === PerformanceWidgetSetting.HIGHEST_CACHE_MISS_RATE_TRANSACTIONS
+  ) {
+    emptyComponent = HighestCacheMissRateTransactionsWidgetEmptyStateWarning;
   } else {
     emptyComponent = canHaveIntegrationEmptyState
       ? () => (
@@ -239,6 +248,28 @@ export function LineChartListWidget(props: PerformanceWidgetProps) {
             {'resource.render_blocking_status': 'blocking'},
             DEFAULT_RESOURCE_TYPES
           ).join(' ')}`;
+        } else if (
+          props.chartSetting ===
+          PerformanceWidgetSetting.HIGHEST_CACHE_MISS_RATE_TRANSACTIONS
+        ) {
+          eventView.fields = [
+            {field: SpanMetricsField.TRANSACTION},
+            {field: 'project.id'},
+            {field},
+          ];
+
+          // Change data set to spansMetrics
+          eventView.dataset = DiscoverDatasets.SPANS_METRICS;
+          extraQueryParams = {
+            ...extraQueryParams,
+            dataset: DiscoverDatasets.SPANS_METRICS,
+          };
+
+          // Update query
+          const mutableSearch = MutableSearch.fromQueryObject(BASE_FILTERS);
+          eventView.additionalConditions.removeFilter('event.type');
+          eventView.additionalConditions.removeFilter('transaction.op');
+          eventView.query = mutableSearch.formatString();
         } else if (isSlowestType || isFramesType) {
           eventView.additionalConditions.setFilterValues('epm()', ['>0.01']);
           eventView.fields = [
@@ -257,6 +288,7 @@ export function LineChartListWidget(props: PerformanceWidgetProps) {
             PerformanceWidgetSetting.MOST_TIME_SPENT_DB_QUERIES,
             PerformanceWidgetSetting.MOST_TIME_CONSUMING_RESOURCES,
             PerformanceWidgetSetting.MOST_TIME_CONSUMING_DOMAINS,
+            PerformanceWidgetSetting.HIGHEST_CACHE_MISS_RATE_TRANSACTIONS,
           ].includes(props.chartSetting)
         ) {
           eventView.additionalConditions.setFilterValues(field, ['>0']);
@@ -389,6 +421,34 @@ export function LineChartListWidget(props: PerformanceWidgetProps) {
               );
             }
 
+            const mutableSearch = new MutableSearch(eventView.query);
+            mutableSearch.removeFilter('transaction');
+            eventView.query = mutableSearch.formatString();
+          } else if (
+            props.chartSetting ===
+            PerformanceWidgetSetting.HIGHEST_CACHE_MISS_RATE_TRANSACTIONS
+          ) {
+            // Update request params
+            eventView.dataset = DiscoverDatasets.SPANS_METRICS;
+            extraQueryParams = {
+              ...extraQueryParams,
+              dataset: DiscoverDatasets.SPANS_METRICS,
+              excludeOther: false,
+              per_page: 50,
+            };
+            eventView.fields = [];
+
+            // Update chart options
+            partialDataParam = false;
+            yAxis = `${SpanFunction.CACHE_MISS_RATE}()`;
+            interval = getInterval(pageFilterDatetime, STARFISH_CHART_INTERVAL_FIDELITY);
+            includePreviousParam = false;
+            currentSeriesNames = [`${SpanFunction.CACHE_MISS_RATE}()`];
+
+            // Update search query
+            eventView.additionalConditions.removeFilter('event.type');
+            eventView.additionalConditions.removeFilter('transaction.op');
+
             const mutableSearch = new MutableSearch(eventView.query);
             mutableSearch.removeFilter('transaction');
             eventView.query = mutableSearch.formatString();
@@ -624,6 +684,30 @@ export function LineChartListWidget(props: PerformanceWidgetProps) {
                   </Fragment>
                 </RoutingContextProvider>
               );
+            case PerformanceWidgetSetting.HIGHEST_CACHE_MISS_RATE_TRANSACTIONS:
+              const cacheMissRate = listItem[fieldString];
+              const target = normalizeUrl(
+                `${CACHE_BASE_URL}/?${qs.stringify({transaction: transaction})}`
+              );
+              return (
+                <Fragment>
+                  <GrowLink to={target}>
+                    <Truncate value={transaction} maxLength={40} />
+                  </GrowLink>
+                  <RightAlignedCell>{formatPercentage(cacheMissRate)}</RightAlignedCell>
+                  {!props.withStaticFilters && (
+                    <ListClose
+                      setSelectListIndex={setSelectListIndex}
+                      onClick={() =>
+                        excludeTransaction(listItem.transaction, {
+                          eventView: props.eventView,
+                          location,
+                        })
+                      }
+                    />
+                  )}
+                </Fragment>
+              );
             default:
               if (typeof rightValue === 'number') {
                 return (