Browse Source

feat(perf): Aggregate column selector in tags views (#36879)

* feat(perf): Aggregate column selector in tags views

- add "X-Axis" dropdown to the tag view page. The options are "Duration"
  and "LCP"
- pass `aggregateColumn` (AKA the x-axis) down as a prop from the page
  component through the hierarchy

Closes PERF-1616
George Gritsouk 2 years ago
parent
commit
e4fab570fa

+ 5 - 0
static/app/utils/analytics/performanceAnalyticsEvents.tsx

@@ -71,6 +71,9 @@ export type PerformanceEventParameters = {
   'performance_views.spans.change_sort': {
     sort_column?: string;
   };
+  'performance_views.tags.change_aggregate_column': {
+    value: string;
+  };
   'performance_views.tags.change_tag': {
     from_tag: string;
     is_other_tag: boolean;
@@ -158,6 +161,8 @@ export const performanceEventMap: Record<PerformanceEventKey, string | null> = {
     'Performance Views: Transaction Summary status breakdown option clicked',
   'performance_views.all_events.open_in_discover':
     'Performance Views: All Events page open in Discover button clicked',
+  'performance_views.tags.change_aggregate_column':
+    'Performance Views: Tags page changed aggregate column',
   'performance_views.tags.change_tag':
     'Performance Views: Tags Page changed selected tag',
   'performance_views.tags.jump_to_release':

+ 9 - 0
static/app/views/performance/transactionSummary/transactionTags/constants.tsx

@@ -0,0 +1,9 @@
+import {t} from 'sentry/locale';
+import {SelectValue} from 'sentry/types';
+
+import {XAxisOption} from './types';
+
+export const X_AXIS_SELECT_OPTIONS: SelectValue<XAxisOption>[] = [
+  {label: t('LCP'), value: XAxisOption.LCP},
+  {label: t('Duration'), value: XAxisOption.DURATION},
+];

+ 57 - 9
static/app/views/performance/transactionSummary/transactionTags/content.tsx

@@ -7,6 +7,7 @@ import {SectionHeading} from 'sentry/components/charts/styles';
 import DatePageFilter from 'sentry/components/datePageFilter';
 import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
 import SearchBar from 'sentry/components/events/searchBar';
+import CompactSelect from 'sentry/components/forms/compactSelect';
 import * as Layout from 'sentry/components/layouts/thirds';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
@@ -27,6 +28,7 @@ import {SidebarSpacer} from 'sentry/views/performance/transactionSummary/utils';
 import {SpanOperationBreakdownFilter} from '../filter';
 import {getTransactionField} from '../transactionOverview/tagExplorer';
 
+import {X_AXIS_SELECT_OPTIONS} from './constants';
 import TagsDisplay, {TAG_PAGE_TABLE_CURSOR} from './tagsDisplay';
 import {decodeSelectedTagKey} from './utils';
 
@@ -43,10 +45,8 @@ type TagOption = string;
 const TagsPageContent = (props: Props) => {
   const {eventView, location, organization, projects} = props;
 
-  const aggregateColumn = getTransactionField(
-    SpanOperationBreakdownFilter.None,
-    projects,
-    eventView
+  const [aggregateColumn, setAggregateColumn] = useState(
+    getTransactionField(SpanOperationBreakdownFilter.None, projects, eventView)
   );
 
   return (
@@ -61,7 +61,15 @@ const TagsPageContent = (props: Props) => {
         allTagKeys
       >
         {({isLoading, tableData}) => {
-          return <InnerContent {...props} isLoading={isLoading} tableData={tableData} />;
+          return (
+            <InnerContent
+              {...props}
+              isLoading={isLoading}
+              tableData={tableData}
+              aggregateColumn={aggregateColumn}
+              onChangeAggregateColumn={setAggregateColumn}
+            />
+          );
         }}
       </SegmentExplorerQuery>
     </Layout.Main>
@@ -83,9 +91,22 @@ function getTagKeyOptions(tableData: TableData) {
 }
 
 const InnerContent = (
-  props: Props & {tableData: TableData | null; isLoading?: boolean}
+  props: Props & {
+    aggregateColumn: string;
+    onChangeAggregateColumn: (aggregateColumn: string) => void;
+    tableData: TableData | null;
+    isLoading?: boolean;
+  }
 ) => {
-  const {eventView: _eventView, location, organization, tableData, isLoading} = props;
+  const {
+    eventView: _eventView,
+    location,
+    organization,
+    tableData,
+    aggregateColumn,
+    onChangeAggregateColumn,
+    isLoading,
+  } = props;
   const eventView = _eventView.clone();
 
   const tagOptions = tableData ? getTagKeyOptions(tableData) : null;
@@ -175,13 +196,28 @@ const InnerContent = (
             <EnvironmentPageFilter />
             <DatePageFilter alignDropdown="left" />
           </PageFilterBar>
-          <SearchBar
+          <StyledSearchBar
             organization={organization}
             projectIds={eventView.project}
             query={query}
             fields={eventView.fields}
             onSearch={handleSearch}
           />
+          <CompactSelect
+            value={aggregateColumn}
+            options={X_AXIS_SELECT_OPTIONS}
+            onChange={opt => {
+              trackAdvancedAnalyticsEvent(
+                'performance_views.tags.change_aggregate_column',
+                {
+                  organization,
+                  value: opt.value,
+                }
+              );
+              onChangeAggregateColumn(opt.value);
+            }}
+            triggerProps={{prefix: t('X-Axis')}}
+          />
         </FilterActions>
         <TagsDisplay {...props} tagKey={tagSelected} />
       </StyledMain>
@@ -310,13 +346,25 @@ const StyledMain = styled('div')`
   max-width: 100%;
 `;
 
+const StyledSearchBar = styled(SearchBar)`
+  @media (min-width: ${p => p.theme.breakpoints.small}) {
+    order: 1;
+    grid-column: 1/6;
+  }
+
+  @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
+    order: initial;
+    grid-column: auto;
+  }
+`;
+
 const FilterActions = styled('div')`
   display: grid;
   gap: ${space(2)};
   margin-bottom: ${space(2)};
 
   @media (min-width: ${p => p.theme.breakpoints.small}) {
-    grid-template-columns: auto 1fr;
+    grid-template-columns: auto 1fr auto;
   }
 `;
 

+ 2 - 10
static/app/views/performance/transactionSummary/transactionTags/tagsDisplay.tsx

@@ -10,14 +10,12 @@ import SegmentExplorerQuery from 'sentry/utils/performance/segmentExplorer/segme
 import TagKeyHistogramQuery from 'sentry/utils/performance/segmentExplorer/tagKeyHistogramQuery';
 import {decodeScalar} from 'sentry/utils/queryString';
 
-import {SpanOperationBreakdownFilter} from '../filter';
-import {getTransactionField} from '../transactionOverview/tagExplorer';
-
 import TagsHeatMap from './tagsHeatMap';
 import {TagValueTable} from './tagValueTable';
 import {getTagSortForTagsPage} from './utils';
 
 type Props = {
+  aggregateColumn: string;
   eventView: EventView;
   location: Location;
   organization: Organization;
@@ -99,15 +97,9 @@ export const TAGS_TABLE_COLUMN_ORDER: TagsTableColumn[] = [
 ];
 
 const TagsDisplay = (props: Props) => {
-  const {eventView: _eventView, location, organization, projects, tagKey} = props;
+  const {eventView: _eventView, location, organization, aggregateColumn, tagKey} = props;
   const eventView = _eventView.clone();
 
-  const aggregateColumn = getTransactionField(
-    SpanOperationBreakdownFilter.None,
-    projects,
-    eventView
-  );
-
   const handleCursor: CursorHandler = (cursor, pathname, query) =>
     browserHistory.push({
       pathname,

+ 4 - 0
static/app/views/performance/transactionSummary/transactionTags/types.tsx

@@ -0,0 +1,4 @@
+export enum XAxisOption {
+  DURATION = 'transaction.duration',
+  LCP = 'measurements.lcp',
+}

+ 46 - 0
tests/js/spec/views/performance/transactionTags/index.spec.tsx

@@ -1,4 +1,5 @@
 import {browserHistory} from 'react-router';
+import selectEvent from 'react-select-event';
 
 import {initializeOrg} from 'sentry-test/initializeOrg';
 import {act, render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
@@ -324,4 +325,49 @@ describe('Performance > Transaction Tags', function () {
       },
     });
   });
+
+  it('changes the aggregate column when a new x-axis is selected', async function () {
+    const {organization, router, routerContext} = initializeData({
+      query: {tagKey: 'os'},
+    });
+
+    render(<TransactionTags location={router.location} />, {
+      context: routerContext,
+      organization,
+    });
+
+    await waitFor(() => {
+      expect(screen.getByRole('table')).toBeInTheDocument();
+    });
+
+    expect(histogramMock).toHaveBeenCalledTimes(1);
+
+    expect(histogramMock).toHaveBeenNthCalledWith(
+      1,
+      expect.anything(),
+      expect.objectContaining({
+        query: expect.objectContaining({
+          aggregateColumn: 'transaction.duration',
+        }),
+      })
+    );
+
+    await selectEvent.select(screen.getByText('X-Axis'), 'LCP');
+
+    await waitFor(() => {
+      expect(screen.getByRole('table')).toBeInTheDocument();
+    });
+
+    expect(histogramMock).toHaveBeenCalledTimes(2);
+
+    expect(histogramMock).toHaveBeenNthCalledWith(
+      2,
+      expect.anything(),
+      expect.objectContaining({
+        query: expect.objectContaining({
+          aggregateColumn: 'measurements.lcp',
+        }),
+      })
+    );
+  });
 });