Browse Source

feat(browser-starfish): init interactions summary (#55936)

I really wanted to play around with the e2e flow of the interactions
module, so I scaffolded it and put together a basic table we can work
off of.

<img width="1290" alt="image"
src="https://github.com/getsentry/sentry/assets/44422760/f07bb4ce-eb40-4d82-b99a-c0277a8d76a7">
Dominik Buszowiecki 1 year ago
parent
commit
6872fea4cb

+ 13 - 6
static/app/routes.tsx

@@ -1588,12 +1588,19 @@ function buildRoutes() {
         />
       </Route>
       <Route path="browser/">
-        <Route
-          path="interactions/"
-          component={make(
-            () => import('sentry/views/performance/browser/interactionsLandingPage')
-          )}
-        />
+        <Route path="interactions/">
+          <IndexRoute
+            component={make(
+              () => import('sentry/views/performance/browser/interactionsLandingPage')
+            )}
+          />
+          <Route
+            path="summary/"
+            component={make(
+              () => import('sentry/views/performance/browser/interactionSummary/index')
+            )}
+          />
+        </Route>
       </Route>
       <Route path="summary/">
         <IndexRoute

+ 65 - 0
static/app/views/performance/browser/interactionSummary/index.tsx

@@ -0,0 +1,65 @@
+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 {t} from 'sentry/locale';
+import useOrganization from 'sentry/utils/useOrganization';
+import {normalizeUrl} from 'sentry/utils/withDomainRequired';
+import {PaddedContainer} from 'sentry/views/performance/browser/interactionsLandingPage';
+import InteractionSampleTable from 'sentry/views/performance/browser/interactionSummary/sampleTable';
+import {getActionName} from 'sentry/views/performance/browser/interactionTable';
+import {useBrowserModuleFilters} from 'sentry/views/performance/browser/useBrowserFilters';
+import {ModulePageProviders} from 'sentry/views/performance/database/modulePageProviders';
+
+function InteractionSummary() {
+  const organization = useOrganization();
+  const browserFilters = useBrowserModuleFilters();
+
+  return (
+    <ModulePageProviders title={[t('Performance'), t('Interactions')].join(' — ')}>
+      <Layout.Header>
+        <Layout.HeaderContent>
+          <Breadcrumbs
+            crumbs={[
+              {
+                label: 'Performance',
+                to: normalizeUrl(`/organizations/${organization.slug}/performance/`),
+                preservePageFilters: true,
+              },
+              {
+                label: 'Interactions',
+                to: normalizeUrl(
+                  `/organizations/${organization.slug}/performance/browser/interactions/`
+                ),
+                preservePageFilters: true,
+              },
+              {
+                label: 'Interaction Summary',
+              },
+            ]}
+          />
+
+          <Layout.Title>
+            {getActionName(browserFilters?.['transaction.op'] || '')}
+            {` ${browserFilters.component} on ${browserFilters.page}`}
+            <FeatureBadge type="alpha" />
+          </Layout.Title>
+        </Layout.HeaderContent>
+      </Layout.Header>
+
+      <Layout.Body>
+        <Layout.Main fullWidth>
+          <PaddedContainer>
+            <PageFilterBar condensed>
+              <DatePageFilter alignDropdown="left" />
+            </PageFilterBar>
+          </PaddedContainer>
+          <InteractionSampleTable />
+        </Layout.Main>
+      </Layout.Body>
+    </ModulePageProviders>
+  );
+}
+
+export default InteractionSummary;

+ 69 - 0
static/app/views/performance/browser/interactionSummary/sampleTable.tsx

@@ -0,0 +1,69 @@
+import {Fragment} from 'react';
+import {Link} from 'react-router';
+
+import GridEditable, {
+  COL_WIDTH_UNDEFINED,
+  GridColumnHeader,
+  GridColumnOrder,
+} from 'sentry/components/gridEditable';
+import Pagination from 'sentry/components/pagination';
+import {useLocation} from 'sentry/utils/useLocation';
+import {useInteractionQuery} from 'sentry/views/performance/browser/interactionSummary/useInteractionQuery';
+import {DurationCell} from 'sentry/views/starfish/components/tableCells/durationCell';
+import {renderHeadCell} from 'sentry/views/starfish/components/tableCells/renderHeadCell';
+
+type Row = {
+  eventId: string;
+  project: string;
+  'transaction.duration': number;
+};
+
+type Column = GridColumnHeader<keyof Row>;
+
+function InteractionSampleTable() {
+  const location = useLocation();
+  const columnOrder: GridColumnOrder<keyof Row>[] = [
+    {key: 'eventId', width: COL_WIDTH_UNDEFINED, name: 'event'},
+    {key: 'transaction.duration', width: COL_WIDTH_UNDEFINED, name: 'duration'},
+  ];
+  const {data, isLoading, pageLinks} = useInteractionQuery();
+  const tableData: Row[] = data.length ? data : [];
+
+  const renderBodyCell = (col: Column, row: Row) => {
+    const {key} = col;
+    if (key === 'transaction.duration') {
+      return <DurationCell milliseconds={row[key]} />;
+    }
+    if (key === 'eventId') {
+      return (
+        <Link to={`/performance/${row.project}:${row.eventId}`}>
+          {row.eventId.slice(0, 8)}
+        </Link>
+      );
+    }
+    return <span>{row[key]}</span>;
+  };
+
+  return (
+    <Fragment>
+      <GridEditable
+        data={tableData}
+        isLoading={isLoading}
+        columnOrder={columnOrder}
+        columnSortBy={[]}
+        grid={{
+          renderHeadCell: column =>
+            renderHeadCell({
+              column,
+              location,
+            }),
+          renderBodyCell,
+        }}
+        location={location}
+      />
+      <Pagination pageLinks={pageLinks} />
+    </Fragment>
+  );
+}
+
+export default InteractionSampleTable;

+ 39 - 0
static/app/views/performance/browser/interactionSummary/useInteractionQuery.ts

@@ -0,0 +1,39 @@
+import {useDiscoverQuery} from 'sentry/utils/discover/discoverQuery';
+import EventView from 'sentry/utils/discover/eventView';
+import {useLocation} from 'sentry/utils/useLocation';
+import useOrganization from 'sentry/utils/useOrganization';
+import usePageFilters from 'sentry/utils/usePageFilters';
+import {useBrowserModuleFilters} from 'sentry/views/performance/browser/useBrowserFilters';
+
+export const useInteractionQuery = () => {
+  const pageFilters = usePageFilters();
+  const browserFilters = useBrowserModuleFilters();
+  const location = useLocation();
+  const {slug: orgSlug} = useOrganization();
+  const queryConditions = [
+    `transaction:"${browserFilters.page}"`,
+    `interactionElement:"${browserFilters.component?.replaceAll('"', '\\"')}"`,
+  ];
+
+  // TODO - we should be using metrics data here
+  const eventView = EventView.fromNewQueryWithPageFilters(
+    {
+      fields: ['id', 'transaction.duration', 'project'],
+      name: 'Interaction module - interactions table',
+      query: queryConditions.join(' '),
+      orderby: '-transaction.duration',
+      version: 2,
+    },
+    pageFilters.selection
+  );
+
+  const result = useDiscoverQuery({eventView, limit: 15, location, orgSlug});
+
+  const data = result?.data?.data.map(row => ({
+    eventId: row.id.toString(),
+    project: row.project.toString(),
+    'transaction.duration': row['transaction.duration'] as number,
+  }));
+
+  return {...result, data: data || []};
+};

+ 10 - 3
static/app/views/performance/browser/interactionTable.tsx

@@ -1,5 +1,6 @@
 import {Fragment} from 'react';
 import {Link} from 'react-router';
+import * as qs from 'query-string';
 
 import GridEditable, {
   COL_WIDTH_UNDEFINED,
@@ -8,6 +9,7 @@ import GridEditable, {
 } from 'sentry/components/gridEditable';
 import Pagination from 'sentry/components/pagination';
 import {useLocation} from 'sentry/utils/useLocation';
+import {BrowserStarfishFields} from 'sentry/views/performance/browser/useBrowserFilters';
 import {ValidSort} from 'sentry/views/performance/browser/useBrowserSort';
 import {useInteractionsQuery} from 'sentry/views/performance/browser/useInteractionsQuery';
 import {DurationCell} from 'sentry/views/starfish/components/tableCells/durationCell';
@@ -53,9 +55,14 @@ function InteractionsTable({sort}: Props) {
   const renderBodyCell = (col: Column, row: Row) => {
     const {key} = col;
     if (key === 'span.group') {
-      const spanGroup = row['span.group'];
       return (
-        <Link to={`/performance/browser/interactions/${spanGroup}`}>
+        <Link
+          to={`/performance/browser/interactions/summary/?${qs.stringify({
+            [BrowserStarfishFields.COMPONENT]: row.interactionElement,
+            [BrowserStarfishFields.PAGE]: row.transaction,
+            [BrowserStarfishFields.TRANSACTION_OP]: row['transaction.op'],
+          })}`}
+        >
           {getActionName(row['transaction.op'])}
           <span style={{fontWeight: 'bold'}}> {row.interactionElement}</span>
         </Link>
@@ -98,7 +105,7 @@ function InteractionsTable({sort}: Props) {
   );
 }
 
-const getActionName = (transactionOp: string) => {
+export const getActionName = (transactionOp: string) => {
   switch (transactionOp) {
     case 'ui.action.click':
       return 'Click';

+ 1 - 1
static/app/views/performance/browser/interactionsLandingPage.tsx

@@ -181,7 +181,7 @@ function SelectControlWithProps(props: ControlProps & {options: Option[]}) {
   return <SelectControl {...props} />;
 }
 
-const PaddedContainer = styled('div')`
+export const PaddedContainer = styled('div')`
   margin-bottom: ${space(2)};
 `;
 

+ 1 - 0
static/app/views/starfish/components/tableCells/renderHeadCell.tsx

@@ -30,6 +30,7 @@ export const SORTABLE_FIELDS = new Set([
   `avg(${SPAN_SELF_TIME})`,
   `p95(${SPAN_SELF_TIME})`,
   `p75(transaction.duration)`,
+  `transaction.duration`,
   'transaction',
   `count()`,
   `${SPS}()`,