Browse Source

feat(starfish): consume spans-samples endpoint (#51296)

This PR makes a bunch of changes in order to consume the spans-samples
endpoint in the span sample list sidebar. It also fixes some bugs and
does a few refactor items to make it all work. It still needs some work,
but it's almost there!

![image](https://github.com/getsentry/sentry/assets/44422760/b960ef62-c5e3-408b-ba1a-12c9fbb1e248)

Here's a list of changes
1. Consume span-samples endpoint
2. Fix bug where date in span samples duration chart would show 1970.
3. Refactor to use the shared `SpanIndexedFieldTypes`

Future TODO
1. Fix `Load more samples` button - it will load the exact same samples!
I think it's the endpoint not randomizing it everytime, you might need
to wait a bit between requests to get a different result.
2. See if the design needs some changes.
Dominik Buszowiecki 1 year ago
parent
commit
5f5ede7ca1

+ 5 - 6
static/app/views/starfish/components/samplesTable/spanSamplesTable.tsx

@@ -32,7 +32,6 @@ const COLUMN_ORDER: TableColumnHeader[] = [
 ];
 
 type SpanTableRow = {
-  description: string;
   op: string;
   'span.self_time': number;
   span_id: string;
@@ -43,7 +42,7 @@ type SpanTableRow = {
     timestamp: string;
     'transaction.duration': number;
   };
-  transaction_id: string;
+  'transaction.id': string;
 };
 
 type Props = {
@@ -71,9 +70,9 @@ export function SpanSamplesTable({isLoading, data, p95}: Props) {
     if (column.key === 'transaction_id') {
       return (
         <Link
-          to={`/performance/${row.transaction['project.name']}:${row.transaction_id}#span-${row.span_id}`}
+          to={`/performance/${row.transaction?.['project.name']}:${row['transaction.id']}#span-${row.span_id}`}
         >
-          {row.transaction_id.slice(0, 8)}
+          {row['transaction.id'].slice(0, 8)}
         </Link>
       );
     }
@@ -83,7 +82,7 @@ export function SpanSamplesTable({isLoading, data, p95}: Props) {
         <SpanDurationBar
           spanOp={row.op}
           spanDuration={row['span.self_time']}
-          transactionDuration={row.transaction['transaction.duration']}
+          transactionDuration={row.transaction?.['transaction.duration']}
         />
       );
     }
@@ -93,7 +92,7 @@ export function SpanSamplesTable({isLoading, data, p95}: Props) {
     }
 
     if (column.key === 'timestamp') {
-      return <DateTime date={row.timestamp} year timeZone seconds />;
+      return <DateTime date={row['span.timestamp']} year timeZone seconds />;
     }
 
     return <span>{row[column.key]}</span>;

+ 5 - 1
static/app/views/starfish/queries/useSpanMetrics.tsx

@@ -7,7 +7,11 @@ import type {IndexedSpan} from 'sentry/views/starfish/queries/types';
 import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery';
 
 export type SpanMetrics = {
-  [metric: string]: number;
+  [metric: string]: number | string;
+  'p95(span.self_time)': number;
+  'span.op': string;
+  'sps()': number;
+  'time_spent_percentage()': number;
 };
 
 export const useSpanMetrics = (

+ 60 - 53
static/app/views/starfish/queries/useSpanSamples.tsx

@@ -1,60 +1,67 @@
-import {useDiscoverQuery} from 'sentry/utils/discover/discoverQuery';
-import EventView from 'sentry/utils/discover/eventView';
-import {DiscoverDatasets} from 'sentry/utils/discover/types';
+import moment from 'moment';
+import * as qs from 'query-string';
+
+import {useQuery} from 'sentry/utils/queryClient';
+import {MutableSearch} from 'sentry/utils/tokenizeSearch';
+import useApi from 'sentry/utils/useApi';
 import {useLocation} from 'sentry/utils/useLocation';
-import useOrganization from 'sentry/utils/useOrganization';
-import type {IndexedSpan} from 'sentry/views/starfish/queries/types';
-import {SpanIndexedFields} from 'sentry/views/starfish/types';
+import usePageFilters from 'sentry/utils/usePageFilters';
+import {SpanIndexedFields, SpanIndexedFieldTypes} from 'sentry/views/starfish/types';
+import {getDateConditions} from 'sentry/views/starfish/utils/getDateConditions';
+import {DATE_FORMAT} from 'sentry/views/starfish/utils/useSpansQuery';
+
+type Options = {
+  groupId?: string;
+  transactionName?: string;
+};
 
-const DEFAULT_LIMIT = 10;
-const DEFAULT_ORDER_BY = '-duration';
+type SpanSample = Pick<
+  SpanIndexedFieldTypes,
+  | SpanIndexedFields.SPAN_SELF_TIME
+  | SpanIndexedFields.TRANSACTION_ID
+  | SpanIndexedFields.PROJECT
+  | SpanIndexedFields.TIMESTAMP
+  | SpanIndexedFields.ID
+>;
 
-export function useSpanSamples(
-  groupId?: string,
-  transaction?: string,
-  limit?: number,
-  orderBy?: string,
-  referrer: string = 'use-span-samples'
-) {
+export const useSpanSamples = (options: Options) => {
+  const url = '/api/0/organizations/sentry/spans-samples/';
+  const api = useApi();
+  const pageFilter = usePageFilters();
+  const {groupId, transactionName} = options;
   const location = useLocation();
-  const organization = useOrganization();
+  // TODO - add http method when available
+  const query = new MutableSearch([
+    `span.group:${groupId}`,
+    `transaction:${transactionName}`,
+  ]);
 
-  const eventView = EventView.fromNewQueryWithLocation(
-    {
-      name: 'Span Samples',
-      query: `${groupId ? ` group:${groupId}` : ''} ${
-        transaction ? ` transaction:${transaction}` : ''
-      }`,
-      fields: [
-        'span_id',
-        'group',
-        'action',
-        'description',
-        'domain',
-        'module',
-        SpanIndexedFields.SPAN_SELF_TIME,
-        'op',
-        'transaction_id',
-        'timestamp',
-      ],
-      dataset: DiscoverDatasets.SPANS_INDEXED,
-      orderby: orderBy ?? DEFAULT_ORDER_BY,
-      projects: [1],
-      version: 2,
-    },
-    location
-  );
+  const dateCondtions = getDateConditions(pageFilter.selection);
 
-  const response = useDiscoverQuery({
-    eventView,
-    orgSlug: organization.slug,
-    location,
-    referrer,
-    limit: limit ?? DEFAULT_LIMIT,
+  return useQuery<SpanSample[]>({
+    queryKey: [
+      'span-samples',
+      groupId,
+      transactionName,
+      dateCondtions.statsPeriod,
+      dateCondtions.start,
+      dateCondtions.end,
+    ],
+    queryFn: async () => {
+      const {data} = await api.requestPromise(
+        `${url}?${qs.stringify({
+          ...dateCondtions,
+          ...{utc: location.query.utc},
+          query: query.formatString(),
+        })}`
+      );
+      return data?.map((d: SpanSample) => ({
+        ...d,
+        timestamp: moment(d.timestamp).format(DATE_FORMAT),
+      }));
+    },
+    refetchOnWindowFocus: false,
+    enabled: Boolean(groupId && transactionName),
+    initialData: [],
   });
-
-  const data = (response.data?.data ?? []) as unknown as IndexedSpan[];
-  const pageLinks = response.pageLinks;
-
-  return {...response, data, pageLinks};
-}
+};

+ 9 - 1
static/app/views/starfish/queries/useTransactions.tsx

@@ -25,7 +25,15 @@ export function useTransactions(eventIDs: string[], referrer = 'use-transactions
     location
   );
 
-  const response = useDiscoverQuery({eventView, location, orgSlug: slug, referrer});
+  const response = useDiscoverQuery({
+    eventView,
+    location,
+    orgSlug: slug,
+    referrer,
+    options: {
+      enabled: Boolean(eventIDs.length),
+    },
+  });
   const data = (response.data?.data ?? []) as unknown as Transaction[];
 
   return {

+ 19 - 0
static/app/views/starfish/types.tsx

@@ -18,8 +18,27 @@ export enum SpanMetricsFields {
 
 export enum SpanIndexedFields {
   SPAN_SELF_TIME = 'span.self_time',
+  MODULE = 'span.module',
+  ID = 'span_id',
+  DESCRIPTION = 'span.description',
+  TIMESTAMP = 'timestamp',
+  ACTION = 'span.action',
+  TRANSACTION_ID = 'transaction.id',
+  DOMAIN = 'span.domain',
+  GROUP = 'span.group',
+  PROJECT = 'project',
 }
 
+export type SpanIndexedFieldTypes = {
+  [SpanIndexedFields.SPAN_SELF_TIME]: number;
+  [SpanIndexedFields.TIMESTAMP]: string;
+  [SpanIndexedFields.ACTION]: string;
+  [SpanIndexedFields.TRANSACTION_ID]: string;
+  [SpanIndexedFields.DOMAIN]: string;
+  [SpanIndexedFields.PROJECT]: string;
+  [SpanIndexedFields.ID]: string;
+};
+
 export const StarfishDatasetFields = {
   [DiscoverDatasets.SPANS_METRICS]: SpanIndexedFields,
   [DiscoverDatasets.SPANS_INDEXED]: SpanIndexedFields,

+ 8 - 4
static/app/views/starfish/utils/useSpansQuery.tsx

@@ -9,7 +9,7 @@ import {
 import {useLocation} from 'sentry/utils/useLocation';
 import useOrganization from 'sentry/utils/useOrganization';
 
-const DATE_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ';
+export const DATE_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ';
 
 // Setting return type since I'd rather not know if its discover query or not
 export type UseSpansQueryReturnType<T> = {
@@ -148,12 +148,16 @@ function processDiscoverTimeseriesResult(result, eventView: EventView) {
     (typeof eventView.yAxis === 'string' || eventView.yAxis.length === 1);
   const firstYAxis =
     typeof eventView.yAxis === 'string' ? eventView.yAxis : eventView.yAxis[0];
-
   if (result.data) {
-    return processSingleDiscoverTimeseriesResult(
+    const timeSeriesResult: Interval[] = processSingleDiscoverTimeseriesResult(
       result,
       singleYAxis ? firstYAxis : 'count'
-    );
+    ).map(data => ({
+      interval: moment(parseInt(data.interval, 10) * 1000).format(DATE_FORMAT),
+      [firstYAxis]: data[firstYAxis],
+      group: data.group,
+    }));
+    return timeSeriesResult;
   }
   Object.keys(result).forEach(key => {
     if (result[key].data) {

+ 1 - 1
static/app/views/starfish/views/spanSummaryPage/index.tsx

@@ -77,7 +77,7 @@ function SpanSummaryPage({params, location}: Props) {
       {group: groupId},
       queryFilter,
       [`p95(${SPAN_SELF_TIME})`, 'sps()'],
-      'sidebar-span-metrics'
+      'span-summary-page-metrics'
     );
 
   useSynchronizeCharts([!areSpanMetricsSeriesLoading]);

+ 13 - 9
static/app/views/starfish/views/spanSummaryPage/sampleList/durationChart/index.tsx

@@ -1,6 +1,5 @@
 import {Fragment} from 'react';
 import {useTheme} from '@emotion/react';
-import moment from 'moment';
 
 import {Series} from 'sentry/types/echarts';
 import {P95_COLOR} from 'sentry/views/starfish/colours';
@@ -28,19 +27,20 @@ function DurationChart({groupId, transactionName}: Props) {
     'sidebar-span-metrics'
   );
 
-  const {data: spans, isLoading: areSpanSamplesLoading} = useSpanSamples(
+  const {
+    data: spans,
+    isLoading: areSpanSamplesLoading,
+    isRefetching: areSpanSamplesRefetching,
+  } = useSpanSamples({
     groupId,
     transactionName,
-    undefined,
-    '-duration',
-    'span-summary-panel-samples-table-spans'
-  );
+  });
 
   const sampledSpanDataSeries: Series[] = spans.map(
-    ({timestamp, 'span.self_time': duration, transaction_id}) => ({
+    ({timestamp, 'span.self_time': duration, 'transaction.id': transaction_id}) => ({
       data: [
         {
-          name: moment(timestamp).unix(),
+          name: timestamp,
           value: duration,
         },
       ],
@@ -61,7 +61,11 @@ function DurationChart({groupId, transactionName}: Props) {
         start=""
         end=""
         loading={isLoading}
-        scatterPlot={areSpanSamplesLoading ? undefined : sampledSpanDataSeries}
+        scatterPlot={
+          areSpanSamplesLoading || areSpanSamplesRefetching
+            ? undefined
+            : sampledSpanDataSeries
+        }
         utc={false}
         chartColors={[P95_COLOR]}
         isLineChart

+ 24 - 14
static/app/views/starfish/views/spanSummaryPage/sampleList/sampleTable/sampleTable.tsx

@@ -1,14 +1,15 @@
 import {Fragment} from 'react';
 import keyBy from 'lodash/keyBy';
 
-import Pagination from 'sentry/components/pagination';
+import {Button} from 'sentry/components/button';
+import {t} from 'sentry/locale';
 import {SpanSamplesTable} from 'sentry/views/starfish/components/samplesTable/spanSamplesTable';
 import {useSpanMetrics} from 'sentry/views/starfish/queries/useSpanMetrics';
 import {useSpanSamples} from 'sentry/views/starfish/queries/useSpanSamples';
 import {useTransactions} from 'sentry/views/starfish/queries/useTransactions';
 import {SpanMetricsFields} from 'sentry/views/starfish/types';
 
-const {SPAN_SELF_TIME} = SpanMetricsFields;
+const {SPAN_SELF_TIME, SPAN_OP} = SpanMetricsFields;
 
 type Props = {
   groupId: string;
@@ -20,42 +21,51 @@ function SampleTable({groupId, transactionName}: Props) {
   const {data: spanMetrics} = useSpanMetrics(
     {group: groupId},
     {transactionName},
-    [`p95(${SPAN_SELF_TIME})`],
+    [`p95(${SPAN_SELF_TIME})`, SPAN_OP],
     'span-summary-panel-samples-table-p95'
   );
 
   const {
     data: spans,
     isLoading: areSpanSamplesLoading,
-    pageLinks,
-  } = useSpanSamples(
+    isRefetching: areSpanSamplesRefetching,
+    refetch,
+  } = useSpanSamples({
     groupId,
     transactionName,
-    undefined,
-    '-duration',
-    'span-summary-panel-samples-table-spans'
-  );
+  });
 
-  const {data: transactions, isLoading: areTransactionsLoading} = useTransactions(
-    spans.map(span => span.transaction_id),
+  const {
+    data: transactions,
+    isLoading: areTransactionsLoading,
+    isRefetching: areTransactionsRefetching,
+  } = useTransactions(
+    spans.map(span => span['transaction.id']),
     'span-summary-panel-samples-table-transactions'
   );
 
   const transactionsById = keyBy(transactions, 'id');
 
+  const isLoading =
+    areSpanSamplesLoading ||
+    areSpanSamplesRefetching ||
+    areTransactionsLoading ||
+    areTransactionsRefetching;
+
   return (
     <Fragment>
       <SpanSamplesTable
         data={spans.map(sample => {
           return {
             ...sample,
-            transaction: transactionsById[sample.transaction_id],
+            op: spanMetrics['span.op'],
+            transaction: transactionsById[sample['transaction.id']],
           };
         })}
-        isLoading={areSpanSamplesLoading || areTransactionsLoading}
+        isLoading={isLoading}
         p95={spanMetrics?.[`p95(${SPAN_SELF_TIME})`]}
       />
-      <Pagination pageLinks={pageLinks} />
+      <Button onClick={() => refetch()}>{t('Load More Samples')}</Button>
     </Fragment>
   );
 }