Skip to content

Commit

Permalink
feat(ai): Add preliminary AI analytics page content (getsentry#69010)
Browse files Browse the repository at this point in the history
<img width="908" alt="Screenshot 2024-04-16 at 2 36 14 PM"
src="https://github.com/getsentry/sentry/assets/161344340/5c1c9d45-774d-42bf-9f15-9de1d5f4b8e7">
  • Loading branch information
colin-sentry authored Apr 16, 2024
1 parent 92e8a0c commit 144ff52
Show file tree
Hide file tree
Showing 7 changed files with 423 additions and 230 deletions.
6 changes: 1 addition & 5 deletions static/app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1491,11 +1491,7 @@ function buildRoutes() {
);

const aiAnalyticsRoutes = (
<Route
path="/ai-analytics/"
component={make(() => import('sentry/views/aiAnalytics'))}
withOrgPath
>
<Route path="/ai-analytics/" withOrgPath>
<IndexRoute component={make(() => import('sentry/views/aiAnalytics/landing'))} />
</Route>
);
Expand Down
181 changes: 181 additions & 0 deletions static/app/views/aiAnalytics/PipelinesTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import {browserHistory} from 'react-router';
import type {Location} from 'history';

import type {GridColumnHeader} from 'sentry/components/gridEditable';
import GridEditable, {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
import Link from 'sentry/components/links/link';
import type {CursorHandler} from 'sentry/components/pagination';
import Pagination from 'sentry/components/pagination';
import {t} from 'sentry/locale';
import type {Organization} from 'sentry/types';
import type {EventsMetaType} from 'sentry/utils/discover/eventView';
import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
import type {Sort} from 'sentry/utils/discover/fields';
import {RATE_UNIT_TITLE, RateUnit} from 'sentry/utils/discover/fields';
import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
import {decodeScalar, decodeSorts} from 'sentry/utils/queryString';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';
import {normalizeUrl} from 'sentry/utils/withDomainRequired';
import {renderHeadCell} from 'sentry/views/starfish/components/tableCells/renderHeadCell';
import {useSpanMetrics} from 'sentry/views/starfish/queries/useSpanMetrics';
import type {MetricsResponse} from 'sentry/views/starfish/types';
import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters';
import {DataTitles} from 'sentry/views/starfish/views/spans/types';

type Row = Pick<
MetricsResponse,
| 'project.id'
| 'span.description'
| 'span.group'
| 'spm()'
| 'avg(span.self_time)'
| 'sum(span.self_time)'
| 'time_spent_percentage()'
>;

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

const COLUMN_ORDER: Column[] = [
{
key: 'span.description',
name: t('AI Pipeline name'),
width: COL_WIDTH_UNDEFINED,
},
{
key: 'spm()',
name: `${t('Times')} ${RATE_UNIT_TITLE[RateUnit.PER_MINUTE]}`,
width: COL_WIDTH_UNDEFINED,
},
{
key: `avg(span.self_time)`,
name: DataTitles.avg,
width: COL_WIDTH_UNDEFINED,
},
{
key: 'time_spent_percentage()',
name: DataTitles.timeSpent,
width: COL_WIDTH_UNDEFINED,
},
];

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

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

export function isAValidSort(sort: Sort): sort is ValidSort {
return (SORTABLE_FIELDS as unknown as string[]).includes(sort.field);
}

export function PipelinesTable() {
const location = useLocation();
const organization = useOrganization();
const cursor = decodeScalar(location.query?.[QueryParameterNames.SPANS_CURSOR]);
const sortField = decodeScalar(location.query?.[QueryParameterNames.SPANS_SORT]);

let sort = decodeSorts(sortField).filter(isAValidSort)[0];
if (!sort) {
sort = {field: 'time_spent_percentage()', kind: 'desc'};
}
const {data, isLoading, meta, pageLinks, error} = useSpanMetrics({
search: new MutableSearch('span.op:ai.pipeline.langchain'),
fields: [
'project.id',
'span.group',
'span.description',
'spm()',
'avg(span.self_time)',
'sum(span.self_time)',
'time_spent_percentage()',
],
sorts: [sort],
limit: 25,
cursor,
});

const handleCursor: CursorHandler = (newCursor, pathname, query) => {
browserHistory.push({
pathname,
query: {...query, [QueryParameterNames.SPANS_CURSOR]: newCursor},
});
};

return (
<VisuallyCompleteWithData
id="PipelinesTable"
hasData={data.length > 0}
isLoading={isLoading}
>
<GridEditable
isLoading={isLoading}
error={error}
data={data}
columnOrder={COLUMN_ORDER}
columnSortBy={[
{
key: sort.field,
order: sort.kind,
},
]}
grid={{
renderHeadCell: column =>
renderHeadCell({
column,
sort,
location,
sortParameterName: QueryParameterNames.SPANS_SORT,
}),
renderBodyCell: (column, row) =>
renderBodyCell(column, row, meta, location, organization),
}}
location={location}
/>
<Pagination pageLinks={pageLinks} onCursor={handleCursor} />
</VisuallyCompleteWithData>
);
}

function renderBodyCell(
column: Column,
row: Row,
meta: EventsMetaType | undefined,
location: Location,
organization: Organization
) {
if (column.key === 'span.description') {
if (!row['span.description']) {
return <span>(unknown)</span>;
}
if (!row['span.group']) {
return <span>{row['span.description']}</span>;
}
return (
<Link
to={normalizeUrl(
`/organizations/${organization.slug}/ai-analytics/pipelines/${row['span.group']}`
)}
>
{row['span.description']}
</Link>
);
}

if (!meta || !meta?.fields) {
return row[column.key];
}

const renderer = getFieldRenderer(column.key, meta.fields, false);

const rendered = renderer(row, {
location,
organization,
unit: meta.units?.[column.key],
});

return rendered;
}
168 changes: 168 additions & 0 deletions static/app/views/aiAnalytics/aiAnalyticsCharts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import styled from '@emotion/styled';

import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import {MetricDisplayType} from 'sentry/utils/metrics/types';
import {useMetricsQuery} from 'sentry/utils/metrics/useMetricsQuery';
import usePageFilters from 'sentry/utils/usePageFilters';
import {MetricChartContainer} from 'sentry/views/dashboards/metrics/chart';

export function TotalTokensUsedChart() {
const {selection, isReady: isGlobalSelectionReady} = usePageFilters();
const {
data: timeseriesData,
isLoading,
isError,
error,
} = useMetricsQuery(
[
{
name: 'total',
mri: `c:spans/ai.total_tokens.used@none`,
op: 'sum',
},
],
selection,
{
intervalLadder: 'dashboard',
}
);

if (!isGlobalSelectionReady) {
return null;
}

if (isError) {
return <div>{'' + error}</div>;
}

return (
<TokenChartContainer>
<PanelTitle>{t('Total tokens used')}</PanelTitle>
<MetricChartContainer
timeseriesData={timeseriesData}
isLoading={isLoading}
metricQueries={[
{
name: 'mql',
formula: '$total',
},
]}
displayType={MetricDisplayType.AREA}
chartHeight={200}
/>
</TokenChartContainer>
);
}

export function NumberOfPipelinesChart() {
const {selection, isReady: isGlobalSelectionReady} = usePageFilters();
const {
data: timeseriesData,
isLoading,
isError,
error,
} = useMetricsQuery(
[
{
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
},
],
selection,
{
intervalLadder: 'dashboard',
}
);

if (!isGlobalSelectionReady) {
return null;
}

if (isError) {
return <div>{'' + error}</div>;
}

return (
<TokenChartContainer>
<PanelTitle>{t('Number of AI pipelines')}</PanelTitle>
<MetricChartContainer
timeseriesData={timeseriesData}
isLoading={isLoading}
metricQueries={[
{
name: 'mql',
formula: '$number',
},
]}
displayType={MetricDisplayType.AREA}
chartHeight={200}
/>
</TokenChartContainer>
);
}

export function PipelineDurationChart() {
const {selection, isReady: isGlobalSelectionReady} = usePageFilters();
const {
data: timeseriesData,
isLoading,
isError,
error,
} = useMetricsQuery(
[
{
name: 'number',
mri: `d:spans/exclusive_time@millisecond`,
op: 'avg',
query: 'span.op:"ai.pipeline.langchain"', // TODO: for now this is the only AI "pipeline" supported
},
],
selection,
{
intervalLadder: 'dashboard',
}
);

if (!isGlobalSelectionReady) {
return null;
}

if (isError) {
return <div>{'' + error}</div>;
}

return (
<TokenChartContainer>
<PanelTitle>{t('AI pipeline duration')}</PanelTitle>
<MetricChartContainer
timeseriesData={timeseriesData}
isLoading={isLoading}
metricQueries={[
{
name: 'mql',
formula: '$number',
},
]}
displayType={MetricDisplayType.AREA}
chartHeight={200}
/>
</TokenChartContainer>
);
}

const PanelTitle = styled('h5')`
padding: ${space(3)} ${space(3)} 0;
margin: 0;
`;

const TokenChartContainer = styled('div')`
overflow: hidden;
border: 1px solid ${p => p.theme.border};
border-radius: ${p => p.theme.borderRadius};
height: 100%;
display: flex;
flex-direction: column;
`;
Loading

0 comments on commit 144ff52

Please sign in to comment.