From f9e2cbebf3120f046d84f2ba6dbfc16ace87e988 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 14 Sep 2023 17:15:14 +0200 Subject: [PATCH 01/13] codersdk week --- coderd/insights.go | 4 ++-- codersdk/insights.go | 7 ++++--- site/src/api/typesGenerated.ts | 7 +++++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/coderd/insights.go b/coderd/insights.go index e19f95d40dc0c..5e80f7a5dcd14 100644 --- a/coderd/insights.go +++ b/coderd/insights.go @@ -532,7 +532,7 @@ func parseInsightsStartAndEndTime(ctx context.Context, rw http.ResponseWriter, s func verifyInsightsInterval(ctx context.Context, rw http.ResponseWriter, intervalString string) (codersdk.InsightsReportInterval, bool) { switch v := codersdk.InsightsReportInterval(intervalString); v { - case codersdk.InsightsReportIntervalDay, "": + case codersdk.InsightsReportIntervalDay, codersdk.InsightsReportIntervalWeek, "": return v, true default: httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ @@ -540,7 +540,7 @@ func verifyInsightsInterval(ctx context.Context, rw http.ResponseWriter, interva 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}), }, }, }) diff --git a/codersdk/insights.go b/codersdk/insights.go index 6780b82d45d43..2ed9f290b3c47 100644 --- a/codersdk/insights.go +++ b/codersdk/insights.go @@ -22,7 +22,8 @@ type InsightsReportInterval string // InsightsReportInterval enums. const ( - InsightsReportIntervalDay InsightsReportInterval = "day" + InsightsReportIntervalDay InsightsReportInterval = "day" + InsightsReportIntervalWeek InsightsReportInterval = "week" ) // UserLatencyInsightsResponse is the response from the user latency insights @@ -109,7 +110,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 +156,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/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"; From 18b718c28e222cf4846ff805e47d1621e01b427c Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 14 Sep 2023 17:23:26 +0200 Subject: [PATCH 02/13] make gen --- coderd/apidoc/docs.go | 13 ++++++++++--- coderd/apidoc/swagger.json | 14 +++++++++++--- docs/api/insights.md | 2 +- docs/api/schemas.md | 11 ++++++----- 4 files changed, 28 insertions(+), 12 deletions(-) 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/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"] } From 45bb69c465ce9112786a286ff1b567d131148f03 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 15 Sep 2023 14:59:50 +0200 Subject: [PATCH 03/13] fix parsing request --- coderd/insights.go | 26 +++++++++++++++++++++----- codersdk/insights.go | 12 ++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/coderd/insights.go b/coderd/insights.go index c184faf860c47..6e03773b2bee2 100644 --- a/coderd/insights.go +++ b/coderd/insights.go @@ -23,6 +23,9 @@ import ( // Duplicated in codersdk. const insightsTimeLayout = time.RFC3339 +// Day duration in nanoseconds +var dayNanoseconds = 24 * time.Hour.Nanoseconds() + // @Summary Get deployment DAUs // @ID get-deployment-daus // @Security CoderSessionToken @@ -184,7 +187,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 +201,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 +210,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,9 +534,18 @@ 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, codersdk.InsightsReportIntervalWeek, "": + case codersdk.InsightsReportIntervalDay, "": + return v, true + case codersdk.InsightsReportIntervalWeek: + if !isMultipleOfDay(startTime, endTime) { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Query parameter has invalid value.", + Detail: "Duration between start_time and end_time must multiple of 1 day.", + }) + return "", false + } return v, true default: httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ @@ -548,3 +560,7 @@ func verifyInsightsInterval(ctx context.Context, rw http.ResponseWriter, interva return "", false } } + +func isMultipleOfDay(startTime, endTime time.Time) bool { + return endTime.Sub(startTime).Nanoseconds()%dayNanoseconds == 0 +} diff --git a/codersdk/insights.go b/codersdk/insights.go index 2ed9f290b3c47..c9ac0ed2deb79 100644 --- a/codersdk/insights.go +++ b/codersdk/insights.go @@ -20,6 +20,18 @@ 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" From cee514d51e9f39a4f18ab08f1064ab360cf8dff0 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 15 Sep 2023 15:22:50 +0200 Subject: [PATCH 04/13] test: BadRequest --- coderd/insights.go | 12 ++++++------ coderd/insights_test.go | 7 +++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/coderd/insights.go b/coderd/insights.go index 6e03773b2bee2..4336247b04c14 100644 --- a/coderd/insights.go +++ b/coderd/insights.go @@ -23,8 +23,8 @@ import ( // Duplicated in codersdk. const insightsTimeLayout = time.RFC3339 -// Day duration in nanoseconds -var dayNanoseconds = 24 * time.Hour.Nanoseconds() +// Week duration in nanoseconds +var weekNanoseconds = 7 * 24 * time.Hour.Nanoseconds() // @Summary Get deployment DAUs // @ID get-deployment-daus @@ -539,10 +539,10 @@ func parseInsightsInterval(ctx context.Context, rw http.ResponseWriter, interval case codersdk.InsightsReportIntervalDay, "": return v, true case codersdk.InsightsReportIntervalWeek: - if !isMultipleOfDay(startTime, endTime) { + if !isMultipleOfWeek(startTime, endTime) { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Query parameter has invalid value.", - Detail: "Duration between start_time and end_time must multiple of 1 day.", + Detail: "Duration between start_time and end_time must multiple of 7 days.", }) return "", false } @@ -561,6 +561,6 @@ func parseInsightsInterval(ctx context.Context, rw http.ResponseWriter, interval } } -func isMultipleOfDay(startTime, endTime time.Time) bool { - return endTime.Sub(startTime).Nanoseconds()%dayNanoseconds == 0 +func isMultipleOfWeek(startTime, endTime time.Time) bool { + return endTime.Sub(startTime).Nanoseconds()%weekNanoseconds == 0 } diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 83498bbb365fd..1a2a2a19fc5ec 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -1115,6 +1115,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, -4), + EndTime: today, + Interval: codersdk.InsightsReportIntervalWeek, + }) + assert.Error(t, err, "start_time and end_time must be multiple of 7 days") } func TestTemplateInsights_RBAC(t *testing.T) { From 16ec5ad4280bb15474185640dac035fb6d49d711 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 15 Sep 2023 15:25:50 +0200 Subject: [PATCH 05/13] Delete stats after 6 months --- coderd/database/queries.sql.go | 2 +- coderd/database/queries/workspaceagentstats.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index ffc468eb63862..69fc1cf0be200 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -7490,7 +7490,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 ( From 01afb9bf4aa80a47dca0d956ea2ee8e02ff49958 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 15 Sep 2023 16:04:00 +0200 Subject: [PATCH 06/13] golden files --- coderd/insights_test.go | 43 +++++++ ...es_three_weeks_second_template.json.golden | 88 ++++++++++++++ ...kly_aggregated_deployment_wide.json.golden | 107 ++++++++++++++++++ ...ekly_aggregated_first_template.json.golden | 84 ++++++++++++++ ...es_weekly_aggregated_templates.json.golden | 107 ++++++++++++++++++ 5 files changed, 429 insertions(+) create mode 100644 coderd/testdata/insights/multiple_users_and_workspaces_three_weeks_second_template.json.golden create mode 100644 coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden create mode 100644 coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden create mode 100644 coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 1a2a2a19fc5ec..429f793133458 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -922,6 +922,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 +943,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 +965,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 +987,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 { 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..0c8440ebefc83 --- /dev/null +++ b/coderd/testdata/insights/multiple_users_and_workspaces_three_weeks_second_template.json.golden @@ -0,0 +1,88 @@ +{ + "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": 0 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000002" + ], + "type": "builtin", + "display_name": "SSH", + "slug": "ssh", + "icon": "/icon/terminal.svg", + "seconds": 3600 + }, + { + "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": [], + "interval": "week", + "active_users": 0 + }, + { + "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 + } + ] +} From 60e8db396ba543efbdf66a00c91340e467a60148 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 15 Sep 2023 16:49:23 +0200 Subject: [PATCH 07/13] At least 6 days --- coderd/insights.go | 15 ++++++++------- coderd/insights_test.go | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/coderd/insights.go b/coderd/insights.go index 4336247b04c14..b68f3e85a1bf2 100644 --- a/coderd/insights.go +++ b/coderd/insights.go @@ -23,9 +23,6 @@ import ( // Duplicated in codersdk. const insightsTimeLayout = time.RFC3339 -// Week duration in nanoseconds -var weekNanoseconds = 7 * 24 * time.Hour.Nanoseconds() - // @Summary Get deployment DAUs // @ID get-deployment-daus // @Security CoderSessionToken @@ -539,10 +536,10 @@ func parseInsightsInterval(ctx context.Context, rw http.ResponseWriter, interval case codersdk.InsightsReportIntervalDay, "": return v, true case codersdk.InsightsReportIntervalWeek: - if !isMultipleOfWeek(startTime, endTime) { + if !lastReportIntervalHasAtLeastSixDays(startTime, endTime) { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Query parameter has invalid value.", - Detail: "Duration between start_time and end_time must multiple of 7 days.", + Detail: "Last report interval should have at least 6 days.", }) return "", false } @@ -561,6 +558,10 @@ func parseInsightsInterval(ctx context.Context, rw http.ResponseWriter, interval } } -func isMultipleOfWeek(startTime, endTime time.Time) bool { - return endTime.Sub(startTime).Nanoseconds()%weekNanoseconds == 0 +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! + } + return lastReportIntervalDays >= 6*24*time.Hour } diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 429f793133458..a3ffd75b3b5c8 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -1160,11 +1160,11 @@ func TestTemplateInsights_BadRequest(t *testing.T) { assert.Error(t, err, "want error for bad interval") _, err = client.TemplateInsights(ctx, codersdk.TemplateInsightsRequest{ - StartTime: today.AddDate(0, 0, -4), + StartTime: today.AddDate(0, 0, -5), EndTime: today, Interval: codersdk.InsightsReportIntervalWeek, }) - assert.Error(t, err, "start_time and end_time must be multiple of 7 days") + assert.Error(t, err, "last report interval must have at least 6 days") } func TestTemplateInsights_RBAC(t *testing.T) { From ff38fe4fc88a9f3fe7950ac047c8e2c0a600d4a6 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 15 Sep 2023 17:34:24 +0200 Subject: [PATCH 08/13] internal tests --- coderd/insights_internal_test.go | 109 +++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/coderd/insights_internal_test.go b/coderd/insights_internal_test.go index e4569b33db9b8..cc93818e8860f 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,110 @@ func Test_parseInsightsStartAndEndTime(t *testing.T) { }) } } + +func Test_parseInsightsInterval_week(t *testing.T) { + t.Parallel() + + layout := insightsTimeLayout + now := time.Now().UTC() + y, m, d := now.Date() + today := time.Date(y, m, d, 0, 0, 0, 0, time.UTC) + + thisHour := time.Date(y, m, d, now.Hour(), 0, 0, 0, time.UTC) + 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 + wantInterval codersdk.InsightsReportInterval + wantOk bool + }{ + { + name: "Two full weeks", + args: args{ + startTime: "2023-08-10T00:00:00+02:00", + endTime: "2023-08-24T00:00:00+02:00", + }, + wantInterval: codersdk.InsightsReportIntervalWeek, + wantOk: true, + }, + { + name: "Two weeks", + args: args{ + startTime: thirteenDaysAgo.Format(layout), + endTime: twoHoursAgo.Format(layout), + }, + wantInterval: codersdk.InsightsReportIntervalWeek, + wantOk: true, + }, + { + name: "One full week", + args: args{ + startTime: "2023-09-06T00:00:00+02:00", + endTime: "2023-09-13T00:00:00+02:00", + }, + wantInterval: codersdk.InsightsReportIntervalWeek, + wantOk: true, + }, + { + name: "6 days are acceptable", + args: args{ + startTime: sixDaysAgo.Format(layout), + endTime: thisHour.Format(layout), + }, + wantInterval: codersdk.InsightsReportIntervalWeek, + 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", + }, + wantInterval: codersdk.InsightsReportIntervalWeek, + wantOk: false, + }, + { + name: "9 days (7 + 2) are not acceptable", + args: args{ + startTime: nineDaysAgo.Format(layout), + endTime: thisHour.Format(layout), + }, + wantInterval: codersdk.InsightsReportIntervalWeek, + 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) + } + }) + } +} From 873ddcd0a404b31b4d9b10ebca14b564d8078123 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 18 Sep 2023 12:38:07 +0200 Subject: [PATCH 09/13] Remove want --- coderd/insights_internal_test.go | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/coderd/insights_internal_test.go b/coderd/insights_internal_test.go index cc93818e8860f..eb21b1a500675 100644 --- a/coderd/insights_internal_test.go +++ b/coderd/insights_internal_test.go @@ -171,10 +171,9 @@ func Test_parseInsightsInterval_week(t *testing.T) { endTime string } tests := []struct { - name string - args args - wantInterval codersdk.InsightsReportInterval - wantOk bool + name string + args args + wantOk bool }{ { name: "Two full weeks", @@ -182,8 +181,7 @@ func Test_parseInsightsInterval_week(t *testing.T) { startTime: "2023-08-10T00:00:00+02:00", endTime: "2023-08-24T00:00:00+02:00", }, - wantInterval: codersdk.InsightsReportIntervalWeek, - wantOk: true, + wantOk: true, }, { name: "Two weeks", @@ -191,8 +189,7 @@ func Test_parseInsightsInterval_week(t *testing.T) { startTime: thirteenDaysAgo.Format(layout), endTime: twoHoursAgo.Format(layout), }, - wantInterval: codersdk.InsightsReportIntervalWeek, - wantOk: true, + wantOk: true, }, { name: "One full week", @@ -200,8 +197,7 @@ func Test_parseInsightsInterval_week(t *testing.T) { startTime: "2023-09-06T00:00:00+02:00", endTime: "2023-09-13T00:00:00+02:00", }, - wantInterval: codersdk.InsightsReportIntervalWeek, - wantOk: true, + wantOk: true, }, { name: "6 days are acceptable", @@ -209,8 +205,7 @@ func Test_parseInsightsInterval_week(t *testing.T) { startTime: sixDaysAgo.Format(layout), endTime: thisHour.Format(layout), }, - wantInterval: codersdk.InsightsReportIntervalWeek, - wantOk: true, + wantOk: true, }, { name: "Shorter than a full week", @@ -218,8 +213,7 @@ func Test_parseInsightsInterval_week(t *testing.T) { startTime: "2023-09-08T00:00:00+02:00", endTime: "2023-09-13T00:00:00+02:00", }, - wantInterval: codersdk.InsightsReportIntervalWeek, - wantOk: false, + wantOk: false, }, { name: "9 days (7 + 2) are not acceptable", @@ -227,8 +221,7 @@ func Test_parseInsightsInterval_week(t *testing.T) { startTime: nineDaysAgo.Format(layout), endTime: thisHour.Format(layout), }, - wantInterval: codersdk.InsightsReportIntervalWeek, - wantOk: false, + wantOk: false, }, } for _, tt := range tests { From fa1ad6219e659ac6ce6ecd76aa1949a439467ad1 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 18 Sep 2023 13:00:48 +0200 Subject: [PATCH 10/13] Sydney location --- coderd/insights_internal_test.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/coderd/insights_internal_test.go b/coderd/insights_internal_test.go index eb21b1a500675..1349ed20323e7 100644 --- a/coderd/insights_internal_test.go +++ b/coderd/insights_internal_test.go @@ -155,11 +155,14 @@ func Test_parseInsightsInterval_week(t *testing.T) { t.Parallel() layout := insightsTimeLayout - now := time.Now().UTC() + 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, time.UTC) + today := time.Date(y, m, d, 0, 0, 0, 0, sydneyLoc) - thisHour := time.Date(y, m, d, now.Hour(), 0, 0, 0, time.UTC) + thisHour := time.Date(y, m, d, now.Hour(), 0, 0, 0, sydneyLoc) twoHoursAgo := thisHour.Add(-2 * time.Hour) thirteenDaysAgo := today.AddDate(0, 0, -13) From ecd27f4cdd2f90663e819dc6461a6e20353ab60b Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 18 Sep 2023 13:23:37 +0200 Subject: [PATCH 11/13] more activity --- coderd/insights_test.go | 12 ++++++++++++ ...orkspaces_three_weeks_second_template.json.golden | 10 ++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/coderd/insights_test.go b/coderd/insights_test.go index a3ffd75b3b5c8..a9575c99c891b 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: 0, + }, + { // 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: 0, + sessionCountReconnectingPTY: 1, + }, }, appUsage: []appUsage{ // TODO(mafredri): This doesn't behave correctly right now 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 index 0c8440ebefc83..b59dad7cd92dc 100644 --- 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 @@ -35,7 +35,7 @@ "display_name": "Web Terminal", "slug": "reconnecting-pty", "icon": "/icon/terminal.svg", - "seconds": 0 + "seconds": 3600 }, { "template_ids": [ @@ -45,7 +45,7 @@ "display_name": "SSH", "slug": "ssh", "icon": "/icon/terminal.svg", - "seconds": 3600 + "seconds": 7200 }, { "template_ids": [ @@ -64,9 +64,11 @@ { "start_time": "2023-08-01T00:00:00Z", "end_time": "2023-08-08T00:00:00Z", - "template_ids": [], + "template_ids": [ + "00000000-0000-0000-0000-000000000002" + ], "interval": "week", - "active_users": 0 + "active_users": 1 }, { "start_time": "2023-08-08T00:00:00Z", From 01a1ad8d8be9a5082c821cfda7077ad5203b79df Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 18 Sep 2023 14:03:59 +0200 Subject: [PATCH 12/13] fix six days --- coderd/insights.go | 4 +++- coderd/insights_test.go | 4 ++-- ...ers_and_workspaces_three_weeks_second_template.json.golden | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/coderd/insights.go b/coderd/insights.go index b68f3e85a1bf2..bb080744c3f7f 100644 --- a/coderd/insights.go +++ b/coderd/insights.go @@ -563,5 +563,7 @@ func lastReportIntervalHasAtLeastSixDays(startTime, endTime time.Time) bool { if lastReportIntervalDays == 0 { return true // this is a perfectly full week! } - return lastReportIntervalDays >= 6*24*time.Hour + // 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_test.go b/coderd/insights_test.go index a9575c99c891b..8be3f4b5e6076 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -814,12 +814,12 @@ func TestTemplateInsights_Golden(t *testing.T) { startedAt: frozenWeekAgo.AddDate(0, 0, -12), endedAt: frozenWeekAgo.AddDate(0, 0, -12).Add(time.Hour), sessionCountSSH: 1, - sessionCountReconnectingPTY: 0, + 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: 0, + sessionCountSSH: 1, sessionCountReconnectingPTY: 1, }, }, 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 index b59dad7cd92dc..07c3f52607334 100644 --- 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 @@ -35,7 +35,7 @@ "display_name": "Web Terminal", "slug": "reconnecting-pty", "icon": "/icon/terminal.svg", - "seconds": 3600 + "seconds": 7200 }, { "template_ids": [ @@ -45,7 +45,7 @@ "display_name": "SSH", "slug": "ssh", "icon": "/icon/terminal.svg", - "seconds": 7200 + "seconds": 10800 }, { "template_ids": [ From 7e14a1ffd5f9dd05bb380bec6409849a7d971f0e Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 19 Sep 2023 11:57:46 +0200 Subject: [PATCH 13/13] test: dst vs. 6 days --- coderd/insights_internal_test.go | 50 ++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/coderd/insights_internal_test.go b/coderd/insights_internal_test.go index 1349ed20323e7..b7db1329090ef 100644 --- a/coderd/insights_internal_test.go +++ b/coderd/insights_internal_test.go @@ -253,3 +253,53 @@ func Test_parseInsightsInterval_week(t *testing.T) { }) } } + +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) + } + }) + } +}