Browse Source

feat(browser-starfish): add resource summary page (#57848)

When you click on a resource in the resources module, the following
resource summary page will appear instead of the sidebar.
<img width="1291" alt="image"
src="https://github.com/getsentry/sentry/assets/44422760/7a37f51c-cb9e-4386-942e-da55dfd7256c">


This isn't the final page, we should additionally add a resource size
info, but we will work off this.

The reason for adding this is:
1. To provide some consistency between db and resource module.
2. To provide some additional context aka location, on what pages a
specific resource was loaded in.
3. To provide a place to add more content if we get bundle level
information.
Dominik Buszowiecki 1 year ago
parent
commit
3281fe655f

+ 16 - 6
static/app/routes.tsx

@@ -1630,12 +1630,22 @@ function buildRoutes() {
               import('sentry/views/performance/browser/webVitals/webVitalsLandingPage')
           )}
         />
-        <Route
-          path="resources/"
-          component={make(
-            () => import('sentry/views/performance/browser/resources/index')
-          )}
-        />
+        <Route path="resources/">
+          <IndexRoute
+            component={make(
+              () => import('sentry/views/performance/browser/resources/index')
+            )}
+          />
+          <Route
+            path="resource/:groupId/"
+            component={make(
+              () =>
+                import(
+                  'sentry/views/performance/browser/resources/resourceSummaryPage/index'
+                )
+            )}
+          />
+        </Route>
       </Route>
       <Route path="summary/">
         <IndexRoute

+ 105 - 0
static/app/views/performance/browser/resources/resourceSummaryPage/index.tsx

@@ -0,0 +1,105 @@
+import styled from '@emotion/styled';
+
+import Breadcrumbs from 'sentry/components/breadcrumbs';
+import DatePageFilter from 'sentry/components/datePageFilter';
+import FeatureBadge from 'sentry/components/featureBadge';
+import * as Layout from 'sentry/components/layouts/thirds';
+import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
+import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
+import {t} from 'sentry/locale';
+import {RateUnits} from 'sentry/utils/discover/fields';
+import useOrganization from 'sentry/utils/useOrganization';
+import {useParams} from 'sentry/utils/useParams';
+import {normalizeUrl} from 'sentry/utils/withDomainRequired';
+import {PaddedContainer} from 'sentry/views/performance/browser/resources';
+import ResourceSummaryCharts from 'sentry/views/performance/browser/resources/resourceSummaryPage/resourceSummaryCharts';
+import ResourceSummaryTable from 'sentry/views/performance/browser/resources/resourceSummaryPage/resourceSummaryTable';
+import {ModulePageProviders} from 'sentry/views/performance/database/modulePageProviders';
+import {DurationCell} from 'sentry/views/starfish/components/tableCells/durationCell';
+import {ThroughputCell} from 'sentry/views/starfish/components/tableCells/throughputCell';
+import {useSpanMetrics} from 'sentry/views/starfish/queries/useSpanMetrics';
+import {SpanFunction, SpanMetricsField} from 'sentry/views/starfish/types';
+import {DataTitles, getThroughputTitle} from 'sentry/views/starfish/views/spans/types';
+import {Block, BlockContainer} from 'sentry/views/starfish/views/spanSummaryPage/block';
+
+function ResourceSummary() {
+  const organization = useOrganization();
+  const {groupId} = useParams();
+  const {data: spanMetrics} = useSpanMetrics(groupId, {}, [
+    'avg(span.self_time)',
+    'spm()',
+    'span.op',
+    'span.description',
+  ]);
+
+  return (
+    <ModulePageProviders
+      title={[t('Performance'), t('Resources'), t('Resource Summary')].join(' — ')}
+    >
+      <Layout.Header>
+        <Layout.HeaderContent>
+          <Breadcrumbs
+            crumbs={[
+              {
+                label: 'Performance',
+                to: normalizeUrl(`/organizations/${organization.slug}/performance/`),
+                preservePageFilters: true,
+              },
+              {
+                label: 'Resources',
+                to: normalizeUrl(
+                  `/organizations/${organization.slug}/performance/browser/resources/`
+                ),
+                preservePageFilters: true,
+              },
+              {
+                label: 'Resource Summary',
+              },
+            ]}
+          />
+
+          <Layout.Title>
+            {spanMetrics[SpanMetricsField.SPAN_DESCRIPTION]}
+            <FeatureBadge type="alpha" />
+          </Layout.Title>
+        </Layout.HeaderContent>
+      </Layout.Header>
+
+      <Layout.Body>
+        <Layout.Main fullWidth>
+          <HeaderContainer>
+            <PaddedContainer>
+              <PageFilterBar condensed>
+                <ProjectPageFilter />
+                <DatePageFilter alignDropdown="left" />
+              </PageFilterBar>
+            </PaddedContainer>
+            <BlockContainer>
+              <Block title={DataTitles.avg}>
+                <DurationCell
+                  milliseconds={spanMetrics?.[`avg(${SpanMetricsField.SPAN_SELF_TIME})`]}
+                />
+              </Block>
+              <Block title={getThroughputTitle('http')}>
+                <ThroughputCell
+                  rate={spanMetrics?.[`${SpanFunction.SPM}()`] * 60}
+                  unit={RateUnits.PER_SECOND}
+                />
+              </Block>
+            </BlockContainer>
+          </HeaderContainer>
+          <ResourceSummaryCharts groupId={groupId} />
+          <ResourceSummaryTable />
+        </Layout.Main>
+      </Layout.Body>
+    </ModulePageProviders>
+  );
+}
+
+const HeaderContainer = styled('div')`
+  display: flex;
+  justify-content: space-between;
+  flex-wrap: wrap;
+`;
+
+export default ResourceSummary;

