Browse Source

feat(ai-monitoring): Work on implementing the design (#70158)

Change fields in the table, make charts be line charts, move some gauges
around.
colin-sentry 10 months ago
parent
commit
9b097d5365

+ 68 - 37
static/app/views/aiMonitoring/PipelinesTable.tsx

@@ -1,3 +1,4 @@
+import styled from '@emotion/styled';
 import type {Location} from 'history';
 
 import GridEditable, {
@@ -7,7 +8,9 @@ import GridEditable, {
 import Link from 'sentry/components/links/link';
 import type {CursorHandler} from 'sentry/components/pagination';
 import Pagination from 'sentry/components/pagination';
+import SearchBar from 'sentry/components/searchBar';
 import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
 import type {Organization} from 'sentry/types/organization';
 import {browserHistory} from 'sentry/utils/browserHistory';
 import type {EventsMetaType} from 'sentry/utils/discover/eventView';
@@ -24,7 +27,6 @@ import {renderHeadCell} from 'sentry/views/starfish/components/tableCells/render
 import {useSpanMetrics} from 'sentry/views/starfish/queries/useSpanMetrics';
 import type {MetricsResponse} from 'sentry/views/starfish/types';
 import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters';
-import {DataTitles} from 'sentry/views/starfish/views/spans/types';
 
 type Row = Pick<
   MetricsResponse,
@@ -47,11 +49,6 @@ const COLUMN_ORDER: Column[] = [
     name: t('AI Pipeline Name'),
     width: COL_WIDTH_UNDEFINED,
   },
-  {
-    key: 'spm()',
-    name: `${t('Times')} ${RATE_UNIT_TITLE[RateUnit.PER_MINUTE]}`,
-    width: COL_WIDTH_UNDEFINED,
-  },
   {
     key: 'ai_total_tokens_used()',
     name: t('Total tokens used'),
@@ -59,12 +56,17 @@ const COLUMN_ORDER: Column[] = [
   },
   {
     key: `avg(span.duration)`,
-    name: DataTitles.avg,
+    name: t('Pipeline Duration'),
+    width: COL_WIDTH_UNDEFINED,
+  },
+  {
+    key: 'spm()',
+    name: `${t('Pipeline runs')} ${RATE_UNIT_TITLE[RateUnit.PER_MINUTE]}`,
     width: COL_WIDTH_UNDEFINED,
   },
 ];
 
-const SORTABLE_FIELDS = ['avg(span.duration)', 'spm()'];
+const SORTABLE_FIELDS = ['ai_total_tokens_used()', 'avg(span.duration)', 'spm()'];
 
 type ValidSort = Sort & {
   field: 'spm()' | 'avg(span.duration)';
@@ -79,13 +81,17 @@ export function PipelinesTable() {
   const organization = useOrganization();
   const cursor = decodeScalar(location.query?.[QueryParameterNames.SPANS_CURSOR]);
   const sortField = decodeScalar(location.query?.[QueryParameterNames.SPANS_SORT]);
+  const spanDescription = decodeScalar(location.query?.['span.description'], '');
 
   let sort = decodeSorts(sortField).filter(isAValidSort)[0];
   if (!sort) {
     sort = {field: 'spm()', kind: 'desc'};
   }
   const {data, isLoading, meta, pageLinks, error} = useSpanMetrics({
-    search: new MutableSearch('span.category:ai.pipeline'),
+    search: MutableSearch.fromQueryObject({
+      'span.category': 'ai.pipeline',
+      'span.description': spanDescription ? `*${spanDescription}*` : undefined,
+    }),
     fields: [
       'project.id',
       'span.group',
@@ -106,9 +112,10 @@ export function PipelinesTable() {
     isLoading: tokensUsedLoading,
     error: tokensUsedError,
   } = useSpanMetrics({
-    search: new MutableSearch(
-      `span.ai.pipeline.group:[${(data as Row[])?.map(x => x['span.group']).join(',')}] span.category:ai`
-    ),
+    search: MutableSearch.fromQueryObject({
+      'span.ai.pipeline.group': (data as Row[])?.map(x => x['span.group']).join(','),
+      'span.category': 'ai',
+    }),
     fields: ['span.ai.pipeline.group', 'ai_total_tokens_used()'],
   });
   if (!tokensUsedLoading) {
@@ -128,37 +135,55 @@ export function PipelinesTable() {
     });
   };
 
+  const handleSearch = (newQuery: string) => {
+    browserHistory.push({
+      ...location,
+      query: {
+        ...location.query,
+        'span.description': newQuery === '' ? undefined : newQuery,
+        [QueryParameterNames.SPANS_CURSOR]: undefined,
+      },
+    });
+  };
+
   return (
     <VisuallyCompleteWithData
       id="PipelinesTable"
       hasData={data.length > 0}
       isLoading={isLoading}
     >
-      <GridEditable
-        isLoading={isLoading}
-        error={error ?? tokensUsedError}
-        data={data}
-        columnOrder={COLUMN_ORDER}
-        columnSortBy={[
-          {
-            key: sort.field,
-            order: sort.kind,
-          },
-        ]}
-        grid={{
-          renderHeadCell: column =>
-            renderHeadCell({
-              column,
-              sort,
-              location,
-              sortParameterName: QueryParameterNames.SPANS_SORT,
-            }),
-          renderBodyCell: (column, row) =>
-            renderBodyCell(column, row, meta, location, organization),
-        }}
-        location={location}
-      />
-      <Pagination pageLinks={pageLinks} onCursor={handleCursor} />
+      <Container>
+        <SearchBar
+          placeholder={t('Search for pipeline')}
+          query={spanDescription}
+          onSearch={handleSearch}
+        />
+        <GridEditable
+          isLoading={isLoading}
+          error={error ?? tokensUsedError}
+          data={data}
+          columnOrder={COLUMN_ORDER}
+          columnSortBy={[
+            {
+              key: sort.field,
+              order: sort.kind,
+            },
+          ]}
+          grid={{
+            renderHeadCell: column =>
+              renderHeadCell({
+                column,
+                sort,
+                location,
+                sortParameterName: QueryParameterNames.SPANS_SORT,
+              }),
+            renderBodyCell: (column, row) =>
+              renderBodyCell(column, row, meta, location, organization),
+          }}
+          location={location}
+        />
+        <Pagination pageLinks={pageLinks} onCursor={handleCursor} />
+      </Container>
     </VisuallyCompleteWithData>
   );
 }
@@ -202,3 +227,9 @@ function renderBodyCell(
 
   return rendered;
 }
+
+const Container = styled('div')`
+  display: flex;
+  flex-direction: column;
+  gap: ${space(1)};
+`;

+ 4 - 4
static/app/views/aiMonitoring/aiMonitoringCharts.tsx

@@ -57,7 +57,7 @@ export function TotalTokensUsedChart({groupId}: TotalTokensUsedChartProps) {
             formula: '$total',
           },
         ]}
-        displayType={MetricDisplayType.AREA}
+        displayType={MetricDisplayType.LINE}
         chartHeight={200}
       />
     </TokenChartContainer>
@@ -113,7 +113,7 @@ export function NumberOfPipelinesChart({groupId}: NumberOfPipelinesChartProps) {
             formula: '$number',
           },
         ]}
-        displayType={MetricDisplayType.AREA}
+        displayType={MetricDisplayType.LINE}
         chartHeight={200}
       />
     </TokenChartContainer>
@@ -151,7 +151,7 @@ export function PipelineDurationChart({groupId}: PipelineDurationChartProps) {
   const lastMeta = timeseriesData?.meta?.findLast(_ => true);
   if (lastMeta && lastMeta.length >= 2) {
     // TODO hack: there is a bug somewhere that is dropping the unit
-    (lastMeta[1] as MetricsQueryApiResponseLastMeta).unit = 'millisecond';
+    (lastMeta[1] as MetricsQueryApiResponseLastMeta).unit ??= 'millisecond';
   }
 
   if (!isGlobalSelectionReady) {
@@ -174,7 +174,7 @@ export function PipelineDurationChart({groupId}: PipelineDurationChartProps) {
             formula: '$duration',
           },
         ]}
-        displayType={MetricDisplayType.AREA}
+        displayType={MetricDisplayType.LINE}
         chartHeight={200}
       />
     </TokenChartContainer>

+ 27 - 26
static/app/views/aiMonitoring/aiMonitoringDetailsPage.tsx

@@ -2,6 +2,7 @@ import styled from '@emotion/styled';
 
 import Feature from 'sentry/components/acl/feature';
 import {Alert} from 'sentry/components/alert';
+import {Breadcrumbs} from 'sentry/components/breadcrumbs';
 import * as Layout from 'sentry/components/layouts/thirds';
 import NoProjectMessage from 'sentry/components/noProjectMessage';
 import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
@@ -9,13 +10,13 @@ import {EnvironmentPageFilter} from 'sentry/components/organizations/environment
 import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
 import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
 import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
-import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
 import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import {DurationUnit, RateUnit} from 'sentry/utils/discover/fields';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
 import useOrganization from 'sentry/utils/useOrganization';
+import {normalizeUrl} from 'sentry/utils/withDomainRequired';
 import {
   NumberOfPipelinesChart,
   PipelineDurationChart,
@@ -30,7 +31,6 @@ import {
   SpanMetricsField,
   type SpanMetricsQueryFilters,
 } from 'sentry/views/starfish/types';
-import {DataTitles} from 'sentry/views/starfish/views/spans/types';
 
 function NoAccessComponent() {
   return (
@@ -93,15 +93,23 @@ export default function AiMonitoringPage({params}: Props) {
             <NoProjectMessage organization={organization}>
               <Layout.Header>
                 <Layout.HeaderContent>
-                  <Layout.Title>
-                    {`${t('AI Monitoring')} - ${spanMetrics['span.description'] ?? t('(no name)')}`}
-                    <PageHeadingQuestionTooltip
-                      title={t(
-                        'If this name is too generic, read the docs to learn how to change it.'
-                      )}
-                      docsUrl="https://docs.sentry.io/product/ai-monitoring/"
-                    />
-                  </Layout.Title>
+                  <Breadcrumbs
+                    crumbs={[
+                      {
+                        label: t('Dashboard'),
+                      },
+                      {
+                        label: t('AI Monitoring'),
+                      },
+                      {
+                        label: spanMetrics['span.description'] ?? t('(no name)'),
+                        to: normalizeUrl(
+                          `/organizations/${organization.slug}/ai-monitoring`
+                        ),
+                      },
+                    ]}
+                  />
+                  <Layout.Title>{t('AI Monitoring')}</Layout.Title>
                 </Layout.HeaderContent>
               </Layout.Header>
               <Layout.Body>
@@ -115,13 +123,6 @@ export default function AiMonitoringPage({params}: Props) {
                           <DatePageFilter />
                         </PageFilterBar>
                         <MetricsRibbon>
-                          <MetricReadout
-                            title={t('Total Runs')}
-                            value={spanMetrics['count()']}
-                            unit={'count'}
-                            isLoading={areSpanMetricsLoading}
-                          />
-
                           <MetricReadout
                             title={t('Total Tokens Used')}
                             value={tokenUsedMetric['ai_total_tokens_used()']}
@@ -130,20 +131,20 @@ export default function AiMonitoringPage({params}: Props) {
                           />
 
                           <MetricReadout
-                            title={t('Runs Per Minute')}
-                            value={spanMetrics?.[`${SpanFunction.SPM}()`]}
-                            unit={RateUnit.PER_MINUTE}
-                            isLoading={areSpanMetricsLoading}
-                          />
-
-                          <MetricReadout
-                            title={DataTitles.avg}
+                            title={t('Pipeline Duration')}
                             value={
                               spanMetrics?.[`avg(${SpanMetricsField.SPAN_DURATION})`]
                             }
                             unit={DurationUnit.MILLISECOND}
                             isLoading={areSpanMetricsLoading}
                           />
+
+                          <MetricReadout
+                            title={t('Pipeline Runs Per Minute')}
+                            value={spanMetrics?.[`${SpanFunction.SPM}()`]}
+                            unit={RateUnit.PER_MINUTE}
+                            isLoading={areSpanMetricsLoading}
+                          />
                         </MetricsRibbon>
                       </SpaceBetweenWrap>
                     </ModuleLayout.Full>

+ 11 - 0
static/app/views/aiMonitoring/landing.tsx

@@ -2,6 +2,7 @@ import {Fragment} from 'react';
 
 import Feature from 'sentry/components/acl/feature';
 import {Alert} from 'sentry/components/alert';
+import {Breadcrumbs} from 'sentry/components/breadcrumbs';
 import * as Layout from 'sentry/components/layouts/thirds';
 import NoProjectMessage from 'sentry/components/noProjectMessage';
 import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
@@ -48,6 +49,16 @@ export default function AiMonitoringPage() {
             <NoProjectMessage organization={organization}>
               <Layout.Header>
                 <Layout.HeaderContent>
+                  <Breadcrumbs
+                    crumbs={[
+                      {
+                        label: t('Dashboard'),
+                      },
+                      {
+                        label: t('AI Monitoring'),
+                      },
+                    ]}
+                  />
                   <Layout.Title>
                     {t('AI Monitoring')}
                     <PageHeadingQuestionTooltip

+ 6 - 6
static/app/views/aiMonitoring/pipelineSpansTable.tsx

@@ -31,14 +31,9 @@ type Column = GridColumnHeader<
 const COLUMN_ORDER: Column[] = [
   {
     key: SpanIndexedField.ID,
-    name: t('ID'),
+    name: t('Span ID'),
     width: COL_WIDTH_UNDEFINED,
   },
-  {
-    key: SpanIndexedField.SPAN_DURATION,
-    name: t('Total duration'),
-    width: 150,
-  },
   {
     key: SpanIndexedField.USER,
     name: t('User'),
@@ -49,6 +44,11 @@ const COLUMN_ORDER: Column[] = [
     name: t('Timestamp'),
     width: COL_WIDTH_UNDEFINED,
   },
+  {
+    key: SpanIndexedField.SPAN_DURATION,
+    name: t('Total duration'),
+    width: 150,
+  },
 ];
 
 const SORTABLE_FIELDS = [