feat: report user secrets adoption summary in telemetry (#24854)

Add a deployment-wide user secrets summary to the telemetry snapshot so
we can track adoption of user secrets
The summary reports:

- A breakdown of secrets by which injection fields are populated:
EnvNameOnly, FilePathOnly, Both, Neither
- The distribution of secrets per user (max, p25, p50, p75, p90)

All metrics are scoped to active non-system users. Soft-deleted users
are excluded. The percentile distribution is computed across the entire
active non-system user base, including users with zero secrets, so the
percentiles reflect deployment-wide adoption.

Assisted by Coder Agents.
This commit is contained in:
Zach
2026-05-05 10:56:39 -06:00
committed by GitHub
parent e189f73cc0
commit 1b2a1af097
13 changed files with 575 additions and 1 deletions
+57
View File
@@ -65,3 +65,60 @@ RETURNING *;
DELETE FROM user_secrets
WHERE user_id = @user_id AND name = @name
RETURNING *;
-- name: GetUserSecretsTelemetrySummary :one
-- Returns deployment-wide aggregates for the telemetry snapshot.
--
-- The denominator for both user-level counts and the per-user
-- distribution is active non-system users. Specifically:
--
-- * deleted = false: Coder soft-deletes by flipping users.deleted
-- rather than removing rows, so secrets persist after delete but
-- are unreachable.
-- * status = 'active': dormant users (no recent activity) and
-- suspended users (explicitly disabled) cannot use secrets, so
-- they shouldn't dilute the percentile distribution as
-- zero-secret entries.
-- * is_system = false: internal subjects like the prebuilds user
-- never use secrets in the normal flow.
--
-- Status transitions move users in and out of this denominator, so a
-- snapshot's UsersWithSecrets can drop without any secret being
-- deleted.
--
-- The percentile distribution is computed across all active non-system
-- users, including those with zero secrets, so the percentiles reflect
-- deployment-wide adoption rather than only the power-user subset.
-- percentile_disc returns an actual integer count from the underlying
-- values rather than interpolating between rows.
WITH active_users AS (
SELECT id AS user_id
FROM users
WHERE deleted = false
AND is_system = false
AND status = 'active'::user_status
),
per_user AS (
SELECT au.user_id, COUNT(us.id)::bigint AS n
FROM active_users au
LEFT JOIN user_secrets us ON us.user_id = au.user_id
GROUP BY au.user_id
),
secrets_filtered AS (
SELECT us.env_name, us.file_path
FROM user_secrets us
JOIN active_users au ON au.user_id = us.user_id
)
SELECT
COUNT(*) FILTER (WHERE n > 0)::bigint AS users_with_secrets,
(SELECT COUNT(*) FROM secrets_filtered)::bigint AS total_secrets,
(SELECT COUNT(*) FROM secrets_filtered WHERE env_name != '' AND file_path = '' )::bigint AS env_name_only,
(SELECT COUNT(*) FROM secrets_filtered WHERE env_name = '' AND file_path != '')::bigint AS file_path_only,
(SELECT COUNT(*) FROM secrets_filtered WHERE env_name != '' AND file_path != '')::bigint AS both,
(SELECT COUNT(*) FROM secrets_filtered WHERE env_name = '' AND file_path = '' )::bigint AS neither,
COALESCE(MAX(n), 0)::bigint AS secrets_per_user_max,
COALESCE(percentile_disc(0.25) WITHIN GROUP (ORDER BY n), 0)::bigint AS secrets_per_user_p25,
COALESCE(percentile_disc(0.50) WITHIN GROUP (ORDER BY n), 0)::bigint AS secrets_per_user_p50,
COALESCE(percentile_disc(0.75) WITHIN GROUP (ORDER BY n), 0)::bigint AS secrets_per_user_p75,
COALESCE(percentile_disc(0.90) WITHIN GROUP (ORDER BY n), 0)::bigint AS secrets_per_user_p90
FROM per_user;