Skip to content

Commit b100e55

Browse files
authored
feat(mcp-insights): Add grouped by type widgets (#95424)
Added widgets that are grouped by type (tool, resource, prompt). https://github.com/user-attachments/assets/dd7a5b16-e744-4fab-acd0-2a7fb930d8b6 - closes [TET-855: Widget: Error rate by (tool/resource/prompt)](https://linear.app/getsentry/issue/TET-855/widget-error-rate-by-toolresourceprompt) - closes [TET-854: Widget: Duration by (tool/resource/prompt)](https://linear.app/getsentry/issue/TET-854/widget-duration-by-toolresourceprompt) - closes [TET-853: Widget: Traffic by (tool/resource/prompt)](https://linear.app/getsentry/issue/TET-853/widget-traffic-by-toolresourceprompt) Note: We still don't have data for everything that is why some charts are empty.
1 parent 8e94889 commit b100e55

17 files changed

+714
-212
lines changed
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import {Fragment} from 'react';
2+
import {useTheme} from '@emotion/react';
3+
4+
import {openInsightChartModal} from 'sentry/actionCreators/modal';
5+
import ExternalLink from 'sentry/components/links/externalLink';
6+
import {t, tct} from 'sentry/locale';
7+
import getDuration from 'sentry/utils/duration/getDuration';
8+
import useOrganization from 'sentry/utils/useOrganization';
9+
import {Line} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/line';
10+
import {TimeSeriesWidgetVisualization} from 'sentry/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization';
11+
import {Widget} from 'sentry/views/dashboards/widgets/widget/widget';
12+
import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode';
13+
import {useCombinedQuery} from 'sentry/views/insights/agentMonitoring/hooks/useCombinedQuery';
14+
import {ChartType} from 'sentry/views/insights/common/components/chart';
15+
import {useEAPSpans} from 'sentry/views/insights/common/queries/useDiscover';
16+
import {useTopNSpanEAPSeries} from 'sentry/views/insights/common/queries/useTopNDiscoverSeries';
17+
import {convertSeriesToTimeseries} from 'sentry/views/insights/common/utils/convertSeriesToTimeseries';
18+
import {usePageFilterChartParams} from 'sentry/views/insights/pages/platform/laravel/utils';
19+
import {WidgetVisualizationStates} from 'sentry/views/insights/pages/platform/laravel/widgetVisualizationStates';
20+
import {
21+
ModalChartContainer,
22+
ModalTableWrapper,
23+
SeriesColorIndicator,
24+
WidgetFooterTable,
25+
} from 'sentry/views/insights/pages/platform/shared/styles';
26+
import {Toolbar} from 'sentry/views/insights/pages/platform/shared/toolbar';
27+
import type {SpanStringFields} from 'sentry/views/insights/types';
28+
import {GenericWidgetEmptyStateWarning} from 'sentry/views/performance/landing/widgets/components/selectableList';
29+
30+
interface GroupedDurationWidgetProps {
31+
groupBy: SpanStringFields;
32+
query: string;
33+
referrer: string;
34+
title: string;
35+
}
36+
37+
export default function GroupedDurationWidget(props: GroupedDurationWidgetProps) {
38+
const organization = useOrganization();
39+
const pageFilterChartParams = usePageFilterChartParams({
40+
granularity: 'spans-low',
41+
});
42+
43+
const theme = useTheme();
44+
const fullQuery = useCombinedQuery(props.query);
45+
46+
const topEventsRequest = useEAPSpans(
47+
{
48+
fields: [props.groupBy, 'avg(span.duration)'],
49+
sorts: [{field: 'avg(span.duration)', kind: 'desc'}],
50+
search: fullQuery,
51+
limit: 3,
52+
},
53+
props.referrer
54+
);
55+
56+
const timeSeriesRequest = useTopNSpanEAPSeries(
57+
{
58+
...pageFilterChartParams,
59+
search: fullQuery,
60+
fields: [props.groupBy, 'avg(span.duration)'],
61+
yAxis: ['avg(span.duration)'],
62+
sort: {field: 'avg(span.duration)', kind: 'desc'},
63+
topN: 3,
64+
enabled: !!topEventsRequest.data && topEventsRequest.data.length > 0,
65+
},
66+
props.referrer
67+
);
68+
69+
const timeSeries = timeSeriesRequest.data;
70+
71+
const isLoading = timeSeriesRequest.isLoading || topEventsRequest.isLoading;
72+
const error = timeSeriesRequest.error || topEventsRequest.error;
73+
74+
const events = topEventsRequest.data;
75+
76+
const hasData = events && events.length > 0 && timeSeries.length > 0;
77+
78+
const colorPalette = theme.chart.getColorPalette(timeSeries.length - 1);
79+
80+
const visualization = (
81+
<WidgetVisualizationStates
82+
isEmpty={!hasData}
83+
isLoading={isLoading}
84+
error={error}
85+
emptyMessage={
86+
<GenericWidgetEmptyStateWarning
87+
message={tct(
88+
'No MCP spans found. Try updating your filters or learn more about MCP monitoring in our [link:documentation].',
89+
{
90+
link: <ExternalLink href="https://docs.sentry.io/product/insights/mcp/" />,
91+
}
92+
)}
93+
/>
94+
}
95+
VisualizationType={TimeSeriesWidgetVisualization}
96+
visualizationProps={{
97+
showLegend: 'never',
98+
plottables: timeSeries.map(
99+
(ts, index) =>
100+
new Line(convertSeriesToTimeseries(ts), {
101+
color:
102+
ts.seriesName === 'Other' ? theme.chart.neutral : colorPalette[index],
103+
alias: ts.seriesName,
104+
})
105+
),
106+
}}
107+
/>
108+
);
109+
110+
const footer = hasData && (
111+
<WidgetFooterTable>
112+
{events?.map((item, index) => {
113+
const groupName = item[props.groupBy] ?? t('Other');
114+
return (
115+
<Fragment key={groupName}>
116+
<div>
117+
<SeriesColorIndicator
118+
style={{
119+
backgroundColor: colorPalette[index],
120+
}}
121+
/>
122+
</div>
123+
<div>{groupName}</div>
124+
<span>{getDuration((item['avg(span.duration)'] ?? 0) / 1000, 2, true)}</span>
125+
</Fragment>
126+
);
127+
})}
128+
</WidgetFooterTable>
129+
);
130+
131+
return (
132+
<Widget
133+
Title={<Widget.WidgetTitle title={props.title} />}
134+
Visualization={visualization}
135+
Actions={
136+
organization.features.includes('visibility-explore-view') &&
137+
hasData && (
138+
<Toolbar
139+
showCreateAlert
140+
referrer={props.referrer}
141+
exploreParams={{
142+
mode: Mode.AGGREGATE,
143+
visualize: [
144+
{
145+
chartType: ChartType.BAR,
146+
yAxes: ['avg(span.duration)'],
147+
},
148+
],
149+
groupBy: [props.groupBy],
150+
query: fullQuery,
151+
sort: `-avg(span.duration)`,
152+
interval: pageFilterChartParams.interval,
153+
}}
154+
onOpenFullScreen={() => {
155+
openInsightChartModal({
156+
title: props.title,
157+
children: (
158+
<Fragment>
159+
<ModalChartContainer>{visualization}</ModalChartContainer>
160+
<ModalTableWrapper>{footer}</ModalTableWrapper>
161+
</Fragment>
162+
),
163+
});
164+
}}
165+
/>
166+
)
167+
}
168+
noFooterPadding
169+
Footer={footer}
170+
/>
171+
);
172+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import {Fragment} from 'react';
2+
import {useTheme} from '@emotion/react';
3+
4+
import {openInsightChartModal} from 'sentry/actionCreators/modal';
5+
import ExternalLink from 'sentry/components/links/externalLink';
6+
import {t, tct} from 'sentry/locale';
7+
import {formatPercentage} from 'sentry/utils/number/formatPercentage';
8+
import useOrganization from 'sentry/utils/useOrganization';
9+
import {Bars} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/bars';
10+
import {TimeSeriesWidgetVisualization} from 'sentry/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization';
11+
import {Widget} from 'sentry/views/dashboards/widgets/widget/widget';
12+
import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode';
13+
import {useCombinedQuery} from 'sentry/views/insights/agentMonitoring/hooks/useCombinedQuery';
14+
import {ChartType} from 'sentry/views/insights/common/components/chart';
15+
import {useEAPSpans} from 'sentry/views/insights/common/queries/useDiscover';
16+
import {useTopNSpanEAPSeries} from 'sentry/views/insights/common/queries/useTopNDiscoverSeries';
17+
import {convertSeriesToTimeseries} from 'sentry/views/insights/common/utils/convertSeriesToTimeseries';
18+
import {usePageFilterChartParams} from 'sentry/views/insights/pages/platform/laravel/utils';
19+
import {WidgetVisualizationStates} from 'sentry/views/insights/pages/platform/laravel/widgetVisualizationStates';
20+
import {
21+
ModalChartContainer,
22+
ModalTableWrapper,
23+
SeriesColorIndicator,
24+
WidgetFooterTable,
25+
} from 'sentry/views/insights/pages/platform/shared/styles';
26+
import {Toolbar} from 'sentry/views/insights/pages/platform/shared/toolbar';
27+
import type {SpanStringFields} from 'sentry/views/insights/types';
28+
import {GenericWidgetEmptyStateWarning} from 'sentry/views/performance/landing/widgets/components/selectableList';
29+
30+
interface GroupedErrorRateWidgetProps {
31+
groupBy: SpanStringFields;
32+
query: string;
33+
referrer: string;
34+
title: string;
35+
}
36+
37+
export default function GroupedErrorRateWidget(props: GroupedErrorRateWidgetProps) {
38+
const organization = useOrganization();
39+
const pageFilterChartParams = usePageFilterChartParams({
40+
granularity: 'spans-low',
41+
});
42+
43+
const theme = useTheme();
44+
const fullQuery = useCombinedQuery(props.query);
45+
46+
const topEventsRequest = useEAPSpans(
47+
{
48+
fields: [props.groupBy, 'failure_rate()'],
49+
sorts: [{field: 'failure_rate()', kind: 'desc'}],
50+
search: fullQuery,
51+
limit: 3,
52+
},
53+
props.referrer
54+
);
55+
56+
const timeSeriesRequest = useTopNSpanEAPSeries(
57+
{
58+
...pageFilterChartParams,
59+
search: fullQuery,
60+
fields: [props.groupBy, 'failure_rate()'],
61+
yAxis: ['failure_rate()'],
62+
sort: {field: 'failure_rate()', kind: 'desc'},
63+
topN: 3,
64+
enabled: !!topEventsRequest.data && topEventsRequest.data.length > 0,
65+
},
66+
props.referrer
67+
);
68+
69+
const timeSeries = timeSeriesRequest.data;
70+
71+
const isLoading = timeSeriesRequest.isLoading || topEventsRequest.isLoading;
72+
const error = timeSeriesRequest.error || topEventsRequest.error;
73+
74+
const events = topEventsRequest.data;
75+
76+
const hasData = events && events.length > 0 && timeSeries.length > 0;
77+
78+
const colorPalette = theme.chart.getColorPalette(timeSeries.length - 1);
79+
80+
const visualization = (
81+
<WidgetVisualizationStates
82+
isEmpty={!hasData}
83+
isLoading={isLoading}
84+
error={error}
85+
emptyMessage={
86+
<GenericWidgetEmptyStateWarning
87+
message={tct(
88+
'No MCP spans found. Try updating your filters or learn more about MCP monitoring in our [link:documentation].',
89+
{
90+
link: <ExternalLink href="https://docs.sentry.io/product/insights/mcp/" />,
91+
}
92+
)}
93+
/>
94+
}
95+
VisualizationType={TimeSeriesWidgetVisualization}
96+
visualizationProps={{
97+
showLegend: 'never',
98+
plottables: timeSeries.map(
99+
(ts, index) =>
100+
new Bars(convertSeriesToTimeseries(ts), {
101+
color:
102+
ts.seriesName === 'Other' ? theme.chart.neutral : colorPalette[index],
103+
alias: ts.seriesName,
104+
stack: 'stack',
105+
})
106+
),
107+
}}
108+
/>
109+
);
110+
111+
const footer = hasData && (
112+
<WidgetFooterTable>
113+
{events?.map((item, index) => {
114+
const groupName = item[props.groupBy] ?? t('Other');
115+
return (
116+
<Fragment key={groupName}>
117+
<div>
118+
<SeriesColorIndicator
119+
style={{
120+
backgroundColor: colorPalette[index],
121+
}}
122+
/>
123+
</div>
124+
<div>{groupName}</div>
125+
<span>{formatPercentage(item['failure_rate()'] ?? 0, 2)}</span>
126+
</Fragment>
127+
);
128+
})}
129+
</WidgetFooterTable>
130+
);
131+
132+
return (
133+
<Widget
134+
Title={<Widget.WidgetTitle title={props.title} />}
135+
Visualization={visualization}
136+
Actions={
137+
organization.features.includes('visibility-explore-view') &&
138+
hasData && (
139+
<Toolbar
140+
showCreateAlert
141+
referrer={props.referrer}
142+
exploreParams={{
143+
mode: Mode.AGGREGATE,
144+
visualize: [
145+
{
146+
chartType: ChartType.BAR,
147+
yAxes: ['failure_rate()'],
148+
},
149+
],
150+
groupBy: [props.groupBy],
151+
query: fullQuery,
152+
sort: `-failure_rate()`,
153+
interval: pageFilterChartParams.interval,
154+
}}
155+
onOpenFullScreen={() => {
156+
openInsightChartModal({
157+
title: props.title,
158+
children: (
159+
<Fragment>
160+
<ModalChartContainer>{visualization}</ModalChartContainer>
161+
<ModalTableWrapper>{footer}</ModalTableWrapper>
162+
</Fragment>
163+
),
164+
});
165+
}}
166+
/>
167+
)
168+
}
169+
noFooterPadding
170+
Footer={footer}
171+
/>
172+
);
173+
}

0 commit comments

Comments
 (0)