Просмотр исходного кода

feat: add ability to power LLM monitoring with EAP (#77750)

Requires https://github.com/getsentry/sentry/pull/77749 first, this
implements the LLM monitoring frontend using EAP instead of metrics.
colin-sentry 5 месяцев назад
Родитель
Сommit
30d12eba68

+ 62 - 16
static/app/components/events/interfaces/llm-monitoring/llmMonitoringSection.tsx

@@ -1,4 +1,3 @@
-import Alert from 'sentry/components/alert';
 import {LinkButton} from 'sentry/components/button';
 import ButtonBar from 'sentry/components/buttonBar';
 import {IconOpen} from 'sentry/icons';
@@ -7,13 +6,22 @@ import type {Event} from 'sentry/types/event';
 import type {Organization} from 'sentry/types/organization';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
 import * as ModuleLayout from 'sentry/views/insights/common/components/moduleLayout';
-import {useSpansIndexed} from 'sentry/views/insights/common/queries/useDiscover';
+import {
+  useEAPSpans,
+  useSpansIndexed,
+} from 'sentry/views/insights/common/queries/useDiscover';
 import {useModuleURL} from 'sentry/views/insights/common/utils/useModuleURL';
 import {
+  EAPNumberOfPipelinesChart,
+  EAPTotalTokensUsedChart,
   NumberOfPipelinesChart,
   TotalTokensUsedChart,
 } from 'sentry/views/insights/llmMonitoring/components/charts/llmMonitoringCharts';
-import {SpanIndexedField, type SpanIndexedResponse} from 'sentry/views/insights/types';
+import {
+  type EAPSpanResponse,
+  SpanIndexedField,
+  type SpanIndexedResponse,
+} from 'sentry/views/insights/types';
 import {SectionKey} from 'sentry/views/issueDetails/streamline/context';
 import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection';
 
@@ -22,20 +30,53 @@ interface Props {
   organization: Organization;
 }
 
-export default function LLMMonitoringSection({event}: Props) {
-  const traceId = event.contexts.trace?.trace_id;
-  const spanId = event.contexts.trace?.span_id;
-  const {data, error, isPending} = useSpansIndexed(
+function useAIPipelineGroup({
+  useEAP,
+  traceId,
+  spanId,
+}: {
+  useEAP: boolean;
+  spanId?: string;
+  traceId?: string;
+}): string | null {
+  const {data: indexedData} = useSpansIndexed(
     {
       limit: 1,
       fields: [SpanIndexedField.SPAN_AI_PIPELINE_GROUP],
       search: new MutableSearch(`trace:${traceId} id:"${spanId}"`),
+      enabled: !useEAP,
     },
     'api.ai-pipelines.view'
   );
+  const {data: eapData} = useEAPSpans(
+    {
+      limit: 1,
+      fields: [SpanIndexedField.SPAN_AI_PIPELINE_GROUP_TAG],
+      search: new MutableSearch(`trace:${traceId} id:"${spanId}"`),
+      enabled: useEAP,
+    },
+    'api.ai-pipelines-eap.view'
+  );
+
+  if (useEAP) {
+    return (
+      eapData &&
+      (eapData[0] as EAPSpanResponse)?.[SpanIndexedField.SPAN_AI_PIPELINE_GROUP_TAG]
+    );
+  }
+  return (
+    indexedData &&
+    (indexedData[0] as SpanIndexedResponse)?.[SpanIndexedField.SPAN_AI_PIPELINE_GROUP]
+  );
+}
+
+export default function LLMMonitoringSection({event, organization}: Props) {
   const moduleUrl = useModuleURL('ai');
-  const aiPipelineGroup =
-    data && (data[0] as SpanIndexedResponse)?.[SpanIndexedField.SPAN_AI_PIPELINE_GROUP];
+  const aiPipelineGroup = useAIPipelineGroup({
+    useEAP: organization.features.includes('insights-use-eap'),
+    traceId: event.contexts.trace?.trace_id,
+    spanId: event.contexts.trace?.span_id,
+  });
 
   const actions = (
     <ButtonBar gap={1}>
@@ -44,6 +85,7 @@ export default function LLMMonitoringSection({event}: Props) {
       </LinkButton>
     </ButtonBar>
   );
+  const useEAP = organization.features.includes('insights-use-eap');
 
   return (
     <InterimSection
@@ -52,19 +94,23 @@ export default function LLMMonitoringSection({event}: Props) {
       help={t('Charts showing how many tokens are being used')}
       actions={actions}
     >
-      {error ? (
-        <Alert type="error" showIcon>
-          {'' + error}
-        </Alert>
-      ) : isPending ? (
+      {!aiPipelineGroup ? (
         'loading'
       ) : (
         <ModuleLayout.Layout>
           <ModuleLayout.Half>
-            <TotalTokensUsedChart groupId={aiPipelineGroup} />
+            {useEAP ? (
+              <EAPTotalTokensUsedChart groupId={aiPipelineGroup} />
+            ) : (
+              <TotalTokensUsedChart groupId={aiPipelineGroup} />
+            )}
           </ModuleLayout.Half>
           <ModuleLayout.Half>
-            <NumberOfPipelinesChart groupId={aiPipelineGroup} />
+            {useEAP ? (
+              <EAPNumberOfPipelinesChart groupId={aiPipelineGroup} />
+            ) : (
+              <NumberOfPipelinesChart groupId={aiPipelineGroup} />
+            )}
           </ModuleLayout.Half>
         </ModuleLayout.Layout>
       )}

+ 7 - 3
static/app/views/insights/common/queries/useDiscover.spec.tsx

@@ -13,7 +13,11 @@ import {
   useSpanMetrics,
   useSpansIndexed,
 } from 'sentry/views/insights/common/queries/useDiscover';
-import {SpanIndexedField, type SpanMetricsProperty} from 'sentry/views/insights/types';
+import {
+  SpanIndexedField,
+  type SpanIndexedProperty,
+  type SpanMetricsProperty,
+} from 'sentry/views/insights/types';
 import {OrganizationContext} from 'sentry/views/organizationContext';
 
 jest.mock('sentry/utils/useLocation');
@@ -196,7 +200,7 @@ describe('useDiscover', () => {
         {
           wrapper: Wrapper,
           initialProps: {
-            fields: [SpanIndexedField.SPAN_DESCRIPTION],
+            fields: [SpanIndexedField.SPAN_DESCRIPTION] as SpanIndexedProperty[],
             enabled: false,
           },
         }
@@ -253,7 +257,7 @@ describe('useDiscover', () => {
               SpanIndexedField.SPAN_OP,
               SpanIndexedField.SPAN_GROUP,
               SpanIndexedField.SPAN_DESCRIPTION,
-            ],
+            ] as SpanIndexedProperty[],
             sorts: [{field: 'span.group', kind: 'desc' as const}],
             limit: 10,
             referrer: 'api-spec',

+ 15 - 2
static/app/views/insights/common/queries/useDiscover.ts

@@ -6,9 +6,11 @@ import type {MutableSearch} from 'sentry/utils/tokenizeSearch';
 import usePageFilters from 'sentry/utils/usePageFilters';
 import {useWrappedDiscoverQuery} from 'sentry/views/insights/common/queries/useSpansQuery';
 import type {
+  EAPSpanProperty,
+  EAPSpanResponse,
   MetricsProperty,
   MetricsResponse,
-  SpanIndexedField,
+  SpanIndexedProperty,
   SpanIndexedResponse,
   SpanMetricsProperty,
   SpanMetricsResponse,
@@ -25,7 +27,7 @@ interface UseMetricsOptions<Fields> {
   sorts?: Sort[];
 }
 
-export const useSpansIndexed = <Fields extends SpanIndexedField[]>(
+export const useSpansIndexed = <Fields extends SpanIndexedProperty[]>(
   options: UseMetricsOptions<Fields> = {},
   referrer: string
 ) => {
@@ -36,6 +38,17 @@ export const useSpansIndexed = <Fields extends SpanIndexedField[]>(
   );
 };
 
+export const useEAPSpans = <Fields extends EAPSpanProperty[]>(
+  options: UseMetricsOptions<Fields> = {},
+  referrer: string
+) => {
+  return useDiscover<Fields, EAPSpanResponse>(
+    options,
+    DiscoverDatasets.SPANS_EAP,
+    referrer
+  );
+};
+
 export const useSpanMetrics = <Fields extends SpanMetricsProperty[]>(
   options: UseMetricsOptions<Fields> = {},
   referrer: string

+ 2 - 2
static/app/views/insights/common/queries/useFullSpanFromTrace.tsx

@@ -4,7 +4,7 @@ import type {Sort} from 'sentry/utils/discover/fields';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
 import {useSpansIndexed} from 'sentry/views/insights/common/queries/useDiscover';
 import {useEventDetails} from 'sentry/views/insights/common/queries/useEventDetails';
-import {SpanIndexedField} from 'sentry/views/insights/types';
+import {SpanIndexedField, type SpanIndexedProperty} from 'sentry/views/insights/types';
 
 const DEFAULT_SORT: Sort[] = [{field: 'timestamp', kind: 'desc'}];
 
@@ -34,7 +34,7 @@ export function useFullSpanFromTrace(
         SpanIndexedField.TRANSACTION_ID,
         SpanIndexedField.PROJECT,
         SpanIndexedField.ID,
-        ...(sorts?.map(sort => sort.field as SpanIndexedField) || []),
+        ...(sorts?.map(sort => sort.field as SpanIndexedProperty) || []),
       ],
     },
     'api.starfish.full-span-from-trace'

+ 9 - 3
static/app/views/insights/http/queries/useSpanSamples.spec.tsx

@@ -8,7 +8,7 @@ import {QueryClientProvider} from 'sentry/utils/queryClient';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
 import usePageFilters from 'sentry/utils/usePageFilters';
 import {useSpanSamples} from 'sentry/views/insights/http/queries/useSpanSamples';
-import {SpanIndexedField} from 'sentry/views/insights/types';
+import {SpanIndexedField, type SpanIndexedProperty} from 'sentry/views/insights/types';
 import {OrganizationContext} from 'sentry/views/organizationContext';
 
 jest.mock('sentry/utils/usePageFilters');
@@ -59,7 +59,10 @@ describe('useSpanSamples', () => {
       {
         wrapper: Wrapper,
         initialProps: {
-          fields: [SpanIndexedField.TRANSACTION_ID, SpanIndexedField.ID],
+          fields: [
+            SpanIndexedField.TRANSACTION_ID,
+            SpanIndexedField.ID,
+          ] as SpanIndexedProperty[],
           enabled: false,
         },
       }
@@ -100,7 +103,10 @@ describe('useSpanSamples', () => {
             release: '0.0.1',
             environment: undefined,
           },
-          fields: [SpanIndexedField.TRANSACTION_ID, SpanIndexedField.ID],
+          fields: [
+            SpanIndexedField.TRANSACTION_ID,
+            SpanIndexedField.ID,
+          ] as SpanIndexedProperty[],
           referrer: 'api-spec',
         },
       }

+ 6 - 2
static/app/views/insights/http/queries/useSpanSamples.tsx

@@ -6,7 +6,11 @@ import type {MutableSearch} from 'sentry/utils/tokenizeSearch';
 import useOrganization from 'sentry/utils/useOrganization';
 import usePageFilters from 'sentry/utils/usePageFilters';
 import {getDateConditions} from 'sentry/views/insights/common/utils/getDateConditions';
-import type {SpanIndexedField, SpanIndexedResponse} from 'sentry/views/insights/types';
+import type {
+  SpanIndexedField,
+  SpanIndexedProperty,
+  SpanIndexedResponse,
+} from 'sentry/views/insights/types';
 
 interface UseSpanSamplesOptions<Fields> {
   enabled?: boolean;
@@ -17,7 +21,7 @@ interface UseSpanSamplesOptions<Fields> {
   search?: MutableSearch;
 }
 
-export const useSpanSamples = <Fields extends SpanIndexedField[]>(
+export const useSpanSamples = <Fields extends SpanIndexedProperty[]>(
   options: UseSpanSamplesOptions<Fields> = {}
 ) => {
   const {

+ 119 - 1
static/app/views/insights/llmMonitoring/components/charts/llmMonitoringCharts.tsx

@@ -1,15 +1,58 @@
 import {CHART_PALETTE} from 'sentry/constants/chartPalette';
 import {t} from 'sentry/locale';
+import {DiscoverDatasets} from 'sentry/utils/discover/types';
 import {MutableSearch} from 'sentry/utils/tokenizeSearch';
 import Chart, {ChartType} from 'sentry/views/insights/common/components/chart';
 import ChartPanel from 'sentry/views/insights/common/components/chartPanel';
-import {useSpanMetricsSeries} from 'sentry/views/insights/common/queries/useDiscoverSeries';
+import {
+  useSpanIndexedSeries,
+  useSpanMetricsSeries,
+} from 'sentry/views/insights/common/queries/useDiscoverSeries';
 import {ALERTS} from 'sentry/views/insights/llmMonitoring/alerts';
 
 interface TotalTokensUsedChartProps {
   groupId?: string;
 }
 
+export function EAPTotalTokensUsedChart({groupId}: TotalTokensUsedChartProps) {
+  const aggregate = 'sum(ai.total_tokens.used)';
+
+  let query = 'span.category:"ai"';
+  if (groupId) {
+    query = `${query} span.ai.pipeline.group:"${groupId}"`;
+  }
+  const {data, isPending, error} = useSpanIndexedSeries(
+    {
+      yAxis: [aggregate],
+      search: new MutableSearch(query),
+    },
+    'api.ai-pipelines.view',
+    DiscoverDatasets.SPANS_EAP
+  );
+
+  return (
+    <ChartPanel
+      title={t('Total tokens used')}
+      alertConfigs={[{...ALERTS.tokensUsed, query}]}
+    >
+      <Chart
+        height={200}
+        grid={{
+          left: '4px',
+          right: '0',
+          top: '8px',
+          bottom: '0',
+        }}
+        data={[data[aggregate]]}
+        loading={isPending}
+        error={error}
+        type={ChartType.LINE}
+        chartColors={[CHART_PALETTE[2][0]]}
+      />
+    </ChartPanel>
+  );
+}
+
 export function TotalTokensUsedChart({groupId}: TotalTokensUsedChartProps) {
   const aggregate = 'sum(ai.total_tokens.used)';
 
@@ -51,6 +94,42 @@ export function TotalTokensUsedChart({groupId}: TotalTokensUsedChartProps) {
 interface NumberOfPipelinesChartProps {
   groupId?: string;
 }
+
+export function EAPNumberOfPipelinesChart({groupId}: NumberOfPipelinesChartProps) {
+  const aggregate = 'count()';
+
+  let query = 'span.category:"ai.pipeline"';
+  if (groupId) {
+    query = `${query} span.group:"${groupId}"`;
+  }
+  const {data, isPending, error} = useSpanIndexedSeries(
+    {
+      yAxis: [aggregate],
+      search: new MutableSearch(query),
+    },
+    'api.ai-pipelines-eap.view',
+    DiscoverDatasets.SPANS_EAP
+  );
+
+  return (
+    <ChartPanel title={t('Number of AI pipelines')}>
+      <Chart
+        height={200}
+        grid={{
+          left: '4px',
+          right: '0',
+          top: '8px',
+          bottom: '0',
+        }}
+        data={[data[aggregate]]}
+        loading={isPending}
+        error={error}
+        type={ChartType.LINE}
+        chartColors={[CHART_PALETTE[2][1]]}
+      />
+    </ChartPanel>
+  );
+}
 export function NumberOfPipelinesChart({groupId}: NumberOfPipelinesChartProps) {
   const aggregate = 'count()';
 
@@ -89,6 +168,45 @@ export function NumberOfPipelinesChart({groupId}: NumberOfPipelinesChartProps) {
 interface PipelineDurationChartProps {
   groupId?: string;
 }
+
+export function EAPPipelineDurationChart({groupId}: PipelineDurationChartProps) {
+  const aggregate = 'avg(span.duration)';
+  let query = 'span.category:"ai.pipeline"';
+  if (groupId) {
+    query = `${query} span.group:"${groupId}"`;
+  }
+  const {data, isPending, error} = useSpanIndexedSeries(
+    {
+      yAxis: [aggregate],
+      search: new MutableSearch(query),
+    },
+    'api.ai-pipelines-eap.view',
+    DiscoverDatasets.SPANS_EAP
+  );
+
+  return (
+    <ChartPanel
+      title={t('Pipeline Duration')}
+      alertConfigs={[{...ALERTS.duration, query}]}
+    >
+      <Chart
+        height={200}
+        grid={{
+          left: '4px',
+          right: '0',
+          top: '8px',
+          bottom: '0',
+        }}
+        data={[data[aggregate]]}
+        loading={isPending}
+        error={error}
+        type={ChartType.LINE}
+        chartColors={[CHART_PALETTE[2][2]]}
+      />
+    </ChartPanel>
+  );
+}
+
 export function PipelineDurationChart({groupId}: PipelineDurationChartProps) {
   const aggregate = 'avg(span.duration)';
   let query = 'span.category:"ai.pipeline"';

+ 36 - 7
static/app/views/insights/llmMonitoring/components/tables/pipelineSpansTable.tsx

@@ -17,7 +17,10 @@ import {MutableSearch} from 'sentry/utils/tokenizeSearch';
 import {useLocation} from 'sentry/utils/useLocation';
 import useOrganization from 'sentry/utils/useOrganization';
 import {renderHeadCell} from 'sentry/views/insights/common/components/tableCells/renderHeadCell';
-import {useSpansIndexed} from 'sentry/views/insights/common/queries/useDiscover';
+import {
+  useEAPSpans,
+  useSpansIndexed,
+} from 'sentry/views/insights/common/queries/useDiscover';
 import {QueryParameterNames} from 'sentry/views/insights/common/views/queryParameters';
 import {SpanIndexedField} from 'sentry/views/insights/types';
 
@@ -70,8 +73,9 @@ export function isAValidSort(sort: Sort): sort is ValidSort {
 
 interface Props {
   groupId: string;
+  useEAP: boolean;
 }
-export function PipelineSpansTable({groupId}: Props) {
+export function PipelineSpansTable({groupId, useEAP}: Props) {
   const location = useLocation();
   const organization = useOrganization();
 
@@ -101,21 +105,46 @@ export function PipelineSpansTable({groupId}: Props) {
         SpanIndexedField.PROJECT,
       ],
       search: new MutableSearch(`span.category:ai.pipeline span.group:"${groupId}"`),
+      enabled: !useEAP,
     },
     'api.ai-pipelines.view'
   );
-  const data = rawData || [];
-  const meta = rawMeta as EventsMetaType;
+
+  const {
+    data: eapData,
+    meta: eapMeta,
+    error: eapError,
+    isPending: eapPending,
+  } = useEAPSpans(
+    {
+      limit: 30,
+      sorts: [sort],
+      fields: [
+        SpanIndexedField.ID,
+        SpanIndexedField.TRACE,
+        SpanIndexedField.SPAN_DURATION,
+        SpanIndexedField.TRANSACTION_ID,
+        SpanIndexedField.USER,
+        SpanIndexedField.TIMESTAMP,
+        SpanIndexedField.PROJECT,
+      ],
+      search: new MutableSearch(`span.category:ai.pipeline span.group:"${groupId}"`),
+      enabled: useEAP,
+    },
+    'api.ai-pipelines.view'
+  );
+  const data = (useEAP ? eapData : rawData) ?? [];
+  const meta = (useEAP ? eapMeta : rawMeta) as EventsMetaType;
 
   return (
     <VisuallyCompleteWithData
       id="PipelineSpansTable"
       hasData={data.length > 0}
-      isLoading={isPending}
+      isLoading={useEAP ? eapPending : isPending}
     >
       <GridEditable
-        isLoading={isPending}
-        error={error}
+        isLoading={useEAP ? eapPending : isPending}
+        error={useEAP ? eapError : error}
         data={data}
         columnOrder={COLUMN_ORDER}
         columnSortBy={[

+ 153 - 2
static/app/views/insights/llmMonitoring/components/tables/pipelinesTable.tsx

@@ -26,10 +26,14 @@ import {MutableSearch} from 'sentry/utils/tokenizeSearch';
 import {useLocation} from 'sentry/utils/useLocation';
 import useOrganization from 'sentry/utils/useOrganization';
 import {renderHeadCell} from 'sentry/views/insights/common/components/tableCells/renderHeadCell';
-import {useSpanMetrics} from 'sentry/views/insights/common/queries/useDiscover';
+import {
+  useEAPSpans,
+  useSpanMetrics,
+} from 'sentry/views/insights/common/queries/useDiscover';
 import {useModuleURL} from 'sentry/views/insights/common/utils/useModuleURL';
 import {QueryParameterNames} from 'sentry/views/insights/common/views/queryParameters';
 import type {SpanMetricsResponse} from 'sentry/views/insights/types';
+import {SpanIndexedField} from 'sentry/views/insights/types';
 
 type Row = Pick<
   SpanMetricsResponse,
@@ -89,6 +93,154 @@ export function isAValidSort(sort: Sort): sort is ValidSort {
   return (SORTABLE_FIELDS as unknown as string[]).includes(sort.field);
 }
 
+export function EAPPipelinesTable() {
+  const location = useLocation();
+  const moduleURL = useModuleURL('ai');
+
+  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, isPending, meta, pageLinks, error} = useEAPSpans(
+    {
+      search: MutableSearch.fromQueryObject({
+        'span.category': 'ai.pipeline',
+        [SpanIndexedField.SPAN_DESCRIPTION]: spanDescription
+          ? `*${spanDescription}*`
+          : undefined,
+      }),
+      fields: [
+        SpanIndexedField.SPAN_GROUP,
+        SpanIndexedField.SPAN_DESCRIPTION,
+        'spm()',
+        'avg(span.duration)',
+        'sum(span.duration)',
+      ],
+      sorts: [sort],
+      limit: 25,
+      cursor,
+    },
+    'api.ai-pipelines-eap.table'
+  );
+
+  const {data: tokensUsedData, isPending: tokensUsedLoading} = useEAPSpans(
+    {
+      search: new MutableSearch(
+        `span.category:ai span.ai.pipeline.group:[${(data as Row[])
+          ?.map(x => x['span.group'])
+          ?.filter(x => !!x)
+          .join(',')}]`
+      ),
+      fields: ['span.ai.pipeline.group', 'sum(ai.total_tokens.used)'],
+    },
+    'api.ai-pipelines-eap.table'
+  );
+
+  const {
+    data: tokenCostData,
+    isPending: tokenCostLoading,
+    error: tokenCostError,
+  } = useEAPSpans(
+    {
+      search: new MutableSearch(
+        `span.category:ai span.ai.pipeline.group:[${(data as Row[])?.map(x => x['span.group']).join(',')}]`
+      ),
+      fields: ['span.ai.pipeline.group', 'sum(ai.total_cost)'],
+    },
+    'api.ai-pipelines-eap.table'
+  );
+
+  const rows: Row[] = (data as Row[]).map(baseRow => {
+    const row: Row = {
+      ...baseRow,
+      'sum(ai.total_tokens.used)': 0,
+      'sum(ai.total_cost)': 0,
+    };
+    if (!tokensUsedLoading) {
+      const tokenUsedDataPoint = tokensUsedData.find(
+        tokenRow => tokenRow['span.ai.pipeline.group'] === row['span.group']
+      );
+      if (tokenUsedDataPoint) {
+        row['sum(ai.total_tokens.used)'] =
+          tokenUsedDataPoint['sum(ai.total_tokens.used)'];
+      }
+    }
+    if (!tokenCostLoading && !tokenCostError) {
+      const tokenCostDataPoint = tokenCostData.find(
+        tokenRow => tokenRow['span.ai.pipeline.group'] === row['span.group']
+      );
+      if (tokenCostDataPoint) {
+        row['sum(ai.total_cost)'] = tokenCostDataPoint['sum(ai.total_cost)'];
+      }
+    }
+    return row;
+  });
+
+  const handleCursor: CursorHandler = (newCursor, pathname, query) => {
+    browserHistory.push({
+      pathname,
+      query: {...query, [QueryParameterNames.SPANS_CURSOR]: newCursor},
+    });
+  };
+
+  const handleSearch = (newQuery: string) => {
+    browserHistory.push({
+      ...location,
+      query: {
+        ...location.query,
+        'span.description': newQuery === '' ? undefined : newQuery,
+        [QueryParameterNames.SPANS_CURSOR]: undefined,
+      },
+    });
+  };
+
+  return (
+    <VisuallyCompleteWithData
+      id="PipelinesTable"
+      hasData={rows.length > 0}
+      isLoading={isPending}
+    >
+      <Container>
+        <SearchBar
+          placeholder={t('Search for pipeline')}
+          query={spanDescription}
+          onSearch={handleSearch}
+        />
+        <GridEditable
+          isLoading={isPending}
+          error={error}
+          data={rows}
+          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(moduleURL, column, row, meta, location, organization),
+          }}
+        />
+        <Pagination pageLinks={pageLinks} onCursor={handleCursor} />
+      </Container>
+    </VisuallyCompleteWithData>
+  );
+}
+
 export function PipelinesTable() {
   const location = useLocation();
   const moduleURL = useModuleURL('ai');
@@ -110,7 +262,6 @@ export function PipelinesTable() {
         'span.description': spanDescription ? `*${spanDescription}*` : undefined,
       }),
       fields: [
-        'project.id',
         'span.group',
         'span.description',
         'spm()',

+ 65 - 16
static/app/views/insights/llmMonitoring/views/llmMonitoringDetailsPage.tsx

@@ -17,9 +17,15 @@ import {MetricReadout} from 'sentry/views/insights/common/components/metricReado
 import * as ModuleLayout from 'sentry/views/insights/common/components/moduleLayout';
 import {ModulePageProviders} from 'sentry/views/insights/common/components/modulePageProviders';
 import {ReadoutRibbon, ToolRibbon} from 'sentry/views/insights/common/components/ribbon';
-import {useSpanMetrics} from 'sentry/views/insights/common/queries/useDiscover';
+import {
+  useEAPSpans,
+  useSpanMetrics,
+} from 'sentry/views/insights/common/queries/useDiscover';
 import {useModuleBreadcrumbs} from 'sentry/views/insights/common/utils/useModuleBreadcrumbs';
 import {
+  EAPNumberOfPipelinesChart,
+  EAPPipelineDurationChart,
+  EAPTotalTokensUsedChart,
   NumberOfPipelinesChart,
   PipelineDurationChart,
   TotalTokensUsedChart,
@@ -54,8 +60,9 @@ export function LLMMonitoringPage({params}: Props) {
     'span.group': groupId,
     'span.category': 'ai.pipeline',
   };
+  const useEAP = organization?.features?.includes('insights-use-eap');
 
-  const {data, isPending: areSpanMetricsLoading} = useSpanMetrics(
+  const {data: spanMetricData, isPending: areSpanMetricsLoading} = useSpanMetrics(
     {
       search: MutableSearch.fromQueryObject(filters),
       fields: [
@@ -64,11 +71,25 @@ export function LLMMonitoringPage({params}: Props) {
         `${SpanFunction.SPM}()`,
         `avg(${SpanMetricsField.SPAN_DURATION})`,
       ],
-      enabled: Boolean(groupId),
+      enabled: Boolean(groupId) && !useEAP,
     },
-    'api.ai-pipelines.view'
+    'api.ai-pipelines.details.view'
   );
-  const spanMetrics = data[0] ?? {};
+
+  const {data: eapData, isPending: isEAPPending} = useEAPSpans(
+    {
+      search: MutableSearch.fromQueryObject(filters),
+      fields: [
+        SpanMetricsField.SPAN_OP,
+        'count()',
+        `${SpanFunction.SPM}()`,
+        `avg(${SpanMetricsField.SPAN_DURATION})`,
+      ],
+      enabled: Boolean(groupId) && useEAP,
+    },
+    'api.ai-pipelines.details-eap.view'
+  );
+  const spanMetrics = (useEAP ? eapData[0] : spanMetricData[0]) ?? {};
 
   const {data: totalTokenData, isPending: isTotalTokenDataLoading} = useSpanMetrics(
     {
@@ -77,11 +98,23 @@ export function LLMMonitoringPage({params}: Props) {
         'span.ai.pipeline.group': groupId,
       }),
       fields: ['sum(ai.total_tokens.used)', 'sum(ai.total_cost)'],
-      enabled: Boolean(groupId),
+      enabled: Boolean(groupId) && !useEAP,
+    },
+    'api.ai-pipelines.details.view'
+  );
+
+  const {data: eapTokenData, isPending: isEAPTotalTokenDataLoading} = useEAPSpans(
+    {
+      search: MutableSearch.fromQueryObject({
+        'span.category': 'ai',
+        'span.ai.pipeline.group': groupId,
+      }),
+      fields: ['sum(ai.total_tokens.used)', 'sum(ai.total_cost)'],
+      enabled: Boolean(groupId) && useEAP,
     },
-    'api.ai-pipelines.view'
+    'api.ai-pipelines.details.view'
   );
-  const tokenUsedMetric = totalTokenData[0] ?? {};
+  const tokenUsedMetric = (useEAP ? eapTokenData[0] : totalTokenData[0]) ?? {};
 
   const crumbs = useModuleBreadcrumbs('ai');
 
@@ -122,43 +155,59 @@ export function LLMMonitoringPage({params}: Props) {
                       title={t('Total Tokens Used')}
                       value={tokenUsedMetric['sum(ai.total_tokens.used)']}
                       unit={'count'}
-                      isLoading={isTotalTokenDataLoading}
+                      isLoading={
+                        useEAP ? isEAPTotalTokenDataLoading : isTotalTokenDataLoading
+                      }
                     />
 
                     <MetricReadout
                       title={t('Total Cost')}
                       value={tokenUsedMetric['sum(ai.total_cost)']}
                       unit={CurrencyUnit.USD}
-                      isLoading={isTotalTokenDataLoading}
+                      isLoading={
+                        useEAP ? isEAPTotalTokenDataLoading : isTotalTokenDataLoading
+                      }
                     />
 
                     <MetricReadout
                       title={t('Pipeline Duration')}
                       value={spanMetrics?.[`avg(${SpanMetricsField.SPAN_DURATION})`]}
                       unit={DurationUnit.MILLISECOND}
-                      isLoading={areSpanMetricsLoading}
+                      isLoading={useEAP ? isEAPPending : areSpanMetricsLoading}
                     />
 
                     <MetricReadout
                       title={t('Pipeline Runs Per Minute')}
                       value={spanMetrics?.[`${SpanFunction.SPM}()`]}
                       unit={RateUnit.PER_MINUTE}
-                      isLoading={areSpanMetricsLoading}
+                      isLoading={useEAP ? isEAPPending : areSpanMetricsLoading}
                     />
                   </ReadoutRibbon>
                 </HeaderContainer>
               </ModuleLayout.Full>
               <ModuleLayout.Third>
-                <TotalTokensUsedChart groupId={groupId} />
+                {useEAP ? (
+                  <EAPTotalTokensUsedChart groupId={groupId} />
+                ) : (
+                  <TotalTokensUsedChart groupId={groupId} />
+                )}
               </ModuleLayout.Third>
               <ModuleLayout.Third>
-                <NumberOfPipelinesChart groupId={groupId} />
+                {useEAP ? (
+                  <EAPNumberOfPipelinesChart groupId={groupId} />
+                ) : (
+                  <NumberOfPipelinesChart groupId={groupId} />
+                )}
               </ModuleLayout.Third>
               <ModuleLayout.Third>
-                <PipelineDurationChart groupId={groupId} />
+                {useEAP ? (
+                  <EAPPipelineDurationChart groupId={groupId} />
+                ) : (
+                  <PipelineDurationChart groupId={groupId} />
+                )}
               </ModuleLayout.Third>
               <ModuleLayout.Full>
-                <PipelineSpansTable groupId={groupId} />
+                <PipelineSpansTable groupId={groupId} useEAP={useEAP} />
               </ModuleLayout.Full>
             </ModuleLayout.Layout>
           </Layout.Main>

Некоторые файлы не были показаны из-за большого количества измененных файлов