Browse Source

feat(starfish): Updates spans explorer to accept query parameters for filtering spans (#49191)

Updates spans explorer to accept query parameters for filtering spans
results.
Removes facet bars from spans explorer. 
Updates WSV to link to spans explorer. 
Removes usage of Cluster type since span explorer filter state comes
from query parameters now.
edwardgou-sentry 1 year ago
parent
commit
e1eda28c47

+ 30 - 11
static/app/views/starfish/components/breakdownBar.tsx

@@ -1,13 +1,17 @@
 import {Fragment, useState} from 'react';
+import {Link} from 'react-router';
 import isPropValid from '@emotion/is-prop-valid';
 import styled from '@emotion/styled';
 import {motion} from 'framer-motion';
+import * as qs from 'query-string';
 
 import {Tooltip} from 'sentry/components/tooltip';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import {percent} from 'sentry/utils';
+import {getUtcDateString} from 'sentry/utils/dates';
 import {useQuery} from 'sentry/utils/queryClient';
+import usePageFilters from 'sentry/utils/usePageFilters';
 import {
   getOtherDomainsActionsAndOpTimeseries,
   getTopDomainsActionsAndOp,
@@ -43,6 +47,7 @@ export function getSegmentLabel(span_operation, action, domain) {
 }
 
 function FacetBreakdownBar({title, transaction: maybeTransaction}: Props) {
+  const {selection} = usePageFilters();
   const [hoveredValue, setHoveredValue] = useState<ModuleSegment | null>(null);
 
   const transaction = maybeTransaction ?? '';
@@ -219,20 +224,34 @@ function FacetBreakdownBar({title, transaction: maybeTransaction}: Props) {
               segment.action,
               segment.domain
             );
+            const {start, end, utc, period} = selection.datetime;
+            const spansLinkQueryParams =
+              start && end
+                ? {start: getUtcDateString(start), end: getUtcDateString(end), utc}
+                : {statsPeriod: period};
+            ['span_operation', 'action', 'domain'].forEach(key => {
+              if (segment[key] !== undefined && segment[key] !== null) {
+                spansLinkQueryParams[key] = segment[key];
+              }
+            });
+
+            const spansLink = `/starfish/spans/?${qs.stringify(spansLinkQueryParams)}`;
 
             return (
               <li key={`segment-${label}-${index}`}>
-                <LegendRow
-                  onMouseOver={() => setHoveredValue(segment)}
-                  onMouseLeave={() => setHoveredValue(null)}
-                  onClick={() => {}}
-                >
-                  <LegendDot color={COLORS[index]} focus={focus} />
-                  <LegendText unfocus={unfocus}>
-                    {label ?? <NotApplicableLabel>{t('n/a')}</NotApplicableLabel>}
-                  </LegendText>
-                  {<LegendPercent>{`${pctLabel}%`}</LegendPercent>}
-                </LegendRow>
+                <Link to={spansLink}>
+                  <LegendRow
+                    onMouseOver={() => setHoveredValue(segment)}
+                    onMouseLeave={() => setHoveredValue(null)}
+                    onClick={() => {}}
+                  >
+                    <LegendDot color={COLORS[index]} focus={focus} />
+                    <LegendText unfocus={unfocus}>
+                      {label ?? <NotApplicableLabel>{t('n/a')}</NotApplicableLabel>}
+                    </LegendText>
+                    {<LegendPercent>{`${pctLabel}%`}</LegendPercent>}
+                  </LegendRow>
+                </Link>
               </li>
             );
           })}

+ 5 - 2
static/app/views/starfish/views/spans/index.tsx

@@ -3,6 +3,7 @@ import {RouteComponentProps} from 'react-router';
 import {Location} from 'history';
 
 import * as Layout from 'sentry/components/layouts/thirds';
