Skip to content

Commit d384842

Browse files
committed
feat(coderd/database): rewrite GetTemplateAppInsights to use template_usage_stats
1 parent 2eae8f8 commit d384842

File tree

4 files changed

+186
-114
lines changed

4 files changed

+186
-114
lines changed

coderd/database/dbmem/dbmem.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -2988,8 +2988,8 @@ func (q *FakeQuerier) GetTemplateAppInsights(ctx context.Context, arg database.G
29882988
ActiveUserIDs: activeUserIDs,
29892989
AccessMethod: appKey.AccessMethod,
29902990
SlugOrPort: appKey.SlugOrPort,
2991-
DisplayName: sql.NullString{String: appKey.DisplayName, Valid: appKey.DisplayName != ""},
2992-
Icon: sql.NullString{String: appKey.Icon, Valid: appKey.Icon != ""},
2991+
DisplayName: appKey.DisplayName,
2992+
Icon: appKey.Icon,
29932993
IsApp: appKey.Slug != "",
29942994
UsageSeconds: usage,
29952995
})

coderd/database/queries.sql.go

+94-58
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/insights.sql

+84-48
Original file line numberDiff line numberDiff line change
@@ -182,57 +182,93 @@ GROUP BY template_id;
182182
-- GetTemplateAppInsights returns the aggregate usage of each app in a given
183183
-- timeframe. The result can be filtered on template_ids, meaning only user data
184184
-- from workspaces based on those templates will be included.
185-
WITH app_stats_by_user_and_agent AS (
186-
SELECT
187-
s.start_time,
188-
60 as seconds,
189-
w.template_id,
190-
was.user_id,
191-
was.agent_id,
192-
was.access_method,
193-
was.slug_or_port,
194-
wa.display_name,
195-
wa.icon,
196-
(wa.slug IS NOT NULL)::boolean AS is_app
197-
FROM workspace_app_stats was
198-
JOIN workspaces w ON (
199-
w.id = was.workspace_id
200-
AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN w.template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
201-
)
202-
-- We do a left join here because we want to include user IDs that have used
203-
-- e.g. ports when counting active users.
204-
LEFT JOIN workspace_apps wa ON (
205-
wa.agent_id = was.agent_id
206-
AND wa.slug = was.slug_or_port
185+
WITH
186+
app_insights AS (
187+
SELECT
188+
tus.user_id,
189+
array_agg(DISTINCT tus.template_id)::uuid[] AS template_ids,
190+
app_usage.key::text AS app_name,
191+
COALESCE(wa.display_name, '') AS display_name,
192+
COALESCE(wa.icon, '') AS icon,
193+
(wa.slug IS NOT NULL)::boolean AS is_app,
194+
LEAST(SUM(app_usage.value::int), 30) AS app_usage_mins
195+
FROM
196+
template_usage_stats AS tus, jsonb_each(app_usage_mins) AS app_usage
197+
LEFT JOIN LATERAL (
198+
-- Fetch the latest app info for each app based on slug and template.
199+
SELECT
200+
app.display_name,
201+
app.icon,
202+
app.slug
203+
FROM
204+
workspace_apps AS app
205+
JOIN
206+
workspace_agents AS agent
207+
ON
208+
agent.id = app.agent_id
209+
JOIN
210+
workspace_resources AS resource
211+
ON
212+
resource.id = agent.resource_id
213+
JOIN
214+
workspace_builds AS build
215+
ON
216+
build.job_id = resource.job_id
217+
JOIN
218+
workspaces AS workspace
219+
ON
220+
workspace.id = build.workspace_id
221+
WHERE
222+
-- Requires lateral join.
223+
app.slug = app_usage.key
224+
AND workspace.owner_id = tus.user_id
225+
AND workspace.template_id = tus.template_id
226+
ORDER BY
227+
app.created_at DESC
228+
LIMIT 1
229+
) AS wa
230+
ON
231+
true
232+
WHERE
233+
tus.start_time >= @start_time::timestamptz
234+
AND tus.end_time <= @end_time::timestamptz
235+
AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN tus.template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
236+
GROUP BY
237+
tus.start_time, tus.user_id, app_usage.key::text, wa.display_name, wa.icon, wa.slug
238+
),
239+
templates AS (
240+
SELECT
241+
app_name,
242+
display_name,
243+
icon,
244+
is_app,
245+
array_agg(DISTINCT template_id)::uuid[] AS template_ids
246+
FROM
247+
app_insights, unnest(template_ids) AS template_id
248+
GROUP BY
249+
app_name, display_name, icon, is_app
207250
)
208-
-- This table contains both 1 minute entries and >1 minute entries,
209-
-- to calculate this with our uniqueness constraints, we generate series
210-
-- for the longer intervals.
211-
CROSS JOIN LATERAL generate_series(
212-
date_trunc('minute', was.session_started_at),
213-
-- Subtract 1 microsecond to avoid creating an extra series.
214-
date_trunc('minute', was.session_ended_at - '1 microsecond'::interval),
215-
'1 minute'::interval
216-
) s(start_time)
217-
WHERE
218-
s.start_time >= @start_time::timestamptz
219-
-- Subtract one minute because the series only contains the start time.
220-
AND s.start_time < (@end_time::timestamptz) - '1 minute'::interval
221-
GROUP BY s.start_time, w.template_id, was.user_id, was.agent_id, was.access_method, was.slug_or_port, wa.display_name, wa.icon, wa.slug
222-
)
223251

224252
SELECT
225-
array_agg(DISTINCT template_id)::uuid[] AS template_ids,
226-
-- Return IDs so we can combine this with GetTemplateInsights.
227-
array_agg(DISTINCT user_id)::uuid[] AS active_user_ids,
228-
access_method,
229-
slug_or_port,
230-
display_name,
231-
icon,
232-
is_app,
233-
SUM(seconds) AS usage_seconds
234-
FROM app_stats_by_user_and_agent
235-
GROUP BY access_method, slug_or_port, display_name, icon, is_app;
253+
t.template_ids,
254+
array_agg(DISTINCT ai.user_id)::uuid[] AS active_user_ids,
255+
''::text AS access_method, -- TODO(mafredri): Remove.
256+
ai.app_name AS slug_or_port,
257+
ai.display_name,
258+
ai.icon,
259+
ai.is_app,
260+
(SUM(ai.app_usage_mins) * 60)::bigint AS usage_seconds
261+
FROM
262+
app_insights AS ai
263+
JOIN
264+
templates AS t
265+
ON
266+
ai.app_name = t.app_name
267+
AND ai.display_name = t.display_name
268+
AND ai.icon = t.icon
269+
AND ai.is_app = t.is_app
270+
GROUP BY
271+
t.template_ids, ai.app_name, ai.display_name, ai.icon, ai.is_app;
236272

237273
-- name: GetTemplateAppInsightsByTemplate :many
238274
WITH app_stats_by_user_and_agent AS (

coderd/insights.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -474,11 +474,11 @@ func convertTemplateInsightsApps(usage database.GetTemplateInsightsRow, appUsage
474474
if a.SlugOrPort != b.SlugOrPort {
475475
return strings.Compare(a.SlugOrPort, b.SlugOrPort)
476476
}
477-
if a.DisplayName.String != b.DisplayName.String {
478-
return strings.Compare(a.DisplayName.String, b.DisplayName.String)
477+
if a.DisplayName != b.DisplayName {
478+
return strings.Compare(a.DisplayName, b.DisplayName)
479479
}
480-
if a.Icon.String != b.Icon.String {
481-
return strings.Compare(a.Icon.String, b.Icon.String)
480+
if a.Icon != b.Icon {
481+
return strings.Compare(a.Icon, b.Icon)
482482
}
483483
if !a.IsApp && b.IsApp {
484484
return -1
@@ -496,9 +496,9 @@ func convertTemplateInsightsApps(usage database.GetTemplateInsightsRow, appUsage
496496
apps = append(apps, codersdk.TemplateAppUsage{
497497
TemplateIDs: app.TemplateIDs,
498498
Type: codersdk.TemplateAppsTypeApp,
499-
DisplayName: app.DisplayName.String,
499+
DisplayName: app.DisplayName,
500500
Slug: app.SlugOrPort,
501-
Icon: app.Icon.String,
501+
Icon: app.Icon,
502502
Seconds: app.UsageSeconds,
503503
})
504504
}

0 commit comments

Comments
 (0)