2023-01-26 01:03:47 +00:00
package coderd
import (
2023-07-21 18:00:19 +00:00
"context"
2024-03-25 14:16:41 +00:00
"database/sql"
2023-07-21 18:00:19 +00:00
"fmt"
2023-01-26 01:03:47 +00:00
"net/http"
2023-08-24 10:36:40 +00:00
"strings"
2023-07-21 18:00:19 +00:00
"time"
2023-01-26 01:03:47 +00:00
2023-07-21 18:00:19 +00:00
"github.com/google/uuid"
"golang.org/x/exp/slices"
2023-08-23 15:31:23 +00:00
"golang.org/x/sync/errgroup"
2023-07-21 18:00:19 +00:00
"golang.org/x/xerrors"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
2023-01-26 01:03:47 +00:00
)
2023-07-21 18:00:19 +00:00
// Duplicated in codersdk.
const insightsTimeLayout = time . RFC3339
2023-01-26 01:03:47 +00:00
// @Summary Get deployment DAUs
// @ID get-deployment-daus
// @Security CoderSessionToken
// @Produce json
// @Tags Insights
2023-05-30 17:18:27 +00:00
// @Success 200 {object} codersdk.DAUsResponse
2023-01-26 01:03:47 +00:00
// @Router /insights/daus [get]
func ( api * API ) deploymentDAUs ( rw http . ResponseWriter , r * http . Request ) {
2023-03-07 21:10:01 +00:00
if ! api . Authorize ( r , rbac . ActionRead , rbac . ResourceDeploymentValues ) {
2023-01-26 01:03:47 +00:00
httpapi . Forbidden ( rw )
return
}
2024-03-27 16:10:14 +00:00
api . returnDAUsInternal ( rw , r , nil )
}
func ( api * API ) returnDAUsInternal ( rw http . ResponseWriter , r * http . Request , templateIDs [ ] uuid . UUID ) {
ctx := r . Context ( )
2023-05-30 17:18:27 +00:00
p := httpapi . NewQueryParamParser ( )
2024-03-27 16:10:14 +00:00
vals := r . URL . Query ( )
2023-05-30 17:18:27 +00:00
tzOffset := p . Int ( vals , 0 , "tz_offset" )
p . ErrorExcessParams ( vals )
if len ( p . Errors ) > 0 {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Query parameters have invalid values." ,
Validations : p . Errors ,
} )
return
}
2024-03-27 16:10:14 +00:00
loc := time . FixedZone ( "" , tzOffset * 3600 )
// If the time is 14:01 or 14:31, we still want to include all the
// data between 14:00 and 15:00. Our rollups buckets are 30 minutes
// so this works nicely. It works just as well for 23:59 as well.
nextHourInLoc := time . Now ( ) . In ( loc ) . Truncate ( time . Hour ) . Add ( time . Hour )
// Always return 60 days of data (2 months).
sixtyDaysAgo := nextHourInLoc . In ( loc ) . Truncate ( 24 * time . Hour ) . AddDate ( 0 , 0 , - 60 )
rows , err := api . Database . GetTemplateInsightsByInterval ( ctx , database . GetTemplateInsightsByIntervalParams {
StartTime : sixtyDaysAgo ,
EndTime : nextHourInLoc ,
IntervalDays : 1 ,
TemplateIDs : templateIDs ,
} )
if err != nil {
if httpapi . Is404Error ( err ) {
httpapi . ResourceNotFound ( rw )
return
}
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error fetching DAUs." ,
Detail : err . Error ( ) ,
} )
}
resp := codersdk . DAUsResponse {
TZHourOffset : tzOffset ,
Entries : make ( [ ] codersdk . DAUEntry , 0 , len ( rows ) ) ,
}
for _ , row := range rows {
resp . Entries = append ( resp . Entries , codersdk . DAUEntry {
Date : row . StartTime . Format ( time . DateOnly ) ,
Amount : int ( row . ActiveUsers ) ,
2023-01-26 01:03:47 +00:00
} )
}
httpapi . Write ( ctx , rw , http . StatusOK , resp )
}
2023-07-21 18:00:19 +00:00
2023-09-26 16:42:16 +00:00
// @Summary Get insights about user activity
// @ID get-insights-about-user-activity
// @Security CoderSessionToken
// @Produce json
// @Tags Insights
2024-02-07 13:38:54 +00:00
// @Param before query int true "Start time"
// @Param after query int true "End time"
2023-09-26 16:42:16 +00:00
// @Success 200 {object} codersdk.UserActivityInsightsResponse
// @Router /insights/user-activity [get]
func ( api * API ) insightsUserActivity ( rw http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
p := httpapi . NewQueryParamParser ( ) .
2024-02-20 23:58:43 +00:00
RequiredNotEmpty ( "start_time" ) .
RequiredNotEmpty ( "end_time" )
2023-09-26 16:42:16 +00:00
vals := r . URL . Query ( )
var (
// The QueryParamParser does not preserve timezone, so we need
// to parse the time ourselves.
startTimeString = p . String ( vals , "" , "start_time" )
endTimeString = p . String ( vals , "" , "end_time" )
templateIDs = p . UUIDs ( vals , [ ] uuid . UUID { } , "template_ids" )
)
p . ErrorExcessParams ( vals )
if len ( p . Errors ) > 0 {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Query parameters have invalid values." ,
Validations : p . Errors ,
} )
return
}
2023-12-04 17:20:22 +00:00
startTime , endTime , ok := parseInsightsStartAndEndTime ( ctx , rw , time . Now ( ) , startTimeString , endTimeString )
2023-09-26 16:42:16 +00:00
if ! ok {
return
}
rows , err := api . Database . GetUserActivityInsights ( ctx , database . GetUserActivityInsightsParams {
StartTime : startTime ,
EndTime : endTime ,
TemplateIDs : templateIDs ,
} )
if err != nil {
2024-03-25 14:16:41 +00:00
// No data is not an error.
if xerrors . Is ( err , sql . ErrNoRows ) {
httpapi . Write ( ctx , rw , http . StatusOK , codersdk . UserActivityInsightsResponse {
Report : codersdk . UserActivityInsightsReport {
StartTime : startTime ,
EndTime : endTime ,
TemplateIDs : [ ] uuid . UUID { } ,
Users : [ ] codersdk . UserActivity { } ,
} ,
} )
return
}
// Check authorization.
2023-09-26 16:42:16 +00:00
if httpapi . Is404Error ( err ) {
httpapi . ResourceNotFound ( rw )
return
}
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error fetching user activity." ,
Detail : err . Error ( ) ,
} )
return
}
templateIDSet := make ( map [ uuid . UUID ] struct { } )
userActivities := make ( [ ] codersdk . UserActivity , 0 , len ( rows ) )
for _ , row := range rows {
for _ , templateID := range row . TemplateIDs {
templateIDSet [ templateID ] = struct { } { }
}
userActivities = append ( userActivities , codersdk . UserActivity {
TemplateIDs : row . TemplateIDs ,
UserID : row . UserID ,
Username : row . Username ,
2023-12-11 17:09:51 +00:00
AvatarURL : row . AvatarURL ,
2023-09-26 16:42:16 +00:00
Seconds : row . UsageSeconds ,
} )
}
// TemplateIDs that contributed to the data.
seenTemplateIDs := make ( [ ] uuid . UUID , 0 , len ( templateIDSet ) )
for templateID := range templateIDSet {
seenTemplateIDs = append ( seenTemplateIDs , templateID )
}
slices . SortFunc ( seenTemplateIDs , func ( a , b uuid . UUID ) int {
return slice . Ascending ( a . String ( ) , b . String ( ) )
} )
resp := codersdk . UserActivityInsightsResponse {
Report : codersdk . UserActivityInsightsReport {
StartTime : startTime ,
EndTime : endTime ,
TemplateIDs : seenTemplateIDs ,
Users : userActivities ,
} ,
}
httpapi . Write ( ctx , rw , http . StatusOK , resp )
}
2023-07-21 18:00:19 +00:00
// @Summary Get insights about user latency
// @ID get-insights-about-user-latency
// @Security CoderSessionToken
// @Produce json
// @Tags Insights
2024-02-07 13:38:54 +00:00
// @Param before query int true "Start time"
// @Param after query int true "End time"
2023-07-21 18:00:19 +00:00
// @Success 200 {object} codersdk.UserLatencyInsightsResponse
// @Router /insights/user-latency [get]
func ( api * API ) insightsUserLatency ( rw http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
p := httpapi . NewQueryParamParser ( ) .
2024-02-20 23:58:43 +00:00
RequiredNotEmpty ( "start_time" ) .
RequiredNotEmpty ( "end_time" )
2023-07-21 18:00:19 +00:00
vals := r . URL . Query ( )
var (
// The QueryParamParser does not preserve timezone, so we need
// to parse the time ourselves.
startTimeString = p . String ( vals , "" , "start_time" )
endTimeString = p . String ( vals , "" , "end_time" )
templateIDs = p . UUIDs ( vals , [ ] uuid . UUID { } , "template_ids" )
)
p . ErrorExcessParams ( vals )
if len ( p . Errors ) > 0 {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Query parameters have invalid values." ,
Validations : p . Errors ,
} )
return
}
2023-12-04 17:20:22 +00:00
startTime , endTime , ok := parseInsightsStartAndEndTime ( ctx , rw , time . Now ( ) , startTimeString , endTimeString )
2023-07-21 18:00:19 +00:00
if ! ok {
return
}
rows , err := api . Database . GetUserLatencyInsights ( ctx , database . GetUserLatencyInsightsParams {
StartTime : startTime ,
EndTime : endTime ,
TemplateIDs : templateIDs ,
} )
if err != nil {
2023-07-31 13:44:32 +00:00
if httpapi . Is404Error ( err ) {
httpapi . ResourceNotFound ( rw )
return
}
2023-07-21 18:00:19 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error fetching user latency." ,
Detail : err . Error ( ) ,
} )
return
}
templateIDSet := make ( map [ uuid . UUID ] struct { } )
userLatencies := make ( [ ] codersdk . UserLatency , 0 , len ( rows ) )
for _ , row := range rows {
for _ , templateID := range row . TemplateIDs {
templateIDSet [ templateID ] = struct { } { }
}
userLatencies = append ( userLatencies , codersdk . UserLatency {
TemplateIDs : row . TemplateIDs ,
UserID : row . UserID ,
Username : row . Username ,
2023-12-11 17:09:51 +00:00
AvatarURL : row . AvatarURL ,
2023-07-21 18:00:19 +00:00
LatencyMS : codersdk . ConnectionLatency {
P50 : row . WorkspaceConnectionLatency50 ,
P95 : row . WorkspaceConnectionLatency95 ,
} ,
} )
}
// TemplateIDs that contributed to the data.
seenTemplateIDs := make ( [ ] uuid . UUID , 0 , len ( templateIDSet ) )
for templateID := range templateIDSet {
seenTemplateIDs = append ( seenTemplateIDs , templateID )
}
2023-08-09 19:50:26 +00:00
slices . SortFunc ( seenTemplateIDs , func ( a , b uuid . UUID ) int {
return slice . Ascending ( a . String ( ) , b . String ( ) )
2023-07-21 18:00:19 +00:00
} )
resp := codersdk . UserLatencyInsightsResponse {
Report : codersdk . UserLatencyInsightsReport {
StartTime : startTime ,
EndTime : endTime ,
TemplateIDs : seenTemplateIDs ,
Users : userLatencies ,
} ,
}
httpapi . Write ( ctx , rw , http . StatusOK , resp )
}
// @Summary Get insights about templates
// @ID get-insights-about-templates
// @Security CoderSessionToken
// @Produce json
// @Tags Insights
2024-02-07 13:38:54 +00:00
// @Param before query int true "Start time"
// @Param after query int true "End time"
2023-07-21 18:00:19 +00:00
// @Success 200 {object} codersdk.TemplateInsightsResponse
// @Router /insights/templates [get]
func ( api * API ) insightsTemplates ( rw http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
p := httpapi . NewQueryParamParser ( ) .
2024-02-20 23:58:43 +00:00
RequiredNotEmpty ( "start_time" ) .
RequiredNotEmpty ( "end_time" )
2023-07-21 18:00:19 +00:00
vals := r . URL . Query ( )
var (
// The QueryParamParser does not preserve timezone, so we need
// to parse the time ourselves.
startTimeString = p . String ( vals , "" , "start_time" )
endTimeString = p . String ( vals , "" , "end_time" )
intervalString = p . String ( vals , "" , "interval" )
templateIDs = p . UUIDs ( vals , [ ] uuid . UUID { } , "template_ids" )
2023-10-03 13:44:50 +00:00
sectionStrings = p . Strings ( vals , templateInsightsSectionAsStrings ( codersdk . TemplateInsightsSectionIntervalReports , codersdk . TemplateInsightsSectionReport ) , "sections" )
2023-07-21 18:00:19 +00:00
)
p . ErrorExcessParams ( vals )
if len ( p . Errors ) > 0 {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Query parameters have invalid values." ,
Validations : p . Errors ,
} )
return
}
2023-12-04 17:20:22 +00:00
startTime , endTime , ok := parseInsightsStartAndEndTime ( ctx , rw , time . Now ( ) , startTimeString , endTimeString )
2023-07-21 18:00:19 +00:00
if ! ok {
return
}
2023-09-19 11:06:19 +00:00
interval , ok := parseInsightsInterval ( ctx , rw , intervalString , startTime , endTime )
2023-07-21 18:00:19 +00:00
if ! ok {
return
}
2023-10-03 13:44:50 +00:00
sections , ok := parseTemplateInsightsSections ( ctx , rw , sectionStrings )
if ! ok {
return
}
2023-07-21 18:00:19 +00:00
var usage database . GetTemplateInsightsRow
2023-08-21 12:08:58 +00:00
var appUsage [ ] database . GetTemplateAppInsightsRow
2023-09-15 12:01:00 +00:00
var dailyUsage [ ] database . GetTemplateInsightsByIntervalRow
2023-08-23 15:31:23 +00:00
var parameterRows [ ] database . GetTemplateParameterInsightsRow
2023-07-31 13:44:32 +00:00
2023-08-23 15:31:23 +00:00
eg , egCtx := errgroup . WithContext ( ctx )
eg . SetLimit ( 4 )
2023-07-21 18:00:19 +00:00
2023-08-23 15:31:23 +00:00
// The following insights data queries have a theoretical chance to be
2023-09-19 11:06:19 +00:00
// inconsistent between each other when looking at "today", however, the
2023-08-23 15:31:23 +00:00
// overhead from a transaction is not worth it.
eg . Go ( func ( ) error {
var err error
2023-10-03 13:44:50 +00:00
if interval != "" && slices . Contains ( sections , codersdk . TemplateInsightsSectionIntervalReports ) {
2023-09-15 12:01:00 +00:00
dailyUsage , err = api . Database . GetTemplateInsightsByInterval ( egCtx , database . GetTemplateInsightsByIntervalParams {
StartTime : startTime ,
EndTime : endTime ,
TemplateIDs : templateIDs ,
2023-09-19 11:06:19 +00:00
IntervalDays : interval . Days ( ) ,
2023-07-21 18:00:19 +00:00
} )
if err != nil {
return xerrors . Errorf ( "get template daily insights: %w" , err )
}
}
2023-08-23 15:31:23 +00:00
return nil
} )
eg . Go ( func ( ) error {
2023-10-03 13:44:50 +00:00
if ! slices . Contains ( sections , codersdk . TemplateInsightsSectionReport ) {
return nil
}
2023-08-23 15:31:23 +00:00
var err error
usage , err = api . Database . GetTemplateInsights ( egCtx , database . GetTemplateInsightsParams {
2023-07-21 18:00:19 +00:00
StartTime : startTime ,
EndTime : endTime ,
TemplateIDs : templateIDs ,
} )
if err != nil {
return xerrors . Errorf ( "get template insights: %w" , err )
}
2023-08-23 15:31:23 +00:00
return nil
} )
eg . Go ( func ( ) error {
2023-10-03 13:44:50 +00:00
if ! slices . Contains ( sections , codersdk . TemplateInsightsSectionReport ) {
return nil
}
2023-08-23 15:31:23 +00:00
var err error
appUsage , err = api . Database . GetTemplateAppInsights ( egCtx , database . GetTemplateAppInsightsParams {
2023-08-21 12:08:58 +00:00
StartTime : startTime ,
EndTime : endTime ,
TemplateIDs : templateIDs ,
} )
if err != nil {
return xerrors . Errorf ( "get template app insights: %w" , err )
}
2023-08-23 15:31:23 +00:00
return nil
} )
2023-08-21 12:08:58 +00:00
2023-08-23 15:31:23 +00:00
// Template parameter insights have no risk of inconsistency with the other
// insights.
eg . Go ( func ( ) error {
2023-10-03 13:44:50 +00:00
if ! slices . Contains ( sections , codersdk . TemplateInsightsSectionReport ) {
return nil
}
2023-08-23 15:31:23 +00:00
var err error
parameterRows , err = api . Database . GetTemplateParameterInsights ( ctx , database . GetTemplateParameterInsightsParams {
StartTime : startTime ,
EndTime : endTime ,
TemplateIDs : templateIDs ,
} )
if err != nil {
return xerrors . Errorf ( "get template parameter insights: %w" , err )
}
2023-07-21 18:00:19 +00:00
return nil
2023-08-23 15:31:23 +00:00
} )
err := eg . Wait ( )
2023-07-31 13:44:32 +00:00
if httpapi . Is404Error ( err ) {
httpapi . ResourceNotFound ( rw )
return
}
2023-07-21 18:00:19 +00:00
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error fetching template insights." ,
Detail : err . Error ( ) ,
} )
return
}
2023-08-07 16:11:44 +00:00
parametersUsage , err := db2sdk . TemplateInsightsParameters ( parameterRows )
2023-08-03 14:43:23 +00:00
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error converting template parameter insights." ,
Detail : err . Error ( ) ,
} )
return
}
2023-07-21 18:00:19 +00:00
resp := codersdk . TemplateInsightsResponse {
2023-10-03 13:44:50 +00:00
IntervalReports : [ ] codersdk . TemplateInsightsIntervalReport { } ,
}
if slices . Contains ( sections , codersdk . TemplateInsightsSectionReport ) {
resp . Report = & codersdk . TemplateInsightsReport {
2023-08-03 14:43:23 +00:00
StartTime : startTime ,
EndTime : endTime ,
2024-03-25 13:33:31 +00:00
TemplateIDs : usage . TemplateIDs ,
ActiveUsers : usage . ActiveUsers ,
2023-08-21 12:08:58 +00:00
AppsUsage : convertTemplateInsightsApps ( usage , appUsage ) ,
2023-08-03 14:43:23 +00:00
ParametersUsage : parametersUsage ,
2023-10-03 13:44:50 +00:00
}
2023-07-21 18:00:19 +00:00
}
2023-10-03 13:44:50 +00:00
2023-07-21 18:00:19 +00:00
for _ , row := range dailyUsage {
resp . IntervalReports = append ( resp . IntervalReports , codersdk . TemplateInsightsIntervalReport {
2023-08-24 10:36:40 +00:00
// NOTE(mafredri): This might not be accurate over DST since the
// parsed location only contains the offset.
StartTime : row . StartTime . In ( startTime . Location ( ) ) ,
EndTime : row . EndTime . In ( startTime . Location ( ) ) ,
2023-07-21 18:00:19 +00:00
Interval : interval ,
TemplateIDs : row . TemplateIDs ,
ActiveUsers : row . ActiveUsers ,
} )
}
httpapi . Write ( ctx , rw , http . StatusOK , resp )
}
2023-08-21 12:08:58 +00:00
// convertTemplateInsightsApps builds the list of builtin apps and template apps
// from the provided database rows, builtin apps are implicitly a part of all
// templates.
func convertTemplateInsightsApps ( usage database . GetTemplateInsightsRow , appUsage [ ] database . GetTemplateAppInsightsRow ) [ ] codersdk . TemplateAppUsage {
// Builtin apps.
apps := [ ] codersdk . TemplateAppUsage {
2023-07-21 18:00:19 +00:00
{
2024-03-25 13:33:31 +00:00
TemplateIDs : usage . VscodeTemplateIds ,
2023-07-21 18:00:19 +00:00
Type : codersdk . TemplateAppsTypeBuiltin ,
2023-11-07 16:14:59 +00:00
DisplayName : codersdk . TemplateBuiltinAppDisplayNameVSCode ,
2023-07-21 18:00:19 +00:00
Slug : "vscode" ,
2023-07-25 14:06:58 +00:00
Icon : "/icon/code.svg" ,
2023-07-21 18:00:19 +00:00
Seconds : usage . UsageVscodeSeconds ,
} ,
{
2024-03-25 13:33:31 +00:00
TemplateIDs : usage . JetbrainsTemplateIds ,
2023-07-21 18:00:19 +00:00
Type : codersdk . TemplateAppsTypeBuiltin ,
2023-11-07 16:14:59 +00:00
DisplayName : codersdk . TemplateBuiltinAppDisplayNameJetBrains ,
2023-07-21 18:00:19 +00:00
Slug : "jetbrains" ,
2023-07-25 14:06:58 +00:00
Icon : "/icon/intellij.svg" ,
2023-07-21 18:00:19 +00:00
Seconds : usage . UsageJetbrainsSeconds ,
} ,
2023-08-21 12:08:58 +00:00
// TODO(mafredri): We could take Web Terminal usage from appUsage since
// that should be more accurate. The difference is that this reflects
// the rpty session as seen by the agent (can live past the connection),
// whereas appUsage reflects the lifetime of the client connection. The
// condition finding the corresponding app entry in appUsage is:
// !app.IsApp && app.AccessMethod == "terminal" && app.SlugOrPort == ""
2023-07-21 18:00:19 +00:00
{
2024-03-25 13:33:31 +00:00
TemplateIDs : usage . ReconnectingPtyTemplateIds ,
2023-07-21 18:00:19 +00:00
Type : codersdk . TemplateAppsTypeBuiltin ,
2023-11-07 16:14:59 +00:00
DisplayName : codersdk . TemplateBuiltinAppDisplayNameWebTerminal ,
2023-07-21 18:00:19 +00:00
Slug : "reconnecting-pty" ,
2023-07-25 14:06:58 +00:00
Icon : "/icon/terminal.svg" ,
2023-07-21 18:00:19 +00:00
Seconds : usage . UsageReconnectingPtySeconds ,
} ,
{
2024-03-25 13:33:31 +00:00
TemplateIDs : usage . SshTemplateIds ,
2023-07-21 18:00:19 +00:00
Type : codersdk . TemplateAppsTypeBuiltin ,
2023-11-07 16:14:59 +00:00
DisplayName : codersdk . TemplateBuiltinAppDisplayNameSSH ,
2023-07-21 18:00:19 +00:00
Slug : "ssh" ,
2023-07-25 14:06:58 +00:00
Icon : "/icon/terminal.svg" ,
2023-07-21 18:00:19 +00:00
Seconds : usage . UsageSshSeconds ,
} ,
2024-03-27 12:09:29 +00:00
{
TemplateIDs : usage . SftpTemplateIds ,
Type : codersdk . TemplateAppsTypeBuiltin ,
DisplayName : codersdk . TemplateBuiltinAppDisplayNameSFTP ,
Slug : "sftp" ,
Icon : "/icon/terminal.svg" ,
Seconds : usage . UsageSftpSeconds ,
} ,
2023-07-21 18:00:19 +00:00
}
2023-08-21 12:08:58 +00:00
2023-08-24 10:36:40 +00:00
// Use a stable sort, similarly to how we would sort in the query, note that
// we don't sort in the query because order varies depending on the table
// collation.
//
2024-03-27 10:28:36 +00:00
// ORDER BY slug, display_name, icon
2023-08-24 10:36:40 +00:00
slices . SortFunc ( appUsage , func ( a , b database . GetTemplateAppInsightsRow ) int {
2024-03-27 10:28:36 +00:00
if a . Slug != b . Slug {
return strings . Compare ( a . Slug , b . Slug )
2023-08-24 10:36:40 +00:00
}
2024-03-25 13:58:37 +00:00
if a . DisplayName != b . DisplayName {
return strings . Compare ( a . DisplayName , b . DisplayName )
2023-08-24 10:36:40 +00:00
}
2024-03-27 10:28:36 +00:00
return strings . Compare ( a . Icon , b . Icon )
2023-08-24 10:36:40 +00:00
} )
2023-08-21 12:08:58 +00:00
// Template apps.
for _ , app := range appUsage {
apps = append ( apps , codersdk . TemplateAppUsage {
TemplateIDs : app . TemplateIDs ,
Type : codersdk . TemplateAppsTypeApp ,
2024-03-25 13:58:37 +00:00
DisplayName : app . DisplayName ,
2024-03-27 10:28:36 +00:00
Slug : app . Slug ,
2024-03-25 13:58:37 +00:00
Icon : app . Icon ,
2023-08-21 12:08:58 +00:00
Seconds : app . UsageSeconds ,
} )
}
return apps
2023-07-21 18:00:19 +00:00
}
// parseInsightsStartAndEndTime parses the start and end time query parameters
// and returns the parsed values. The client provided timezone must be preserved
// when parsing the time. Verification is performed so that the start and end
// time are not zero and that the end time is not before the start time. The
// clock must be set to 00:00:00, except for "today", where end time is allowed
// to provide the hour of the day (e.g. 14:00:00).
2023-12-04 17:20:22 +00:00
func parseInsightsStartAndEndTime ( ctx context . Context , rw http . ResponseWriter , now time . Time , startTimeString , endTimeString string ) ( startTime , endTime time . Time , ok bool ) {
2023-07-21 18:00:19 +00:00
for _ , qp := range [ ] struct {
name , value string
dest * time . Time
} {
{ "start_time" , startTimeString , & startTime } ,
{ "end_time" , endTimeString , & endTime } ,
} {
t , err := time . Parse ( insightsTimeLayout , qp . value )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Query parameter has invalid value." ,
Validations : [ ] codersdk . ValidationError {
{
Field : qp . name ,
Detail : fmt . Sprintf ( "Query param %q must be a valid date format (%s): %s" , qp . name , insightsTimeLayout , err . Error ( ) ) ,
} ,
} ,
} )
return time . Time { } , time . Time { } , false
}
2023-12-04 17:20:22 +00:00
// Change now to the same timezone as the parsed time.
now := now . In ( t . Location ( ) )
2023-07-21 18:00:19 +00:00
if t . IsZero ( ) {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Query parameter has invalid value." ,
Validations : [ ] codersdk . ValidationError {
{
Field : qp . name ,
Detail : fmt . Sprintf ( "Query param %q must not be zero" , qp . name ) ,
} ,
} ,
} )
return time . Time { } , time . Time { } , false
}
// Round upwards one hour to ensure we can fetch the latest data.
if t . After ( now . Truncate ( time . Hour ) . Add ( time . Hour ) ) {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Query parameter has invalid value." ,
Validations : [ ] codersdk . ValidationError {
{
Field : qp . name ,
Detail : fmt . Sprintf ( "Query param %q must not be in the future" , qp . name ) ,
} ,
} ,
} )
return time . Time { } , time . Time { } , false
}
ensureZeroHour := true
if qp . name == "end_time" {
ey , em , ed := t . Date ( )
ty , tm , td := now . Date ( )
ensureZeroHour = ey != ty || em != tm || ed != td
}
h , m , s := t . Clock ( )
if ensureZeroHour && ( h != 0 || m != 0 || s != 0 ) {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Query parameter has invalid value." ,
Validations : [ ] codersdk . ValidationError {
{
Field : qp . name ,
2023-12-04 17:20:22 +00:00
Detail : fmt . Sprintf ( "Query param %q must have the clock set to 00:00:00, got %s" , qp . name , qp . value ) ,
2023-07-21 18:00:19 +00:00
} ,
} ,
} )
return time . Time { } , time . Time { } , false
} else if m != 0 || s != 0 {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Query parameter has invalid value." ,
Validations : [ ] codersdk . ValidationError {
{
Field : qp . name ,
2023-12-04 17:20:22 +00:00
Detail : fmt . Sprintf ( "Query param %q must have the clock set to %02d:00:00, got %s" , qp . name , h , qp . value ) ,
2023-07-21 18:00:19 +00:00
} ,
} ,
} )
return time . Time { } , time . Time { } , false
}
* qp . dest = t
}
if endTime . Before ( startTime ) {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Query parameter has invalid value." ,
Validations : [ ] codersdk . ValidationError {
{
Field : "end_time" ,
Detail : fmt . Sprintf ( "Query param %q must be after than %q" , "end_time" , "start_time" ) ,
} ,
} ,
} )
return time . Time { } , time . Time { } , false
}
return startTime , endTime , true
}
2023-09-19 11:06:19 +00:00
func parseInsightsInterval ( ctx context . Context , rw http . ResponseWriter , intervalString string , startTime , endTime time . Time ) ( codersdk . InsightsReportInterval , bool ) {
2023-07-21 18:00:19 +00:00
switch v := codersdk . InsightsReportInterval ( intervalString ) ; v {
case codersdk . InsightsReportIntervalDay , "" :
return v , true
2023-09-19 11:06:19 +00:00
case codersdk . InsightsReportIntervalWeek :
if ! lastReportIntervalHasAtLeastSixDays ( startTime , endTime ) {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Query parameter has invalid value." ,
Detail : "Last report interval should have at least 6 days." ,
} )
return "" , false
}
return v , true
2023-07-21 18:00:19 +00:00
default :
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Query parameter has invalid value." ,
Validations : [ ] codersdk . ValidationError {
{
Field : "interval" ,
2023-09-19 11:06:19 +00:00
Detail : fmt . Sprintf ( "must be one of %v" , [ ] codersdk . InsightsReportInterval { codersdk . InsightsReportIntervalDay , codersdk . InsightsReportIntervalWeek } ) ,
2023-07-21 18:00:19 +00:00
} ,
} ,
} )
return "" , false
}
}
2023-09-19 11:06:19 +00:00
func lastReportIntervalHasAtLeastSixDays ( startTime , endTime time . Time ) bool {
lastReportIntervalDays := endTime . Sub ( startTime ) % ( 7 * 24 * time . Hour )
if lastReportIntervalDays == 0 {
return true // this is a perfectly full week!
}
// Ensure that the last interval has at least 6 days, or check the special case, forward DST change,
// when the duration can be shorter than 6 days: 5 days 23 hours.
return lastReportIntervalDays >= 6 * 24 * time . Hour || startTime . AddDate ( 0 , 0 , 6 ) . Equal ( endTime )
}
2023-10-03 13:44:50 +00:00
func templateInsightsSectionAsStrings ( sections ... codersdk . TemplateInsightsSection ) [ ] string {
t := make ( [ ] string , len ( sections ) )
for i , s := range sections {
t [ i ] = string ( s )
}
return t
}
func parseTemplateInsightsSections ( ctx context . Context , rw http . ResponseWriter , sections [ ] string ) ( [ ] codersdk . TemplateInsightsSection , bool ) {
t := make ( [ ] codersdk . TemplateInsightsSection , len ( sections ) )
for i , s := range sections {
switch v := codersdk . TemplateInsightsSection ( s ) ; v {
case codersdk . TemplateInsightsSectionIntervalReports , codersdk . TemplateInsightsSectionReport :
t [ i ] = v
default :
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Query parameter has invalid value." ,
Validations : [ ] codersdk . ValidationError {
{
Field : "sections" ,
Detail : fmt . Sprintf ( "must be one of %v" , [ ] codersdk . TemplateInsightsSection { codersdk . TemplateInsightsSectionIntervalReports , codersdk . TemplateInsightsSectionReport } ) ,
} ,
} ,
} )
return nil , false
}
}
return t , true
}