Browse Source

feat(starfish): Add top events to the WSV (#57592)

- This updates the web service view to display top-5 graphs based on the
table below
- Adds checkboxes on each row so the corresponding line can be removed
from the graphs
William Mak 1 year ago
parent
commit
a88c7377be

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

@@ -381,7 +381,7 @@ function getPayload<T, P>(props: Props<T, P>) {
     ? getRequestPayload(props)
     : eventView.getEventsAPIPayload(location, forceAppendRawQueryString);
 
-  if (cursor) {
+  if (cursor !== undefined) {
     payload.cursor = cursor;
   }
   if (limit) {

+ 104 - 24
static/app/views/starfish/views/webServiceView/endpointList.tsx

@@ -1,11 +1,13 @@
 import {Fragment, useEffect, useState} from 'react';
 import styled from '@emotion/styled';
 import {Location, LocationDescriptorObject} from 'history';
+import isEqual from 'lodash/isEqual';
 import omit from 'lodash/omit';
 import * as qs from 'query-string';
 
 import GuideAnchor from 'sentry/components/assistant/guideAnchor';
 import {Button} from 'sentry/components/button';
+import Checkbox from 'sentry/components/checkbox';
 import GridEditable, {
   COL_WIDTH_UNDEFINED,
   GridColumn,
@@ -29,6 +31,7 @@ import DiscoverQuery, {
 import EventView, {isFieldSortable, MetaType} from 'sentry/utils/discover/eventView';
 import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
 import {getAggregateAlias, RateUnits} from 'sentry/utils/discover/fields';
+import TopResultsIndicator from 'sentry/views/discover/table/topResultsIndicator';
 import {TableColumn} from 'sentry/views/discover/table/types';
 import {ThroughputCell} from 'sentry/views/starfish/components/tableCells/throughputCell';
 import {TimeSpentCell} from 'sentry/views/starfish/components/tableCells/timeSpentCell';
@@ -47,23 +50,40 @@ const COLUMN_TITLES = [
 
 type Props = {
   eventView: EventView;
+  inactiveTransactions: string[];
   location: Location;
   organization: Organization;
   setError: (msg: string | undefined) => void;
+  setInactiveTransactions: (endpoints: string[]) => void;
+  setTransactionsList: (endpoints: string[]) => void;
+  transactionsList: string[];
 };
 
 export type TableColumnHeader = GridColumnHeader<keyof TableDataRow> & {
   column?: TableColumn<keyof TableDataRow>['column']; // TODO - remove this once gridEditable is properly typed
 };
 
-function QueryIssueCounts({eventView, tableData, children}) {
+function QueryIssueCounts({
+  setTransactionsList,
+  transactionsList,
+  eventView,
+  tableData,
+  children,
+}) {
   const transactions: Map<string, string> = new Map();
+  const newTransactionsList: string[] = [];
   transactions.set('is:unresolved', '');
   if (tableData) {
     tableData.data.forEach(row => {
       transactions.set(`transaction:${row.transaction} is:unresolved`, row.transaction);
+      newTransactionsList.push(row.transaction);
     });
   }
+  useEffect(() => {
+    if (!isEqual(transactionsList, newTransactionsList)) {
+      setTransactionsList(newTransactionsList);
+    }
+  });
   const {data, isLoading} = useIssueCounts(eventView, Array.from(transactions.keys()));
   const result: Map<string, IssueCounts> = new Map();
   for (const [query, count] of data ? Object.entries(data) : []) {
@@ -75,7 +95,16 @@ function QueryIssueCounts({eventView, tableData, children}) {
   return children({issueCounts: result, isIssueLoading: isLoading});
 }
 
-function EndpointList({eventView, location, organization, setError}: Props) {
+function EndpointList({
+  eventView,
+  location,
+  organization,
+  setError,
+  setTransactionsList,
+  transactionsList,
+  inactiveTransactions,
+  setInactiveTransactions,
+}: Props) {
   const [widths, setWidths] = useState<number[]>([]);
   const [_eventView, setEventView] = useState<EventView>(eventView);
   const overallEventView = _eventView.clone();
@@ -109,11 +138,37 @@ function EndpointList({eventView, location, organization, setError}: Props) {
     const field = String(column.key);
     const fieldRenderer = getFieldRenderer(field, tableMeta, false);
     const rendered = fieldRenderer(dataRow, {organization, location});
+    const index = tableData.data.indexOf(dataRow);
+
+    function toggleCheckbox(transaction: string) {
+      let newInactiveTransactions = inactiveTransactions.slice();
+      if (inactiveTransactions.indexOf(transaction) === -1) {
+        newInactiveTransactions.push(transaction);
+      } else {
+        newInactiveTransactions = inactiveTransactions.filter(
+          name => name !== transaction
+        );
+      }
+      setInactiveTransactions(newInactiveTransactions);
+    }
 
     if (field === 'transaction') {
       const method = dataRow['http.method'];
       if (method === undefined && dataRow.transaction === 'Overall') {
-        return <span>{dataRow.transaction}</span>;
+        return (
+          <span>
+            <TopResultsIndicator count={tableData.data.length + 2} index={6} />
+            <TransactionColumn>
+              <TransactionCheckbox
+                checked={
+                  inactiveTransactions.indexOf(dataRow.transaction as string) === -1
+                }
+                onChange={() => toggleCheckbox(dataRow.transaction as string)}
+              />
+              <TransactionText>{dataRow.transaction}</TransactionText>
+            </TransactionColumn>
+          </span>
+        );
       }
       const endpointName =
         method && !dataRow.transaction.toString().startsWith(method.toString())
@@ -121,27 +176,34 @@ function EndpointList({eventView, location, organization, setError}: Props) {
           : dataRow.transaction;
 
       return (
-        <Link
-          to={`/organizations/${
-            organization.slug
-          }/starfish/endpoint-overview/?${qs.stringify({
-            endpoint: dataRow.transaction,
-            'http.method': dataRow['http.method'],
-            statsPeriod: eventView.statsPeriod,
-            project: eventView.project,
-            start: eventView.start,
-            end: eventView.end,
-          })}`}
-          style={{display: `block`, width: `100%`}}
-          onClick={() => {
-            trackAnalytics('starfish.web_service_view.endpoint_list.endpoint.clicked', {
-              organization,
+        <TransactionColumn>
+          <TransactionCheckbox
+            checked={inactiveTransactions.indexOf(dataRow.transaction as string) === -1}
+            onChange={() => toggleCheckbox(dataRow.transaction as string)}
+          />
+          <Link
+            to={`/organizations/${
+              organization.slug
+            }/starfish/endpoint-overview/?${qs.stringify({
               endpoint: dataRow.transaction,
-            });
-          }}
-        >
-          {endpointName}
-        </Link>
+              'http.method': dataRow['http.method'],
+              statsPeriod: eventView.statsPeriod,
+              project: eventView.project,
+              start: eventView.start,
+              end: eventView.end,
+            })}`}
+            style={{display: `block`, width: `100%`}}
+            onClick={() => {
+              trackAnalytics('starfish.web_service_view.endpoint_list.endpoint.clicked', {
+                organization,
+                endpoint: dataRow.transaction,
+              });
+            }}
+          >
+            <TopResultsIndicator count={tableData.data.length + 2} index={index} />
+            <TransactionText>{endpointName}</TransactionText>
+          </Link>
+        </TransactionColumn>
       );
     }
 
@@ -381,7 +443,12 @@ function EndpointList({eventView, location, organization, setError}: Props) {
           >
             {({pageLinks, isLoading, tableData}) => (
               <Fragment>
-                <QueryIssueCounts eventView={eventView} tableData={tableData}>
+                <QueryIssueCounts
+                  setTransactionsList={setTransactionsList}
+                  transactionsList={transactionsList}
+                  eventView={eventView}
+                  tableData={tableData}
+                >
                   {({issueCounts, isIssueLoading}) => (
                     <GridEditable
                       isLoading={isLoading && isIssueLoading}
@@ -418,6 +485,19 @@ const StyledSearchBar = styled(BaseSearchBar)`
   margin-bottom: ${space(2)};
 `;
 
+const TransactionColumn = styled('div')`
+  display: flex;
+`;
+
+const TransactionText = styled('div')`
+  margin-top: ${space(0.5)};
+`;
+
+const TransactionCheckbox = styled(Checkbox)`
+  top: ${space(0.5)};
+  margin-right: ${space(1)};
+`;
+
 const IssueButton = styled(Button)`
   width: 100%;
   margin-left: auto;

+ 1 - 0
static/app/views/starfish/views/webServiceView/spanGroupBar.tsx

@@ -53,6 +53,7 @@ export function SpanGroupBar(props: SpanGroupBarProps) {
         selection
       ),
       referrer: 'api.starfish-web-service.span-category-breakdown',
+      cursor: '',
       limit: 4,
     }
   );

+ 70 - 17
static/app/views/starfish/views/webServiceView/starfishView.tsx

@@ -15,7 +15,7 @@ import {formatRate} from 'sentry/utils/formatters';
 import {usePageError} from 'sentry/utils/performance/contexts/pageError';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
 import usePageFilters from 'sentry/utils/usePageFilters';
-import {AVG_COLOR, THROUGHPUT_COLOR} from 'sentry/views/starfish/colours';
+import {THROUGHPUT_COLOR} from 'sentry/views/starfish/colours';
 import Chart, {useSynchronizeCharts} from 'sentry/views/starfish/components/chart';
 import MiniChartPanel from 'sentry/views/starfish/components/miniChartPanel';
 import {STARFISH_CHART_INTERVAL_FIDELITY} from 'sentry/views/starfish/utils/constants';
@@ -30,6 +30,8 @@ export function StarfishView(props: BaseStarfishViewProps) {
   const pageFilter = usePageFilters();
   const {selection} = pageFilter;
   const [activeSpanGroup, setActiveSpanGroup] = useState<string | null>(null);
+  const [transactionsList, setTransactionsList] = useState<string[]>([]);
+  const [inactiveTransactions, setInactiveTransactions] = useState<string[]>([]);
 
   function useRenderCharts() {
     let query = new MutableSearch([
@@ -55,8 +57,29 @@ export function StarfishView(props: BaseStarfishViewProps) {
         }`,
       ];
     }
+    const transactionsFilter = `"${transactionsList.join('","')}"`;
+    const queryString = query.formatString() + ` transaction:[${transactionsFilter}]`;
 
     const {isLoading: loading, data: results} = useEventsStatsQuery({
+      eventView: EventView.fromNewQueryWithPageFilters(
+        {
+          name: '',
+          fields: ['transaction', yAxis[0]],
+          yAxis,
+          query: queryString,
+          dataset,
+          version: 2,
+          topEvents: '5',
+          interval: getInterval(selection.datetime, STARFISH_CHART_INTERVAL_FIDELITY),
+        },
+        selection
+      ),
+      enabled: transactionsList.length > 0,
+      excludeOther: true,
+      referrer: 'api.starfish-web-service.homepage-charts',
+      initialData: {},
+    });
+    const {isLoading: totalLoading, data: totalResults} = useEventsStatsQuery({
       eventView: EventView.fromNewQueryWithPageFilters(
         {
           name: '',
@@ -74,19 +97,42 @@ export function StarfishView(props: BaseStarfishViewProps) {
       initialData: {},
     });
     useSynchronizeCharts([!loading]);
-    if (loading || !results || !results[yAxis[0]]) {
+    if (loading || totalLoading || !totalResults || !results) {
       return <ChartPlaceholder />;
     }
-    const seriesByName: {[category: string]: Series} = {};
-    Object.keys(results).forEach(key => {
-      const seriesData: EventsStats = results?.[key];
-      seriesByName[key] = {
-        seriesName: key,
-        data:
-          seriesData?.data.map(datum => {
-            return {name: datum[0] * 1000, value: datum[1][0].count} as SeriesDataUnit;
-          }) ?? [],
-      };
+    const seriesByName: {[category: string]: Series[]} = {};
+    yAxis.forEach(axis => {
+      seriesByName[axis] = [];
+    });
+    if (!inactiveTransactions.includes('Overall')) {
+      Object.keys(totalResults).forEach(key => {
+        seriesByName[key].push({
+          seriesName: 'Overall',
+          color: CHART_PALETTE[transactionsList.length][5],
+          data:
+            totalResults[key]?.data.map(datum => {
+              return {name: datum[0] * 1000, value: datum[1][0].count} as SeriesDataUnit;
+            }) ?? [],
+        });
+      });
+    }
+    transactionsList.forEach((transaction, index) => {
+      const seriesData: EventsStats = results?.[transaction];
+      if (!inactiveTransactions.includes(transaction)) {
+        yAxis.forEach(key => {
+          seriesByName[key].push({
+            seriesName: transaction,
+            color: CHART_PALETTE[transactionsList.length][index],
+            data:
+              seriesData?.[key]?.data.map(datum => {
+                return {
+                  name: datum[0] * 1000,
+                  value: datum[1][0].count,
+                } as SeriesDataUnit;
+              }) ?? [],
+          });
+        });
+      }
     });
 
     return (
@@ -95,7 +141,7 @@ export function StarfishView(props: BaseStarfishViewProps) {
           <MiniChartPanel title={titles[2]}>
             <Chart
               height={142}
-              data={[seriesByName[yAxis[2]]]}
+              data={seriesByName[yAxis[2]]}
               loading={loading}
               utc={false}
               grid={{
@@ -104,9 +150,9 @@ export function StarfishView(props: BaseStarfishViewProps) {
                 top: '8px',
                 bottom: '0',
               }}
+              aggregateOutputFormat="duration"
               definedAxisTicks={2}
               isLineChart
-              chartColors={[AVG_COLOR]}
               tooltipFormatterOptions={{
                 valueFormatter: value =>
                   tooltipFormatterUsingAggregateOutputType(value, 'duration'),
@@ -118,7 +164,7 @@ export function StarfishView(props: BaseStarfishViewProps) {
           <MiniChartPanel title={titles[0]}>
             <Chart
               height={142}
-              data={[seriesByName[yAxis[0]]]}
+              data={seriesByName[yAxis[0]]}
               loading={loading}
               utc={false}
               grid={{
@@ -144,7 +190,7 @@ export function StarfishView(props: BaseStarfishViewProps) {
           <MiniChartPanel title={titles[1]}>
             <Chart
               height={142}
-              data={[seriesByName[yAxis[1]]]}
+              data={seriesByName[yAxis[1]]}
               loading={loading}
               utc={false}
               grid={{
@@ -172,7 +218,14 @@ export function StarfishView(props: BaseStarfishViewProps) {
         </ChartsContainer>
       </StyledRow>
 
-      <EndpointList {...props} setError={usePageError().setPageError} />
+      <EndpointList
+        {...props}
+        transactionsList={transactionsList}
+        setTransactionsList={setTransactionsList}
+        setError={usePageError().setPageError}
+        inactiveTransactions={inactiveTransactions}
+        setInactiveTransactions={setInactiveTransactions}
+      />
     </div>
   );
 }