@@ -799,6 +799,91 @@ func least[T constraints.Ordered](a, b T) T {
799
799
return b
800
800
}
801
801
802
+ func (q * FakeQuerier ) getLatestWorkspaceAppByTemplateIDUserIDSlugNoLock (ctx context.Context , templateID , userID uuid.UUID , slug string ) (database.WorkspaceApp , error ) {
803
+ /*
804
+ SELECT
805
+ app.display_name,
806
+ app.icon,
807
+ app.slug
808
+ FROM
809
+ workspace_apps AS app
810
+ JOIN
811
+ workspace_agents AS agent
812
+ ON
813
+ agent.id = app.agent_id
814
+ JOIN
815
+ workspace_resources AS resource
816
+ ON
817
+ resource.id = agent.resource_id
818
+ JOIN
819
+ workspace_builds AS build
820
+ ON
821
+ build.job_id = resource.job_id
822
+ JOIN
823
+ workspaces AS workspace
824
+ ON
825
+ workspace.id = build.workspace_id
826
+ WHERE
827
+ -- Requires lateral join.
828
+ app.slug = app_usage.key
829
+ AND workspace.owner_id = tus.user_id
830
+ AND workspace.template_id = tus.template_id
831
+ ORDER BY
832
+ app.created_at DESC
833
+ LIMIT 1
834
+ */
835
+
836
+ var workspaces []database.Workspace
837
+ for _ , w := range q .workspaces {
838
+ if w .TemplateID != templateID || w .OwnerID != userID {
839
+ continue
840
+ }
841
+ workspaces = append (workspaces , w )
842
+ }
843
+ slices .SortFunc (workspaces , func (a , b database.Workspace ) int {
844
+ if a .CreatedAt .Before (b .CreatedAt ) {
845
+ return 1
846
+ } else if a .CreatedAt .Equal (b .CreatedAt ) {
847
+ return 0
848
+ }
849
+ return - 1
850
+ })
851
+
852
+ for _ , workspace := range workspaces {
853
+ build , err := q .getLatestWorkspaceBuildByWorkspaceIDNoLock (ctx , workspace .ID )
854
+ if err != nil {
855
+ continue
856
+ }
857
+
858
+ resources , err := q .getWorkspaceResourcesByJobIDNoLock (ctx , build .JobID )
859
+ if err != nil {
860
+ continue
861
+ }
862
+ var resourceIDs []uuid.UUID
863
+ for _ , resource := range resources {
864
+ resourceIDs = append (resourceIDs , resource .ID )
865
+ }
866
+
867
+ agents , err := q .getWorkspaceAgentsByResourceIDsNoLock (ctx , resourceIDs )
868
+ if err != nil {
869
+ continue
870
+ }
871
+
872
+ for _ , agent := range agents {
873
+ app , err := q .getWorkspaceAppByAgentIDAndSlugNoLock (ctx , database.GetWorkspaceAppByAgentIDAndSlugParams {
874
+ AgentID : agent .ID ,
875
+ Slug : slug ,
876
+ })
877
+ if err != nil {
878
+ continue
879
+ }
880
+ return app , nil
881
+ }
882
+ }
883
+
884
+ return database.WorkspaceApp {}, sql .ErrNoRows
885
+ }
886
+
802
887
func (* FakeQuerier ) AcquireLock (_ context.Context , _ int64 ) error {
803
888
return xerrors .New ("AcquireLock must only be called within a transaction" )
804
889
}
@@ -2896,119 +2981,214 @@ func (q *FakeQuerier) GetTemplateAppInsights(ctx context.Context, arg database.G
2896
2981
q .mutex .RLock ()
2897
2982
defer q .mutex .RUnlock ()
2898
2983
2899
- type appKey struct {
2900
- AccessMethod string
2901
- SlugOrPort string
2902
- Slug string
2903
- DisplayName string
2904
- Icon string
2984
+ /*
2985
+ WITH
2986
+ */
2987
+
2988
+ /*
2989
+ app_insights AS (
2990
+ SELECT
2991
+ 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
2998
+ 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
3033
+ ON
3034
+ true
3035
+ WHERE
3036
+ tus.start_time >= @start_time::timestamptz
3037
+ AND tus.end_time <= @end_time::timestamptz
3038
+ AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN tus.template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
3039
+ GROUP BY
3040
+ tus.start_time, tus.user_id, app_usage.key::text, wa.display_name, wa.icon, wa.slug
3041
+ ),
3042
+ */
3043
+
3044
+ type appInsightsGroupBy struct {
3045
+ StartTime time.Time
3046
+ UserID uuid.UUID
3047
+ AppName string
3048
+ DisplayName string
3049
+ Icon string
3050
+ IsApp bool
2905
3051
}
2906
- type uniqueKey struct {
2907
- TemplateID uuid.UUID
2908
- UserID uuid.UUID
2909
- AgentID uuid.UUID
2910
- AppKey appKey
3052
+ type appInsightsRow struct {
3053
+ appInsightsGroupBy
3054
+ TemplateIDs []uuid.UUID
3055
+ AppUsageMins int64
2911
3056
}
2912
-
2913
- appUsageIntervalsByUserAgentApp := make (map [uniqueKey ]map [time.Time ]int64 )
2914
- for _ , s := range q .workspaceAppStats {
2915
- // (was.session_started_at >= ts.from_ AND was.session_started_at < ts.to_)
2916
- // OR (was.session_ended_at > ts.from_ AND was.session_ended_at < ts.to_)
2917
- // OR (was.session_started_at < ts.from_ AND was.session_ended_at >= ts.to_)
2918
- if ! (((s .SessionStartedAt .After (arg .StartTime ) || s .SessionStartedAt .Equal (arg .StartTime )) && s .SessionStartedAt .Before (arg .EndTime )) ||
2919
- (s .SessionEndedAt .After (arg .StartTime ) && s .SessionEndedAt .Before (arg .EndTime )) ||
2920
- (s .SessionStartedAt .Before (arg .StartTime ) && (s .SessionEndedAt .After (arg .EndTime ) || s .SessionEndedAt .Equal (arg .EndTime )))) {
3057
+ appInsightRows := make (map [appInsightsGroupBy ]appInsightsRow )
3058
+ // FROM
3059
+ for _ , stat := range q .templateUsageStats {
3060
+ // WHERE
3061
+ if stat .StartTime .Before (arg .StartTime ) || stat .StartTime .After (arg .EndTime ) {
2921
3062
continue
2922
3063
}
2923
-
2924
- w , err := q .getWorkspaceByIDNoLock (ctx , s .WorkspaceID )
2925
- if err != nil {
2926
- return nil , err
2927
- }
2928
-
2929
- if len (arg .TemplateIDs ) > 0 && ! slices .Contains (arg .TemplateIDs , w .TemplateID ) {
3064
+ if len (arg .TemplateIDs ) > 0 && ! slices .Contains (arg .TemplateIDs , stat .TemplateID ) {
2930
3065
continue
2931
3066
}
2932
3067
2933
- app , _ := q . getWorkspaceAppByAgentIDAndSlugNoLock ( ctx , database. GetWorkspaceAppByAgentIDAndSlugParams {
2934
- AgentID : s . AgentID ,
2935
- Slug : s . SlugOrPort ,
2936
- } )
3068
+ // json_each
3069
+ for appName , appUsage := range stat . AppUsageMins {
3070
+ // LEFT JOIN LATERAL
3071
+ app , _ := q . getLatestWorkspaceAppByTemplateIDUserIDSlugNoLock ( ctx , stat . TemplateID , stat . UserID , appName )
2937
3072
2938
- key := uniqueKey {
2939
- TemplateID : w .TemplateID ,
2940
- UserID : s .UserID ,
2941
- AgentID : s .AgentID ,
2942
- AppKey : appKey {
2943
- AccessMethod : s .AccessMethod ,
2944
- SlugOrPort : s .SlugOrPort ,
2945
- Slug : app .Slug ,
2946
- DisplayName : app .DisplayName ,
2947
- Icon : app .Icon ,
2948
- },
2949
- }
2950
- if appUsageIntervalsByUserAgentApp [key ] == nil {
2951
- appUsageIntervalsByUserAgentApp [key ] = make (map [time.Time ]int64 )
3073
+ // SELECT
3074
+ key := appInsightsGroupBy {
3075
+ StartTime : stat .StartTime ,
3076
+ UserID : stat .UserID ,
3077
+ AppName : appName ,
3078
+ DisplayName : app .DisplayName ,
3079
+ Icon : app .Icon ,
3080
+ IsApp : app .Slug != "" ,
3081
+ }
3082
+ row , ok := appInsightRows [key ]
3083
+ if ! ok {
3084
+ row = appInsightsRow {
3085
+ appInsightsGroupBy : key ,
3086
+ }
3087
+ }
3088
+ row .TemplateIDs = append (row .TemplateIDs , stat .TemplateID )
3089
+ row .AppUsageMins = least (row .AppUsageMins + appUsage , 30 )
3090
+ appInsightRows [key ] = row
2952
3091
}
3092
+ }
2953
3093
2954
- t := s .SessionStartedAt .Truncate (5 * time .Minute )
2955
- if t .Before (arg .StartTime ) {
2956
- t = arg .StartTime
2957
- }
2958
- for t .Before (s .SessionEndedAt ) && t .Before (arg .EndTime ) {
2959
- appUsageIntervalsByUserAgentApp [key ][t ] = 60 // 1 minute.
2960
- t = t .Add (1 * time .Minute )
2961
- }
3094
+ /*
3095
+ templates AS (
3096
+ SELECT
3097
+ app_name,
3098
+ display_name,
3099
+ icon,
3100
+ is_app,
3101
+ array_agg(DISTINCT template_id)::uuid[] AS template_ids
3102
+ FROM
3103
+ app_insights, unnest(template_ids) AS template_id
3104
+ GROUP BY
3105
+ app_name, display_name, icon, is_app
3106
+ )
3107
+ */
3108
+
3109
+ type appGroupBy struct {
3110
+ AppName string
3111
+ DisplayName string
3112
+ Icon string
3113
+ IsApp bool
3114
+ }
3115
+ type templateRow struct {
3116
+ appGroupBy
3117
+ TemplateIDs []uuid.UUID
2962
3118
}
2963
3119
2964
- appUsageTemplateIDs := make (map [appKey ]map [uuid.UUID ]struct {})
2965
- appUsageUserIDs := make (map [appKey ]map [uuid.UUID ]struct {})
2966
- appUsage := make (map [appKey ]int64 )
2967
- for uniqueKey , usage := range appUsageIntervalsByUserAgentApp {
2968
- for _ , seconds := range usage {
2969
- if appUsageTemplateIDs [uniqueKey .AppKey ] == nil {
2970
- appUsageTemplateIDs [uniqueKey .AppKey ] = make (map [uuid.UUID ]struct {})
2971
- }
2972
- appUsageTemplateIDs [uniqueKey.AppKey ][uniqueKey.TemplateID ] = struct {}{}
2973
- if appUsageUserIDs [uniqueKey .AppKey ] == nil {
2974
- appUsageUserIDs [uniqueKey .AppKey ] = make (map [uuid.UUID ]struct {})
3120
+ templateRows := make (map [appGroupBy ]templateRow )
3121
+ for _ , row := range appInsightRows {
3122
+ key := appGroupBy {
3123
+ AppName : row .AppName ,
3124
+ DisplayName : row .DisplayName ,
3125
+ Icon : row .Icon ,
3126
+ IsApp : row .IsApp ,
3127
+ }
3128
+ row , ok := templateRows [key ]
3129
+ if ! ok {
3130
+ row = templateRow {
3131
+ appGroupBy : key ,
2975
3132
}
2976
- appUsageUserIDs [uniqueKey.AppKey ][uniqueKey.UserID ] = struct {}{}
2977
- appUsage [uniqueKey .AppKey ] += seconds
2978
3133
}
3134
+ row .TemplateIDs = append (row .TemplateIDs , row .TemplateIDs ... )
3135
+ templateRows [key ] = row
2979
3136
}
2980
3137
2981
- var rows []database.GetTemplateAppInsightsRow
2982
- for appKey , usage := range appUsage {
2983
- templateIDs := make ([]uuid.UUID , 0 , len (appUsageTemplateIDs [appKey ]))
2984
- for templateID := range appUsageTemplateIDs [appKey ] {
2985
- templateIDs = append (templateIDs , templateID )
2986
- }
2987
- slices .SortFunc (templateIDs , func (a , b uuid.UUID ) int {
2988
- return slice .Ascending (a .String (), b .String ())
2989
- })
2990
- activeUserIDs := make ([]uuid.UUID , 0 , len (appUsageUserIDs [appKey ]))
2991
- for userID := range appUsageUserIDs [appKey ] {
2992
- activeUserIDs = append (activeUserIDs , userID )
3138
+ /*
3139
+ SELECT
3140
+ t.template_ids,
3141
+ array_agg(DISTINCT ai.user_id)::uuid[] AS active_user_ids,
3142
+ ai.app_name AS slug_or_port,
3143
+ ai.display_name,
3144
+ ai.icon,
3145
+ ai.is_app,
3146
+ (SUM(ai.app_usage_mins) * 60)::bigint AS usage_seconds
3147
+ FROM
3148
+ app_insights AS ai
3149
+ JOIN
3150
+ templates AS t
3151
+ 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
3156
+ GROUP BY
3157
+ t.template_ids, ai.app_name, ai.display_name, ai.icon, ai.is_app;
3158
+ */
3159
+
3160
+ type templateAppInsightsRow struct {
3161
+ TemplateIDs []uuid.UUID
3162
+ ActiveUserIDs []uuid.UUID
3163
+ UsageSeconds int64
3164
+ }
3165
+ groupedRows := make (map [appGroupBy ]templateAppInsightsRow )
3166
+ for _ , aiRow := range appInsightRows {
3167
+ key := appGroupBy {
3168
+ AppName : aiRow .AppName ,
3169
+ DisplayName : aiRow .DisplayName ,
3170
+ Icon : aiRow .Icon ,
3171
+ IsApp : aiRow .IsApp ,
2993
3172
}
2994
- slices .SortFunc (activeUserIDs , func (a , b uuid.UUID ) int {
2995
- return slice .Ascending (a .String (), b .String ())
2996
- })
3173
+ row := groupedRows [key ]
3174
+ row .TemplateIDs = append (row .TemplateIDs , aiRow .TemplateIDs ... )
3175
+ row .ActiveUserIDs = append (row .ActiveUserIDs , aiRow .UserID )
3176
+ row .UsageSeconds += aiRow .AppUsageMins * 60
3177
+ }
2997
3178
3179
+ var rows []database.GetTemplateAppInsightsRow
3180
+ for k , gr := range groupedRows {
2998
3181
rows = append (rows , database.GetTemplateAppInsightsRow {
2999
- TemplateIDs : templateIDs ,
3000
- ActiveUserIDs : activeUserIDs ,
3001
- AccessMethod : appKey .AccessMethod ,
3002
- SlugOrPort : appKey .SlugOrPort ,
3003
- DisplayName : appKey .DisplayName ,
3004
- Icon : appKey .Icon ,
3005
- IsApp : appKey .Slug != "" ,
3006
- UsageSeconds : usage ,
3182
+ TemplateIDs : uniqueSortedUUIDs (gr .TemplateIDs ),
3183
+ ActiveUsers : int64 (len (uniqueSortedUUIDs (gr .ActiveUserIDs ))),
3184
+ DisplayName : k .DisplayName ,
3185
+ SlugOrPort : k .AppName ,
3186
+ UsageSeconds : gr .UsageSeconds ,
3007
3187
})
3008
3188
}
3009
3189
3010
3190
// NOTE(mafredri): Add sorting if we decide on how to handle PostgreSQL collations.
3011
- // ORDER BY access_method, slug_or_port, display_name, icon, is_app
3191
+ // ORDER BY slug_or_port, display_name, icon, is_app
3012
3192
return rows , nil
3013
3193
}
3014
3194
0 commit comments