+ 30 - 0
static/app/views/performance/browser/resources/resourceSummaryPage/resourceSummaryCharts.tsx

@@ -0,0 +1,30 @@
+import {AVG_COLOR} from 'sentry/views/starfish/colours';
+import Chart from 'sentry/views/starfish/components/chart';
+import ChartPanel from 'sentry/views/starfish/components/chartPanel';
+import {useSpanMetricsSeries} from 'sentry/views/starfish/queries/useSpanMetricsSeries';
+import {SpanMetricsField} from 'sentry/views/starfish/types';
+import {getDurationChartTitle} from 'sentry/views/starfish/views/spans/types';
+import {Block} from 'sentry/views/starfish/views/spanSummaryPage/block';
+
+function ResourceSummaryCharts(props: {groupId: string}) {
+  const {data: spanMetricsSeriesData, isLoading: areSpanMetricsSeriesLoading} =
+    useSpanMetricsSeries(props.groupId, {}, [`avg(${SpanMetricsField.SPAN_SELF_TIME})`]);
+
+  return (
+    <Block>
+      <ChartPanel title={getDurationChartTitle('http')}>
+        <Chart
+          height={160}
+          data={[spanMetricsSeriesData?.[`avg(${SpanMetricsField.SPAN_SELF_TIME})`]]}
+          loading={areSpanMetricsSeriesLoading}
+          utc={false}
+          chartColors={[AVG_COLOR]}
+          isLineChart
+          definedAxisTicks={4}
+        />
+      </ChartPanel>
+    </Block>
+  );
+}
+
+export default ResourceSummaryCharts;

+ 93 - 0
static/app/views/performance/browser/resources/resourceSummaryPage/resourceSummaryTable.tsx

@@ -0,0 +1,93 @@
+import {Fragment} from 'react';
+
+import GridEditable, {
+  COL_WIDTH_UNDEFINED,
+  GridColumnHeader,
+  GridColumnOrder,
+} from 'sentry/components/gridEditable';
+import {RateUnits} from 'sentry/utils/discover/fields';
+import {useLocation} from 'sentry/utils/useLocation';
+import {useParams} from 'sentry/utils/useParams';
+import {useResourcePagesQuery} from 'sentry/views/performance/browser/resources/utils/useResourcePageQuery';
+import {DurationCell} from 'sentry/views/starfish/components/tableCells/durationCell';
+import {renderHeadCell} from 'sentry/views/starfish/components/tableCells/renderHeadCell';
+import {ThroughputCell} from 'sentry/views/starfish/components/tableCells/throughputCell';
+
+type Row = {
+  'avg(span.self_time)': number;
+  'spm()': number;
+  transaction: string;
+};
+
+type Column = GridColumnHeader<keyof Row>;
+
+function ResourceSummaryTable() {
+  const location = useLocation();
+  const {groupId} = useParams();
+  const {data, isLoading} = useResourcePagesQuery(groupId);
+
+  const columnOrder: GridColumnOrder<keyof Row>[] = [
+    {key: 'transaction', width: COL_WIDTH_UNDEFINED, name: 'Found on page'},
+    {
+      key: 'spm()',
+      width: COL_WIDTH_UNDEFINED,
+      name: 'Throughput',
+    },
+    {
+      key: 'avg(span.self_time)',
+      width: COL_WIDTH_UNDEFINED,
+      name: 'Avg Duration',
+    },
+  ];
+
+  const renderBodyCell = (col: Column, row: Row) => {
+    const {key} = col;
+    if (key === 'spm()') {
+      return <ThroughputCell rate={row[key] * 60} unit={RateUnits.PER_SECOND} />;
+    }
+    if (key === 'avg(span.self_time)') {
+      return <DurationCell milliseconds={row[key]} />;
+    }
+    return <span>{row[key]}</span>;
+  };
+
+  return (
+    <Fragment>
+      <GridEditable
+        data={data || []}
+        isLoading={isLoading}
+        columnOrder={columnOrder}
+        columnSortBy={[
+          {
+            key: 'avg(span.self_time)',
+            order: 'desc',
+          },
+        ]}
+        grid={{
+          renderHeadCell: column =>
+            renderHeadCell({
+              column,
+              location,
+              sort: {
+                field: 'avg(span.self_time)',
+                kind: 'desc',
+              },
+            }),
+          renderBodyCell,
+        }}
+        location={location}
+      />
+    </Fragment>
+  );
+}
+
+export const getActionName = (transactionOp: string) => {
+  switch (transactionOp) {
+    case 'ui.action.click':
+      return 'Click';
+    default:
+      return transactionOp;
+  }
+};
+
+export default ResourceSummaryTable;

