Browse Source

feat(profiling)fetch function metrics when expanding the function in the slowest function list (#76259)

Add the data fetching hook and expand/close functionality
Jonas 6 months ago
parent
commit
3bbc1d5913

+ 41 - 0
static/app/utils/profiling/hooks/useProfilingFunctionMetrics.tsx

@@ -0,0 +1,41 @@
+import {useMemo} from 'react';
+
+import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
+import type {EventsStatsData} from 'sentry/types/organization';
+import {useApiQuery, type UseApiQueryResult} from 'sentry/utils/queryClient';
+import type RequestError from 'sentry/utils/requestError/requestError';
+import useOrganization from 'sentry/utils/useOrganization';
+import usePageFilters from 'sentry/utils/usePageFilters';
+
+interface UseProfilingFunctionMetricsProps {
+  fingerprint: Profiling.FunctionMetric['fingerprint'];
+  projects: number[];
+}
+
+export function useProfilingFunctionMetrics(
+  props: UseProfilingFunctionMetricsProps
+): UseApiQueryResult<EventsStatsData, RequestError> {
+  const organization = useOrganization();
+  const {selection} = usePageFilters();
+
+  const path = `/organizations/${organization.slug}/events-stats/`;
+  const endpointOptions = useMemo(() => {
+    const params: {
+      query: Record<string, any>;
+    } = {
+      query: {
+        project: props.projects,
+        environment: selection.environments ?? selection.environments,
+        dataset: 'profileFunctionsMetrics',
+        query: `fingerprint:${props.fingerprint}`,
+        ...normalizeDateTimeParams(selection.datetime ?? selection.datetime),
+      },
+    };
+    return params;
+  }, [props.fingerprint, props.projects, selection.datetime, selection.environments]);
+
+  return useApiQuery<EventsStatsData>([path, endpointOptions], {
+    enabled: !!props.fingerprint,
+    staleTime: 0,
+  });
+}

+ 60 - 1
static/app/views/profiling/landing/slowestFunctionsTable.spec.tsx

@@ -1,4 +1,4 @@
-import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
+import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
 
 import {SlowestFunctionsTable} from 'sentry/views/profiling/landing/slowestFunctionsTable';
 
@@ -129,6 +129,7 @@ describe('SlowestFunctionsTable', () => {
     });
 
     render(<SlowestFunctionsTable />);
+
     expect(await screen.findAllByText('slow-function')).toHaveLength(5);
   });
 
@@ -184,4 +185,62 @@ describe('SlowestFunctionsTable', () => {
     }
     expect(screen.getByLabelText('Previous')).toBeDisabled();
   });
+  it('fetches function metrics', async () => {
+    // @ts-expect-error partial schema mock
+    const schema: Profiling.Schema = {
+      metrics: [],
+    };
+
+    for (let i = 0; i < 10; i++) {
+      schema.metrics?.push({
+        name: 'slow-function-' + i,
+        package: 'slow-package',
+        p75: 1500 * 1e6,
+        p95: 2000 * 1e6,
+        p99: 3000 * 1e6,
+        sum: 60_000 * 1e6,
+        count: 5000,
+        avg: 0.5 * 1e6,
+        in_app: true,
+        fingerprint: 12345,
+        examples: [
+          {
+            project_id: 1,
+            profile_id: 'profile-id',
+          },
+        ],
+      });
+    }
+
+    MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/profiling/flamegraph/',
+      match: [
+        MockApiClient.matchQuery({
+          expand: 'metrics',
+        }),
+      ],
+      body: schema,
+    });
+
+    const functionMetricsRequest = MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/events-stats/',
+      match: [
+        MockApiClient.matchQuery({
+          query: 'fingerprint:12345',
+          dataset: 'profileFunctionsMetrics',
+        }),
+      ],
+      body: [],
+    });
+
+    render(<SlowestFunctionsTable />);
+
+    const expandButtons = await screen.findAllByLabelText('View Function Metrics');
+    expect(expandButtons).toHaveLength(5);
+
+    await userEvent.click(expandButtons[0]);
+    await waitFor(() => {
+      expect(functionMetricsRequest).toHaveBeenCalled();
+    });
+  });
 });

+ 52 - 3
static/app/views/profiling/landing/slowestFunctionsTable.tsx

@@ -16,6 +16,7 @@ import {space} from 'sentry/styles/space';
 import type {Project} from 'sentry/types/project';
 import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
 import {useAggregateFlamegraphQuery} from 'sentry/utils/profiling/hooks/useAggregateFlamegraphQuery';
+import {useProfilingFunctionMetrics} from 'sentry/utils/profiling/hooks/useProfilingFunctionMetrics';
 import {generateProfileRouteFromProfileReference} from 'sentry/utils/profiling/routes';
 import useOrganization from 'sentry/utils/useOrganization';
 import useProjects from 'sentry/utils/useProjects';
