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>;