+ 2 - 9
static/app/views/performance/browser/resources/resourceTable.tsx

@@ -1,6 +1,5 @@
 import {Fragment} from 'react';
 import {Link} from 'react-router';
-import * as qs from 'query-string';
 
 import FileSize from 'sentry/components/fileSize';
 import GridEditable, {
@@ -12,7 +11,6 @@ import Pagination from 'sentry/components/pagination';
 import {t} from 'sentry/locale';
 import {RateUnits} from 'sentry/utils/discover/fields';
 import {useLocation} from 'sentry/utils/useLocation';
-import {BrowserStarfishFields} from 'sentry/views/performance/browser/resources/utils/useResourceFilters';
 import {ValidSort} from 'sentry/views/performance/browser/resources/utils/useResourceSort';
 import {useResourcesQuery} from 'sentry/views/performance/browser/resources/utils/useResourcesQuery';
 import {DurationCell} from 'sentry/views/starfish/components/tableCells/durationCell';
@@ -73,7 +71,6 @@ function ResourceTable({sort}: Props) {
           Math.random() * (1000 - 500) + 500
         ),
         'http.response_content_length': Math.floor(Math.random() * (500 - 50) + 50),
-        'span.group': 'group123',
         domain: 's1.sentry-cdn.com',
       }))
     : [];
@@ -81,18 +78,14 @@ function ResourceTable({sort}: Props) {
   const renderBodyCell = (col: Column, row: Row) => {
     const {key} = col;
     if (key === 'span.description') {
-      const query = {
-        ...location.query,
-        [BrowserStarfishFields.DESCRIPTION]: row[key],
-      };
       return (
-        <Link to={`/performance/browser/resources/?${qs.stringify(query)}`}>
+        <Link to={`/performance/browser/resources/resource/${row['span.group']}`}>
           {row[key]}
         </Link>
       );
     }
     if (key === 'spm()') {
-      return <ThroughputCell rate={row[key]} unit={RateUnits.PER_SECOND} />;
+      return <ThroughputCell rate={row[key] * 60} unit={RateUnits.PER_SECOND} />;
     }
     if (key === 'http.response_content_length') {
       return <FileSize bytes={row[key]} />;

+ 6 - 0
static/app/views/performance/browser/resources/utils/useResourcePageQuery.ts

@@ -0,0 +1,6 @@
+import {useSpanTransactionMetrics} from 'sentry/views/starfish/queries/useSpanTransactionMetrics';
+
+export const useResourcePagesQuery = (groupId: string) => {
+  // We'll do more this when we have the transaction tag on resource spans.
+  return useSpanTransactionMetrics({'span.group': groupId});
+};

+ 2 - 0
static/app/views/performance/browser/resources/utils/useResourcesQuery.ts

@@ -28,6 +28,7 @@ export const useResourcesQuery = ({sort}: {sort: ValidSort}) => {
         'count()',
         'avg(span.self_time)',
         'spm()',
+        'span.group',
         'resource.render_blocking_status',
       ],
       name: 'Resource module - resource table',
@@ -51,6 +52,7 @@ export const useResourcesQuery = ({sort}: {sort: ValidSort}) => {
     'avg(span.self_time)': row['avg(span.self_time)'] as number,
     'count()': row['count()'] as number,
     'spm()': row['spm()'] as number,
+    'span.group': row['span.group'].toString(),
     'resource.render_blocking_status': row['resource.render_blocking_status'] as
       | ''
       | 'non-blocking'

+ 1 - 1
static/app/views/starfish/components/spanDescription.tsx

@@ -12,7 +12,7 @@ type Props = {
 };
 
 export function SpanDescription({span}: Props) {
-  if (span[SpanMetricsField.SPAN_OP].startsWith('db')) {
+  if (span[SpanMetricsField.SPAN_OP]?.startsWith('db')) {
     return <DatabaseSpanDescription span={span} />;
   }