Просмотр исходного кода

feat(new-trace): Updating canFetch property of transactions based on spans count (#75849)

Note: For testing, try out next.js traces, they usually have the Missing
Spans Case.

Customers are confused by the `Missing spans `scenario, for which we
currently render a `No Data` node and the following CTA:
![Screenshot 2024-08-12 at 1 14
48 PM](https://github.com/user-attachments/assets/6c6a07e2-f8b7-433e-80ef-7ca7a685ad73)

We've decided to make transactions that don't have embedded spans unable
to fetch: We wont render the '+' icons, they will just be leaf nodes. We
still need to decide if we want a CTA for this case, as the sdk team
doesn't always see Missing spans as a problem.

The `events-trace-meta` endpoint now gives us an array of
'transaction_id' -> 'span_children_count' called
`transaction_child_count_map` that I used to achieve this new
functionality. This means that we need to wait for the meta endpoint
response for the tree, which is fine since the endpoint is mostly faster
than the trace endpoint: [comparing
durations](https://sentry.sentry.io/discover/homepage/?dataset=discover&display=top5&environment=prod&field=transaction&field=p99%28transaction.duration%29&id=16351&interval=1d&name=&project=1&query=transaction%3A%2Fapi%2F0%2Forganizations%2F%7Borganization_id_or_slug%7D%2Fevents-trace%2F%7Btrace_id%7D%2F+or+transaction%3A%2Fapi%2F0%2Forganizations%2F%7Borganization_id_or_slug%7D%2Fevents-trace-meta%2F%7Btrace_id%7D%2F&queryDataset=error-events&sort=-transaction&statsPeriod=14d&topEvents=5&yAxis=p99%28transaction.duration%29)

I will put up two stacked PRs:
- Pass the correct timestamp to the `events-trace-meta` endpoint for
replay traces. Otherwise we are currently not getting the correct
`transaction_child_count_map` for replay traces.
- Remove all the `No data` node code.

---------

Co-authored-by: Abdullah Khan <abdullahkhan@PG9Y57YDXQ.local>
Abdullah Khan 6 месяцев назад
Родитель
Сommit
07be1fbed4

+ 1 - 0
static/app/utils/performance/quickTrace/types.tsx

@@ -139,4 +139,5 @@ export type TraceMeta = {
   performance_issues: number;
   performance_issues: number;
   projects: number;
   projects: number;
   transactions: number;
   transactions: number;
+  transactiontoSpanChildrenCount: Record<string, number>;
 };
 };

+ 1 - 8
static/app/views/performance/newTraceDetails/guards.tsx

@@ -1,6 +1,5 @@
 import {
 import {
   MissingInstrumentationNode,
   MissingInstrumentationNode,
-  NoDataNode,
   ParentAutogroupNode,
   ParentAutogroupNode,
   SiblingAutogroupNode,
   SiblingAutogroupNode,
   type TraceTree,
   type TraceTree,
@@ -55,7 +54,7 @@ export function isTraceErrorNode(
 export function isRootNode(
 export function isRootNode(
   node: TraceTreeNode<TraceTree.NodeValue>
   node: TraceTreeNode<TraceTree.NodeValue>
 ): node is TraceTreeNode<null> {
 ): node is TraceTreeNode<null> {
-  return node.value === null && !(node instanceof NoDataNode);
+  return node.value === null;
 }
 }
 
 
 export function isTraceNode(
 export function isTraceNode(
@@ -67,12 +66,6 @@ export function isTraceNode(
   );
   );
 }
 }
 
 
-export function isNoDataNode(
-  node: TraceTreeNode<TraceTree.NodeValue>
-): node is NoDataNode {
-  return node instanceof NoDataNode;
-}
-
 export function shouldAddMissingInstrumentationSpan(sdk: string | undefined): boolean {
 export function shouldAddMissingInstrumentationSpan(sdk: string | undefined): boolean {
   if (!sdk) return true;
   if (!sdk) return true;
   if (sdk.length < 'sentry.javascript.'.length) return true;
   if (sdk.length < 'sentry.javascript.'.length) return true;

+ 26 - 5
static/app/views/performance/newTraceDetails/index.tsx

@@ -34,7 +34,7 @@ import {
   cancelAnimationTimeout,
   cancelAnimationTimeout,
   requestAnimationTimeout,
   requestAnimationTimeout,
 } from 'sentry/utils/profiling/hooks/useVirtualizedTree/virtualizedTreeUtils';
 } from 'sentry/utils/profiling/hooks/useVirtualizedTree/virtualizedTreeUtils';
-import type {UseApiQueryResult} from 'sentry/utils/queryClient';
+import type {QueryStatus, UseApiQueryResult} from 'sentry/utils/queryClient';
 import {decodeScalar} from 'sentry/utils/queryString';
 import {decodeScalar} from 'sentry/utils/queryString';
 import type RequestError from 'sentry/utils/requestError/requestError';
 import type RequestError from 'sentry/utils/requestError/requestError';
 import {capitalize} from 'sentry/utils/string/capitalize';
 import {capitalize} from 'sentry/utils/string/capitalize';
@@ -126,6 +126,21 @@ function logTraceMetadata(
   }
   }
 }
 }
 
 
+export function getTraceViewQueryStatus(
+  traceQueryStatus: QueryStatus,
+  traceMetaQueryStatus: QueryStatus
+): QueryStatus {
+  if (traceQueryStatus === 'error' || traceMetaQueryStatus === 'error') {
+    return 'error';
+  }
+
+  if (traceQueryStatus === 'loading' || traceMetaQueryStatus === 'loading') {
+    return 'loading';
+  }
+
+  return 'success';
+}
+
 export function TraceView() {
 export function TraceView() {
   const params = useParams<{traceSlug?: string}>();
   const params = useParams<{traceSlug?: string}>();
   const organization = useOrganization();
   const organization = useOrganization();
@@ -185,7 +200,7 @@ export function TraceView() {
     });
     });
   }, [queryParams, traceSlug]);
   }, [queryParams, traceSlug]);
 
 
