Browse Source

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 months ago
parent
commit
07be1fbed4

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

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

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

@@ -1,6 +1,5 @@
 import {
   MissingInstrumentationNode,
-  NoDataNode,
   ParentAutogroupNode,
   SiblingAutogroupNode,
   type TraceTree,
@@ -55,7 +54,7 @@ export function isTraceErrorNode(
 export function isRootNode(
   node: TraceTreeNode<TraceTree.NodeValue>
 ): node is TraceTreeNode<null> {
-  return node.value === null && !(node instanceof NoDataNode);
+  return node.value === null;
 }
 
 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 {
   if (!sdk) 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,
   requestAnimationTimeout,
 } 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 type RequestError from 'sentry/utils/requestError/requestError';
 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() {
   const params = useParams<{traceSlug?: string}>();
   const organization = useOrganization();
@@ -185,7 +200,7 @@ export function TraceView() {
     });
   }, [queryParams, traceSlug]);
 
-  const meta = useTraceMeta([traceSlug]);
+  const meta = useTraceMeta([{traceSlug, timestamp: queryParams.timestamp}]);
 
   const preferences = useMemo(
     () =>
@@ -218,7 +233,7 @@ export function TraceView() {
               <TraceViewWaterfall
                 traceSlug={traceSlug}
                 trace={trace.data ?? null}
-                status={trace.status}
+                status={getTraceViewQueryStatus(trace.status, meta.status)}
                 organization={organization}
                 rootEvent={rootEvent}
                 traceEventView={traceEventView}
@@ -346,8 +361,12 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
       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
       const promises: Promise<void>[] = [];
@@ -367,6 +386,7 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
     props.traceSlug,
     props.trace,
     props.status,
+    props.metaResults,
     props.replayRecord,
     projects,
     api,
@@ -385,6 +405,7 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) {
       organization: props.organization,
       urlParams: qs.parse(location.search),
       rerender: forceRerender,
+      metaResults: props.metaResults,
     });
 
     return () => cleanup();

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

@@ -79,6 +79,7 @@ function mockTraceMetaResponse(resp?: Partial<ResponseType>) {
         performance_issues: 0,
         projects: 0,
         transactions: 0,
+        transaction_child_count_map: [],
       },
     }),
   });
