Skip to content

Commit 20c6cc4

Browse files
authored
feat(mcp-insights): Add traffic and traffic by transport (#95419)
Add traffic and traffic by transport distribution widgets. Adding a list of top issues to the traffic widget will be done in a follow up. - closes [TET-851: Widget: Transport distribution](https://linear.app/getsentry/issue/TET-851/widget-transport-distribution)
1 parent c232d0b commit 20c6cc4

File tree

9 files changed

+207
-26
lines changed

9 files changed

+207
-26
lines changed

static/app/components/charts/chartWidgetLoader.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ const CHART_MAP = {
188188
import(
189189
'sentry/views/insights/common/components/widgets/overviewSlowQueriesChartWidget'
190190
),
191+
mcpTrafficWidget: () =>
192+
import('sentry/views/insights/common/components/widgets/mcpTrafficWidget'),
191193
} satisfies Record<string, () => Promise<{default: React.FC<LoadableChartWidgetProps>}>>;
192194

193195
/**
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {t} from 'sentry/locale';
2+
import {useCombinedQuery} from 'sentry/views/insights/agentMonitoring/hooks/useCombinedQuery';
3+
import type {LoadableChartWidgetProps} from 'sentry/views/insights/common/components/widgets/types';
4+
import {MCPReferrer} from 'sentry/views/insights/mcp/utils/referrer';
5+
import {BaseTrafficWidget} from 'sentry/views/insights/pages/platform/shared/baseTrafficWidget';
6+
7+
export default function McpTrafficWidget(props: LoadableChartWidgetProps) {
8+
const query = useCombinedQuery('span.op:mcp.server');
9+
return (
10+
<BaseTrafficWidget
11+
id="mcpTrafficWidget"
12+
title={t('Traffic')}
13+
trafficSeriesName={t('Requests')}
14+
query={query}
15+
referrer={MCPReferrer.MCP_TRAFFIC_WIDGET}
16+
{...props}
17+
/>
18+
);
19+
}

static/app/views/insights/common/components/widgets/overviewJobsChartWidget.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,14 @@ export default function OverviewJobsChartWidget(props: LoadableChartWidgetProps)
6060
return [
6161
new Bars(convertSeriesToTimeseries(data['count(span.duration)']), {
6262
alias: ALIASES['count(span.duration)'],
63-
color: theme.gray200,
63+
color: theme.chart.neutral,
6464
}),
6565
new Line(convertSeriesToTimeseries(data['trace_status_rate(internal_error)']), {
6666
alias: ALIASES['trace_status_rate(internal_error)'],
6767
color: theme.error,
6868
}),
6969
];
70-
}, [data, theme.error, theme.gray200]);
70+
}, [data, theme.error, theme.chart.neutral]);
7171

7272
const isEmpty = useMemo(
7373
() =>
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 Count from 'sentry/components/count';
6+
import ExternalLink from 'sentry/components/links/externalLink';
7+
import {t, tct} from 'sentry/locale';
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 {MCPReferrer} from 'sentry/views/insights/mcp/utils/referrer';
19+
import {usePageFilterChartParams} from 'sentry/views/insights/pages/platform/laravel/utils';
20+
import {WidgetVisualizationStates} from 'sentry/views/insights/pages/platform/laravel/widgetVisualizationStates';
21+
import {
22+
ModalChartContainer,
23+
ModalTableWrapper,
24+
SeriesColorIndicator,
25+
WidgetFooterTable,
26+
} from 'sentry/views/insights/pages/platform/shared/styles';
27+
import {Toolbar} from 'sentry/views/insights/pages/platform/shared/toolbar';
28+
import {SpanFields} from 'sentry/views/insights/types';
29+
import {GenericWidgetEmptyStateWarning} from 'sentry/views/performance/landing/widgets/components/selectableList';
30+
31+
export default function McpTransportWidget() {
32+
const organization = useOrganization();
33+
const pageFilterChartParams = usePageFilterChartParams({
34+
granularity: 'spans-low',
35+
});
36+
37+
const theme = useTheme();
38+
const fullQuery = useCombinedQuery('span.op:mcp.server');
39+
40+
const topEventsRequest = useEAPSpans(
41+
{
42+
fields: [SpanFields.MCP_TRANSPORT, 'count()'],
43+
sorts: [{field: 'count()', kind: 'desc'}],
44+
search: fullQuery,
45+
limit: 3,
46+
},
47+
MCPReferrer.MCP_TRANSPORT_WIDGET
48+
);
49+
50+
const timeSeriesRequest = useTopNSpanEAPSeries(
51+
{
52+
...pageFilterChartParams,
53+
search: fullQuery,
54+
fields: [SpanFields.MCP_TRANSPORT, 'count(span.duration)'],
55+
yAxis: ['count(span.duration)'],
56+
sort: {field: 'count(span.duration)', kind: 'desc'},
57+
topN: 3,
58+
enabled: !!topEventsRequest.data && topEventsRequest.data.length > 0,
59+
},
60+
MCPReferrer.MCP_TRANSPORT_WIDGET
61+
);
62+
63+
const timeSeries = timeSeriesRequest.data;
64+
65+
const isLoading = timeSeriesRequest.isLoading || topEventsRequest.isLoading;
66+
const error = timeSeriesRequest.error || topEventsRequest.error;
67+
68+
// TODO(telex): Add model id attribute to Fields and get rid of this cast
69+
const models = topEventsRequest.data as unknown as Array<
70+
Record<string, string | number>
71+
>;
72+
73+
const hasData = models && models.length > 0 && timeSeries.length > 0;
74+
75+
const colorPalette = theme.chart.getColorPalette(timeSeries.length - 1);
76+
77+
const visualization = (
78+
<WidgetVisualizationStates
79+
isEmpty={!hasData}
80+
isLoading={isLoading}
81+
error={error}
82+
emptyMessage={
83+
<GenericWidgetEmptyStateWarning
84+
message={tct(
85+
'No MCP spans found. Try updating your filters or learn more about MCP monitoring in our [link:documentation].',
86+
{
87+
link: <ExternalLink href="https://docs.sentry.io/product/insights/mcp/" />,
88+
}
89+
)}
90+
/>
91+
}
92+
VisualizationType={TimeSeriesWidgetVisualization}
93+
visualizationProps={{
94+
showLegend: 'never',
95+
plottables: timeSeries.map(
96+
(ts, index) =>
97+
new Bars(convertSeriesToTimeseries(ts), {
98+
color:
99+
ts.seriesName === 'Other' ? theme.chart.neutral : colorPalette[index],
100+
alias: ts.seriesName,
101+
stack: 'stack',
102+
})
103+
),
104+
}}
105+
/>
106+
);
107+
108+
const footer = hasData && (
109+
<WidgetFooterTable>
110+
{models?.map((item, index) => {
111+
const transportName = item[SpanFields.MCP_TRANSPORT] ?? t('(none)');
112+
return (
113+
<Fragment key={transportName}>
114+
<div>
115+
<SeriesColorIndicator
116+
style={{
117+
backgroundColor: colorPalette[index],
118+
}}
119+
/>
120+
</div>
121+
<div>{transportName}</div>
122+
<span>
123+
<Count value={item['count()'] ?? 0} />
124+
</span>
125+
</Fragment>
126+
);
127+
})}
128+
</WidgetFooterTable>
129+
);
130+
131+
return (
132+
<Widget
133+
Title={<Widget.WidgetTitle title={t('Transport Distribution')} />}
134+
Visualization={visualization}
135+
Actions={
136+
organization.features.includes('visibility-explore-view') &&
137+
hasData && (
138+
<Toolbar
139+
showCreateAlert
140+
referrer={MCPReferrer.MCP_TRANSPORT_WIDGET}
141+
exploreParams={{
142+
mode: Mode.AGGREGATE,
143+
visualize: [
144+
{
145+
chartType: ChartType.BAR,
146+
yAxes: ['count(span.duration)'],
147+
},
148+
],
149+
groupBy: [SpanFields.MCP_TRANSPORT],
150+
query: fullQuery,
151+
sort: `-count(span.duration)`,
152+
interval: pageFilterChartParams.interval,
153+
}}
154+
onOpenFullScreen={() => {
155+
openInsightChartModal({
156+
title: t('Transport Distribution'),
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+
}

static/app/views/insights/mcp/components/placeholders.tsx

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,6 @@ function PlaceholderText() {
2828
return <PlaceholderContent>{t('Placeholder')}</PlaceholderContent>;
2929
}
3030

31-
export function McpTrafficWidget() {
32-
return (
33-
<Widget
34-
Title={<Widget.WidgetTitle title={t('Traffic + Error rate')} />}
35-
Visualization={<PlaceholderText />}
36-
/>
37-
);
38-
}
39-
4031
export function RequestsBySourceWidget() {
4132
return (
4233
<Widget
@@ -46,15 +37,6 @@ export function RequestsBySourceWidget() {
4637
);
4738
}
4839

49-
export function TransportDistributionWidget() {
50-
return (
51-
<Widget
52-
Title={<Widget.WidgetTitle title={t('Transport distribution')} />}
53-
Visualization={<PlaceholderText />}
54-
/>
55-
);
56-
}
57-
5840
export function GroupedTrafficWidget({
5941
groupBy,
6042
}: {

static/app/views/insights/mcp/components/styles.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,23 @@ const StyledGrid = styled('div')`
77
gap: ${space(2)};
88
99
grid-template-columns: minmax(0, 1fr);
10-
grid-template-rows: 300px 300px 300px;
10+
grid-template-rows: 260px 260px 260px;
1111
grid-template-areas:
1212
'pos1'
1313
'pos2'
1414
'pos3';
1515
1616
@media (min-width: ${p => p.theme.breakpoints.sm}) {
1717
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
18-
grid-template-rows: 300px 300px;
18+
grid-template-rows: 260px 260px;
1919
grid-template-areas:
2020
'pos1 pos2'
2121
'pos3 pos3';
2222
}
2323
2424
@media (min-width: ${p => p.theme.breakpoints.lg}) {
2525
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
26-
grid-template-rows: 300px;
26+
grid-template-rows: 260px;
2727
grid-template-areas: 'pos1 pos2 pos3';
2828
}
2929
`;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export enum MCPReferrer {
2+
MCP_TRAFFIC_WIDGET = 'mcp-traffic-widget',
3+
MCP_TRANSPORT_WIDGET = 'mcp-transport-widget',
4+
}

static/app/views/insights/mcp/views/overview.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,16 @@ import {ModulesOnboardingPanel} from 'sentry/views/insights/common/components/mo
2929
import {ModuleBodyUpsellHook} from 'sentry/views/insights/common/components/moduleUpsellHookWrapper';
3030
import {InsightsProjectSelector} from 'sentry/views/insights/common/components/projectSelector';
3131
import {ToolRibbon} from 'sentry/views/insights/common/components/ribbon';
32+
import McpTrafficWidget from 'sentry/views/insights/common/components/widgets/mcpTrafficWidget';
33+
import McpTransportWidget from 'sentry/views/insights/mcp/components/mcpTransportWidget';
3234
import {
3335
GroupedDurationWidget,
3436
GroupedErrorRateWidget,
3537
GroupedTrafficWidget,
36-
McpTrafficWidget,
3738
PromptsTable,
3839
RequestsBySourceWidget,
3940
ResourcesTable,
4041
ToolsTable,
41-
TransportDistributionWidget,
4242
} from 'sentry/views/insights/mcp/components/placeholders';
4343
import {WidgetGrid} from 'sentry/views/insights/mcp/components/styles';
4444
import {MODULE_TITLE} from 'sentry/views/insights/mcp/settings';
@@ -142,7 +142,7 @@ function McpOverviewPage() {
142142
<RequestsBySourceWidget />
143143
</WidgetGrid.Position2>
144144
<WidgetGrid.Position3>
145-
<TransportDistributionWidget />
145+
<McpTransportWidget />
146146
</WidgetGrid.Position3>
147147
</WidgetGrid>
148148
<ControlsWrapper>

static/app/views/insights/types.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export enum SpanFields {
111111
GEN_AI_USAGE_OUTPUT_TOKENS = 'gen_ai.usage.output_tokens',
112112
GEN_AI_USAGE_TOTAL_COST = 'gen_ai.usage.total_cost',
113113
GEN_AI_USAGE_TOTAL_TOKENS = 'gen_ai.usage.total_tokens',
114+
MCP_TRANSPORT = 'mcp.transport',
114115
}
115116

116117
type WebVitalsMeasurements =
@@ -168,6 +169,7 @@ type SpanStringFields =
168169
| SpanFields.GEN_AI_REQUEST_MODEL
169170
| SpanFields.GEN_AI_RESPONSE_MODEL
170171
| SpanFields.GEN_AI_TOOL_NAME
172+
| SpanFields.MCP_TRANSPORT
171173
| 'span_id'
172174
| 'span.op'
173175
| 'span.description'

0 commit comments

Comments
 (0)