Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions apps/mesh/src/storage/monitoring-sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,8 +331,10 @@ export class SqlMonitoringStorage implements MonitoringStorage {
async query(filters: {
organizationId: string;
connectionId?: string;
connectionIds?: string[];
excludeConnectionIds?: string[];
virtualMcpId?: string;
virtualMcpIds?: string[];
toolName?: string;
isError?: boolean;
startDate?: Date;
Expand All @@ -351,7 +353,10 @@ export class SqlMonitoringStorage implements MonitoringStorage {
`organization_id = '${esc(filters.organizationId)}'`,
];

if (filters.connectionId) {
if (filters.connectionIds?.length) {
const ids = filters.connectionIds.map((id) => `'${esc(id)}'`).join(",");
where.push(`connection_id IN (${ids})`);
} else if (filters.connectionId) {
where.push(`connection_id = '${esc(filters.connectionId)}'`);
}
if (filters.excludeConnectionIds?.length) {
Expand All @@ -360,7 +365,10 @@ export class SqlMonitoringStorage implements MonitoringStorage {
.join(",");
where.push(`connection_id NOT IN (${ids})`);
}
if (filters.virtualMcpId) {
if (filters.virtualMcpIds?.length) {
const ids = filters.virtualMcpIds.map((id) => `'${esc(id)}'`).join(",");
where.push(`virtual_mcp_id IN (${ids})`);
} else if (filters.virtualMcpId) {
where.push(`virtual_mcp_id = '${esc(filters.virtualMcpId)}'`);
}
if (filters.toolName) {
Expand Down
2 changes: 2 additions & 0 deletions apps/mesh/src/storage/ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,10 @@ export interface MonitoringStorage {
query(filters: {
organizationId: string;
connectionId?: string;
connectionIds?: string[];
excludeConnectionIds?: string[];
virtualMcpId?: string;
virtualMcpIds?: string[];
toolName?: string;
isError?: boolean;
startDate?: Date;
Expand Down
18 changes: 17 additions & 1 deletion apps/mesh/src/tools/monitoring/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export const MONITORING_LOGS_LIST = defineTool({
},
inputSchema: z.object({
connectionId: z.string().optional().describe("Filter by connection ID"),
connectionIds: z
.array(z.string())
.optional()
.describe("Filter by multiple connection IDs"),
excludeConnectionIds: z
.array(z.string())
.optional()
Expand All @@ -60,6 +64,10 @@ export const MONITORING_LOGS_LIST = defineTool({
.string()
.optional()
.describe("Filter by Virtual MCP (Agent) ID"),
virtualMcpIds: z
.array(z.string())
.optional()
.describe("Filter by multiple Virtual MCP (Agent) IDs"),
toolName: z.string().optional().describe("Filter by tool name"),
isError: z.boolean().optional().describe("Filter by error status"),
startDate: z
Expand Down Expand Up @@ -104,7 +112,13 @@ export const MONITORING_LOGS_LIST = defineTool({
}),
handler: async (input, ctx) => {
// Flush buffered spans so the query sees the latest data (local mode).
await flushMonitoringData();
// Catch flush errors to avoid failing the entire query — stale data is
// preferable to no data at all.
try {
await flushMonitoringData();
} catch (err) {
console.warn("[monitoring] flush failed before list query:", err);
}

const org = requireOrganization(ctx);

Expand All @@ -126,8 +140,10 @@ export const MONITORING_LOGS_LIST = defineTool({
const filters = {
organizationId: org.id,
connectionId: input.connectionId,
connectionIds: input.connectionIds,
excludeConnectionIds: input.excludeConnectionIds,
virtualMcpId: input.virtualMcpId,
virtualMcpIds: input.virtualMcpIds,
toolName: input.toolName,
isError: input.isError,
startDate: input.startDate ? new Date(input.startDate) : undefined,
Expand Down
8 changes: 7 additions & 1 deletion apps/mesh/src/tools/monitoring/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,13 @@ export const MONITORING_STATS = defineTool({
handler: async (input, ctx) => {
const org = requireOrganization(ctx);
await ctx.access.check();
await flushMonitoringData();
// Catch flush errors to avoid failing the entire query — stale data is
// preferable to no data at all.
try {
await flushMonitoringData();
} catch (err) {
console.warn("[monitoring] flush failed before stats query:", err);
}

if (input.interval) {
const stats = await ctx.storage.monitoring.queryMetricTimeseries({
Expand Down
72 changes: 61 additions & 11 deletions apps/mesh/src/web/components/monitoring/analytics-top-tools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ChartContainer, ChartTooltip } from "@deco/ui/components/chart.tsx";
import { useConnections, useProjectContext } from "@decocms/mesh-sdk";
import { useNavigate } from "@tanstack/react-router";
import { Container } from "@untitledui/icons";
import { Line, LineChart, XAxis } from "recharts";
import { Line, LineChart, XAxis, YAxis } from "recharts";
import { useMonitoringLlmStats, useMonitoringTopTools } from "./hooks";

export type TopChartMetric =
Expand Down Expand Up @@ -72,10 +72,15 @@ function buildToolBuckets(
}
>();

// Always create exactly 10 display buckets spanning the full range
const BUCKET_COUNT = 20;
// Compute bucket count from interval so display buckets align with server data
const startMs = start.getTime();
const endMs = end.getTime();
const intervalMs =
interval === "1m" ? 60_000 : interval === "1h" ? 3_600_000 : 86_400_000;
const BUCKET_COUNT = Math.max(
2,
Math.min(60, Math.ceil((endMs - startMs) / intervalMs)),
);
const step = (endMs - startMs) / (BUCKET_COUNT - 1);

for (let i = 0; i < BUCKET_COUNT; i++) {
Expand Down Expand Up @@ -222,9 +227,14 @@ function buildLlmBuckets(
end: Date,
interval: "1m" | "1h" | "1d",
): LlmBucket[] {
const BUCKET_COUNT = 20;
const startMs = start.getTime();
const endMs = end.getTime();
const intervalMs =
interval === "1m" ? 60_000 : interval === "1h" ? 3_600_000 : 86_400_000;
const BUCKET_COUNT = Math.max(
2,
Math.min(60, Math.ceil((endMs - startMs) / intervalMs)),
);
const step = (endMs - startMs) / (BUCKET_COUNT - 1);

// Create empty display buckets
Expand Down Expand Up @@ -305,6 +315,7 @@ interface TopToolsContentProps {
status?: "success" | "error";
isStreaming?: boolean;
streamingRefetchInterval?: number;
onToolClick?: (toolName: string) => void;
}

function TopToolsContent({
Expand All @@ -316,6 +327,7 @@ function TopToolsContent({
status,
isStreaming,
streamingRefetchInterval,
onToolClick,
}: TopToolsContentProps) {
const { org } = useProjectContext();
const navigate = useNavigate();
Expand Down Expand Up @@ -466,14 +478,27 @@ function TopToolsContent({
>
<LineChart
data={llmBuckets}
margin={{ left: 8, right: 8, top: 5, bottom: 5 }}
margin={{ left: 0, right: 8, top: 5, bottom: 5 }}
>
<XAxis
dataKey="label"
axisLine={false}
tickLine={false}
tick={{ fontSize: 10, fill: "var(--muted-foreground)" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fontSize: 10, fill: "var(--muted-foreground)" }}
width={44}
tickFormatter={(value: number) =>
value >= 10000
? `${(value / 1000).toFixed(0)}k`
: value >= 1000
? `${(value / 1000).toFixed(1)}k`
: String(value)
}
Comment on lines +494 to +500
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The YAxis formatter is always using k-suffix counts, so latency metrics render as 1.5k instead of ms/s. Format latency metrics with ms/s on the axis to avoid misleading units.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/monitoring/analytics-top-tools.tsx, line 494:

<comment>The YAxis formatter is always using k-suffix counts, so latency metrics render as `1.5k` instead of ms/s. Format latency metrics with ms/s on the axis to avoid misleading units.</comment>

<file context>
@@ -466,14 +478,27 @@ function TopToolsContent({
+                  tickLine={false}
+                  tick={{ fontSize: 10, fill: "var(--muted-foreground)" }}
+                  width={44}
+                  tickFormatter={(value: number) =>
+                    value >= 10000
+                      ? `${(value / 1000).toFixed(0)}k`
</file context>
Suggested change
tickFormatter={(value: number) =>
value >= 10000
? `${(value / 1000).toFixed(0)}k`
: value >= 1000
? `${(value / 1000).toFixed(1)}k`
: String(value)
}
tickFormatter={(value: number) =>
metricsMode.includes("latency")
? formatTooltipValue(value, metricsMode)
: value >= 10000
? `${(value / 1000).toFixed(0)}k`
: value >= 1000
? `${(value / 1000).toFixed(1)}k`
: String(value)
}
Fix with Cubic

/>
<ChartTooltip
content={({ active, payload }) => {
if (!active || !payload || payload.length === 0)
Expand Down Expand Up @@ -525,22 +550,34 @@ function TopToolsContent({
<p className="text-sm text-muted-foreground">
{METRIC_LABELS[metricsMode]}
</p>
<div className="flex items-center gap-3">
{topTools.slice(0, 3).map((tool) => {
<div className="flex items-center gap-4 flex-wrap">
{topTools.map((tool) => {
const connection = connectionMap.get(tool.connectionId || "");
const color = toolColors.get(tool.toolName);
return (
<div key={tool.toolName} className="flex items-center gap-1">
<button
key={tool.toolName}
className="flex items-center gap-1.5 hover:opacity-70 transition-opacity cursor-pointer"
onClick={(e) => {
e.stopPropagation();
onToolClick?.(tool.toolName);
}}
>
<div
className="size-2.5 shrink-0 rounded-full"
style={{ backgroundColor: color }}
/>
<IntegrationIcon
icon={connection?.icon || null}
name={tool.toolName}
size="xs"
fallbackIcon={<Container />}
className="shrink-0 size-4! min-w-4! aspect-square rounded-sm"
/>
<span className="text-[10px] text-foreground truncate max-w-32">
<span className="text-xs text-foreground truncate max-w-40">
{tool.toolName}
</span>
</div>
</button>
);
})}
</div>
Expand All @@ -560,14 +597,27 @@ function TopToolsContent({
>
<LineChart
data={toolBuckets}
margin={{ left: 8, right: 8, top: 5, bottom: 5 }}
margin={{ left: 0, right: 8, top: 5, bottom: 5 }}
>
<XAxis
dataKey="label"
axisLine={false}
tickLine={false}
tick={{ fontSize: 10, fill: "var(--muted-foreground)" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fontSize: 10, fill: "var(--muted-foreground)" }}
width={44}
tickFormatter={(value: number) =>
value >= 10000
? `${(value / 1000).toFixed(0)}k`
: value >= 1000
? `${(value / 1000).toFixed(1)}k`
: String(value)
}
/>
<ChartTooltip
content={({ active, payload }) => {
if (!active || !payload || payload.length === 0) return null;
Expand Down
Loading
Loading