Skip to content

Commit

Permalink
feat(ai): AI Analytics details pages (#69308)
Browse files Browse the repository at this point in the history
Adds a page for viewing each pipeline in AI analytics, as well as a
table with clickthroughs to the trace view
  • Loading branch information
colin-sentry authored and MichaelSun48 committed Apr 25, 2024
1 parent e4219b3 commit 6d9606f
Show file tree
Hide file tree
Showing 6 changed files with 392 additions and 18 deletions.
4 changes: 4 additions & 0 deletions static/app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1495,6 +1495,10 @@ function buildRoutes() {
const aiAnalyticsRoutes = (
<Route path="/ai-analytics/" withOrgPath>
<IndexRoute component={make(() => import('sentry/views/aiAnalytics/landing'))} />
<Route
path="pipeline-type/:groupId/"
component={make(() => import('sentry/views/aiAnalytics/aiAnalyticsDetailsPage'))}
/>
</Route>
);

Expand Down
21 changes: 11 additions & 10 deletions static/app/views/aiAnalytics/PipelinesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ type Row = Pick<
| 'span.description'
| 'span.group'
| 'spm()'
| 'avg(span.self_time)'
| 'sum(span.self_time)'
| 'avg(span.duration)'
| 'sum(span.duration)'
| 'time_spent_percentage()'
>;

type Column = GridColumnHeader<
'span.description' | 'spm()' | 'avg(span.self_time)' | 'time_spent_percentage()'
'span.description' | 'spm()' | 'avg(span.duration)' | 'time_spent_percentage()'
>;

const COLUMN_ORDER: Column[] = [
Expand All @@ -51,7 +51,7 @@ const COLUMN_ORDER: Column[] = [
width: COL_WIDTH_UNDEFINED,
},
{
key: `avg(span.self_time)`,
key: `avg(span.duration)`,
name: DataTitles.avg,
width: COL_WIDTH_UNDEFINED,
},
Expand All @@ -62,10 +62,10 @@ const COLUMN_ORDER: Column[] = [
},
];

const SORTABLE_FIELDS = ['avg(span.self_time)', 'spm()', 'time_spent_percentage()'];
const SORTABLE_FIELDS = ['avg(span.duration)', 'spm()', 'time_spent_percentage()'];

type ValidSort = Sort & {
field: 'spm()' | 'avg(span.self_time)' | 'time_spent_percentage()';
field: 'spm()' | 'avg(span.duration)' | 'time_spent_percentage()';
};

export function isAValidSort(sort: Sort): sort is ValidSort {
Expand All @@ -83,19 +83,20 @@ export function PipelinesTable() {
sort = {field: 'time_spent_percentage()', kind: 'desc'};
}
const {data, isLoading, meta, pageLinks, error} = useSpanMetrics({
search: new MutableSearch('span.op:ai.pipeline.langchain'),
search: new MutableSearch('span.category:ai.pipeline'),
fields: [
'project.id',
'span.group',
'span.description',
'spm()',
'avg(span.self_time)',
'sum(span.self_time)',
'avg(span.duration)',
'sum(span.duration)',
'time_spent_percentage()',
],
sorts: [sort],
limit: 25,
cursor,
referrer: 'api.ai-pipelines.view',
});

const handleCursor: CursorHandler = (newCursor, pathname, query) => {
Expand Down Expand Up @@ -157,7 +158,7 @@ function renderBodyCell(
return (
<Link
to={normalizeUrl(
`/organizations/${organization.slug}/ai-analytics/pipelines/${row['span.group']}`
`/organizations/${organization.slug}/ai-analytics/pipeline-type/${row['span.group']}`
)}
>
{row['span.description']}
Expand Down
36 changes: 28 additions & 8 deletions static/app/views/aiAnalytics/aiAnalyticsCharts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import styled from '@emotion/styled';

import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {MetricsQueryApiResponseLastMeta} from 'sentry/types';
import {MetricDisplayType} from 'sentry/utils/metrics/types';
import {useMetricsQuery} from 'sentry/utils/metrics/useMetricsQuery';
import usePageFilters from 'sentry/utils/usePageFilters';
Expand All @@ -20,6 +21,7 @@ export function TotalTokensUsedChart() {
name: 'total',
mri: `c:spans/ai.total_tokens.used@none`,
op: 'sum',
// TODO this double counts the (e.g.) langchain and openai token usage
},
],
selection,
Expand Down Expand Up @@ -55,8 +57,15 @@ export function TotalTokensUsedChart() {
);
}

export function NumberOfPipelinesChart() {
interface NumberOfPipelinesChartProps {
groupId?: string;
}
export function NumberOfPipelinesChart({groupId}: NumberOfPipelinesChartProps) {
const {selection, isReady: isGlobalSelectionReady} = usePageFilters();
let query = 'span.category:"ai.pipeline"';
if (groupId) {
query = `${query} span.group:"${groupId}"`;
}
const {
data: timeseriesData,
isLoading,
Expand All @@ -68,7 +77,7 @@ export function NumberOfPipelinesChart() {
name: 'number',
mri: `d:spans/exclusive_time@millisecond`,
op: 'count',
query: 'span.op:"ai.pipeline.langchain"', // TODO: for now this is the only AI "pipeline" supported
query,
},
],
selection,
Expand Down Expand Up @@ -104,8 +113,15 @@ export function NumberOfPipelinesChart() {
);
}

export function PipelineDurationChart() {
interface PipelineDurationChartProps {
groupId?: string;
}
export function PipelineDurationChart({groupId}: PipelineDurationChartProps) {
const {selection, isReady: isGlobalSelectionReady} = usePageFilters();
let query = 'span.category:"ai.pipeline"';
if (groupId) {
query = `${query} span.group:"${groupId}"`;
}
const {
data: timeseriesData,
isLoading,
Expand All @@ -114,17 +130,22 @@ export function PipelineDurationChart() {
} = useMetricsQuery(
[
{
name: 'number',
mri: `d:spans/exclusive_time@millisecond`,
name: 'a',
mri: `d:spans/duration@millisecond`,
op: 'avg',
query: 'span.op:"ai.pipeline.langchain"', // TODO: for now this is the only AI "pipeline" supported
query,
},
],
selection,
{
intervalLadder: 'dashboard',
}
);
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';
}

if (!isGlobalSelectionReady) {
return null;
Expand All @@ -143,7 +164,7 @@ export function PipelineDurationChart() {
metricQueries={[
{
name: 'mql',
formula: '$number',
formula: '$a',
},
]}
displayType={MetricDisplayType.AREA}
Expand All @@ -159,7 +180,6 @@ const PanelTitle = styled('h5')`
`;

const TokenChartContainer = styled('div')`
overflow: hidden;
border: 1px solid ${p => p.theme.border};
border-radius: ${p => p.theme.borderRadius};
height: 100%;
Expand Down
161 changes: 161 additions & 0 deletions static/app/views/aiAnalytics/aiAnalyticsDetailsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import styled from '@emotion/styled';

import Feature from 'sentry/components/acl/feature';
import {Alert} from 'sentry/components/alert';
import * as Layout from 'sentry/components/layouts/thirds';
import NoProjectMessage from 'sentry/components/noProjectMessage';
import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
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 {
NumberOfPipelinesChart,
PipelineDurationChart,
} from 'sentry/views/aiAnalytics/aiAnalyticsCharts';
import {PipelineSpansTable} from 'sentry/views/aiAnalytics/pipelineSpansTable';
import {MetricReadout} from 'sentry/views/performance/metricReadout';
import * as ModuleLayout from 'sentry/views/performance/moduleLayout';
import {useSpanMetrics} from 'sentry/views/starfish/queries/useSpanMetrics';
import {
SpanFunction,
SpanMetricsField,
type SpanMetricsQueryFilters,
} from 'sentry/views/starfish/types';
import {DataTitles} from 'sentry/views/starfish/views/spans/types';

function NoAccessComponent() {
return (
<Layout.Page withPadding>
<Alert type="warning">{t("You don't have access to this feature")}</Alert>
</Layout.Page>
);
}
interface Props {
params: {
groupId: string;
};
}

export default function AiAnalyticsPage({params}: Props) {
const organization = useOrganization();
const {groupId} = params;

const filters: SpanMetricsQueryFilters = {
'span.group': groupId,
'span.category': 'ai.pipeline',
};

const {data, isLoading: areSpanMetricsLoading} = useSpanMetrics({
search: MutableSearch.fromQueryObject(filters),
fields: [
SpanMetricsField.SPAN_OP,
SpanMetricsField.SPAN_DESCRIPTION,
'count()',
`${SpanFunction.SPM}()`,
`avg(${SpanMetricsField.SPAN_DURATION})`,
],
enabled: Boolean(groupId),
referrer: 'api.ai-pipelines.view',
});
const spanMetrics = data[0] ?? {};

return (
<PageFiltersContainer>
<SentryDocumentTitle
title={`AI Analytics — ${spanMetrics['span.description'] ?? t('(no name)')}`}
>
<Layout.Page>
<Feature
features="ai-analytics"
organization={organization}
renderDisabled={NoAccessComponent}
>
<NoProjectMessage organization={organization}>
<Layout.Header>
<Layout.HeaderContent>
<Layout.Title>
{`${t('AI Analytics')} - ${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-analytics/"
/>
</Layout.Title>
</Layout.HeaderContent>
</Layout.Header>
<Layout.Body>
<Layout.Main fullWidth>
<ModuleLayout.Layout>
<ModuleLayout.Full>
<SpaceBetweenWrap>
<PageFilterBar condensed>
<ProjectPageFilter />
<EnvironmentPageFilter />
<DatePageFilter />
</PageFilterBar>
<MetricsRibbon>
<MetricReadout
title={t('Total Runs')}
value={spanMetrics['count()']}
unit={'count'}
isLoading={areSpanMetricsLoading}
/>

<MetricReadout
title={t('Runs Per Minute')}
value={spanMetrics?.[`${SpanFunction.SPM}()`]}
unit={RateUnit.PER_MINUTE}
isLoading={areSpanMetricsLoading}
/>

<MetricReadout
title={DataTitles.avg}
value={
spanMetrics?.[`avg(${SpanMetricsField.SPAN_DURATION})`]
}
unit={DurationUnit.MILLISECOND}
isLoading={areSpanMetricsLoading}
/>
</MetricsRibbon>
</SpaceBetweenWrap>
</ModuleLayout.Full>
<ModuleLayout.Half>
<NumberOfPipelinesChart groupId={groupId} />
</ModuleLayout.Half>
<ModuleLayout.Half>
<PipelineDurationChart groupId={groupId} />
</ModuleLayout.Half>
<ModuleLayout.Full>
<PipelineSpansTable groupId={groupId} />
</ModuleLayout.Full>
</ModuleLayout.Layout>
</Layout.Main>
</Layout.Body>
</NoProjectMessage>
</Feature>
</Layout.Page>
</SentryDocumentTitle>
</PageFiltersContainer>
);
}

const SpaceBetweenWrap = styled('div')`
display: flex;
justify-content: space-between;
flex-wrap: wrap;
`;

const MetricsRibbon = styled('div')`
display: flex;
flex-wrap: wrap;
gap: ${space(4)};
`;
Loading

0 comments on commit 6d9606f

Please sign in to comment.