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
This commit is contained in:
Mathias Fredriksson
2026-02-02 18:55:10 +02:00
committed by GitHub
parent f1dae81fd5
commit b612762a6a
3 changed files with 174 additions and 29 deletions
@@ -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.",
@@ -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) => (
<div css={{ "--collapse-body-height": "600px" }}>
<Story />
</div>
),
],
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: {
@@ -52,21 +52,6 @@ export const WorkspaceTimings: FC<WorkspaceTimingsProps> = ({
defaultIsOpen = false,
}) => {
const [view, setView] = useState<TimingView>({ 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<WorkspaceTimingsProps> = ({
// 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<WorkspaceTimingsProps> = ({
{view.name === "default" && (
<StagesChart
timings={stages.map((s) => {
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<WorkspaceTimingsProps> = ({
{view.stage.name === "start" && (
<ScriptsChart
timings={agentScriptTimings
.filter((t) => 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<string, Interpolation<Theme>>;