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;