Browse Source

feat(profiling): rename old view and init new (#56363)

Out with the old, in with the new
Jonas 1 year ago
parent
commit
1629135bb4

+ 0 - 1
static/app/components/profiling/aggregateFlamegraphPanel.tsx

@@ -22,7 +22,6 @@ export function AggregateFlamegraphPanel({transaction}: {transaction: string}) {
   );
 
   const {data, isLoading} = useAggregateFlamegraphQuery({transaction});
-
   const isEmpty = data?.shared.frames.length === 0;
 
   return (

+ 16 - 13
static/app/components/profiling/flamegraph/aggregateFlamegraph.tsx

@@ -46,6 +46,7 @@ const LOADING_OR_FALLBACK_FLAMEGRAPH = FlamegraphModel.Empty();
 interface AggregateFlamegraphProps {
   hideSystemFrames: boolean;
   setHideSystemFrames: (hideSystemFrames: boolean) => void;
+  hideToolbar?: boolean;
 }
 
 export function AggregateFlamegraph(props: AggregateFlamegraphProps): ReactElement {
@@ -282,20 +283,22 @@ export function AggregateFlamegraph(props: AggregateFlamegraphProps): ReactEleme
         disableGrid
         disableCallOrderSort
       />
-      <AggregateFlamegraphToolbar>
-        <Flex justify="space-between" align="center">
-          <Button size="xs" onClick={() => scheduler.dispatch('reset zoom')}>
-            {t('Reset Zoom')}
-          </Button>
-          <Flex align="center" gap={space(1)}>
-            <span>{t('Hide System Frames')}</span>
-            <SwitchButton
-              toggle={() => props.setHideSystemFrames(!props.hideSystemFrames)}
-              isActive={props.hideSystemFrames}
-            />
+      {props.hideToolbar ? null : (
+        <AggregateFlamegraphToolbar>
+          <Flex justify="space-between" align="center">
+            <Button size="xs" onClick={() => scheduler.dispatch('reset zoom')}>
+              {t('Reset Zoom')}
+            </Button>
+            <Flex align="center" gap={space(1)}>
+              <span>{t('Hide System Frames')}</span>
+              <SwitchButton
+                toggle={() => props.setHideSystemFrames(!props.hideSystemFrames)}
+                isActive={props.hideSystemFrames}
+              />
+            </Flex>
           </Flex>
-        </Flex>
-      </AggregateFlamegraphToolbar>
+        </AggregateFlamegraphToolbar>
+      )}
     </Fragment>
   );
 }

+ 2 - 2
static/app/views/profiling/content.tsx

@@ -40,7 +40,7 @@ import useProjects from 'sentry/utils/useProjects';
 import {DEFAULT_PROFILING_DATETIME_SELECTION} from 'sentry/views/profiling/utils';
 
 import {LandingWidgetSelector} from './landing/landingWidgetSelector';
-import {ProfileCharts} from './landing/profileCharts';
+import {ProfilesChart} from './landing/profileCharts';
 import {ProfilesChartWidget} from './landing/profilesChartWidget';
 import {ProfilingSlowestTransactionsPanel} from './landing/profilingSlowestTransactionsPanel';
 import {ProfilingOnboardingPanel} from './profilingOnboardingPanel';
@@ -279,7 +279,7 @@ function ProfilingContent({location}: ProfilingContentProps) {
                   ) : (
                     <PanelsGrid>
                       <ProfilingSlowestTransactionsPanel />
-                      <ProfileCharts
+                      <ProfilesChart
                         referrer="api.profiling.landing-chart"
                         query={query}
                         selection={selection}

+ 83 - 74
static/app/views/profiling/landing/profileCharts.tsx

@@ -2,8 +2,9 @@ import {useMemo} from 'react';
 import {useTheme} from '@emotion/react';
 import styled from '@emotion/styled';
 
-import {AreaChart} from 'sentry/components/charts/areaChart';
+import {AreaChart, AreaChartProps} from 'sentry/components/charts/areaChart';
 import ChartZoom from 'sentry/components/charts/chartZoom';
+import {LineChartProps} from 'sentry/components/charts/lineChart';
 import {HeaderTitle} from 'sentry/components/charts/styles';
 import Panel from 'sentry/components/panels/panel';
 import {t} from 'sentry/locale';
@@ -15,7 +16,12 @@ import {aggregateOutputType} from 'sentry/utils/discover/fields';
 import {useProfileEventsStats} from 'sentry/utils/profiling/hooks/useProfileEventsStats';
 import useRouter from 'sentry/utils/useRouter';
 
-interface ProfileChartsProps {
+// We want p99 to be before p75 because echarts renders the series in order.
+// So if p75 is before p99, p99 will be rendered on top of p75 which will
+// cover it up.
+const SERIES_ORDER = ['count()', 'p99()', 'p95()', 'p75()'] as const;
+
+interface ProfilesChartProps {
   query: string;
   referrer: string;
   compact?: boolean;
@@ -23,18 +29,13 @@ interface ProfileChartsProps {
   selection?: PageFilters;
 }
 
-// We want p99 to be before p75 because echarts renders the series in order.
-// So if p75 is before p99, p99 will be rendered on top of p75 which will
-// cover it up.
-const SERIES_ORDER = ['count()', 'p99()', 'p95()', 'p75()'] as const;
-
-export function ProfileCharts({
+export function ProfilesChart({
   query,
   referrer,
   selection,
   hideCount,
   compact = false,
-}: ProfileChartsProps) {
+}: ProfilesChartProps) {
   const router = useRouter();
   const theme = useTheme();
 
@@ -104,6 +105,78 @@ export function ProfileCharts({
     return allSeries;
   }, [profileStats, seriesOrder]);
 
+  const chartProps: LineChartProps | AreaChartProps = useMemo(() => {
+    const baseProps: LineChartProps | AreaChartProps = {
+      height: compact ? 150 : 300,
+      series,
+      grid: [
+        {
+          top: '32px',
+          left: '24px',
+          right: '52%',
+          bottom: '16px',
+        },
+        {
+          top: '32px',
+          left: hideCount ? '24px' : '52%',
+          right: '24px',
+          bottom: '16px',
+        },
+      ],
+      legend: {
+        right: 16,
+        top: 12,
+        data: seriesOrder.slice(),
+      },
+      tooltip: {
+        valueFormatter: (value, label) =>
+          tooltipFormatter(value, aggregateOutputType(label)),
+      },
+      axisPointer: {
+        link: [
+          {
+            xAxisIndex: [0, 1],
+          },
+        ],
+      },
+      xAxes: [
+        {
+          show: !hideCount,
+          gridIndex: 0,
+          type: 'time' as const,
+        },
+        {
+          gridIndex: 1,
+          type: 'time' as const,
+        },
+      ],
+      yAxes: [
+        {
+          gridIndex: 0,
+          scale: true,
+          axisLabel: {
+            color: theme.chartLabel,
+            formatter(value: number) {
+              return axisLabelFormatter(value, 'integer');
+            },
+          },
+        },
+        {
+          gridIndex: 1,
+          scale: true,
+          axisLabel: {
+            color: theme.chartLabel,
+            formatter(value: number) {
+              return axisLabelFormatter(value, 'duration');
+            },
+          },
+        },
+      ],
+    };
+
+    return baseProps;
+  }, [compact, hideCount, series, seriesOrder, theme.chartLabel]);
+
   return (
     <ChartZoom router={router} {...selection?.datetime}>
       {zoomRenderProps => (
@@ -115,71 +188,7 @@ export function ProfileCharts({
             <StyledHeaderTitle compact>{t('Profiles Duration')}</StyledHeaderTitle>
           </TitleContainer>
           <AreaChart
-            height={compact ? 150 : 300}
-            series={series}
-            grid={[
-              {
-                top: '32px',
-                left: '24px',
-                right: '52%',
-                bottom: '16px',
-              },
-              {
-                top: '32px',
-                left: hideCount ? '24px' : '52%',
-                right: '24px',
-                bottom: '16px',
-              },
-            ]}
-            legend={{
-              right: 16,
-              top: 12,
-              data: seriesOrder.slice(),
-            }}
-            axisPointer={{
-              link: [
-                {
-                  xAxisIndex: [0, 1],
-                },
-              ],
-            }}
-            xAxes={[
-              {
-                show: !hideCount,
-                gridIndex: 0,
-                type: 'time' as const,
-              },
-              {
-                gridIndex: 1,
-                type: 'time' as const,
-              },
-            ]}
-            yAxes={[
-              {
-                gridIndex: 0,
-                scale: true,
-                axisLabel: {
-                  color: theme.chartLabel,
-                  formatter(value: number) {
-                    return axisLabelFormatter(value, 'integer');
-                  },
-                },
-              },
-              {
-                gridIndex: 1,
-                scale: true,
-                axisLabel: {
-                  color: theme.chartLabel,
-                  formatter(value: number) {
-                    return axisLabelFormatter(value, 'duration');
-                  },
-                },
-              },
-            ]}
-            tooltip={{
-              valueFormatter: (value, label) =>
-                tooltipFormatter(value, aggregateOutputType(label)),
-            }}
+            {...chartProps}
             isGroupedByDate
             showTimeInTooltip
             {...zoomRenderProps}

+ 184 - 0
static/app/views/profiling/landing/profilesSummaryChart.tsx

@@ -0,0 +1,184 @@
+import {useMemo} from 'react';
+import {useTheme} from '@emotion/react';
+
+import ChartZoom from 'sentry/components/charts/chartZoom';
+import {LineChart, LineChartProps} from 'sentry/components/charts/lineChart';
+import {PageFilters} from 'sentry/types';
+import {Series} from 'sentry/types/echarts';
+import {axisLabelFormatter, tooltipFormatter} from 'sentry/utils/discover/charts';
+import {aggregateOutputType} from 'sentry/utils/discover/fields';
+import {useProfileEventsStats} from 'sentry/utils/profiling/hooks/useProfileEventsStats';
+import useRouter from 'sentry/utils/useRouter';
+
+// We want p99 to be before p75 because echarts renders the series in order.
+// So if p75 is before p99, p99 will be rendered on top of p75 which will
+// cover it up.
+const SERIES_ORDER = ['count()', 'p99()', 'p95()', 'p75()'] as const;
+
+interface ProfileSummaryChartProps {
+  query: string;
+  referrer: string;
+  hideCount?: boolean;
+  selection?: PageFilters;
+}
+
+export function ProfilesSummaryChart({
+  query,
+  referrer,
+  selection,
+  hideCount,
+}: ProfileSummaryChartProps) {
+  const router = useRouter();
+  const theme = useTheme();
+
+  const seriesOrder = useMemo(() => {
+    if (hideCount) {
+      return SERIES_ORDER.filter(s => s !== 'count()');
+    }
+    return SERIES_ORDER;
+  }, [hideCount]);
+
+  const profileStats = useProfileEventsStats({
+    query,
+    referrer,
+    yAxes: seriesOrder,
+  });
+
+  const series: Series[] = useMemo(() => {
+    if (profileStats.status !== 'success') {
+      return [];
+    }
+
+    // the timestamps in the response is in seconds but echarts expects
+    // a timestamp in milliseconds, so multiply by 1e3 to do the conversion
+    const timestamps = profileStats.data[0].timestamps.map(ts => ts * 1e3);
+
+    const allSeries = profileStats.data[0].data
+      .filter(rawData => seriesOrder.includes(rawData.axis))
+      .map(rawData => {
+        if (timestamps.length !== rawData.values.length) {
+          throw new Error('Invalid stats response');
+        }
+
+        if (rawData.axis === 'count()') {
+          return {
+            data: rawData.values.map((value, i) => ({
+              name: timestamps[i]!,
+              // the response value contains nulls when no data is
+              // available, use 0 to represent it
+              value: value ?? 0,
+            })),
+            seriesName: rawData.axis,
+            xAxisIndex: 0,
+            yAxisIndex: 0,
+          };
+        }
+
+        return {
+          data: rawData.values.map((value, i) => ({
+            name: timestamps[i]!,
+            // the response value contains nulls when no data
+            // is available, use 0 to represent it
+            value: value ?? 0,
+          })),
+          seriesName: rawData.axis,
+          xAxisIndex: 1,
+          yAxisIndex: 1,
+        };
+      });
+
+    allSeries.sort((a, b) => {
+      const idxA = seriesOrder.indexOf(a.seriesName as any);
+      const idxB = seriesOrder.indexOf(b.seriesName as any);
+
+      return idxA - idxB;
+    });
+
+    return allSeries;
+  }, [profileStats, seriesOrder]);
+
+  const chartProps: LineChartProps = useMemo(() => {
+    const baseProps: LineChartProps = {
+      height: 150,
+      series,
+      grid: [
+        {
+          top: '32px',
+          left: '24px',
+          right: '52%',
+          bottom: '16px',
+        },
+        {
+          top: '32px',
+          left: hideCount ? '24px' : '52%',
+          right: '24px',
+          bottom: '16px',
+        },
+      ],
+      legend: {
+        right: 16,
+        top: 12,
+        data: seriesOrder.slice(),
+      },
+      tooltip: {
+        valueFormatter: (value, label) =>
+          tooltipFormatter(value, aggregateOutputType(label)),
+      },
+      axisPointer: {
+        link: [
+          {
+            xAxisIndex: [0, 1],
+          },
+        ],
+      },
+      xAxes: [
+        {
+          show: !hideCount,
+          gridIndex: 0,
+          type: 'time' as const,
+        },
+        {
+          gridIndex: 1,
+          type: 'time' as const,
+        },
+      ],
+      yAxes: [
+        {
+          gridIndex: 0,
+          scale: true,
+          axisLabel: {
+            color: theme.chartLabel,
+            formatter(value: number) {
+              return axisLabelFormatter(value, 'integer');
+            },
+          },
+        },
+        {
+          gridIndex: 1,
+          scale: true,
+          axisLabel: {
+            color: theme.chartLabel,
+            formatter(value: number) {
+              return axisLabelFormatter(value, 'duration');
+            },
+          },
+        },
+      ],
+    };
+
+    return baseProps;
+  }, [hideCount, series, seriesOrder, theme.chartLabel]);
+
+  return (
+    <ChartZoom router={router} {...selection?.datetime}>
+      {zoomRenderProps => (
+        <LineChart
+          {...chartProps}
+          isGroupedByDate
+          showTimeInTooltip
+          {...zoomRenderProps}
+        />
+      )}
+    </ChartZoom>
+  );
+}

+ 2 - 2
static/app/views/profiling/profileSummary/content.tsx

@@ -16,7 +16,7 @@ import {PageFilters, Project} from 'sentry/types';
 import {useProfileEvents} from 'sentry/utils/profiling/hooks/useProfileEvents';
 import {formatSort} from 'sentry/utils/profiling/hooks/utils';
 import {decodeScalar} from 'sentry/utils/queryString';
-import {ProfileCharts} from 'sentry/views/profiling/landing/profileCharts';
+import {ProfilesChart} from 'sentry/views/profiling/landing/profileCharts';
 
 interface ProfileSummaryContentProps {
   location: Location;
@@ -68,7 +68,7 @@ function ProfileSummaryContent(props: ProfileSummaryContentProps) {
   return (
     <Fragment>
       <Layout.Main fullWidth>
-        <ProfileCharts
+        <ProfilesChart
           referrer="api.profiling.profile-summary-chart"
           query={props.query}
           hideCount

+ 284 - 221
static/app/views/profiling/profileSummary/index.tsx

@@ -1,9 +1,9 @@
-import {Fragment, useCallback, useEffect, useMemo} from 'react';
+import {useCallback, useMemo} from 'react';
 import {browserHistory} from 'react-router';
 import styled from '@emotion/styled';
-import {Location} from 'history';
+import type {Location} from 'history';
 
-import {Button} from 'sentry/components/button';
+import {LinkButton} from 'sentry/components/button';
 import DatePageFilter from 'sentry/components/datePageFilter';
 import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
 import SearchBar from 'sentry/components/events/searchBar';
@@ -11,32 +11,181 @@ import IdBadge from 'sentry/components/idBadge';
 import * as Layout from 'sentry/components/layouts/thirds';
 import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
 import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
+import {AggregateFlamegraph} from 'sentry/components/profiling/flamegraph/aggregateFlamegraph';
 import {
   ProfilingBreadcrumbs,
   ProfilingBreadcrumbsProps,
 } from 'sentry/components/profiling/profilingBreadcrumbs';
 import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
-import SmartSearchBar, {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
+import type {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
+import SmartSearchBar from 'sentry/components/smartSearchBar';
 import {MAX_QUERY_LENGTH} from 'sentry/constants';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
-import {PageFilters, Project} from 'sentry/types';
-import {defined, generateQueryWithTag} from 'sentry/utils';
-import {trackAnalytics} from 'sentry/utils/analytics';
+import type {Organization, PageFilters, Project} from 'sentry/types';
+import {defined} from 'sentry/utils';
 import EventView from 'sentry/utils/discover/eventView';
-import {formatTagKey, isAggregateField} from 'sentry/utils/discover/fields';
+import {isAggregateField} from 'sentry/utils/discover/fields';
+import {FlamegraphStateProvider} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContextProvider';
+import {FlamegraphThemeProvider} from 'sentry/utils/profiling/flamegraph/flamegraphThemeProvider';
+import {useAggregateFlamegraphQuery} from 'sentry/utils/profiling/hooks/useAggregateFlamegraphQuery';
 import {useCurrentProjectFromRouteParam} from 'sentry/utils/profiling/hooks/useCurrentProjectFromRouteParam';
-import {useProfileEvents} from 'sentry/utils/profiling/hooks/useProfileEvents';
 import {useProfileFilters} from 'sentry/utils/profiling/hooks/useProfileFilters';
 import {decodeScalar} from 'sentry/utils/queryString';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
 import useOrganization from 'sentry/utils/useOrganization';
-import withPageFilters from 'sentry/utils/withPageFilters';
-import Tags from 'sentry/views/discover/tags';
 import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
+import {ProfilesSummaryChart} from 'sentry/views/profiling/landing/profilesSummaryChart';
+import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider';
+import {LegacySummaryPage} from 'sentry/views/profiling/profileSummary/legacySummaryPage';
 import {DEFAULT_PROFILING_DATETIME_SELECTION} from 'sentry/views/profiling/utils';
 
-import {ProfileSummaryContent} from './content';
+interface ProfileSummaryHeaderProps {
+  location: Location;
+  organization: Organization;
+  project: Project | null;
+  query: string;
+  transaction: string | undefined;
+}
+function ProfileSummaryHeader(props: ProfileSummaryHeaderProps) {
+  const breadcrumbTrails: ProfilingBreadcrumbsProps['trails'] = useMemo(() => {
+    return [
+      {
+        type: 'landing',
+        payload: {
+          query: props.location.query,
+        },
+      },
+      {
+        type: 'profile summary',
+        payload: {
+          projectSlug: props.project?.slug ?? '',
+          query: props.location.query,
+          transaction: props.transaction ?? '',
+        },
+      },
+    ];
+  }, [props.location.query, props.project?.slug, props.transaction]);
+
+  const transactionSummaryTarget =
+    props.project &&
+    props.transaction &&
+    transactionSummaryRouteWithQuery({
+      orgSlug: props.organization.slug,
+      transaction: props.transaction,
+      projectID: props.project.id,
+      query: {query: props.query},
+    });
+
+  return (
+    <Layout.Header>
+      <Layout.HeaderContent>
+        <ProfilingBreadcrumbs
+          organization={props.organization}
+          trails={breadcrumbTrails}
+        />
+        <Layout.Title>
+          {props.project ? (
+            <IdBadge
+              hideName
+              project={props.project}
+              avatarSize={28}
+              avatarProps={{hasTooltip: true, tooltip: props.project.slug}}
+            />
+          ) : null}
+          {props.transaction}
+        </Layout.Title>
+      </Layout.HeaderContent>
+      {transactionSummaryTarget && (
+        <Layout.HeaderActions>
+          <LinkButton to={transactionSummaryTarget} size="sm">
+            {t('View Transaction Summary')}
+          </LinkButton>
+        </Layout.HeaderActions>
+      )}
+    </Layout.Header>
+  );
+}
+
+interface ProfileFiltersProps {
+  location: Location;
+  organization: Organization;
+  projectIds: EventView['project'];
+  query: string;
+  selection: PageFilters;
+  transaction: string | undefined;
+  usingTransactions: boolean;
+}
+
+function ProfileFilters(props: ProfileFiltersProps) {
+  const filtersQuery = useMemo(() => {
+    // To avoid querying for the filters each time the query changes,
+    // do not pass the user query to get the filters.
+    const search = new MutableSearch('');
+
+    if (defined(props.transaction)) {
+      search.setFilterValues('transaction_name', [props.transaction]);
+    }
+
+    return search.formatString();
+  }, [props.transaction]);
+
+  const profileFilters = useProfileFilters({
+    query: filtersQuery,
+    selection: props.selection,
+    disabled: props.usingTransactions,
+  });
+
+  const handleSearch: SmartSearchBarProps['onSearch'] = useCallback(
+    (searchQuery: string) => {
+      browserHistory.push({
+        ...props.location,
+        query: {
+          ...props.location.query,
+          query: searchQuery || undefined,
+          cursor: undefined,
+        },
+      });
+    },
+    [props.location]
+  );
+
+  return (
+    <ActionBar>
+      <PageFilterBar condensed>
+        <EnvironmentPageFilter />
+        <DatePageFilter alignDropdown="left" />
+      </PageFilterBar>
+      {props.usingTransactions ? (
+        <SearchBar
+          searchSource="profile_summary"
+          organization={props.organization}
+          projectIds={props.projectIds}
+          query={props.query}
+          onSearch={handleSearch}
+          maxQueryLength={MAX_QUERY_LENGTH}
+        />
+      ) : (
+        <SmartSearchBar
+          organization={props.organization}
+          hasRecentSearches
+          searchSource="profile_summary"
+          supportedTags={profileFilters}
+          query={props.query}
+          onSearch={handleSearch}
+          maxQueryLength={MAX_QUERY_LENGTH}
+        />
+      )}
+    </ActionBar>
+  );
+}
+
+const ActionBar = styled('div')`
+  display: grid;
+  gap: ${space(2)};
+  grid-template-columns: min-content auto;
+  margin-bottom: ${space(2)};
+`;
 
 interface ProfileSummaryPageProps {
   location: Location;
@@ -54,23 +203,25 @@ function ProfileSummaryPage(props: ProfileSummaryPageProps) {
     'profiling-using-transactions'
   );
 
-  useEffect(() => {
-    trackAnalytics('profiling_views.profile_summary', {
-      organization,
-      project_platform: project?.platform,
-      project_id: project?.id,
-    });
-    // ignore  currentProject so we don't block the analytics event
-    // or fire more than once unnecessarily
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [organization]);
-
   const transaction = decodeScalar(props.location.query.transaction);
+  const rawQuery = decodeScalar(props.location?.query?.query, '');
 
-  const rawQuery = useMemo(
-    () => decodeScalar(props.location.query.query, ''),
-    [props.location.query.query]
-  );
+  const projectIds: number[] = useMemo(() => {
+    if (!defined(project)) {
+      return [];
+    }
+
+    const projects = parseInt(project.id, 10);
+    if (isNaN(projects)) {
+      return [];
+    }
+
+    return [projects];
+  }, [project]);
+
+  const projectSlugs: string[] = useMemo(() => {
+    return defined(project) ? [project.slug] : [];
+  }, [project]);
 
   const query = useMemo(() => {
     const search = new MutableSearch(rawQuery);
@@ -90,211 +241,123 @@ function ProfileSummaryPage(props: ProfileSummaryPageProps) {
     return search.formatString();
   }, [rawQuery, transaction]);
 
-  const profilesAggregateQuery = useProfileEvents<'count()'>({
-    fields: ['count()'],
-    sort: {key: 'count()', order: 'desc'},
-    referrer: 'api.profiling.profile-summary-totals',
-    query,
-    enabled: profilingUsingTransactions,
-  });
-
-  const profilesCount = useMemo(() => {
-    if (profilesAggregateQuery.status !== 'success') {
-      return null;
-    }
-
-    return (profilesAggregateQuery.data?.data?.[0]?.['count()'] as number) || null;
-  }, [profilesAggregateQuery]);
-
-  const filtersQuery = useMemo(() => {
-    // To avoid querying for the filters each time the query changes,
-    // do not pass the user query to get the filters.
-    const search = new MutableSearch('');
-
-    if (defined(transaction)) {
-      search.setFilterValues('transaction_name', [transaction]);
-    }
-
-    return search.formatString();
-  }, [transaction]);
-
-  const profileFilters = useProfileFilters({
-    query: filtersQuery,
-    selection: props.selection,
-    disabled: profilingUsingTransactions,
-  });
-
-  const transactionSummaryTarget =
-    project &&
-    transaction &&
-    transactionSummaryRouteWithQuery({
-      orgSlug: organization.slug,
-      transaction,
-      projectID: project.id,
-      query: {query},
-    });
-
-  const handleSearch: SmartSearchBarProps['onSearch'] = useCallback(
-    (searchQuery: string) => {
-      browserHistory.push({
-        ...props.location,
-        query: {
-          ...props.location.query,
-          query: searchQuery || undefined,
-          cursor: undefined,
-        },
-      });
-    },
-    [props.location]
-  );
-
-  const breadcrumbTrails: ProfilingBreadcrumbsProps['trails'] = useMemo(() => {
-    return [
-      {
-        type: 'landing',
-        payload: {
-          query: props.location.query,
-        },
-      },
-      {
-        type: 'profile summary',
-        payload: {
-          projectSlug: project?.slug ?? '',
-          query: props.location.query,
-          transaction: transaction ?? '',
-        },
-      },
-    ];
-  }, [props.location.query, project?.slug, transaction]);
-
-  const eventView = useMemo(() => {
-    const _eventView = EventView.fromNewQueryWithLocation(
-      {
-        id: undefined,
-        version: 2,
-        name: transaction || '',
-        fields: [],
-        query,
-        projects: project ? [parseInt(project.id, 10)] : [],
-      },
-      props.location
-    );
-    _eventView.additionalConditions.setFilterValues('has', ['profile.id']);
-    return _eventView;
-  }, [props.location, project, query, transaction]);
-
-  function generateTagUrl(key: string, value: string) {
-    return {
-      ...props.location,
-      query: generateQueryWithTag(props.location.query, {key: formatTagKey(key), value}),
-    };
-  }
+  const {data} = useAggregateFlamegraphQuery({transaction: transaction ?? ''});
 
   return (
     <SentryDocumentTitle
       title={t('Profiling \u2014 Profile Summary')}
       orgSlug={organization.slug}
     >
-      <PageFiltersContainer
-        shouldForceProject={defined(project)}
-        forceProject={project}
-        specificProjectSlugs={defined(project) ? [project.slug] : []}
-        defaultSelection={
-          profilingUsingTransactions
-            ? {datetime: DEFAULT_PROFILING_DATETIME_SELECTION}
-            : undefined
-        }
-      >
-        <Layout.Page>
-          {project && transaction && (
-            <Fragment>
-              <Layout.Header>
-                <Layout.HeaderContent>
-                  <ProfilingBreadcrumbs
-                    organization={organization}
-                    trails={breadcrumbTrails}
-                  />
-                  <Layout.Title>
-                    {project ? (
-                      <IdBadge
-                        project={project}
-                        avatarSize={28}
-                        hideName
-                        avatarProps={{hasTooltip: true, tooltip: project.slug}}
-                      />
-                    ) : null}
-                    {transaction}
-                  </Layout.Title>
-                </Layout.HeaderContent>
-                {transactionSummaryTarget && (
-                  <Layout.HeaderActions>
-                    <Button to={transactionSummaryTarget} size="sm">
-                      {t('View Transaction Summary')}
-                    </Button>
-                  </Layout.HeaderActions>
-                )}
-              </Layout.Header>
-              <Layout.Body>
-                <Layout.Main fullWidth={!profilingUsingTransactions}>
-                  <ActionBar>
-                    <PageFilterBar condensed>
-                      <EnvironmentPageFilter />
-                      <DatePageFilter alignDropdown="left" />
-                    </PageFilterBar>
-                    {profilingUsingTransactions ? (
-                      <SearchBar
-                        searchSource="profile_summary"
-                        organization={organization}
-                        projectIds={eventView.project}
-                        query={rawQuery}
-                        onSearch={handleSearch}
-                        maxQueryLength={MAX_QUERY_LENGTH}
-                      />
-                    ) : (
-                      <SmartSearchBar
-                        organization={organization}
-                        hasRecentSearches
-                        searchSource="profile_summary"
-                        supportedTags={profileFilters}
-                        query={rawQuery}
-                        onSearch={handleSearch}
-                        maxQueryLength={MAX_QUERY_LENGTH}
-                      />
-                    )}
-                  </ActionBar>
-                  <ProfileSummaryContent
-                    location={props.location}
-                    project={project}
-                    selection={props.selection}
-                    transaction={transaction}
-                    query={query}
-                  />
-                </Layout.Main>
-                {profilingUsingTransactions && (
-                  <Layout.Side>
-                    <Tags
-                      generateUrl={generateTagUrl}
-                      totalValues={profilesCount}
-                      eventView={eventView}
-                      organization={organization}
-                      location={props.location}
+      <ProfileSummaryContainer>
+        <PageFiltersContainer
+          shouldForceProject={defined(project)}
+          forceProject={project}
+          specificProjectSlugs={projectSlugs}
+          defaultSelection={
+            profilingUsingTransactions
+              ? {datetime: DEFAULT_PROFILING_DATETIME_SELECTION}
+              : undefined
+          }
+        >
+          <ProfileSummaryHeader
+            organization={organization}
+            location={props.location}
+            project={project}
+            query={query}
+            transaction={transaction}
+          />
+          <ProfileFilters
+            projectIds={projectIds}
+            organization={organization}
+            location={props.location}
+            query={rawQuery}
+            selection={props.selection}
+            transaction={transaction}
+            usingTransactions={profilingUsingTransactions}
+          />
+          <ProfilesSummaryChart
+            referrer="api.profiling.profile-summary-chart"
+            query={query}
+            hideCount
+          />
+          <ProfileVisualizationContainer>
+            <ProfileVisualization>
+              <ProfileGroupProvider
+                type="flamegraph"
+                input={data ?? null}
+                traceID=""
+                frameFilter={undefined}
+              >
+                <FlamegraphStateProvider
+                  initialState={{
+                    preferences: {
+                      sorting: 'alphabetical',
+                    },
+                  }}
+                >
+                  <FlamegraphThemeProvider>
+                    <AggregateFlamegraph
+                      hideToolbar
+                      hideSystemFrames={false}
+                      setHideSystemFrames={() => void 0}
                     />
-                  </Layout.Side>
-                )}
-              </Layout.Body>
-            </Fragment>
-          )}
-        </Layout.Page>
-      </PageFiltersContainer>
+                  </FlamegraphThemeProvider>
+                </FlamegraphStateProvider>
+              </ProfileGroupProvider>
+            </ProfileVisualization>
+            <ProfileDigest>
+              <div>TODO: Profile Digest</div>
+            </ProfileDigest>
+          </ProfileVisualizationContainer>
+        </PageFiltersContainer>
+      </ProfileSummaryContainer>
     </SentryDocumentTitle>
   );
 }
 
-const ActionBar = styled('div')`
+const ProfileVisualization = styled('div')`
+  grid-area: visualization;
+`;
+
+const ProfileDigest = styled('div')`
+  grid-area: digest;
+`;
+
+const ProfileVisualizationContainer = styled('div')`
   display: grid;
-  gap: ${space(2)};
-  grid-template-columns: min-content auto;
-  margin-bottom: ${space(2)};
+  grid-template-areas: 'visualization digest';
+  flex: 1 1 100%;
 `;
 
-export default withPageFilters(ProfileSummaryPage);
+const ProfileSummaryContainer = styled('div')`
+  display: flex;
+  flex-direction: column;
+  flex: 1 1 100%;
+
+  /*
+   * The footer component is a sibling of this div.
+   * Remove it so the flamegraph can take up the
+   * entire screen.
+   */
+  ~ footer {
+    display: none;
+  }
+`;
+
+export default function ProfileSummaryPageToggle(props: ProfileSummaryPageProps) {
+  const organization = useOrganization();
+
+  if (organization.features.includes('profiling-summary-redesign')) {
+    return (
+      <ProfileSummaryContainer data-test-id="profile-summary-redesign">
+        <ProfileSummaryPage {...props} />
+      </ProfileSummaryContainer>
+    );
+  }
+
+  return (
+    <div data-test-id="profile-summary-legacy">
+      <LegacySummaryPage {...props} />
+    </div>
+  );
+}

+ 300 - 0
static/app/views/profiling/profileSummary/legacySummaryPage.tsx

@@ -0,0 +1,300 @@
+import {Fragment, useCallback, useEffect, useMemo} from 'react';
+import {browserHistory} from 'react-router';
+import styled from '@emotion/styled';
+import {Location} from 'history';
+
+import {Button} from 'sentry/components/button';
+import DatePageFilter from 'sentry/components/datePageFilter';
+import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
+import SearchBar from 'sentry/components/events/searchBar';
+import IdBadge from 'sentry/components/idBadge';
+import * as Layout from 'sentry/components/layouts/thirds';
+import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
+import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
+import {
+  ProfilingBreadcrumbs,
+  ProfilingBreadcrumbsProps,
+} from 'sentry/components/profiling/profilingBreadcrumbs';
+import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
+import SmartSearchBar, {SmartSearchBarProps} from 'sentry/components/smartSearchBar';
+import {MAX_QUERY_LENGTH} from 'sentry/constants';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import {PageFilters, Project} from 'sentry/types';
+import {defined, generateQueryWithTag} from 'sentry/utils';
+import {trackAnalytics} from 'sentry/utils/analytics';
+import EventView from 'sentry/utils/discover/eventView';
+import {formatTagKey, isAggregateField} from 'sentry/utils/discover/fields';
+import {useCurrentProjectFromRouteParam} from 'sentry/utils/profiling/hooks/useCurrentProjectFromRouteParam';
+import {useProfileEvents} from 'sentry/utils/profiling/hooks/useProfileEvents';
+import {useProfileFilters} from 'sentry/utils/profiling/hooks/useProfileFilters';
+import {decodeScalar} from 'sentry/utils/queryString';
+import {MutableSearch} from 'sentry/utils/tokenizeSearch';
+import useOrganization from 'sentry/utils/useOrganization';
+import withPageFilters from 'sentry/utils/withPageFilters';
+import Tags from 'sentry/views/discover/tags';
+import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
+import {DEFAULT_PROFILING_DATETIME_SELECTION} from 'sentry/views/profiling/utils';
+
+import {ProfileSummaryContent} from './content';
+
+interface ProfileSummaryPageProps {
+  location: Location;
+  params: {
+    projectId?: Project['slug'];
+  };
+  selection: PageFilters;
+}
+
+function LegacyProfileSummaryPage(props: ProfileSummaryPageProps) {
+  const organization = useOrganization();
+  const project = useCurrentProjectFromRouteParam();
+
+  const profilingUsingTransactions = organization.features.includes(
+    'profiling-using-transactions'
+  );
+
+  useEffect(() => {
+    trackAnalytics('profiling_views.profile_summary', {
+      organization,
+      project_platform: project?.platform,
+      project_id: project?.id,
+    });
+    // ignore  currentProject so we don't block the analytics event
+    // or fire more than once unnecessarily
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [organization]);
+
+  const transaction = decodeScalar(props.location.query.transaction);
+
+  const rawQuery = useMemo(
+    () => decodeScalar(props.location.query.query, ''),
+    [props.location.query.query]
+  );
+
+  const query = useMemo(() => {
+    const search = new MutableSearch(rawQuery);
+
+    if (defined(transaction)) {
+      search.setFilterValues('transaction', [transaction]);
+    }
+
+    // there are no aggregations happening on this page,
+    // so remove any aggregate filters
+    Object.keys(search.filters).forEach(field => {
+      if (isAggregateField(field)) {
+        search.removeFilter(field);
+      }
+    });
+
+    return search.formatString();
+  }, [rawQuery, transaction]);
+
+  const profilesAggregateQuery = useProfileEvents<'count()'>({
+    fields: ['count()'],
+    sort: {key: 'count()', order: 'desc'},
+    referrer: 'api.profiling.profile-summary-totals',
+    query,
+    enabled: profilingUsingTransactions,
+  });
+
+  const profilesCount = useMemo(() => {
+    if (profilesAggregateQuery.status !== 'success') {
+      return null;
+    }
+
+    return (profilesAggregateQuery.data?.data?.[0]?.['count()'] as number) || null;
+  }, [profilesAggregateQuery]);
+
+  const filtersQuery = useMemo(() => {
+    // To avoid querying for the filters each time the query changes,
+    // do not pass the user query to get the filters.
+    const search = new MutableSearch('');
+
+    if (defined(transaction)) {
+      search.setFilterValues('transaction_name', [transaction]);
+    }
+
+    return search.formatString();
+  }, [transaction]);
+
+  const profileFilters = useProfileFilters({
+    query: filtersQuery,
+    selection: props.selection,
+    disabled: profilingUsingTransactions,
+  });
+
+  const transactionSummaryTarget =
+    project &&
+    transaction &&
+    transactionSummaryRouteWithQuery({
+      orgSlug: organization.slug,
+      transaction,
+      projectID: project.id,
+      query: {query},
+    });
+
+  const handleSearch: SmartSearchBarProps['onSearch'] = useCallback(
+    (searchQuery: string) => {
+      browserHistory.push({
+        ...props.location,
+        query: {
+          ...props.location.query,
+          query: searchQuery || undefined,
+          cursor: undefined,
+        },
+      });
+    },
+    [props.location]
+  );
+
+  const breadcrumbTrails: ProfilingBreadcrumbsProps['trails'] = useMemo(() => {
+    return [
+      {
+        type: 'landing',
+        payload: {
+          query: props.location.query,
+        },
+      },
+      {
+        type: 'profile summary',
+        payload: {
+          projectSlug: project?.slug ?? '',
+          query: props.location.query,
+          transaction: transaction ?? '',
+        },
+      },
+    ];
+  }, [props.location.query, project?.slug, transaction]);
+
+  const eventView = useMemo(() => {
+    const _eventView = EventView.fromNewQueryWithLocation(
+      {
+        id: undefined,
+        version: 2,
+        name: transaction || '',
+        fields: [],
+        query,
+        projects: project ? [parseInt(project.id, 10)] : [],
+      },
+      props.location
+    );
+    _eventView.additionalConditions.setFilterValues('has', ['profile.id']);
+    return _eventView;
+  }, [props.location, project, query, transaction]);
+
+  function generateTagUrl(key: string, value: string) {
+    return {
+      ...props.location,
+      query: generateQueryWithTag(props.location.query, {key: formatTagKey(key), value}),
+    };
+  }
+
+  return (
+    <SentryDocumentTitle
+      title={t('Profiling \u2014 Profile Summary')}
+      orgSlug={organization.slug}
+    >
+      <PageFiltersContainer
+        shouldForceProject={defined(project)}
+        forceProject={project}
+        specificProjectSlugs={defined(project) ? [project.slug] : []}
+        defaultSelection={
+          profilingUsingTransactions
+            ? {datetime: DEFAULT_PROFILING_DATETIME_SELECTION}
+            : undefined
+        }
+      >
+        <Layout.Page>
+          {project && transaction && (
+            <Fragment>
+              <Layout.Header>
+                <Layout.HeaderContent>
+                  <ProfilingBreadcrumbs
+                    organization={organization}
+                    trails={breadcrumbTrails}
+                  />
+                  <Layout.Title>
+                    {project ? (
+                      <IdBadge
+                        project={project}
+                        avatarSize={28}
+                        hideName
+                        avatarProps={{hasTooltip: true, tooltip: project.slug}}
+                      />
+                    ) : null}
+                    {transaction}
+                  </Layout.Title>
+                </Layout.HeaderContent>
+                {transactionSummaryTarget && (
+                  <Layout.HeaderActions>
+                    <Button to={transactionSummaryTarget} size="sm">
+                      {t('View Transaction Summary')}
+                    </Button>
+                  </Layout.HeaderActions>
+                )}
+              </Layout.Header>
+              <Layout.Body>
+                <Layout.Main fullWidth={!profilingUsingTransactions}>
+                  <ActionBar>
+                    <PageFilterBar condensed>
+                      <EnvironmentPageFilter />
+                      <DatePageFilter alignDropdown="left" />
+                    </PageFilterBar>
+                    {profilingUsingTransactions ? (
+                      <SearchBar
+                        searchSource="profile_summary"
+                        organization={organization}
+                        projectIds={eventView.project}
+                        query={rawQuery}
+                        onSearch={handleSearch}
+                        maxQueryLength={MAX_QUERY_LENGTH}
+                      />
+                    ) : (
+                      <SmartSearchBar
+                        organization={organization}
+                        hasRecentSearches
+                        searchSource="profile_summary"
+                        supportedTags={profileFilters}
+                        query={rawQuery}
+                        onSearch={handleSearch}
+                        maxQueryLength={MAX_QUERY_LENGTH}
+                      />
+                    )}
+                  </ActionBar>
+                  <ProfileSummaryContent
+                    location={props.location}
+                    project={project}
+                    selection={props.selection}
+                    transaction={transaction}
+                    query={query}
+                  />
+                </Layout.Main>
+                {profilingUsingTransactions && (
+                  <Layout.Side>
+                    <Tags
+                      generateUrl={generateTagUrl}
+                      totalValues={profilesCount}
+                      eventView={eventView}
+                      organization={organization}
+                      location={props.location}
+                    />
+                  </Layout.Side>
+                )}
+              </Layout.Body>
+            </Fragment>
+          )}
+        </Layout.Page>
+      </PageFiltersContainer>
+    </SentryDocumentTitle>
+  );
+}
+
+const ActionBar = styled('div')`
+  display: grid;
+  gap: ${space(2)};
+  grid-template-columns: min-content auto;
+  margin-bottom: ${space(2)};
+`;
+
+export const LegacySummaryPage = withPageFilters(LegacyProfileSummaryPage);

+ 110 - 0
static/app/views/profiling/profileSummary/profileSummaryPage.spec.tsx

@@ -0,0 +1,110 @@
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import OrganizationStore from 'sentry/stores/organizationStore';
+import ProfileSummaryPage from 'sentry/views/profiling/profileSummary';
+
+Object.defineProperty(window, 'matchMedia', {
+  writable: true,
+  value: jest.fn().mockImplementation(query => ({
+    matches: false,
+    media: query,
+    onchange: null,
+    addListener: jest.fn(), // Deprecated
+    removeListener: jest.fn(), // Deprecated
+    addEventListener: jest.fn(),
+    removeEventListener: jest.fn(),
+    dispatchEvent: jest.fn(),
+  })),
+});
+
+// Replace the webgl renderer with a dom renderer for tests
+jest.mock('sentry/utils/profiling/renderers/flamegraphRendererWebGL', () => {
+  const {
+    FlamegraphRendererDOM,
+  } = require('sentry/utils/profiling/renderers/flamegraphRendererDOM');
+
+  return {
+    FlamegraphRendererWebGL: FlamegraphRendererDOM,
+  };
+});
+
+window.ResizeObserver =
+  window.ResizeObserver ||
+  jest.fn().mockImplementation(() => ({
+    disconnect: jest.fn(),
+    observe: jest.fn(),
+    unobserve: jest.fn(),
+  }));
+
+describe('ProfileSummaryPage', () => {
+  it('renders legacy page', async () => {
+    const organization = TestStubs.Organization({
+      features: [],
+      projects: [TestStubs.Project()],
+    });
+    OrganizationStore.onUpdate(organization);
+
+    MockApiClient.addMockResponse({
+      url: `/organizations/${organization.slug}/projects/`,
+      body: [TestStubs.Project()],
+    });
+
+    MockApiClient.addMockResponse({
+      url: `/organizations/${organization.slug}/profiling/filters/`,
+      body: [],
+    });
+
+    render(
+      <ProfileSummaryPage
+        params={{}}
+        selection={TestStubs.GlobalSelection()}
+        location={TestStubs.location()}
+      />,
+      {
+        organization,
+        context: TestStubs.routerContext(),
+      }
+    );
+
+    expect(await screen.findByTestId(/profile-summary-legacy/i)).toBeInTheDocument();
+  });
+
+  it('renders new page', async () => {
+    const organization = TestStubs.Organization({
+      features: [],
+      projects: [TestStubs.Project()],
+    });
+    OrganizationStore.onUpdate(organization);
+
+    MockApiClient.addMockResponse({
+      url: `/organizations/${organization.slug}/profiling/filters/`,
+      body: [],
+    });
+
+    MockApiClient.addMockResponse({
+      url: `/organizations/${organization.slug}/events-stats/`,
+      body: [],
+    });
+
+    MockApiClient.addMockResponse({
+      url: `/organizations/${organization.slug}/profiling/flamegraph/`,
+      body: [],
+    });
+
+    render(
+      <ProfileSummaryPage
+        params={{}}
+        selection={TestStubs.GlobalSelection()}
+        location={TestStubs.location()}
+      />,
+      {
+        organization: TestStubs.Organization({
+          features: ['profiling-summary-redesign'],
+        }),
+        context: TestStubs.routerContext(),
+      }
+    );
+
+    expect(await screen.findByTestId(/profile-summary-redesign/i)).toBeInTheDocument();
+  });
+});