Browse Source

feat(transaction-summary): Add analytics to track biases sufficiency assessment (#48865)

Priscila Oliveira 1 year ago
parent
commit
d8bdb0504f

+ 151 - 61
static/app/components/discover/transactionsList.tsx

@@ -1,4 +1,4 @@
-import {Component, Fragment} from 'react';
+import React, {Component, Fragment, useContext, useEffect} from 'react';
 import {browserHistory} from 'react-router';
 import styled from '@emotion/styled';
 import {Location, LocationDescriptor, Query} from 'history';
@@ -22,7 +22,11 @@ import {TableColumn} from 'sentry/views/discover/table/types';
 import {decodeColumnOrder} from 'sentry/views/discover/utils';
 import {SpanOperationBreakdownFilter} from 'sentry/views/performance/transactionSummary/filter';
 import {mapShowTransactionToPercentile} from 'sentry/views/performance/transactionSummary/transactionEvents/utils';
-import {TransactionFilterOptions} from 'sentry/views/performance/transactionSummary/utils';
+import {PerformanceAtScaleContext} from 'sentry/views/performance/transactionSummary/transactionOverview/performanceAtScaleContext';
+import {
+  DisplayModes,
+  TransactionFilterOptions,
+} from 'sentry/views/performance/transactionSummary/utils';
 import {TrendChangeType, TrendView} from 'sentry/views/performance/trends/types';
 
 import TransactionsTable from './transactionsTable';
@@ -121,6 +125,103 @@ type Props = {
   trendView?: TrendView;
 };
 
+type TableRenderProps = Omit<React.ComponentProps<typeof Pagination>, 'size'> &
+  React.ComponentProps<typeof TransactionsTable> & {
+    header: React.ReactNode;
+    paginationCursorSize: React.ComponentProps<typeof Pagination>['size'];
+    target?: string;
+  };
+
+function TableRender({
+  pageLinks,
+  onCursor,
+  header,
+  eventView,
+  organization,
+  isLoading,
+  location,
+  columnOrder,
+  tableData,
+  titles,
+  generateLink,
+  handleCellAction,
+  referrer,
+  useAggregateAlias,
+  target,
+  paginationCursorSize,
+}: TableRenderProps) {
+  const query = decodeScalar(location.query.query, '');
+  const display = decodeScalar(location.query.display, DisplayModes.DURATION);
+  const performanceAtScaleContext = useContext(PerformanceAtScaleContext);
+  const hasResults =
+    tableData && tableData.data && tableData.meta && tableData.data.length > 0;
+
+  useEffect(() => {
+    if (!performanceAtScaleContext) {
+      return;
+    }
+
+    // we are now only collecting analytics data from the transaction summary page
+    // when the display mode is set to duration
+    if (display !== DisplayModes.DURATION) {
+      return;
+    }
+
+    if (isLoading || hasResults === null) {
+      performanceAtScaleContext.setTransactionListTableData(undefined);
+      return;
+    }
+
+    if (
+      !hasResults === performanceAtScaleContext.transactionListTableData?.empty &&
+      query === performanceAtScaleContext.transactionListTableData?.query
+    ) {
+      return;
+    }
+
+    performanceAtScaleContext.setTransactionListTableData({
+      empty: !hasResults,
+      query,
+    });
+  }, [display, isLoading, hasResults, performanceAtScaleContext, query]);
+
+  const content = (
+    <TransactionsTable
+      eventView={eventView}
+      organization={organization}
+      location={location}
+      isLoading={isLoading}
+      tableData={tableData}
+      columnOrder={columnOrder}
+      titles={titles}
+      generateLink={generateLink}
+      handleCellAction={handleCellAction}
+      useAggregateAlias={useAggregateAlias}
+      referrer={referrer}
+    />
+  );
+
+  return (
+    <Fragment>
+      <Header>
+        {header}
+        <StyledPagination
+          pageLinks={pageLinks}
+          onCursor={onCursor}
+          size={paginationCursorSize}
+        />
+      </Header>
+      {target ? (
+        <GuideAnchor target={target} position="top-start">
+          {content}
+        </GuideAnchor>
+      ) : (
+        content
+      )}
+    </Fragment>
+  );
+}
+
 class _TransactionsList extends Component<Props> {
   static defaultProps = {
     cursorName: 'transactionCursor',
@@ -235,41 +336,29 @@ class _TransactionsList extends Component<Props> {
     const eventView = this.getEventView();
     const columnOrder = eventView.getColumns();
     const cursor = decodeScalar(location.query?.[cursorName]);
-
-    const tableRenderer = ({isLoading, pageLinks, tableData}) => (
-      <Fragment>
-        <Header>
-          {this.renderHeader()}
-          <StyledPagination
-            pageLinks={pageLinks}
-            onCursor={this.handleCursor}
-            size="xs"
-          />
-        </Header>
-        <GuideAnchor target="transactions_table" position="top-start">
-          <TransactionsTable
-            eventView={eventView}
-            organization={organization}
-            location={location}
-            isLoading={isLoading}
-            tableData={tableData}
-            columnOrder={columnOrder}
-            titles={titles}
-            generateLink={generateLink}
-            handleCellAction={handleCellAction}
-            useAggregateAlias={false}
-            referrer={referrer}
-          />
-        </GuideAnchor>
-      </Fragment>
-    );
+    const tableCommonProps: Omit<
+      TableRenderProps,
+      'isLoading' | 'pageLinks' | 'tableData'
+    > = {
+      handleCellAction,
+      referrer,
+      eventView,
+      organization,
+      location,
+      columnOrder,
+      titles,
+      generateLink,
+      useAggregateAlias: false,
+      header: this.renderHeader(),
+      target: 'transactions_table',
+      paginationCursorSize: 'xs',
+      onCursor: this.handleCursor,
+    };
 
     if (forceLoading) {
-      return tableRenderer({
-        isLoading: true,
-        pageLinks: null,
-        tableData: null,
-      });
+      return (
+        <TableRender {...tableCommonProps} isLoading pageLinks={null} tableData={null} />
+      );
     }
 
     return (
@@ -281,7 +370,14 @@ class _TransactionsList extends Component<Props> {
         cursor={cursor}
         referrer="api.discover.transactions-list"
       >
-        {tableRenderer}
+        {({isLoading, pageLinks, tableData}) => (
+          <TableRender
+            {...tableCommonProps}
+            isLoading={isLoading}
+            pageLinks={pageLinks}
+            tableData={tableData}
+          />
+        )}
       </DiscoverQuery>
     );
   }
@@ -309,31 +405,25 @@ class _TransactionsList extends Component<Props> {
         limit={5}
       >
         {({isLoading, trendsData, pageLinks}) => (
-          <Fragment>
-            <Header>
-              {this.renderHeader()}
-              <StyledPagination
-                pageLinks={pageLinks}
-                onCursor={this.handleCursor}
-                size="sm"
-              />
-            </Header>
-            <TransactionsTable
-              eventView={sortedEventView}
-              organization={organization}
-              location={location}
-              isLoading={isLoading}
-              tableData={trendsData}
-              titles={['transaction', 'percentage', 'difference']}
-              columnOrder={decodeColumnOrder([
-                {field: 'transaction'},
-                {field: 'trend_percentage()'},
-                {field: 'trend_difference()'},
-              ])}
-              generateLink={generateLink}
-              useAggregateAlias
-            />
-          </Fragment>
+          <TableRender
+            organization={organization}
+            eventView={sortedEventView}
+            location={location}
+            isLoading={isLoading}
+            tableData={trendsData}
+            pageLinks={pageLinks}
+            onCursor={this.handleCursor}
+            paginationCursorSize="sm"
+            header={this.renderHeader()}
+            titles={['transaction', 'percentage', 'difference']}
+            columnOrder={decodeColumnOrder([
+              {field: 'transaction'},
+              {field: 'trend_percentage()'},
+              {field: 'trend_difference()'},
+            ])}
+            generateLink={generateLink}
+            useAggregateAlias
+          />
         )}
       </TrendsEventsDiscoverQuery>
     );

+ 1 - 0
static/app/types/organization.tsx

@@ -50,6 +50,7 @@ export interface Organization extends OrganizationSummary {
   eventsMemberAdmin: boolean;
   experiments: Partial<OrgExperiments>;
   isDefault: boolean;
+  isDynamicallySampled: boolean;
   onboardingTasks: OnboardingTaskStatus[];
   openMembership: boolean;
   orgRoleList: OrgRole[];

+ 10 - 0
static/app/utils/analytics/dynamicSamplingAnalyticsEvents.tsx

@@ -9,6 +9,12 @@ export type DynamicSamplingEventParameters = {
     id: DynamicSamplingBiasType;
     project_id: string;
   };
+  'dynamic_sampling_transaction_summary.baseline': {
+    query: string;
+  };
+  'dynamic_sampling_transaction_summary.no_samples': {
+    query: string;
+  };
 };
 
 type DynamicSamplingAnalyticsKey = keyof DynamicSamplingEventParameters;
@@ -16,4 +22,8 @@ type DynamicSamplingAnalyticsKey = keyof DynamicSamplingEventParameters;
 export const dynamicSamplingEventMap: Record<DynamicSamplingAnalyticsKey, string> = {
   'dynamic_sampling_settings.priority_disabled': 'Disabled dynamic sampling priority',
   'dynamic_sampling_settings.priority_enabled': 'Enabled dynamic sampling priority',
+  'dynamic_sampling_transaction_summary.baseline':
+    'Dynamic Sampling: Transaction overview baseline',
+  'dynamic_sampling_transaction_summary.no_samples':
+    'Dynamic Sampling: Transaction without samples',
 };

+ 41 - 36
static/app/views/performance/transactionSummary/transactionOverview/content.tsx

@@ -67,6 +67,7 @@ import {
 } from '../utils';
 
 import TransactionSummaryCharts from './charts';
+import {PerformanceAtScaleContextProvider} from './performanceAtScaleContext';
 import RelatedIssues from './relatedIssues';
 import SidebarCharts from './sidebarCharts';
 import StatusBreakdown from './statusBreakdown';
@@ -105,6 +106,7 @@ function SummaryContent({
 }: Props) {
   const routes = useRoutes();
   const mepDataContext = useMEPDataContext();
+
   function handleSearch(query: string) {
     const queryParams = normalizeDateTimeParams({
       ...(location.query || {}),
@@ -360,42 +362,45 @@ function SummaryContent({
             )}
           />
         </FilterActions>
-        <TransactionSummaryCharts
-          organization={organization}
-          location={location}
-          eventView={eventView}
-          totalValue={totalCount}
-          currentFilter={spanOperationBreakdownFilter}
-          withoutZerofill={hasPerformanceChartInterpolation}
-        />
-        <TransactionsList
-          location={location}
-          organization={organization}
-          eventView={transactionsListEventView}
-          {...openAllEventsProps}
-          showTransactions={
-            decodeScalar(
-              location.query.showTransactions,
-              TransactionFilterOptions.SLOW
-            ) as TransactionFilterOptions
-          }
-          breakdown={decodeFilterFromLocation(location)}
-          titles={transactionsListTitles}
-          handleDropdownChange={handleTransactionsListSortChange}
-          generateLink={{
-            id: generateTransactionLink(transactionName),
-            trace: generateTraceLink(eventView.normalizeDateSelection(location)),
-            replayId: generateReplayLink(routes),
-            'profile.id': generateProfileLink(),
-          }}
-          handleCellAction={handleCellAction}
-          {...getTransactionsListSort(location, {
-            p95: totalValues?.['p95()'] ?? 0,
-            spanOperationBreakdownFilter,
-          })}
-          forceLoading={isLoading}
-          referrer="performance.transactions_summary"
-        />
+        <PerformanceAtScaleContextProvider>
+          <TransactionSummaryCharts
+            organization={organization}
+            location={location}
+            eventView={eventView}
+            totalValue={totalCount}
+            currentFilter={spanOperationBreakdownFilter}
+            withoutZerofill={hasPerformanceChartInterpolation}
+          />
+          <TransactionsList
+            location={location}
+            organization={organization}
+            eventView={transactionsListEventView}
+            {...openAllEventsProps}
+            showTransactions={
+              decodeScalar(
+                location.query.showTransactions,
+                TransactionFilterOptions.SLOW
+              ) as TransactionFilterOptions
+            }
+            breakdown={decodeFilterFromLocation(location)}
+            titles={transactionsListTitles}
+            handleDropdownChange={handleTransactionsListSortChange}
+            generateLink={{
+              id: generateTransactionLink(transactionName),
+              trace: generateTraceLink(eventView.normalizeDateSelection(location)),
+              replayId: generateReplayLink(routes),
+              'profile.id': generateProfileLink(),
+            }}
+            handleCellAction={handleCellAction}
+            {...getTransactionsListSort(location, {
+              p95: totalValues?.['p95()'] ?? 0,
+              spanOperationBreakdownFilter,
+            })}
+            forceLoading={isLoading}
+            referrer="performance.transactions_summary"
+          />
+        </PerformanceAtScaleContextProvider>
+
         <SuspectSpans
           location={location}
           organization={organization}

+ 20 - 0
static/app/views/performance/transactionSummary/transactionOverview/durationChart/content.tsx

@@ -1,3 +1,4 @@
+import {useContext, useEffect} from 'react';
 import {InjectedRouter} from 'react-router';
 import {Theme} from '@emotion/react';
 import {Query} from 'history';
@@ -19,6 +20,7 @@ import {
 } from 'sentry/utils/discover/charts';
 import {aggregateOutputType} from 'sentry/utils/discover/fields';
 import getDynamicText from 'sentry/utils/getDynamicText';
+import {PerformanceAtScaleContext} from 'sentry/views/performance/transactionSummary/transactionOverview/performanceAtScaleContext';
 
 type Props = {
   errored: boolean;
@@ -53,6 +55,24 @@ function Content({
   router,
   onLegendSelectChanged,
 }: Props) {
+  const performanceAtScaleContext = useContext(PerformanceAtScaleContext);
+  const isSeriesDataEmpty = data?.every(values => {
+    return values.data.every(value => !value.value);
+  });
+
+  useEffect(() => {
+    if (!performanceAtScaleContext || isSeriesDataEmpty === undefined) {
+      return;
+    }
+
+    if (loading || reloading) {
+      performanceAtScaleContext.setMetricsSeriesDataEmpty(undefined);
+      return;
+    }
+
+    performanceAtScaleContext.setMetricsSeriesDataEmpty(isSeriesDataEmpty);
+  }, [loading, reloading, isSeriesDataEmpty, performanceAtScaleContext]);
+
   if (errored) {
     return (
       <ErrorPanel>

+ 98 - 0
static/app/views/performance/transactionSummary/transactionOverview/performanceAtScaleContext.tsx

@@ -0,0 +1,98 @@
+import {createContext, useEffect, useState} from 'react';
+
+import {trackAnalytics} from 'sentry/utils/analytics';
+import {useMEPDataContext} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext';
+import useOrganization from 'sentry/utils/useOrganization';
+
+type TransactionListTableData =
+  | {
+      empty: boolean;
+      query: string;
+    }
+  | undefined;
+
+export type PerformanceAtScaleContextProps = {
+  metricsSeriesDataEmpty: boolean | undefined;
+  setMetricsSeriesDataEmpty: (data: boolean | undefined) => void;
+  setTransactionListTableData: (data: TransactionListTableData) => void;
+  transactionListTableData: TransactionListTableData;
+};
+
+export const PerformanceAtScaleContext = createContext<
+  PerformanceAtScaleContextProps | undefined
+>(undefined);
+
+type ProviderProps = {
+  children: React.ReactNode;
+};
+
+export function PerformanceAtScaleContextProvider({children}: ProviderProps) {
+  const [metricsSeriesDataEmpty, setMetricsSeriesDataEmpty] = useState<
+    boolean | undefined
+  >(false);
+
+  const [transactionListTableData, setTransactionListTableData] =
+    useState<TransactionListTableData>(undefined);
+
+  const mepContext = useMEPDataContext();
+  const organization = useOrganization();
+
+  const query = transactionListTableData?.query ?? '';
+  const transactionListTableEmpty = transactionListTableData?.empty;
+  const isMetricsData = mepContext?.isMetricsData ?? false;
+
+  useEffect(() => {
+    // We only want to collect analytics events if we have metrics data
+    // and if everything is dynamically samples
+    if (!isMetricsData || !organization.isDynamicallySampled) {
+      return;
+    }
+
+    // if the chart or the transaction list table are undefined,
+    // some loading is probably still hapenning
+    if (metricsSeriesDataEmpty === undefined || transactionListTableEmpty === undefined) {
+      return;
+    }
+
+    // metricsSeriesDataEmpty comes from the series data response (events-stats request)
+    // and if we don't have metrics data, we don't want to fire analytics events
+    if (metricsSeriesDataEmpty) {
+      return;
+    }
+
+    // If the transaction list table is empty, we want to fire the no_samples event
+    // as it means that there is a gap in the dynamic sampling and we want to track that
+    if (transactionListTableEmpty) {
+      trackAnalytics('dynamic_sampling_transaction_summary.no_samples', {
+        organization,
+        query,
+      });
+    }
+
+    // If the transaction list table is not empty and there are metrics, it means that
+    // dynamic sampling is working properly and there is no gap
+    trackAnalytics('dynamic_sampling_transaction_summary.baseline', {
+      organization,
+      query,
+    });
+  }, [
+    metricsSeriesDataEmpty,
+    isMetricsData,
+    query,
+    transactionListTableEmpty,
+    organization,
+  ]);
+
+  return (
+    <PerformanceAtScaleContext.Provider
+      value={{
+        metricsSeriesDataEmpty,
+        setMetricsSeriesDataEmpty,
+        setTransactionListTableData,
+        transactionListTableData,
+      }}
+    >
+      {children}
+    </PerformanceAtScaleContext.Provider>
+  );
+}