+import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
 import {t} from 'sentry/locale';
 import {
   PageErrorAlert,
@@ -38,8 +39,10 @@ export default function Spans(props: Props) {
         <Layout.Body>
           <Layout.Main fullWidth>
             <PageErrorAlert />
-            <SpansView location={props.location} onSelect={setSelectedRow} />
-            <SpanDetail row={selectedRow} onClose={unsetSelectedSpanGroup} />
+            <PageFiltersContainer>
+              <SpansView location={props.location} onSelect={setSelectedRow} />
+              <SpanDetail row={selectedRow} onClose={unsetSelectedSpanGroup} />
+            </PageFiltersContainer>
           </Layout.Main>
         </Layout.Body>
       </PageErrorProvider>

+ 1 - 1
static/app/views/starfish/views/spans/queries.tsx

@@ -41,7 +41,7 @@ export const getSpanListQuery = (
     quantile(0.50)(exclusive_time) as p50,
     count() as count,
     (divide(count, ${
-      (moment(end_timestamp).unix() - moment(start_timestamp).unix()) / 60
+      (moment(end_timestamp ?? undefined).unix() - moment(start_timestamp).unix()) / 60
     }) AS epm)
     FROM spans_experimental_starfish
     WHERE greaterOrEquals(start_timestamp, '${start_timestamp}')

+ 4 - 15
static/app/views/starfish/views/spans/spanTimeCharts.tsx

@@ -16,14 +16,12 @@ import {
 import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery';
 import {zeroFillSeries} from 'sentry/views/starfish/utils/zeroFillSeries';
 
-import type {Cluster} from './clusters';
-
 type Props = {
-  clusters: Cluster[];
   descriptionFilter: string;
+  queryConditions: string[];
 };
 
-export function SpanTimeCharts({descriptionFilter, clusters}: Props) {
+export function SpanTimeCharts({descriptionFilter, queryConditions}: Props) {
   const themes = useTheme();
 
   const pageFilter = usePageFilters();
@@ -34,22 +32,15 @@ export function SpanTimeCharts({descriptionFilter, clusters}: Props) {
       : moment(pageFilter.selection.datetime.start);
   const endTime = moment(pageFilter.selection.datetime.end ?? undefined);
 
-  const lastCluster = clusters.at(-1);
-
   const {isLoading, data} = useSpansQuery({
     queryString: `${getSpanTotalTimeChartQuery(
       pageFilter.selection.datetime,
       descriptionFilter,
-      lastCluster?.grouping_column || '',
-      clusters.map(c => c.condition(c.name))
+      queryConditions
     )}&referrer=span-time-charts`,
     initialData: [],
   });
 
-  if (!lastCluster) {
-    return null;
-  }
-
   const dataByGroup = groupBy(data, 'primary_group');
 
   const throughputTimeSeries = Object.keys(dataByGroup).map(groupName => {
@@ -185,14 +176,12 @@ export function SpanTimeCharts({descriptionFilter, clusters}: Props) {
 export const getSpanTotalTimeChartQuery = (
   datetime: DateTimeObject,
   descriptionFilter: string | undefined,
-  groupingColumn: string,
   conditions: string[] = []
 ) => {
   const {start_timestamp, end_timestamp} = datetimeToClickhouseFilterTimestamps(datetime);
   const validConditions = conditions.filter(Boolean);
 
   return `SELECT
-    ${groupingColumn ? `${groupingColumn} AS primary_group,` : ''}
     count() AS throughput,
     sum(exclusive_time) AS total_time,
     quantile(0.50)(exclusive_time) AS p50,
@@ -203,7 +192,7 @@ export const getSpanTotalTimeChartQuery = (
     ${validConditions.length > 0 ? 'AND' : ''}
     ${validConditions.join(' AND ')}
     ${descriptionFilter ? `AND match(lower(description), '${descriptionFilter}')` : ''}
-    GROUP BY ${groupingColumn ? 'primary_group, ' : ''} interval
+    GROUP BY interval
     ORDER BY interval ASC
   `;
 };

+ 27 - 14
static/app/views/starfish/views/spans/spansTable.tsx

@@ -18,15 +18,13 @@ import Sparkline from 'sentry/views/starfish/components/sparkline';
 import {DataRow} from 'sentry/views/starfish/modules/databaseModule/databaseTableView';
 import {zeroFillSeries} from 'sentry/views/starfish/utils/zeroFillSeries';
 
-import type {Cluster} from './clusters';
-
 type Props = {
-  clusters: Cluster[];
   isLoading: boolean;
   location: Location;
   onSelect: (row: SpanDataRow) => void;
   onSetOrderBy: (orderBy: string) => void;
   orderBy: string;
+  queryConditions: string[];
   spansData: SpanDataRow[];
   spansTrendsData: SpanTrendDataRow[];
 };
@@ -54,7 +52,7 @@ export default function SpansTable({
   spansData,
   orderBy,
   onSetOrderBy,
-  clusters,
+  queryConditions,
   spansTrendsData,
   isLoading,
   onSelect,
@@ -96,7 +94,7 @@ export default function SpansTable({
     <GridEditable
       isLoading={isLoading}
       data={combinedSpansData}
-      columnOrder={getColumns(clusters)}
+      columnOrder={getColumns(queryConditions)}
       columnSortBy={
         orderBy ? [] : [{key: orderBy, order: 'desc'} as TableColumnSort<string>]
       }
@@ -203,17 +201,32 @@ const mapRowKeys = (row: SpanDataRow, spanOperation: string) => {
   }
 };
 
-function getColumns(clusters: Cluster[]): GridColumnOrder[] {
-  const secondCluster = clusters.at(1);
-  const description =
-    clusters.findLast(cluster => Boolean(cluster.description_label))?.description_label ||
-    'Description';
+function getDomainHeader(queryConditions: string[]) {
+  if (queryConditions.includes("span_operation = 'db'")) {
+    return 'Table';
+  }
+  if (queryConditions.includes("span_operation = 'http.client'")) {
+    return 'Host';
+  }
+  return 'Domain';
+}
+function getDescriptionHeader(queryConditions: string[]) {
+  if (queryConditions.includes("span_operation = 'db'")) {
+    return 'Query';
+  }
+  if (queryConditions.includes("span_operation = 'http.client'")) {
+    return 'URL';
+  }
+  return 'Description';
+}
+
+function getColumns(queryConditions: string[]): GridColumnOrder[] {
+  const description = getDescriptionHeader(queryConditions);
 
-  const domain =
-    clusters.findLast(cluster => Boolean(cluster.domain_label))?.domain_label || 'Domain';
+  const domain = getDomainHeader(queryConditions);
 
   const order: Array<GridColumnOrder | false> = [
-    !secondCluster && {
+    {
       key: 'span_operation',
       name: 'Operation',
       width: COL_WIDTH_UNDEFINED,
@@ -223,7 +236,7 @@ function getColumns(clusters: Cluster[]): GridColumnOrder[] {
       name: description,
       width: COL_WIDTH_UNDEFINED,
     },
-    !!secondCluster && {
+    {
       key: 'domain',
       name: domain,
       width: COL_WIDTH_UNDEFINED,

+ 23 - 116
static/app/views/starfish/views/spans/spansView.tsx

@@ -1,22 +1,17 @@
 import {Fragment, useState} from 'react';
 import styled from '@emotion/styled';
-import {useQueries, useQuery} from '@tanstack/react-query';
+import {useQuery} from '@tanstack/react-query';
 import {Location} from 'history';
-import keyBy from 'lodash/keyBy';
 import _orderBy from 'lodash/orderBy';
-import sumBy from 'lodash/sumBy';
 
 import DatePageFilter from 'sentry/components/datePageFilter';
 import SearchBar from 'sentry/components/searchBar';
-import TagDistributionMeter from 'sentry/components/tagDistributionMeter';
 import {space} from 'sentry/styles/space';
 import usePageFilters from 'sentry/utils/usePageFilters';
-import {HostDetails} from 'sentry/views/starfish/modules/APIModule/hostDetails';
 import {HOST} from 'sentry/views/starfish/utils/constants';
 import {SpanTimeCharts} from 'sentry/views/starfish/views/spans/spanTimeCharts';
 
-import {CLUSTERS} from './clusters';
-import {getSpanListQuery, getSpansTrendsQuery, getTimeSpentQuery} from './queries';
+import {getSpanListQuery, getSpansTrendsQuery} from './queries';
 import type {SpanDataRow, SpanTrendDataRow} from './spansTable';
 import SpansTable from './spansTable';
 
@@ -32,6 +27,7 @@ type State = {
 };
 
 export default function SpansView(props: Props) {
+  const location = props.location;
   const pageFilter = usePageFilters();
   const [state, setState] = useState<State>({orderBy: 'total_exclusive_time'});
 
@@ -39,60 +35,22 @@ export default function SpansView(props: Props) {
   const [didConfirmSearch, setDidConfirmSearch] = useState<boolean>(false);
   const {orderBy} = state;
 
-  const [clusterPath, setClusterPath] = useState<string[]>(['top']);
-  const currentClusters = clusterPath.map(
-    clusterName =>
-      CLUSTERS[clusterName] || {
-        isDynamic: true,
-        name: clusterName.split(':')[1],
-        value: clusterName.split(':')[1],
-        parentClusterName: clusterName.split(':')[0],
-      }
-  );
-
   const descriptionFilter = didConfirmSearch && searchTerm ? `${searchTerm}` : undefined;
-
-  const currentCluster = currentClusters.at(-1);
-  if (currentCluster?.isDynamic) {
-    const previousCluster = currentClusters.at(-2);
-    currentCluster.condition =
-      previousCluster?.grouping_condition?.(currentCluster.name) || (() => '');
-  }
-
-  const lastStaticCluster = currentClusters.findLast(cluster => !cluster.isDynamic);
-
-  const clusterBreakdowns = useQueries({
-    queries: currentClusters.map(cluster => {
-      return {
-        queryKey: ['clusterBreakdown', descriptionFilter, cluster.name],
-        queryFn: () =>
-          fetch(
-            `${HOST}/?query=${getTimeSpentQuery(
-              descriptionFilter,
-              cluster.grouping_column || '',
-              currentClusters.map(c => c.condition(c.name))
-            )}`
-          ).then(res => res.json()),
-        retry: false,
-        enabled: Boolean(cluster.grouping_column),
-        initialData: [],
-      };
-    }),
-  });
-
+  const queryConditions = buildQueryFilterFromLocation(location);
   const {isLoading: areSpansLoading, data: spansData} = useQuery<SpanDataRow[]>({
-    queryKey: ['spans', currentCluster?.name || 'none', descriptionFilter, orderBy],
+    queryKey: ['spans', descriptionFilter, orderBy, pageFilter.selection.datetime],
     queryFn: () =>
       fetch(
         `${HOST}/?query=${getSpanListQuery(
           descriptionFilter,
           pageFilter.selection.datetime,
-          currentClusters.map(c => c.condition(c.name)),
+          queryConditions,
           orderBy,
           LIMIT
         )}&format=sql`
       ).then(res => res.json()),
     retry: false,
+    refetchOnWindowFocus: false,
     initialData: [],
   });
 
@@ -101,7 +59,7 @@ export default function SpansView(props: Props) {
   const {isLoading: areSpansTrendsLoading, data: spansTrendsData} = useQuery<
     SpanTrendDataRow[]
   >({
-    queryKey: ['spansTrends', currentCluster?.name || 'none', descriptionFilter],
+    queryKey: ['spansTrends', descriptionFilter],
     queryFn: () =>
       fetch(
         `${HOST}/?query=${getSpansTrendsQuery(
@@ -111,6 +69,7 @@ export default function SpansView(props: Props) {
         )}`
       ).then(res => res.json()),
     retry: false,
+    refetchOnWindowFocus: false,
     initialData: [],
     enabled: groupIDs.length > 0,
   });
@@ -121,66 +80,6 @@ export default function SpansView(props: Props) {
         <FilterOptionsContainer>
           <DatePageFilter alignDropdown="left" />
         </FilterOptionsContainer>
-
-        {currentClusters.map((cluster, depth) => {
-          const clusterBreakdownResponse = clusterBreakdowns[depth];
-          if (
-            !clusterBreakdownResponse ||
-            clusterBreakdownResponse.isLoading ||
-            clusterBreakdownResponse.error
-          ) {
-            return null;
-          }
-
-          const exclusiveTimeBySubCluster = keyBy(
-            clusterBreakdownResponse.data,
-            'primary_group'
-          );
-
-          const clusters = Object.keys(exclusiveTimeBySubCluster);
-
-          const segments = _orderBy(
-            (clusters || []).map(clusterName => {
-              const subCluster = CLUSTERS[clusterName];
-
-              return {
-                name: subCluster?.label || clusterName,
-                value: clusterName,
-                count: exclusiveTimeBySubCluster[clusterName]?.exclusive_time,
-                url: '',
-              };
-            }),
-            'count',
-            'desc'
-          );
-
-          if (segments.length === 0) {
-            return null;
-          }
-
-          return (
-            <TagDistributionMeter
-              key={cluster.name}
-              title={cluster.explanation || cluster.label}
-              onTagClick={(_name, tag) => {
-                const incomingCluster = CLUSTERS[tag.value];
-                const bottomCluster = currentClusters.at(-1);
-
-                const incomingClusterName = incomingCluster
-                  ? tag.value
-                  : `${bottomCluster?.name || ''}:${tag.value}`;
-
-                setClusterPath([...clusterPath.slice(0, depth + 1), incomingClusterName]);
-              }}
-              segments={segments}
-              totalValues={sumBy(segments, 'count')}
-            />
-          );
-        })}
-      </div>
-
-      <div>
-        <button onClick={() => setClusterPath(['top'])}>Reset</button>
       </div>
 
       <SearchBar
@@ -195,18 +94,14 @@ export default function SpansView(props: Props) {
         }}
       />
 
-      {lastStaticCluster?.name === 'http.client.get' && currentCluster?.value && (
-        <HostDetails host={currentCluster.value} />
-      )}
-
       <SpanTimeCharts
         descriptionFilter={descriptionFilter || ''}
-        clusters={currentClusters}
+        queryConditions={queryConditions}
       />
 
       <SpansTable
         location={props.location}
-        clusters={currentClusters}
+        queryConditions={queryConditions}
         isLoading={areSpansLoading || areSpansTrendsLoading}
         spansData={spansData}
         orderBy={orderBy}
@@ -224,3 +119,15 @@ const FilterOptionsContainer = styled('div')`
   gap: ${space(1)};
   margin-bottom: ${space(2)};
 `;
+
+const SPAN_FILTER_KEYS = ['action', 'span_operation', 'domain'];
+
+const buildQueryFilterFromLocation = (location: Location) => {
+  const {query} = location;
+  const result = Object.keys(query)
+    .filter(key => SPAN_FILTER_KEYS.includes(key))
+    .map(key => {
+      return `${key} = '${query[key]}'`;
+    });
+  return result;
+};