Skip to content

Commit e7fc468

Browse files
authored
ref(insights): Refactor Insights -> LLM charts (#90094)
Refactors the Insights -> LLM charts to better support deep linking in a [future PR](#88562). Notable in this PR is that these widgets are used in 3 different sections: 1) Landing Page - no additional args 2) Details Page - takes a `groupId` from URL parameters 3) "LLM Monitor Section" (group details page) - The wrapping components takes in an `event` argument, which uses the trace (span id + trace id) to query for a groupId to pass to widgets We're going to refactor the 3 widgets into: - ~(3) base widgets that accept an optional `groupId` (referring to AI Pipeline Group, and not our normal Issues Group)~ - (3) widgets that do not require a groupId from URL params. These widgets handle point 1) above - (3) widgets that use `groupId` from URL parameters. These widgets handle points ~1) and~ 2) above. - (2) widgets (`llmEvent*`) that take `groupId` (Issues Group!) and `eventId` from URL parameters to fetch the Issues Group, and then tries to find the AI Pipeline Group id via the event's trace. This AI Pipeline Group ID is then used in the query and renders the appropriate charts. These widgets account for point 3) above. ~In order to handle this[^1] (and for future widgets), this PR introduces a `chartProperties` prop for `LoadableChartWidgetProps`. These are to be used as URL query params when linking to the Releases Drawer, but also used to render the chart as well. They are chart-specific, which is the reason for the additional level of object nesting. Think of it as these two stages:~ ~- "Serialization": we have a chart, and want to "serialize" it into a URL. This will involve 2 steps~ ~- taking the *keys* of `chartProperties` object and using a known query parameter key (e.g. `rdChartPropertyKey`) to create a list of query parameters (e.g. `rdChartPropertyKey=foo&rdChartPropertyKey=bar`)~ ~- turning `chartProperties` into query parameters (e.g. `foo=val1&bar=val2`)~ ~- "Parsing": inverse of "serialization" -- we have a URL and want to render a chart.~ ~- using known key (`rdChartPropertyKey`), we're able to fetch the keys of `chartProperties` (which can vary from chart to chart)~ ~- with the keys, we can again look through the query parameters to get the values for each key~ ~The known key (e.g. `rdChartPropertKey`) acts as an indirect pointer to the `chartProperties` object which has dynamic keys. See [this commit](00d9fd6#diff-0269df2be334787a8238014bd4dae87472c3adf1b5f8021a7a151fc464427c3fR33-R42) for an example.~ Part of #88560 [^1]: There is another solution, since (1) and (2) can be handled without any changes, we would just need slightly diff widgets for (3) where it's mostly the same except it expects `eventId` to available via URL params and it fetches the event to get the trace and then fetches the group id
1 parent 7d87c7f commit e7fc468

15 files changed

+391
-158
lines changed

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

Lines changed: 11 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,15 @@ import {LinkButton} from 'sentry/components/core/button';
22
import {ButtonBar} from 'sentry/components/core/button/buttonBar';
33
import {IconOpen} from 'sentry/icons';
44
import {t} from 'sentry/locale';
5-
import type {Event} from 'sentry/types/event';
6-
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
75
import * as ModuleLayout from 'sentry/views/insights/common/components/moduleLayout';
8-
import {useSpansIndexed} from 'sentry/views/insights/common/queries/useDiscover';
6+
import LlmEventNumberOfPipelinesChartWidget from 'sentry/views/insights/common/components/widgets/llmEventNumberOfPipelinesChartWidget';
7+
import LlmEventTotalTokensUsedChartWidget from 'sentry/views/insights/common/components/widgets/llmEventTotalTokensUsedChartWidget';
98
import {useModuleURL} from 'sentry/views/insights/common/utils/useModuleURL';
10-
import {
11-
NumberOfPipelinesChart,
12-
TotalTokensUsedChart,
13-
} from 'sentry/views/insights/llmMonitoring/components/charts/llmMonitoringCharts';
14-
import {SpanIndexedField} from 'sentry/views/insights/types';
159
import {SectionKey} from 'sentry/views/issueDetails/streamline/context';
1610
import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection';
1711

18-
interface Props {
19-
event: Event;
20-
}
21-
22-
export default function LLMMonitoringSection({event}: Props) {
12+
export default function LLMMonitoringSection() {
2313
const moduleUrl = useModuleURL('ai');
24-
const trace = event.contexts.trace;
25-
26-
const {data} = useSpansIndexed(
27-
{
28-
limit: 1,
29-
fields: [SpanIndexedField.SPAN_AI_PIPELINE_GROUP],
30-
search: new MutableSearch(`trace:${trace?.trace_id} id:"${trace?.span_id}"`),
31-
enabled: Boolean(trace?.span_id) && Boolean(trace?.trace_id),
32-
},
33-
'api.ai-pipelines.view'
34-
);
35-
36-
const aiPipelineGroup = data[0]?.[SpanIndexedField.SPAN_AI_PIPELINE_GROUP];
3714

3815
const actions = (
3916
<ButtonBar gap={1}>
@@ -50,18 +27,14 @@ export default function LLMMonitoringSection({event}: Props) {
5027
help={t('Charts showing how many tokens are being used')}
5128
actions={actions}
5229
>
53-
{aiPipelineGroup ? (
54-
<ModuleLayout.Layout>
55-
<ModuleLayout.Half>
56-
<TotalTokensUsedChart groupId={aiPipelineGroup} />
57-
</ModuleLayout.Half>
58-
<ModuleLayout.Half>
59-
<NumberOfPipelinesChart groupId={aiPipelineGroup} />
60-
</ModuleLayout.Half>
61-
</ModuleLayout.Layout>
62-
) : (
63-
'loading'
64-
)}
30+
<ModuleLayout.Layout>
31+
<ModuleLayout.Half>
32+
<LlmEventTotalTokensUsedChartWidget />
33+
</ModuleLayout.Half>
34+
<ModuleLayout.Half>
35+
<LlmEventNumberOfPipelinesChartWidget />
36+
</ModuleLayout.Half>
37+
</ModuleLayout.Layout>
6538
</InterimSection>
6639
);
6740
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
2+
import {useSpansIndexed} from 'sentry/views/insights/common/queries/useDiscover';
3+
import {SpanIndexedField} from 'sentry/views/insights/types';
4+
import {useGroupEvent} from 'sentry/views/issueDetails/useGroupEvent';
5+
6+
/**
7+
* Given an issue's groupId + eventId, fetch the AI
8+
* Pipeline's group ID via event's trace
9+
*/
10+
export function useAiPipelineGroup({
11+
groupId,
12+
eventId,
13+
}: {
14+
groupId: string;
15+
eventId?: string;
16+
}) {
17+
const {
18+
data: event,
19+
isPending: isGroupPending,
20+
error: groupError,
21+
} = useGroupEvent({groupId, eventId});
22+
23+
const trace = event?.contexts.trace;
24+
const {
25+
data,
26+
isPending: isSpanPending,
27+
error: spanError,
28+
} = useSpansIndexed(
29+
{
30+
limit: 1,
31+
fields: [SpanIndexedField.SPAN_AI_PIPELINE_GROUP],
32+
search: new MutableSearch(`trace:${trace?.trace_id} id:"${trace?.span_id}"`),
33+
enabled: Boolean(trace?.span_id) && Boolean(trace?.trace_id),
34+
},
35+
'api.ai-pipelines.view'
36+
);
37+
38+
return {
39+
groupId: data[0]?.[SpanIndexedField.SPAN_AI_PIPELINE_GROUP],
40+
isPending: isGroupPending || isSpanPending,
41+
error: groupError || spanError,
42+
};
43+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {useTheme} from '@emotion/react';
2+
3+
import {t} from 'sentry/locale';
4+
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
5+
import {useParams} from 'sentry/utils/useParams';
6+
import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget';
7+
import {useAiPipelineGroup} from 'sentry/views/insights/common/components/widgets/hooks/useAiPipelineGroup';
8+
import type {LoadableChartWidgetProps} from 'sentry/views/insights/common/components/widgets/types';
9+
import {useSpanMetricsSeries} from 'sentry/views/insights/common/queries/useDiscoverSeries';
10+
11+
export default function LlmEventNumberOfPipelinesChartWidget(
12+
props: LoadableChartWidgetProps
13+
) {
14+
const params = useParams<{groupId: string; eventId?: string}>();
15+
const {groupId, isPending, error} = useAiPipelineGroup(params);
16+
17+
const theme = useTheme();
18+
const aggregate = 'count()';
19+
20+
let query = 'span.category:"ai.pipeline"';
21+
if (groupId) {
22+
query = `${query} span.group:"${groupId}"`;
23+
}
24+
const {
25+
data,
26+
isPending: spanMetricsSeriesIsPending,
27+
error: spanMetricsSeriesError,
28+
} = useSpanMetricsSeries(
29+
{
30+
yAxis: [aggregate],
31+
search: new MutableSearch(query),
32+
transformAliasToInputFormat: true,
33+
enabled: !!groupId,
34+
},
35+
'api.ai-pipelines.view',
36+
props.pageFilters
37+
);
38+
39+
const colors = theme.chart.getColorPalette(2);
40+
return (
41+
<InsightsLineChartWidget
42+
{...props}
43+
id="llmEventNumberOfPipelinesChartWidget"
44+
isLoading={isPending || spanMetricsSeriesIsPending}
45+
error={error || spanMetricsSeriesError}
46+
title={t('Number of AI pipelines')}
47+
series={[{...data[aggregate], color: colors[1]}]}
48+
/>
49+
);
50+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {useTheme} from '@emotion/react';
2+
3+
import {t} from 'sentry/locale';
4+
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
5+
import {useParams} from 'sentry/utils/useParams';
6+
import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget';
7+
import {useAiPipelineGroup} from 'sentry/views/insights/common/components/widgets/hooks/useAiPipelineGroup';
8+
import type {LoadableChartWidgetProps} from 'sentry/views/insights/common/components/widgets/types';
9+
import {useSpanMetricsSeries} from 'sentry/views/insights/common/queries/useDiscoverSeries';
10+
11+
export default function LlmEventTotalTokensUsedChartWidget(
12+
props: LoadableChartWidgetProps
13+
) {
14+
const params = useParams<{groupId: string; eventId?: string}>();
15+
const {groupId, isPending, error} = useAiPipelineGroup(params);
16+
17+
const theme = useTheme();
18+
const aggregate = 'sum(ai.total_tokens.used)';
19+
20+
let query = 'span.category:"ai"';
21+
if (groupId) {
22+
query = `${query} span.ai.pipeline.group:"${groupId}"`;
23+
}
24+
const {
25+
data,
26+
isPending: spanMetricsSeriesIsPending,
27+
error: spanMetricsSeriesError,
28+
} = useSpanMetricsSeries(
29+
{
30+
yAxis: [aggregate],
31+
search: new MutableSearch(query),
32+
transformAliasToInputFormat: true,
33+
enabled: !!groupId,
34+
},
35+
'api.ai-pipelines.view',
36+
props.pageFilters
37+
);
38+
39+
const colors = theme.chart.getColorPalette(2);
40+
return (
41+
<InsightsLineChartWidget
42+
{...props}
43+
id="llmEventTotalTokensUsedChartWidget"
44+
isLoading={isPending || spanMetricsSeriesIsPending}
45+
error={error || spanMetricsSeriesError}
46+
title={t('Total tokens used')}
47+
series={[{...data[aggregate], color: colors[0]}]}
48+
/>
49+
);
50+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {useTheme} from '@emotion/react';
2+
3+
import {t} from 'sentry/locale';
4+
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
5+
import {useParams} from 'sentry/utils/useParams';
6+
import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget';
7+
import type {LoadableChartWidgetProps} from 'sentry/views/insights/common/components/widgets/types';
8+
import {useSpanMetricsSeries} from 'sentry/views/insights/common/queries/useDiscoverSeries';
9+
10+
export default function LlmGroupNumberOfPipelinesChartWidget(
11+
props: LoadableChartWidgetProps
12+
) {
13+
const {groupId} = useParams<{groupId: string}>();
14+
const theme = useTheme();
15+
const aggregate = 'count()';
16+
17+
const query = `span.category:"ai.pipeline" span.group:"${groupId}"`;
18+
const {data, isPending, error} = useSpanMetricsSeries(
19+
{
20+
yAxis: [aggregate],
21+
search: new MutableSearch(query),
22+
transformAliasToInputFormat: true,
23+
},
24+
'api.ai-pipelines.view',
25+
props.pageFilters
26+
);
27+
28+
const colors = theme.chart.getColorPalette(2);
29+
return (
30+
<InsightsLineChartWidget
31+
{...props}
32+
id="llmGroupNumberOfPipelinesChartWidget"
33+
title={t('Number of AI pipelines')}
34+
series={[{...data[aggregate], color: colors[1]}]}
35+
isLoading={isPending}
36+
error={error}
37+
/>
38+
);
39+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {useTheme} from '@emotion/react';
2+
3+
import {t} from 'sentry/locale';
4+
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
5+
import {useParams} from 'sentry/utils/useParams';
6+
import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget';
7+
import type {LoadableChartWidgetProps} from 'sentry/views/insights/common/components/widgets/types';
8+
import {useSpanMetricsSeries} from 'sentry/views/insights/common/queries/useDiscoverSeries';
9+
10+
export default function LlmGroupPipelineDurationChartWidget(
11+
props: LoadableChartWidgetProps
12+
) {
13+
const {groupId} = useParams<{groupId: string}>();
14+
const theme = useTheme();
15+
const aggregate = 'avg(span.duration)';
16+
17+
const query = `span.category:"ai.pipeline" span.group:"${groupId}"`;
18+
const {data, isPending, error} = useSpanMetricsSeries(
19+
{
20+
yAxis: [aggregate],
21+
search: new MutableSearch(query),
22+
transformAliasToInputFormat: true,
23+
},
24+
'api.ai-pipelines.view',
25+
props.pageFilters
26+
);
27+
28+
const colors = theme.chart.getColorPalette(2);
29+
return (
30+
<InsightsLineChartWidget
31+
{...props}
32+
id="llmGroupPipelineDurationChartWidget"
33+
title={t('Pipeline duration')}
34+
series={[{...data[aggregate], color: colors[2]}]}
35+
isLoading={isPending}
36+
error={error}
37+
/>
38+
);
39+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {useTheme} from '@emotion/react';
2+
3+
import {t} from 'sentry/locale';
4+
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
5+
import {useParams} from 'sentry/utils/useParams';
6+
import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget';
7+
import type {LoadableChartWidgetProps} from 'sentry/views/insights/common/components/widgets/types';
8+
import {useSpanMetricsSeries} from 'sentry/views/insights/common/queries/useDiscoverSeries';
9+
10+
export default function LlmGroupTotalTokensUsedChartWidget(
11+
props: LoadableChartWidgetProps
12+
) {
13+
const {groupId} = useParams<{groupId: string}>();
14+
const theme = useTheme();
15+
const aggregate = 'sum(ai.total_tokens.used)';
16+
17+
const query = `span.category:"ai" span.ai.pipeline.group:"${groupId}"`;
18+
const {data, isPending, error} = useSpanMetricsSeries(
19+
{
20+
yAxis: [aggregate],
21+
search: new MutableSearch(query),
22+
transformAliasToInputFormat: true,
23+
},
24+
'api.ai-pipelines.view',
25+
props.pageFilters
26+
);
27+
28+
const colors = theme.chart.getColorPalette(2);
29+
return (
30+
<InsightsLineChartWidget
31+
{...props}
32+
id="llmGroupTotalTokensUsedChartWidget"
33+
title={t('Total tokens used')}
34+
series={[{...data[aggregate], color: colors[0]}]}
35+
isLoading={isPending}
36+
error={error}
37+
/>
38+
);
39+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {useTheme} from '@emotion/react';
2+
3+
import {t} from 'sentry/locale';
4+
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
5+
import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget';
6+
import type {LoadableChartWidgetProps} from 'sentry/views/insights/common/components/widgets/types';
7+
import {useSpanMetricsSeries} from 'sentry/views/insights/common/queries/useDiscoverSeries';
8+
9+
export default function LlmNumberOfPipelinesChartWidget(props: LoadableChartWidgetProps) {
10+
const theme = useTheme();
11+
const aggregate = 'count()';
12+
13+
const query = 'span.category:"ai.pipeline"';
14+
const {data, isPending, error} = useSpanMetricsSeries(
15+
{
16+
yAxis: [aggregate],
17+
search: new MutableSearch(query),
18+
transformAliasToInputFormat: true,
19+
},
20+
'api.ai-pipelines.view',
21+
props.pageFilters
22+
);
23+
24+
const colors = theme.chart.getColorPalette(2);
25+
return (
26+
<InsightsLineChartWidget
27+
{...props}
28+
id="llmNumberOfPipelinesChartWidget"
29+
title={t('Number of AI pipelines')}
30+
series={[{...data[aggregate], color: colors[1]}]}
31+
isLoading={isPending}
32+
error={error}
33+
/>
34+
);
35+
}

0 commit comments

Comments
 (0)