Browse Source

feat(starfish): Show full description on span summary page (#53618)

This mostly affects database queries, since we truncate them
aggressively. On the span summary page, fetches an indexed span, and
then the full span description from the span's trace. A bit of a bother,
but works for now! Also shuffled the layout a bit, giving the query
full-width. This will change too, as we continue working on query
display.

## Changes

- `MutableSearch.addFilterValue`
- Add type for `group_raw` span field
- Add hook to fetch the span description
- Fetch full span description on span details page
- Remove height limit from description panel
- Show query in its own row
George Gritsouk 1 year ago
parent
commit
9d06f817ed

+ 7 - 0
static/app/utils/tokenizeSearch.spec.tsx

@@ -213,6 +213,13 @@ describe('utils/tokenizeSearch', function () {
       );
     });
 
+    it('adds individual values to query object', function () {
+      const results = new MutableSearch([]);
+
+      results.addFilterValue('e', 'e1*e2\\e3');
+      expect(results.formatString()).toEqual('e:"e1\\*e2\\e3"');
+    });
+
     it('add text searches to query object', function () {
       const results = new MutableSearch(['a:a']);
 

+ 9 - 5
static/app/utils/tokenizeSearch.tsx

@@ -143,15 +143,19 @@ export class MutableSearch {
 
   addFilterValues(key: string, values: string[], shouldEscape = true) {
     for (const value of values) {
-      // Filter values that we insert through the UI can contain special characters
-      // that need to escaped. User entered filters should not be escaped.
-      const escaped = shouldEscape ? escapeFilterValue(value) : value;
-      const token: Token = {type: TokenType.FILTER, key, value: escaped};
-      this.tokens.push(token);
+      this.addFilterValue(key, value, shouldEscape);
     }
     return this;
   }
 
+  addFilterValue(key: string, value: string, shouldEscape = true) {
+    // Filter values that we insert through the UI can contain special characters
+    // that need to escaped. User entered filters should not be escaped.
+    const escaped = shouldEscape ? escapeFilterValue(value) : value;
+    const token: Token = {type: TokenType.FILTER, key, value: escaped};
+    this.tokens.push(token);
+  }
+
   setFilterValues(key: string, values: string[], shouldEscape = true) {
     this.removeFilter(key);
     this.addFilterValues(key, values, shouldEscape);

+ 21 - 0
static/app/views/starfish/queries/useEventJSON.ts

@@ -0,0 +1,21 @@
+import {RawSpanType} from 'sentry/components/events/interfaces/spans/types';
+import {EventTransaction} from 'sentry/types';
+import {useApiQuery} from 'sentry/utils/queryClient';
+import useOrganization from 'sentry/utils/useOrganization';
+
+interface RawTransactionEvent {
+  spans: RawSpanType[];
+  type: 'transaction';
+}
+
+export function useEventJSON(
+  eventID?: EventTransaction['eventID'],
+  projectSlug?: string
+) {
+  const organization = useOrganization();
+
+  return useApiQuery<RawTransactionEvent>(
+    [`/projects/${organization.slug}/${projectSlug}/events/${eventID}/json/`],
+    {staleTime: Infinity, enabled: Boolean(eventID && projectSlug && organization.slug)}
+  );
+}

+ 31 - 0
static/app/views/starfish/queries/useFullSpanDescription.tsx

@@ -0,0 +1,31 @@
+import {useEventJSON} from 'sentry/views/starfish/queries/useEventJSON';
+import {useIndexedSpans} from 'sentry/views/starfish/queries/useIndexedSpans';
+import {SpanIndexedFields} from 'sentry/views/starfish/types';
+
+// NOTE: Fetching the top one is a bit naive, but works for now. A better
+// approach might be to fetch several at a time, and let the hook consumer
+// decide how to display them
+export function useFullSpanDescription(group: string) {
+  const {data: indexedSpans} = useIndexedSpans(
+    {
+      [SpanIndexedFields.SPAN_GROUP]: group,
+    },
+    1
+  );
+
+  const firstIndexedSpan = indexedSpans?.[0];
+
+  const response = useEventJSON(
+    firstIndexedSpan ? firstIndexedSpan[SpanIndexedFields.TRANSACTION_ID] : undefined,
+    firstIndexedSpan ? firstIndexedSpan[SpanIndexedFields.PROJECT] : undefined
+  );
+
+  const fullSpanDescription = response?.data?.spans?.find(
+    span => span.span_id === firstIndexedSpan?.[SpanIndexedFields.ID]
+  )?.description;
+
+  return {
+    ...response,
+    data: fullSpanDescription,
+  };
+}

+ 52 - 0
static/app/views/starfish/queries/useIndexedSpans.tsx

@@ -0,0 +1,52 @@
+import {Location} from 'history';
+
+import EventView from 'sentry/utils/discover/eventView';
+import {DiscoverDatasets} from 'sentry/utils/discover/types';
+import {MutableSearch} from 'sentry/utils/tokenizeSearch';
+import {useLocation} from 'sentry/utils/useLocation';
+import {SpanIndexedFields, SpanIndexedFieldTypes} from 'sentry/views/starfish/types';
+import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery';
+
+const DEFAULT_LIMIT = 10;
+
+interface Filters {
+  [key: string]: string;
+}
+
+export const useIndexedSpans = (
+  filters: Filters,
+  limit: number = DEFAULT_LIMIT,
+  enabled: boolean = true,
+  referrer: string = 'use-indexed-spans'
+) => {
+  const location = useLocation();
+  const eventView = getEventView(filters, location);
+
+  return useSpansQuery<SpanIndexedFieldTypes[]>({
+    eventView,
+    limit,
+    initialData: [],
+    enabled,
+    referrer,
+  });
+};
+
+function getEventView(filters: Filters, location: Location) {
+  // TODO: Add a `MutableSearch` constructor that accept a key-value mapping
+  const search = new MutableSearch([]);
+
+  for (const filterName in filters) {
+    search.addFilterValue(filterName, filters[filterName]);
+  }
+
+  return EventView.fromNewQueryWithLocation(
+    {
+      name: '',
+      query: search.formatString(),
+      fields: Object.values(SpanIndexedFields),
+      dataset: DiscoverDatasets.SPANS_INDEXED,
+      version: 2,
+    },
+    location
+  );
+}

+ 3 - 1
static/app/views/starfish/types.tsx

@@ -40,7 +40,8 @@ export type SpanMetricsFieldTypes = {
 
 export enum SpanIndexedFields {
   SPAN_SELF_TIME = 'span.self_time',
-  SPAN_GROUP = 'span.group',
+  SPAN_GROUP = 'span.group', // Span group computed from the normalized description. Matches the group in the metrics data set
+  SPAN_GROUP_RAW = 'span.group_raw', // Span group computed from non-normalized description. Matches the group in the event payload
   SPAN_MODULE = 'span.module',
   SPAN_DESCRIPTION = 'span.description',
   SPAN_OP = 'span.op',
@@ -57,6 +58,7 @@ export enum SpanIndexedFields {
 export type SpanIndexedFieldTypes = {
   [SpanIndexedFields.SPAN_SELF_TIME]: number;
   [SpanIndexedFields.SPAN_GROUP]: string;
+  [SpanIndexedFields.SPAN_GROUP_RAW]: string;
   [SpanIndexedFields.SPAN_MODULE]: string;
   [SpanIndexedFields.SPAN_DESCRIPTION]: string;
   [SpanIndexedFields.SPAN_OP]: string;

+ 68 - 56
static/app/views/starfish/views/spanSummaryPage/index.tsx

@@ -30,6 +30,7 @@ import {CountCell} from 'sentry/views/starfish/components/tableCells/countCell';
 import DurationCell from 'sentry/views/starfish/components/tableCells/durationCell';
 import ThroughputCell from 'sentry/views/starfish/components/tableCells/throughputCell';
 import {TimeSpentCell} from 'sentry/views/starfish/components/tableCells/timeSpentCell';
+import {useFullSpanDescription} from 'sentry/views/starfish/queries/useFullSpanDescription';
 import {
   SpanSummaryQueryFilters,
   useSpanMetrics,
@@ -64,6 +65,8 @@ function SpanSummaryPage({params, location}: Props) {
   const {groupId} = params;
   const {transaction, transactionMethod, endpoint, endpointMethod} = location.query;
 
+  const {data: fullSpanDescription} = useFullSpanDescription(groupId);
+
   const queryFilter: SpanSummaryQueryFilters = endpoint
     ? {transactionName: endpoint, 'transaction.method': endpointMethod}
     : {};
@@ -235,69 +238,77 @@ function SpanSummaryPage({params, location}: Props) {
                             <DescriptionTitle>
                               {spanDescriptionCardTitle}
                             </DescriptionTitle>
-                            <SpanDescription span={span} />
+                            <SpanDescription
+                              span={{
+                                ...span,
+                                [SpanMetricsFields.SPAN_DESCRIPTION]:
+                                  fullSpanDescription ?? '',
+                              }}
+                            />
                           </DescriptionContainer>
                         </DescriptionPanelBody>
                       </Panel>
                     </Block>
+                  </BlockContainer>
+                )}
 
-                    <Block>
-                      <ChartPanel
-                        title={getThroughputChartTitle(span?.[SpanMetricsFields.SPAN_OP])}
-                      >
-                        <Chart
-                          height={140}
-                          data={[spanMetricsThroughputSeries]}
-                          loading={areSpanMetricsSeriesLoading}
-                          utc={false}
-                          chartColors={[THROUGHPUT_COLOR]}
-                          isLineChart
-                          definedAxisTicks={4}
-                          aggregateOutputFormat="rate"
-                          rateUnit={RateUnits.PER_MINUTE}
-                          tooltipFormatterOptions={{
-                            valueFormatter: value =>
-                              formatRate(value, RateUnits.PER_MINUTE),
-                          }}
-                        />
-                      </ChartPanel>
-                    </Block>
+                <BlockContainer>
+                  <Block>
+                    <ChartPanel
+                      title={getThroughputChartTitle(span?.[SpanMetricsFields.SPAN_OP])}
+                    >
+                      <Chart
+                        height={140}
+                        data={[spanMetricsThroughputSeries]}
+                        loading={areSpanMetricsSeriesLoading}
+                        utc={false}
+                        chartColors={[THROUGHPUT_COLOR]}
+                        isLineChart
+                        definedAxisTicks={4}
+                        aggregateOutputFormat="rate"
+                        rateUnit={RateUnits.PER_MINUTE}
+                        tooltipFormatterOptions={{
+                          valueFormatter: value =>
+                            formatRate(value, RateUnits.PER_MINUTE),
+                        }}
+                      />
+                    </ChartPanel>
+                  </Block>
+
+                  <Block>
+                    <ChartPanel title={DataTitles.avg}>
+                      <Chart
+                        height={140}
+                        data={[
+                          spanMetricsSeriesData?.[
+                            `avg(${SpanMetricsFields.SPAN_SELF_TIME})`
+                          ],
+                        ]}
+                        loading={areSpanMetricsSeriesLoading}
+                        utc={false}
+                        chartColors={[AVG_COLOR]}
+                        isLineChart
+                        definedAxisTicks={4}
+                      />
+                    </ChartPanel>
+                  </Block>
 
+                  {span?.[SpanMetricsFields.SPAN_OP]?.startsWith('http') && (
                     <Block>
-                      <ChartPanel title={DataTitles.avg}>
+                      <ChartPanel title={DataTitles.errorCount}>
                         <Chart
                           height={140}
-                          data={[
-                            spanMetricsSeriesData?.[
-                              `avg(${SpanMetricsFields.SPAN_SELF_TIME})`
-                            ],
-                          ]}
+                          data={[spanMetricsSeriesData?.[`http_error_count()`]]}
                           loading={areSpanMetricsSeriesLoading}
                           utc={false}
-                          chartColors={[AVG_COLOR]}
+                          chartColors={[ERRORS_COLOR]}
                           isLineChart
                           definedAxisTicks={4}
                         />
                       </ChartPanel>
                     </Block>
-
-                    {span?.[SpanMetricsFields.SPAN_OP]?.startsWith('http') && (
-                      <Block>
-                        <ChartPanel title={DataTitles.errorCount}>
-                          <Chart
-                            height={140}
-                            data={[spanMetricsSeriesData?.[`http_error_count()`]]}
-                            loading={areSpanMetricsSeriesLoading}
-                            utc={false}
-                            chartColors={[ERRORS_COLOR]}
-                            isLineChart
-                            definedAxisTicks={4}
-                          />
-                        </ChartPanel>
-                      </Block>
-                    )}
-                  </BlockContainer>
-                )}
+                  )}
+                </BlockContainer>
 
                 {span && (
                   <SpanTransactionsTable
@@ -339,14 +350,16 @@ type BlockProps = {
 export function Block({title, description, children}: BlockProps) {
   return (
     <BlockWrapper>
-      <BlockTitle>
-        {title}
-        {description && (
-          <BlockTooltipContainer>
-            <QuestionTooltip size="sm" position="right" title={description} />
-          </BlockTooltipContainer>
-        )}
-      </BlockTitle>
+      {title && (
+        <BlockTitle>
+          {title}
+          {description && (
+            <BlockTooltipContainer>
+              <QuestionTooltip size="sm" position="right" title={description} />
+            </BlockTooltipContainer>
+          )}
+        </BlockTitle>
+      )}
       <BlockContent>{children}</BlockContent>
     </BlockWrapper>
   );
@@ -388,7 +401,6 @@ const DescriptionContainer = styled('div')`
 
 const DescriptionPanelBody = styled(PanelBody)`
   padding: ${space(2)};
-  height: 208px;
 `;
 
 const BlockWrapper = styled('div')`