@@ -73,6 +74,9 @@ export function SlowestFunctionsTable() {
   }, [query.data?.metrics]);
 
   const pagination = useMemoryPagination(sortedMetrics, 5);
+  const [expandedFingerprint, setExpandedFingerprint] = useState<
+    Profiling.FunctionMetric['fingerprint'] | null
+  >(null);
 
   const projectsLookupTable = useMemo(() => {
     return projects.reduce(
@@ -92,7 +96,7 @@ export function SlowestFunctionsTable() {
         <ContentContainer>
           {query.isLoading && (
             <StatusContainer>
-              <LoadingIndicator />
+              <LoadingIndicator size={36} />
             </StatusContainer>
           )}
           {query.isError && (
@@ -118,6 +122,7 @@ export function SlowestFunctionsTable() {
                   <SlowestFunctionCell>{t('p99()')}</SlowestFunctionCell>
                   {/* @TODO remove sum before relasing */}
                   <SlowestFunctionCell>{t('Sum()')}</SlowestFunctionCell>
+                  <SlowestFunctionCell />
                 </SlowestFunctionHeader>
                 {sortedMetrics.slice(pagination.start, pagination.end).map((f, i) => {
                   return (
@@ -125,6 +130,8 @@ export function SlowestFunctionsTable() {
                       key={i}
                       function={f}
                       projectsLookupTable={projectsLookupTable}
+                      expanded={f.fingerprint === expandedFingerprint}
+                      onExpandClick={setExpandedFingerprint}
                     />
                   );
                 })}
@@ -154,7 +161,11 @@ export function SlowestFunctionsTable() {
 }
 
 interface SlowestFunctionProps {
+  expanded: boolean;
   function: NonNullable<Profiling.Schema['metrics']>[0];
+  onExpandClick: React.Dispatch<
+    React.SetStateAction<Profiling.FunctionMetric['fingerprint'] | null>
+  >;
   projectsLookupTable: Record<string, Project>;
 }
 
@@ -211,14 +222,24 @@ function SlowestFunction(props: SlowestFunctionProps) {
         {/* @TODO remove sum before relasing */}
         {getPerformanceDuration(props.function.sum / 1e6)}
       </SlowestFunctionCell>
+      <SlowestFunctionCell>
+        <Button
+          icon={<IconChevron direction={props.expanded ? 'up' : 'down'} />}
+          aria-label={t('View Function Metrics')}
+          onClick={() => props.onExpandClick(props.function.fingerprint)}
+          size="xs"
+        />
+      </SlowestFunctionCell>
+      {props.expanded ? <SlowestFunctionTimeSeries function={props.function} /> : null}
     </SlowestFunctionContainer>
   );
 }
 
 interface SlowestFunctionsProjectBadgeProps {
-  examples: NonNullable<Profiling.Schema['metrics']>[0]['examples'];
+  examples: Profiling.FunctionMetric['examples'];
   projectsLookupTable: Record<string, Project>;
 }
+
 function SlowestFunctionsProjectBadge(props: SlowestFunctionsProjectBadgeProps) {
   const resolvedProjects = useMemo(() => {
     const projects: Project[] = [];
@@ -238,6 +259,34 @@ function SlowestFunctionsProjectBadge(props: SlowestFunctionsProjectBadgeProps)
   ) : null;
 }
 
+interface SlowestFunctionTimeSeriesProps {
+  function: Profiling.FunctionMetric;
+}
+
+function SlowestFunctionTimeSeries(props: SlowestFunctionTimeSeriesProps) {
+  const projects = useMemo(() => {
+    const projectsMap = props.function.examples.reduce<Record<string, number>>(
+      (acc, f) => {
+        if (typeof f !== 'string' && 'project_id' in f) {
+          acc[f.project_id] = f.project_id;
+        }
+        return acc;
+      },
+      {}
+    );
+
+    return Object.values(projectsMap);
+  }, [props.function]);
+
+  useProfilingFunctionMetrics({
+    fingerprint: props.function.fingerprint,
+    projects,
+  });
+
+  // @TODO add chart
+  return null;
+}
+
 const SlowestFunctionsPaginationContainer = styled('div')`
   display: flex;
   justify-content: flex-end;
@@ -270,7 +319,7 @@ const SlowestFunctionHeader = styled('div')`
 const SlowestFunctionsContainer = styled('div')`
   display: grid;
   grid-template-columns:
-    minmax(90px, auto) minmax(90px, auto) minmax(40px, 140px) min-content
+    minmax(90px, auto) minmax(90px, auto) minmax(40px, 140px) min-content min-content
     min-content min-content min-content min-content;
   border-collapse: collapse;
 `;