-  const meta = useTraceMeta([traceSlug]);
+  const meta = useTraceMeta([{traceSlug, timestamp: queryParams.timestamp}]);
 
 
   const preferences = useMemo(
   const preferences = useMemo(
     () =>
     () =>
@@ -218,7 +233,7 @@ export function TraceView() {
               <TraceViewWaterfall
               <TraceViewWaterfall
                 traceSlug={traceSlug}
                 traceSlug={traceSlug}
                 trace={trace.data ?? null}
                 trace={trace.data ?? null}
-                status={trace.status}
+                status={getTraceViewQueryStatus(trace.status, meta.status)}
                 organization={organization}
                 organization={organization}
                 rootEvent={rootEvent}
                 rootEvent={rootEvent}
                 traceEventView={traceEventView}
                 traceEventView={traceEventView}
@@ -346,8 +361,12 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
       return;
       return;
     }
     }
 
 
-    if (props.trace) {
-      const trace = TraceTree.FromTrace(props.trace, props.replayRecord);
+    if (props.trace && props.metaResults.data) {
+      const trace = TraceTree.FromTrace(
+        props.trace,
+        props.metaResults,
+        props.replayRecord
+      );
 
 
       // Root frame + 2 nodes
       // Root frame + 2 nodes
       const promises: Promise<void>[] = [];
       const promises: Promise<void>[] = [];
@@ -367,6 +386,7 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
     props.traceSlug,
     props.traceSlug,
     props.trace,
     props.trace,
     props.status,
     props.status,
+    props.metaResults,
     props.replayRecord,
     props.replayRecord,
     projects,
     projects,
     api,
     api,
@@ -385,6 +405,7 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
       organization: props.organization,
       organization: props.organization,
       urlParams: qs.parse(location.search),
       urlParams: qs.parse(location.search),
       rerender: forceRerender,
       rerender: forceRerender,
+      metaResults: props.metaResults,
     });
     });
 
 
     return () => cleanup();
     return () => cleanup();

+ 40 - 2
static/app/views/performance/newTraceDetails/trace.spec.tsx

@@ -79,6 +79,7 @@ function mockTraceMetaResponse(resp?: Partial<ResponseType>) {
         performance_issues: 0,
         performance_issues: 0,
         projects: 0,
         projects: 0,
         transactions: 0,
         transactions: 0,
+        transaction_child_count_map: [],
       },
       },
     }),
     }),
   });
   });
@@ -261,7 +262,18 @@ async function keyboardNavigationTestSetup() {
       orphan_errors: [],
       orphan_errors: [],
     },
     },
   });
   });
-  mockTraceMetaResponse();
+  mockTraceMetaResponse({
+    body: {
+      errors: 0,
+      performance_issues: 0,
+      projects: 0,
+      transactions: 0,
+      transaction_child_count_map: keyboard_navigation_transactions.map(t => ({
+        'transaction.id': t.event_id,
+        count: 5,
+      })),
+    },
+  });
   mockTraceRootFacets();
   mockTraceRootFacets();
   mockTraceRootEvent('0');
   mockTraceRootEvent('0');
   mockTraceEventDetails();
   mockTraceEventDetails();
@@ -1082,7 +1094,6 @@ describe('trace view', () => {
       });
       });
     });
     });
     it('during search, expanding a row retriggers search', async () => {
     it('during search, expanding a row retriggers search', async () => {
-      mockTraceMetaResponse();
       mockTraceRootFacets();
       mockTraceRootFacets();
       mockTraceRootEvent('0');
       mockTraceRootEvent('0');
       mockTraceEventDetails();
       mockTraceEventDetails();
@@ -1120,6 +1131,33 @@ describe('trace view', () => {
         },
         },
       });
       });
 
 
