Browse Source

feat(profiling): Make each point on scatter plot clickable (#32047)

The scatter plot should also serve as a method of navigation. This change allows
clicking on a data point on the scatter plot in order to navigate to the
corresponding flamegraph.
Tony Xiao 3 years ago
parent
commit
d7a0ab5051

+ 1 - 0
.github/CODEOWNERS

@@ -178,6 +178,7 @@ build-utils/        @getsentry/owners-js-build
 /static/app/types/jsSelfProfiling.d.ts                              @getsentry/profiling
 /static/app/types/profiling.d.ts                                    @getsentry/profiling
 /static/app/utils/profiling                                         @getsentry/profiling
+/static/app/views/profiling                                         @getsentry/profiling
 /tests/js/spec/utils/profiling                                      @getsentry/profiling
 /src/sentry/utils/profiling.py                                      @getsentry/profiling
 /src/sentry/api/endpoints/project_profiling_profile.py              @getsentry/profiling

+ 73 - 23
static/app/views/profiling/landing/profilingScatterChart.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import {useCallback, useMemo} from 'react';
 import {browserHistory, withRouter, WithRouterProps} from 'react-router';
 import {useTheme} from '@emotion/react';
 import {Location} from 'history';
@@ -16,11 +16,16 @@ import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingM
 import {getSeriesSelection} from 'sentry/components/charts/utils';
 import {Panel} from 'sentry/components/panels';
 import {t} from 'sentry/locale';
-import {Series, SeriesDataUnit} from 'sentry/types/echarts';
+import {Organization, Project} from 'sentry/types';
+import {Series} from 'sentry/types/echarts';
 import {Trace} from 'sentry/types/profiling/core';
+import {defined} from 'sentry/utils';
 import {axisLabelFormatter, tooltipFormatter} from 'sentry/utils/discover/charts';
 import {Theme} from 'sentry/utils/theme';
+import useOrganization from 'sentry/utils/useOrganization';
+import useProjects from 'sentry/utils/useProjects';
 
+import {generateFlamegraphRoute} from '../routes';
 import {COLOR_ENCODINGS, getColorEncodingFromLocation} from '../utils';
 
 interface ProfilingScatterChartProps extends WithRouterProps {
@@ -43,36 +48,49 @@ function ProfilingScatterChart({
   statsPeriod,
   utc,
 }: ProfilingScatterChartProps) {
+  const organization = useOrganization();
+  const {projects} = useProjects();
   const theme = useTheme();
 
-  const colorEncoding = React.useMemo(
-    () => getColorEncodingFromLocation(location),
-    [location]
-  );
-
-  const series: Series[] = React.useMemo(() => {
-    const seriesMap: Record<string, SeriesDataUnit[]> = {};
+  const colorEncoding = useMemo(() => getColorEncodingFromLocation(location), [location]);
 
+  const data: Record<string, Trace[]> = useMemo(() => {
+    const dataMap = {};
     for (const row of traces) {
       const seriesName = row[colorEncoding];
-      if (!seriesMap[seriesName]) {
-        seriesMap[seriesName] = [];
+      if (!dataMap[seriesName]) {
+        dataMap[seriesName] = [];
       }
-      seriesMap[seriesName].push({
-        name: row.start_time_unix * 1000,
-        value: row.trace_duration_ms,
-      });
+      dataMap[seriesName].push(row);
     }
-
-    return Object.entries(seriesMap).map(([seriesName, data]) => ({seriesName, data}));
+    return dataMap;
   }, [colorEncoding, traces]);
 
-  const chartOptions = React.useMemo(
-    () => makeScatterChartOptions({location, theme}),
-    [location, theme]
+  const series: Series[] = useMemo(() => {
+    return Object.entries(data).map(([seriesName, seriesData]) => {
+      return {
+        seriesName,
+        data: seriesData.map(row => ({
+          name: row.start_time_unix * 1000,
+          value: row.trace_duration_ms,
+        })),
+      };
+    });
+  }, [data]);
+
+  const chartOptions = useMemo(
+    () =>
+      makeScatterChartOptions({
+        data,
+        location,
+        organization,
+        projects,
+        theme,
+      }),
+    [location, theme, data]
   );
 
-  const handleColorEncodingChange = React.useCallback(
+  const handleColorEncodingChange = useCallback(
     value => {
       browserHistory.push({
         ...location,
@@ -119,7 +137,23 @@ function ProfilingScatterChart({
   );
 }
 
-function makeScatterChartOptions({location, theme}: {location: Location; theme: Theme}) {
+function makeScatterChartOptions({
+  data,
+  location,
+  organization,
+  projects,
+  theme,
+}: {
+  /**
+   * The data is a mapping from the series name to a list of traces in the series. In particular,
+   * the order of the traces must match the order of the data in the series in the scatter plot.
+   */
+  data: Record<string, Trace[]>;
+  location: Location;
+  organization: Organization;
+  projects: Project[];
+  theme: Theme;
+}) {
   return {
     grid: {
       left: '10px',
@@ -142,7 +176,23 @@ function makeScatterChartOptions({location, theme}: {location: Location; theme:
       top: 5,
       selected: getSeriesSelection(location),
     },
-    onClick: _params => {}, // TODO
+    onClick: params => {
+      const dataPoint = data[params.seriesName]?.[params.dataIndex];
+      if (!defined(dataPoint)) {
+        return;
+      }
+      const project = projects.find(proj => proj.id === dataPoint.app_id);
+      if (!defined(project)) {
+        return;
+      }
+      browserHistory.push(
+        generateFlamegraphRoute({
+          orgSlug: organization.slug,
+          projectSlug: project.slug,
+          profileId: dataPoint.id,
+        })
+      );
+    },
   };
 }
 

+ 2 - 14
static/app/views/profiling/landing/profilingTableCell.tsx

@@ -3,14 +3,14 @@ import Duration from 'sentry/components/duration';
 import Link from 'sentry/components/links/link';
 import {IconCheckmark, IconClose} from 'sentry/icons';
 import {t} from 'sentry/locale';
-import {Organization, Project} from 'sentry/types';
-import {Trace} from 'sentry/types/profiling/core';
 import {defined} from 'sentry/utils';
 import {Container, NumberContainer} from 'sentry/utils/discover/styles';
 import {getShortEventId} from 'sentry/utils/events';
 import useOrganization from 'sentry/utils/useOrganization';
 import useProjects from 'sentry/utils/useProjects';
 
+import {generateFlamegraphRoute} from '../routes';
+
 import {TableColumn, TableDataRow} from './types';
 
 interface ProfilingTableCellProps {
@@ -72,16 +72,4 @@ function ProfilingTableCell({column, dataRow}: ProfilingTableCellProps) {
   }
 }
 
-function generateFlamegraphRoute({
-  orgSlug,
-  projectSlug,
-  profileId,
-}: {
-  orgSlug: Organization['slug'];
-  profileId: Trace['id'];
-  projectSlug: Project['slug'];
-}) {
-  return `/organizations/${orgSlug}/profiling/flamegraph/${projectSlug}/${profileId}/`;
-}
-
 export {ProfilingTableCell};

+ 14 - 0
static/app/views/profiling/routes.tsx

@@ -0,0 +1,14 @@
+import {Organization, Project} from 'sentry/types';
+import {Trace} from 'sentry/types/profiling/core';
+
+export function generateFlamegraphRoute({
+  orgSlug,
+  projectSlug,
+  profileId,
+}: {
+  orgSlug: Organization['slug'];
+  profileId: Trace['id'];
+  projectSlug: Project['slug'];
+}) {
+  return `/organizations/${orgSlug}/profiling/flamegraph/${projectSlug}/${profileId}/`;
+}