@@ -261,7 +262,18 @@ async function keyboardNavigationTestSetup() {
       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();
   mockTraceRootEvent('0');
   mockTraceEventDetails();
@@ -1082,7 +1094,6 @@ describe('trace view', () => {
       });
     });
     it('during search, expanding a row retriggers search', async () => {
-      mockTraceMetaResponse();
       mockTraceRootFacets();
       mockTraceRootEvent('0');
       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(
         '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 Placeholder from 'sentry/components/placeholder';
-import {t, tct} from 'sentry/locale';
+import {t} from 'sentry/locale';
 import ConfigStore from 'sentry/stores/configStore';
 import {space} from 'sentry/styles/space';
 import type {Organization} from 'sentry/types/organization';
@@ -56,7 +56,6 @@ import {useTraceState, useTraceStateDispatch} from './traceState/traceStateProvi
 import {
   isAutogroupedNode,
   isMissingInstrumentationNode,
-  isNoDataNode,
   isParentAutogroupedNode,
   isSpanNode,
   isTraceErrorNode,
@@ -791,12 +790,18 @@ function RenderRow(props: {
               {props.node.children.length > 0 || props.node.canFetch ? (
                 <ChildrenButton
                   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}
@@ -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;
 }
 

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

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

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

@@ -1,6 +1,5 @@
 import {useMemo} from 'react';
 import {useQuery} from '@tanstack/react-query';
-import type {Location} from 'history';
 import * as qs from 'query-string';
 
 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 {Organization} from 'sentry/types/organization';
 import type {TraceMeta} from 'sentry/utils/performance/quickTrace/types';
+import type {QueryStatus} from 'sentry/utils/queryClient';
 import {decodeScalar} from 'sentry/utils/queryString';
 import useApi from 'sentry/utils/useApi';
 import useOrganization from 'sentry/utils/useOrganization';
 import usePageFilters from 'sentry/utils/usePageFilters';
+import type {ReplayTrace} from 'sentry/views/replays/detail/trace/useReplayTraces';
 
 type TraceMetaQueryParams =
   | {
       // demo has the format ${projectSlug}:${eventId}
       // used to query a demo transaction event from the backend.
-      demo: string | undefined;
       statsPeriod: string;
     }
   | {
-      demo: string | undefined;
-      timestamp: string;
+      timestamp: number;
     };
 
 function getMetaQueryParams(
-  query: Location['query'],
+  row: ReplayTrace,
+  normalizedParams: any,
   filters: Partial<PageFilters> = {}
 ): TraceMetaQueryParams {
-  const normalizedParams = normalizeDateTimeParams(query, {
-    allowAbsolutePageDatetime: true,
-  });
-
   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 {
     statsPeriod: (statsPeriod || filters?.datetime?.period) ?? DEFAULT_STATS_PERIOD,
-    demo: decodeScalar(normalizedParams.demo),
   };
 }
 
 async function fetchSingleTraceMetaNew(
   api: Client,
   organization: Organization,
-  traceSlug: string,
+  replayTrace: ReplayTrace,
   queryParams: any
 ) {
   const data = await api.requestPromise(
-    `/organizations/${organization.slug}/events-trace-meta/${traceSlug}/`,
+    `/organizations/${organization.slug}/events-trace-meta/${replayTrace.traceSlug}/`,
     {
       method: 'GET',
       data: queryParams,
@@ -64,17 +58,19 @@ async function fetchSingleTraceMetaNew(
 }
 
 async function fetchTraceMetaInBatches(
-  traceIds: string[],
   api: Client,
   organization: Organization,
-  queryParams: TraceMetaQueryParams
+  replayTraces: ReplayTrace[],
+  normalizedParams: any,
+  filters: Partial<PageFilters> = {}
 ) {
-  const clonedTraceIds = [...traceIds];
+  const clonedTraceIds = [...replayTraces];
   const metaResults: TraceMeta = {
     errors: 0,
     performance_issues: 0,
     projects: 0,
     transactions: 0,
+    transactiontoSpanChildrenCount: {},
   };
 
   const apiErrors: Error[] = [];
@@ -82,19 +78,32 @@ async function fetchTraceMetaInBatches(
   while (clonedTraceIds.length > 0) {
     const batch = clonedTraceIds.splice(0, 3);
     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(
       (acc, result) => {
         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.performance_issues += performance_issues;
           acc.projects = Math.max(acc.projects, projects);
           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 {
           apiErrors.push(new Error(result.reason));
         }
@@ -107,6 +116,8 @@ async function fetchTraceMetaInBatches(
     metaResults.performance_issues = updatedData.performance_issues;
     metaResults.projects = Math.max(updatedData.projects, metaResults.projects);
     metaResults.transactions = updatedData.transactions;
+    metaResults.transactiontoSpanChildrenCount =
+      updatedData.transactiontoSpanChildrenCount;
   }
 
   return {metaResults, apiErrors};
@@ -116,33 +127,54 @@ export type TraceMetaQueryResults = {
   data: TraceMeta | undefined;
   errors: Error[];
   isLoading: boolean;
+  status: QueryStatus;
 };
 
-export function useTraceMeta(traceSlugs: string[]): TraceMetaQueryResults {
+export function useTraceMeta(replayTraces: ReplayTrace[]): TraceMetaQueryResults {
   const filters = usePageFilters();
   const api = useApi();
   const organization = useOrganization();
 
-  const queryParams = useMemo(() => {
+  const normalizedParams = useMemo(() => {
     const query = qs.parse(location.search);
-    return getMetaQueryParams(query, filters.selection);
+    return normalizeDateTimeParams(query, {
+      allowAbsolutePageDatetime: true,
+    });
     // 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[];
       metaResults: TraceMeta;
     },
     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.
   // 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.
@@ -156,15 +188,13 @@ export function useTraceMeta(traceSlugs: string[]): TraceMetaQueryResults {
         performance_issues: 0,
         projects: 1,
         transactions: 1,
+        transactiontoSpanChildrenCount: {},
       },
       isLoading: false,
       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};
 }
 
-function getCustomInstrumentationLink(project: Project | undefined): string {
+export function getCustomInstrumentationLink(project: Project | undefined): string {
   // Default to JavaScript guide if project or platform is not available
   if (!project || !project.platform) {
     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>
-  );
-}

Some files were not shown because too many files changed in this diff