+      mockTraceMetaResponse({
+        body: {
+          errors: 0,
+          performance_issues: 0,
+          projects: 0,
+          transactions: 0,
+          transaction_child_count_map: [
+            {
+              'transaction.id': '0',
+              count: 5,
+            },
+            {
+              'transaction.id': '1',
+              count: 5,
+            },
+            {
+              'transaction.id': '2',
+              count: 5,
+            },
+            {
+              'transaction.id': '3',
+              count: 5,
+            },
+          ],
+        },
+      });
+
       const spansRequest = mockSpansResponse(
       const spansRequest = mockSpansResponse(
         '0',
         '0',
         {},
         {},

+ 12 - 51
static/app/views/performance/newTraceDetails/trace.tsx

@@ -15,7 +15,7 @@ import {PlatformIcon} from 'platformicons';
 
 
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import Placeholder from 'sentry/components/placeholder';
 import Placeholder from 'sentry/components/placeholder';
-import {t, tct} from 'sentry/locale';
+import {t} from 'sentry/locale';
 import ConfigStore from 'sentry/stores/configStore';
 import ConfigStore from 'sentry/stores/configStore';
 import {space} from 'sentry/styles/space';
 import {space} from 'sentry/styles/space';
 import type {Organization} from 'sentry/types/organization';
 import type {Organization} from 'sentry/types/organization';
@@ -56,7 +56,6 @@ import {useTraceState, useTraceStateDispatch} from './traceState/traceStateProvi
 import {
 import {
   isAutogroupedNode,
   isAutogroupedNode,
   isMissingInstrumentationNode,
   isMissingInstrumentationNode,
-  isNoDataNode,
   isParentAutogroupedNode,
   isParentAutogroupedNode,
   isSpanNode,
   isSpanNode,
   isTraceErrorNode,
   isTraceErrorNode,
@@ -791,12 +790,18 @@ function RenderRow(props: {
               {props.node.children.length > 0 || props.node.canFetch ? (
               {props.node.children.length > 0 || props.node.canFetch ? (
                 <ChildrenButton
                 <ChildrenButton
                   icon={
                   icon={
-                    props.node.canFetch && props.node.fetchStatus === 'idle' ? (
-                      '+'
-                    ) : props.node.canFetch && props.node.zoomedIn ? (
-                      <TraceIcons.Chevron direction="down" />
+                    props.node.canFetch ? (
+                      props.node.fetchStatus === 'idle' ? (
+                        '+'
+                      ) : props.node.zoomedIn ? (
+                        <TraceIcons.Chevron direction="up" />
+                      ) : (
+                        '+'
+                      )
                     ) : (
                     ) : (
-                      '+'
+                      <TraceIcons.Chevron
+                        direction={props.node.expanded ? 'up' : 'down'}
+                      />
                     )
                     )
                   }
                   }
                   status={props.node.fetchStatus}
                   status={props.node.fetchStatus}
@@ -1116,50 +1121,6 @@ function RenderRow(props: {
     );
     );
   }
   }
 
 
-  if (isNoDataNode(props.node)) {
-    return (
-      <div
-        key={props.index}
-        ref={r =>
-          props.tabIndex === 0
-            ? maybeFocusRow(r, props.node, props.previouslyFocusedNodeRef)
-            : null
-        }
-        tabIndex={props.tabIndex}
-        className={`TraceRow ${rowSearchClassName}`}
-        onClick={onRowClick}
-        onKeyDown={onRowKeyDown}
-        style={props.style}
-      >
-        <div className="TraceLeftColumn" ref={registerListColumnRef}>
-          <div
-            className="TraceLeftColumnInner"
-            style={listColumnStyle}
-            onDoubleClick={onRowDoubleClick}
-          >
-            <div className="TraceChildrenCountWrapper">
-              <Connectors node={props.node} manager={props.manager} />
-            </div>
-            <span className="TraceOperation">{t('Empty')}</span>{' '}
-            <strong className="TraceEmDash"> — </strong>
-            <span className="TraceDescription">
-              {tct('[type] did not report any span data', {
-                type: props.node.parent
-                  ? isTransactionNode(props.node.parent)
-                    ? 'Transaction'
-                    : isSpanNode(props.node.parent)
-                      ? 'Span'
-                      : ''
-                  : '',
-              })}
-            </span>
-          </div>
-        </div>
-        <div ref={registerSpanColumnRef} className={spanColumnClassName} />
-      </div>
-    );
-  }
-
   return null;
   return null;
 }
 }
 
 

+ 17 - 4
static/app/views/performance/newTraceDetails/traceApi/useReplayTraceMeta.tsx

@@ -1,11 +1,12 @@
 import {useMemo} from 'react';
 import {useMemo} from 'react';
 import type {Location} from 'history';
 import type {Location} from 'history';
 
 
-import {getUtcDateString} from 'sentry/utils/dates';
+import {getTimeStampFromTableDateField, getUtcDateString} from 'sentry/utils/dates';
 import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
 import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
 import EventView from 'sentry/utils/discover/eventView';
 import EventView from 'sentry/utils/discover/eventView';
 import {useApiQuery} from 'sentry/utils/queryClient';
 import {useApiQuery} from 'sentry/utils/queryClient';
 import useOrganization from 'sentry/utils/useOrganization';
 import useOrganization from 'sentry/utils/useOrganization';
+import type {ReplayTrace} from 'sentry/views/replays/detail/trace/useReplayTraces';
 import type {ReplayRecord} from 'sentry/views/replays/types';
 import type {ReplayRecord} from 'sentry/views/replays/types';
 
 
 import {type TraceMetaQueryResults, useTraceMeta} from './useTraceMeta';
 import {type TraceMetaQueryResults, useTraceMeta} from './useTraceMeta';
@@ -66,17 +67,29 @@ export function useReplayTraceMeta(
     }
     }
   );
   );
 
 
-  const traceIds = useMemo(() => {
-    return (eventsData?.data ?? []).map(({trace}) => String(trace)).filter(Boolean);
+  const replayTraces = useMemo(() => {
+    const traces: ReplayTrace[] = [];
+
+    for (const row of eventsData?.data ?? []) {
+      if (row.trace) {
+        traces.push({
+          traceSlug: String(row.trace),
+          timestamp: getTimeStampFromTableDateField(row['min(timestamp)']),
+        });
+      }
+    }
+
+    return traces;
   }, [eventsData]);
   }, [eventsData]);
 
 
-  const meta = useTraceMeta(traceIds);
+  const meta = useTraceMeta(replayTraces);
 
 
   const metaResults = useMemo(() => {
   const metaResults = useMemo(() => {
     return {
     return {
       data: meta.data,
       data: meta.data,
       isLoading: eventsIsLoading || meta.isLoading,
       isLoading: eventsIsLoading || meta.isLoading,
       errors: meta.errors,
       errors: meta.errors,
+      status: meta.status,
     };
     };
   }, [meta, eventsIsLoading]);
   }, [meta, eventsIsLoading]);
 
 

+ 36 - 9
static/app/views/performance/newTraceDetails/traceApi/useTraceMeta.spec.tsx

@@ -5,12 +5,28 @@ import {makeTestQueryClient} from 'sentry-test/queryClient';
 import {renderHook, waitFor} from 'sentry-test/reactTestingLibrary';
 import {renderHook, waitFor} from 'sentry-test/reactTestingLibrary';
 
 
 import * as useOrganization from 'sentry/utils/useOrganization';
 import * as useOrganization from 'sentry/utils/useOrganization';
+import type {ReplayTrace} from 'sentry/views/replays/detail/trace/useReplayTraces';
 
 
 import {useTraceMeta} from './useTraceMeta';
 import {useTraceMeta} from './useTraceMeta';
 
 
 const organization = OrganizationFixture();
 const organization = OrganizationFixture();
 const queryClient = makeTestQueryClient();
 const queryClient = makeTestQueryClient();
 
 
+const mockedReplayTraces: ReplayTrace[] = [
+  {
+    traceSlug: 'slug1',
+    timestamp: 1,
+  },
+  {
+    traceSlug: 'slug2',
+    timestamp: 2,
+  },
+  {
+    traceSlug: 'slug3',
+    timestamp: 3,
+  },
+];
+
 describe('useTraceMeta', () => {
 describe('useTraceMeta', () => {
   beforeEach(function () {
   beforeEach(function () {
     queryClient.clear();
     queryClient.clear();
@@ -19,8 +35,6 @@ describe('useTraceMeta', () => {
   });
   });
 
 
   it('Returns merged metaResults', async () => {
   it('Returns merged metaResults', async () => {
-    const traceSlugs = ['slug1', 'slug2', 'slug3'];
-
     // Mock the API calls
     // Mock the API calls
     MockApiClient.addMockResponse({
     MockApiClient.addMockResponse({
       method: 'GET',
       method: 'GET',
@@ -30,6 +44,7 @@ describe('useTraceMeta', () => {
         performance_issues: 1,
         performance_issues: 1,
         projects: 1,
         projects: 1,
         transactions: 1,
         transactions: 1,
+        transaction_child_count_map: [{'transaction.id': '1', count: 1}],
       },
       },
     });
     });
     MockApiClient.addMockResponse({
     MockApiClient.addMockResponse({
@@ -40,6 +55,7 @@ describe('useTraceMeta', () => {
         performance_issues: 1,
         performance_issues: 1,
         projects: 1,
         projects: 1,
         transactions: 1,
         transactions: 1,
+        transaction_child_count_map: [{'transaction.id': '2', count: 2}],
       },
       },
     });
     });
     MockApiClient.addMockResponse({
     MockApiClient.addMockResponse({
@@ -50,6 +66,7 @@ describe('useTraceMeta', () => {
         performance_issues: 1,
         performance_issues: 1,
         projects: 1,
         projects: 1,
         transactions: 1,
         transactions: 1,
+        transaction_child_count_map: [],
       },
       },
     });
     });
 
 
@@ -57,12 +74,13 @@ describe('useTraceMeta', () => {
       <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
       <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
     );
     );
 
 
-    const {result} = renderHook(() => useTraceMeta(traceSlugs), {wrapper});
+    const {result} = renderHook(() => useTraceMeta(mockedReplayTraces), {wrapper});
 
 
     expect(result.current).toEqual({
     expect(result.current).toEqual({
       data: undefined,
       data: undefined,
       errors: [],
       errors: [],
       isLoading: true,
       isLoading: true,
+      status: 'loading',
     });
     });
 
 
     await waitFor(() => expect(result.current.isLoading).toBe(false));
     await waitFor(() => expect(result.current.isLoading).toBe(false));
@@ -73,15 +91,18 @@ describe('useTraceMeta', () => {
         performance_issues: 3,
         performance_issues: 3,
         projects: 1,
         projects: 1,
         transactions: 3,
         transactions: 3,
+        transactiontoSpanChildrenCount: {
+          '1': 1,
+          '2': 2,
+        },
       },
       },
       errors: [],
       errors: [],
       isLoading: false,
       isLoading: false,
+      status: 'success',
     });
     });
   });
   });
 
 
   it('Collects errors from rejected api calls', async () => {
   it('Collects errors from rejected api calls', async () => {
-    const traceSlugs = ['slug1', 'slug2', 'slug3'];
-
     // Mock the API calls
     // Mock the API calls
     const mockRequest1 = MockApiClient.addMockResponse({
     const mockRequest1 = MockApiClient.addMockResponse({
       method: 'GET',
       method: 'GET',
@@ -103,12 +124,13 @@ describe('useTraceMeta', () => {
       <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
       <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
     );
     );
 
 
-    const {result} = renderHook(() => useTraceMeta(traceSlugs), {wrapper});
+    const {result} = renderHook(() => useTraceMeta(mockedReplayTraces), {wrapper});
 
 
     expect(result.current).toEqual({
     expect(result.current).toEqual({
       data: undefined,
       data: undefined,
       errors: [],
       errors: [],
       isLoading: true,
       isLoading: true,
+      status: 'loading',
     });
     });
 
 
     await waitFor(() => expect(result.current.isLoading).toBe(false));
     await waitFor(() => expect(result.current.isLoading).toBe(false));
@@ -119,9 +141,11 @@ describe('useTraceMeta', () => {
         performance_issues: 0,
         performance_issues: 0,
         projects: 0,
         projects: 0,
         transactions: 0,
         transactions: 0,
+        transactiontoSpanChildrenCount: {},
       },
       },
       errors: [expect.any(Error), expect.any(Error), expect.any(Error)],
       errors: [expect.any(Error), expect.any(Error), expect.any(Error)],
       isLoading: false,
       isLoading: false,
+      status: 'success',
     });
     });
 
 
     expect(mockRequest1).toHaveBeenCalledTimes(1);
     expect(mockRequest1).toHaveBeenCalledTimes(1);
@@ -130,8 +154,6 @@ describe('useTraceMeta', () => {
   });
   });
 
 
   it('Accumulates metaResults and collects errors from rejected api calls', async () => {
   it('Accumulates metaResults and collects errors from rejected api calls', async () => {
-    const traceSlugs = ['slug1', 'slug2', 'slug3'];
-
     // Mock the API calls
     // Mock the API calls
     const mockRequest1 = MockApiClient.addMockResponse({
     const mockRequest1 = MockApiClient.addMockResponse({
       method: 'GET',
       method: 'GET',
@@ -146,6 +168,7 @@ describe('useTraceMeta', () => {
         performance_issues: 1,
         performance_issues: 1,
         projects: 1,
         projects: 1,
         transactions: 1,
         transactions: 1,
+        transaction_child_count_map: [],
       },
       },
     });
     });
     const mockRequest3 = MockApiClient.addMockResponse({
     const mockRequest3 = MockApiClient.addMockResponse({
@@ -156,6 +179,7 @@ describe('useTraceMeta', () => {
         performance_issues: 1,
         performance_issues: 1,
         projects: 1,
         projects: 1,
         transactions: 1,
         transactions: 1,
+        transaction_child_count_map: [],
       },
       },
     });
     });
 
 
@@ -163,12 +187,13 @@ describe('useTraceMeta', () => {
       <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
       <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
     );
     );
 
 
-    const {result} = renderHook(() => useTraceMeta(traceSlugs), {wrapper});
+    const {result} = renderHook(() => useTraceMeta(mockedReplayTraces), {wrapper});
 
 
     expect(result.current).toEqual({
     expect(result.current).toEqual({
       data: undefined,
       data: undefined,
       errors: [],
       errors: [],
       isLoading: true,
       isLoading: true,
+      status: 'loading',
     });
     });
 
 
     await waitFor(() => expect(result.current.isLoading).toBe(false));
     await waitFor(() => expect(result.current.isLoading).toBe(false));
@@ -179,9 +204,11 @@ describe('useTraceMeta', () => {
         performance_issues: 2,
         performance_issues: 2,
         projects: 1,
         projects: 1,
         transactions: 2,
         transactions: 2,
+        transactiontoSpanChildrenCount: {},
       },
       },
       errors: [expect.any(Error)],
       errors: [expect.any(Error)],
       isLoading: false,
       isLoading: false,
+      status: 'success',
     });
     });
 
 
     expect(mockRequest1).toHaveBeenCalledTimes(1);
     expect(mockRequest1).toHaveBeenCalledTimes(1);

+ 64 - 34
static/app/views/performance/newTraceDetails/traceApi/useTraceMeta.tsx

@@ -1,6 +1,5 @@
 import {useMemo} from 'react';
 import {useMemo} from 'react';
 import {useQuery} from '@tanstack/react-query';
 import {useQuery} from '@tanstack/react-query';
-import type {Location} from 'history';
 import * as qs from 'query-string';
 import * as qs from 'query-string';
 
 
 import type {Client} from 'sentry/api';
 import type {Client} from 'sentry/api';
@@ -9,52 +8,47 @@ import {DEFAULT_STATS_PERIOD} from 'sentry/constants';
 import type {PageFilters} from 'sentry/types/core';
 import type {PageFilters} from 'sentry/types/core';
 import type {Organization} from 'sentry/types/organization';
 import type {Organization} from 'sentry/types/organization';
 import type {TraceMeta} from 'sentry/utils/performance/quickTrace/types';
 import type {TraceMeta} from 'sentry/utils/performance/quickTrace/types';
+import type {QueryStatus} from 'sentry/utils/queryClient';
 import {decodeScalar} from 'sentry/utils/queryString';
 import {decodeScalar} from 'sentry/utils/queryString';
 import useApi from 'sentry/utils/useApi';
 import useApi from 'sentry/utils/useApi';
 import useOrganization from 'sentry/utils/useOrganization';
 import useOrganization from 'sentry/utils/useOrganization';
 import usePageFilters from 'sentry/utils/usePageFilters';
 import usePageFilters from 'sentry/utils/usePageFilters';
+import type {ReplayTrace} from 'sentry/views/replays/detail/trace/useReplayTraces';
 
 
 type TraceMetaQueryParams =
 type TraceMetaQueryParams =
   | {
   | {
       // demo has the format ${projectSlug}:${eventId}
       // demo has the format ${projectSlug}:${eventId}
       // used to query a demo transaction event from the backend.
       // used to query a demo transaction event from the backend.
-      demo: string | undefined;
       statsPeriod: string;
       statsPeriod: string;
     }
     }
   | {
   | {
-      demo: string | undefined;
-      timestamp: string;
+      timestamp: number;
     };
     };
 
 
 function getMetaQueryParams(
 function getMetaQueryParams(
-  query: Location['query'],
+  row: ReplayTrace,
+  normalizedParams: any,
   filters: Partial<PageFilters> = {}
   filters: Partial<PageFilters> = {}
 ): TraceMetaQueryParams {
 ): TraceMetaQueryParams {
-  const normalizedParams = normalizeDateTimeParams(query, {
-    allowAbsolutePageDatetime: true,
-  });
-
   const statsPeriod = decodeScalar(normalizedParams.statsPeriod);
   const statsPeriod = decodeScalar(normalizedParams.statsPeriod);
-  const timestamp = decodeScalar(normalizedParams.timestamp);
 
 
-  if (timestamp) {
-    return {timestamp, demo: decodeScalar(normalizedParams.demo)};
+  if (row.timestamp) {
+    return {timestamp: row.timestamp};
   }
   }
 
 
   return {
   return {
     statsPeriod: (statsPeriod || filters?.datetime?.period) ?? DEFAULT_STATS_PERIOD,
     statsPeriod: (statsPeriod || filters?.datetime?.period) ?? DEFAULT_STATS_PERIOD,
-    demo: decodeScalar(normalizedParams.demo),
   };
   };
 }
 }
 
 
 async function fetchSingleTraceMetaNew(
 async function fetchSingleTraceMetaNew(
   api: Client,
   api: Client,
   organization: Organization,
   organization: Organization,
-  traceSlug: string,
+  replayTrace: ReplayTrace,
   queryParams: any
   queryParams: any
 ) {
 ) {
   const data = await api.requestPromise(
   const data = await api.requestPromise(
-    `/organizations/${organization.slug}/events-trace-meta/${traceSlug}/`,
+    `/organizations/${organization.slug}/events-trace-meta/${replayTrace.traceSlug}/`,
     {
     {
       method: 'GET',
       method: 'GET',
       data: queryParams,
       data: queryParams,
@@ -64,17 +58,19 @@ async function fetchSingleTraceMetaNew(
 }
 }
 
 
 async function fetchTraceMetaInBatches(
 async function fetchTraceMetaInBatches(
-  traceIds: string[],
   api: Client,
   api: Client,
   organization: Organization,
   organization: Organization,
-  queryParams: TraceMetaQueryParams
+  replayTraces: ReplayTrace[],
+  normalizedParams: any,
+  filters: Partial<PageFilters> = {}
 ) {
 ) {
-  const clonedTraceIds = [...traceIds];
+  const clonedTraceIds = [...replayTraces];
   const metaResults: TraceMeta = {
   const metaResults: TraceMeta = {
     errors: 0,
     errors: 0,
     performance_issues: 0,
     performance_issues: 0,
     projects: 0,
     projects: 0,
     transactions: 0,
     transactions: 0,
+    transactiontoSpanChildrenCount: {},
   };
   };
 
 
   const apiErrors: Error[] = [];
   const apiErrors: Error[] = [];
@@ -82,19 +78,32 @@ async function fetchTraceMetaInBatches(
   while (clonedTraceIds.length > 0) {
   while (clonedTraceIds.length > 0) {
     const batch = clonedTraceIds.splice(0, 3);
     const batch = clonedTraceIds.splice(0, 3);
     const results = await Promise.allSettled(
     const results = await Promise.allSettled(
-      batch.map(slug => {
-        return fetchSingleTraceMetaNew(api, organization, slug, queryParams);
+      batch.map(replayTrace => {
+        const queryParams = getMetaQueryParams(replayTrace, normalizedParams, filters);
+        return fetchSingleTraceMetaNew(api, organization, replayTrace, queryParams);
       })
       })
     );
     );
 
 
     const updatedData = results.reduce(
     const updatedData = results.reduce(
       (acc, result) => {
       (acc, result) => {
         if (result.status === 'fulfilled') {
         if (result.status === 'fulfilled') {
-          const {errors, performance_issues, projects, transactions} = result.value;
+          const {
+            errors,
+            performance_issues,
+            projects,
+            transactions,
+            transaction_child_count_map,
+          } = result.value;
           acc.errors += errors;
           acc.errors += errors;
           acc.performance_issues += performance_issues;
           acc.performance_issues += performance_issues;
           acc.projects = Math.max(acc.projects, projects);
           acc.projects = Math.max(acc.projects, projects);
           acc.transactions += transactions;
           acc.transactions += transactions;
+
+          // Turn the transaction_child_count_map array into a map of transaction id to child count
+          // for more efficient lookups.
+          transaction_child_count_map.forEach(({'transaction.id': id, count}) => {
+            acc.transactiontoSpanChildrenCount[id] = count;
+          });
         } else {
         } else {
           apiErrors.push(new Error(result.reason));
           apiErrors.push(new Error(result.reason));
         }
         }
@@ -107,6 +116,8 @@ async function fetchTraceMetaInBatches(
     metaResults.performance_issues = updatedData.performance_issues;
     metaResults.performance_issues = updatedData.performance_issues;
     metaResults.projects = Math.max(updatedData.projects, metaResults.projects);
     metaResults.projects = Math.max(updatedData.projects, metaResults.projects);
     metaResults.transactions = updatedData.transactions;
     metaResults.transactions = updatedData.transactions;
+    metaResults.transactiontoSpanChildrenCount =
+      updatedData.transactiontoSpanChildrenCount;
   }
   }
 
 
   return {metaResults, apiErrors};
   return {metaResults, apiErrors};
@@ -116,33 +127,54 @@ export type TraceMetaQueryResults = {
   data: TraceMeta | undefined;
   data: TraceMeta | undefined;
   errors: Error[];
   errors: Error[];
   isLoading: boolean;
   isLoading: boolean;
+  status: QueryStatus;
 };
 };
 
 
-export function useTraceMeta(traceSlugs: string[]): TraceMetaQueryResults {
+export function useTraceMeta(replayTraces: ReplayTrace[]): TraceMetaQueryResults {
   const filters = usePageFilters();
   const filters = usePageFilters();
   const api = useApi();
   const api = useApi();
   const organization = useOrganization();
   const organization = useOrganization();
 
 
-  const queryParams = useMemo(() => {
+  const normalizedParams = useMemo(() => {
     const query = qs.parse(location.search);
     const query = qs.parse(location.search);
-    return getMetaQueryParams(query, filters.selection);
+    return normalizeDateTimeParams(query, {
+      allowAbsolutePageDatetime: true,
+    });
     // eslint-disable-next-line react-hooks/exhaustive-deps
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
   }, []);
 
 
-  const mode = queryParams.demo ? 'demo' : undefined;
+  // demo has the format ${projectSlug}:${eventId}
+  // used to query a demo transaction event from the backend.
+  const mode = decodeScalar(normalizedParams.demo) ? 'demo' : undefined;
 
 
-  const {data, isLoading} = useQuery<
+  const {data, isLoading, status} = useQuery<
     {
     {
       apiErrors: Error[];
       apiErrors: Error[];
       metaResults: TraceMeta;
       metaResults: TraceMeta;
     },
     },
     Error
     Error
   >({
   >({
-    queryKey: ['traceData', traceSlugs],
-    queryFn: () => fetchTraceMetaInBatches(traceSlugs, api, organization, queryParams),
-    enabled: traceSlugs.length > 0,
+    queryKey: ['traceData', replayTraces],
+    queryFn: () =>
+      fetchTraceMetaInBatches(
+        api,
+        organization,
+        replayTraces,
+        normalizedParams,
+        filters.selection
+      ),
+    enabled: replayTraces.length > 0,
   });
   });
 
 
+  const results = useMemo(() => {
+    return {
+      data: data?.metaResults,
+      errors: data?.apiErrors || [],
+      isLoading,
+      status,
+    };
+  }, [data, isLoading, status]);
+
   // When projects don't have performance set up, we allow them to view a sample transaction.
   // When projects don't have performance set up, we allow them to view a sample transaction.
   // The backend creates the sample transaction, however the trace is created async, so when the
   // The backend creates the sample transaction, however the trace is created async, so when the
   // page loads, we cannot guarantee that querying the trace will succeed as it may not have been stored yet.
   // page loads, we cannot guarantee that querying the trace will succeed as it may not have been stored yet.
@@ -156,15 +188,13 @@ export function useTraceMeta(traceSlugs: string[]): TraceMetaQueryResults {
         performance_issues: 0,
         performance_issues: 0,
         projects: 1,
         projects: 1,
         transactions: 1,
         transactions: 1,
+        transactiontoSpanChildrenCount: {},
       },
       },
       isLoading: false,
       isLoading: false,
       errors: [],
       errors: [],
+      status: 'success',
     };
     };
   }
   }
 
 
-  return {
-    data: data?.metaResults,
-    isLoading,
-    errors: data?.apiErrors || [],
-  };
+  return results;
 }
 }

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

@@ -60,7 +60,7 @@ function parsePlatform(platform: string): ParsedPlatform {
   return {platformName, framework};
   return {platformName, framework};
 }
 }
 
 
-function getCustomInstrumentationLink(project: Project | undefined): string {
+export function getCustomInstrumentationLink(project: Project | undefined): string {
   // Default to JavaScript guide if project or platform is not available
   // Default to JavaScript guide if project or platform is not available
   if (!project || !project.platform) {
   if (!project || !project.platform) {
     return `https://docs.sentry.io/platforms/javascript/tracing/instrumentation/custom-instrumentation/`;
     return `https://docs.sentry.io/platforms/javascript/tracing/instrumentation/custom-instrumentation/`;

+ 0 - 67
static/app/views/performance/newTraceDetails/traceDrawer/details/noData.tsx

@@ -1,67 +0,0 @@
-import {useRef} from 'react';
-import {useTheme} from '@emotion/react';
-
-import useFeedbackWidget from 'sentry/components/feedback/widget/useFeedbackWidget';
-import {IconGroup} from 'sentry/icons';
-import {t, tct} from 'sentry/locale';
-import {
-  type SectionCardKeyValueList,
-  TraceDrawerComponents,
-} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/styles';
-import type {TraceTreeNodeDetailsProps} from 'sentry/views/performance/newTraceDetails/traceDrawer/tabs/traceTreeNodeDetails';
-import {
-  makeTraceNodeBarColor,
-  type NoDataNode,
-} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
-
-export function NoDataDetails(props: TraceTreeNodeDetailsProps<NoDataNode>) {
-  const theme = useTheme();
-
-  const items: SectionCardKeyValueList = [
-    {
-      key: 'data_quality',
-      subject: t('Data quality'),
-      value: tct(
-        'The cause of missing data could be misconfiguration or lack of instrumentation. Send us [feedback] if you are having trouble figuring this out.',
-        {feedback: <InlineFeedbackLink />}
-      ),
-    },
-  ];
-
-  return (
-    <TraceDrawerComponents.DetailContainer>
-      <TraceDrawerComponents.HeaderContainer>
-        <TraceDrawerComponents.IconTitleWrapper>
-          <TraceDrawerComponents.IconBorder
-            backgroundColor={makeTraceNodeBarColor(theme, props.node)}
-          >
-            <IconGroup />
-          </TraceDrawerComponents.IconBorder>
-          <div style={{fontWeight: 'bold'}}>{t('Empty')}</div>
-        </TraceDrawerComponents.IconTitleWrapper>
-
-        <TraceDrawerComponents.NodeActions
-          organization={props.organization}
-          node={props.node}
-          onTabScrollToNode={props.onTabScrollToNode}
-        />
-      </TraceDrawerComponents.HeaderContainer>
-
-      <TraceDrawerComponents.SectionCard items={items} title={t('General')} />
-    </TraceDrawerComponents.DetailContainer>
-  );
-}
-
-function InlineFeedbackLink() {
-  const linkref = useRef<HTMLAnchorElement>(null);
-  const feedback = useFeedbackWidget({buttonRef: linkref});
-  return feedback ? (
-    <a href="#" ref={linkref}>
-      {t('feedback')}
-    </a>
-  ) : (
-    <a href="mailto:support@sentry.io?subject=Trace%20does%20not%20contain%20data">
-      {t('feedback')}
-    </a>
-  );
-}

Некоторые файлы не были показаны из-за большого количества измененных файлов