Browse Source

feat(trace): extract data fetching and partially implement incremental loading (#66009)

Start passing the new timestamp query param if available and hoists the
logic fetching logic so that we can incrementally fetch more data
Jonas 1 year ago
parent
commit
7a0f33f941

+ 43 - 54
static/app/views/performance/newTraceDetails/index.tsx

@@ -1,4 +1,4 @@
-import {Fragment, useMemo, useState} from 'react';
+import {Fragment, useMemo} from 'react';
 import type {Location} from 'history';
 
 import ButtonBar from 'sentry/components/buttonBar';
@@ -12,7 +12,6 @@ import {t} from 'sentry/locale';
 import type {EventTransaction, Organization} from 'sentry/types';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import EventView from 'sentry/utils/discover/eventView';
-import {TraceFullDetailedQuery} from 'sentry/utils/performance/quickTrace/traceFullQuery';
 import TraceMetaQuery, {
   type TraceMetaQueryChildrenProps,
 } from 'sentry/utils/performance/quickTrace/traceMetaQuery';
@@ -26,6 +25,7 @@ import {useLocation} from 'sentry/utils/useLocation';
 import useOrganization from 'sentry/utils/useOrganization';
 import {useParams} from 'sentry/utils/useParams';
 import useProjects from 'sentry/utils/useProjects';
+import {useTrace} from 'sentry/views/performance/newTraceDetails/useTrace';
 
 import Breadcrumb from '../breadcrumb';
 
@@ -46,19 +46,19 @@ export function TraceView() {
 
   const traceSlug = params.traceSlug?.trim() ?? '';
 
-  const dateSelection = useMemo(() => {
-    const queryParams = normalizeDateTimeParams(location.query, {
+  const queryParams = useMemo(() => {
+    const normalizedParams = normalizeDateTimeParams(location.query, {
       allowAbsolutePageDatetime: true,
     });
-    const start = decodeScalar(queryParams.start);
-    const end = decodeScalar(queryParams.end);
-    const statsPeriod = decodeScalar(queryParams.statsPeriod);
+    const start = decodeScalar(normalizedParams.start);
+    const end = decodeScalar(normalizedParams.end);
+    const statsPeriod = decodeScalar(normalizedParams.statsPeriod);
 
-    return {start, end, statsPeriod};
+    return {start, end, statsPeriod, useSpans: 1};
   }, [location.query]);
 
-  const _traceEventView = useMemo(() => {
-    const {start, end, statsPeriod} = dateSelection;
+  const traceEventView = useMemo(() => {
+    const {start, end, statsPeriod} = queryParams;
 
     return EventView.fromSavedQuery({
       id: undefined,
@@ -72,48 +72,34 @@ export function TraceView() {
       end,
       range: statsPeriod,
     });
-  }, [dateSelection, traceSlug]);
+  }, [queryParams, traceSlug]);
 
-  const [_limit, _setLimit] = useState<number>();
-  // const _handleLimithange = useCallback((newLimit: number) => {
-  //   setLimit(newLimit);
-  // }, []);
+  const trace = useTrace();
 
   return (
     <SentryDocumentTitle title={DOCUMENT_TITLE} orgSlug={organization.slug}>
       <Layout.Page>
         <NoProjectMessage organization={organization}>
-          <TraceFullDetailedQuery
-            type="spans"
+          <TraceMetaQuery
             location={location}
             orgSlug={organization.slug}
             traceId={traceSlug}
-            start={dateSelection.start}
-            end={dateSelection.end}
-            statsPeriod={dateSelection.statsPeriod}
+            start={queryParams.start}
+            end={queryParams.end}
+            statsPeriod={queryParams.statsPeriod}
           >
-            {trace => (
-              <TraceMetaQuery
+            {metaResults => (
+              <TraceViewContent
+                status={trace.status}
+                trace={trace.data}
+                traceSlug={traceSlug}
+                organization={organization}
                 location={location}
-                orgSlug={organization.slug}
-                traceId={traceSlug}
-                start={dateSelection.start}
-                end={dateSelection.end}
-                statsPeriod={dateSelection.statsPeriod}
-              >
-                {metaResults => (
-                  <TraceViewContent
-                    traceSplitResult={trace?.traces}
-                    traceSlug={traceSlug}
-                    organization={organization}
-                    location={location}
-                    traceEventView={_traceEventView}
-                    metaResults={metaResults}
-                  />
-                )}
-              </TraceMetaQuery>
+                traceEventView={traceEventView}
+                metaResults={metaResults}
+              />
             )}
-          </TraceFullDetailedQuery>
+          </TraceMetaQuery>
         </NoProjectMessage>
       </Layout.Page>
     </SentryDocumentTitle>
@@ -124,35 +110,41 @@ type TraceViewContentProps = {
   location: Location;
   metaResults: TraceMetaQueryChildrenProps;
   organization: Organization;
+  status: 'pending' | 'resolved' | 'error' | 'initial';
+  trace: TraceSplitResults<TraceFullDetailed> | null;
   traceEventView: EventView;
   traceSlug: string;
-  traceSplitResult: TraceSplitResults<TraceFullDetailed> | null;
 };
 
 function TraceViewContent(props: TraceViewContentProps) {
-  const rootEvent = props.traceSplitResult?.transactions?.[0];
   const {projects} = useProjects();
+
   const tree = useMemo(() => {
-    if (!props.traceSplitResult) {
+    if (props.status === 'pending') {
       return TraceTree.Loading({
         project_slug: projects?.[0]?.slug ?? '',
         event_id: props.traceSlug,
       });
     }
 
-    return TraceTree.FromTrace(props.traceSplitResult);
-  }, [props.traceSlug, props.traceSplitResult, projects]);
+    if (props.trace) {
+      return TraceTree.FromTrace(props.trace);
+    }
+
+    return TraceTree.Empty();
+  }, [props.traceSlug, props.trace, props.status, projects]);
 
   const traceType = useMemo(() => {
-    if (tree.type === 'loading') {
+    if (props.status !== 'resolved' || !tree) {
       return null;
     }
     return TraceTree.GetTraceType(tree.root);
-  }, [tree]);
+  }, [props.status, tree]);
 
+  const root = props.trace?.transactions?.[0];
   const rootEventResults = useApiQuery<EventTransaction>(
     [
-      `/organizations/${props.organization.slug}/events/${rootEvent?.project_slug}:${rootEvent?.event_id}/`,
+      `/organizations/${props.organization.slug}/events/${root?.project_slug}:${root?.event_id}/`,
       {
         query: {
           referrer: 'trace-details-summary',
@@ -161,10 +153,7 @@ function TraceViewContent(props: TraceViewContentProps) {
     ],
     {
       staleTime: 0,
-      enabled: !!(
-        props.traceSplitResult?.transactions &&
-        props.traceSplitResult.transactions.length > 0
-      ),
+      enabled: !!(props.trace?.transactions && props.trace.transactions.length > 0),
     }
   );
 
@@ -209,14 +198,14 @@ function TraceViewContent(props: TraceViewContentProps) {
               rootEventResults={rootEventResults}
               metaResults={props.metaResults}
               organization={props.organization}
-              traces={props.traceSplitResult}
+              traces={props.trace}
             />
             <Trace trace={tree} trace_id={props.traceSlug} />
             <TraceFooter
               rootEventResults={rootEventResults}
               organization={props.organization}
               location={props.location}
-              traces={props.traceSplitResult}
+              traces={props.trace}
               traceEventView={props.traceEventView}
             />
             <TraceDetailPanel />

+ 57 - 6
static/app/views/performance/newTraceDetails/trace.tsx

@@ -55,6 +55,8 @@ function decodeScrollQueue(maybePath: unknown): TraceTree.NodePath[] | null {
   return null;
 }
 
+const COUNT_FORMATTER = Intl.NumberFormat(undefined, {notation: 'compact'});
+
 interface RovingTabIndexState {
   index: number | null;
   items: number | null;
@@ -308,6 +310,55 @@ function Trace({trace, trace_id}: TraceProps) {
     [viewManager.list]
   );
 
+  // @TODO this is the implementation of infinite scroll. Once the user
+  // reaches the end of the list, we fetch more data. The data is not yet
+  // being appended to the tree as we need to figure out UX for this.
+  // onRowsRendered callback should be passed to the List component
+
+  // const limitRef = useRef<number | null>(null);
+  // if (limitRef.current === null) {
+  //   let decodedLimit = getTraceQueryParams(qs.parse(location.search)).limit;
+  //   if (typeof decodedLimit === 'string') {
+  //     decodedLimit = parseInt(decodedLimit, 2);
+  //   }
+
+  //   limitRef.current = decodedLimit;
+  // }
+
+  // const loadMoreRequestRef =
+  //   useRef<Promise<TraceSplitResults<TraceFullDetailed> | null> | null>(null);
+
+  // const onRowsRendered = useCallback((rows: RenderedRows) => {
+  //   if (loadMoreRequestRef.current) {
+  //     // in flight request
+  //     return;
+  //   }
+  //   if (rows.stopIndex !== treeRef.current.list.length - 1) {
+  //     // not at the end
+  //     return;
+  //   }
+  //   if (
+  //     !loadMoreRequestRef.current &&
+  //     limitRef.current &&
+  //     rows.stopIndex === treeRef.current.list.length - 1
+  //   ) {
+  //     limitRef.current = limitRef.current + 500;
+  //     const promise = fetchTrace(api, {
+  //       traceId: trace_id,
+  //       orgSlug: organization.slug,
+  //       query: qs.stringify(getTraceQueryParams(location, {limit: limitRef.current})),
+  //     })
+  //       .then(data => {
+  //         return data;
+  //       })
+  //       .catch(e => {
+  //         return e;
+  //       });
+
+  //     loadMoreRequestRef.current = promise;
+  //   }
+  // }, []);
+
   const projectLookup = useMemo(() => {
     return projects.reduce<Record<Project['slug'], Project>>((acc, project) => {
       acc[project.slug] = project;
@@ -489,7 +540,7 @@ function RenderRow(props: {
                 expanded={!props.node.expanded}
                 onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
               >
-                {props.node.groupCount}{' '}
+                {COUNT_FORMATTER.format(props.node.groupCount)}{' '}
               </ChildrenCountButton>
             </div>
 
@@ -578,7 +629,7 @@ function RenderRow(props: {
                   expanded={props.node.expanded || props.node.zoomedIn}
                   onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
                 >
-                  {props.node.children.length}{' '}
+                  {COUNT_FORMATTER.format(props.node.children.length)}{' '}
                 </ChildrenCountButton>
               ) : null}
             </div>
@@ -666,7 +717,7 @@ function RenderRow(props: {
                   expanded={props.node.expanded || props.node.zoomedIn}
                   onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
                 >
-                  {props.node.children.length}{' '}
+                  {COUNT_FORMATTER.format(props.node.children.length)}{' '}
                 </ChildrenCountButton>
               ) : null}
             </div>
@@ -821,7 +872,7 @@ function RenderRow(props: {
                   expanded={props.node.expanded || props.node.zoomedIn}
                   onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
                 >
-                  {props.node.children.length}{' '}
+                  {COUNT_FORMATTER.format(props.node.children.length)}{' '}
                 </ChildrenCountButton>
               ) : null}
             </div>
@@ -899,7 +950,7 @@ function RenderRow(props: {
                   expanded={props.node.expanded || props.node.zoomedIn}
                   onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
                 >
-                  {props.node.children.length}{' '}
+                  {COUNT_FORMATTER.format(props.node.children.length)}{' '}
                 </ChildrenCountButton>
               ) : null}
             </div>
@@ -978,7 +1029,7 @@ function RenderPlaceholderRow(props: {
                 expanded={props.node.expanded || props.node.zoomedIn}
                 onClick={() => void 0}
               >
-                {props.node.children.length}{' '}
+                {COUNT_FORMATTER.format(props.node.children.length)}{' '}
               </ChildrenCountButton>
             ) : null}
           </div>

+ 132 - 0
static/app/views/performance/newTraceDetails/useTrace.tsx

@@ -0,0 +1,132 @@
+import {useEffect, useMemo, useState} from 'react';
+import type {Location} from 'history';
+import * as qs from 'query-string';
+
+import type {Client} from 'sentry/api';
+import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
+import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
+import type {
+  TraceFullDetailed,
+  TraceSplitResults,
+} from 'sentry/utils/performance/quickTrace/types';
+import {decodeScalar} from 'sentry/utils/queryString';
+import useApi from 'sentry/utils/useApi';
+import {useLocation} from 'sentry/utils/useLocation';
+import useOrganization from 'sentry/utils/useOrganization';
+import {useParams} from 'sentry/utils/useParams';
+
+export function fetchTrace(
+  api: Client,
+  params: {
+    orgSlug: string;
+    query: string;
+    traceId: string;
+  }
+): Promise<TraceSplitResults<TraceFullDetailed>> {
+  return api.requestPromise(
+    `/organizations/${params.orgSlug}/events-trace/${params.traceId}/?${params.query}`
+  );
+}
+
+const DEFAULT_TIMESTAMP_LIMIT = 10_000;
+const DEFAULT_LIMIT = 1_000;
+
+export function getTraceQueryParams(
+  query: Location['query'],
+  options: {limit?: number} = {}
+) {
+  const normalizedParams = normalizeDateTimeParams(query, {
+    allowAbsolutePageDatetime: true,
+  });
+  const statsPeriod = decodeScalar(normalizedParams.statsPeriod);
+  const project = decodeScalar(normalizedParams.project, ALL_ACCESS_PROJECTS + '');
+  const timestamp = decodeScalar(normalizedParams.timestamp);
+  let decodedLimit: string | number | undefined =
+    options.limit ?? decodeScalar(normalizedParams.limit);
+
+  if (typeof decodedLimit === 'string') {
+    decodedLimit = parseInt(decodedLimit, 10);
+  }
+
+  if (timestamp) {
+    decodedLimit = decodedLimit ?? DEFAULT_TIMESTAMP_LIMIT;
+  } else {
+    decodedLimit = decodedLimit ?? DEFAULT_LIMIT;
+  }
+
+  const limit = decodedLimit;
+
+  return {limit, statsPeriod, project, timestamp, useSpans: 1};
+}
+
+type UseTraceParams = {
+  limit?: number;
+};
+
+type RequestState<T> = {
+  data: T | null;
+  status: 'resolved' | 'pending' | 'error' | 'initial';
+  error?: Error | null;
+};
+
+const DEFAULT_OPTIONS = {};
+export function useTrace(
+  options: Partial<UseTraceParams> = DEFAULT_OPTIONS
+): RequestState<TraceSplitResults<TraceFullDetailed> | null> {
+  const api = useApi();
+  const location = useLocation();
+  const organization = useOrganization();
+  const params = useParams<{traceSlug?: string}>();
+
+  const [trace, setTrace] = useState<
+    RequestState<TraceSplitResults<TraceFullDetailed> | null>
+  >({
+    status: 'initial',
+    data: null,
+  });
+
+  const queryParams = useMemo(() => {
+    return getTraceQueryParams(location.query, options);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [options]);
+
+  useEffect(() => {
+    if (!params.traceSlug) {
+      return undefined;
+    }
+
+    let unmounted = false;
+
+    setTrace({
+      status: 'pending',
+      data: null,
+    });
+
+    fetchTrace(api, {
+      traceId: params.traceSlug,
+      orgSlug: organization.slug,
+      query: qs.stringify(queryParams),
+    })
+      .then(resp => {
+        if (unmounted) return;
+        setTrace({
+          status: 'resolved',
+          data: resp,
+        });
+      })
+      .catch(e => {
+        if (unmounted) return;
+        setTrace({
+          status: 'error',
+          data: null,
+          error: e,
+        });
+      });
+
+    return () => {
+      unmounted = true;
+    };
+  }, [api, organization.slug, params.traceSlug, queryParams]);
+
+  return trace;
+}

+ 1 - 1
static/app/views/performance/newTraceDetails/virtualizedViewManager.tsx

@@ -799,7 +799,7 @@ export class VirtualizedViewManager {
 
     // Span "spans" the entire view
     if (span_left <= this.trace_view.x && span_right >= this.trace_view.right) {
-      return anchor_left ? [1, window_left] : [0, window_right];
+      return anchor_left ? [1, window_left] : [1, window_right];
     }
 
     const full_span_px_width = span_space[1] / this.span_to_px[0];