@@ -294,56 +294,51 @@ GROUP BY
294
294
t .template_ids ,ai .app_name ,ai .display_name ,ai .icon ,ai .is_app ;
295
295
296
296
-- name: GetTemplateAppInsightsByTemplate :many
297
- SELECT
298
- tus .template_id ,
299
- COUNT (DISTINCTtus .user_id )AS active_users,
300
- app_usage .key ::text AS slug_or_port,
301
- COALESCE(wa .display_name ,' ' )AS display_name,
302
- (SUM (app_usage .value ::int )* 60 )::bigint AS usage_seconds
303
- FROM
304
- template_usage_statsAS tus, jsonb_each(app_usage_mins)AS app_usage
305
- LEFT JOIN LATERAL (
306
- -- The joins in this query are necessary to associate an app with a
307
- -- template, we use this to get the app metadata like display name
308
- -- and icon.
297
+ WITH app_stats_by_user_and_agentAS (
309
298
SELECT
310
- app .display_name ,
311
- app .slug
312
- FROM
313
- workspace_appsAS app
314
- JOIN
315
- workspace_agentsAS agent
316
- ON
317
- agent .id = app .agent_id
318
- JOIN
319
- workspace_resourcesAS resource
320
- ON
321
- resource .id = agent .resource_id
322
- JOIN
323
- workspace_buildsAS build
324
- ON
325
- build .job_id = resource .job_id
326
- JOIN
327
- workspacesAS workspace
328
- ON
329
- workspace .id = build .workspace_id
299
+ s .start_time ,
300
+ 60 as seconds,
301
+ w .template_id ,
302
+ was .user_id ,
303
+ was .agent_id ,
304
+ was .slug_or_port ,
305
+ wa .display_name ,
306
+ (wa .slug IS NOT NULL )::boolean AS is_app
307
+ FROM workspace_app_stats was
308
+ JOIN workspaces wON (
309
+ w .id = was .workspace_id
310
+ )
311
+ -- We do a left join here because we want to include user IDs that have used
312
+ -- e.g. ports when counting active users.
313
+ LEFT JOIN workspace_apps waON (
314
+ wa .agent_id = was .agent_id
315
+ AND wa .slug = was .slug_or_port
316
+ )
317
+ -- This table contains both 1 minute entries and >1 minute entries,
318
+ -- to calculate this with our uniqueness constraints, we generate series
319
+ -- for the longer intervals.
320
+ CROSS JOIN LATERAL generate_series(
321
+ date_trunc(' minute' ,was .session_started_at ),
322
+ -- Subtract 1 microsecond to avoid creating an extra series.
323
+ date_trunc(' minute' ,was .session_ended_at - ' 1 microsecond' ::interval),
324
+ ' 1 minute' ::interval
325
+ ) s(start_time)
330
326
WHERE
331
- -- Requires lateral join.
332
- app .slug = app_usage .key
333
- AND workspace .owner_id = tus .user_id
334
- AND workspace .template_id = tus .template_id
335
- ORDER BY
336
- app .created_at DESC
337
- LIMIT 1
338
- ) wa
339
- ON
340
- true
341
- WHERE
342
- tus .start_time >= @start_time::timestamptz
343
- AND tus .end_time <= @end_time::timestamptz
344
- AND wa .slug IS NOT NULL -- Check is_app.
345
- GROUP BY
346
- tus .template_id ,app_usage .key ::text ,wa .display_name ;
327
+ s .start_time >= @start_time::timestamptz
328
+ -- Subtract one minute because the series only contains the start time.
329
+ AND s .start_time < (@end_time::timestamptz )- ' 1 minute' ::interval
330
+ GROUP BY s .start_time ,w .template_id ,was .user_id ,was .agent_id ,was .slug_or_port ,wa .display_name ,wa .slug
331
+ )
332
+
333
+ SELECT
334
+ template_id,
335
+ display_name,
336
+ slug_or_port,
337
+ COALESCE(COUNT (DISTINCT user_id))::bigint AS active_users,
338
+ SUM (seconds)AS usage_seconds
339
+ FROM app_stats_by_user_and_agent
340
+ WHERE is_app IS TRUE
341
+ GROUP BY template_id, display_name, slug_or_port;
347
342
348
343
-- name: GetTemplateInsightsByInterval :many
349
344
-- GetTemplateInsightsByInterval returns all intervals between start and end