From f7f5d6652eb6bb70466e08a19bc705819794c310 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 23 May 2023 13:01:26 +0200 Subject: [PATCH 01/18] chore: Add timezone param to DAU SQL query Previously only worked for UTC timezone --- coderd/database/dbauthz/system.go | 8 ++++---- coderd/database/dbfake/databasefake.go | 10 +++++----- coderd/database/dbmock/store.go | 10 +++++----- coderd/database/querier.go | 4 ++-- coderd/database/queries.sql.go | 17 +++++++++++------ coderd/database/queries/workspaceagentstats.sql | 4 ++-- coderd/metricscache/metricscache.go | 7 +++++-- 7 files changed, 34 insertions(+), 26 deletions(-) diff --git a/coderd/database/dbauthz/system.go b/coderd/database/dbauthz/system.go index 1252788f375ce..f8e504395bc3c 100644 --- a/coderd/database/dbauthz/system.go +++ b/coderd/database/dbauthz/system.go @@ -205,19 +205,19 @@ func (q *querier) GetTemplateAverageBuildTime(ctx context.Context, arg database. } // Only used by metrics cache. -func (q *querier) GetTemplateDAUs(ctx context.Context, templateID uuid.UUID) ([]database.GetTemplateDAUsRow, error) { +func (q *querier) GetTemplateDAUs(ctx context.Context, arg database.GetTemplateDAUsParams) ([]database.GetTemplateDAUsRow, error) { if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } - return q.db.GetTemplateDAUs(ctx, templateID) + return q.db.GetTemplateDAUs(ctx, arg) } // Only used by metrics cache. -func (q *querier) GetDeploymentDAUs(ctx context.Context) ([]database.GetDeploymentDAUsRow, error) { +func (q *querier) GetDeploymentDAUs(ctx context.Context, tzOffset int32) ([]database.GetDeploymentDAUsRow, error) { if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil { return nil, err } - return q.db.GetDeploymentDAUs(ctx) + return q.db.GetDeploymentDAUs(ctx, tzOffset) } // UpdateWorkspaceBuildCostByID is used by the provisioning system to update the cost of a workspace build. diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index 41800dbcd9e74..2021c8bcfbe87 100644 --- a/coderd/database/dbfake/databasefake.go +++ b/coderd/database/dbfake/databasefake.go @@ -434,21 +434,21 @@ func (q *fakeQuerier) InsertWorkspaceAgentStat(_ context.Context, p database.Ins return stat, nil } -func (q *fakeQuerier) GetTemplateDAUs(_ context.Context, templateID uuid.UUID) ([]database.GetTemplateDAUsRow, error) { +func (q *fakeQuerier) GetTemplateDAUs(_ context.Context, arg database.GetTemplateDAUsParams) ([]database.GetTemplateDAUsRow, error) { q.mutex.RLock() defer q.mutex.RUnlock() seens := make(map[time.Time]map[uuid.UUID]struct{}) for _, as := range q.workspaceAgentStats { - if as.TemplateID != templateID { + if as.TemplateID != arg.TemplateID { continue } if as.ConnectionCount == 0 { continue } - date := as.CreatedAt.Truncate(time.Hour * 24) + date := as.CreatedAt.UTC().Add(time.Duration(arg.TzOffset) * time.Hour).Truncate(time.Hour * 24) dateEntry := seens[date] if dateEntry == nil { @@ -477,7 +477,7 @@ func (q *fakeQuerier) GetTemplateDAUs(_ context.Context, templateID uuid.UUID) ( return rs, nil } -func (q *fakeQuerier) GetDeploymentDAUs(_ context.Context) ([]database.GetDeploymentDAUsRow, error) { +func (q *fakeQuerier) GetDeploymentDAUs(_ context.Context, tzOffset int32) ([]database.GetDeploymentDAUsRow, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -487,7 +487,7 @@ func (q *fakeQuerier) GetDeploymentDAUs(_ context.Context) ([]database.GetDeploy if as.ConnectionCount == 0 { continue } - date := as.CreatedAt.Truncate(time.Hour * 24) + date := as.CreatedAt.UTC().Add(time.Duration(tzOffset) * time.Hour).Truncate(time.Hour * 24) dateEntry := seens[date] if dateEntry == nil { diff --git a/coderd/database/dbmock/store.go b/coderd/database/dbmock/store.go index 5fe83e6f514d6..a145f24802c07 100644 --- a/coderd/database/dbmock/store.go +++ b/coderd/database/dbmock/store.go @@ -433,18 +433,18 @@ func (mr *MockStoreMockRecorder) GetDERPMeshKey(arg0 interface{}) *gomock.Call { } // GetDeploymentDAUs mocks base method. -func (m *MockStore) GetDeploymentDAUs(arg0 context.Context) ([]database.GetDeploymentDAUsRow, error) { +func (m *MockStore) GetDeploymentDAUs(arg0 context.Context, arg1 int32) ([]database.GetDeploymentDAUsRow, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDeploymentDAUs", arg0) + ret := m.ctrl.Call(m, "GetDeploymentDAUs", arg0, arg1) ret0, _ := ret[0].([]database.GetDeploymentDAUsRow) ret1, _ := ret[1].(error) return ret0, ret1 } // GetDeploymentDAUs indicates an expected call of GetDeploymentDAUs. -func (mr *MockStoreMockRecorder) GetDeploymentDAUs(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetDeploymentDAUs(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeploymentDAUs", reflect.TypeOf((*MockStore)(nil).GetDeploymentDAUs), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeploymentDAUs", reflect.TypeOf((*MockStore)(nil).GetDeploymentDAUs), arg0, arg1) } // GetDeploymentID mocks base method. @@ -1093,7 +1093,7 @@ func (mr *MockStoreMockRecorder) GetTemplateByOrganizationAndName(arg0, arg1 int } // GetTemplateDAUs mocks base method. -func (m *MockStore) GetTemplateDAUs(arg0 context.Context, arg1 uuid.UUID) ([]database.GetTemplateDAUsRow, error) { +func (m *MockStore) GetTemplateDAUs(arg0 context.Context, arg1 database.GetTemplateDAUsParams) ([]database.GetTemplateDAUsRow, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetTemplateDAUs", arg0, arg1) ret0, _ := ret[0].([]database.GetTemplateDAUsRow) diff --git a/coderd/database/querier.go b/coderd/database/querier.go index e30207c7ba44e..26978d240623e 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -55,7 +55,7 @@ type sqlcQuerier interface { // are included. GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error) GetDERPMeshKey(ctx context.Context) (string, error) - GetDeploymentDAUs(ctx context.Context) ([]GetDeploymentDAUsRow, error) + GetDeploymentDAUs(ctx context.Context, tzOffset int32) ([]GetDeploymentDAUsRow, error) GetDeploymentID(ctx context.Context) (string, error) GetDeploymentWorkspaceAgentStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentStatsRow, error) GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploymentWorkspaceStatsRow, error) @@ -101,7 +101,7 @@ type sqlcQuerier interface { GetTemplateAverageBuildTime(ctx context.Context, arg GetTemplateAverageBuildTimeParams) (GetTemplateAverageBuildTimeRow, error) GetTemplateByID(ctx context.Context, id uuid.UUID) (Template, error) GetTemplateByOrganizationAndName(ctx context.Context, arg GetTemplateByOrganizationAndNameParams) (Template, error) - GetTemplateDAUs(ctx context.Context, templateID uuid.UUID) ([]GetTemplateDAUsRow, error) + GetTemplateDAUs(ctx context.Context, arg GetTemplateDAUsParams) ([]GetTemplateDAUsRow, error) GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (TemplateVersion, error) GetTemplateVersionByJobID(ctx context.Context, jobID uuid.UUID) (TemplateVersion, error) GetTemplateVersionByTemplateIDAndName(ctx context.Context, arg GetTemplateVersionByTemplateIDAndNameParams) (TemplateVersion, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 9955deb439ffd..358c5af7934a8 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6335,7 +6335,7 @@ func (q *sqlQuerier) DeleteOldWorkspaceAgentStats(ctx context.Context) error { const getDeploymentDAUs = `-- name: GetDeploymentDAUs :many SELECT - (created_at at TIME ZONE 'UTC')::date as date, + (created_at at TIME ZONE cast($1::integer as text))::date as date, user_id FROM workspace_agent_stats @@ -6352,8 +6352,8 @@ type GetDeploymentDAUsRow struct { UserID uuid.UUID `db:"user_id" json:"user_id"` } -func (q *sqlQuerier) GetDeploymentDAUs(ctx context.Context) ([]GetDeploymentDAUsRow, error) { - rows, err := q.db.QueryContext(ctx, getDeploymentDAUs) +func (q *sqlQuerier) GetDeploymentDAUs(ctx context.Context, tzOffset int32) ([]GetDeploymentDAUsRow, error) { + rows, err := q.db.QueryContext(ctx, getDeploymentDAUs, tzOffset) if err != nil { return nil, err } @@ -6428,7 +6428,7 @@ func (q *sqlQuerier) GetDeploymentWorkspaceAgentStats(ctx context.Context, creat const getTemplateDAUs = `-- name: GetTemplateDAUs :many SELECT - (created_at at TIME ZONE 'UTC')::date as date, + (created_at at TIME ZONE cast($2::integer as text))::date as date, user_id FROM workspace_agent_stats @@ -6441,13 +6441,18 @@ ORDER BY date ASC ` +type GetTemplateDAUsParams struct { + TemplateID uuid.UUID `db:"template_id" json:"template_id"` + TzOffset int32 `db:"tz_offset" json:"tz_offset"` +} + type GetTemplateDAUsRow struct { Date time.Time `db:"date" json:"date"` UserID uuid.UUID `db:"user_id" json:"user_id"` } -func (q *sqlQuerier) GetTemplateDAUs(ctx context.Context, templateID uuid.UUID) ([]GetTemplateDAUsRow, error) { - rows, err := q.db.QueryContext(ctx, getTemplateDAUs, templateID) +func (q *sqlQuerier) GetTemplateDAUs(ctx context.Context, arg GetTemplateDAUsParams) ([]GetTemplateDAUsRow, error) { + rows, err := q.db.QueryContext(ctx, getTemplateDAUs, arg.TemplateID, arg.TzOffset) if err != nil { return nil, err } diff --git a/coderd/database/queries/workspaceagentstats.sql b/coderd/database/queries/workspaceagentstats.sql index 4432fbcdaf663..1a598bd6a6263 100644 --- a/coderd/database/queries/workspaceagentstats.sql +++ b/coderd/database/queries/workspaceagentstats.sql @@ -24,7 +24,7 @@ VALUES -- name: GetTemplateDAUs :many SELECT - (created_at at TIME ZONE 'UTC')::date as date, + (created_at at TIME ZONE cast(@tz_offset::integer as text))::date as date, user_id FROM workspace_agent_stats @@ -38,7 +38,7 @@ ORDER BY -- name: GetDeploymentDAUs :many SELECT - (created_at at TIME ZONE 'UTC')::date as date, + (created_at at TIME ZONE cast(@tz_offset::integer as text))::date as date, user_id FROM workspace_agent_stats diff --git a/coderd/metricscache/metricscache.go b/coderd/metricscache/metricscache.go index 7d8ce52ffd452..4160625c05a17 100644 --- a/coderd/metricscache/metricscache.go +++ b/coderd/metricscache/metricscache.go @@ -180,7 +180,7 @@ func (c *Cache) refreshTemplateDAUs(ctx context.Context) error { templateAverageBuildTimes = make(map[uuid.UUID]database.GetTemplateAverageBuildTimeRow) ) - rows, err := c.database.GetDeploymentDAUs(ctx) + rows, err := c.database.GetDeploymentDAUs(ctx, 0) if err != nil { return err } @@ -188,7 +188,10 @@ func (c *Cache) refreshTemplateDAUs(ctx context.Context) error { c.deploymentDAUResponses.Store(&deploymentDAUs) for _, template := range templates { - rows, err := c.database.GetTemplateDAUs(ctx, template.ID) + rows, err := c.database.GetTemplateDAUs(ctx, database.GetTemplateDAUsParams{ + TemplateID: template.ID, + TzOffset: 0, + }) if err != nil { return err } From 45e8342e0eaea4042a47c605f0cf0526588a01c4 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 23 May 2023 13:13:05 +0200 Subject: [PATCH 02/18] Merge DAUs response --- coderd/insights.go | 4 +- coderd/insights_test.go | 2 +- coderd/metricscache/metricscache.go | 57 +++++++++++------------------ coderd/templates.go | 4 +- coderd/templates_test.go | 4 +- codersdk/deployment.go | 11 ++++-- codersdk/templates.go | 14 +------ 7 files changed, 39 insertions(+), 57 deletions(-) diff --git a/coderd/insights.go b/coderd/insights.go index 79cf14210ec6e..c2466fc1e4055 100644 --- a/coderd/insights.go +++ b/coderd/insights.go @@ -13,7 +13,7 @@ import ( // @Security CoderSessionToken // @Produce json // @Tags Insights -// @Success 200 {object} codersdk.DeploymentDAUsResponse +// @Success 200 {object} codersdk.DAUsResponse // @Router /insights/daus [get] func (api *API) deploymentDAUs(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -24,7 +24,7 @@ func (api *API) deploymentDAUs(rw http.ResponseWriter, r *http.Request) { resp, _ := api.metricsCache.DeploymentDAUs() if resp == nil || resp.Entries == nil { - httpapi.Write(ctx, rw, http.StatusOK, &codersdk.DeploymentDAUsResponse{ + httpapi.Write(ctx, rw, http.StatusOK, &codersdk.DAUsResponse{ Entries: []codersdk.DAUEntry{}, }) return diff --git a/coderd/insights_test.go b/coderd/insights_test.go index ada90a916253a..faf9d6d092f9c 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -74,7 +74,7 @@ func TestDeploymentInsights(t *testing.T) { require.NoError(t, err) _ = sshConn.Close() - wantDAUs := &codersdk.DeploymentDAUsResponse{ + wantDAUs := &codersdk.DAUsResponse{ Entries: []codersdk.DAUEntry{ { Date: time.Now().UTC().Truncate(time.Hour * 24), diff --git a/coderd/metricscache/metricscache.go b/coderd/metricscache/metricscache.go index 4160625c05a17..d0f8c8a3bc78a 100644 --- a/coderd/metricscache/metricscache.go +++ b/coderd/metricscache/metricscache.go @@ -3,6 +3,7 @@ package metricscache import ( "context" "database/sql" + "fmt" "sync" "sync/atomic" "time" @@ -29,8 +30,8 @@ type Cache struct { log slog.Logger intervals Intervals - deploymentDAUResponses atomic.Pointer[codersdk.DeploymentDAUsResponse] - templateDAUResponses atomic.Pointer[map[uuid.UUID]codersdk.TemplateDAUsResponse] + deploymentDAUResponses atomic.Pointer[codersdk.DAUsResponse] + templateDAUResponses atomic.Pointer[map[uuid.UUID]codersdk.DAUsResponse] templateUniqueUsers atomic.Pointer[map[uuid.UUID]int] templateAverageBuildTime atomic.Pointer[map[uuid.UUID]database.GetTemplateAverageBuildTimeRow] deploymentStatsResponse atomic.Pointer[codersdk.DeploymentStats] @@ -107,37 +108,23 @@ func fillEmptyDays(sortedDates []time.Time) []time.Time { return newDates } -func convertDAUResponse(rows []database.GetTemplateDAUsRow) codersdk.TemplateDAUsResponse { - respMap := make(map[time.Time][]uuid.UUID) - for _, row := range rows { - uuids := respMap[row.Date] - if uuids == nil { - uuids = make([]uuid.UUID, 0, 8) - } - uuids = append(uuids, row.UserID) - respMap[row.Date] = uuids - } - - dates := maps.Keys(respMap) - slices.SortFunc(dates, func(a, b time.Time) bool { - return a.Before(b) - }) - - var resp codersdk.TemplateDAUsResponse - for _, date := range fillEmptyDays(dates) { - resp.Entries = append(resp.Entries, codersdk.DAUEntry{ - Date: date, - Amount: len(respMap[date]), - }) - } - - return resp +type dauRow interface { + database.GetTemplateDAUsRow | + database.GetDeploymentDAUsRow } -func convertDeploymentDAUResponse(rows []database.GetDeploymentDAUsRow) codersdk.DeploymentDAUsResponse { +func convertDAUResponse[T dauRow](rows []T) codersdk.DAUsResponse { respMap := make(map[time.Time][]uuid.UUID) for _, row := range rows { - respMap[row.Date] = append(respMap[row.Date], row.UserID) + switch row := any(row).(type) { + case database.GetDeploymentDAUsRow: + respMap[row.Date] = append(respMap[row.Date], row.UserID) + case database.GetTemplateDAUsRow: + respMap[row.Date] = append(respMap[row.Date], row.UserID) + default: + // This should never happen. + panic(fmt.Sprintf("%T not acceptable, developer error", row)) + } } dates := maps.Keys(respMap) @@ -145,7 +132,7 @@ func convertDeploymentDAUResponse(rows []database.GetDeploymentDAUsRow) codersdk return a.Before(b) }) - var resp codersdk.DeploymentDAUsResponse + var resp codersdk.DAUsResponse for _, date := range fillEmptyDays(dates) { resp.Entries = append(resp.Entries, codersdk.DAUEntry{ Date: date, @@ -174,8 +161,8 @@ func (c *Cache) refreshTemplateDAUs(ctx context.Context) error { } var ( - deploymentDAUs = codersdk.DeploymentDAUsResponse{} - templateDAUs = make(map[uuid.UUID]codersdk.TemplateDAUsResponse, len(templates)) + deploymentDAUs = codersdk.DAUsResponse{} + templateDAUs = make(map[uuid.UUID]codersdk.DAUsResponse, len(templates)) templateUniqueUsers = make(map[uuid.UUID]int) templateAverageBuildTimes = make(map[uuid.UUID]database.GetTemplateAverageBuildTimeRow) ) @@ -184,7 +171,7 @@ func (c *Cache) refreshTemplateDAUs(ctx context.Context) error { if err != nil { return err } - deploymentDAUs = convertDeploymentDAUResponse(rows) + deploymentDAUs = convertDAUResponse(rows) c.deploymentDAUResponses.Store(&deploymentDAUs) for _, template := range templates { @@ -297,14 +284,14 @@ func (c *Cache) Close() error { return nil } -func (c *Cache) DeploymentDAUs() (*codersdk.DeploymentDAUsResponse, bool) { +func (c *Cache) DeploymentDAUs() (*codersdk.DAUsResponse, bool) { m := c.deploymentDAUResponses.Load() return m, m != nil } // TemplateDAUs returns an empty response if the template doesn't have users // or is loading for the first time. -func (c *Cache) TemplateDAUs(id uuid.UUID) (*codersdk.TemplateDAUsResponse, bool) { +func (c *Cache) TemplateDAUs(id uuid.UUID) (*codersdk.DAUsResponse, bool) { m := c.templateDAUResponses.Load() if m == nil { // Data loading. diff --git a/coderd/templates.go b/coderd/templates.go index f15cf1ec3b3f6..d20fe6ac064d1 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -614,7 +614,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { // @Produce json // @Tags Templates // @Param template path string true "Template ID" format(uuid) -// @Success 200 {object} codersdk.TemplateDAUsResponse +// @Success 200 {object} codersdk.DAUsResponse // @Router /templates/{template}/daus [get] func (api *API) templateDAUs(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -622,7 +622,7 @@ func (api *API) templateDAUs(rw http.ResponseWriter, r *http.Request) { resp, _ := api.metricsCache.TemplateDAUs(template.ID) if resp == nil || resp.Entries == nil { - httpapi.Write(ctx, rw, http.StatusOK, &codersdk.TemplateDAUsResponse{ + httpapi.Write(ctx, rw, http.StatusOK, &codersdk.DAUsResponse{ Entries: []codersdk.DAUEntry{}, }) return diff --git a/coderd/templates_test.go b/coderd/templates_test.go index 34e1637454ad6..7130134f3d282 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -981,7 +981,7 @@ func TestTemplateMetrics(t *testing.T) { daus, err := client.TemplateDAUs(context.Background(), template.ID) require.NoError(t, err) - require.Equal(t, &codersdk.TemplateDAUsResponse{ + require.Equal(t, &codersdk.DAUsResponse{ Entries: []codersdk.DAUEntry{}, }, daus, "no DAUs when stats are empty") @@ -1001,7 +1001,7 @@ func TestTemplateMetrics(t *testing.T) { require.NoError(t, err) _ = sshConn.Close() - wantDAUs := &codersdk.TemplateDAUsResponse{ + wantDAUs := &codersdk.DAUsResponse{ Entries: []codersdk.DAUEntry{ { Date: time.Now().UTC().Truncate(time.Hour * 24), diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 84c295aefee7f..df00dd357b0e7 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -1720,11 +1720,16 @@ func (c *Client) Experiments(ctx context.Context) (Experiments, error) { return exp, json.NewDecoder(res.Body).Decode(&exp) } -type DeploymentDAUsResponse struct { +type DAUsResponse struct { Entries []DAUEntry `json:"entries"` } -func (c *Client) DeploymentDAUs(ctx context.Context) (*DeploymentDAUsResponse, error) { +type DAUEntry struct { + Date time.Time `json:"date" format:"date-time"` + Amount int `json:"amount"` +} + +func (c *Client) DeploymentDAUs(ctx context.Context) (*DAUsResponse, error) { res, err := c.Request(ctx, http.MethodGet, "/api/v2/insights/daus", nil) if err != nil { return nil, xerrors.Errorf("execute request: %w", err) @@ -1735,7 +1740,7 @@ func (c *Client) DeploymentDAUs(ctx context.Context) (*DeploymentDAUsResponse, e return nil, ReadBodyAsError(res) } - var resp DeploymentDAUsResponse + var resp DAUsResponse return &resp, json.NewDecoder(res.Body).Decode(&resp) } diff --git a/codersdk/templates.go b/codersdk/templates.go index b1f258d9daa5b..ec65fdf38935a 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -232,17 +232,7 @@ func (c *Client) TemplateVersionByName(ctx context.Context, template uuid.UUID, return templateVersion, json.NewDecoder(res.Body).Decode(&templateVersion) } -type DAUEntry struct { - Date time.Time `json:"date" format:"date-time"` - Amount int `json:"amount"` -} - -// TemplateDAUsResponse contains statistics of daily active users of the template. -type TemplateDAUsResponse struct { - Entries []DAUEntry `json:"entries"` -} - -func (c *Client) TemplateDAUs(ctx context.Context, templateID uuid.UUID) (*TemplateDAUsResponse, error) { +func (c *Client) TemplateDAUs(ctx context.Context, templateID uuid.UUID) (*DAUsResponse, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s/daus", templateID), nil) if err != nil { return nil, xerrors.Errorf("execute request: %w", err) @@ -253,7 +243,7 @@ func (c *Client) TemplateDAUs(ctx context.Context, templateID uuid.UUID) (*Templ return nil, ReadBodyAsError(res) } - var resp TemplateDAUsResponse + var resp DAUsResponse return &resp, json.NewDecoder(res.Body).Decode(&resp) } From b50294017be17a523661f93482a0140c4fb885d6 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 23 May 2023 13:29:49 +0200 Subject: [PATCH 03/18] Pass time offsets to metricscache --- coderd/insights.go | 2 +- coderd/metricscache/metricscache.go | 77 ++++++++++++++++++++++++----- coderd/templates.go | 2 +- 3 files changed, 66 insertions(+), 15 deletions(-) diff --git a/coderd/insights.go b/coderd/insights.go index c2466fc1e4055..7e50aeff6e14b 100644 --- a/coderd/insights.go +++ b/coderd/insights.go @@ -22,7 +22,7 @@ func (api *API) deploymentDAUs(rw http.ResponseWriter, r *http.Request) { return } - resp, _ := api.metricsCache.DeploymentDAUs() + _, resp, _ := api.metricsCache.DeploymentDAUs(0) if resp == nil || resp.Entries == nil { httpapi.Write(ctx, rw, http.StatusOK, &codersdk.DAUsResponse{ Entries: []codersdk.DAUEntry{}, diff --git a/coderd/metricscache/metricscache.go b/coderd/metricscache/metricscache.go index d0f8c8a3bc78a..70241695e13ca 100644 --- a/coderd/metricscache/metricscache.go +++ b/coderd/metricscache/metricscache.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "math" "sync" "sync/atomic" "time" @@ -30,8 +31,8 @@ type Cache struct { log slog.Logger intervals Intervals - deploymentDAUResponses atomic.Pointer[codersdk.DAUsResponse] - templateDAUResponses atomic.Pointer[map[uuid.UUID]codersdk.DAUsResponse] + deploymentDAUResponses atomic.Pointer[map[int]codersdk.DAUsResponse] + templateDAUResponses atomic.Pointer[map[int]map[uuid.UUID]codersdk.DAUsResponse] templateUniqueUsers atomic.Pointer[map[uuid.UUID]int] templateAverageBuildTime atomic.Pointer[map[uuid.UUID]database.GetTemplateAverageBuildTimeRow] deploymentStatsResponse atomic.Pointer[codersdk.DeploymentStats] @@ -161,8 +162,8 @@ func (c *Cache) refreshTemplateDAUs(ctx context.Context) error { } var ( - deploymentDAUs = codersdk.DAUsResponse{} - templateDAUs = make(map[uuid.UUID]codersdk.DAUsResponse, len(templates)) + deploymentDAUs = map[int]codersdk.DAUsResponse{} + templateDAUs = make(map[int]map[uuid.UUID]codersdk.DAUsResponse, len(templates)) templateUniqueUsers = make(map[uuid.UUID]int) templateAverageBuildTimes = make(map[uuid.UUID]database.GetTemplateAverageBuildTimeRow) ) @@ -171,7 +172,7 @@ func (c *Cache) refreshTemplateDAUs(ctx context.Context) error { if err != nil { return err } - deploymentDAUs = convertDAUResponse(rows) + deploymentDAUs[0] = convertDAUResponse(rows) c.deploymentDAUResponses.Store(&deploymentDAUs) for _, template := range templates { @@ -182,7 +183,7 @@ func (c *Cache) refreshTemplateDAUs(ctx context.Context) error { if err != nil { return err } - templateDAUs[template.ID] = convertDAUResponse(rows) + templateDAUs[0][template.ID] = convertDAUResponse(rows) templateUniqueUsers[template.ID] = countUniqueUsers(rows) templateAvgBuildTime, err := c.database.GetTemplateAverageBuildTime(ctx, database.GetTemplateAverageBuildTimeParams{ @@ -284,26 +285,76 @@ func (c *Cache) Close() error { return nil } -func (c *Cache) DeploymentDAUs() (*codersdk.DAUsResponse, bool) { +func (c *Cache) DeploymentDAUs(offset int) (int, *codersdk.DAUsResponse, bool) { m := c.deploymentDAUResponses.Load() - return m, m != nil + if m == nil { + return 0, nil, false + } + closestOffset, resp, ok := closest(*m, offset) + if !ok { + return 0, nil, false + } + return closestOffset, &resp, ok } // TemplateDAUs returns an empty response if the template doesn't have users // or is loading for the first time. -func (c *Cache) TemplateDAUs(id uuid.UUID) (*codersdk.DAUsResponse, bool) { +// The cache will select the closest DAUs response to given timezone offset. +func (c *Cache) TemplateDAUs(id uuid.UUID, offset int) (int, *codersdk.DAUsResponse, bool) { m := c.templateDAUResponses.Load() if m == nil { // Data loading. - return nil, false + return 0, nil, false } - resp, ok := (*m)[id] + closestOffset, resp, ok := closest(*m, offset) if !ok { // Probably no data. - return nil, false + return 0, nil, false + } + + tpl, ok := resp[id] + if !ok { + // Probably no data. + return 0, nil, false + } + + return closestOffset, &tpl, true +} + +// closest returns the value in the values map that has a key with the value most +// close to the requested key. This is so if a user requests a timezone offset that +// we do not have, we return the closest one we do have to the user. +func closest[V any](values map[int]V, offset int) (int, V, bool) { + if len(values) == 0 { + var v V + return -1, v, false + } + + v, ok := values[offset] + if ok { + // We have the exact offset, that was easy! + return offset, v, true + } + + var closest int + var closestV V + diff := math.MaxInt + for k, v := range values { + if abs(k-offset) < diff { + // new closest + closest = k + closestV = v + } + } + return closest, closestV, true +} + +func abs(a int) int { + if a < 0 { + return -1 * a } - return &resp, true + return a } // TemplateUniqueUsers returns the number of unique Template users diff --git a/coderd/templates.go b/coderd/templates.go index d20fe6ac064d1..e5c7f74832516 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -620,7 +620,7 @@ func (api *API) templateDAUs(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() template := httpmw.TemplateParam(r) - resp, _ := api.metricsCache.TemplateDAUs(template.ID) + _, resp, _ := api.metricsCache.TemplateDAUs(template.ID, 0) if resp == nil || resp.Entries == nil { httpapi.Write(ctx, rw, http.StatusOK, &codersdk.DAUsResponse{ Entries: []codersdk.DAUEntry{}, From 9b970aeeecf367935775ad6d0f3bdbd327fa5afe Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 23 May 2023 13:34:54 +0200 Subject: [PATCH 04/18] Fix unit tests --- coderd/metricscache/metricscache.go | 3 +++ coderd/metricscache/metricscache_test.go | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/coderd/metricscache/metricscache.go b/coderd/metricscache/metricscache.go index 70241695e13ca..dbac4fc28de71 100644 --- a/coderd/metricscache/metricscache.go +++ b/coderd/metricscache/metricscache.go @@ -183,6 +183,9 @@ func (c *Cache) refreshTemplateDAUs(ctx context.Context) error { if err != nil { return err } + if templateDAUs[0] == nil { + templateDAUs[0] = make(map[uuid.UUID]codersdk.DAUsResponse) + } templateDAUs[0][template.ID] = convertDAUResponse(rows) templateUniqueUsers[template.ID] = countUniqueUsers(rows) diff --git a/coderd/metricscache/metricscache_test.go b/coderd/metricscache/metricscache_test.go index 90d5e93e1d750..d60575c611244 100644 --- a/coderd/metricscache/metricscache_test.go +++ b/coderd/metricscache/metricscache_test.go @@ -182,7 +182,7 @@ func TestCache_TemplateUsers(t *testing.T) { } require.Eventuallyf(t, func() bool { - _, ok := cache.TemplateDAUs(template.ID) + _, _, ok := cache.TemplateDAUs(template.ID, 0) return ok }, testutil.WaitShort, testutil.IntervalMedium, "TemplateDAUs never populated", @@ -191,8 +191,9 @@ func TestCache_TemplateUsers(t *testing.T) { gotUniqueUsers, ok := cache.TemplateUniqueUsers(template.ID) require.True(t, ok) - gotEntries, ok := cache.TemplateDAUs(template.ID) + offset, gotEntries, ok := cache.TemplateDAUs(template.ID, 0) require.True(t, ok) + require.Equal(t, offset, 0) require.Equal(t, tt.want.entries, gotEntries.Entries) require.Equal(t, tt.want.uniqueUsers, gotUniqueUsers) }) From 4eb43c119e32e93f1899ff69f450ba72f4f5c7f2 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 23 May 2023 14:13:15 +0200 Subject: [PATCH 05/18] Add unit tests for tzs to offsets --- coderd/insights.go | 13 +++++- coderd/metricscache/metricscache.go | 65 +++++++++++++++++++++-------- coderd/templates.go | 13 +++++- coderd/templates_test.go | 6 +-- codersdk/deployment.go | 35 ++++++++++++++-- codersdk/deployment_test.go | 57 +++++++++++++++++++++++++ codersdk/templates.go | 10 ++++- 7 files changed, 172 insertions(+), 27 deletions(-) diff --git a/coderd/insights.go b/coderd/insights.go index 7e50aeff6e14b..d199631315f99 100644 --- a/coderd/insights.go +++ b/coderd/insights.go @@ -22,7 +22,18 @@ func (api *API) deploymentDAUs(rw http.ResponseWriter, r *http.Request) { return } - _, resp, _ := api.metricsCache.DeploymentDAUs(0) + vals := r.URL.Query() + p := httpapi.NewQueryParamParser() + 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, + }) + } + + _, resp, _ := api.metricsCache.DeploymentDAUs(tzOffset) if resp == nil || resp.Entries == nil { httpapi.Write(ctx, rw, http.StatusOK, &codersdk.DAUsResponse{ Entries: []codersdk.DAUEntry{}, diff --git a/coderd/metricscache/metricscache.go b/coderd/metricscache/metricscache.go index dbac4fc28de71..0dd1eb1dd2ae3 100644 --- a/coderd/metricscache/metricscache.go +++ b/coderd/metricscache/metricscache.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "golang.org/x/xerrors" "math" "sync" "sync/atomic" @@ -21,6 +22,15 @@ import ( "github.com/coder/retry" ) +var ( + // timezoneOffsets are the timezones that are cached and supported. + // Any non-listed timezone offsets will need to use the closest supported one. + timezoneOffsets = []int{ + 0, // UTC - is listed first intentionally. + -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12} +) + // Cache holds the template metrics. // The aggregation queries responsible for these values can take up to a minute // on large deployments. Even in small deployments, aggregation queries can @@ -114,7 +124,7 @@ type dauRow interface { database.GetDeploymentDAUsRow } -func convertDAUResponse[T dauRow](rows []T) codersdk.DAUsResponse { +func convertDAUResponse[T dauRow](rows []T, tzOffset int) codersdk.DAUsResponse { respMap := make(map[time.Time][]uuid.UUID) for _, row := range rows { switch row := any(row).(type) { @@ -140,6 +150,7 @@ func convertDAUResponse[T dauRow](rows []T) codersdk.DAUsResponse { Amount: len(respMap[date]), }) } + resp.TZHourOffset = tzOffset return resp } @@ -152,6 +163,23 @@ func countUniqueUsers(rows []database.GetTemplateDAUsRow) int { return len(seen) } +func (c *Cache) refreshDeploymentDAUs(ctx context.Context) error { + //nolint:gocritic // This is a system service. + ctx = dbauthz.AsSystemRestricted(ctx) + + deploymentDAUs := make(map[int]codersdk.DAUsResponse) + for _, tzOffset := range timezoneOffsets { + rows, err := c.database.GetDeploymentDAUs(ctx, int32(tzOffset)) + if err != nil { + return err + } + deploymentDAUs[tzOffset] = convertDAUResponse(rows, tzOffset) + } + + c.deploymentDAUResponses.Store(&deploymentDAUs) + return nil +} + func (c *Cache) refreshTemplateDAUs(ctx context.Context) error { //nolint:gocritic // This is a system service. ctx = dbauthz.AsSystemRestricted(ctx) @@ -162,32 +190,35 @@ func (c *Cache) refreshTemplateDAUs(ctx context.Context) error { } var ( - deploymentDAUs = map[int]codersdk.DAUsResponse{} templateDAUs = make(map[int]map[uuid.UUID]codersdk.DAUsResponse, len(templates)) templateUniqueUsers = make(map[uuid.UUID]int) templateAverageBuildTimes = make(map[uuid.UUID]database.GetTemplateAverageBuildTimeRow) ) - rows, err := c.database.GetDeploymentDAUs(ctx, 0) + err = c.refreshDeploymentDAUs(ctx) if err != nil { - return err + return xerrors.Errorf("deployment daus: %w", err) } - deploymentDAUs[0] = convertDAUResponse(rows) - c.deploymentDAUResponses.Store(&deploymentDAUs) for _, template := range templates { - rows, err := c.database.GetTemplateDAUs(ctx, database.GetTemplateDAUsParams{ - TemplateID: template.ID, - TzOffset: 0, - }) - if err != nil { - return err - } - if templateDAUs[0] == nil { - templateDAUs[0] = make(map[uuid.UUID]codersdk.DAUsResponse) + for _, tzOffset := range timezoneOffsets { + rows, err := c.database.GetTemplateDAUs(ctx, database.GetTemplateDAUsParams{ + TemplateID: template.ID, + TzOffset: int32(tzOffset), + }) + if err != nil { + return err + } + if templateDAUs[tzOffset] == nil { + templateDAUs[tzOffset] = make(map[uuid.UUID]codersdk.DAUsResponse) + } + templateDAUs[tzOffset][template.ID] = convertDAUResponse(rows, tzOffset) + if _, set := templateUniqueUsers[template.ID]; !set { + // If the uniqueUsers has not been counted yet, set the unique count with the rows we have. + // We only need to calculate this once. + templateUniqueUsers[template.ID] = countUniqueUsers(rows) + } } - templateDAUs[0][template.ID] = convertDAUResponse(rows) - templateUniqueUsers[template.ID] = countUniqueUsers(rows) templateAvgBuildTime, err := c.database.GetTemplateAverageBuildTime(ctx, database.GetTemplateAverageBuildTimeParams{ TemplateID: uuid.NullUUID{ diff --git a/coderd/templates.go b/coderd/templates.go index e5c7f74832516..0e33a17d5b871 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -620,7 +620,18 @@ func (api *API) templateDAUs(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() template := httpmw.TemplateParam(r) - _, resp, _ := api.metricsCache.TemplateDAUs(template.ID, 0) + vals := r.URL.Query() + p := httpapi.NewQueryParamParser() + 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, + }) + } + + _, resp, _ := api.metricsCache.TemplateDAUs(template.ID, tzOffset) if resp == nil || resp.Entries == nil { httpapi.Write(ctx, rw, http.StatusOK, &codersdk.DAUsResponse{ Entries: []codersdk.DAUEntry{}, diff --git a/coderd/templates_test.go b/coderd/templates_test.go index 7130134f3d282..03a3556f56e1a 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -978,7 +978,7 @@ func TestTemplateMetrics(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - daus, err := client.TemplateDAUs(context.Background(), template.ID) + daus, err := client.TemplateDAUs(context.Background(), template.ID, codersdk.TimezoneOffsetHour(time.UTC)) require.NoError(t, err) require.Equal(t, &codersdk.DAUsResponse{ @@ -1010,14 +1010,14 @@ func TestTemplateMetrics(t *testing.T) { }, } require.Eventuallyf(t, func() bool { - daus, err = client.TemplateDAUs(ctx, template.ID) + daus, err = client.TemplateDAUs(ctx, template.ID, codersdk.TimezoneOffsetHour(time.UTC)) require.NoError(t, err) return len(daus.Entries) > 0 }, testutil.WaitShort, testutil.IntervalFast, "template daus never loaded", ) - gotDAUs, err := client.TemplateDAUs(ctx, template.ID) + gotDAUs, err := client.TemplateDAUs(ctx, template.ID, codersdk.TimezoneOffsetHour(time.UTC)) require.NoError(t, err) require.Equal(t, gotDAUs, wantDAUs) diff --git a/codersdk/deployment.go b/codersdk/deployment.go index df00dd357b0e7..1b9deec695011 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -1721,7 +1721,8 @@ func (c *Client) Experiments(ctx context.Context) (Experiments, error) { } type DAUsResponse struct { - Entries []DAUEntry `json:"entries"` + Entries []DAUEntry `json:"entries"` + TZHourOffset int `json:"tz_hour_offset"` } type DAUEntry struct { @@ -1729,8 +1730,36 @@ type DAUEntry struct { Amount int `json:"amount"` } -func (c *Client) DeploymentDAUs(ctx context.Context) (*DAUsResponse, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/v2/insights/daus", nil) +type DAURequest struct { + TZHourOffset int +} + +func (d DAURequest) asRequestOption() RequestOption { + return func(r *http.Request) { + q := r.URL.Query() + q.Set("tz_offset", strconv.Itoa(d.TZHourOffset)) + r.URL.RawQuery = q.Encode() + } +} + +func TimezoneOffsetHour(loc *time.Location) int { + if loc == nil { + // Default to local + loc = time.Local + } + _, offsetSec := time.Now().In(loc).Zone() + // Convert to hours + return offsetSec / 60 / 60 +} + +func (c *Client) DeploymentDAUsLocalTZ(ctx context.Context) (*DAUsResponse, error) { + return c.DeploymentDAUs(ctx, TimezoneOffsetHour(time.Local)) +} + +func (c *Client) DeploymentDAUs(ctx context.Context, tzOffset int) (*DAUsResponse, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/insights/daus", nil, DAURequest{ + TZHourOffset: tzOffset, + }.asRequestOption()) if err != nil { return nil, xerrors.Errorf("execute request: %w", err) } diff --git a/codersdk/deployment_test.go b/codersdk/deployment_test.go index 454995dd22418..eb4e267364c98 100644 --- a/codersdk/deployment_test.go +++ b/codersdk/deployment_test.go @@ -3,6 +3,7 @@ package codersdk_test import ( "strings" "testing" + "time" "github.com/stretchr/testify/require" @@ -195,3 +196,59 @@ func TestSSHConfig_ParseOptions(t *testing.T) { }) } } + +func TestTimezoneOffsets(t *testing.T) { + t.Parallel() + + testCases := []struct { + Name string + Loc *time.Location + ExpectedOffset int + }{ + { + Name: "UTX", + Loc: time.UTC, + ExpectedOffset: 0, + }, + { + Name: "Eastern", + Loc: must(time.LoadLocation("America/New_York")), + ExpectedOffset: -4, + }, + { + Name: "Central", + Loc: must(time.LoadLocation("America/Chicago")), + ExpectedOffset: -5, + }, + { + Name: "Ireland", + Loc: must(time.LoadLocation("Europe/Dublin")), + ExpectedOffset: 1, + }, + { + Name: "HalfHourTz", + // This timezone is +6:30, but the function rounds to the nearest hour. + // This is intentional because our DAUs endpoint only covers 1-hour offsets. + // If the user is in a non-hour timezone, they get the closest hour bucket. + Loc: must(time.LoadLocation("Asia/Yangon")), + ExpectedOffset: 6, + }, + } + + for _, c := range testCases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + + offset := codersdk.TimezoneOffsetHour(c.Loc) + require.Equal(t, c.ExpectedOffset, offset) + }) + } +} + +func must[T any](value T, err error) T { + if err != nil { + panic(err) + } + return value +} diff --git a/codersdk/templates.go b/codersdk/templates.go index ec65fdf38935a..472dfd3775c1f 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -232,8 +232,14 @@ func (c *Client) TemplateVersionByName(ctx context.Context, template uuid.UUID, return templateVersion, json.NewDecoder(res.Body).Decode(&templateVersion) } -func (c *Client) TemplateDAUs(ctx context.Context, templateID uuid.UUID) (*DAUsResponse, error) { - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s/daus", templateID), nil) +func (c *Client) TemplateDAUsLocalTZ(ctx context.Context, templateID uuid.UUID) (*DAUsResponse, error) { + return c.TemplateDAUs(ctx, templateID, TimezoneOffsetHour(time.Local)) +} + +func (c *Client) TemplateDAUs(ctx context.Context, templateID uuid.UUID, tzOffset int) (*DAUsResponse, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s/daus", templateID), nil, DAURequest{ + TZHourOffset: tzOffset, + }.asRequestOption()) if err != nil { return nil, xerrors.Errorf("execute request: %w", err) } From 1949217a53adadb0377b920f08a1a9e33c820fcb Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 23 May 2023 14:23:03 +0200 Subject: [PATCH 06/18] Add unit test for closest --- coderd/metricscache/metrics_internal_test.go | 92 ++++++++++++++++++++ coderd/metricscache/metricscache.go | 6 +- 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 coderd/metricscache/metrics_internal_test.go diff --git a/coderd/metricscache/metrics_internal_test.go b/coderd/metricscache/metrics_internal_test.go new file mode 100644 index 0000000000000..592485bba8481 --- /dev/null +++ b/coderd/metricscache/metrics_internal_test.go @@ -0,0 +1,92 @@ +package metricscache + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestClosest(t *testing.T) { + t.Parallel() + + testCases := []struct { + Name string + Keys []int + Input int + Expected int + NotFound bool + }{ + { + Name: "Empty", + Input: 10, + NotFound: true, + }, + { + Name: "Equal", + Keys: []int{1, 2, 3, 4, 5, 6, 10, 12, 15}, + Input: 10, + Expected: 10, + }, + { + Name: "ZeroOnly", + Keys: []int{0}, + Input: 10, + Expected: 0, + }, + { + Name: "NegativeOnly", + Keys: []int{-10, -5}, + Input: 10, + Expected: -5, + }, + { + Name: "CloseBothSides", + Keys: []int{-10, -5, 0, 5, 8, 12}, + Input: 10, + Expected: 8, + }, + { + Name: "CloseNoZero", + Keys: []int{-10, -5, 5, 8, 12}, + Input: 0, + Expected: -5, + }, + { + Name: "CloseLeft", + Keys: []int{-10, -5, 0, 5, 8, 12}, + Input: 20, + Expected: 12, + }, + { + Name: "CloseRight", + Keys: []int{-10, -5, 0, 5, 8, 12}, + Input: -20, + Expected: -10, + }, + { + Name: "ChooseZero", + Keys: []int{-10, -5, 0, 5, 8, 12}, + Input: 2, + Expected: 0, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + + m := make(map[int]int) + for _, k := range tc.Keys { + m[k] = k + } + + found, _, ok := closest(m, tc.Input) + if tc.NotFound { + require.False(t, ok, "should not be found") + } else { + require.True(t, ok) + require.Equal(t, tc.Expected, found, "closest") + } + }) + } +} diff --git a/coderd/metricscache/metricscache.go b/coderd/metricscache/metricscache.go index 0dd1eb1dd2ae3..de05f81750b49 100644 --- a/coderd/metricscache/metricscache.go +++ b/coderd/metricscache/metricscache.go @@ -375,10 +375,14 @@ func closest[V any](values map[int]V, offset int) (int, V, bool) { var closestV V diff := math.MaxInt for k, v := range values { - if abs(k-offset) < diff { + newDiff := abs(k - offset) + // Take the closest value that is also the smallest value. We do this + // to make the output deterministic + if newDiff < diff || (newDiff == diff && k < closest) { // new closest closest = k closestV = v + diff = newDiff } } return closest, closestV, true From b5b5824e4e8841d2b5c3874590a7bcf259cc261b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 23 May 2023 14:37:43 +0200 Subject: [PATCH 07/18] Return after http error --- coderd/insights.go | 1 + coderd/insights_test.go | 6 +++--- coderd/templates.go | 1 + codersdk/deployment.go | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/coderd/insights.go b/coderd/insights.go index d199631315f99..b1ed1b2dc379d 100644 --- a/coderd/insights.go +++ b/coderd/insights.go @@ -31,6 +31,7 @@ func (api *API) deploymentDAUs(rw http.ResponseWriter, r *http.Request) { Message: "Query parameters have invalid values.", Validations: p.Errors, }) + return } _, resp, _ := api.metricsCache.DeploymentDAUs(tzOffset) diff --git a/coderd/insights_test.go b/coderd/insights_test.go index faf9d6d092f9c..20ddc6bb62a29 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -55,7 +55,7 @@ func TestDeploymentInsights(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - daus, err := client.DeploymentDAUs(context.Background()) + daus, err := client.DeploymentDAUs(context.Background(), codersdk.TimezoneOffsetHour(time.UTC)) require.NoError(t, err) res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{}) @@ -83,14 +83,14 @@ func TestDeploymentInsights(t *testing.T) { }, } require.Eventuallyf(t, func() bool { - daus, err = client.DeploymentDAUs(ctx) + daus, err = client.DeploymentDAUs(ctx, codersdk.TimezoneOffsetHour(time.UTC)) require.NoError(t, err) return len(daus.Entries) > 0 }, testutil.WaitShort, testutil.IntervalFast, "deployment daus never loaded", ) - gotDAUs, err := client.DeploymentDAUs(ctx) + gotDAUs, err := client.DeploymentDAUs(ctx, codersdk.TimezoneOffsetHour(time.UTC)) require.NoError(t, err) require.Equal(t, gotDAUs, wantDAUs) diff --git a/coderd/templates.go b/coderd/templates.go index 0e33a17d5b871..ee73c8e43c833 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -629,6 +629,7 @@ func (api *API) templateDAUs(rw http.ResponseWriter, r *http.Request) { Message: "Query parameters have invalid values.", Validations: p.Errors, }) + return } _, resp, _ := api.metricsCache.TemplateDAUs(template.ID, tzOffset) diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 1b9deec695011..d5a2565012b2d 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -1744,8 +1744,8 @@ func (d DAURequest) asRequestOption() RequestOption { func TimezoneOffsetHour(loc *time.Location) int { if loc == nil { - // Default to local - loc = time.Local + // Default to UTC time to be consistent across all callers. + loc = time.UTC } _, offsetSec := time.Now().In(loc).Zone() // Convert to hours From 448d5fbc94c08b5ff92b62232894cf7f5714c0a2 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 23 May 2023 12:40:17 +0000 Subject: [PATCH 08/18] make gen --- coderd/apidoc/docs.go | 40 ++++++------- coderd/apidoc/swagger.json | 40 ++++++------- coderd/database/models.go | 42 +++++++------- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 2 +- coderd/metricscache/metrics_internal_test.go | 3 +- coderd/metricscache/metricscache.go | 3 +- docs/api/insights.md | 9 +-- docs/api/schemas.md | 59 +++++++------------- docs/api/templates.md | 9 +-- site/src/api/typesGenerated.ts | 23 ++++---- 11 files changed, 102 insertions(+), 130 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index bb2f2b602621c..63c07fc22fa19 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -747,7 +747,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.DeploymentDAUsResponse" + "$ref": "#/definitions/codersdk.DAUsResponse" } } } @@ -2124,7 +2124,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.TemplateDAUsResponse" + "$ref": "#/definitions/codersdk.DAUsResponse" } } } @@ -7190,6 +7190,20 @@ const docTemplate = `{ } } }, + "codersdk.DAUsResponse": { + "type": "object", + "properties": { + "entries": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.DAUEntry" + } + }, + "tz_hour_offset": { + "type": "integer" + } + } + }, "codersdk.DERP": { "type": "object", "properties": { @@ -7277,17 +7291,6 @@ const docTemplate = `{ } } }, - "codersdk.DeploymentDAUsResponse": { - "type": "object", - "properties": { - "entries": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.DAUEntry" - } - } - } - }, "codersdk.DeploymentStats": { "type": "object", "properties": { @@ -8900,17 +8903,6 @@ const docTemplate = `{ "$ref": "#/definitions/codersdk.TransitionStats" } }, - "codersdk.TemplateDAUsResponse": { - "type": "object", - "properties": { - "entries": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.DAUEntry" - } - } - } - }, "codersdk.TemplateExample": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index ba4da7aa72b43..265ba54aad771 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -639,7 +639,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.DeploymentDAUsResponse" + "$ref": "#/definitions/codersdk.DAUsResponse" } } } @@ -1848,7 +1848,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/codersdk.TemplateDAUsResponse" + "$ref": "#/definitions/codersdk.DAUsResponse" } } } @@ -6392,6 +6392,20 @@ } } }, + "codersdk.DAUsResponse": { + "type": "object", + "properties": { + "entries": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.DAUEntry" + } + }, + "tz_hour_offset": { + "type": "integer" + } + } + }, "codersdk.DERP": { "type": "object", "properties": { @@ -6479,17 +6493,6 @@ } } }, - "codersdk.DeploymentDAUsResponse": { - "type": "object", - "properties": { - "entries": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.DAUEntry" - } - } - } - }, "codersdk.DeploymentStats": { "type": "object", "properties": { @@ -7994,17 +7997,6 @@ "$ref": "#/definitions/codersdk.TransitionStats" } }, - "codersdk.TemplateDAUsResponse": { - "type": "object", - "properties": { - "entries": { - "type": "array", - "items": { - "$ref": "#/definitions/codersdk.DAUEntry" - } - } - } - }, "codersdk.TemplateExample": { "type": "object", "properties": { diff --git a/coderd/database/models.go b/coderd/database/models.go index 4cef35e644a4c..a0bd9595c89ce 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.17.2 +// sqlc v1.16.0 package database @@ -56,7 +56,7 @@ func (ns NullAPIKeyScope) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return string(ns.APIKeyScope), nil + return ns.APIKeyScope, nil } func (e APIKeyScope) Valid() bool { @@ -115,7 +115,7 @@ func (ns NullAppSharingLevel) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return string(ns.AppSharingLevel), nil + return ns.AppSharingLevel, nil } func (e AppSharingLevel) Valid() bool { @@ -181,7 +181,7 @@ func (ns NullAuditAction) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return string(ns.AuditAction), nil + return ns.AuditAction, nil } func (e AuditAction) Valid() bool { @@ -252,7 +252,7 @@ func (ns NullBuildReason) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return string(ns.BuildReason), nil + return ns.BuildReason, nil } func (e BuildReason) Valid() bool { @@ -315,7 +315,7 @@ func (ns NullLogLevel) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return string(ns.LogLevel), nil + return ns.LogLevel, nil } func (e LogLevel) Valid() bool { @@ -379,7 +379,7 @@ func (ns NullLogSource) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return string(ns.LogSource), nil + return ns.LogSource, nil } func (e LogSource) Valid() bool { @@ -439,7 +439,7 @@ func (ns NullLoginType) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return string(ns.LoginType), nil + return ns.LoginType, nil } func (e LoginType) Valid() bool { @@ -502,7 +502,7 @@ func (ns NullParameterDestinationScheme) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return string(ns.ParameterDestinationScheme), nil + return ns.ParameterDestinationScheme, nil } func (e ParameterDestinationScheme) Valid() bool { @@ -563,7 +563,7 @@ func (ns NullParameterScope) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return string(ns.ParameterScope), nil + return ns.ParameterScope, nil } func (e ParameterScope) Valid() bool { @@ -623,7 +623,7 @@ func (ns NullParameterSourceScheme) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return string(ns.ParameterSourceScheme), nil + return ns.ParameterSourceScheme, nil } func (e ParameterSourceScheme) Valid() bool { @@ -681,7 +681,7 @@ func (ns NullParameterTypeSystem) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return string(ns.ParameterTypeSystem), nil + return ns.ParameterTypeSystem, nil } func (e ParameterTypeSystem) Valid() bool { @@ -740,7 +740,7 @@ func (ns NullProvisionerJobType) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return string(ns.ProvisionerJobType), nil + return ns.ProvisionerJobType, nil } func (e ProvisionerJobType) Valid() bool { @@ -799,7 +799,7 @@ func (ns NullProvisionerStorageMethod) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return string(ns.ProvisionerStorageMethod), nil + return ns.ProvisionerStorageMethod, nil } func (e ProvisionerStorageMethod) Valid() bool { @@ -855,7 +855,7 @@ func (ns NullProvisionerType) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return string(ns.ProvisionerType), nil + return ns.ProvisionerType, nil } func (e ProvisionerType) Valid() bool { @@ -922,7 +922,7 @@ func (ns NullResourceType) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return string(ns.ResourceType), nil + return ns.ResourceType, nil } func (e ResourceType) Valid() bool { @@ -998,7 +998,7 @@ func (ns NullUserStatus) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return string(ns.UserStatus), nil + return ns.UserStatus, nil } func (e UserStatus) Valid() bool { @@ -1063,7 +1063,7 @@ func (ns NullWorkspaceAgentLifecycleState) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return string(ns.WorkspaceAgentLifecycleState), nil + return ns.WorkspaceAgentLifecycleState, nil } func (e WorkspaceAgentLifecycleState) Valid() bool { @@ -1136,7 +1136,7 @@ func (ns NullWorkspaceAgentSubsystem) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return string(ns.WorkspaceAgentSubsystem), nil + return ns.WorkspaceAgentSubsystem, nil } func (e WorkspaceAgentSubsystem) Valid() bool { @@ -1198,7 +1198,7 @@ func (ns NullWorkspaceAppHealth) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return string(ns.WorkspaceAppHealth), nil + return ns.WorkspaceAppHealth, nil } func (e WorkspaceAppHealth) Valid() bool { @@ -1261,7 +1261,7 @@ func (ns NullWorkspaceTransition) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return string(ns.WorkspaceTransition), nil + return ns.WorkspaceTransition, nil } func (e WorkspaceTransition) Valid() bool { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 26978d240623e..318cdb14f469f 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.17.2 +// sqlc v1.16.0 package database diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 358c5af7934a8..d4c2307d3e3a7 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.17.2 +// sqlc v1.16.0 package database diff --git a/coderd/metricscache/metrics_internal_test.go b/coderd/metricscache/metrics_internal_test.go index 592485bba8481..97f036b766327 100644 --- a/coderd/metricscache/metrics_internal_test.go +++ b/coderd/metricscache/metrics_internal_test.go @@ -1,8 +1,9 @@ package metricscache import ( - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) func TestClosest(t *testing.T) { diff --git a/coderd/metricscache/metricscache.go b/coderd/metricscache/metricscache.go index de05f81750b49..2a3f9f9d43040 100644 --- a/coderd/metricscache/metricscache.go +++ b/coderd/metricscache/metricscache.go @@ -4,12 +4,13 @@ import ( "context" "database/sql" "fmt" - "golang.org/x/xerrors" "math" "sync" "sync/atomic" "time" + "golang.org/x/xerrors" + "golang.org/x/exp/maps" "golang.org/x/exp/slices" diff --git a/docs/api/insights.md b/docs/api/insights.md index b72dec3c3dc05..90f236d1574cf 100644 --- a/docs/api/insights.md +++ b/docs/api/insights.md @@ -24,14 +24,15 @@ curl -X GET http://coder-server:8080/api/v2/insights/daus \ "amount": 0, "date": "2019-08-24T14:15:22Z" } - ] + ], + "tz_hour_offset": 0 } ``` ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------- | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.DeploymentDAUsResponse](schemas.md#codersdkdeploymentdausresponse) | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.DAUsResponse](schemas.md#codersdkdausresponse) | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/api/schemas.md b/docs/api/schemas.md index e428c8bc2f9c3..676021aa17bcd 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1690,6 +1690,27 @@ CreateParameterRequest is a structure used to create a new parameter value for a | `amount` | integer | false | | | | `date` | string | false | | | +## codersdk.DAUsResponse + +```json +{ + "entries": [ + { + "amount": 0, + "date": "2019-08-24T14:15:22Z" + } + ], + "tz_hour_offset": 0 +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ---------------- | ----------------------------------------------- | -------- | ------------ | ----------- | +| `entries` | array of [codersdk.DAUEntry](#codersdkdauentry) | false | | | +| `tz_hour_offset` | integer | false | | | + ## codersdk.DERP ```json @@ -2102,25 +2123,6 @@ CreateParameterRequest is a structure used to create a new parameter value for a | `config` | [codersdk.DeploymentValues](#codersdkdeploymentvalues) | false | | | | `options` | array of [clibase.Option](#clibaseoption) | false | | | -## codersdk.DeploymentDAUsResponse - -```json -{ - "entries": [ - { - "amount": 0, - "date": "2019-08-24T14:15:22Z" - } - ] -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| --------- | ----------------------------------------------- | -------- | ------------ | ----------- | -| `entries` | array of [codersdk.DAUEntry](#codersdkdauentry) | false | | | - ## codersdk.DeploymentStats ```json @@ -3979,25 +3981,6 @@ Parameter represents a set value for the scope. | ---------------- | ---------------------------------------------------- | -------- | ------------ | ----------- | | `[any property]` | [codersdk.TransitionStats](#codersdktransitionstats) | false | | | -## codersdk.TemplateDAUsResponse - -```json -{ - "entries": [ - { - "amount": 0, - "date": "2019-08-24T14:15:22Z" - } - ] -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| --------- | ----------------------------------------------- | -------- | ------------ | ----------- | -| `entries` | array of [codersdk.DAUEntry](#codersdkdauentry) | false | | | - ## codersdk.TemplateExample ```json diff --git a/docs/api/templates.md b/docs/api/templates.md index 829f43f410216..3c17f36329d35 100644 --- a/docs/api/templates.md +++ b/docs/api/templates.md @@ -801,15 +801,16 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/daus \ "amount": 0, "date": "2019-08-24T14:15:22Z" } - ] + ], + "tz_hour_offset": 0 } ``` ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------------ | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.TemplateDAUsResponse](schemas.md#codersdktemplatedausresponse) | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.DAUsResponse](schemas.md#codersdkdausresponse) | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 768c74f8f640e..4555c007ae559 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -273,12 +273,23 @@ export interface CreateWorkspaceRequest { readonly rich_parameter_values?: WorkspaceBuildParameter[] } -// From codersdk/templates.go +// From codersdk/deployment.go export interface DAUEntry { readonly date: string readonly amount: number } +// From codersdk/deployment.go +export interface DAURequest { + readonly TZHourOffset: number +} + +// From codersdk/deployment.go +export interface DAUsResponse { + readonly entries: DAUEntry[] + readonly tz_hour_offset: number +} + // From codersdk/deployment.go export interface DERP { readonly server: DERPServerConfig @@ -315,11 +326,6 @@ export interface DangerousConfig { readonly allow_all_cors: boolean } -// From codersdk/deployment.go -export interface DeploymentDAUsResponse { - readonly entries: DAUEntry[] -} - // From codersdk/deployment.go export interface DeploymentStats { readonly aggregated_from: string @@ -863,11 +869,6 @@ export type TemplateBuildTimeStats = Record< TransitionStats > -// From codersdk/templates.go -export interface TemplateDAUsResponse { - readonly entries: DAUEntry[] -} - // From codersdk/templates.go export interface TemplateExample { readonly id: string From 5976ac4ac5490133e7ae8393c21d05141486a957 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 23 May 2023 14:44:16 +0200 Subject: [PATCH 09/18] add comments --- coderd/metricscache/metricscache.go | 3 --- codersdk/deployment.go | 2 ++ codersdk/templates.go | 2 ++ 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/coderd/metricscache/metricscache.go b/coderd/metricscache/metricscache.go index 2a3f9f9d43040..2ee1901e30567 100644 --- a/coderd/metricscache/metricscache.go +++ b/coderd/metricscache/metricscache.go @@ -10,12 +10,9 @@ import ( "time" "golang.org/x/xerrors" - "golang.org/x/exp/maps" "golang.org/x/exp/slices" - "github.com/google/uuid" - "cdr.dev/slog" "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/database/dbauthz" diff --git a/codersdk/deployment.go b/codersdk/deployment.go index d5a2565012b2d..d285a78bcb647 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -1756,6 +1756,8 @@ func (c *Client) DeploymentDAUsLocalTZ(ctx context.Context) (*DAUsResponse, erro return c.DeploymentDAUs(ctx, TimezoneOffsetHour(time.Local)) } +// DeploymentDAUs requires a tzOffset in hours. Use 0 for UTC, and TimezoneOffsetHour(time.Local) for the +// local timezone. func (c *Client) DeploymentDAUs(ctx context.Context, tzOffset int) (*DAUsResponse, error) { res, err := c.Request(ctx, http.MethodGet, "/api/v2/insights/daus", nil, DAURequest{ TZHourOffset: tzOffset, diff --git a/codersdk/templates.go b/codersdk/templates.go index 472dfd3775c1f..1fc6196832e3e 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -236,6 +236,8 @@ func (c *Client) TemplateDAUsLocalTZ(ctx context.Context, templateID uuid.UUID) return c.TemplateDAUs(ctx, templateID, TimezoneOffsetHour(time.Local)) } +// TemplateDAUs requires a tzOffset in hours. Use 0 for UTC, and TimezoneOffsetHour(time.Local) for the +// local timezone. func (c *Client) TemplateDAUs(ctx context.Context, templateID uuid.UUID, tzOffset int) (*DAUsResponse, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templates/%s/daus", templateID), nil, DAURequest{ TZHourOffset: tzOffset, From 6ff576b3991ece8d80bc0cf63ab5a6685a791410 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 23 May 2023 17:05:49 +0200 Subject: [PATCH 10/18] Fix imports --- coderd/metricscache/metricscache.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/metricscache/metricscache.go b/coderd/metricscache/metricscache.go index 2ee1901e30567..030a4aea39ed0 100644 --- a/coderd/metricscache/metricscache.go +++ b/coderd/metricscache/metricscache.go @@ -9,9 +9,9 @@ import ( "sync/atomic" "time" - "golang.org/x/xerrors" "golang.org/x/exp/maps" "golang.org/x/exp/slices" + "golang.org/x/xerrors" "cdr.dev/slog" "github.com/coder/coder/coderd/database" From a91debd5f17676ee5f604ef8586bb9a0b21ed333 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 23 May 2023 17:06:55 +0200 Subject: [PATCH 11/18] missing import --- coderd/metricscache/metricscache.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/metricscache/metricscache.go b/coderd/metricscache/metricscache.go index 030a4aea39ed0..d7235af843de1 100644 --- a/coderd/metricscache/metricscache.go +++ b/coderd/metricscache/metricscache.go @@ -9,6 +9,7 @@ import ( "sync/atomic" "time" + "github.com/google/uuid" "golang.org/x/exp/maps" "golang.org/x/exp/slices" "golang.org/x/xerrors" From 0d19300a2cf2ed3499a31e749c35a233cbb19d5b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 23 May 2023 18:23:51 +0200 Subject: [PATCH 12/18] add unit test --- coderd/metricscache/metricscache_test.go | 190 +++++++++++++---------- 1 file changed, 110 insertions(+), 80 deletions(-) diff --git a/coderd/metricscache/metricscache_test.go b/coderd/metricscache/metricscache_test.go index d60575c611244..7815bc2635c19 100644 --- a/coderd/metricscache/metricscache_test.go +++ b/coderd/metricscache/metricscache_test.go @@ -18,12 +18,22 @@ import ( "github.com/coder/coder/testutil" ) +func dateH(year, month, day, hour int) time.Time { + return time.Date(year, time.Month(month), day, hour, 0, 0, 0, time.UTC) +} + func date(year, month, day int) time.Time { return time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) } func TestCache_TemplateUsers(t *testing.T) { t.Parallel() + statRow := func(user uuid.UUID, date time.Time) database.InsertWorkspaceAgentStatParams { + return database.InsertWorkspaceAgentStatParams{ + CreatedAt: date, + UserID: user, + } + } var ( zebra = uuid.UUID{1} @@ -38,24 +48,21 @@ func TestCache_TemplateUsers(t *testing.T) { uniqueUsers int } tests := []struct { - name string - args args - want want + name string + args args + want want + tzOffset int }{ - {"empty", args{}, want{nil, 0}}, + {name: "empty", args: args{}, want: want{nil, 0}}, { - "one hole", args{ + name: "one hole", + args: args{ rows: []database.InsertWorkspaceAgentStatParams{ - { - CreatedAt: date(2022, 8, 27), - UserID: zebra, - }, - { - CreatedAt: date(2022, 8, 30), - UserID: zebra, - }, + statRow(zebra, dateH(2022, 8, 27, 0)), + statRow(zebra, dateH(2022, 8, 30, 0)), }, - }, want{[]codersdk.DAUEntry{ + }, + want: want{[]codersdk.DAUEntry{ { Date: date(2022, 8, 27), Amount: 1, @@ -74,88 +81,111 @@ func TestCache_TemplateUsers(t *testing.T) { }, }, 1}, }, - {"no holes", args{ - rows: []database.InsertWorkspaceAgentStatParams{ + { + name: "no holes", + args: args{ + rows: []database.InsertWorkspaceAgentStatParams{ + statRow(zebra, dateH(2022, 8, 27, 0)), + statRow(zebra, dateH(2022, 8, 28, 0)), + statRow(zebra, dateH(2022, 8, 29, 0)), + }, + }, + want: want{[]codersdk.DAUEntry{ { - CreatedAt: date(2022, 8, 27), - UserID: zebra, + Date: date(2022, 8, 27), + Amount: 1, }, { - CreatedAt: date(2022, 8, 28), - UserID: zebra, + Date: date(2022, 8, 28), + Amount: 1, }, { - CreatedAt: date(2022, 8, 29), - UserID: zebra, + Date: date(2022, 8, 29), + Amount: 1, + }, + }, 1}}, + { + name: "holes", + args: args{ + rows: []database.InsertWorkspaceAgentStatParams{ + statRow(zebra, dateH(2022, 1, 1, 0)), + statRow(tiger, dateH(2022, 1, 1, 0)), + statRow(zebra, dateH(2022, 1, 4, 0)), + statRow(zebra, dateH(2022, 1, 7, 0)), + statRow(tiger, dateH(2022, 1, 7, 0)), }, }, - }, want{[]codersdk.DAUEntry{ - { - Date: date(2022, 8, 27), - Amount: 1, - }, - { - Date: date(2022, 8, 28), - Amount: 1, - }, - { - Date: date(2022, 8, 29), - Amount: 1, - }, - }, 1}}, - {"holes", args{ - rows: []database.InsertWorkspaceAgentStatParams{ + want: want{[]codersdk.DAUEntry{ + { + Date: date(2022, 1, 1), + Amount: 2, + }, { - CreatedAt: date(2022, 1, 1), - UserID: zebra, + Date: date(2022, 1, 2), + Amount: 0, }, { - CreatedAt: date(2022, 1, 1), - UserID: tiger, + Date: date(2022, 1, 3), + Amount: 0, }, { - CreatedAt: date(2022, 1, 4), - UserID: zebra, + Date: date(2022, 1, 4), + Amount: 1, }, { - CreatedAt: date(2022, 1, 7), - UserID: zebra, + Date: date(2022, 1, 5), + Amount: 0, }, { - CreatedAt: date(2022, 1, 7), - UserID: tiger, + Date: date(2022, 1, 6), + Amount: 0, + }, + { + Date: date(2022, 1, 7), + Amount: 2, + }, + }, 2}}, + { + name: "tzOffset", + tzOffset: -1, + args: args{ + rows: []database.InsertWorkspaceAgentStatParams{ + statRow(zebra, dateH(2022, 1, 2, 1)), + statRow(tiger, dateH(2022, 1, 2, 1)), + // With offset these should be in the previous day + statRow(zebra, dateH(2022, 1, 2, 0)), + statRow(tiger, dateH(2022, 1, 2, 0)), }, }, - }, want{[]codersdk.DAUEntry{ - { - Date: date(2022, 1, 1), - Amount: 2, - }, - { - Date: date(2022, 1, 2), - Amount: 0, - }, - { - Date: date(2022, 1, 3), - Amount: 0, - }, - { - Date: date(2022, 1, 4), - Amount: 1, - }, - { - Date: date(2022, 1, 5), - Amount: 0, - }, - { - Date: date(2022, 1, 6), - Amount: 0, - }, - { - Date: date(2022, 1, 7), - Amount: 2, + want: want{[]codersdk.DAUEntry{ + { + Date: date(2022, 1, 1), + Amount: 2, + }, + { + Date: date(2022, 1, 2), + Amount: 2, + }, + }, 2}, + }, + { + name: "tzOffsetPreviousDay", + tzOffset: -6, + args: args{ + rows: []database.InsertWorkspaceAgentStatParams{ + statRow(zebra, dateH(2022, 1, 2, 1)), + statRow(tiger, dateH(2022, 1, 2, 1)), + statRow(zebra, dateH(2022, 1, 2, 0)), + statRow(tiger, dateH(2022, 1, 2, 0)), + }, }, - }, 2}}, + want: want{[]codersdk.DAUEntry{ + { + Date: date(2022, 1, 1), + Amount: 2, + }, + }, 2}, + }, } for _, tt := range tests { @@ -182,7 +212,7 @@ func TestCache_TemplateUsers(t *testing.T) { } require.Eventuallyf(t, func() bool { - _, _, ok := cache.TemplateDAUs(template.ID, 0) + _, _, ok := cache.TemplateDAUs(template.ID, tt.tzOffset) return ok }, testutil.WaitShort, testutil.IntervalMedium, "TemplateDAUs never populated", @@ -191,9 +221,9 @@ func TestCache_TemplateUsers(t *testing.T) { gotUniqueUsers, ok := cache.TemplateUniqueUsers(template.ID) require.True(t, ok) - offset, gotEntries, ok := cache.TemplateDAUs(template.ID, 0) + offset, gotEntries, ok := cache.TemplateDAUs(template.ID, tt.tzOffset) require.True(t, ok) - require.Equal(t, offset, 0) + require.Equal(t, offset, tt.tzOffset) require.Equal(t, tt.want.entries, gotEntries.Entries) require.Equal(t, tt.want.uniqueUsers, gotUniqueUsers) }) From d7e8b25fc26d7a7173adb7976f5e7cc9dd492c6f Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 23 May 2023 18:24:44 +0200 Subject: [PATCH 13/18] Formatting --- coderd/metricscache/metricscache.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coderd/metricscache/metricscache.go b/coderd/metricscache/metricscache.go index d7235af843de1..40af1f860edd7 100644 --- a/coderd/metricscache/metricscache.go +++ b/coderd/metricscache/metricscache.go @@ -27,7 +27,8 @@ var ( timezoneOffsets = []int{ 0, // UTC - is listed first intentionally. -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12} + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, + } ) // Cache holds the template metrics. From bf1bd47ea2fdfd639a02d5ebb68ee5322586f930 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 23 May 2023 18:28:02 +0200 Subject: [PATCH 14/18] Fix FE to use singulare DAUsResponse --- site/src/api/api.ts | 4 ++-- site/src/components/DAUChart/DAUChart.tsx | 2 +- .../DeploySettingsLayout/DeploySettingsLayout.tsx | 4 ++-- .../GeneralSettingsPage/GeneralSettingsPageView.tsx | 4 ++-- site/src/testHelpers/entities.ts | 6 ++++-- .../xServices/deploymentConfig/deploymentConfigMachine.ts | 6 +++--- 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 31a634997427d..7445fa485e2bf 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -826,13 +826,13 @@ export const getAuditLogs = async ( export const getTemplateDAUs = async ( templateId: string, -): Promise => { +): Promise => { const response = await axios.get(`/api/v2/templates/${templateId}/daus`) return response.data } export const getDeploymentDAUs = - async (): Promise => { + async (): Promise => { const response = await axios.get(`/api/v2/insights/daus`) return response.data } diff --git a/site/src/components/DAUChart/DAUChart.tsx b/site/src/components/DAUChart/DAUChart.tsx index 1ccfa00114b4e..2b9c508b2060b 100644 --- a/site/src/components/DAUChart/DAUChart.tsx +++ b/site/src/components/DAUChart/DAUChart.tsx @@ -38,7 +38,7 @@ ChartJS.register( ) export interface DAUChartProps { - daus: TypesGen.TemplateDAUsResponse | TypesGen.DeploymentDAUsResponse + daus: TypesGen.DAUsResponse } export const Language = { loadingText: "DAU stats are loading. Check back later.", diff --git a/site/src/components/DeploySettingsLayout/DeploySettingsLayout.tsx b/site/src/components/DeploySettingsLayout/DeploySettingsLayout.tsx index d0c15de104390..035748b3eb1ce 100644 --- a/site/src/components/DeploySettingsLayout/DeploySettingsLayout.tsx +++ b/site/src/components/DeploySettingsLayout/DeploySettingsLayout.tsx @@ -5,7 +5,7 @@ import { Sidebar } from "./Sidebar" import { createContext, Suspense, useContext, FC } from "react" import { useMachine } from "@xstate/react" import { Loader } from "components/Loader/Loader" -import { DeploymentDAUsResponse } from "api/typesGenerated" +import { DAUsResponse } from "api/typesGenerated" import { deploymentConfigMachine } from "xServices/deploymentConfig/deploymentConfigMachine" import { RequirePermission } from "components/RequirePermission/RequirePermission" import { usePermissions } from "hooks/usePermissions" @@ -15,7 +15,7 @@ import { DeploymentConfig } from "api/types" type DeploySettingsContextValue = { deploymentValues: DeploymentConfig getDeploymentValuesError: unknown - deploymentDAUs?: DeploymentDAUsResponse + deploymentDAUs?: DAUsResponse getDeploymentDAUsError: unknown } diff --git a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx index b63fb74a2d1c3..4b664de25a570 100644 --- a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx +++ b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx @@ -1,5 +1,5 @@ import { DeploymentOption } from "api/types" -import { DeploymentDAUsResponse } from "api/typesGenerated" +import { DAUsResponse } from "api/typesGenerated" import { ErrorAlert } from "components/Alert/ErrorAlert" import { DAUChart } from "components/DAUChart/DAUChart" import { Header } from "components/DeploySettingsLayout/Header" @@ -9,7 +9,7 @@ import { useDeploymentOptions } from "utils/deployOptions" export type GeneralSettingsPageViewProps = { deploymentOptions: DeploymentOption[] - deploymentDAUs?: DeploymentDAUsResponse + deploymentDAUs?: DAUsResponse getDeploymentDAUsError: unknown } export const GeneralSettingsPageView = ({ diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 0039cf19a84e6..36082228d4f8c 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -16,14 +16,16 @@ export const MockOrganization: TypesGen.Organization = { updated_at: "", } -export const MockTemplateDAUResponse: TypesGen.TemplateDAUsResponse = { +export const MockTemplateDAUResponse: TypesGen.DAUsResponse = { + tz_hour_offset: 0, entries: [ { date: "2022-08-27T00:00:00Z", amount: 1 }, { date: "2022-08-29T00:00:00Z", amount: 2 }, { date: "2022-08-30T00:00:00Z", amount: 1 }, ], } -export const MockDeploymentDAUResponse: TypesGen.DeploymentDAUsResponse = { +export const MockDeploymentDAUResponse: TypesGen.DAUsResponse = { + tz_hour_offset: 0, entries: [ { date: "2022-08-27T00:00:00Z", amount: 1 }, { date: "2022-08-29T00:00:00Z", amount: 2 }, diff --git a/site/src/xServices/deploymentConfig/deploymentConfigMachine.ts b/site/src/xServices/deploymentConfig/deploymentConfigMachine.ts index 343bdd7fd3b1b..7c4fc4c074720 100644 --- a/site/src/xServices/deploymentConfig/deploymentConfigMachine.ts +++ b/site/src/xServices/deploymentConfig/deploymentConfigMachine.ts @@ -1,4 +1,4 @@ -import { DeploymentDAUsResponse } from "./../../api/typesGenerated" +import { DAUsResponse } from "./../../api/typesGenerated" import { getDeploymentValues, getDeploymentDAUs } from "api/api" import { createMachine, assign } from "xstate" import { DeploymentConfig } from "api/types" @@ -12,7 +12,7 @@ export const deploymentConfigMachine = createMachine( context: {} as { deploymentValues?: DeploymentConfig getDeploymentValuesError?: unknown - deploymentDAUs?: DeploymentDAUsResponse + deploymentDAUs?: DAUsResponse getDeploymentDAUsError?: unknown }, events: {} as { type: "LOAD" }, @@ -21,7 +21,7 @@ export const deploymentConfigMachine = createMachine( data: DeploymentConfig } getDeploymentDAUs: { - data: DeploymentDAUsResponse + data: DAUsResponse } }, }, From dc324a8b244dcc4c618d3c4233781d2dbb0eaac8 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 23 May 2023 18:33:52 +0200 Subject: [PATCH 15/18] Fix unit test --- site/src/api/api.ts | 9 ++++----- site/src/components/DAUChart/DAUChart.test.tsx | 2 ++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 7445fa485e2bf..1fc8ed56f9b50 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -831,11 +831,10 @@ export const getTemplateDAUs = async ( return response.data } -export const getDeploymentDAUs = - async (): Promise => { - const response = await axios.get(`/api/v2/insights/daus`) - return response.data - } +export const getDeploymentDAUs = async (): Promise => { + const response = await axios.get(`/api/v2/insights/daus`) + return response.data +} export const getTemplateACL = async ( templateId: string, diff --git a/site/src/components/DAUChart/DAUChart.test.tsx b/site/src/components/DAUChart/DAUChart.test.tsx index 9a48c1069faef..6a6866a5d095d 100644 --- a/site/src/components/DAUChart/DAUChart.test.tsx +++ b/site/src/components/DAUChart/DAUChart.test.tsx @@ -14,6 +14,7 @@ describe("DAUChart", () => { render( , @@ -25,6 +26,7 @@ describe("DAUChart", () => { render( , From bb7c392a12dfbdbe81b6d6db56b2aed43753a30d Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 23 May 2023 17:01:22 +0000 Subject: [PATCH 16/18] Make gen --- coderd/database/models.go | 42 +++++++++++++++++----------------- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 2 +- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/coderd/database/models.go b/coderd/database/models.go index a0bd9595c89ce..4cef35e644a4c 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.16.0 +// sqlc v1.17.2 package database @@ -56,7 +56,7 @@ func (ns NullAPIKeyScope) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return ns.APIKeyScope, nil + return string(ns.APIKeyScope), nil } func (e APIKeyScope) Valid() bool { @@ -115,7 +115,7 @@ func (ns NullAppSharingLevel) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return ns.AppSharingLevel, nil + return string(ns.AppSharingLevel), nil } func (e AppSharingLevel) Valid() bool { @@ -181,7 +181,7 @@ func (ns NullAuditAction) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return ns.AuditAction, nil + return string(ns.AuditAction), nil } func (e AuditAction) Valid() bool { @@ -252,7 +252,7 @@ func (ns NullBuildReason) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return ns.BuildReason, nil + return string(ns.BuildReason), nil } func (e BuildReason) Valid() bool { @@ -315,7 +315,7 @@ func (ns NullLogLevel) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return ns.LogLevel, nil + return string(ns.LogLevel), nil } func (e LogLevel) Valid() bool { @@ -379,7 +379,7 @@ func (ns NullLogSource) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return ns.LogSource, nil + return string(ns.LogSource), nil } func (e LogSource) Valid() bool { @@ -439,7 +439,7 @@ func (ns NullLoginType) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return ns.LoginType, nil + return string(ns.LoginType), nil } func (e LoginType) Valid() bool { @@ -502,7 +502,7 @@ func (ns NullParameterDestinationScheme) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return ns.ParameterDestinationScheme, nil + return string(ns.ParameterDestinationScheme), nil } func (e ParameterDestinationScheme) Valid() bool { @@ -563,7 +563,7 @@ func (ns NullParameterScope) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return ns.ParameterScope, nil + return string(ns.ParameterScope), nil } func (e ParameterScope) Valid() bool { @@ -623,7 +623,7 @@ func (ns NullParameterSourceScheme) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return ns.ParameterSourceScheme, nil + return string(ns.ParameterSourceScheme), nil } func (e ParameterSourceScheme) Valid() bool { @@ -681,7 +681,7 @@ func (ns NullParameterTypeSystem) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return ns.ParameterTypeSystem, nil + return string(ns.ParameterTypeSystem), nil } func (e ParameterTypeSystem) Valid() bool { @@ -740,7 +740,7 @@ func (ns NullProvisionerJobType) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return ns.ProvisionerJobType, nil + return string(ns.ProvisionerJobType), nil } func (e ProvisionerJobType) Valid() bool { @@ -799,7 +799,7 @@ func (ns NullProvisionerStorageMethod) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return ns.ProvisionerStorageMethod, nil + return string(ns.ProvisionerStorageMethod), nil } func (e ProvisionerStorageMethod) Valid() bool { @@ -855,7 +855,7 @@ func (ns NullProvisionerType) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return ns.ProvisionerType, nil + return string(ns.ProvisionerType), nil } func (e ProvisionerType) Valid() bool { @@ -922,7 +922,7 @@ func (ns NullResourceType) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return ns.ResourceType, nil + return string(ns.ResourceType), nil } func (e ResourceType) Valid() bool { @@ -998,7 +998,7 @@ func (ns NullUserStatus) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return ns.UserStatus, nil + return string(ns.UserStatus), nil } func (e UserStatus) Valid() bool { @@ -1063,7 +1063,7 @@ func (ns NullWorkspaceAgentLifecycleState) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return ns.WorkspaceAgentLifecycleState, nil + return string(ns.WorkspaceAgentLifecycleState), nil } func (e WorkspaceAgentLifecycleState) Valid() bool { @@ -1136,7 +1136,7 @@ func (ns NullWorkspaceAgentSubsystem) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return ns.WorkspaceAgentSubsystem, nil + return string(ns.WorkspaceAgentSubsystem), nil } func (e WorkspaceAgentSubsystem) Valid() bool { @@ -1198,7 +1198,7 @@ func (ns NullWorkspaceAppHealth) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return ns.WorkspaceAppHealth, nil + return string(ns.WorkspaceAppHealth), nil } func (e WorkspaceAppHealth) Valid() bool { @@ -1261,7 +1261,7 @@ func (ns NullWorkspaceTransition) Value() (driver.Value, error) { if !ns.Valid { return nil, nil } - return ns.WorkspaceTransition, nil + return string(ns.WorkspaceTransition), nil } func (e WorkspaceTransition) Valid() bool { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 318cdb14f469f..26978d240623e 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.16.0 +// sqlc v1.17.2 package database diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index d4c2307d3e3a7..358c5af7934a8 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.16.0 +// sqlc v1.17.2 package database From 16c9c8f92e666a257c2bf35184ada619b57b984b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 24 May 2023 15:13:12 +0200 Subject: [PATCH 17/18] Format --- coderd/metricscache/metricscache_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/coderd/metricscache/metricscache_test.go b/coderd/metricscache/metricscache_test.go index 7815bc2635c19..ee6c9e8e54884 100644 --- a/coderd/metricscache/metricscache_test.go +++ b/coderd/metricscache/metricscache_test.go @@ -103,7 +103,8 @@ func TestCache_TemplateUsers(t *testing.T) { Date: date(2022, 8, 29), Amount: 1, }, - }, 1}}, + }, 1}, + }, { name: "holes", args: args{ @@ -144,7 +145,8 @@ func TestCache_TemplateUsers(t *testing.T) { Date: date(2022, 1, 7), Amount: 2, }, - }, 2}}, + }, 2}, + }, { name: "tzOffset", tzOffset: -1, From a86bcd67e2904cf3c09203fcf11c0f3adbcffda1 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 24 May 2023 16:24:27 +0200 Subject: [PATCH 18/18] Format --- coderd/metricscache/metricscache.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/coderd/metricscache/metricscache.go b/coderd/metricscache/metricscache.go index 40af1f860edd7..e70f193ee0c3c 100644 --- a/coderd/metricscache/metricscache.go +++ b/coderd/metricscache/metricscache.go @@ -21,15 +21,13 @@ import ( "github.com/coder/retry" ) -var ( - // timezoneOffsets are the timezones that are cached and supported. - // Any non-listed timezone offsets will need to use the closest supported one. - timezoneOffsets = []int{ - 0, // UTC - is listed first intentionally. - -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, - } -) +// timezoneOffsets are the timezones that are cached and supported. +// Any non-listed timezone offsets will need to use the closest supported one. +var timezoneOffsets = []int{ + 0, // UTC - is listed first intentionally. + -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, +} // Cache holds the template metrics. // The aggregation queries responsible for these values can take up to a minute pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy