From b612762a6ac63b39cc72466c468abaaf444ce922 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Mon, 2 Feb 2026 18:55:10 +0200 Subject: [PATCH] fix(site): filter build timeline events by agent ID (#21831) When a workspace has multiple agents (e.g., main + devcontainer), the build timeline was showing all events duplicated under each agent instead of filtering by the agent they belong to. Added agentId to the Stage type and filter timings by workspace_agent_id so each agent section only shows its own events. Fixes #18002 --- .../WorkspaceTiming/StagesChart.tsx | 9 +- .../WorkspaceTimings.stories.tsx | 129 ++++++++++++++++++ .../WorkspaceTiming/WorkspaceTimings.tsx | 65 +++++---- 3 files changed, 174 insertions(+), 29 deletions(-) diff --git a/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx b/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx index 84011a3eaf..29fcd0a218 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/StagesChart.tsx @@ -41,6 +41,11 @@ export type Stage = { * The section is used to group stages together. */ section: string; + /** + * The agent ID for agent-related stages. Used to filter timings correctly + * when multiple agents exist. + */ + agentId?: string; /** * The tooltip is used to provide additional information about the stage. */ @@ -268,12 +273,13 @@ export const provisioningStages: Stage[] = [ }, ]; -export const agentStages = (section: string): Stage[] => { +export const agentStages = (section: string, agentId: string): Stage[] => { return [ { name: "connect", label: "connect", section, + agentId, tooltip: { heading: "Connect", description: "Establish an RPC connection with the control plane.", @@ -283,6 +289,7 @@ export const agentStages = (section: string): Stage[] => { name: "start", label: "run startup scripts", section, + agentId, tooltip: { heading: "Run startup scripts", description: "Execute each agent startup script.", diff --git a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx index f82f1ccae5..5c7f91ddd6 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx @@ -273,11 +273,140 @@ export const InvalidTimeRange: Story = { display_name: "Startup Script 1", started_at: "0001-01-01T00:00:00Z", ended_at: "2025-01-01T00:10:00Z", + workspace_agent_id: "67e37a9d-ccac-497e-8f48-4093bcc4f3e7", + workspace_agent_name: "main", }, ], }, }; +// Test case for multiple agents (e.g., main + devcontainer) where each agent +// should only show its own timings, not duplicated across all agents. +export const MultipleAgents: Story = { + decorators: [ + (Story) => ( +
+ +
+ ), + ], + args: { + provisionerTimings: [ + { + job_id: "fb0a0941-5052-4f8b-8046-64da139220cd", + started_at: "2026-02-02T08:34:16.067798Z", + ended_at: "2026-02-02T08:34:16.626888Z", + stage: "init", + source: "coder", + action: "terraform", + resource: "coder_stage_init", + }, + { + job_id: "fb0a0941-5052-4f8b-8046-64da139220cd", + started_at: "2026-02-02T08:34:16.649547Z", + ended_at: "2026-02-02T08:34:18.65871Z", + stage: "plan", + source: "coder", + action: "terraform", + resource: "coder_stage_plan", + }, + { + job_id: "fb0a0941-5052-4f8b-8046-64da139220cd", + started_at: "2026-02-02T08:34:18.722631Z", + ended_at: "2026-02-02T08:34:21.332458Z", + stage: "apply", + source: "coder", + action: "terraform", + resource: "coder_stage_apply", + }, + { + job_id: "fb0a0941-5052-4f8b-8046-64da139220cd", + started_at: "2026-02-02T08:34:21.698283Z", + ended_at: "2026-02-02T08:34:22.014735Z", + stage: "graph", + source: "coder", + action: "terraform", + resource: "coder_stage_graph", + }, + ], + agentConnectionTimings: [ + { + started_at: "2026-02-02T08:34:22.092544Z", + ended_at: "2026-02-02T08:34:23.090936Z", + stage: "connect", + workspace_agent_id: "a1d50955-a671-4e6c-8e0e-6bc938e931bf", + workspace_agent_name: "dev", + }, + { + started_at: "2026-02-02T08:34:50.171921Z", + ended_at: "2026-02-02T08:34:51.36274Z", + stage: "connect", + workspace_agent_id: "afbdd368-b7b8-453e-af5a-02b13bc45553", + workspace_agent_name: "coder", + }, + ], + agentScriptTimings: [ + { + started_at: "2026-02-02T08:34:23.745887Z", + ended_at: "2026-02-02T08:34:25.23973Z", + exit_code: 0, + stage: "start", + status: "ok", + display_name: "Installing Dependencies", + workspace_agent_id: "a1d50955-a671-4e6c-8e0e-6bc938e931bf", + workspace_agent_name: "dev", + }, + { + started_at: "2026-02-02T08:34:23.743853Z", + ended_at: "2026-02-02T08:34:23.809943Z", + exit_code: 0, + stage: "start", + status: "ok", + display_name: "Git Clone", + workspace_agent_id: "a1d50955-a671-4e6c-8e0e-6bc938e931bf", + workspace_agent_name: "dev", + }, + { + started_at: "2026-02-02T08:34:23.74382Z", + ended_at: "2026-02-02T08:34:40.822488Z", + exit_code: 0, + stage: "start", + status: "ok", + display_name: "code-server", + workspace_agent_id: "a1d50955-a671-4e6c-8e0e-6bc938e931bf", + workspace_agent_name: "dev", + }, + // Same display_name as dev agent to test dedup scoping by agent ID. + { + started_at: "2026-02-02T08:34:51.5Z", + ended_at: "2026-02-02T08:34:53.2Z", + exit_code: 0, + stage: "start", + status: "ok", + display_name: "Installing Dependencies", + workspace_agent_id: "afbdd368-b7b8-453e-af5a-02b13bc45553", + workspace_agent_name: "coder", + }, + { + started_at: "2026-02-02T08:34:53.3Z", + ended_at: "2026-02-02T08:34:55.1Z", + exit_code: 0, + stage: "start", + status: "ok", + display_name: "Personalize", + workspace_agent_id: "afbdd368-b7b8-453e-af5a-02b13bc45553", + workspace_agent_name: "coder", + }, + ], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + // Verify both agents are shown + await canvas.findByText("agent (dev)"); + await canvas.findByText("agent (coder)"); + }, +}; + // A template with no agent scripts. export const NoAgentScripts: Story = { args: { diff --git a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx index c4e0e63228..67365cf941 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx @@ -52,21 +52,6 @@ export const WorkspaceTimings: FC = ({ defaultIsOpen = false, }) => { const [view, setView] = useState({ name: "default" }); - // This is a workaround to deal with the BE returning multiple timings for a - // single agent script when it should return only one. Reference: - // https://github.com/coder/coder/issues/15413#issuecomment-2493663571 - const uniqScriptTimings = uniqBy( - sortBy(agentScriptTimings, (t) => new Date(t.started_at).getTime() * -1), - (t) => t.display_name, - ); - const timings = [ - ...provisionerTimings, - ...uniqScriptTimings, - ...agentConnectionTimings, - ].sort((a, b) => { - return new Date(a.started_at).getTime() - new Date(b.started_at).getTime(); - }); - const [isOpen, setIsOpen] = useState(defaultIsOpen); // If any of the timings are empty, we are still loading the data. They can be @@ -79,21 +64,37 @@ export const WorkspaceTimings: FC = ({ // at least one entry. ].some((t) => t.length === 0); + // This is a workaround to deal with the BE returning multiple timings for a + // single agent script when it should return only one. Reference: + // https://github.com/coder/coder/issues/15413#issuecomment-2493663571 + const uniqScriptTimings = sortBy( + uniqBy( + sortBy(agentScriptTimings, (t) => t.started_at).reverse(), + (t) => `${t.workspace_agent_id}:${t.display_name}`, + ), + (t) => t.started_at, + ); + + // Combine agent timings for filtering by agent ID. + const agentTimings = [...agentConnectionTimings, ...uniqScriptTimings]; + // Each agent connection timing is a stage in the timeline to make it easier // to users to see the timing for connection and the other scripts. - const agentStageLabels = Array.from( - new Set( - agentConnectionTimings.map((t) => `agent (${t.workspace_agent_name})`), - ), - ); + const agents = uniqBy(agentConnectionTimings, (t) => t.workspace_agent_id); const stages = [ ...provisioningStages, - ...agentStageLabels.flatMap((a) => agentStages(a)), + ...agents.flatMap((agent) => + agentStages( + `agent (${agent.workspace_agent_name})`, + agent.workspace_agent_id, + ), + ), ]; const displayProvisioningTime = () => { - const totalRange = mergeTimeRanges(timings.map(toTimeRange)); + const allTimings = [...provisionerTimings, ...agentTimings]; + const totalRange = mergeTimeRanges(allTimings.map(toTimeRange)); const totalDuration = calcDuration(totalRange); return formatTime(totalDuration); }; @@ -131,9 +132,13 @@ export const WorkspaceTimings: FC = ({ {view.name === "default" && ( { - const stageTimings = timings.filter( - (t) => t.stage === s.name, - ); + const stageTimings = s.agentId + ? agentTimings.filter( + (t) => + t.stage === s.name && + t.workspace_agent_id === s.agentId, + ) + : provisionerTimings.filter((t) => t.stage === s.name); const stageRange = stageTimings.length === 0 ? undefined @@ -196,8 +201,12 @@ export const WorkspaceTimings: FC = ({ {view.stage.name === "start" && ( t.stage === view.stage.name) + timings={uniqScriptTimings + .filter( + (t) => + t.stage === view.stage.name && + t.workspace_agent_id === view.stage.agentId, + ) .map((t) => { return { range: toTimeRange(t), @@ -270,6 +279,6 @@ const styles = { borderTop: `1px solid ${theme.palette.divider}`, display: "flex", flexDirection: "column", - height: 420, + height: "var(--collapse-body-height, 420px)", }), } satisfies Record>;