@@ -799,6 +799,92 @@ 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
+
854
+ build ,err := q .getLatestWorkspaceBuildByWorkspaceIDNoLock (ctx ,workspace .ID )
855
+ if err != nil {
856
+ continue
857
+ }
858
+
859
+ resources ,err := q .getWorkspaceResourcesByJobIDNoLock (ctx ,build .JobID )
860
+ if err != nil {
861
+ continue
862
+ }
863
+ var resourceIDs []uuid.UUID
864
+ for _ ,resource := range resources {
865
+ resourceIDs = append (resourceIDs ,resource .ID )
866
+ }
867
+
868
+ agents ,err := q .getWorkspaceAgentsByResourceIDsNoLock (ctx ,resourceIDs )
869
+ if err != nil {
870
+ continue
871
+ }
872
+
873
+ for _ ,agent := range agents {
874
+ app ,err := q .getWorkspaceAppByAgentIDAndSlugNoLock (ctx , database.GetWorkspaceAppByAgentIDAndSlugParams {
875
+ AgentID :agent .ID ,
876
+ Slug :slug ,
877
+ })
878
+ if err != nil {
879
+ continue
880
+ }
881
+ return app ,nil
882
+ }
883
+ }
884
+
885
+ return database.WorkspaceApp {},sql .ErrNoRows
886
+ }
887
+
802
888
func (* FakeQuerier )AcquireLock (_ context.Context ,_ int64 )error {
803
889
return xerrors .New ("AcquireLock must only be called within a transaction" )
804
890
}
@@ -2896,119 +2982,214 @@ func (q *FakeQuerier) GetTemplateAppInsights(ctx context.Context, arg database.G
2896
2982
q .mutex .RLock ()
2897
2983
defer q .mutex .RUnlock ()
2898
2984
2899
- type appKey struct {
2900
- AccessMethod string
2901
- SlugOrPort string
2902
- Slug string
2903
- DisplayName string
2904
- Icon string
2985
+ /*
2986
+ WITH
2987
+ */
2988
+
2989
+ /*
2990
+ app_insights AS (
2991
+ SELECT
2992
+ tus.user_id,
2993
+ array_agg(DISTINCT tus.template_id)::uuid[] AS template_ids,
2994
+ app_usage.key::text AS app_name,
2995
+ COALESCE(wa.display_name, '') AS display_name,
2996
+ COALESCE(wa.icon, '') AS icon,
2997
+ (wa.slug IS NOT NULL)::boolean AS is_app,
2998
+ LEAST(SUM(app_usage.value::int), 30) AS app_usage_mins
2999
+ FROM
3000
+ template_usage_stats AS tus, jsonb_each(app_usage_mins) AS app_usage
3001
+ LEFT JOIN LATERAL (
3002
+ -- Fetch the latest app info for each app based on slug and template.
3003
+ SELECT
3004
+ app.display_name,
3005
+ app.icon,
3006
+ app.slug
3007
+ FROM
3008
+ workspace_apps AS app
3009
+ JOIN
3010
+ workspace_agents AS agent
3011
+ ON
3012
+ agent.id = app.agent_id
3013
+ JOIN
3014
+ workspace_resources AS resource
3015
+ ON
3016
+ resource.id = agent.resource_id
3017
+ JOIN
3018
+ workspace_builds AS build
3019
+ ON
3020
+ build.job_id = resource.job_id
3021
+ JOIN
3022
+ workspaces AS workspace
3023
+ ON
3024
+ workspace.id = build.workspace_id
3025
+ WHERE
3026
+ -- Requires lateral join.
3027
+ app.slug = app_usage.key
3028
+ AND workspace.owner_id = tus.user_id
3029
+ AND workspace.template_id = tus.template_id
3030
+ ORDER BY
3031
+ app.created_at DESC
3032
+ LIMIT 1
3033
+ ) AS wa
3034
+ ON
3035
+ true
3036
+ WHERE
3037
+ tus.start_time >= @start_time::timestamptz
3038
+ AND tus.end_time <= @end_time::timestamptz
3039
+ AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN tus.template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
3040
+ GROUP BY
3041
+ tus.start_time, tus.user_id, app_usage.key::text, wa.display_name, wa.icon, wa.slug
3042
+ ),
3043
+ */
3044
+
3045
+ type appInsightsGroupBy struct {
3046
+ StartTime time.Time
3047
+ UserID uuid.UUID
3048
+ AppName string
3049
+ DisplayName string
3050
+ Icon string
3051
+ IsApp bool
2905
3052
}
2906
- type uniqueKey struct {
2907
- TemplateID uuid.UUID
2908
- UserID uuid.UUID
2909
- AgentID uuid.UUID
2910
- AppKey appKey
3053
+ type appInsightsRow struct {
3054
+ appInsightsGroupBy
3055
+ TemplateIDs []uuid.UUID
3056
+ AppUsageMins int64
2911
3057
}
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 )))) {
3058
+ appInsightRows := make (map [appInsightsGroupBy ]appInsightsRow )
3059
+ // FROM
3060
+ for _ ,stat := range q .templateUsageStats {
3061
+ // WHERE
3062
+ if stat .StartTime .Before (arg .StartTime )|| stat .StartTime .After (arg .EndTime ) {
2921
3063
continue
2922
3064
}
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 ) {
3065
+ if len (arg .TemplateIDs )> 0 && ! slices .Contains (arg .TemplateIDs ,stat .TemplateID ) {
2930
3066
continue
2931
3067
}
2932
3068
2933
- app , _ := q . getWorkspaceAppByAgentIDAndSlugNoLock ( ctx , database. GetWorkspaceAppByAgentIDAndSlugParams {
2934
- AgentID : s . AgentID ,
2935
- Slug : s . SlugOrPort ,
2936
- } )
3069
+ // json_each
3070
+ for appName , appUsage := range stat . AppUsageMins {
3071
+ // LEFT JOIN LATERAL
3072
+ app , _ := q . getLatestWorkspaceAppByTemplateIDUserIDSlugNoLock ( ctx , stat . TemplateID , stat . UserID , appName )
2937
3073
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 )
3074
+ // SELECT
3075
+ key := appInsightsGroupBy {
3076
+ StartTime :stat .StartTime ,
3077
+ UserID :stat .UserID ,
3078
+ AppName :appName ,
3079
+ DisplayName :app .DisplayName ,
3080
+ Icon :app .Icon ,
3081
+ IsApp :app .Slug != "" ,
3082
+ }
3083
+ row ,ok := appInsightRows [key ]
3084
+ if ! ok {
3085
+ row = appInsightsRow {
3086
+ appInsightsGroupBy :key ,
3087
+ }
3088
+ }
3089
+ row .TemplateIDs = append (row .TemplateIDs ,stat .TemplateID )
3090
+ row .AppUsageMins = least (row .AppUsageMins + appUsage ,30 )
3091
+ appInsightRows [key ]= row
2952
3092
}
3093
+ }
2953
3094
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
- }
3095
+ /*
3096
+ templates AS (
3097
+ SELECT
3098
+ app_name,
3099
+ display_name,
3100
+ icon,
3101
+ is_app,
3102
+ array_agg(DISTINCT template_id)::uuid[] AS template_ids
3103
+ FROM
3104
+ app_insights, unnest(template_ids) AS template_id
3105
+ GROUP BY
3106
+ app_name, display_name, icon, is_app
3107
+ )
3108
+ */
3109
+
3110
+ type appGroupBy struct {
3111
+ AppName string
3112
+ DisplayName string
3113
+ Icon string
3114
+ IsApp bool
3115
+ }
3116
+ type templateRow struct {
3117
+ appGroupBy
3118
+ TemplateIDs []uuid.UUID
2962
3119
}
2963
3120
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 {})
3121
+ templateRows := make (map [appGroupBy ]templateRow )
3122
+ for _ ,row := range appInsightRows {
3123
+ key := appGroupBy {
3124
+ AppName :row .AppName ,
3125
+ DisplayName :row .DisplayName ,
3126
+ Icon :row .Icon ,
3127
+ IsApp :row .IsApp ,
3128
+ }
3129
+ row ,ok := templateRows [key ]
3130
+ if ! ok {
3131
+ row = templateRow {
3132
+ appGroupBy :key ,
2975
3133
}
2976
- appUsageUserIDs [uniqueKey.AppKey ][uniqueKey.UserID ]= struct {}{}
2977
- appUsage [uniqueKey .AppKey ]+= seconds
2978
3134
}
3135
+ row .TemplateIDs = append (row .TemplateIDs ,row .TemplateIDs ... )
3136
+ templateRows [key ]= row
2979
3137
}
2980
3138
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 )
3139
+ /*
3140
+ SELECT
3141
+ t.template_ids,
3142
+ array_agg(DISTINCT ai.user_id)::uuid[] AS active_user_ids,
3143
+ ai.app_name AS slug_or_port,
3144
+ ai.display_name,
3145
+ ai.icon,
3146
+ ai.is_app,
3147
+ (SUM(ai.app_usage_mins) * 60)::bigint AS usage_seconds
3148
+ FROM
3149
+ app_insights AS ai
3150
+ JOIN
3151
+ templates AS t
3152
+ ON
3153
+ ai.app_name = t.app_name
3154
+ AND ai.display_name = t.display_name
3155
+ AND ai.icon = t.icon
3156
+ AND ai.is_app = t.is_app
3157
+ GROUP BY
3158
+ t.template_ids, ai.app_name, ai.display_name, ai.icon, ai.is_app;
3159
+ */
3160
+
3161
+ type templateAppInsightsRow struct {
3162
+ TemplateIDs []uuid.UUID
3163
+ ActiveUserIDs []uuid.UUID
3164
+ UsageSeconds int64
3165
+ }
3166
+ groupedRows := make (map [appGroupBy ]templateAppInsightsRow )
3167
+ for _ ,aiRow := range appInsightRows {
3168
+ key := appGroupBy {
3169
+ AppName :aiRow .AppName ,
3170
+ DisplayName :aiRow .DisplayName ,
3171
+ Icon :aiRow .Icon ,
3172
+ IsApp :aiRow .IsApp ,
2993
3173
}
2994
- slices .SortFunc (activeUserIDs ,func (a ,b uuid.UUID )int {
2995
- return slice .Ascending (a .String (),b .String ())
2996
- })
3174
+ row := groupedRows [key ]
3175
+ row .TemplateIDs = append (row .TemplateIDs ,aiRow .TemplateIDs ... )
3176
+ row .ActiveUserIDs = append (row .ActiveUserIDs ,aiRow .UserID )
3177
+ row .UsageSeconds += aiRow .AppUsageMins * 60
3178
+ }
2997
3179
3180
+ var rows []database.GetTemplateAppInsightsRow
3181
+ for k ,gr := range groupedRows {
2998
3182
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 ,
3183
+ TemplateIDs :uniqueSortedUUIDs (gr .TemplateIDs ),
3184
+ ActiveUsers :int64 (len (uniqueSortedUUIDs (gr .ActiveUserIDs ))),
3185
+ DisplayName :k .DisplayName ,
3186
+ SlugOrPort :k .AppName ,
3187
+ UsageSeconds :gr .UsageSeconds ,
3007
3188
})
3008
3189
}
3009
3190
3010
3191
// NOTE(mafredri): Add sorting if we decide on how to handle PostgreSQL collations.
3011
- // ORDER BYaccess_method, slug_or_port, display_name, icon, is_app
3192
+ // ORDER BY slug_or_port, display_name, icon, is_app
3012
3193
return rows ,nil
3013
3194
}
3014
3195