Skip to content

feat: add telemetry for task lifecycle events - #21922

Merged
johnstcn merged 17 commits into
mainfrom
cj/internal-1259
Feb 24, 2026
Merged

johnstcn merged 17 commits into
mainfrom
cj/internal-1259

Conversation

@johnstcn

@johnstcn johnstcn commented Feb 4, 2026 •

Copy link
Copy Markdown
Member

Relates to coder/internal#1259

Adds new database queries and telemetry collection functions to gather task lifecycle events (pause/resume cycles, idle time) for analytics.

Task events track pause/resume activity, idle duration before pausing, paused duration, and time from resume to first app status, filtered to recent activity based on the telemetry snapshot interval.

🤖 Created with Mux (Opus 4.6).

@johnstcn johnstcn self-assigned this Feb 4, 2026
@johnstcn johnstcn changed the title Cj/internal 1259 feat: add telemetry for task lifecycle events Feb 4, 2026
johnstcn added a commit that referenced this pull request Feb 6, 2026
…amp control (#21944)

Relates to #21922 /
coder/internal#1259

* Adds `dbfake.BuilderOption func(*WorkspaceBuildBuilder)`
* Adds `BuilderOption` methods for setting various provisioner job
related fields on `WorkspaceBuildBuilder`.
* Migrates a number of existing tests that previously dependeded on
provisioner job timing to use these updated methods in the following
packages:
  * `coderd/jobreaper`
  * `coderd/notifications/reports`
  * `enterprise/coderd/schedule`
  * `enterprise/coderd/prebuilds`
  * `scripts/workspace-runtime-audit` 

🤖 Created using Mux (Opus 4.5)

---------

Co-authored-by: Copilot <[email protected]>
@johnstcn
johnstcn marked this pull request as ready for review February 9, 2026 18:24
Copilot AI review requested due to automatic review settings February 9, 2026 18:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds richer telemetry around AI task lifecycle events (pause/resume/idle/first status timing) and wires it into the periodic telemetry snapshot reporter.

Changes:

  • Introduces CollectTasks to gather task telemetry enriched with lifecycle signals derived from workspace builds and app statuses.
  • Extends the telemetry Task payload with pause/resume timestamps, pause reason, and duration metrics.
  • Adds DB support for efficiently fetching task lifecycle workspace builds and expands tests covering many lifecycle scenarios.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
coderd/telemetry/telemetry.go Adds CollectTasks, extends Task telemetry schema, and uses it in snapshot creation.
coderd/telemetry/telemetry_test.go Adds comprehensive task telemetry test cases and fixtures.
coderd/database/queries/workspacebuilds.sql Adds SQL query to fetch pause/resume-related workspace builds by workspace IDs.
coderd/database/queries.sql.go sqlc-generated Go for GetTaskLifecycleBuildsByWorkspaceIDs.
coderd/database/querier.go Adds new query method to the sqlcQuerier interface.
coderd/database/dbmock/dbmock.go Adds mock support for GetTaskLifecycleBuildsByWorkspaceIDs.
coderd/database/dbmetrics/querymetrics.go Adds query metrics instrumentation for the new DB method.
coderd/database/dbauthz/dbauthz.go Adds authz wrapper for GetTaskLifecycleBuildsByWorkspaceIDs.
coderd/database/dbauthz/dbauthz_test.go Adds RBAC expectations test for the new DB method.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread coderd/telemetry/telemetry_test.go Resolved
Comment thread coderd/telemetry/telemetry.go Resolved
Comment thread coderd/telemetry/telemetry.go Resolved
Comment thread coderd/telemetry/telemetry.go Resolved
Comment thread coderd/telemetry/telemetry.go Resolved

@coderagents coderagents Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

Reviewed telemetry additions for task lifecycle events. The implementation adds comprehensive tracking for task pause/resume operations with metrics like idle duration, paused duration, and time to active. Found 2 important issues related to time calculations and 1 logic bug in the PausedDurationMS computation.

Summary

  • 🟡 IMPORTANT: Negative duration calculations possible in IdleDurationMS (line ~1058)
  • 🟡 IMPORTANT: PausedDurationMS logic has incorrect loop termination (line ~1083)
  • 🔵 NITPICK: TimeToActiveMS could return first non-working status instead of first working status

Detailed Findings

The database queries look well-structured with appropriate indexes. Authorization patterns follow Coder conventions correctly. Test coverage is comprehensive with good edge case handling.

Comment thread coderd/telemetry/telemetry.go Resolved
Comment thread coderd/telemetry/telemetry.go Resolved
Comment thread coderd/telemetry/telemetry.go Resolved
@johnstcn
johnstcn marked this pull request as draft February 11, 2026 17:44
@johnstcn
johnstcn force-pushed the cj/internal-1259 branch 3 times, most recently from 525c7b4 to c87ab0b Compare February 12, 2026 00:11
@coder coder deleted a comment from chatgpt-codex-connector Bot Feb 12, 2026
@johnstcn
johnstcn requested a review from Copilot February 12, 2026 09:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread coderd/telemetry/telemetry.go Resolved
Comment thread coderd/database/queries/tasks.sql Resolved
Comment thread coderd/telemetry/telemetry.go Resolved
Comment thread coderd/telemetry/telemetry.go Resolved
Comment thread coderd/telemetry/telemetry.go Resolved
Comment thread coderd/telemetry/telemetry.go Resolved
Adds new database queries and telemetry collection functions to gather
task lifecycle events (pause/resume cycles, idle time) for analytics.

Task events track pause/resume activity, idle duration before pausing,
paused duration, and time from resume to first app status, filtered to
recent activity based on the telemetry snapshot interval.
@johnstcn
johnstcn marked this pull request as ready for review February 12, 2026 16:06
@johnstcn
johnstcn requested a review from mafredri February 12, 2026 16:06
Comment thread coderd/telemetry/telemetry.go Resolved
Comment on lines +323 to +328
assert.Nil(t, taskEvent.LastResumedAt)
assert.Nil(t, taskEvent.LastPausedAt)
assert.Nil(t, taskEvent.PauseReason)
assert.Nil(t, taskEvent.IdleDurationMS)
assert.Nil(t, taskEvent.PausedDurationMS)
assert.Nil(t, taskEvent.ResumeToStatusMS)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

self-review: adding some task app statuses here may not be a bad idea.

@mafredri mafredri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice work!

Some small suggestions, little cleanup, and consolidating query/code conditionals, but otherwise this is looking good.

Comment thread coderd/database/queries/tasks.sql Resolved
Comment thread coderd/database/queries/tasks.sql Resolved
Comment thread coderd/database/queries/tasks.sql Outdated
WHERE was.app_id = twa.workspace_app_id
AND was.state = 'working'
ORDER BY was.created_at DESC
LIMIT 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: This is something of an approximation. Consider a situation where the agent reports working, goes on to do 1h of work, and then reports done. We'd be missing 1h of work from this timeframe.

We could improve this, but I'm not sure it's worth the lift. But a comment would be good IMO.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Comment added above:

-- - Idle duration approximation: If the agent reports "working", does
--   work, then reports "done", we miss that working time.

Comment thread coderd/database/queries/tasks.sql Outdated
lws.created_at AS last_working_status_at,
-- First app status after resume (for resume-to-status duration).
-- Only populated for workspaces in an active phase (started more
-- recently than stopped).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Was this in the ticket/RFC? Either way, this seems like a useful metric to add. 👍🏻

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Nope, it was just a drive-by idea I mentioned briefly in Slack 👍

Comment thread coderd/telemetry/telemetry.go Resolved
Comment thread coderd/telemetry/telemetry.go Resolved
Comment thread coderd/database/queries/tasks.sql Outdated
-- Latest start build.
start_build.created_at AS start_build_created_at,
-- Last "working" app status (for idle duration).
lws.created_at AS last_working_status_at,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This was lost in the RFC -> ticket conversion, but we also want a "active duration", i.e. how much time the task spent working.

Comment thread coderd/telemetry/telemetry.go Resolved
Comment thread coderd/telemetry/telemetry.go Resolved
Comment thread coderd/database/queries/tasks.sql Outdated
AND EXISTS (
SELECT 1 FROM workspace_builds wb
WHERE wb.workspace_id = t.workspace_id
AND wb.created_at > @created_after

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

review: now filtering by created_after

Comment thread coderd/database/queries/tasks.sql Outdated
FROM workspace_builds wb
WHERE wb.workspace_id = t.workspace_id
AND wb.transition = 'start'
AND wb.reason = 'task_resume'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we should, rather than filter on reason here, select the reason in the query and add task_resume/other in the telemetry, similar to stop.

If a user manually starts the task workspace, that's still a task resumption, but not the expected way (valuable in telemetry IMO).

Comment thread coderd/database/queries/tasks.sql Resolved
Comment thread coderd/database/queries/tasks.sql Outdated
was.created_at AS interval_start,
COALESCE(
LEAD(was.created_at) OVER (ORDER BY was.created_at ASC),
COALESCE(stop_build.created_at, @now::timestamptz)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this might need a rethink.

We actually know which build twa.workspace_app_id belongs to, we just don't select the build number from that table.

Also, a stop build (theoretically) never has was content. So these can only come from the start build (i.e. task is running). For a running task, was.created_at will always be newer than stop_build.created_at. So if we ever hit this condition, we'll get interval_start > interval_end.

By the time the task gets paused, the twa app will have changed and we will not get any data attached.

So we get a pause event, but no indication how long the task was active.

One way we could improve the data and make it more explicit is to select only the start_builds app ID from twa, and use that in our joins. That would always give us the relevant data.

Bool: true,
Valid: true,
},
})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We're missing UpsertTaskWorkspaceApp, which might be why the test isn't capturing what I raised on the query.

