Browse Source

feat(starfish): adds some improvements to api details slide out (#47664)

Updates the api details drawer to more closely match intended design
edwardgou-sentry 1 year ago
parent
commit
d7ebb8580e

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

@@ -283,6 +283,7 @@ function Chart({
               yAxis={areaChartProps.yAxes ? areaChartProps.yAxes[0] : []}
               tooltip={areaChartProps.tooltip}
               colors={colors}
+              grid={grid}
             />
           );
         }

+ 3 - 2
static/app/views/starfish/components/slideOverPanel.tsx

@@ -17,7 +17,7 @@ export default function SlideOverPanel({collapsed, children}: SlideOverPanelProp
       animate={!collapsed ? {opacity: 1, x: 0} : {opacity: 0, x: PANEL_WIDTH}}
       transition={{
         type: 'spring',
-        stiffness: 1000,
+        stiffness: 500,
         damping: 50,
       }}
     >
@@ -28,7 +28,8 @@ export default function SlideOverPanel({collapsed, children}: SlideOverPanelProp
 
 const _SlideOverPanel = styled(motion.div, {
   shouldForwardProp: prop =>
-    prop === 'animate' || (prop !== 'collapsed' && isPropValid(prop)),
+    ['animate', 'transition'].includes(prop) ||
+    (prop !== 'collapsed' && isPropValid(prop)),
 })<{
   collapsed: boolean;
 }>`

+ 1 - 1
static/app/views/starfish/modules/APIModule/endpointTable.tsx

@@ -90,7 +90,7 @@ export function renderBodyCell(
   return <OverflowEllipsisTextContainer>{row[column.key]}</OverflowEllipsisTextContainer>;
 }
 
-const OverflowEllipsisTextContainer = styled('span')`
+export const OverflowEllipsisTextContainer = styled('span')`
   text-overflow: ellipsis;
   overflow: hidden;
   white-space: nowrap;

+ 31 - 12
static/app/views/starfish/modules/APIModule/queries.js

@@ -1,8 +1,13 @@
-export const ENDPOINT_LIST_QUERY = `SELECT description, domain, quantile(0.5)(exclusive_time) AS "p50(exclusive_time)", uniq(user) as user_count, uniq(transaction) as transaction_count
+export const ENDPOINT_LIST_QUERY = `SELECT
+ description,
+ domain,
+ quantile(0.5)(exclusive_time) AS "p50(exclusive_time)",
+ uniq(user) as user_count, uniq(transaction) as transaction_count,
+ count() as count
  FROM spans_experimental_starfish
  WHERE module = 'http'
  GROUP BY description, domain
- ORDER BY "p50(exclusive_time)" DESC
+ ORDER BY count DESC
  LIMIT 10
 `;
 
@@ -20,15 +25,29 @@ export const ENDPOINT_GRAPH_QUERY = `SELECT
 
 export const getEndpointDetailSeriesQuery = description => {
   return `SELECT
-    toStartOfInterval(start_timestamp, INTERVAL 12 HOUR) as interval,
-    quantile(0.5)(exclusive_time) as p50,
-    count() as count
-    FROM spans_experimental_starfish
-    WHERE module = 'http'
-    AND description = '${description}'
-    GROUP BY interval
-    ORDER BY interval asc
- `;
+     toStartOfInterval(start_timestamp, INTERVAL 12 HOUR) as interval,
+     quantile(0.5)(exclusive_time) as p50,
+     quantile(0.95)(exclusive_time) as p95,
+     count() as count
+     FROM spans_experimental_starfish
+     WHERE module = 'http'
+     AND description = '${description}'
+     GROUP BY interval
+     ORDER BY interval asc
+  `;
+};
+
+export const getEndpointDetailErrorRateSeriesQuery = description => {
+  return `SELECT
+     toStartOfInterval(start_timestamp, INTERVAL 12 HOUR) as interval,
+     count() as count
+     FROM spans_experimental_starfish
+     WHERE module = 'http'
+     AND description = '${description}'
+     AND status >= 400 AND status < 600
+     GROUP BY interval
+     ORDER BY interval asc
+  `;
 };
 
 export const getEndpointDetailQuery = description => {
@@ -39,7 +58,7 @@ export const getEndpointDetailQuery = description => {
     AND description = '${description}'
     GROUP BY transaction
     ORDER BY count DESC
-    LIMIT 10
+    LIMIT 5
  `;
 };
 

+ 120 - 46
static/app/views/starfish/views/endpointDetails/index.tsx

@@ -3,6 +3,7 @@ import styled from '@emotion/styled';
 import {useQuery} from '@tanstack/react-query';
 import moment from 'moment';
 
+import Duration from 'sentry/components/duration';
 import GridEditable, {GridColumnHeader} from 'sentry/components/gridEditable';
 import Link from 'sentry/components/links/link';
 import {t} from 'sentry/locale';
@@ -11,8 +12,12 @@ import {useLocation} from 'sentry/utils/useLocation';
 import Chart from 'sentry/views/starfish/components/chart';
 import Detail from 'sentry/views/starfish/components/detailPanel';
 import {HOST} from 'sentry/views/starfish/modules/APIModule/APIModuleView';
-import {renderHeadCell} from 'sentry/views/starfish/modules/APIModule/endpointTable';
 import {
+  OverflowEllipsisTextContainer,
+  renderHeadCell,
+} from 'sentry/views/starfish/modules/APIModule/endpointTable';
+import {
+  getEndpointDetailErrorRateSeriesQuery,
   getEndpointDetailQuery,
   getEndpointDetailSeriesQuery,
 } from 'sentry/views/starfish/modules/APIModule/queries';
@@ -38,7 +43,7 @@ const COLUMN_ORDER = [
   {
     key: 'transaction',
     name: 'Transaction',
-    width: 400,
+    width: 350,
   },
   {
     key: 'count',
@@ -62,23 +67,33 @@ export default function EndpointDetail({
 
 function EndpointDetailBody({row}: EndpointDetailBodyProps) {
   const location = useLocation();
-  const theme = useTheme();
   const seriesQuery = getEndpointDetailSeriesQuery(row.description);
+  const errorRateSeriesQuery = getEndpointDetailErrorRateSeriesQuery(row.description);
   const tableQuery = getEndpointDetailQuery(row.description);
   const {isLoading: seriesIsLoading, data: seriesData} = useQuery({
-    queryKey: ['endpointDetailSeries'],
+    queryKey: [seriesQuery],
     queryFn: () => fetch(`${HOST}/?query=${seriesQuery}`).then(res => res.json()),
-    retry: true,
+    retry: false,
+    initialData: [],
+  });
+  const {isLoading: errorRateSeriesIsLoading, data: errorRateSeriesData} = useQuery({
+    queryKey: [errorRateSeriesQuery],
+    queryFn: () =>
+      fetch(`${HOST}/?query=${errorRateSeriesQuery}`).then(res => res.json()),
+    retry: false,
     initialData: [],
   });
   const {isLoading: tableIsLoading, data: tableData} = useQuery({
-    queryKey: ['endpointDetailTable'],
+    queryKey: [tableQuery],
     queryFn: () => fetch(`${HOST}/?query=${tableQuery}`).then(res => res.json()),
-    retry: true,
+    retry: false,
     initialData: [],
   });
-  const [countSeries, p50Series] = endpointDetailDataToChartData(seriesData).map(series =>
-    zeroFillSeries(series, moment.duration(12, 'hours'))
+  const [p50Series, p95Series, countSeries] = endpointDetailDataToChartData(
+    seriesData
+  ).map(series => zeroFillSeries(series, moment.duration(12, 'hours')));
+  const [errorRateSeries] = endpointDetailDataToChartData(errorRateSeriesData).map(
+    series => zeroFillSeries(series, moment.duration(12, 'hours'))
   );
 
   return (
@@ -94,41 +109,44 @@ function EndpointDetailBody({row}: EndpointDetailBodyProps) {
       <SubHeader>{t('Domain')}</SubHeader>
       <pre>{row?.domain}</pre>
       <FlexRowContainer>
+        <FlexRowItem>
+          <SubHeader>{t('Duration (P50)')}</SubHeader>
+          <SubSubHeader>{'123ms'}</SubSubHeader>
+          <APIDetailChart
+            series={p50Series}
+            isLoading={seriesIsLoading}
+            index={2}
+            outOf={4}
+          />
+        </FlexRowItem>
+        <FlexRowItem>
+          <SubHeader>{t('Duration (P95)')}</SubHeader>
+          <SubSubHeader>{'123ms'}</SubSubHeader>
+          <APIDetailChart
+            series={p95Series}
+            isLoading={seriesIsLoading}
+            index={3}
+            outOf={4}
+          />
+        </FlexRowItem>
         <FlexRowItem>
           <SubHeader>{t('Throughput')}</SubHeader>
           <SubSubHeader>{row.count}</SubSubHeader>
-          <Chart
-            statsPeriod="24h"
-            height={140}
-            data={[countSeries]}
-            start=""
-            end=""
-            loading={seriesIsLoading}
-            utc={false}
-            disableMultiAxis
-            stacked
-            isLineChart
-            disableXAxis
-            hideYAxisSplitLine
+          <APIDetailChart
+            series={countSeries}
+            isLoading={seriesIsLoading}
+            index={0}
+            outOf={4}
           />
         </FlexRowItem>
         <FlexRowItem>
-          <SubHeader>{t('Duration (P50)')}</SubHeader>
-          <SubSubHeader>{'123ms'}</SubSubHeader>
-          <Chart
-            statsPeriod="24h"
-            height={140}
-            data={[p50Series]}
-            start=""
-            end=""
-            loading={seriesIsLoading}
-            utc={false}
-            chartColors={[theme.charts.getColorPalette(4)[3]]}
-            disableMultiAxis
-            stacked
-            isLineChart
-            disableXAxis
-            hideYAxisSplitLine
+          <SubHeader>{t('Error Rate')}</SubHeader>
+          <SubSubHeader>{row.count}</SubSubHeader>
+          <APIDetailChart
+            series={errorRateSeries}
+            isLoading={errorRateSeriesIsLoading}
+            index={1}
+            outOf={4}
           />
         </FlexRowItem>
       </FlexRowContainer>
@@ -165,22 +183,72 @@ function renderBodyCell(
     );
   }
 
-  return row[column.key];
+  if (column.key.toString().match(/^p\d\d/)) {
+    return <Duration seconds={row[column.key] / 1000} fixedDigits={2} abbreviation />;
+  }
+
+  return <OverflowEllipsisTextContainer>{row[column.key]}</OverflowEllipsisTextContainer>;
 }
 
 function endpointDetailDataToChartData(data: any) {
-  const countSeries = {seriesName: 'count()', data: [] as any[]};
-  const p50Series = {seriesName: 'p50()', data: [] as any[]};
-  data.forEach(({count, p50, interval}: any) => {
-    countSeries.data.push({value: count, name: interval});
-    p50Series.data.push({value: p50, name: interval});
+  const series = [] as any[];
+  if (data.length > 0) {
+    Object.keys(data[0])
+      .filter(key => key !== 'interval')
+      .forEach(key => {
+        series.push({seriesName: `${key}()`, data: [] as any[]});
+      });
+  }
+  data.forEach(point => {
+    Object.keys(point).forEach(key => {
+      if (key !== 'interval') {
+        series
+          .find(serie => serie.seriesName === `${key}()`)
+          ?.data.push({
+            name: point.interval,
+            value: point[key],
+          });
+      }
+    });
   });
-  return [countSeries, p50Series];
+  return series;
+}
+
+function APIDetailChart(props: {
+  index: number;
+  isLoading: boolean;
+  outOf: number;
+  series: any;
+}) {
+  const theme = useTheme();
+  return (
+    <Chart
+      statsPeriod="24h"
+      height={110}
+      data={props.series ? [props.series] : []}
+      start=""
+      end=""
+      loading={props.isLoading}
+      utc={false}
+      disableMultiAxis
+      stacked
+      isLineChart
+      disableXAxis
+      hideYAxisSplitLine
+      chartColors={[theme.charts.getColorPalette(props.outOf - 2)[props.index]]}
+      grid={{
+        left: '0',
+        right: '0',
+        top: '8px',
+        bottom: '16px',
+      }}
+    />
+  );
 }
 
 const SubHeader = styled('h3')`
   color: ${p => p.theme.gray300};
-  font-size: ${p => p.theme.fontSizeLarge};
+  font-size: ${p => p.theme.fontSizeMedium};
   margin: 0;
   margin-bottom: ${space(1)};
 `;
@@ -195,9 +263,15 @@ const FlexRowContainer = styled('div')`
   & > div:last-child {
     padding-right: ${space(1)};
   }
+  flex-wrap: wrap;
 `;
 
 const FlexRowItem = styled('div')`
   padding-right: ${space(4)};
   flex: 1;
+  flex-grow: 0;
+  min-width: 280px;
+  & > h3 {
+    margin-bottom: 0;
+  }
 `;