diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index d96f03b6cf25a..c849cd2ef81e3 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8438,10 +8438,12 @@ const docTemplate = `{ "codersdk.InsightsReportInterval": { "type": "string", "enum": [ - "day" + "day", + "week" ], "x-enum-varnames": [ - "InsightsReportIntervalDay" + "InsightsReportIntervalDay", + "InsightsReportIntervalWeek" ] }, "codersdk.IssueReconnectingPTYSignedTokenRequest": { @@ -9732,7 +9734,12 @@ const docTemplate = `{ "format": "date-time" }, "interval": { - "$ref": "#/definitions/codersdk.InsightsReportInterval" + "allOf": [ + { + "$ref": "#/definitions/codersdk.InsightsReportInterval" + } + ], + "example": "week" }, "start_time": { "type": "string", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 99582f5665b2a..843a09d51c464 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7578,8 +7578,11 @@ }, "codersdk.InsightsReportInterval": { "type": "string", - "enum": ["day"], - "x-enum-varnames": ["InsightsReportIntervalDay"] + "enum": ["day", "week"], + "x-enum-varnames": [ + "InsightsReportIntervalDay", + "InsightsReportIntervalWeek" + ] }, "codersdk.IssueReconnectingPTYSignedTokenRequest": { "type": "object", @@ -8794,7 +8797,12 @@ "format": "date-time" }, "interval": { - "$ref": "#/definitions/codersdk.InsightsReportInterval" + "allOf": [ + { + "$ref": "#/definitions/codersdk.InsightsReportInterval" + } + ], + "example": "week" }, "start_time": { "type": "string", diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 5e1d2b83d4b44..17b12293e0c08 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -7518,7 +7518,7 @@ func (q *sqlQuerier) UpdateWorkspaceAgentStartupByID(ctx context.Context, arg Up } const deleteOldWorkspaceAgentStats = `-- name: DeleteOldWorkspaceAgentStats :exec -DELETE FROM workspace_agent_stats WHERE created_at < NOW() - INTERVAL '30 days' +DELETE FROM workspace_agent_stats WHERE created_at < NOW() - INTERVAL '6 months' ` func (q *sqlQuerier) DeleteOldWorkspaceAgentStats(ctx context.Context) error { diff --git a/coderd/database/queries/workspaceagentstats.sql b/coderd/database/queries/workspaceagentstats.sql index daba093a3d9e1..d199f3617acbf 100644 --- a/coderd/database/queries/workspaceagentstats.sql +++ b/coderd/database/queries/workspaceagentstats.sql @@ -90,7 +90,7 @@ ORDER BY date ASC; -- name: DeleteOldWorkspaceAgentStats :exec -DELETE FROM workspace_agent_stats WHERE created_at < NOW() - INTERVAL '30 days'; +DELETE FROM workspace_agent_stats WHERE created_at < NOW() - INTERVAL '6 months'; -- name: GetDeploymentWorkspaceAgentStats :one WITH agent_stats AS ( diff --git a/coderd/insights.go b/coderd/insights.go index 6b2c121033e8c..bb080744c3f7f 100644 --- a/coderd/insights.go +++ b/coderd/insights.go @@ -184,7 +184,7 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) { if !ok { return } - interval, ok := verifyInsightsInterval(ctx, rw, intervalString) + interval, ok := parseInsightsInterval(ctx, rw, intervalString, startTime, endTime) if !ok { return } @@ -198,7 +198,7 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) { eg.SetLimit(4) // The following insights data queries have a theoretical chance to be - // inconsistent between eachother when looking at "today", however, the + // inconsistent between each other when looking at "today", however, the // overhead from a transaction is not worth it. eg.Go(func() error { var err error @@ -207,7 +207,7 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) { StartTime: startTime, EndTime: endTime, TemplateIDs: templateIDs, - IntervalDays: 1, + IntervalDays: interval.Days(), }) if err != nil { return xerrors.Errorf("get template daily insights: %w", err) @@ -531,20 +531,39 @@ func parseInsightsStartAndEndTime(ctx context.Context, rw http.ResponseWriter, s return startTime, endTime, true } -func verifyInsightsInterval(ctx context.Context, rw http.ResponseWriter, intervalString string) (codersdk.InsightsReportInterval, bool) { +func parseInsightsInterval(ctx context.Context, rw http.ResponseWriter, intervalString string, startTime, endTime time.Time) (codersdk.InsightsReportInterval, bool) { switch v := codersdk.InsightsReportInterval(intervalString); v { case codersdk.InsightsReportIntervalDay, "": return v, true + 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 default: httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Query parameter has invalid value.", Validations: []codersdk.ValidationError{ { Field: "interval", - Detail: fmt.Sprintf("must be one of %v", []codersdk.InsightsReportInterval{codersdk.InsightsReportIntervalDay}), + Detail: fmt.Sprintf("must be one of %v", []codersdk.InsightsReportInterval{codersdk.InsightsReportIntervalDay, codersdk.InsightsReportIntervalWeek}), }, }, }) return "", false } } + +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) +} diff --git a/coderd/insights_internal_test.go b/coderd/insights_internal_test.go index e4569b33db9b8..b7db1329090ef 100644 --- a/coderd/insights_internal_test.go +++ b/coderd/insights_internal_test.go @@ -8,6 +8,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/codersdk" ) func Test_parseInsightsStartAndEndTime(t *testing.T) { @@ -148,3 +150,156 @@ func Test_parseInsightsStartAndEndTime(t *testing.T) { }) } } + +func Test_parseInsightsInterval_week(t *testing.T) { + t.Parallel() + + layout := insightsTimeLayout + sydneyLoc, err := time.LoadLocation("Australia/Sydney") // Random location + require.NoError(t, err) + + now := time.Now() + y, m, d := now.Date() + today := time.Date(y, m, d, 0, 0, 0, 0, sydneyLoc) + + thisHour := time.Date(y, m, d, now.Hour(), 0, 0, 0, sydneyLoc) + twoHoursAgo := thisHour.Add(-2 * time.Hour) + thirteenDaysAgo := today.AddDate(0, 0, -13) + + sixDaysAgo := today.AddDate(0, 0, -6) + nineDaysAgo := today.AddDate(0, 0, -9) + + type args struct { + startTime string + endTime string + } + tests := []struct { + name string + args args + wantOk bool + }{ + { + name: "Two full weeks", + args: args{ + startTime: "2023-08-10T00:00:00+02:00", + endTime: "2023-08-24T00:00:00+02:00", + }, + wantOk: true, + }, + { + name: "Two weeks", + args: args{ + startTime: thirteenDaysAgo.Format(layout), + endTime: twoHoursAgo.Format(layout), + }, + wantOk: true, + }, + { + name: "One full week", + args: args{ + startTime: "2023-09-06T00:00:00+02:00", + endTime: "2023-09-13T00:00:00+02:00", + }, + wantOk: true, + }, + { + name: "6 days are acceptable", + args: args{ + startTime: sixDaysAgo.Format(layout), + endTime: thisHour.Format(layout), + }, + wantOk: true, + }, + { + name: "Shorter than a full week", + args: args{ + startTime: "2023-09-08T00:00:00+02:00", + endTime: "2023-09-13T00:00:00+02:00", + }, + wantOk: false, + }, + { + name: "9 days (7 + 2) are not acceptable", + args: args{ + startTime: nineDaysAgo.Format(layout), + endTime: thisHour.Format(layout), + }, + wantOk: false, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + rw := httptest.NewRecorder() + startTime, endTime, ok := parseInsightsStartAndEndTime(context.Background(), rw, tt.args.startTime, tt.args.endTime) + if !ok { + //nolint:bodyclose + t.Log("Status: ", rw.Result().StatusCode) + t.Log("Body: ", rw.Body.String()) + } + require.True(t, ok, "start_time and end_time must be valid") + + parsedInterval, gotOk := parseInsightsInterval(context.Background(), rw, "week", startTime, endTime) + if !assert.Equal(t, tt.wantOk, gotOk) { + //nolint:bodyclose + t.Log("Status: ", rw.Result().StatusCode) + t.Log("Body: ", rw.Body.String()) + } + if tt.wantOk { + assert.Equal(t, codersdk.InsightsReportIntervalWeek, parsedInterval) + } + }) + } +} + +func TestLastReportIntervalHasAtLeastSixDays(t *testing.T) { + t.Parallel() + + loc, err := time.LoadLocation("Europe/Warsaw") + require.NoError(t, err) + + testCases := []struct { + name string + startTime time.Time + endTime time.Time + expected bool + }{ + { + name: "perfectly full week", + startTime: time.Date(2023, time.September, 11, 12, 0, 0, 0, loc), + endTime: time.Date(2023, time.September, 18, 12, 0, 0, 0, loc), + expected: true, + }, + { + name: "exactly 6 days apart", + startTime: time.Date(2023, time.September, 11, 12, 0, 0, 0, loc), + endTime: time.Date(2023, time.September, 17, 12, 0, 0, 0, loc), + expected: true, + }, + { + name: "less than 6 days apart", + startTime: time.Date(2023, time.September, 11, 12, 0, 0, 0, time.UTC), + endTime: time.Date(2023, time.September, 17, 11, 0, 0, 0, time.UTC), + expected: false, + }, + { + name: "forward DST change, 5 days and 23 hours apart", + startTime: time.Date(2023, time.March, 22, 12, 0, 0, 0, loc), // A day before DST starts + endTime: time.Date(2023, time.March, 28, 12, 0, 0, 0, loc), // Exactly 6 "days" apart + expected: true, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + result := lastReportIntervalHasAtLeastSixDays(tc.startTime, tc.endTime) + if result != tc.expected { + t.Errorf("Expected %v, but got %v for start time %v and end time %v", tc.expected, result, tc.startTime, tc.endTime) + } + }) + } +} diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 83498bbb365fd..8be3f4b5e6076 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -810,6 +810,18 @@ func TestTemplateInsights_Golden(t *testing.T) { sessionCountVSCode: 1, sessionCountSSH: 1, }, + { // One hour of usage. + startedAt: frozenWeekAgo.AddDate(0, 0, -12), + endedAt: frozenWeekAgo.AddDate(0, 0, -12).Add(time.Hour), + sessionCountSSH: 1, + sessionCountReconnectingPTY: 1, + }, + { // Another one hour of usage, but "active users" shouldn't be increased twice. + startedAt: frozenWeekAgo.AddDate(0, 0, -10), + endedAt: frozenWeekAgo.AddDate(0, 0, -10).Add(time.Hour), + sessionCountSSH: 1, + sessionCountReconnectingPTY: 1, + }, }, appUsage: []appUsage{ // TODO(mafredri): This doesn't behave correctly right now @@ -922,6 +934,16 @@ func TestTemplateInsights_Golden(t *testing.T) { } }, }, + { + name: "weekly aggregated deployment wide", + makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest { + return codersdk.TemplateInsightsRequest{ + StartTime: frozenWeekAgo.AddDate(0, 0, -3), + EndTime: frozenWeekAgo.AddDate(0, 0, 4), + Interval: codersdk.InsightsReportIntervalWeek, + } + }, + }, { name: "week all templates", makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest { @@ -933,6 +955,17 @@ func TestTemplateInsights_Golden(t *testing.T) { } }, }, + { + name: "weekly aggregated templates", + makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest { + return codersdk.TemplateInsightsRequest{ + TemplateIDs: []uuid.UUID{templates[0].id, templates[1].id, templates[2].id}, + StartTime: frozenWeekAgo.AddDate(0, 0, -1), + EndTime: frozenWeekAgo.AddDate(0, 0, 6), + Interval: codersdk.InsightsReportIntervalWeek, + } + }, + }, { name: "week first template", makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest { @@ -944,6 +977,17 @@ func TestTemplateInsights_Golden(t *testing.T) { } }, }, + { + name: "weekly aggregated first template", + makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest { + return codersdk.TemplateInsightsRequest{ + TemplateIDs: []uuid.UUID{templates[0].id}, + StartTime: frozenWeekAgo, + EndTime: frozenWeekAgo.AddDate(0, 0, 7), + Interval: codersdk.InsightsReportIntervalWeek, + } + }, + }, { name: "week second template", makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest { @@ -955,6 +999,17 @@ func TestTemplateInsights_Golden(t *testing.T) { } }, }, + { + name: "three weeks second template", + makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest { + return codersdk.TemplateInsightsRequest{ + TemplateIDs: []uuid.UUID{templates[1].id}, + StartTime: frozenWeekAgo.AddDate(0, 0, -14), + EndTime: frozenWeekAgo.AddDate(0, 0, 7), + Interval: codersdk.InsightsReportIntervalWeek, + } + }, + }, { name: "week third template", makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest { @@ -1115,6 +1170,13 @@ func TestTemplateInsights_BadRequest(t *testing.T) { Interval: "invalid", }) assert.Error(t, err, "want error for bad interval") + + _, err = client.TemplateInsights(ctx, codersdk.TemplateInsightsRequest{ + StartTime: today.AddDate(0, 0, -5), + EndTime: today, + Interval: codersdk.InsightsReportIntervalWeek, + }) + assert.Error(t, err, "last report interval must have at least 6 days") } func TestTemplateInsights_RBAC(t *testing.T) { diff --git a/coderd/testdata/insights/multiple_users_and_workspaces_three_weeks_second_template.json.golden b/coderd/testdata/insights/multiple_users_and_workspaces_three_weeks_second_template.json.golden new file mode 100644 index 0000000000000..07c3f52607334 --- /dev/null +++ b/coderd/testdata/insights/multiple_users_and_workspaces_three_weeks_second_template.json.golden @@ -0,0 +1,90 @@ +{ + "report": { + "start_time": "2023-08-01T00:00:00Z", + "end_time": "2023-08-22T00:00:00Z", + "template_ids": [ + "00000000-0000-0000-0000-000000000002" + ], + "active_users": 1, + "apps_usage": [ + { + "template_ids": [ + "00000000-0000-0000-0000-000000000002" + ], + "type": "builtin", + "display_name": "Visual Studio Code", + "slug": "vscode", + "icon": "/icon/code.svg", + "seconds": 3600 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000002" + ], + "type": "builtin", + "display_name": "JetBrains", + "slug": "jetbrains", + "icon": "/icon/intellij.svg", + "seconds": 0 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000002" + ], + "type": "builtin", + "display_name": "Web Terminal", + "slug": "reconnecting-pty", + "icon": "/icon/terminal.svg", + "seconds": 7200 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000002" + ], + "type": "builtin", + "display_name": "SSH", + "slug": "ssh", + "icon": "/icon/terminal.svg", + "seconds": 10800 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000002" + ], + "type": "app", + "display_name": "app1", + "slug": "app1", + "icon": "/icon1.png", + "seconds": 21600 + } + ], + "parameters_usage": [] + }, + "interval_reports": [ + { + "start_time": "2023-08-01T00:00:00Z", + "end_time": "2023-08-08T00:00:00Z", + "template_ids": [ + "00000000-0000-0000-0000-000000000002" + ], + "interval": "week", + "active_users": 1 + }, + { + "start_time": "2023-08-08T00:00:00Z", + "end_time": "2023-08-15T00:00:00Z", + "template_ids": [], + "interval": "week", + "active_users": 0 + }, + { + "start_time": "2023-08-15T00:00:00Z", + "end_time": "2023-08-22T00:00:00Z", + "template_ids": [ + "00000000-0000-0000-0000-000000000002" + ], + "interval": "week", + "active_users": 1 + } + ] +} diff --git a/coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden b/coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden new file mode 100644 index 0000000000000..e6f3425f27aa5 --- /dev/null +++ b/coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden @@ -0,0 +1,107 @@ +{ + "report": { + "start_time": "2023-08-12T00:00:00Z", + "end_time": "2023-08-19T00:00:00Z", + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002", + "00000000-0000-0000-0000-000000000003" + ], + "active_users": 3, + "apps_usage": [ + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002", + "00000000-0000-0000-0000-000000000003" + ], + "type": "builtin", + "display_name": "Visual Studio Code", + "slug": "vscode", + "icon": "/icon/code.svg", + "seconds": 7200 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002", + "00000000-0000-0000-0000-000000000003" + ], + "type": "builtin", + "display_name": "JetBrains", + "slug": "jetbrains", + "icon": "/icon/intellij.svg", + "seconds": 120 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002", + "00000000-0000-0000-0000-000000000003" + ], + "type": "builtin", + "display_name": "Web Terminal", + "slug": "reconnecting-pty", + "icon": "/icon/terminal.svg", + "seconds": 3600 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002", + "00000000-0000-0000-0000-000000000003" + ], + "type": "builtin", + "display_name": "SSH", + "slug": "ssh", + "icon": "/icon/terminal.svg", + "seconds": 15120 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002" + ], + "type": "app", + "display_name": "app1", + "slug": "app1", + "icon": "/icon1.png", + "seconds": 25380 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "type": "app", + "display_name": "app3", + "slug": "app3", + "icon": "/icon2.png", + "seconds": 3600 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000003" + ], + "type": "app", + "display_name": "otherapp1", + "slug": "otherapp1", + "icon": "/icon1.png", + "seconds": 300 + } + ], + "parameters_usage": [] + }, + "interval_reports": [ + { + "start_time": "2023-08-12T00:00:00Z", + "end_time": "2023-08-19T00:00:00Z", + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002", + "00000000-0000-0000-0000-000000000003" + ], + "interval": "week", + "active_users": 3 + } + ] +} diff --git a/coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden b/coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden new file mode 100644 index 0000000000000..3c0483f7feb48 --- /dev/null +++ b/coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden @@ -0,0 +1,84 @@ +{ + "report": { + "start_time": "2023-08-15T00:00:00Z", + "end_time": "2023-08-22T00:00:00Z", + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "active_users": 2, + "apps_usage": [ + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "type": "builtin", + "display_name": "Visual Studio Code", + "slug": "vscode", + "icon": "/icon/code.svg", + "seconds": 3600 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "type": "builtin", + "display_name": "JetBrains", + "slug": "jetbrains", + "icon": "/icon/intellij.svg", + "seconds": 120 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "type": "builtin", + "display_name": "Web Terminal", + "slug": "reconnecting-pty", + "icon": "/icon/terminal.svg", + "seconds": 0 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "type": "builtin", + "display_name": "SSH", + "slug": "ssh", + "icon": "/icon/terminal.svg", + "seconds": 7920 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "type": "app", + "display_name": "app1", + "slug": "app1", + "icon": "/icon1.png", + "seconds": 3780 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "type": "app", + "display_name": "app3", + "slug": "app3", + "icon": "/icon2.png", + "seconds": 720 + } + ], + "parameters_usage": [] + }, + "interval_reports": [ + { + "start_time": "2023-08-15T00:00:00Z", + "end_time": "2023-08-22T00:00:00Z", + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "interval": "week", + "active_users": 2 + } + ] +} diff --git a/coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden b/coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden new file mode 100644 index 0000000000000..185a7fe143a2b --- /dev/null +++ b/coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden @@ -0,0 +1,107 @@ +{ + "report": { + "start_time": "2023-08-14T00:00:00Z", + "end_time": "2023-08-21T00:00:00Z", + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002", + "00000000-0000-0000-0000-000000000003" + ], + "active_users": 3, + "apps_usage": [ + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002", + "00000000-0000-0000-0000-000000000003" + ], + "type": "builtin", + "display_name": "Visual Studio Code", + "slug": "vscode", + "icon": "/icon/code.svg", + "seconds": 7200 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002", + "00000000-0000-0000-0000-000000000003" + ], + "type": "builtin", + "display_name": "JetBrains", + "slug": "jetbrains", + "icon": "/icon/intellij.svg", + "seconds": 120 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002", + "00000000-0000-0000-0000-000000000003" + ], + "type": "builtin", + "display_name": "Web Terminal", + "slug": "reconnecting-pty", + "icon": "/icon/terminal.svg", + "seconds": 3600 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002", + "00000000-0000-0000-0000-000000000003" + ], + "type": "builtin", + "display_name": "SSH", + "slug": "ssh", + "icon": "/icon/terminal.svg", + "seconds": 15120 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002" + ], + "type": "app", + "display_name": "app1", + "slug": "app1", + "icon": "/icon1.png", + "seconds": 25380 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "type": "app", + "display_name": "app3", + "slug": "app3", + "icon": "/icon2.png", + "seconds": 3600 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000003" + ], + "type": "app", + "display_name": "otherapp1", + "slug": "otherapp1", + "icon": "/icon1.png", + "seconds": 300 + } + ], + "parameters_usage": [] + }, + "interval_reports": [ + { + "start_time": "2023-08-14T00:00:00Z", + "end_time": "2023-08-21T00:00:00Z", + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002", + "00000000-0000-0000-0000-000000000003" + ], + "interval": "week", + "active_users": 3 + } + ] +} diff --git a/codersdk/insights.go b/codersdk/insights.go index 6780b82d45d43..c9ac0ed2deb79 100644 --- a/codersdk/insights.go +++ b/codersdk/insights.go @@ -20,9 +20,22 @@ const insightsTimeLayout = time.RFC3339 // smaller insights report within a time range. type InsightsReportInterval string +// Days returns the duration of the interval in days. +func (interval InsightsReportInterval) Days() int32 { + switch interval { + case InsightsReportIntervalDay: + return 1 + case InsightsReportIntervalWeek: + return 7 + default: + panic("developer error: unsupported report interval") + } +} + // InsightsReportInterval enums. const ( - InsightsReportIntervalDay InsightsReportInterval = "day" + InsightsReportIntervalDay InsightsReportInterval = "day" + InsightsReportIntervalWeek InsightsReportInterval = "week" ) // UserLatencyInsightsResponse is the response from the user latency insights @@ -109,7 +122,7 @@ type TemplateInsightsIntervalReport struct { StartTime time.Time `json:"start_time" format:"date-time"` EndTime time.Time `json:"end_time" format:"date-time"` TemplateIDs []uuid.UUID `json:"template_ids" format:"uuid"` - Interval InsightsReportInterval `json:"interval"` + Interval InsightsReportInterval `json:"interval" example:"week"` ActiveUsers int64 `json:"active_users" example:"14"` } @@ -155,7 +168,7 @@ type TemplateInsightsRequest struct { StartTime time.Time `json:"start_time" format:"date-time"` EndTime time.Time `json:"end_time" format:"date-time"` TemplateIDs []uuid.UUID `json:"template_ids" format:"uuid"` - Interval InsightsReportInterval `json:"interval"` + Interval InsightsReportInterval `json:"interval" example:"day"` } func (c *Client) TemplateInsights(ctx context.Context, req TemplateInsightsRequest) (TemplateInsightsResponse, error) { diff --git a/docs/api/insights.md b/docs/api/insights.md index 3c421c64173cd..3c2bb2ba9261b 100644 --- a/docs/api/insights.md +++ b/docs/api/insights.md @@ -60,7 +60,7 @@ curl -X GET http://coder-server:8080/api/v2/insights/templates \ { "active_users": 14, "end_time": "2019-08-24T14:15:22Z", - "interval": "day", + "interval": "week", "start_time": "2019-08-24T14:15:22Z", "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"] } diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 5fb5a846cb86f..e2546adcf8ad7 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -3085,9 +3085,10 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in #### Enumerated Values -| Value | -| ----- | -| `day` | +| Value | +| ------ | +| `day` | +| `week` | ## codersdk.IssueReconnectingPTYSignedTokenRequest @@ -4421,7 +4422,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in { "active_users": 14, "end_time": "2019-08-24T14:15:22Z", - "interval": "day", + "interval": "week", "start_time": "2019-08-24T14:15:22Z", "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"] } @@ -4500,7 +4501,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in { "active_users": 14, "end_time": "2019-08-24T14:15:22Z", - "interval": "day", + "interval": "week", "start_time": "2019-08-24T14:15:22Z", "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"] } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index a1b1c9e29f8d1..cd272eb99b12c 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1654,8 +1654,11 @@ export type GroupSource = "oidc" | "user"; export const GroupSources: GroupSource[] = ["oidc", "user"]; // From codersdk/insights.go -export type InsightsReportInterval = "day"; -export const InsightsReportIntervals: InsightsReportInterval[] = ["day"]; +export type InsightsReportInterval = "day" | "week"; +export const InsightsReportIntervals: InsightsReportInterval[] = [ + "day", + "week", +]; // From codersdk/provisionerdaemons.go export type JobErrorCode = "REQUIRED_TEMPLATE_VARIABLES";