From b08a96a25baa1ff9013e90543396fe57c4ae798a Mon Sep 17 00:00:00 2001
From: colin-sentry <161344340+colin-sentry@users.noreply.github.com>
Date: Thu, 19 Sep 2024 16:11:35 -0400
Subject: [PATCH] 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.
---
.../llm-monitoring/llmMonitoringSection.tsx | 78 +++++++--
.../common/queries/useDiscover.spec.tsx | 10 +-
.../insights/common/queries/useDiscover.ts | 17 +-
.../common/queries/useFullSpanFromTrace.tsx | 4 +-
.../http/queries/useSpanSamples.spec.tsx | 12 +-
.../insights/http/queries/useSpanSamples.tsx | 8 +-
.../components/charts/llmMonitoringCharts.tsx | 120 +++++++++++++-
.../components/tables/pipelineSpansTable.tsx | 43 ++++-
.../components/tables/pipelinesTable.tsx | 155 +++++++++++++++++-
.../views/llmMonitoringDetailsPage.tsx | 81 +++++++--
.../views/llmMonitoringLandingPage.tsx | 17 +-
static/app/views/insights/types.tsx | 39 ++++-
12 files changed, 523 insertions(+), 61 deletions(-)
diff --git a/static/app/components/events/interfaces/llm-monitoring/llmMonitoringSection.tsx b/static/app/components/events/interfaces/llm-monitoring/llmMonitoringSection.tsx
index 1d538582c37adb..76713326fd5afe 100644
--- a/static/app/components/events/interfaces/llm-monitoring/llmMonitoringSection.tsx
+++ b/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 = (
@@ -44,6 +85,7 @@ export default function LLMMonitoringSection({event}: Props) {
);
+ const useEAP = organization.features.includes('insights-use-eap');
return (
- {error ? (
-
- {'' + error}
-
- ) : isPending ? (
+ {!aiPipelineGroup ? (
'loading'
) : (
-
+ {useEAP ? (
+
+ ) : (
+
+ )}
-
+ {useEAP ? (
+
+ ) : (
+
+ )}
)}
diff --git a/static/app/views/insights/common/queries/useDiscover.spec.tsx b/static/app/views/insights/common/queries/useDiscover.spec.tsx
index 7d197288829291..1c91ae369fa4e9 100644
--- a/static/app/views/insights/common/queries/useDiscover.spec.tsx
+++ b/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',
diff --git a/static/app/views/insights/common/queries/useDiscover.ts b/static/app/views/insights/common/queries/useDiscover.ts
index f4ced0f20ab234..0457344f5fbe71 100644
--- a/static/app/views/insights/common/queries/useDiscover.ts
+++ b/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 {
sorts?: Sort[];
}
-export const useSpansIndexed = (
+export const useSpansIndexed = (
options: UseMetricsOptions = {},
referrer: string
) => {
@@ -36,6 +38,17 @@ export const useSpansIndexed = (
);
};
+export const useEAPSpans = (
+ options: UseMetricsOptions = {},
+ referrer: string
+) => {
+ return useDiscover(
+ options,
+ DiscoverDatasets.SPANS_EAP,
+ referrer
+ );
+};
+
export const useSpanMetrics = (
options: UseMetricsOptions = {},
referrer: string
diff --git a/static/app/views/insights/common/queries/useFullSpanFromTrace.tsx b/static/app/views/insights/common/queries/useFullSpanFromTrace.tsx
index 0099be493f40e7..2c792329243f72 100644
--- a/static/app/views/insights/common/queries/useFullSpanFromTrace.tsx
+++ b/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'
diff --git a/static/app/views/insights/http/queries/useSpanSamples.spec.tsx b/static/app/views/insights/http/queries/useSpanSamples.spec.tsx
index 646a3836d81a44..d0274dfb0d0ed9 100644
--- a/static/app/views/insights/http/queries/useSpanSamples.spec.tsx
+++ b/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',
},
}
diff --git a/static/app/views/insights/http/queries/useSpanSamples.tsx b/static/app/views/insights/http/queries/useSpanSamples.tsx
index 635699130cc025..92af450d9b452b 100644
--- a/static/app/views/insights/http/queries/useSpanSamples.tsx
+++ b/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 {
enabled?: boolean;
@@ -17,7 +21,7 @@ interface UseSpanSamplesOptions {
search?: MutableSearch;
}
-export const useSpanSamples = (
+export const useSpanSamples = (
options: UseSpanSamplesOptions = {}
) => {
const {
diff --git a/static/app/views/insights/llmMonitoring/components/charts/llmMonitoringCharts.tsx b/static/app/views/insights/llmMonitoring/components/charts/llmMonitoringCharts.tsx
index 6e83f9db2d9f55..16f3c29d03b627 100644
--- a/static/app/views/insights/llmMonitoring/components/charts/llmMonitoringCharts.tsx
+++ b/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 (
+
+
+
+ );
+}
+
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 (
+
+
+
+ );
+}
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 (
+
+
+
+ );
+}
+
export function PipelineDurationChart({groupId}: PipelineDurationChartProps) {
const aggregate = 'avg(span.duration)';
let query = 'span.category:"ai.pipeline"';
diff --git a/static/app/views/insights/llmMonitoring/components/tables/pipelineSpansTable.tsx b/static/app/views/insights/llmMonitoring/components/tables/pipelineSpansTable.tsx
index de008bf783fde3..6ff915c3d75cb6 100644
--- a/static/app/views/insights/llmMonitoring/components/tables/pipelineSpansTable.tsx
+++ b/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 (
0}
- isLoading={isPending}
+ isLoading={useEAP ? eapPending : isPending}
>
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 (
+ 0}
+ isLoading={isPending}
+ >
+
+
+
+ renderHeadCell({
+ column,
+ sort,
+ location,
+ sortParameterName: QueryParameterNames.SPANS_SORT,
+ }),
+ renderBodyCell: (column, row) =>
+ renderBodyCell(moduleURL, column, row, meta, location, organization),
+ }}
+ />
+
+
+
+ );
+}
+
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()',
diff --git a/static/app/views/insights/llmMonitoring/views/llmMonitoringDetailsPage.tsx b/static/app/views/insights/llmMonitoring/views/llmMonitoringDetailsPage.tsx
index 72df5368bd8e7c..cce0a6c8352d1a 100644
--- a/static/app/views/insights/llmMonitoring/views/llmMonitoringDetailsPage.tsx
+++ b/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
+ }
/>
-
+ {useEAP ? (
+
+ ) : (
+
+ )}
-
+ {useEAP ? (
+
+ ) : (
+
+ )}
-
+ {useEAP ? (
+
+ ) : (
+
+ )}
-
+
diff --git a/static/app/views/insights/llmMonitoring/views/llmMonitoringLandingPage.tsx b/static/app/views/insights/llmMonitoring/views/llmMonitoringLandingPage.tsx
index dcb4cce2c03dce..e23b3180671bed 100644
--- a/static/app/views/insights/llmMonitoring/views/llmMonitoringLandingPage.tsx
+++ b/static/app/views/insights/llmMonitoring/views/llmMonitoringLandingPage.tsx
@@ -13,11 +13,17 @@ import {ModulePageProviders} from 'sentry/views/insights/common/components/modul
import {ModulesOnboarding} from 'sentry/views/insights/common/components/modulesOnboarding';
import {useModuleBreadcrumbs} from 'sentry/views/insights/common/utils/useModuleBreadcrumbs';
import {
+ EAPNumberOfPipelinesChart,
+ EAPPipelineDurationChart,
+ EAPTotalTokensUsedChart,
NumberOfPipelinesChart,
PipelineDurationChart,
TotalTokensUsedChart,
} from 'sentry/views/insights/llmMonitoring/components/charts/llmMonitoringCharts';
-import {PipelinesTable} from 'sentry/views/insights/llmMonitoring/components/tables/pipelinesTable';
+import {
+ EAPPipelinesTable,
+ PipelinesTable,
+} from 'sentry/views/insights/llmMonitoring/components/tables/pipelinesTable';
import {
MODULE_DOC_LINK,
MODULE_TITLE,
@@ -29,6 +35,7 @@ export function LLMMonitoringPage({disableHeader}: InsightLandingProps) {
const organization = useOrganization();
const crumbs = useModuleBreadcrumbs('ai');
+ const useEAP = organization?.features?.includes('insights-use-eap');
return (
@@ -61,16 +68,16 @@ export function LLMMonitoringPage({disableHeader}: InsightLandingProps) {
-
+ {useEAP ? : }
-
+ {useEAP ? : }
-
+ {useEAP ? : }
-
+ {useEAP ? : }
diff --git a/static/app/views/insights/types.tsx b/static/app/views/insights/types.tsx
index 16d4fa44ab4b4a..bf4645447a0b5d 100644
--- a/static/app/views/insights/types.tsx
+++ b/static/app/views/insights/types.tsx
@@ -73,6 +73,7 @@ export type SpanNumberFields =
| SpanMetricsField.CACHE_ITEM_SIZE;
export type SpanStringFields =
+ | 'span_id'
| 'span.op'
| 'span.description'
| 'span.module'
@@ -80,14 +81,18 @@ export type SpanStringFields =
| 'span.group'
| 'span.category'
| 'span.system'
+ | 'timestamp'
+ | 'trace'
| 'transaction'
+ | 'transaction.id'
| 'transaction.method'
| 'release'
| 'os.name'
| 'span.status_code'
| 'span.ai.pipeline.group'
| 'project'
- | 'messaging.destination.name';
+ | 'messaging.destination.name'
+ | 'user';
export type SpanMetricsQueryFilters = {
[Field in SpanStringFields]?: string;
@@ -178,6 +183,35 @@ export type MetricsFilters = {
export type SpanMetricsProperty = keyof SpanMetricsResponse;
+export type EAPSpanResponse = {
+ [Property in SpanNumberFields as `${Aggregate}(${Property})`]: number;
+} & {
+ [Property in SpanFunctions as `${Property}()`]: number;
+} & {
+ [Property in SpanStringFields as `${Property}`]: string;
+} & {
+ [Property in SpanNumberFields as `${Property}`]: number;
+} & {
+ [Property in SpanStringArrayFields as `${Property}`]: string[];
+} & {
+ ['project']: string;
+ ['project.id']: number;
+} & {
+ [Function in RegressionFunctions]: number;
+} & {
+ [Function in SpanAnyFunction]: string;
+} & {
+ [Property in ConditionalAggregate as
+ | `${Property}(${string})`
+ | `${Property}(${string},${string})`
+ | `${Property}(${string},${string},${string})`]: number;
+} & {
+ [SpanMetricsField.USER_GEO_SUBREGION]: SubregionCode;
+ [SpanIndexedField.SPAN_AI_PIPELINE_GROUP_TAG]: string;
+};
+
+export type EAPSpanProperty = keyof EAPSpanResponse;
+
export enum SpanIndexedField {
ENVIRONMENT = 'environment',
RESOURCE_RENDER_BLOCKING_STATUS = 'resource.render_blocking_status',
@@ -193,6 +227,7 @@ export enum SpanIndexedField {
ID = 'span_id',
SPAN_ACTION = 'span.action',
SPAN_AI_PIPELINE_GROUP = 'span.ai.pipeline.group',
+ SPAN_AI_PIPELINE_GROUP_TAG = 'ai_pipeline_group',
SDK_NAME = 'sdk.name',
TRACE = 'trace',
TRANSACTION_ID = 'transaction.id',
@@ -297,7 +332,7 @@ export type SpanIndexedResponse = {
[SpanIndexedField.USER_GEO_SUBREGION]: string;
};
-export type SpanIndexedPropery = keyof SpanIndexedResponse;
+export type SpanIndexedProperty = keyof SpanIndexedResponse;
// TODO: When convenient, remove this alias and use `IndexedResponse` everywhere
export type SpanIndexedFieldTypes = SpanIndexedResponse;