@@ -2986,68 +2986,93 @@ func (q *FakeQuerier) GetTemplateAppInsights(ctx context.Context, arg database.G
2986
2986
*/
2987
2987
2988
2988
/*
2989
- app_insights AS (
2989
+ -- Create a list of all unique apps by template, this is used to
2990
+ -- filter out irrelevant template usage stats.
2991
+ apps AS (
2992
+ SELECT DISTINCT ON (ws.template_id, app.slug)
2993
+ ws.template_id,
2994
+ app.slug,
2995
+ app.display_name,
2996
+ app.icon
2997
+ FROM
2998
+ workspaces ws
2999
+ JOIN
3000
+ workspace_builds AS build
3001
+ ON
3002
+ build.workspace_id = ws.id
3003
+ JOIN
3004
+ workspace_resources AS resource
3005
+ ON
3006
+ resource.job_id = build.job_id
3007
+ JOIN
3008
+ workspace_agents AS agent
3009
+ ON
3010
+ agent.resource_id = resource.id
3011
+ JOIN
3012
+ workspace_apps AS app
3013
+ ON
3014
+ app.agent_id = agent.id
3015
+ WHERE
3016
+ -- Partial query parameter filter.
3017
+ CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN ws.template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
3018
+ ORDER BY
3019
+ ws.template_id, app.slug, app.created_at DESC
3020
+ ),
3021
+ -- Join apps and template usage stats to filter out irrelevant rows.
3022
+ -- Note that this way of joining will eliminate all data-points that
3023
+ -- aren't for "real" apps. That means ports are ignored (even though
3024
+ -- they're part of the dataset), as well as are "[terminal]" entries
3025
+ -- which are alternate datapoints for reconnecting pty usage.
3026
+ template_usage_stats_with_apps AS (
2990
3027
SELECT
3028
+ tus.start_time,
3029
+ tus.template_id,
2991
3030
tus.user_id,
2992
- array_agg(DISTINCT tus.template_id)::uuid[] AS template_ids,
2993
- app_usage.key::text AS app_name,
2994
- COALESCE(wa.display_name, '') AS display_name,
2995
- COALESCE(wa.icon, '') AS icon,
2996
- (wa.slug IS NOT NULL)::boolean AS is_app,
2997
- LEAST(SUM(app_usage.value::int), 30) AS app_usage_mins
3031
+ apps.slug,
3032
+ apps.display_name,
3033
+ apps.icon,
3034
+ tus.app_usage_mins
2998
3035
FROM
2999
- template_usage_stats AS tus, jsonb_each(app_usage_mins) AS app_usage
3000
- LEFT JOIN LATERAL (
3001
- -- Fetch the latest app info for each app based on slug and template.
3002
- SELECT
3003
- app.display_name,
3004
- app.icon,
3005
- app.slug
3006
- FROM
3007
- workspace_apps AS app
3008
- JOIN
3009
- workspace_agents AS agent
3010
- ON
3011
- agent.id = app.agent_id
3012
- JOIN
3013
- workspace_resources AS resource
3014
- ON
3015
- resource.id = agent.resource_id
3016
- JOIN
3017
- workspace_builds AS build
3018
- ON
3019
- build.job_id = resource.job_id
3020
- JOIN
3021
- workspaces AS workspace
3022
- ON
3023
- workspace.id = build.workspace_id
3024
- WHERE
3025
- -- Requires lateral join.
3026
- app.slug = app_usage.key
3027
- AND workspace.owner_id = tus.user_id
3028
- AND workspace.template_id = tus.template_id
3029
- ORDER BY
3030
- app.created_at DESC
3031
- LIMIT 1
3032
- ) AS wa
3036
+ apps
3037
+ JOIN
3038
+ template_usage_stats AS tus
3033
3039
ON
3034
- true
3035
- WHERE
3040
+ -- Query parameter filter.
3036
3041
tus.start_time >= @start_time::timestamptz
3037
3042
AND tus.end_time <= @end_time::timestamptz
3038
3043
AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN tus.template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
3044
+ -- Primary join condition.
3045
+ AND tus.template_id = apps.template_id
3046
+ AND apps.slug IN (SELECT jsonb_object_keys(tus.app_usage_mins))
3047
+ ),
3048
+ -- Group the app insights by interval, user and unique app. This
3049
+ -- allows us to deduplicate a user using the same app across
3050
+ -- multiple templates.
3051
+ app_insights AS (
3052
+ SELECT
3053
+ user_id,
3054
+ slug,
3055
+ display_name,
3056
+ icon,
3057
+ -- See motivation in GetTemplateInsights for LEAST(SUM(n), 30).
3058
+ LEAST(SUM(app_usage.value::smallint), 30) AS usage_mins
3059
+ FROM
3060
+ template_usage_stats_with_apps, jsonb_each(app_usage_mins) AS app_usage
3061
+ WHERE
3062
+ app_usage.key = slug
3039
3063
GROUP BY
3040
- tus. start_time, tus. user_id, app_usage.key::text, wa. display_name, wa. icon, wa.slug
3064
+ start_time, user_id, slug, display_name, icon
3041
3065
),
3042
3066
*/
3043
3067
3068
+ // Due to query optimizations, this logic is somewhat inverted from
3069
+ // the above query.
3044
3070
type appInsightsGroupBy struct {
3045
3071
StartTime time.Time
3046
3072
UserID uuid.UUID
3047
- AppName string
3073
+ Slug string
3048
3074
DisplayName string
3049
3075
Icon string
3050
- IsApp bool
3051
3076
}
3052
3077
type appInsightsRow struct {
3053
3078
appInsightsGroupBy
@@ -3066,18 +3091,20 @@ func (q *FakeQuerier) GetTemplateAppInsights(ctx context.Context, arg database.G
3066
3091
}
3067
3092
3068
3093
// json_each
3069
- for appName , appUsage := range stat .AppUsageMins {
3070
- // LEFT JOIN LATERAL
3071
- app , _ := q .getLatestWorkspaceAppByTemplateIDUserIDSlugNoLock (ctx , stat .TemplateID , stat .UserID , appName )
3094
+ for slug , appUsage := range stat .AppUsageMins {
3095
+ // FROM apps JOIN template_usage_stats
3096
+ app , _ := q .getLatestWorkspaceAppByTemplateIDUserIDSlugNoLock (ctx , stat .TemplateID , stat .UserID , slug )
3097
+ if app .Slug == "" {
3098
+ continue
3099
+ }
3072
3100
3073
3101
// SELECT
3074
3102
key := appInsightsGroupBy {
3075
3103
StartTime : stat .StartTime ,
3076
3104
UserID : stat .UserID ,
3077
- AppName : appName ,
3105
+ Slug : slug ,
3078
3106
DisplayName : app .DisplayName ,
3079
3107
Icon : app .Icon ,
3080
- IsApp : app .Slug != "" ,
3081
3108
}
3082
3109
row , ok := appInsightRows [key ]
3083
3110
if ! ok {
@@ -3092,25 +3119,26 @@ func (q *FakeQuerier) GetTemplateAppInsights(ctx context.Context, arg database.G
3092
3119
}
3093
3120
3094
3121
/*
3122
+ -- Even though we allow identical apps to be aggregated across
3123
+ -- templates, we still want to be able to report which templates
3124
+ -- the data comes from.
3095
3125
templates AS (
3096
3126
SELECT
3097
- app_name ,
3127
+ slug ,
3098
3128
display_name,
3099
3129
icon,
3100
- is_app,
3101
3130
array_agg(DISTINCT template_id)::uuid[] AS template_ids
3102
3131
FROM
3103
- app_insights, unnest(template_ids) AS template_id
3132
+ template_usage_stats_with_apps
3104
3133
GROUP BY
3105
- app_name , display_name, icon, is_app
3134
+ slug , display_name, icon
3106
3135
)
3107
3136
*/
3108
3137
3109
3138
type appGroupBy struct {
3110
- AppName string
3139
+ Slug string
3111
3140
DisplayName string
3112
3141
Icon string
3113
- IsApp bool
3114
3142
}
3115
3143
type templateRow struct {
3116
3144
appGroupBy
@@ -3120,10 +3148,9 @@ func (q *FakeQuerier) GetTemplateAppInsights(ctx context.Context, arg database.G
3120
3148
templateRows := make (map [appGroupBy ]templateRow )
3121
3149
for _ , aiRow := range appInsightRows {
3122
3150
key := appGroupBy {
3123
- AppName : aiRow .AppName ,
3151
+ Slug : aiRow .Slug ,
3124
3152
DisplayName : aiRow .DisplayName ,
3125
3153
Icon : aiRow .Icon ,
3126
- IsApp : aiRow .IsApp ,
3127
3154
}
3128
3155
row , ok := templateRows [key ]
3129
3156
if ! ok {
@@ -3138,23 +3165,21 @@ func (q *FakeQuerier) GetTemplateAppInsights(ctx context.Context, arg database.G
3138
3165
/*
3139
3166
SELECT
3140
3167
t.template_ids,
3141
- array_agg (DISTINCT ai.user_id)::uuid[] AS active_user_ids ,
3142
- ai.app_name AS slug_or_port ,
3168
+ COUNT (DISTINCT ai.user_id) AS active_users ,
3169
+ ai.slug ,
3143
3170
ai.display_name,
3144
3171
ai.icon,
3145
- ai.is_app,
3146
- (SUM(ai.app_usage_mins) * 60)::bigint AS usage_seconds
3172
+ (SUM(ai.usage_mins) * 60)::bigint AS usage_seconds
3147
3173
FROM
3148
3174
app_insights AS ai
3149
3175
JOIN
3150
3176
templates AS t
3151
3177
ON
3152
- ai.app_name = t.app_name
3153
- AND ai.display_name = t.display_name
3154
- AND ai.icon = t.icon
3155
- AND ai.is_app = t.is_app
3178
+ t.slug = ai.slug
3179
+ AND t.display_name = ai.display_name
3180
+ AND t.icon = ai.icon
3156
3181
GROUP BY
3157
- t.template_ids, ai.app_name , ai.display_name, ai.icon, ai.is_app ;
3182
+ t.template_ids, ai.slug , ai.display_name, ai.icon;
3158
3183
*/
3159
3184
3160
3185
type templateAppInsightsRow struct {
@@ -3165,10 +3190,9 @@ func (q *FakeQuerier) GetTemplateAppInsights(ctx context.Context, arg database.G
3165
3190
groupedRows := make (map [appGroupBy ]templateAppInsightsRow )
3166
3191
for _ , aiRow := range appInsightRows {
3167
3192
key := appGroupBy {
3168
- AppName : aiRow .AppName ,
3193
+ Slug : aiRow .Slug ,
3169
3194
DisplayName : aiRow .DisplayName ,
3170
3195
Icon : aiRow .Icon ,
3171
- IsApp : aiRow .IsApp ,
3172
3196
}
3173
3197
row := groupedRows [key ]
3174
3198
row .ActiveUserIDs = append (row .ActiveUserIDs , aiRow .UserID )
@@ -3181,10 +3205,9 @@ func (q *FakeQuerier) GetTemplateAppInsights(ctx context.Context, arg database.G
3181
3205
rows = append (rows , database.GetTemplateAppInsightsRow {
3182
3206
TemplateIDs : templateRows [key ].TemplateIDs ,
3183
3207
ActiveUsers : int64 (len (uniqueSortedUUIDs (gr .ActiveUserIDs ))),
3184
- SlugOrPort : key .AppName ,
3208
+ Slug : key .Slug ,
3185
3209
DisplayName : key .DisplayName ,
3186
3210
Icon : key .Icon ,
3187
- IsApp : key .IsApp ,
3188
3211
UsageSeconds : gr .UsageSeconds ,
3189
3212
})
3190
3213
}
0 commit comments