fix(coderd): render collected_at as UTC RFC3339 in the agent metadata aggregate (#27991)

Follow-up to #27934; this fix was pushed to the branch after the
squash-merge and missed it.

`jsonb_build_object` renders timestamptz in the session `TimeZone`,
which Coder never pins, and `collected_at` defaults to year 1 until the
agent's first report. On a non-UTC Postgres session a
registered-but-never-collected item renders with an LMT second-offset
(even `BC`, e.g. `0001-12-31T19:03:58-04:56:02 BC`), which Go's RFC3339
parsing rejects - a 500 for the entire list page whenever
`include_agent_metadata` is used.

- `to_char(... AT TIME ZONE 'UTC', ...)` pins the rendering;
never-collected items round-trip as Go's zero time.
- The test now runs against a named-zone database
(`dbtestutil.WithTimezone("America/Caracas")`) and requests a registered
but never-collected key; it reproduces the 500 without the fix.

Also contains the failure mode Go-side: an unparsable aggregate now
degrades to missing metadata for that workspace (with a warning log)
instead of failing the entire page. The SQL fix prevents the known
cause; the containment covers any future one. The test still catches
regressions because it asserts the metadata values, not just a 200.

---

Authored by Coder Agents on behalf of @Emyrk.
This commit is contained in:
Steven Masley
2026-08-10 14:53:48 -05:00
committed by GitHub
parent 66b065323b
commit 053b38944d
4 changed files with 54 additions and 30 deletions
+11 -12
View File
@@ -249,14 +249,7 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
}
if len(filter.IncludeAgentMetadata) > 0 {
err = attachAgentMetadata(wss, workspaceRows)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error converting agent metadata.",
Detail: err.Error(),
})
return
}
attachAgentMetadata(ctx, api.Logger, wss, workspaceRows)
}
httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspacesResponse{
@@ -2771,8 +2764,11 @@ func (api *API) workspaceData(ctx context.Context, workspaces []database.Workspa
// attachAgentMetadata maps the agent metadata the workspaces query
// aggregated per workspace onto the agents in the converted response.
// Each aggregated datum carries its workspace_agent_id.
func attachAgentMetadata(workspaces []codersdk.Workspace, rows []database.GetWorkspacesRow) error {
// Each aggregated datum carries its workspace_agent_id. An unparsable
// aggregate degrades to missing metadata for that workspace rather
// than failing the page: the expansion is best-effort decoration on
// top of the list.
func attachAgentMetadata(ctx context.Context, logger slog.Logger, workspaces []codersdk.Workspace, rows []database.GetWorkspacesRow) {
byAgent := map[uuid.UUID][]database.WorkspaceAgentMetadatum{}
for _, row := range rows {
if len(row.AgentMetadata) == 0 {
@@ -2781,7 +2777,11 @@ func attachAgentMetadata(workspaces []codersdk.Workspace, rows []database.GetWor
var metadata database.AgentMetadataAggregate
err := metadata.Scan(row.AgentMetadata)
if err != nil {
return xerrors.Errorf("scan agent metadata for workspace %q: %w", row.ID, err)
logger.Warn(ctx, "scan agent metadata, omitting it for the workspace",
slog.F("workspace_id", row.ID),
slog.Error(err),
)
continue
}
for _, datum := range metadata {
byAgent[datum.WorkspaceAgentID] = append(byAgent[datum.WorkspaceAgentID], datum)
@@ -2798,7 +2798,6 @@ func attachAgentMetadata(workspaces []codersdk.Workspace, rows []database.GetWor
}
}
}
return nil
}
func convertWorkspaces(