(For start, we need to sett the build/agent/app, for stop just the build).

Comment on lines +125 to +140
task_status_timeline AS (
-- All app statusese across every historical app for each task,
-- plus synthectic obundary rows at each stop/start build transition.
-- Boundaries prevent LEAD() from spanning pause gaps and let us
-- ignore post-resume statuses.
SELECT tai.task_id, was.created_at, was.state::text AS state
FROM workspace_app_statuses was
JOIN task_app_ids tai ON tai.workspace_app_id = was.app_id
UNION ALL
SELECT t.id AS task_id, wb.created_at, '_boundary' AS state
FROM tasks t
JOIN workspace_builds wb ON wb.workspace_id = t.workspace_id
WHERE t.deleted_at IS NULL
AND t.workspace_id IS NOT NULL
AND wb.build_number > 1
),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

review: extracted this into a separate CTE so we can also use this for last_working_status.

Comment on lines +959 to +966
// TODO(cian): We define IdleDurationMS as "the time from the last working status to pause".
// We know that the task has reported working since T-6h and got auto-paused at T-50m.
// We can reasonably assume that it has been 'idle' from when it was stopped (T-30m) to
// its next report at T-25m. This is covered by ResumeToStatusMS.
// But do we consider the time since its last report (T-6h) to its being auto-paused
// as truly "idle"?
idleDurationMS: ptr.Ref(310 * time.Minute.Milliseconds()),
activeDurationMS: ptr.Ref((5*time.Hour + 10*time.Minute + 25*time.Minute).Milliseconds()),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Review: AFAICT the test as written is "correct" as per the definitions laid out in the OG issue. I had some doubts about the definition but my brain may have encountered E_INSUFFICIENT_TOKENS.

@mafredri mafredri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Didn't see any major issues here, nice work on the query and test coverage!

I did call out an edge case that I'd recommend addressing just to avoid any funky data (a test-case to reproduce it might be good to see what happens).

Other than that, minor nits that I don't need to re-review.

Comment thread coderd/database/sqlc.yaml Resolved
JOIN workspace_builds wb ON wb.workspace_id = t.workspace_id
WHERE t.deleted_at IS NULL
AND t.workspace_id IS NOT NULL
AND wb.build_number > 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Pseudo: AND was.created_at <= MAX(wb.created_at)?

This would be relevant for the LEAD / OVER.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It may be easier to filter down in the lws lateral join.

Comment thread coderd/database/queries/tasks.sql Resolved
Comment thread coderd/database/queries/tasks.sql Resolved
Comment thread coderd/database/queries/tasks.sql Resolved
Comment thread coderd/telemetry/telemetry_test.go Resolved
Comment thread coderd/telemetry/telemetry_test.go Resolved
@johnstcn
johnstcn merged commit 6336fee into main Feb 24, 2026
27 checks passed
@johnstcn
johnstcn deleted the cj/internal-1259 branch February 24, 2026 17:04
@github-actions github-actions Bot locked and limited conversation to collaborators Feb 24, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants