From c480f0f322b39a26878018c1b78c884ddfd7f125 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 21 Sep 2023 12:30:56 +0200 Subject: [PATCH 01/23] codersdk --- codersdk/insights.go | 56 ++++++++++++++++++++++++++++++++++ site/src/api/typesGenerated.ts | 29 ++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/codersdk/insights.go b/codersdk/insights.go index c9ac0ed2deb79..6312c6f2c4a58 100644 --- a/codersdk/insights.go +++ b/codersdk/insights.go @@ -62,6 +62,30 @@ type UserLatency struct { LatencyMS ConnectionLatency `json:"latency_ms"` } +// UserActivityInsightsResponse is the response from the user activity insights +// endpoint. +type UserActivityInsightsResponse struct { + Report UserActivityInsightsReport `json:"report"` +} + +// UserActivityInsightsReport is the report from the user activity insights +// endpoint. +type UserActivityInsightsReport 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"` + Users []UserActivity `json:"users"` +} + +// UserActivity shows the session time for a user. +type UserActivity struct { + TemplateIDs []uuid.UUID `json:"template_ids" format:"uuid"` + UserID uuid.UUID `json:"user_id" format:"uuid"` + Username string `json:"username"` + AvatarURL string `json:"avatar_url" format:"uri"` + Seconds int64 `json:"seconds" example:"80500"` +} + // ConnectionLatency shows the latency for a connection. type ConnectionLatency struct { P50 float64 `json:"p50" example:"31.312"` @@ -100,6 +124,38 @@ func (c *Client) UserLatencyInsights(ctx context.Context, req UserLatencyInsight return result, json.NewDecoder(resp.Body).Decode(&result) } +type UserActivityInsightsRequest 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"` +} + +func (c *Client) UserActivityInsights(ctx context.Context, req UserActivityInsightsRequest) (UserActivityInsightsResponse, error) { + qp := url.Values{} + qp.Add("start_time", req.StartTime.Format(insightsTimeLayout)) + qp.Add("end_time", req.EndTime.Format(insightsTimeLayout)) + if len(req.TemplateIDs) > 0 { + var templateIDs []string + for _, id := range req.TemplateIDs { + templateIDs = append(templateIDs, id.String()) + } + qp.Add("template_ids", strings.Join(templateIDs, ",")) + } + + reqURL := fmt.Sprintf("/api/v2/insights/user-activity?%s", qp.Encode()) + resp, err := c.Request(ctx, http.MethodGet, reqURL, nil) + if err != nil { + return UserActivityInsightsResponse{}, xerrors.Errorf("make request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return UserActivityInsightsResponse{}, ReadBodyAsError(resp) + } + var result UserActivityInsightsResponse + return result, json.NewDecoder(resp.Body).Decode(&result) +} + // TemplateInsightsResponse is the response from the template insights endpoint. type TemplateInsightsResponse struct { Report TemplateInsightsReport `json:"report"` diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index cd272eb99b12c..9151788ff2e55 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1193,6 +1193,35 @@ export interface User { readonly login_type: LoginType; } +// From codersdk/insights.go +export interface UserActivity { + readonly template_ids: string[]; + readonly user_id: string; + readonly username: string; + readonly avatar_url: string; + readonly seconds: number; +} + +// From codersdk/insights.go +export interface UserActivityInsightsReport { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: string[]; + readonly users: UserActivity[]; +} + +// From codersdk/insights.go +export interface UserActivityInsightsRequest { + readonly start_time: string; + readonly end_time: string; + readonly template_ids: string[]; +} + +// From codersdk/insights.go +export interface UserActivityInsightsResponse { + readonly report: UserActivityInsightsReport; +} + // From codersdk/insights.go export interface UserLatency { readonly template_ids: string[]; From 0baa80b9545ccb7256bcd9923c6d74ac244eeacd Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 21 Sep 2023 12:35:35 +0200 Subject: [PATCH 02/23] stub for user-activity --- coderd/apidoc/docs.go | 86 ++++++++++++++++++++++++++++++++++++++ coderd/apidoc/swagger.json | 82 ++++++++++++++++++++++++++++++++++++ coderd/coderd.go | 1 + coderd/insights.go | 50 ++++++++++++++++++++++ docs/api/insights.md | 44 +++++++++++++++++++ docs/api/schemas.md | 77 ++++++++++++++++++++++++++++++++++ 6 files changed, 340 insertions(+) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index c849cd2ef81e3..b4b84bc8dab43 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -934,6 +934,31 @@ const docTemplate = `{ } } }, + "/insights/user-activity": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Insights" + ], + "summary": "Get insights about user activity", + "operationId": "get-insights-about-user-activity", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.UserActivityInsightsResponse" + } + } + } + } + }, "/insights/user-latency": { "get": { "security": [ @@ -10384,6 +10409,67 @@ const docTemplate = `{ } } }, + "codersdk.UserActivity": { + "type": "object", + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "seconds": { + "type": "integer", + "example": 80500 + }, + "template_ids": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "user_id": { + "type": "string", + "format": "uuid" + }, + "username": { + "type": "string" + } + } + }, + "codersdk.UserActivityInsightsReport": { + "type": "object", + "properties": { + "end_time": { + "type": "string", + "format": "date-time" + }, + "start_time": { + "type": "string", + "format": "date-time" + }, + "template_ids": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "users": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.UserActivity" + } + } + } + }, + "codersdk.UserActivityInsightsResponse": { + "type": "object", + "properties": { + "report": { + "$ref": "#/definitions/codersdk.UserActivityInsightsReport" + } + } + }, "codersdk.UserLatency": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 843a09d51c464..de2800053e415 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -804,6 +804,27 @@ } } }, + "/insights/user-activity": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Insights"], + "summary": "Get insights about user activity", + "operationId": "get-insights-about-user-activity", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.UserActivityInsightsResponse" + } + } + } + } + }, "/insights/user-latency": { "get": { "security": [ @@ -9398,6 +9419,67 @@ } } }, + "codersdk.UserActivity": { + "type": "object", + "properties": { + "avatar_url": { + "type": "string", + "format": "uri" + }, + "seconds": { + "type": "integer", + "example": 80500 + }, + "template_ids": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "user_id": { + "type": "string", + "format": "uuid" + }, + "username": { + "type": "string" + } + } + }, + "codersdk.UserActivityInsightsReport": { + "type": "object", + "properties": { + "end_time": { + "type": "string", + "format": "date-time" + }, + "start_time": { + "type": "string", + "format": "date-time" + }, + "template_ids": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "users": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.UserActivity" + } + } + } + }, + "codersdk.UserActivityInsightsResponse": { + "type": "object", + "properties": { + "report": { + "$ref": "#/definitions/codersdk.UserActivityInsightsReport" + } + } + }, "codersdk.UserLatency": { "type": "object", "properties": { diff --git a/coderd/coderd.go b/coderd/coderd.go index ccbb0fc703959..24f0b38f08dc9 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -895,6 +895,7 @@ func New(options *Options) *API { r.Route("/insights", func(r chi.Router) { r.Use(apiKeyMiddleware) r.Get("/daus", api.deploymentDAUs) + r.Get("/user-activity", api.insightsUserActivity) r.Get("/user-latency", api.insightsUserLatency) r.Get("/templates", api.insightsTemplates) }) diff --git a/coderd/insights.go b/coderd/insights.go index bb080744c3f7f..ec954d5506300 100644 --- a/coderd/insights.go +++ b/coderd/insights.go @@ -59,6 +59,56 @@ func (api *API) deploymentDAUs(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, resp) } +// @Summary Get insights about user activity +// @ID get-insights-about-user-activity +// @Security CoderSessionToken +// @Produce json +// @Tags Insights +// @Success 200 {object} codersdk.UserActivityInsightsResponse +// @Router /insights/user-activity [get] +func (api *API) insightsUserActivity(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + p := httpapi.NewQueryParamParser(). + Required("start_time"). + Required("end_time") + vals := r.URL.Query() + var ( + // The QueryParamParser does not preserve timezone, so we need + // to parse the time ourselves. + startTimeString = p.String(vals, "", "start_time") + endTimeString = p.String(vals, "", "end_time") + templateIDs = p.UUIDs(vals, []uuid.UUID{}, "template_ids") + ) + p.ErrorExcessParams(vals) + if len(p.Errors) > 0 { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Query parameters have invalid values.", + Validations: p.Errors, + }) + return + } + + startTime, endTime, ok := parseInsightsStartAndEndTime(ctx, rw, startTimeString, endTimeString) + if !ok { + return + } + + // TODO + + seenTemplateIDs := templateIDs // FIXME + + resp := codersdk.UserActivityInsightsResponse{ + Report: codersdk.UserActivityInsightsReport{ + StartTime: startTime, + EndTime: endTime, + TemplateIDs: seenTemplateIDs, + Users: nil, // FIXME + }, + } + httpapi.Write(ctx, rw, http.StatusOK, resp) +} + // @Summary Get insights about user latency // @ID get-insights-about-user-latency // @Security CoderSessionToken diff --git a/docs/api/insights.md b/docs/api/insights.md index 3c2bb2ba9261b..bfa1fcd380d5d 100644 --- a/docs/api/insights.md +++ b/docs/api/insights.md @@ -115,6 +115,50 @@ curl -X GET http://coder-server:8080/api/v2/insights/templates \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Get insights about user activity + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/insights/user-activity \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /insights/user-activity` + +### Example responses + +> 200 Response + +```json +{ + "report": { + "end_time": "2019-08-24T14:15:22Z", + "start_time": "2019-08-24T14:15:22Z", + "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "users": [ + { + "avatar_url": "http://example.com", + "seconds": 80500, + "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5", + "username": "string" + } + ] + } +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.UserActivityInsightsResponse](schemas.md#codersdkuseractivityinsightsresponse) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get insights about user latency ### Code samples diff --git a/docs/api/schemas.md b/docs/api/schemas.md index e2546adcf8ad7..e8a7b0090d21d 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -5179,6 +5179,83 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `status` | `active` | | `status` | `suspended` | +## codersdk.UserActivity + +```json +{ + "avatar_url": "http://example.com", + "seconds": 80500, + "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5", + "username": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| -------------- | --------------- | -------- | ------------ | ----------- | +| `avatar_url` | string | false | | | +| `seconds` | integer | false | | | +| `template_ids` | array of string | false | | | +| `user_id` | string | false | | | +| `username` | string | false | | | + +## codersdk.UserActivityInsightsReport + +```json +{ + "end_time": "2019-08-24T14:15:22Z", + "start_time": "2019-08-24T14:15:22Z", + "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "users": [ + { + "avatar_url": "http://example.com", + "seconds": 80500, + "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5", + "username": "string" + } + ] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| -------------- | ------------------------------------------------------- | -------- | ------------ | ----------- | +| `end_time` | string | false | | | +| `start_time` | string | false | | | +| `template_ids` | array of string | false | | | +| `users` | array of [codersdk.UserActivity](#codersdkuseractivity) | false | | | + +## codersdk.UserActivityInsightsResponse + +```json +{ + "report": { + "end_time": "2019-08-24T14:15:22Z", + "start_time": "2019-08-24T14:15:22Z", + "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "users": [ + { + "avatar_url": "http://example.com", + "seconds": 80500, + "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5", + "username": "string" + } + ] + } +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| -------- | -------------------------------------------------------------------------- | -------- | ------------ | ----------- | +| `report` | [codersdk.UserActivityInsightsReport](#codersdkuseractivityinsightsreport) | false | | | + ## codersdk.UserLatency ```json From 71eea7a7f804d0278fe474078f7a06bc085dfb76 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 21 Sep 2023 14:16:28 +0200 Subject: [PATCH 03/23] WIP --- coderd/database/dbauthz/dbauthz.go | 4 ++ coderd/database/dbfake/dbfake.go | 9 +++ coderd/database/dbmetrics/dbmetrics.go | 7 +++ coderd/database/dbmock/dbmock.go | 15 +++++ coderd/database/querier.go | 4 ++ coderd/database/queries.sql.go | 86 ++++++++++++++++++++++++++ coderd/database/queries/insights.sql | 42 +++++++++++++ 7 files changed, 167 insertions(+) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 8eb3a44560a6a..ecd9778f0efd1 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1464,6 +1464,10 @@ func (q *querier) GetUnexpiredLicenses(ctx context.Context) ([]database.License, return q.db.GetUnexpiredLicenses(ctx) } +func (q *querier) GetUserActivityInsights(ctx context.Context, arg database.GetUserActivityInsightsParams) ([]database.GetUserActivityInsightsRow, error) { + panic("not implemented") +} + func (q *querier) GetUserByEmailOrUsername(ctx context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) { return fetch(q.log, q.auth, q.db.GetUserByEmailOrUsername)(ctx, arg) } diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 7d44c634ee49c..138cfb01be5a9 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -2816,6 +2816,15 @@ func (q *FakeQuerier) GetUnexpiredLicenses(_ context.Context) ([]database.Licens return results, nil } +func (q *FakeQuerier) GetUserActivityInsights(ctx context.Context, arg database.GetUserActivityInsightsParams) ([]database.GetUserActivityInsightsRow, error) { + err := validateDatabaseType(arg) + if err != nil { + return nil, err + } + + panic("not implemented") +} + func (q *FakeQuerier) GetUserByEmailOrUsername(_ context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) { if err := validateDatabaseType(arg); err != nil { return database.User{}, err diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index a1210ad7435eb..892794b0624d1 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -760,6 +760,13 @@ func (m metricsStore) GetUnexpiredLicenses(ctx context.Context) ([]database.Lice return licenses, err } +func (m metricsStore) GetUserActivityInsights(ctx context.Context, arg database.GetUserActivityInsightsParams) ([]database.GetUserActivityInsightsRow, error) { + start := time.Now() + r0, r1 := m.s.GetUserActivityInsights(ctx, arg) + m.queryLatencies.WithLabelValues("GetUserActivityInsights").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m metricsStore) GetUserByEmailOrUsername(ctx context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) { start := time.Now() user, err := m.s.GetUserByEmailOrUsername(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 3a791a601383a..f8fad1afc8298 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -1570,6 +1570,21 @@ func (mr *MockStoreMockRecorder) GetUnexpiredLicenses(arg0 interface{}) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUnexpiredLicenses", reflect.TypeOf((*MockStore)(nil).GetUnexpiredLicenses), arg0) } +// GetUserActivityInsights mocks base method. +func (m *MockStore) GetUserActivityInsights(arg0 context.Context, arg1 database.GetUserActivityInsightsParams) ([]database.GetUserActivityInsightsRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserActivityInsights", arg0, arg1) + ret0, _ := ret[0].([]database.GetUserActivityInsightsRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserActivityInsights indicates an expected call of GetUserActivityInsights. +func (mr *MockStoreMockRecorder) GetUserActivityInsights(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserActivityInsights", reflect.TypeOf((*MockStore)(nil).GetUserActivityInsights), arg0, arg1) +} + // GetUserByEmailOrUsername mocks base method. func (m *MockStore) GetUserByEmailOrUsername(arg0 context.Context, arg1 database.GetUserByEmailOrUsernameParams) (database.User, error) { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 483976c0355d7..97430f4640483 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -151,6 +151,10 @@ type sqlcQuerier interface { GetTemplates(ctx context.Context) ([]Template, error) GetTemplatesWithFilter(ctx context.Context, arg GetTemplatesWithFilterParams) ([]Template, error) GetUnexpiredLicenses(ctx context.Context) ([]License, error) + // GetUserActivityInsights returns the ranking with top active users. + // The result can be filtered on template_ids, meaning only user data from workspaces + // based on those templates will be included. + GetUserActivityInsights(ctx context.Context, arg GetUserActivityInsightsParams) ([]GetUserActivityInsightsRow, error) GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) GetUserCount(ctx context.Context) (int64, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 17b12293e0c08..fd76eafd9f49e 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2022,6 +2022,92 @@ func (q *sqlQuerier) GetTemplateParameterInsights(ctx context.Context, arg GetTe return items, nil } +const getUserActivityInsights = `-- name: GetUserActivityInsights :many +WITH app_stats_by_user_and_agent AS ( + SELECT + s.start_time, + 60 as seconds, + w.template_id, + was.user_id + FROM workspace_app_stats was + JOIN workspaces w ON ( + w.id = was.workspace_id + AND CASE WHEN COALESCE(array_length($1::uuid[], 1), 0) > 0 THEN w.template_id = ANY($1::uuid[]) ELSE TRUE END + ) + -- This table contains both 1 minute entries and >1 minute entries, + -- to calculate this with our uniqueness constraints, we generate series + -- for the longer intervals. + CROSS JOIN LATERAL generate_series( + date_trunc('minute', was.session_started_at), + -- Subtract 1 microsecond to avoid creating an extra series. + date_trunc('minute', was.session_ended_at - '1 microsecond'::interval), + '1 minute'::interval + ) s(start_time) + WHERE + s.start_time >= $2::timestamptz + -- Subtract one minute because the series only contains the start time. + AND s.start_time < ($3::timestamptz) - '1 minute'::interval + GROUP BY s.start_time, w.template_id, was.user_id +) + +SELECT + users.id, + users.username, + users.avatar_url, + array_agg(DISTINCT template_id)::uuid[] AS template_ids, + SUM(seconds) AS usage_seconds +FROM app_stats_by_user_and_agent +JOIN users ON (users.id = app_stats_by_user_and_agent.user_id) +GROUP BY user_id, username, avatar_url +ORDER BY user_id ASC +` + +type GetUserActivityInsightsParams struct { + TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"` + StartTime time.Time `db:"start_time" json:"start_time"` + EndTime time.Time `db:"end_time" json:"end_time"` +} + +type GetUserActivityInsightsRow struct { + ID uuid.UUID `db:"id" json:"id"` + Username string `db:"username" json:"username"` + AvatarURL sql.NullString `db:"avatar_url" json:"avatar_url"` + TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"` + UsageSeconds int64 `db:"usage_seconds" json:"usage_seconds"` +} + +// GetUserActivityInsights returns the ranking with top active users. +// The result can be filtered on template_ids, meaning only user data from workspaces +// based on those templates will be included. +func (q *sqlQuerier) GetUserActivityInsights(ctx context.Context, arg GetUserActivityInsightsParams) ([]GetUserActivityInsightsRow, error) { + rows, err := q.db.QueryContext(ctx, getUserActivityInsights, pq.Array(arg.TemplateIDs), arg.StartTime, arg.EndTime) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetUserActivityInsightsRow + for rows.Next() { + var i GetUserActivityInsightsRow + if err := rows.Scan( + &i.ID, + &i.Username, + &i.AvatarURL, + pq.Array(&i.TemplateIDs), + &i.UsageSeconds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getUserLatencyInsights = `-- name: GetUserLatencyInsights :many SELECT workspace_agent_stats.user_id, diff --git a/coderd/database/queries/insights.sql b/coderd/database/queries/insights.sql index dc69b69de2eef..6e1a3a6ce0a95 100644 --- a/coderd/database/queries/insights.sql +++ b/coderd/database/queries/insights.sql @@ -21,6 +21,48 @@ WHERE GROUP BY workspace_agent_stats.user_id, users.username, users.avatar_url ORDER BY user_id ASC; +-- name: GetUserActivityInsights :many +-- GetUserActivityInsights returns the ranking with top active users. +-- The result can be filtered on template_ids, meaning only user data from workspaces +-- based on those templates will be included. +WITH app_stats_by_user_and_agent AS ( + SELECT + s.start_time, + 60 as seconds, + w.template_id, + was.user_id + FROM workspace_app_stats was + JOIN workspaces w ON ( + w.id = was.workspace_id + AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN w.template_id = ANY(@template_ids::uuid[]) ELSE TRUE END + ) + -- This table contains both 1 minute entries and >1 minute entries, + -- to calculate this with our uniqueness constraints, we generate series + -- for the longer intervals. + CROSS JOIN LATERAL generate_series( + date_trunc('minute', was.session_started_at), + -- Subtract 1 microsecond to avoid creating an extra series. + date_trunc('minute', was.session_ended_at - '1 microsecond'::interval), + '1 minute'::interval + ) s(start_time) + WHERE + s.start_time >= @start_time::timestamptz + -- Subtract one minute because the series only contains the start time. + AND s.start_time < (@end_time::timestamptz) - '1 minute'::interval + GROUP BY s.start_time, w.template_id, was.user_id +) + +SELECT + users.id, + users.username, + users.avatar_url, + array_agg(DISTINCT template_id)::uuid[] AS template_ids, + SUM(seconds) AS usage_seconds +FROM app_stats_by_user_and_agent +JOIN users ON (users.id = app_stats_by_user_and_agent.user_id) +GROUP BY user_id, username, avatar_url +ORDER BY user_id ASC; + -- name: GetTemplateInsights :one -- GetTemplateInsights has a granularity of 5 minutes where if a session/app was -- in use during a minute, we will add 5 minutes to the total usage for that From 8bff641d625a4d8bf927407d97922d28880464a9 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 21 Sep 2023 15:22:47 +0200 Subject: [PATCH 04/23] fixes --- coderd/database/queries.sql.go | 6 ++-- coderd/database/queries/insights.sql | 2 +- coderd/insights.go | 43 ++++++++++++++++++++++++++-- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index fd76eafd9f49e..34f77d9a75bcd 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2051,7 +2051,7 @@ WITH app_stats_by_user_and_agent AS ( ) SELECT - users.id, + users.id as user_id, users.username, users.avatar_url, array_agg(DISTINCT template_id)::uuid[] AS template_ids, @@ -2069,7 +2069,7 @@ type GetUserActivityInsightsParams struct { } type GetUserActivityInsightsRow struct { - ID uuid.UUID `db:"id" json:"id"` + UserID uuid.UUID `db:"user_id" json:"user_id"` Username string `db:"username" json:"username"` AvatarURL sql.NullString `db:"avatar_url" json:"avatar_url"` TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"` @@ -2089,7 +2089,7 @@ func (q *sqlQuerier) GetUserActivityInsights(ctx context.Context, arg GetUserAct for rows.Next() { var i GetUserActivityInsightsRow if err := rows.Scan( - &i.ID, + &i.UserID, &i.Username, &i.AvatarURL, pq.Array(&i.TemplateIDs), diff --git a/coderd/database/queries/insights.sql b/coderd/database/queries/insights.sql index 6e1a3a6ce0a95..b37682826d1fe 100644 --- a/coderd/database/queries/insights.sql +++ b/coderd/database/queries/insights.sql @@ -53,7 +53,7 @@ WITH app_stats_by_user_and_agent AS ( ) SELECT - users.id, + users.id as user_id, users.username, users.avatar_url, array_agg(DISTINCT template_id)::uuid[] AS template_ids, diff --git a/coderd/insights.go b/coderd/insights.go index ec954d5506300..a0113b15c131e 100644 --- a/coderd/insights.go +++ b/coderd/insights.go @@ -94,16 +94,53 @@ func (api *API) insightsUserActivity(rw http.ResponseWriter, r *http.Request) { return } - // TODO + rows, err := api.Database.GetUserActivityInsights(ctx, database.GetUserActivityInsightsParams{ + StartTime: startTime, + EndTime: endTime, + TemplateIDs: templateIDs, + }) + if err != nil { + if httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return + } + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching user activity.", + Detail: err.Error(), + }) + return + } + + templateIDSet := make(map[uuid.UUID]struct{}) + userActivities := make([]codersdk.UserActivity, 0, len(rows)) + for _, row := range rows { + for _, templateID := range row.TemplateIDs { + templateIDSet[templateID] = struct{}{} + } + userActivities = append(userActivities, codersdk.UserActivity{ + TemplateIDs: row.TemplateIDs, + UserID: row.UserID, + Username: row.Username, + AvatarURL: row.AvatarURL.String, + Seconds: row.UsageSeconds, + }) + } - seenTemplateIDs := templateIDs // FIXME + // TemplateIDs that contributed to the data. + seenTemplateIDs := make([]uuid.UUID, 0, len(templateIDSet)) + for templateID := range templateIDSet { + seenTemplateIDs = append(seenTemplateIDs, templateID) + } + slices.SortFunc(seenTemplateIDs, func(a, b uuid.UUID) int { + return slice.Ascending(a.String(), b.String()) + }) resp := codersdk.UserActivityInsightsResponse{ Report: codersdk.UserActivityInsightsReport{ StartTime: startTime, EndTime: endTime, TemplateIDs: seenTemplateIDs, - Users: nil, // FIXME + Users: userActivities, }, } httpapi.Write(ctx, rw, http.StatusOK, resp) From 1c6798fc7743585a90bcdd9d43081439dbfd1f8b Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 21 Sep 2023 15:24:30 +0200 Subject: [PATCH 05/23] dbauthz --- coderd/database/dbauthz/dbauthz.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index ecd9778f0efd1..69603d37c6d78 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1465,7 +1465,22 @@ func (q *querier) GetUnexpiredLicenses(ctx context.Context) ([]database.License, } func (q *querier) GetUserActivityInsights(ctx context.Context, arg database.GetUserActivityInsightsParams) ([]database.GetUserActivityInsightsRow, error) { - panic("not implemented") + for _, templateID := range arg.TemplateIDs { + template, err := q.db.GetTemplateByID(ctx, templateID) + if err != nil { + return nil, err + } + + if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil { + return nil, err + } + } + if len(arg.TemplateIDs) == 0 { + if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil { + return nil, err + } + } + return q.db.GetUserActivityInsights(ctx, arg) } func (q *querier) GetUserByEmailOrUsername(ctx context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) { From 7109d92857aa410600151b6ad5b19232c42eaa2c Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 22 Sep 2023 13:18:48 +0200 Subject: [PATCH 06/23] Adjust SQL query --- coderd/database/queries.sql.go | 52 ++++++++++++++++++++++++---- coderd/database/queries/insights.sql | 52 ++++++++++++++++++++++++---- 2 files changed, 90 insertions(+), 14 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 34f77d9a75bcd..68d8f32cf2fdb 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2023,12 +2023,12 @@ func (q *sqlQuerier) GetTemplateParameterInsights(ctx context.Context, arg GetTe } const getUserActivityInsights = `-- name: GetUserActivityInsights :many -WITH app_stats_by_user_and_agent AS ( +WITH app_stats AS ( SELECT s.start_time, - 60 as seconds, + was.user_id, w.template_id, - was.user_id + 60 as seconds FROM workspace_app_stats was JOIN workspaces w ON ( w.id = was.workspace_id @@ -2048,17 +2048,55 @@ WITH app_stats_by_user_and_agent AS ( -- Subtract one minute because the series only contains the start time. AND s.start_time < ($3::timestamptz) - '1 minute'::interval GROUP BY s.start_time, w.template_id, was.user_id +), session_stats AS ( + SELECT + date_trunc('minute', was.created_at) as start_time, + was.user_id, + was.template_id, + CASE WHEN + SUM(was.session_count_vscode) > 0 OR + SUM(was.session_count_jetbrains) > 0 OR + SUM(was.session_count_reconnecting_pty) > 0 OR + SUM(was.session_count_ssh) > 0 + THEN 60 ELSE 0 END as seconds + FROM workspace_agent_stats was + WHERE + was.created_at >= $2::timestamptz + AND was.created_at $3::timestamptz + AND was.connection_count > 0 + AND CASE WHEN COALESCE(array_length($1::uuid[], 1), 0) > 0 THEN w.template_id = ANY($1::uuid[]) ELSE TRUE END + GROUP BY date_trunc('minute', was.created_at), was.user_id, was.template_id +), combined_stats AS ( + SELECT + user_id, + template_id, + start_time, + seconds + FROM app_stats + UNION ALL + SELECT + user_id, + template_id, + start_time, + seconds + FROM session_stats +), distinct_combined_stats AS ( + SELECT DISTINCT + user_id, + template_id, + start_time, + seconds + FROM combined_stats ) - SELECT users.id as user_id, users.username, users.avatar_url, array_agg(DISTINCT template_id)::uuid[] AS template_ids, SUM(seconds) AS usage_seconds -FROM app_stats_by_user_and_agent -JOIN users ON (users.id = app_stats_by_user_and_agent.user_id) -GROUP BY user_id, username, avatar_url +FROM distinct_combined_stats +JOIN users ON (users.id = distinct_combined_stats.user_id) +GROUP BY users.id, username, avatar_url ORDER BY user_id ASC ` diff --git a/coderd/database/queries/insights.sql b/coderd/database/queries/insights.sql index b37682826d1fe..f7abe95ababa0 100644 --- a/coderd/database/queries/insights.sql +++ b/coderd/database/queries/insights.sql @@ -25,12 +25,12 @@ ORDER BY user_id ASC; -- GetUserActivityInsights returns the ranking with top active users. -- The result can be filtered on template_ids, meaning only user data from workspaces -- based on those templates will be included. -WITH app_stats_by_user_and_agent AS ( +WITH app_stats AS ( SELECT s.start_time, - 60 as seconds, + was.user_id, w.template_id, - was.user_id + 60 as seconds FROM workspace_app_stats was JOIN workspaces w ON ( w.id = was.workspace_id @@ -50,17 +50,55 @@ WITH app_stats_by_user_and_agent AS ( -- Subtract one minute because the series only contains the start time. AND s.start_time < (@end_time::timestamptz) - '1 minute'::interval GROUP BY s.start_time, w.template_id, was.user_id +), session_stats AS ( + SELECT + date_trunc('minute', was.created_at) as start_time, + was.user_id, + was.template_id, + CASE WHEN + SUM(was.session_count_vscode) > 0 OR + SUM(was.session_count_jetbrains) > 0 OR + SUM(was.session_count_reconnecting_pty) > 0 OR + SUM(was.session_count_ssh) > 0 + THEN 60 ELSE 0 END as seconds + FROM workspace_agent_stats was + WHERE + was.created_at >= @start_time::timestamptz + AND was.created_at @end_time::timestamptz + AND was.connection_count > 0 + AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN w.template_id = ANY(@template_ids::uuid[]) ELSE TRUE END + GROUP BY date_trunc('minute', was.created_at), was.user_id, was.template_id +), combined_stats AS ( + SELECT + user_id, + template_id, + start_time, + seconds + FROM app_stats + UNION ALL + SELECT + user_id, + template_id, + start_time, + seconds + FROM session_stats +), distinct_combined_stats AS ( + SELECT DISTINCT + user_id, + template_id, + start_time, + seconds + FROM combined_stats ) - SELECT users.id as user_id, users.username, users.avatar_url, array_agg(DISTINCT template_id)::uuid[] AS template_ids, SUM(seconds) AS usage_seconds -FROM app_stats_by_user_and_agent -JOIN users ON (users.id = app_stats_by_user_and_agent.user_id) -GROUP BY user_id, username, avatar_url +FROM distinct_combined_stats +JOIN users ON (users.id = distinct_combined_stats.user_id) +GROUP BY users.id, username, avatar_url ORDER BY user_id ASC; -- name: GetTemplateInsights :one From 74f7597aa82ee47df5ef1defcac76e80075629db Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 22 Sep 2023 13:32:19 +0200 Subject: [PATCH 07/23] Fixes --- coderd/database/queries.sql.go | 4 ++-- coderd/database/queries/insights.sql | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 68d8f32cf2fdb..e81d2f9440418 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2062,9 +2062,9 @@ WITH app_stats AS ( FROM workspace_agent_stats was WHERE was.created_at >= $2::timestamptz - AND was.created_at $3::timestamptz + AND was.created_at < $3::timestamptz AND was.connection_count > 0 - AND CASE WHEN COALESCE(array_length($1::uuid[], 1), 0) > 0 THEN w.template_id = ANY($1::uuid[]) ELSE TRUE END + AND CASE WHEN COALESCE(array_length($1::uuid[], 1), 0) > 0 THEN was.template_id = ANY($1::uuid[]) ELSE TRUE END GROUP BY date_trunc('minute', was.created_at), was.user_id, was.template_id ), combined_stats AS ( SELECT diff --git a/coderd/database/queries/insights.sql b/coderd/database/queries/insights.sql index f7abe95ababa0..eccdcd1ec0440 100644 --- a/coderd/database/queries/insights.sql +++ b/coderd/database/queries/insights.sql @@ -64,9 +64,9 @@ WITH app_stats AS ( FROM workspace_agent_stats was WHERE was.created_at >= @start_time::timestamptz - AND was.created_at @end_time::timestamptz + AND was.created_at < @end_time::timestamptz AND was.connection_count > 0 - AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN w.template_id = ANY(@template_ids::uuid[]) ELSE TRUE END + AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN was.template_id = ANY(@template_ids::uuid[]) ELSE TRUE END GROUP BY date_trunc('minute', was.created_at), was.user_id, was.template_id ), combined_stats AS ( SELECT From e2077825b29d3453efa761333752a9c1168209cc Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 22 Sep 2023 15:51:03 +0200 Subject: [PATCH 08/23] Fix: combined --- coderd/database/queries.sql.go | 13 +++---------- coderd/database/queries/insights.sql | 13 +++---------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index e81d2f9440418..ef20ccde3f6c3 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2073,20 +2073,13 @@ WITH app_stats AS ( start_time, seconds FROM app_stats - UNION ALL + UNION SELECT user_id, template_id, start_time, seconds FROM session_stats -), distinct_combined_stats AS ( - SELECT DISTINCT - user_id, - template_id, - start_time, - seconds - FROM combined_stats ) SELECT users.id as user_id, @@ -2094,8 +2087,8 @@ SELECT users.avatar_url, array_agg(DISTINCT template_id)::uuid[] AS template_ids, SUM(seconds) AS usage_seconds -FROM distinct_combined_stats -JOIN users ON (users.id = distinct_combined_stats.user_id) +FROM combined_stats +JOIN users ON (users.id = combined_stats.user_id) GROUP BY users.id, username, avatar_url ORDER BY user_id ASC ` diff --git a/coderd/database/queries/insights.sql b/coderd/database/queries/insights.sql index eccdcd1ec0440..ba73e4feb6bc5 100644 --- a/coderd/database/queries/insights.sql +++ b/coderd/database/queries/insights.sql @@ -75,20 +75,13 @@ WITH app_stats AS ( start_time, seconds FROM app_stats - UNION ALL + UNION SELECT user_id, template_id, start_time, seconds FROM session_stats -), distinct_combined_stats AS ( - SELECT DISTINCT - user_id, - template_id, - start_time, - seconds - FROM combined_stats ) SELECT users.id as user_id, @@ -96,8 +89,8 @@ SELECT users.avatar_url, array_agg(DISTINCT template_id)::uuid[] AS template_ids, SUM(seconds) AS usage_seconds -FROM distinct_combined_stats -JOIN users ON (users.id = distinct_combined_stats.user_id) +FROM combined_stats +JOIN users ON (users.id = combined_stats.user_id) GROUP BY users.id, username, avatar_url ORDER BY user_id ASC; From 27e97e92298f8f4e334ce9f13ef0bf3982c8981d Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 22 Sep 2023 18:11:49 +0200 Subject: [PATCH 09/23] Implement dbfake --- coderd/database/dbfake/dbfake.go | 129 ++++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 1 deletion(-) diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 138cfb01be5a9..36f35ab614cdf 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -2822,7 +2822,134 @@ func (q *FakeQuerier) GetUserActivityInsights(ctx context.Context, arg database. return nil, err } - panic("not implemented") + q.mutex.RLock() + defer q.mutex.RUnlock() + + type uniqueKey struct { + TemplateID uuid.UUID + UserID uuid.UUID + } + + combinedStats := make(map[uniqueKey]map[time.Time]int64) + + // Get application stats + for _, s := range q.workspaceAppStats { + if !(((s.SessionStartedAt.After(arg.StartTime) || s.SessionStartedAt.Equal(arg.StartTime)) && s.SessionStartedAt.Before(arg.EndTime)) || + (s.SessionEndedAt.After(arg.StartTime) && s.SessionEndedAt.Before(arg.EndTime)) || + (s.SessionStartedAt.Before(arg.StartTime) && (s.SessionEndedAt.After(arg.EndTime) || s.SessionEndedAt.Equal(arg.EndTime)))) { + continue + } + + w, err := q.getWorkspaceByIDNoLock(ctx, s.WorkspaceID) + if err != nil { + return nil, err + } + + if len(arg.TemplateIDs) > 0 && !slices.Contains(arg.TemplateIDs, w.TemplateID) { + continue + } + + key := uniqueKey{ + TemplateID: w.TemplateID, + UserID: s.UserID, + } + if combinedStats[key] == nil { + combinedStats[key] = make(map[time.Time]int64) + } + + t := s.SessionStartedAt.Truncate(5 * time.Minute) + if t.Before(arg.StartTime) { + t = arg.StartTime + } + for t.Before(s.SessionEndedAt) && t.Before(arg.EndTime) { + combinedStats[key][t] = 60 + t = t.Add(1 * time.Minute) + } + } + + // Get session stats + for _, s := range q.workspaceAgentStats { + if s.CreatedAt.Before(arg.StartTime) || s.CreatedAt.Equal(arg.EndTime) || s.CreatedAt.After(arg.EndTime) { + continue + } + if len(arg.TemplateIDs) > 0 && !slices.Contains(arg.TemplateIDs, s.TemplateID) { + continue + } + if s.ConnectionCount == 0 { + continue + } + + key := uniqueKey{ + TemplateID: s.TemplateID, + UserID: s.UserID, + } + + if combinedStats[key] == nil { + combinedStats[key] = make(map[time.Time]int64) + } + + if s.SessionCountJetBrains > 0 || s.SessionCountVSCode > 0 || s.SessionCountReconnectingPTY > 0 || s.SessionCountSSH > 0 { + t := s.CreatedAt.Truncate(time.Minute) + combinedStats[key][t] = 60 + } + } + + // Use temporary maps for aggregation purposes + mUserIDTemplateIDs := map[uuid.UUID]map[uuid.UUID]struct{}{} + mUserIDUsageSeconds := map[uuid.UUID]int64{} + + for key, times := range combinedStats { + if mUserIDTemplateIDs[key.UserID] == nil { + mUserIDTemplateIDs[key.UserID] = make(map[uuid.UUID]struct{}) + mUserIDUsageSeconds[key.UserID] = 0 + } + + if _, ok := mUserIDTemplateIDs[key.UserID][key.TemplateID]; !ok { + mUserIDTemplateIDs[key.UserID][key.TemplateID] = struct{}{} + } + + for _, t := range times { + mUserIDUsageSeconds[key.UserID] += t + } + } + + userIDs := make([]uuid.UUID, 0, len(mUserIDUsageSeconds)) + for userID := range mUserIDUsageSeconds { + userIDs = append(userIDs, userID) + } + sort.Slice(userIDs, func(i, j int) bool { + return userIDs[i].String() < userIDs[j].String() + }) + + // Finally, select stats + var rows []database.GetUserActivityInsightsRow + + for _, userID := range userIDs { + user, err := q.getUserByIDNoLock(userID) + if err != nil { + return nil, err + } + + tids := mUserIDTemplateIDs[userID] + templateIDs := make([]uuid.UUID, 0, len(tids)) + for key := range tids { + templateIDs = append(templateIDs, key) + } + sort.Slice(templateIDs, func(i, j int) bool { + return templateIDs[i].String() < templateIDs[j].String() + }) + + row := database.GetUserActivityInsightsRow{ + UserID: user.ID, + Username: user.Username, + AvatarURL: user.AvatarURL, + TemplateIDs: templateIDs, + UsageSeconds: mUserIDUsageSeconds[userID], + } + + rows = append(rows, row) + } + return rows, nil } func (q *FakeQuerier) GetUserByEmailOrUsername(_ context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) { From e19c83585295cbc86c58cbd00ffc82115c4d9331 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 25 Sep 2023 12:19:32 +0200 Subject: [PATCH 10/23] RBAC tests --- coderd/insights_test.go | 221 ++++++++++++++++++++++++++++------------ 1 file changed, 156 insertions(+), 65 deletions(-) diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 8be3f4b5e6076..db9dacb3cce97 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -1293,98 +1293,189 @@ func TestUserLatencyInsights_RBAC(t *testing.T) { today := time.Date(y, m, d, 0, 0, 0, 0, time.UTC) type test struct { - interval codersdk.InsightsReportInterval withTemplate bool } tests := []test{ - {codersdk.InsightsReportIntervalDay, true}, - {codersdk.InsightsReportIntervalDay, false}, - {"", true}, - {"", false}, + {true}, + {false}, } for _, tt := range tests { tt := tt - t.Run(fmt.Sprintf("with interval=%q", tt.interval), func(t *testing.T) { + + t.Run("AsOwner", func(t *testing.T) { t.Parallel() - t.Run("AsOwner", func(t *testing.T) { - t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{}) + admin := coderdtest.CreateFirstUser(t, client) - client := coderdtest.New(t, &coderdtest.Options{}) - admin := coderdtest.CreateFirstUser(t, client) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) - defer cancel() + var templateIDs []uuid.UUID + if tt.withTemplate { + version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) + templateIDs = append(templateIDs, template.ID) + } - var templateIDs []uuid.UUID - if tt.withTemplate { - version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) - templateIDs = append(templateIDs, template.ID) - } + _, err := client.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{ + StartTime: today, + EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. + TemplateIDs: templateIDs, + }) + require.NoError(t, err) + }) + t.Run("AsTemplateAdmin", func(t *testing.T) { + t.Parallel() - _, err := client.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{ - StartTime: today, - EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. - TemplateIDs: templateIDs, - }) - require.NoError(t, err) + client := coderdtest.New(t, &coderdtest.Options{}) + admin := coderdtest.CreateFirstUser(t, client) + + templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleTemplateAdmin()) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + var templateIDs []uuid.UUID + if tt.withTemplate { + version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) + templateIDs = append(templateIDs, template.ID) + } + + _, err := templateAdmin.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{ + StartTime: today, + EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. + TemplateIDs: templateIDs, }) - t.Run("AsTemplateAdmin", func(t *testing.T) { - t.Parallel() + require.NoError(t, err) + }) + t.Run("AsRegularUser", func(t *testing.T) { + t.Parallel() - client := coderdtest.New(t, &coderdtest.Options{}) - admin := coderdtest.CreateFirstUser(t, client) + client := coderdtest.New(t, &coderdtest.Options{}) + admin := coderdtest.CreateFirstUser(t, client) - templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleTemplateAdmin()) + regular, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID) - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) - defer cancel() + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() - var templateIDs []uuid.UUID - if tt.withTemplate { - version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) - templateIDs = append(templateIDs, template.ID) - } + var templateIDs []uuid.UUID + if tt.withTemplate { + version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) + templateIDs = append(templateIDs, template.ID) + } - _, err := templateAdmin.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{ - StartTime: today, - EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. - TemplateIDs: templateIDs, - }) - require.NoError(t, err) + _, err := regular.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{ + StartTime: today, + EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. + TemplateIDs: templateIDs, }) - t.Run("AsRegularUser", func(t *testing.T) { - t.Parallel() + require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) + } +} - client := coderdtest.New(t, &coderdtest.Options{}) - admin := coderdtest.CreateFirstUser(t, client) +func TestUserActivityInsights_RBAC(t *testing.T) { + t.Parallel() - regular, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID) + y, m, d := time.Now().UTC().Date() + today := time.Date(y, m, d, 0, 0, 0, 0, time.UTC) - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) - defer cancel() + type test struct { + withTemplate bool + } - var templateIDs []uuid.UUID - if tt.withTemplate { - version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) - templateIDs = append(templateIDs, template.ID) - } + tests := []test{ + {true}, + {false}, + } - _, err := regular.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{ - StartTime: today, - EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. - TemplateIDs: templateIDs, - }) - require.Error(t, err) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + for _, tt := range tests { + tt := tt + + t.Run("AsOwner", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{}) + admin := coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + var templateIDs []uuid.UUID + if tt.withTemplate { + version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) + templateIDs = append(templateIDs, template.ID) + } + + _, err := client.UserActivityInsights(ctx, codersdk.UserActivityInsightsRequest{ + StartTime: today, + EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. + TemplateIDs: templateIDs, + }) + require.NoError(t, err) + }) + t.Run("AsTemplateAdmin", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{}) + admin := coderdtest.CreateFirstUser(t, client) + + templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleTemplateAdmin()) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + var templateIDs []uuid.UUID + if tt.withTemplate { + version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) + templateIDs = append(templateIDs, template.ID) + } + + _, err := templateAdmin.UserActivityInsights(ctx, codersdk.UserActivityInsightsRequest{ + StartTime: today, + EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. + TemplateIDs: templateIDs, + }) + require.NoError(t, err) + }) + t.Run("AsRegularUser", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{}) + admin := coderdtest.CreateFirstUser(t, client) + + regular, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + var templateIDs []uuid.UUID + if tt.withTemplate { + version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) + templateIDs = append(templateIDs, template.ID) + } + + _, err := regular.UserActivityInsights(ctx, codersdk.UserActivityInsightsRequest{ + StartTime: today, + EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. + TemplateIDs: templateIDs, }) + require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) }) } } From 17d96880b229637798995207cb4e994f2fcc7d3e Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 25 Sep 2023 13:33:57 +0200 Subject: [PATCH 11/23] Refactor tests --- coderd/insights_test.go | 240 ++++++++++++++-------------------------- 1 file changed, 85 insertions(+), 155 deletions(-) diff --git a/coderd/insights_test.go b/coderd/insights_test.go index db9dacb3cce97..7f3a0f2cfa0c0 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -1286,196 +1286,126 @@ func TestTemplateInsights_RBAC(t *testing.T) { } } -func TestUserLatencyInsights_RBAC(t *testing.T) { +func TestGenericInsights_RBAC(t *testing.T) { t.Parallel() y, m, d := time.Now().UTC().Date() today := time.Date(y, m, d, 0, 0, 0, 0, time.UTC) + type fetchInsightsFunc func(ctx context.Context, client *codersdk.Client, startTime, endTime time.Time, templateIDs ...uuid.UUID) error + type test struct { withTemplate bool } - tests := []test{ - {true}, - {false}, - } - - for _, tt := range tests { - tt := tt - - t.Run("AsOwner", func(t *testing.T) { - t.Parallel() - - client := coderdtest.New(t, &coderdtest.Options{}) - admin := coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) - defer cancel() - - var templateIDs []uuid.UUID - if tt.withTemplate { - version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) - templateIDs = append(templateIDs, template.ID) - } - + endpoints := map[string]fetchInsightsFunc{ + "UserLatency": func(ctx context.Context, client *codersdk.Client, startTime, endTime time.Time, templateIDs ...uuid.UUID) error { _, err := client.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{ - StartTime: today, - EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. - TemplateIDs: templateIDs, - }) - require.NoError(t, err) - }) - t.Run("AsTemplateAdmin", func(t *testing.T) { - t.Parallel() - - client := coderdtest.New(t, &coderdtest.Options{}) - admin := coderdtest.CreateFirstUser(t, client) - - templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleTemplateAdmin()) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) - defer cancel() - - var templateIDs []uuid.UUID - if tt.withTemplate { - version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) - templateIDs = append(templateIDs, template.ID) - } - - _, err := templateAdmin.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{ - StartTime: today, - EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. + StartTime: startTime, + EndTime: endTime, TemplateIDs: templateIDs, }) - require.NoError(t, err) - }) - t.Run("AsRegularUser", func(t *testing.T) { - t.Parallel() - - client := coderdtest.New(t, &coderdtest.Options{}) - admin := coderdtest.CreateFirstUser(t, client) - - regular, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) - defer cancel() - - var templateIDs []uuid.UUID - if tt.withTemplate { - version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) - templateIDs = append(templateIDs, template.ID) - } - - _, err := regular.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{ - StartTime: today, - EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. + return err + }, + "UserActivity": func(ctx context.Context, client *codersdk.Client, startTime, endTime time.Time, templateIDs ...uuid.UUID) error { + _, err := client.UserActivityInsights(ctx, codersdk.UserActivityInsightsRequest{ + StartTime: startTime, + EndTime: endTime, TemplateIDs: templateIDs, }) - require.Error(t, err) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) - }) + return err + }, } -} -func TestUserActivityInsights_RBAC(t *testing.T) { - t.Parallel() + for endpointName, endpoint := range endpoints { + endpointName := endpointName + endpoint := endpoint - y, m, d := time.Now().UTC().Date() - today := time.Date(y, m, d, 0, 0, 0, 0, time.UTC) + t.Run(fmt.Sprintf("With%sEndpoint", endpointName), func(t *testing.T) { + t.Parallel() - type test struct { - withTemplate bool - } + tests := []test{ + {true}, + {false}, + } - tests := []test{ - {true}, - {false}, - } + for _, tt := range tests { + tt := tt - for _, tt := range tests { - tt := tt + t.Run("AsOwner", func(t *testing.T) { + t.Parallel() - t.Run("AsOwner", func(t *testing.T) { - t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{}) + admin := coderdtest.CreateFirstUser(t, client) - client := coderdtest.New(t, &coderdtest.Options{}) - admin := coderdtest.CreateFirstUser(t, client) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) - defer cancel() + var templateIDs []uuid.UUID + if tt.withTemplate { + version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) + templateIDs = append(templateIDs, template.ID) + } - var templateIDs []uuid.UUID - if tt.withTemplate { - version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) - templateIDs = append(templateIDs, template.ID) - } + err := endpoint(ctx, client, + today, + time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. + templateIDs...) + require.NoError(t, err) + }) + t.Run("AsTemplateAdmin", func(t *testing.T) { + t.Parallel() - _, err := client.UserActivityInsights(ctx, codersdk.UserActivityInsightsRequest{ - StartTime: today, - EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. - TemplateIDs: templateIDs, - }) - require.NoError(t, err) - }) - t.Run("AsTemplateAdmin", func(t *testing.T) { - t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{}) + admin := coderdtest.CreateFirstUser(t, client) - client := coderdtest.New(t, &coderdtest.Options{}) - admin := coderdtest.CreateFirstUser(t, client) + templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleTemplateAdmin()) - templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleTemplateAdmin()) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) - defer cancel() + var templateIDs []uuid.UUID + if tt.withTemplate { + version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) + templateIDs = append(templateIDs, template.ID) + } - var templateIDs []uuid.UUID - if tt.withTemplate { - version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) - templateIDs = append(templateIDs, template.ID) - } + err := endpoint(ctx, templateAdmin, + today, + time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. + templateIDs...) + require.NoError(t, err) + }) + t.Run("AsRegularUser", func(t *testing.T) { + t.Parallel() - _, err := templateAdmin.UserActivityInsights(ctx, codersdk.UserActivityInsightsRequest{ - StartTime: today, - EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. - TemplateIDs: templateIDs, - }) - require.NoError(t, err) - }) - t.Run("AsRegularUser", func(t *testing.T) { - t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{}) + admin := coderdtest.CreateFirstUser(t, client) - client := coderdtest.New(t, &coderdtest.Options{}) - admin := coderdtest.CreateFirstUser(t, client) + regular, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID) - regular, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) - defer cancel() + var templateIDs []uuid.UUID + if tt.withTemplate { + version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) + templateIDs = append(templateIDs, template.ID) + } - var templateIDs []uuid.UUID - if tt.withTemplate { - version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) - templateIDs = append(templateIDs, template.ID) + err := endpoint(ctx, regular, + today, + time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. + templateIDs...) + require.Error(t, err) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) + }) } - - _, err := regular.UserActivityInsights(ctx, codersdk.UserActivityInsightsRequest{ - StartTime: today, - EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. - TemplateIDs: templateIDs, - }) - require.Error(t, err) - var apiErr *codersdk.Error - require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusNotFound, apiErr.StatusCode()) }) } } From 6a7d1ced7c39b9cf0b7be50618fc1062322b0cff Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 25 Sep 2023 15:34:46 +0200 Subject: [PATCH 12/23] More tests --- coderd/insights_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 7f3a0f2cfa0c0..1dbc964d8a87f 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -240,6 +240,37 @@ func TestUserLatencyInsights_BadRequest(t *testing.T) { assert.Error(t, err, "want error for end time partial day when not today") } +func TestUserActivityInsights_BadRequest(t *testing.T) { + t.Parallel() + + saoPaulo, err := time.LoadLocation("America/Sao_Paulo") + require.NoError(t, err) + y, m, d := time.Now().UTC().Date() + today := time.Date(y, m, d, 0, 0, 0, 0, saoPaulo) + + // Prepare + + client := coderdtest.New(t, &coderdtest.Options{}) + _ = coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // Test insights request + _, err = client.UserActivityInsights(ctx, codersdk.UserActivityInsightsRequest{ + StartTime: today, + EndTime: today.AddDate(0, 0, -1), + }) + assert.Error(t, err, "want error for end time before start time") + + // Test insights request + _, err = client.UserActivityInsights(ctx, codersdk.UserActivityInsightsRequest{ + StartTime: today.AddDate(0, 0, -7), + EndTime: today.Add(-time.Hour), + }) + assert.Error(t, err, "want error for end time partial day when not today") +} + func TestTemplateInsights_Golden(t *testing.T) { t.Parallel() From fafe8e0599e28153c055d6f60195e991b2b4cfec Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 25 Sep 2023 16:04:12 +0200 Subject: [PATCH 13/23] TestUserActivityInsights --- coderd/insights_test.go | 96 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 1dbc964d8a87f..61f8c508fbc76 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -118,6 +118,102 @@ func TestDeploymentInsights(t *testing.T) { require.NoError(t, err) } +func TestUserActivityInsights(t *testing.T) { + t.Parallel() + + logger := slogtest.Make(t, nil) + client := coderdtest.New(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + AgentStatsRefreshInterval: time.Millisecond * 100, + }) + + // Create two users, one that will appear in the report and another that + // won't (due to not having/using a workspace). + user := coderdtest.CreateFirstUser(t, client) + _, _ = coderdtest.CreateAnotherUser(t, client, user.OrganizationID) + authToken := uuid.NewString() + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ProvisionApplyWithAgent(authToken), + }) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart]) + + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + + // Start an agent so that we can generate stats. + agentClient := agentsdk.New(client.URL) + agentClient.SetSessionToken(authToken) + agentCloser := agent.New(agent.Options{ + Logger: logger.Named("agent"), + Client: agentClient, + }) + defer func() { + _ = agentCloser.Close() + }() + resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) + + // Start must be at the beginning of the day, initialize it early in case + // the day changes so that we get the relevant stats faster. + y, m, d := time.Now().UTC().Date() + today := time.Date(y, m, d, 0, 0, 0, 0, time.UTC) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // Connect to the agent to generate usage/latency stats. + conn, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, &codersdk.DialWorkspaceAgentOptions{ + Logger: logger.Named("client"), + }) + require.NoError(t, err) + defer conn.Close() + + sshConn, err := conn.SSHClient(ctx) + require.NoError(t, err) + defer sshConn.Close() + + sess, err := sshConn.NewSession() + require.NoError(t, err) + defer sess.Close() + + r, w := io.Pipe() + defer r.Close() + defer w.Close() + sess.Stdin = r + sess.Stdout = io.Discard + err = sess.Start("cat") + require.NoError(t, err) + + var userActivities codersdk.UserActivityInsightsResponse + require.Eventuallyf(t, func() bool { + // Keep connection active. + _, err := w.Write([]byte("hello world\n")) + if !assert.NoError(t, err) { + return false + } + userActivities, err = client.UserActivityInsights(ctx, codersdk.UserActivityInsightsRequest{ + StartTime: today, + EndTime: time.Now().UTC().Truncate(time.Hour).Add(time.Hour), // Round up to include the current hour. + TemplateIDs: []uuid.UUID{template.ID}, + }) + if !assert.NoError(t, err) { + return false + } + return len(userActivities.Report.Users) > 0 && userActivities.Report.Users[0].Seconds > 0 + }, testutil.WaitMedium, testutil.IntervalFast, "user activity is missing") + + // We got our latency data, close the connection. + _ = sess.Close() + _ = sshConn.Close() + + require.Len(t, userActivities.Report.Users, 1, "want only 1 user") + require.Equal(t, userActivities.Report.Users[0].UserID, user.UserID, "want user id to match") + assert.Greater(t, userActivities.Report.Users[0].Seconds, int64(0), "want usage in seconds to be greater than 0") +} + func TestUserLatencyInsights(t *testing.T) { t.Parallel() From ed698edf7527f309260c1b60a8b78800ccc5f3a9 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 25 Sep 2023 16:05:37 +0200 Subject: [PATCH 14/23] fix: empty line --- coderd/insights_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 61f8c508fbc76..806505bb5384f 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -345,7 +345,6 @@ func TestUserActivityInsights_BadRequest(t *testing.T) { today := time.Date(y, m, d, 0, 0, 0, 0, saoPaulo) // Prepare - client := coderdtest.New(t, &coderdtest.Options{}) _ = coderdtest.CreateFirstUser(t, client) From 7be127c120f13929a7d18128bc4fbd2c6efc8ab0 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 25 Sep 2023 16:06:16 +0200 Subject: [PATCH 15/23] fix --- coderd/insights_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 806505bb5384f..f8018d09afdaf 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -351,14 +351,14 @@ func TestUserActivityInsights_BadRequest(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - // Test insights request + // Send insights request _, err = client.UserActivityInsights(ctx, codersdk.UserActivityInsightsRequest{ StartTime: today, EndTime: today.AddDate(0, 0, -1), }) assert.Error(t, err, "want error for end time before start time") - // Test insights request + // Send insights request _, err = client.UserActivityInsights(ctx, codersdk.UserActivityInsightsRequest{ StartTime: today.AddDate(0, 0, -7), EndTime: today.Add(-time.Hour), From c48684cc6eb598a3294dd4b92bfd3eb9af6161d8 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 26 Sep 2023 08:38:40 +0200 Subject: [PATCH 16/23] Golden files --- coderd/insights_test.go | 747 +++++++++++++++++- ...es_three_weeks_second_template.json.golden | 20 + ..._workspaces_week_all_templates.json.golden | 41 + ...orkspaces_week_deployment_wide.json.golden | 41 + ...workspaces_week_first_template.json.golden | 29 + ...r_timezone_(S\303\243o_Paulo).json.golden" | 41 + ...orkspaces_week_second_template.json.golden | 20 + ...workspaces_week_third_template.json.golden | 20 + ...kly_aggregated_deployment_wide.json.golden | 41 + ...ekly_aggregated_first_template.json.golden | 29 + ...es_weekly_aggregated_templates.json.golden | 41 + 11 files changed, 1069 insertions(+), 1 deletion(-) create mode 100644 coderd/testdata/insights/user-activity/multiple_users_and_workspaces_three_weeks_second_template.json.golden create mode 100644 coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_all_templates.json.golden create mode 100644 coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_deployment_wide.json.golden create mode 100644 coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_first_template.json.golden create mode 100644 "coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden" create mode 100644 coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_second_template.json.golden create mode 100644 coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_third_template.json.golden create mode 100644 coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden create mode 100644 coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden create mode 100644 coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden diff --git a/coderd/insights_test.go b/coderd/insights_test.go index f8018d09afdaf..71fc2d0c7870d 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -118,7 +118,7 @@ func TestDeploymentInsights(t *testing.T) { require.NoError(t, err) } -func TestUserActivityInsights(t *testing.T) { +func TestUserActivityInsights_SanityCheck(t *testing.T) { t.Parallel() logger := slogtest.Make(t, nil) @@ -1266,6 +1266,751 @@ func TestTemplateInsights_Golden(t *testing.T) { } } +func TestUserActivityInsights_Golden(t *testing.T) { + t.Parallel() + + // Prepare test data types. + type templateApp struct { + name string + icon string + } + type testTemplate struct { + name string + apps []templateApp + + // Filled later. + id uuid.UUID + } + type buildParameter struct { + value string + } + type workspaceApp templateApp + type testWorkspace struct { + name string + template *testTemplate + buildParameters []buildParameter + + // Filled later. + id uuid.UUID + user any // *testUser, but it's not available yet, defined below. + agentID uuid.UUID + apps []*workspaceApp + agentClient *agentsdk.Client + } + type testUser struct { + name string + workspaces []*testWorkspace + + client *codersdk.Client + sdk codersdk.User + } + + // Represent agent stats, to be inserted via stats batcher. + type agentStat struct { + // Set a range via start/end, multiple stats will be generated + // within the range. + startedAt time.Time + endedAt time.Time + + sessionCountVSCode int64 + sessionCountJetBrains int64 + sessionCountReconnectingPTY int64 + sessionCountSSH int64 + noConnections bool + } + // Represent app usage stats, to be inserted via stats reporter. + type appUsage struct { + app *workspaceApp + startedAt time.Time + endedAt time.Time + requests int + } + + // Represent actual data being generated on a per-workspace basis. + type testDataGen struct { + agentStats []agentStat + appUsage []appUsage + } + + prepareFixtureAndTestData := func(t *testing.T, makeFixture func() ([]*testTemplate, []*testUser), makeData func([]*testTemplate, []*testUser) map[*testWorkspace]testDataGen) ([]*testTemplate, []*testUser, map[*testWorkspace]testDataGen) { + var stableIDs []uuid.UUID + newStableUUID := func() uuid.UUID { + stableIDs = append(stableIDs, uuid.MustParse(fmt.Sprintf("00000000-0000-0000-0000-%012d", len(stableIDs)+1))) + stableID := stableIDs[len(stableIDs)-1] + return stableID + } + + templates, users := makeFixture() + for _, template := range templates { + template.id = newStableUUID() + } + for _, user := range users { + for _, workspace := range user.workspaces { + workspace.user = user + for _, app := range workspace.template.apps { + app := workspaceApp(app) + workspace.apps = append(workspace.apps, &app) + } + } + } + + testData := makeData(templates, users) + // Sanity check. + for ws, data := range testData { + for _, usage := range data.appUsage { + found := false + for _, app := range ws.apps { + if usage.app == app { // Pointer equality + found = true + break + } + } + if !found { + for _, user := range users { + for _, workspace := range user.workspaces { + for _, app := range workspace.apps { + if usage.app == app { // Pointer equality + require.True(t, found, "test bug: app %q not in workspace %q: want user=%s workspace=%s; got user=%s workspace=%s ", usage.app.name, ws.name, ws.user.(*testUser).name, ws.name, user.name, workspace.name) + break + } + } + } + } + require.True(t, found, "test bug: app %q not in workspace %q", usage.app.name, ws.name) + } + } + } + + return templates, users, testData + } + + prepare := func(t *testing.T, templates []*testTemplate, users []*testUser, testData map[*testWorkspace]testDataGen) *codersdk.Client { + logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: false}).Leveled(slog.LevelDebug) + db, pubsub := dbtestutil.NewDB(t) + client := coderdtest.New(t, &coderdtest.Options{ + Database: db, + Pubsub: pubsub, + Logger: &logger, + IncludeProvisionerDaemon: true, + AgentStatsRefreshInterval: time.Hour, // Not relevant for this test. + }) + firstUser := coderdtest.CreateFirstUser(t, client) + + // Prepare all test users. + for _, user := range users { + user.client, user.sdk = coderdtest.CreateAnotherUserMutators(t, client, firstUser.OrganizationID, nil, func(r *codersdk.CreateUserRequest) { + r.Username = user.name + }) + user.client.SetLogger(logger.Named("user").With(slog.Field{Name: "name", Value: user.name})) + } + + // Prepare all the templates. + for _, template := range templates { + template := template + + // Prepare all workspace resources (agents and apps). + var ( + createWorkspaces []func(uuid.UUID) + waitWorkspaces []func() + ) + var resources []*proto.Resource + for _, user := range users { + user := user + for _, workspace := range user.workspaces { + workspace := workspace + + if workspace.template != template { + continue + } + authToken := uuid.New() + agentClient := agentsdk.New(client.URL) + agentClient.SetSessionToken(authToken.String()) + workspace.agentClient = agentClient + + var apps []*proto.App + for _, app := range workspace.apps { + apps = append(apps, &proto.App{ + Slug: app.name, + DisplayName: app.name, + Icon: app.icon, + SharingLevel: proto.AppSharingLevel_OWNER, + Url: "http://", + }) + } + + resources = append(resources, &proto.Resource{ + Name: "example", + Type: "aws_instance", + Agents: []*proto.Agent{{ + Id: uuid.NewString(), // Doesn't matter, not used in DB. + Name: "dev", + Auth: &proto.Agent_Token{ + Token: authToken.String(), + }, + Apps: apps, + }}, + }) + + createWorkspaces = append(createWorkspaces, func(templateID uuid.UUID) { + // Create workspace using the users client. + createdWorkspace := coderdtest.CreateWorkspace(t, user.client, firstUser.OrganizationID, templateID) + workspace.id = createdWorkspace.ID + waitWorkspaces = append(waitWorkspaces, func() { + coderdtest.AwaitWorkspaceBuildJob(t, user.client, createdWorkspace.LatestBuild.ID) + ctx := testutil.Context(t, testutil.WaitShort) + ws, err := user.client.Workspace(ctx, workspace.id) + require.NoError(t, err, "want no error getting workspace") + + workspace.agentID = ws.LatestBuild.Resources[0].Agents[0].ID + }) + }) + } + } + + // Create the template version and template. + version := coderdtest.CreateTemplateVersion(t, client, firstUser.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: []*proto.Response{{ + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ + Resources: resources, + }, + }, + }}, + }) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + + // Create template, essentially a modified version of CreateTemplate + // where we can control the template ID. + // createdTemplate := coderdtest.CreateTemplate(t, client, firstUser.OrganizationID, version.ID) + createdTemplate := dbgen.Template(t, db, database.Template{ + ID: template.id, + ActiveVersionID: version.ID, + OrganizationID: firstUser.OrganizationID, + CreatedBy: firstUser.UserID, + GroupACL: database.TemplateACL{ + firstUser.OrganizationID.String(): []rbac.Action{rbac.ActionRead}, + }, + }) + err := db.UpdateTemplateVersionByID(context.Background(), database.UpdateTemplateVersionByIDParams{ + ID: version.ID, + TemplateID: uuid.NullUUID{ + UUID: createdTemplate.ID, + Valid: true, + }, + }) + require.NoError(t, err, "want no error updating template version") + + // Create all workspaces and wait for them. + for _, createWorkspace := range createWorkspaces { + createWorkspace(template.id) + } + for _, waitWorkspace := range waitWorkspaces { + waitWorkspace() + } + } + + ctx := testutil.Context(t, testutil.WaitSuperLong) + + // Use agent stats batcher to insert agent stats, similar to live system. + // NOTE(mafredri): Ideally we would pass batcher as a coderd option and + // insert using the agentClient, but we have a circular dependency on + // the database. + batcher, batcherCloser, err := batchstats.New( + ctx, + batchstats.WithStore(db), + batchstats.WithLogger(logger.Named("batchstats")), + batchstats.WithInterval(time.Hour), + ) + require.NoError(t, err) + defer batcherCloser() // Flushes the stats, this is to ensure they're written. + + for workspace, data := range testData { + for _, stat := range data.agentStats { + createdAt := stat.startedAt + connectionCount := int64(1) + if stat.noConnections { + connectionCount = 0 + } + for createdAt.Before(stat.endedAt) { + err = batcher.Add(createdAt, workspace.agentID, workspace.template.id, workspace.user.(*testUser).sdk.ID, workspace.id, agentsdk.Stats{ + ConnectionCount: connectionCount, + SessionCountVSCode: stat.sessionCountVSCode, + SessionCountJetBrains: stat.sessionCountJetBrains, + SessionCountReconnectingPTY: stat.sessionCountReconnectingPTY, + SessionCountSSH: stat.sessionCountSSH, + }) + require.NoError(t, err, "want no error inserting agent stats") + createdAt = createdAt.Add(30 * time.Second) + } + } + } + + // Insert app usage. + var stats []workspaceapps.StatsReport + for workspace, data := range testData { + for _, usage := range data.appUsage { + appName := usage.app.name + accessMethod := workspaceapps.AccessMethodPath + if usage.app.name == "terminal" { + appName = "" + accessMethod = workspaceapps.AccessMethodTerminal + } + stats = append(stats, workspaceapps.StatsReport{ + UserID: workspace.user.(*testUser).sdk.ID, + WorkspaceID: workspace.id, + AgentID: workspace.agentID, + AccessMethod: accessMethod, + SlugOrPort: appName, + SessionID: uuid.New(), + SessionStartedAt: usage.startedAt, + SessionEndedAt: usage.endedAt, + Requests: usage.requests, + }) + } + } + reporter := workspaceapps.NewStatsDBReporter(db, workspaceapps.DefaultStatsDBReporterBatchSize) + //nolint:gocritic // This is a test. + err = reporter.Report(dbauthz.AsSystemRestricted(ctx), stats) + require.NoError(t, err, "want no error inserting app stats") + + return client + } + + baseTemplateAndUserFixture := func() ([]*testTemplate, []*testUser) { + // Test templates and configuration to generate. + templates := []*testTemplate{ + // Create two templates with near-identical apps and parameters + // to allow testing for grouping similar data. + { + name: "template1", + apps: []templateApp{ + {name: "app1", icon: "/icon1.png"}, + {name: "app2", icon: "/icon2.png"}, + {name: "app3", icon: "/icon2.png"}, + }, + }, + { + name: "template2", + apps: []templateApp{ + {name: "app1", icon: "/icon1.png"}, + {name: "app2", icon: "/icon2.png"}, + {name: "app3", icon: "/icon2.png"}, + }, + }, + // Create another template with different parameters and apps. + { + name: "othertemplate", + apps: []templateApp{ + {name: "otherapp1", icon: "/icon1.png"}, + + // This "special test app" will be converted into web + // terminal usage, this is not included in stats since we + // currently rely on agent stats for this data. + {name: "terminal", icon: "/terminal.png"}, + }, + }, + } + + // Users and workspaces to generate. + users := []*testUser{ + { + name: "user1", + workspaces: []*testWorkspace{ + { + name: "workspace1", + template: templates[0], + }, + { + name: "workspace2", + template: templates[1], + }, + { + name: "otherworkspace3", + template: templates[2], + }, + }, + }, + { + name: "user2", + workspaces: []*testWorkspace{ + { + name: "workspace1", + template: templates[0], + }, + }, + }, + { + name: "user3", + workspaces: []*testWorkspace{ + { + name: "otherworkspace1", + template: templates[2], + }, + { + name: "workspace2", + template: templates[0], + }, + }, + }, + } + + return templates, users + } + + // Time range for report, test data will be generated within and + // outside this range, but only data within the range should be + // included in the report. + frozenLastNight := time.Date(2023, 8, 22, 0, 0, 0, 0, time.UTC) + frozenWeekAgo := frozenLastNight.AddDate(0, 0, -7) + + saoPaulo, err := time.LoadLocation("America/Sao_Paulo") + require.NoError(t, err) + frozenWeekAgoSaoPaulo, err := time.ParseInLocation(time.DateTime, frozenWeekAgo.Format(time.DateTime), saoPaulo) + require.NoError(t, err) + + makeBaseTestData := func(templates []*testTemplate, users []*testUser) map[*testWorkspace]testDataGen { + return map[*testWorkspace]testDataGen{ + users[0].workspaces[0]: { + agentStats: []agentStat{ + { // One hour of usage. + startedAt: frozenWeekAgo, + endedAt: frozenWeekAgo.Add(time.Hour), + sessionCountVSCode: 1, + sessionCountSSH: 1, + }, + { // 12 minutes of usage. + startedAt: frozenWeekAgo.AddDate(0, 0, 1), + endedAt: frozenWeekAgo.AddDate(0, 0, 1).Add(12 * time.Minute), + sessionCountSSH: 1, + }, + { // 1m30s of usage -> 2m rounded. + startedAt: frozenWeekAgo.AddDate(0, 0, 2).Add(4*time.Minute + 30*time.Second), + endedAt: frozenWeekAgo.AddDate(0, 0, 2).Add(6 * time.Minute), + sessionCountJetBrains: 1, + }, + }, + appUsage: []appUsage{ + { // One hour of usage. + app: users[0].workspaces[0].apps[0], + startedAt: frozenWeekAgo, + endedAt: frozenWeekAgo.Add(time.Hour), + requests: 1, + }, + { // 30s of app usage -> 1m rounded. + app: users[0].workspaces[0].apps[0], + startedAt: frozenWeekAgo.Add(2*time.Hour + 10*time.Second), + endedAt: frozenWeekAgo.Add(2*time.Hour + 40*time.Second), + requests: 1, + }, + { // 1m30s of app usage -> 2m rounded (included in São Paulo). + app: users[0].workspaces[0].apps[0], + startedAt: frozenWeekAgo.Add(3*time.Hour + 30*time.Second), + endedAt: frozenWeekAgo.Add(3*time.Hour + 90*time.Second), + requests: 1, + }, + { // used an app on the last day, counts as active user, 12m. + app: users[0].workspaces[0].apps[2], + startedAt: frozenWeekAgo.AddDate(0, 0, 6), + endedAt: frozenWeekAgo.AddDate(0, 0, 6).Add(12 * time.Minute), + requests: 1, + }, + }, + }, + users[0].workspaces[1]: { + agentStats: []agentStat{ + { + // One hour of usage in second template at the same time + // as in first template. When selecting both templates + // this user and their app usage will only be counted + // once but the template ID will show up in the data. + startedAt: frozenWeekAgo, + endedAt: frozenWeekAgo.Add(time.Hour), + sessionCountVSCode: 1, + sessionCountSSH: 1, + }, + { // One hour of usage. + startedAt: frozenWeekAgo.AddDate(0, 0, -12), + endedAt: frozenWeekAgo.AddDate(0, 0, -12).Add(time.Hour), + sessionCountSSH: 1, + sessionCountReconnectingPTY: 1, + }, + { // Another one hour of usage, but "active users" shouldn't be increased twice. + startedAt: frozenWeekAgo.AddDate(0, 0, -10), + endedAt: frozenWeekAgo.AddDate(0, 0, -10).Add(time.Hour), + sessionCountSSH: 1, + sessionCountReconnectingPTY: 1, + }, + }, + appUsage: []appUsage{ + // TODO(mafredri): This doesn't behave correctly right now + // and will add more usage to the app. This could be + // considered both correct and incorrect behavior. + // { // One hour of usage, but same user and same template app, only count once. + // app: users[0].workspaces[1].apps[0], + // startedAt: frozenWeekAgo, + // endedAt: frozenWeekAgo.Add(time.Hour), + // requests: 1, + // }, + { + // Different templates but identical apps, apps will be + // combined and usage will be summed. + app: users[0].workspaces[1].apps[0], + startedAt: frozenWeekAgo.AddDate(0, 0, 2), + endedAt: frozenWeekAgo.AddDate(0, 0, 2).Add(6 * time.Hour), + requests: 1, + }, + }, + }, + users[0].workspaces[2]: { + agentStats: []agentStat{}, + appUsage: []appUsage{}, + }, + users[1].workspaces[0]: { + agentStats: []agentStat{ + { // One hour of agent usage before timeframe (exclude). + startedAt: frozenWeekAgo.Add(-time.Hour), + endedAt: frozenWeekAgo, + sessionCountVSCode: 1, + sessionCountSSH: 1, + }, + { // One hour of usage. + startedAt: frozenWeekAgo, + endedAt: frozenWeekAgo.Add(time.Hour), + sessionCountSSH: 1, + }, + { // One hour of agent usage after timeframe (exclude in UTC, include in São Paulo). + startedAt: frozenWeekAgo.AddDate(0, 0, 7), + endedAt: frozenWeekAgo.AddDate(0, 0, 7).Add(time.Hour), + sessionCountVSCode: 1, + sessionCountSSH: 1, + }, + }, + appUsage: []appUsage{ + { // One hour of app usage before timeframe (exclude). + app: users[1].workspaces[0].apps[2], + startedAt: frozenWeekAgo.Add(-time.Hour), + endedAt: frozenWeekAgo, + requests: 1, + }, + { // One hour of app usage after timeframe (exclude in UTC, include in São Paulo). + app: users[1].workspaces[0].apps[2], + startedAt: frozenWeekAgo.AddDate(0, 0, 7), + endedAt: frozenWeekAgo.AddDate(0, 0, 7).Add(time.Hour), + requests: 1, + }, + }, + }, + users[2].workspaces[0]: { + agentStats: []agentStat{ + { // One hour of usage. + startedAt: frozenWeekAgo, + endedAt: frozenWeekAgo.Add(time.Hour), + sessionCountSSH: 1, + sessionCountReconnectingPTY: 1, + }, + }, + appUsage: []appUsage{ + { + app: users[2].workspaces[0].apps[0], + startedAt: frozenWeekAgo.AddDate(0, 0, 2), + endedAt: frozenWeekAgo.AddDate(0, 0, 2).Add(5 * time.Minute), + requests: 1, + }, + { // Special app; excluded from apps, but counted as active during the day. + app: users[2].workspaces[0].apps[1], + startedAt: frozenWeekAgo.AddDate(0, 0, 3), + endedAt: frozenWeekAgo.AddDate(0, 0, 3).Add(5 * time.Minute), + requests: 1, + }, + }, + }, + } + } + type testRequest struct { + name string + makeRequest func([]*testTemplate) codersdk.UserActivityInsightsRequest + ignoreTimes bool + } + tests := []struct { + name string + makeFixture func() ([]*testTemplate, []*testUser) + makeTestData func([]*testTemplate, []*testUser) map[*testWorkspace]testDataGen + requests []testRequest + }{ + { + name: "multiple users and workspaces", + makeFixture: baseTemplateAndUserFixture, + makeTestData: makeBaseTestData, + requests: []testRequest{ + { + name: "week deployment wide", + makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest { + return codersdk.UserActivityInsightsRequest{ + StartTime: frozenWeekAgo, + EndTime: frozenWeekAgo.AddDate(0, 0, 7), + } + }, + }, + { + name: "weekly aggregated deployment wide", + makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest { + return codersdk.UserActivityInsightsRequest{ + StartTime: frozenWeekAgo.AddDate(0, 0, -3), + EndTime: frozenWeekAgo.AddDate(0, 0, 4), + } + }, + }, + { + name: "week all templates", + makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest { + return codersdk.UserActivityInsightsRequest{ + TemplateIDs: []uuid.UUID{templates[0].id, templates[1].id, templates[2].id}, + StartTime: frozenWeekAgo, + EndTime: frozenWeekAgo.AddDate(0, 0, 7), + } + }, + }, + { + name: "weekly aggregated templates", + makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest { + return codersdk.UserActivityInsightsRequest{ + TemplateIDs: []uuid.UUID{templates[0].id, templates[1].id, templates[2].id}, + StartTime: frozenWeekAgo.AddDate(0, 0, -1), + EndTime: frozenWeekAgo.AddDate(0, 0, 6), + } + }, + }, + { + name: "week first template", + makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest { + return codersdk.UserActivityInsightsRequest{ + TemplateIDs: []uuid.UUID{templates[0].id}, + StartTime: frozenWeekAgo, + EndTime: frozenWeekAgo.AddDate(0, 0, 7), + } + }, + }, + { + name: "weekly aggregated first template", + makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest { + return codersdk.UserActivityInsightsRequest{ + TemplateIDs: []uuid.UUID{templates[0].id}, + StartTime: frozenWeekAgo, + EndTime: frozenWeekAgo.AddDate(0, 0, 7), + } + }, + }, + { + name: "week second template", + makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest { + return codersdk.UserActivityInsightsRequest{ + TemplateIDs: []uuid.UUID{templates[1].id}, + StartTime: frozenWeekAgo, + EndTime: frozenWeekAgo.AddDate(0, 0, 7), + } + }, + }, + { + name: "three weeks second template", + makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest { + return codersdk.UserActivityInsightsRequest{ + TemplateIDs: []uuid.UUID{templates[1].id}, + StartTime: frozenWeekAgo.AddDate(0, 0, -14), + EndTime: frozenWeekAgo.AddDate(0, 0, 7), + } + }, + }, + { + name: "week third template", + makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest { + return codersdk.UserActivityInsightsRequest{ + TemplateIDs: []uuid.UUID{templates[2].id}, + StartTime: frozenWeekAgo, + EndTime: frozenWeekAgo.AddDate(0, 0, 7), + } + }, + }, + { + // São Paulo is three hours behind UTC, so we should not see + // any data between weekAgo and weekAgo.Add(3 * time.Hour). + name: "week other timezone (São Paulo)", + makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest { + return codersdk.UserActivityInsightsRequest{ + StartTime: frozenWeekAgoSaoPaulo, + EndTime: frozenWeekAgoSaoPaulo.AddDate(0, 0, 7), + } + }, + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + require.NotNil(t, tt.makeFixture, "test bug: makeFixture must be set") + require.NotNil(t, tt.makeTestData, "test bug: makeTestData must be set") + templates, users, testData := prepareFixtureAndTestData(t, tt.makeFixture, tt.makeTestData) + client := prepare(t, templates, users, testData) + + for _, req := range tt.requests { + req := req + t.Run(req.name, func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitMedium) + + report, err := client.UserActivityInsights(ctx, req.makeRequest(templates)) + require.NoError(t, err, "want no error getting template insights") + + if req.ignoreTimes { + // Ignore times, we're only interested in the data. + report.Report.StartTime = time.Time{} + report.Report.EndTime = time.Time{} + } + + partialName := strings.Join(strings.Split(t.Name(), "/")[1:], "_") + goldenFile := filepath.Join("testdata", "insights", "user-activity", partialName+".json.golden") + if *updateGoldenFiles { + err = os.MkdirAll(filepath.Dir(goldenFile), 0o755) + require.NoError(t, err, "want no error creating golden file directory") + f, err := os.Create(goldenFile) + require.NoError(t, err, "want no error creating golden file") + defer f.Close() + enc := json.NewEncoder(f) + enc.SetIndent("", " ") + enc.Encode(report) + return + } + + f, err := os.Open(goldenFile) + require.NoError(t, err, "open golden file, run \"make update-golden-files\" and commit the changes") + defer f.Close() + var want codersdk.UserActivityInsightsResponse + err = json.NewDecoder(f).Decode(&want) + require.NoError(t, err, "want no error decoding golden file") + + cmpOpts := []cmp.Option{ + // Ensure readable UUIDs in diff. + cmp.Transformer("UUIDs", func(in []uuid.UUID) (s []string) { + for _, id := range in { + s = append(s, id.String()) + } + return s + }), + } + // Use cmp.Diff here because it produces more readable diffs. + assert.Empty(t, cmp.Diff(want, report, cmpOpts...), "golden file mismatch (-want +got): %s, run \"make update-golden-files\", verify and commit the changes", goldenFile) + }) + } + }) + } +} + func TestTemplateInsights_BadRequest(t *testing.T) { t.Parallel() diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_three_weeks_second_template.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_three_weeks_second_template.json.golden new file mode 100644 index 0000000000000..eee8436546a1c --- /dev/null +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_three_weeks_second_template.json.golden @@ -0,0 +1,20 @@ +{ + "report": { + "start_time": "2023-08-01T00:00:00Z", + "end_time": "2023-08-22T00:00:00Z", + "template_ids": [ + "00000000-0000-0000-0000-000000000002" + ], + "users": [ + { + "template_ids": [ + "00000000-0000-0000-0000-000000000002" + ], + "user_id": "e114f18f-4e75-4533-bfe2-cb8acbad7563", + "username": "user1", + "avatar_url": "", + "seconds": 32400 + } + ] + } +} diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_all_templates.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_all_templates.json.golden new file mode 100644 index 0000000000000..7cee822534cde --- /dev/null +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_all_templates.json.golden @@ -0,0 +1,41 @@ +{ + "report": { + "start_time": "2023-08-15T00:00:00Z", + "end_time": "2023-08-22T00:00:00Z", + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002", + "00000000-0000-0000-0000-000000000003" + ], + "users": [ + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "user_id": "31972c11-d06a-44e5-ace7-d95192eaa8d6", + "username": "user2", + "avatar_url": "", + "seconds": 3600 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000003" + ], + "user_id": "cdb811f8-93c2-443a-8266-4d47b55f4094", + "username": "user3", + "avatar_url": "", + "seconds": 4200 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002" + ], + "user_id": "e114f18f-4e75-4533-bfe2-cb8acbad7563", + "username": "user1", + "avatar_url": "", + "seconds": 30540 + } + ] + } +} diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_deployment_wide.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_deployment_wide.json.golden new file mode 100644 index 0000000000000..7cee822534cde --- /dev/null +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_deployment_wide.json.golden @@ -0,0 +1,41 @@ +{ + "report": { + "start_time": "2023-08-15T00:00:00Z", + "end_time": "2023-08-22T00:00:00Z", + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002", + "00000000-0000-0000-0000-000000000003" + ], + "users": [ + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "user_id": "31972c11-d06a-44e5-ace7-d95192eaa8d6", + "username": "user2", + "avatar_url": "", + "seconds": 3600 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000003" + ], + "user_id": "cdb811f8-93c2-443a-8266-4d47b55f4094", + "username": "user3", + "avatar_url": "", + "seconds": 4200 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002" + ], + "user_id": "e114f18f-4e75-4533-bfe2-cb8acbad7563", + "username": "user1", + "avatar_url": "", + "seconds": 30540 + } + ] + } +} diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_first_template.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_first_template.json.golden new file mode 100644 index 0000000000000..22ad5cc6d7d7d --- /dev/null +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_first_template.json.golden @@ -0,0 +1,29 @@ +{ + "report": { + "start_time": "2023-08-15T00:00:00Z", + "end_time": "2023-08-22T00:00:00Z", + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "users": [ + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "user_id": "31972c11-d06a-44e5-ace7-d95192eaa8d6", + "username": "user2", + "avatar_url": "", + "seconds": 3600 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "user_id": "e114f18f-4e75-4533-bfe2-cb8acbad7563", + "username": "user1", + "avatar_url": "", + "seconds": 5340 + } + ] + } +} diff --git "a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden" "b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden" new file mode 100644 index 0000000000000..580facf5ebe28 --- /dev/null +++ "b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden" @@ -0,0 +1,41 @@ +{ + "report": { + "start_time": "2023-08-15T00:00:00-03:00", + "end_time": "2023-08-22T00:00:00-03:00", + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002", + "00000000-0000-0000-0000-000000000003" + ], + "users": [ + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "user_id": "31972c11-d06a-44e5-ace7-d95192eaa8d6", + "username": "user2", + "avatar_url": "", + "seconds": 3600 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000003" + ], + "user_id": "cdb811f8-93c2-443a-8266-4d47b55f4094", + "username": "user3", + "avatar_url": "", + "seconds": 600 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002" + ], + "user_id": "e114f18f-4e75-4533-bfe2-cb8acbad7563", + "username": "user1", + "avatar_url": "", + "seconds": 23280 + } + ] + } +} diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_second_template.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_second_template.json.golden new file mode 100644 index 0000000000000..78449633de5d3 --- /dev/null +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_second_template.json.golden @@ -0,0 +1,20 @@ +{ + "report": { + "start_time": "2023-08-15T00:00:00Z", + "end_time": "2023-08-22T00:00:00Z", + "template_ids": [ + "00000000-0000-0000-0000-000000000002" + ], + "users": [ + { + "template_ids": [ + "00000000-0000-0000-0000-000000000002" + ], + "user_id": "e114f18f-4e75-4533-bfe2-cb8acbad7563", + "username": "user1", + "avatar_url": "", + "seconds": 25200 + } + ] + } +} diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_third_template.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_third_template.json.golden new file mode 100644 index 0000000000000..51da306613205 --- /dev/null +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_third_template.json.golden @@ -0,0 +1,20 @@ +{ + "report": { + "start_time": "2023-08-15T00:00:00Z", + "end_time": "2023-08-22T00:00:00Z", + "template_ids": [ + "00000000-0000-0000-0000-000000000003" + ], + "users": [ + { + "template_ids": [ + "00000000-0000-0000-0000-000000000003" + ], + "user_id": "cdb811f8-93c2-443a-8266-4d47b55f4094", + "username": "user3", + "avatar_url": "", + "seconds": 4200 + } + ] + } +} diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden new file mode 100644 index 0000000000000..c465b966e651c --- /dev/null +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden @@ -0,0 +1,41 @@ +{ + "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" + ], + "users": [ + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "user_id": "31972c11-d06a-44e5-ace7-d95192eaa8d6", + "username": "user2", + "avatar_url": "", + "seconds": 7200 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000003" + ], + "user_id": "cdb811f8-93c2-443a-8266-4d47b55f4094", + "username": "user3", + "avatar_url": "", + "seconds": 4200 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002" + ], + "user_id": "e114f18f-4e75-4533-bfe2-cb8acbad7563", + "username": "user1", + "avatar_url": "", + "seconds": 29820 + } + ] + } +} diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden new file mode 100644 index 0000000000000..22ad5cc6d7d7d --- /dev/null +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden @@ -0,0 +1,29 @@ +{ + "report": { + "start_time": "2023-08-15T00:00:00Z", + "end_time": "2023-08-22T00:00:00Z", + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "users": [ + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "user_id": "31972c11-d06a-44e5-ace7-d95192eaa8d6", + "username": "user2", + "avatar_url": "", + "seconds": 3600 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "user_id": "e114f18f-4e75-4533-bfe2-cb8acbad7563", + "username": "user1", + "avatar_url": "", + "seconds": 5340 + } + ] + } +} diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden new file mode 100644 index 0000000000000..012d3727ad528 --- /dev/null +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden @@ -0,0 +1,41 @@ +{ + "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" + ], + "users": [ + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001" + ], + "user_id": "31972c11-d06a-44e5-ace7-d95192eaa8d6", + "username": "user2", + "avatar_url": "", + "seconds": 7200 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000003" + ], + "user_id": "cdb811f8-93c2-443a-8266-4d47b55f4094", + "username": "user3", + "avatar_url": "", + "seconds": 4200 + }, + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002" + ], + "user_id": "e114f18f-4e75-4533-bfe2-cb8acbad7563", + "username": "user1", + "avatar_url": "", + "seconds": 29820 + } + ] + } +} From 815780961e387b00f41482865f939d461f51b4ff Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 26 Sep 2023 10:01:03 +0200 Subject: [PATCH 17/23] lint --- coderd/insights_test.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 71fc2d0c7870d..cd02e6b3d1020 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -876,6 +876,7 @@ func TestTemplateInsights_Golden(t *testing.T) { frozenWeekAgoSaoPaulo, err := time.ParseInLocation(time.DateTime, frozenWeekAgo.Format(time.DateTime), saoPaulo) require.NoError(t, err) + //nolint:dupl For testing purposes makeBaseTestData := func(templates []*testTemplate, users []*testUser) map[*testWorkspace]testDataGen { return map[*testWorkspace]testDataGen{ users[0].workspaces[0]: { @@ -1281,14 +1282,10 @@ func TestUserActivityInsights_Golden(t *testing.T) { // Filled later. id uuid.UUID } - type buildParameter struct { - value string - } type workspaceApp templateApp type testWorkspace struct { - name string - template *testTemplate - buildParameters []buildParameter + name string + template *testTemplate // Filled later. id uuid.UUID @@ -1670,6 +1667,7 @@ func TestUserActivityInsights_Golden(t *testing.T) { frozenWeekAgoSaoPaulo, err := time.ParseInLocation(time.DateTime, frozenWeekAgo.Format(time.DateTime), saoPaulo) require.NoError(t, err) + //nolint:dupl For testing purposes makeBaseTestData := func(templates []*testTemplate, users []*testUser) map[*testWorkspace]testDataGen { return map[*testWorkspace]testDataGen{ users[0].workspaces[0]: { From 7a5390566d5e1e3f17f7a37d1b761dc79f9df11c Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 26 Sep 2023 10:10:41 +0200 Subject: [PATCH 18/23] linter --- coderd/insights_test.go | 4 ++-- ...s_and_workspaces_three_weeks_second_template.json.golden | 2 +- ...iple_users_and_workspaces_week_all_templates.json.golden | 6 +++--- ...le_users_and_workspaces_week_deployment_wide.json.golden | 6 +++--- ...ple_users_and_workspaces_week_first_template.json.golden | 4 ++-- ...aces_week_other_timezone_(S\303\243o_Paulo).json.golden" | 6 +++--- ...le_users_and_workspaces_week_second_template.json.golden | 2 +- ...ple_users_and_workspaces_week_third_template.json.golden | 2 +- ...workspaces_weekly_aggregated_deployment_wide.json.golden | 6 +++--- ..._workspaces_weekly_aggregated_first_template.json.golden | 4 ++-- ...s_and_workspaces_weekly_aggregated_templates.json.golden | 6 +++--- 11 files changed, 24 insertions(+), 24 deletions(-) diff --git a/coderd/insights_test.go b/coderd/insights_test.go index cd02e6b3d1020..e893358247c2d 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -876,7 +876,7 @@ func TestTemplateInsights_Golden(t *testing.T) { frozenWeekAgoSaoPaulo, err := time.ParseInLocation(time.DateTime, frozenWeekAgo.Format(time.DateTime), saoPaulo) require.NoError(t, err) - //nolint:dupl For testing purposes + //nolint:dupl // For testing purposes makeBaseTestData := func(templates []*testTemplate, users []*testUser) map[*testWorkspace]testDataGen { return map[*testWorkspace]testDataGen{ users[0].workspaces[0]: { @@ -1667,7 +1667,7 @@ func TestUserActivityInsights_Golden(t *testing.T) { frozenWeekAgoSaoPaulo, err := time.ParseInLocation(time.DateTime, frozenWeekAgo.Format(time.DateTime), saoPaulo) require.NoError(t, err) - //nolint:dupl For testing purposes + //nolint:dupl // For testing purposes makeBaseTestData := func(templates []*testTemplate, users []*testUser) map[*testWorkspace]testDataGen { return map[*testWorkspace]testDataGen{ users[0].workspaces[0]: { diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_three_weeks_second_template.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_three_weeks_second_template.json.golden index eee8436546a1c..c6e4e4da52cbc 100644 --- a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_three_weeks_second_template.json.golden +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_three_weeks_second_template.json.golden @@ -10,7 +10,7 @@ "template_ids": [ "00000000-0000-0000-0000-000000000002" ], - "user_id": "e114f18f-4e75-4533-bfe2-cb8acbad7563", + "user_id": "aba84ddd-48c3-4041-98db-f80913a424a3", "username": "user1", "avatar_url": "", "seconds": 32400 diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_all_templates.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_all_templates.json.golden index 7cee822534cde..7f7fc92884eb1 100644 --- a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_all_templates.json.golden +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_all_templates.json.golden @@ -12,7 +12,7 @@ "template_ids": [ "00000000-0000-0000-0000-000000000001" ], - "user_id": "31972c11-d06a-44e5-ace7-d95192eaa8d6", + "user_id": "037c6a21-c805-46d8-a1ff-d98096afb44a", "username": "user2", "avatar_url": "", "seconds": 3600 @@ -21,7 +21,7 @@ "template_ids": [ "00000000-0000-0000-0000-000000000003" ], - "user_id": "cdb811f8-93c2-443a-8266-4d47b55f4094", + "user_id": "297dcbb4-2463-487a-92cd-7443d8d2a82d", "username": "user3", "avatar_url": "", "seconds": 4200 @@ -31,7 +31,7 @@ "00000000-0000-0000-0000-000000000001", "00000000-0000-0000-0000-000000000002" ], - "user_id": "e114f18f-4e75-4533-bfe2-cb8acbad7563", + "user_id": "aba84ddd-48c3-4041-98db-f80913a424a3", "username": "user1", "avatar_url": "", "seconds": 30540 diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_deployment_wide.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_deployment_wide.json.golden index 7cee822534cde..7f7fc92884eb1 100644 --- a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_deployment_wide.json.golden +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_deployment_wide.json.golden @@ -12,7 +12,7 @@ "template_ids": [ "00000000-0000-0000-0000-000000000001" ], - "user_id": "31972c11-d06a-44e5-ace7-d95192eaa8d6", + "user_id": "037c6a21-c805-46d8-a1ff-d98096afb44a", "username": "user2", "avatar_url": "", "seconds": 3600 @@ -21,7 +21,7 @@ "template_ids": [ "00000000-0000-0000-0000-000000000003" ], - "user_id": "cdb811f8-93c2-443a-8266-4d47b55f4094", + "user_id": "297dcbb4-2463-487a-92cd-7443d8d2a82d", "username": "user3", "avatar_url": "", "seconds": 4200 @@ -31,7 +31,7 @@ "00000000-0000-0000-0000-000000000001", "00000000-0000-0000-0000-000000000002" ], - "user_id": "e114f18f-4e75-4533-bfe2-cb8acbad7563", + "user_id": "aba84ddd-48c3-4041-98db-f80913a424a3", "username": "user1", "avatar_url": "", "seconds": 30540 diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_first_template.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_first_template.json.golden index 22ad5cc6d7d7d..caa41d6dea808 100644 --- a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_first_template.json.golden +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_first_template.json.golden @@ -10,7 +10,7 @@ "template_ids": [ "00000000-0000-0000-0000-000000000001" ], - "user_id": "31972c11-d06a-44e5-ace7-d95192eaa8d6", + "user_id": "037c6a21-c805-46d8-a1ff-d98096afb44a", "username": "user2", "avatar_url": "", "seconds": 3600 @@ -19,7 +19,7 @@ "template_ids": [ "00000000-0000-0000-0000-000000000001" ], - "user_id": "e114f18f-4e75-4533-bfe2-cb8acbad7563", + "user_id": "aba84ddd-48c3-4041-98db-f80913a424a3", "username": "user1", "avatar_url": "", "seconds": 5340 diff --git "a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden" "b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden" index 580facf5ebe28..1fb619ee23cd6 100644 --- "a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden" +++ "b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden" @@ -12,7 +12,7 @@ "template_ids": [ "00000000-0000-0000-0000-000000000001" ], - "user_id": "31972c11-d06a-44e5-ace7-d95192eaa8d6", + "user_id": "037c6a21-c805-46d8-a1ff-d98096afb44a", "username": "user2", "avatar_url": "", "seconds": 3600 @@ -21,7 +21,7 @@ "template_ids": [ "00000000-0000-0000-0000-000000000003" ], - "user_id": "cdb811f8-93c2-443a-8266-4d47b55f4094", + "user_id": "297dcbb4-2463-487a-92cd-7443d8d2a82d", "username": "user3", "avatar_url": "", "seconds": 600 @@ -31,7 +31,7 @@ "00000000-0000-0000-0000-000000000001", "00000000-0000-0000-0000-000000000002" ], - "user_id": "e114f18f-4e75-4533-bfe2-cb8acbad7563", + "user_id": "aba84ddd-48c3-4041-98db-f80913a424a3", "username": "user1", "avatar_url": "", "seconds": 23280 diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_second_template.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_second_template.json.golden index 78449633de5d3..58a1f97f8d77d 100644 --- a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_second_template.json.golden +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_second_template.json.golden @@ -10,7 +10,7 @@ "template_ids": [ "00000000-0000-0000-0000-000000000002" ], - "user_id": "e114f18f-4e75-4533-bfe2-cb8acbad7563", + "user_id": "aba84ddd-48c3-4041-98db-f80913a424a3", "username": "user1", "avatar_url": "", "seconds": 25200 diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_third_template.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_third_template.json.golden index 51da306613205..76541b9f0e320 100644 --- a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_third_template.json.golden +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_third_template.json.golden @@ -10,7 +10,7 @@ "template_ids": [ "00000000-0000-0000-0000-000000000003" ], - "user_id": "cdb811f8-93c2-443a-8266-4d47b55f4094", + "user_id": "297dcbb4-2463-487a-92cd-7443d8d2a82d", "username": "user3", "avatar_url": "", "seconds": 4200 diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden index c465b966e651c..0d6328f1fce0a 100644 --- a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden @@ -12,7 +12,7 @@ "template_ids": [ "00000000-0000-0000-0000-000000000001" ], - "user_id": "31972c11-d06a-44e5-ace7-d95192eaa8d6", + "user_id": "037c6a21-c805-46d8-a1ff-d98096afb44a", "username": "user2", "avatar_url": "", "seconds": 7200 @@ -21,7 +21,7 @@ "template_ids": [ "00000000-0000-0000-0000-000000000003" ], - "user_id": "cdb811f8-93c2-443a-8266-4d47b55f4094", + "user_id": "297dcbb4-2463-487a-92cd-7443d8d2a82d", "username": "user3", "avatar_url": "", "seconds": 4200 @@ -31,7 +31,7 @@ "00000000-0000-0000-0000-000000000001", "00000000-0000-0000-0000-000000000002" ], - "user_id": "e114f18f-4e75-4533-bfe2-cb8acbad7563", + "user_id": "aba84ddd-48c3-4041-98db-f80913a424a3", "username": "user1", "avatar_url": "", "seconds": 29820 diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden index 22ad5cc6d7d7d..caa41d6dea808 100644 --- a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden @@ -10,7 +10,7 @@ "template_ids": [ "00000000-0000-0000-0000-000000000001" ], - "user_id": "31972c11-d06a-44e5-ace7-d95192eaa8d6", + "user_id": "037c6a21-c805-46d8-a1ff-d98096afb44a", "username": "user2", "avatar_url": "", "seconds": 3600 @@ -19,7 +19,7 @@ "template_ids": [ "00000000-0000-0000-0000-000000000001" ], - "user_id": "e114f18f-4e75-4533-bfe2-cb8acbad7563", + "user_id": "aba84ddd-48c3-4041-98db-f80913a424a3", "username": "user1", "avatar_url": "", "seconds": 5340 diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden index 012d3727ad528..93380e7f8f782 100644 --- a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden @@ -12,7 +12,7 @@ "template_ids": [ "00000000-0000-0000-0000-000000000001" ], - "user_id": "31972c11-d06a-44e5-ace7-d95192eaa8d6", + "user_id": "037c6a21-c805-46d8-a1ff-d98096afb44a", "username": "user2", "avatar_url": "", "seconds": 7200 @@ -21,7 +21,7 @@ "template_ids": [ "00000000-0000-0000-0000-000000000003" ], - "user_id": "cdb811f8-93c2-443a-8266-4d47b55f4094", + "user_id": "297dcbb4-2463-487a-92cd-7443d8d2a82d", "username": "user3", "avatar_url": "", "seconds": 4200 @@ -31,7 +31,7 @@ "00000000-0000-0000-0000-000000000001", "00000000-0000-0000-0000-000000000002" ], - "user_id": "e114f18f-4e75-4533-bfe2-cb8acbad7563", + "user_id": "aba84ddd-48c3-4041-98db-f80913a424a3", "username": "user1", "avatar_url": "", "seconds": 29820 From 4ce994f39f2b03bb729d59f5c2dd70d811537afd Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 26 Sep 2023 10:33:31 +0200 Subject: [PATCH 19/23] Fix user IDs --- coderd/insights_test.go | 32 +++++++++++++++++-- ...es_three_weeks_second_template.json.golden | 2 +- ..._workspaces_week_all_templates.json.golden | 24 +++++++------- ...orkspaces_week_deployment_wide.json.golden | 24 +++++++------- ...workspaces_week_first_template.json.golden | 12 +++---- ...r_timezone_(S\303\243o_Paulo).json.golden" | 24 +++++++------- ...orkspaces_week_second_template.json.golden | 2 +- ...workspaces_week_third_template.json.golden | 2 +- ...kly_aggregated_deployment_wide.json.golden | 24 +++++++------- ...ekly_aggregated_first_template.json.golden | 12 +++---- ...es_weekly_aggregated_templates.json.golden | 24 +++++++------- 11 files changed, 105 insertions(+), 77 deletions(-) diff --git a/coderd/insights_test.go b/coderd/insights_test.go index e893358247c2d..15da56411c1a1 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -1300,6 +1300,9 @@ func TestUserActivityInsights_Golden(t *testing.T) { client *codersdk.Client sdk codersdk.User + + // Filled later. + id uuid.UUID } // Represent agent stats, to be inserted via stats batcher. @@ -1341,6 +1344,10 @@ func TestUserActivityInsights_Golden(t *testing.T) { for _, template := range templates { template.id = newStableUUID() } + for _, user := range users { + user.id = newStableUUID() + } + for _, user := range users { for _, workspace := range user.workspaces { workspace.user = user @@ -1395,9 +1402,30 @@ func TestUserActivityInsights_Golden(t *testing.T) { // Prepare all test users. for _, user := range users { - user.client, user.sdk = coderdtest.CreateAnotherUserMutators(t, client, firstUser.OrganizationID, nil, func(r *codersdk.CreateUserRequest) { - r.Username = user.name + _ = dbgen.User(t, db, database.User{ + ID: user.id, + Username: user.name, + Status: database.UserStatusActive, + }) + _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{ + UserID: user.id, + OrganizationID: firstUser.OrganizationID, + }) + token, err := client.CreateToken(context.Background(), user.id.String(), codersdk.CreateTokenRequest{ + Lifetime: time.Hour * 24, + Scope: codersdk.APIKeyScopeAll, + TokenName: "no-password-user-token", }) + require.NoError(t, err) + userClient := codersdk.New(client.URL) + userClient.SetSessionToken(token.Key) + + coderUser, err := userClient.User(context.Background(), user.id.String()) + require.NoError(t, err) + + user.client = userClient + user.sdk = coderUser + user.client.SetLogger(logger.Named("user").With(slog.Field{Name: "name", Value: user.name})) } diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_three_weeks_second_template.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_three_weeks_second_template.json.golden index c6e4e4da52cbc..d0656f9e9abda 100644 --- a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_three_weeks_second_template.json.golden +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_three_weeks_second_template.json.golden @@ -10,7 +10,7 @@ "template_ids": [ "00000000-0000-0000-0000-000000000002" ], - "user_id": "aba84ddd-48c3-4041-98db-f80913a424a3", + "user_id": "00000000-0000-0000-0000-000000000004", "username": "user1", "avatar_url": "", "seconds": 32400 diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_all_templates.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_all_templates.json.golden index 7f7fc92884eb1..90177a0d6dc4a 100644 --- a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_all_templates.json.golden +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_all_templates.json.golden @@ -8,11 +8,21 @@ "00000000-0000-0000-0000-000000000003" ], "users": [ + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002" + ], + "user_id": "00000000-0000-0000-0000-000000000004", + "username": "user1", + "avatar_url": "", + "seconds": 30540 + }, { "template_ids": [ "00000000-0000-0000-0000-000000000001" ], - "user_id": "037c6a21-c805-46d8-a1ff-d98096afb44a", + "user_id": "00000000-0000-0000-0000-000000000005", "username": "user2", "avatar_url": "", "seconds": 3600 @@ -21,20 +31,10 @@ "template_ids": [ "00000000-0000-0000-0000-000000000003" ], - "user_id": "297dcbb4-2463-487a-92cd-7443d8d2a82d", + "user_id": "00000000-0000-0000-0000-000000000006", "username": "user3", "avatar_url": "", "seconds": 4200 - }, - { - "template_ids": [ - "00000000-0000-0000-0000-000000000001", - "00000000-0000-0000-0000-000000000002" - ], - "user_id": "aba84ddd-48c3-4041-98db-f80913a424a3", - "username": "user1", - "avatar_url": "", - "seconds": 30540 } ] } diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_deployment_wide.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_deployment_wide.json.golden index 7f7fc92884eb1..90177a0d6dc4a 100644 --- a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_deployment_wide.json.golden +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_deployment_wide.json.golden @@ -8,11 +8,21 @@ "00000000-0000-0000-0000-000000000003" ], "users": [ + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002" + ], + "user_id": "00000000-0000-0000-0000-000000000004", + "username": "user1", + "avatar_url": "", + "seconds": 30540 + }, { "template_ids": [ "00000000-0000-0000-0000-000000000001" ], - "user_id": "037c6a21-c805-46d8-a1ff-d98096afb44a", + "user_id": "00000000-0000-0000-0000-000000000005", "username": "user2", "avatar_url": "", "seconds": 3600 @@ -21,20 +31,10 @@ "template_ids": [ "00000000-0000-0000-0000-000000000003" ], - "user_id": "297dcbb4-2463-487a-92cd-7443d8d2a82d", + "user_id": "00000000-0000-0000-0000-000000000006", "username": "user3", "avatar_url": "", "seconds": 4200 - }, - { - "template_ids": [ - "00000000-0000-0000-0000-000000000001", - "00000000-0000-0000-0000-000000000002" - ], - "user_id": "aba84ddd-48c3-4041-98db-f80913a424a3", - "username": "user1", - "avatar_url": "", - "seconds": 30540 } ] } diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_first_template.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_first_template.json.golden index caa41d6dea808..d3e9363babadb 100644 --- a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_first_template.json.golden +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_first_template.json.golden @@ -10,19 +10,19 @@ "template_ids": [ "00000000-0000-0000-0000-000000000001" ], - "user_id": "037c6a21-c805-46d8-a1ff-d98096afb44a", - "username": "user2", + "user_id": "00000000-0000-0000-0000-000000000004", + "username": "user1", "avatar_url": "", - "seconds": 3600 + "seconds": 5340 }, { "template_ids": [ "00000000-0000-0000-0000-000000000001" ], - "user_id": "aba84ddd-48c3-4041-98db-f80913a424a3", - "username": "user1", + "user_id": "00000000-0000-0000-0000-000000000005", + "username": "user2", "avatar_url": "", - "seconds": 5340 + "seconds": 3600 } ] } diff --git "a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden" "b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden" index 1fb619ee23cd6..9c4c934feef18 100644 --- "a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden" +++ "b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden" @@ -8,11 +8,21 @@ "00000000-0000-0000-0000-000000000003" ], "users": [ + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002" + ], + "user_id": "00000000-0000-0000-0000-000000000004", + "username": "user1", + "avatar_url": "", + "seconds": 23280 + }, { "template_ids": [ "00000000-0000-0000-0000-000000000001" ], - "user_id": "037c6a21-c805-46d8-a1ff-d98096afb44a", + "user_id": "00000000-0000-0000-0000-000000000005", "username": "user2", "avatar_url": "", "seconds": 3600 @@ -21,20 +31,10 @@ "template_ids": [ "00000000-0000-0000-0000-000000000003" ], - "user_id": "297dcbb4-2463-487a-92cd-7443d8d2a82d", + "user_id": "00000000-0000-0000-0000-000000000006", "username": "user3", "avatar_url": "", "seconds": 600 - }, - { - "template_ids": [ - "00000000-0000-0000-0000-000000000001", - "00000000-0000-0000-0000-000000000002" - ], - "user_id": "aba84ddd-48c3-4041-98db-f80913a424a3", - "username": "user1", - "avatar_url": "", - "seconds": 23280 } ] } diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_second_template.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_second_template.json.golden index 58a1f97f8d77d..f7e8c0d09eca5 100644 --- a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_second_template.json.golden +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_second_template.json.golden @@ -10,7 +10,7 @@ "template_ids": [ "00000000-0000-0000-0000-000000000002" ], - "user_id": "aba84ddd-48c3-4041-98db-f80913a424a3", + "user_id": "00000000-0000-0000-0000-000000000004", "username": "user1", "avatar_url": "", "seconds": 25200 diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_third_template.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_third_template.json.golden index 76541b9f0e320..065e837723fd3 100644 --- a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_third_template.json.golden +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_week_third_template.json.golden @@ -10,7 +10,7 @@ "template_ids": [ "00000000-0000-0000-0000-000000000003" ], - "user_id": "297dcbb4-2463-487a-92cd-7443d8d2a82d", + "user_id": "00000000-0000-0000-0000-000000000006", "username": "user3", "avatar_url": "", "seconds": 4200 diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden index 0d6328f1fce0a..458b327c3c392 100644 --- a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden @@ -8,11 +8,21 @@ "00000000-0000-0000-0000-000000000003" ], "users": [ + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002" + ], + "user_id": "00000000-0000-0000-0000-000000000004", + "username": "user1", + "avatar_url": "", + "seconds": 29820 + }, { "template_ids": [ "00000000-0000-0000-0000-000000000001" ], - "user_id": "037c6a21-c805-46d8-a1ff-d98096afb44a", + "user_id": "00000000-0000-0000-0000-000000000005", "username": "user2", "avatar_url": "", "seconds": 7200 @@ -21,20 +31,10 @@ "template_ids": [ "00000000-0000-0000-0000-000000000003" ], - "user_id": "297dcbb4-2463-487a-92cd-7443d8d2a82d", + "user_id": "00000000-0000-0000-0000-000000000006", "username": "user3", "avatar_url": "", "seconds": 4200 - }, - { - "template_ids": [ - "00000000-0000-0000-0000-000000000001", - "00000000-0000-0000-0000-000000000002" - ], - "user_id": "aba84ddd-48c3-4041-98db-f80913a424a3", - "username": "user1", - "avatar_url": "", - "seconds": 29820 } ] } diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden index caa41d6dea808..d3e9363babadb 100644 --- a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden @@ -10,19 +10,19 @@ "template_ids": [ "00000000-0000-0000-0000-000000000001" ], - "user_id": "037c6a21-c805-46d8-a1ff-d98096afb44a", - "username": "user2", + "user_id": "00000000-0000-0000-0000-000000000004", + "username": "user1", "avatar_url": "", - "seconds": 3600 + "seconds": 5340 }, { "template_ids": [ "00000000-0000-0000-0000-000000000001" ], - "user_id": "aba84ddd-48c3-4041-98db-f80913a424a3", - "username": "user1", + "user_id": "00000000-0000-0000-0000-000000000005", + "username": "user2", "avatar_url": "", - "seconds": 5340 + "seconds": 3600 } ] } diff --git a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden index 93380e7f8f782..b91414092f581 100644 --- a/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden +++ b/coderd/testdata/insights/user-activity/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden @@ -8,11 +8,21 @@ "00000000-0000-0000-0000-000000000003" ], "users": [ + { + "template_ids": [ + "00000000-0000-0000-0000-000000000001", + "00000000-0000-0000-0000-000000000002" + ], + "user_id": "00000000-0000-0000-0000-000000000004", + "username": "user1", + "avatar_url": "", + "seconds": 29820 + }, { "template_ids": [ "00000000-0000-0000-0000-000000000001" ], - "user_id": "037c6a21-c805-46d8-a1ff-d98096afb44a", + "user_id": "00000000-0000-0000-0000-000000000005", "username": "user2", "avatar_url": "", "seconds": 7200 @@ -21,20 +31,10 @@ "template_ids": [ "00000000-0000-0000-0000-000000000003" ], - "user_id": "297dcbb4-2463-487a-92cd-7443d8d2a82d", + "user_id": "00000000-0000-0000-0000-000000000006", "username": "user3", "avatar_url": "", "seconds": 4200 - }, - { - "template_ids": [ - "00000000-0000-0000-0000-000000000001", - "00000000-0000-0000-0000-000000000002" - ], - "user_id": "aba84ddd-48c3-4041-98db-f80913a424a3", - "username": "user1", - "avatar_url": "", - "seconds": 29820 } ] } From e3ecbfa1394d319bc73175746217b7e6c78ee8e8 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 26 Sep 2023 10:36:52 +0200 Subject: [PATCH 20/23] cleanup --- coderd/insights_test.go | 2 +- ...users_and_workspaces_three_weeks_second_template.json.golden | 0 ...multiple_users_and_workspaces_week_all_templates.json.golden | 0 ...ltiple_users_and_workspaces_week_deployment_wide.json.golden | 0 ...ultiple_users_and_workspaces_week_first_template.json.golden | 0 ...rkspaces_week_other_timezone_(S\303\243o_Paulo).json.golden" | 0 ...ltiple_users_and_workspaces_week_second_template.json.golden | 0 ...ultiple_users_and_workspaces_week_third_template.json.golden | 0 ...and_workspaces_weekly_aggregated_deployment_wide.json.golden | 0 ..._and_workspaces_weekly_aggregated_first_template.json.golden | 0 ...users_and_workspaces_weekly_aggregated_templates.json.golden | 0 .../{ => template}/parameters_two_days_ago,_no_data.json.golden | 0 .../parameters_yesterday_and_today_deployment_wide.json.golden | 0 13 files changed, 1 insertion(+), 1 deletion(-) rename coderd/testdata/insights/{ => template}/multiple_users_and_workspaces_three_weeks_second_template.json.golden (100%) rename coderd/testdata/insights/{ => template}/multiple_users_and_workspaces_week_all_templates.json.golden (100%) rename coderd/testdata/insights/{ => template}/multiple_users_and_workspaces_week_deployment_wide.json.golden (100%) rename coderd/testdata/insights/{ => template}/multiple_users_and_workspaces_week_first_template.json.golden (100%) rename "coderd/testdata/insights/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden" => "coderd/testdata/insights/template/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden" (100%) rename coderd/testdata/insights/{ => template}/multiple_users_and_workspaces_week_second_template.json.golden (100%) rename coderd/testdata/insights/{ => template}/multiple_users_and_workspaces_week_third_template.json.golden (100%) rename coderd/testdata/insights/{ => template}/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden (100%) rename coderd/testdata/insights/{ => template}/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden (100%) rename coderd/testdata/insights/{ => template}/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden (100%) rename coderd/testdata/insights/{ => template}/parameters_two_days_ago,_no_data.json.golden (100%) rename coderd/testdata/insights/{ => template}/parameters_yesterday_and_today_deployment_wide.json.golden (100%) diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 15da56411c1a1..38c6d3eaf2ce8 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -1230,7 +1230,7 @@ func TestTemplateInsights_Golden(t *testing.T) { } partialName := strings.Join(strings.Split(t.Name(), "/")[1:], "_") - goldenFile := filepath.Join("testdata", "insights", partialName+".json.golden") + goldenFile := filepath.Join("testdata", "insights", "template", partialName+".json.golden") if *updateGoldenFiles { err = os.MkdirAll(filepath.Dir(goldenFile), 0o755) require.NoError(t, err, "want no error creating golden file directory") diff --git a/coderd/testdata/insights/multiple_users_and_workspaces_three_weeks_second_template.json.golden b/coderd/testdata/insights/template/multiple_users_and_workspaces_three_weeks_second_template.json.golden similarity index 100% rename from coderd/testdata/insights/multiple_users_and_workspaces_three_weeks_second_template.json.golden rename to coderd/testdata/insights/template/multiple_users_and_workspaces_three_weeks_second_template.json.golden diff --git a/coderd/testdata/insights/multiple_users_and_workspaces_week_all_templates.json.golden b/coderd/testdata/insights/template/multiple_users_and_workspaces_week_all_templates.json.golden similarity index 100% rename from coderd/testdata/insights/multiple_users_and_workspaces_week_all_templates.json.golden rename to coderd/testdata/insights/template/multiple_users_and_workspaces_week_all_templates.json.golden diff --git a/coderd/testdata/insights/multiple_users_and_workspaces_week_deployment_wide.json.golden b/coderd/testdata/insights/template/multiple_users_and_workspaces_week_deployment_wide.json.golden similarity index 100% rename from coderd/testdata/insights/multiple_users_and_workspaces_week_deployment_wide.json.golden rename to coderd/testdata/insights/template/multiple_users_and_workspaces_week_deployment_wide.json.golden diff --git a/coderd/testdata/insights/multiple_users_and_workspaces_week_first_template.json.golden b/coderd/testdata/insights/template/multiple_users_and_workspaces_week_first_template.json.golden similarity index 100% rename from coderd/testdata/insights/multiple_users_and_workspaces_week_first_template.json.golden rename to coderd/testdata/insights/template/multiple_users_and_workspaces_week_first_template.json.golden diff --git "a/coderd/testdata/insights/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden" "b/coderd/testdata/insights/template/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden" similarity index 100% rename from "coderd/testdata/insights/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden" rename to "coderd/testdata/insights/template/multiple_users_and_workspaces_week_other_timezone_(S\303\243o_Paulo).json.golden" diff --git a/coderd/testdata/insights/multiple_users_and_workspaces_week_second_template.json.golden b/coderd/testdata/insights/template/multiple_users_and_workspaces_week_second_template.json.golden similarity index 100% rename from coderd/testdata/insights/multiple_users_and_workspaces_week_second_template.json.golden rename to coderd/testdata/insights/template/multiple_users_and_workspaces_week_second_template.json.golden diff --git a/coderd/testdata/insights/multiple_users_and_workspaces_week_third_template.json.golden b/coderd/testdata/insights/template/multiple_users_and_workspaces_week_third_template.json.golden similarity index 100% rename from coderd/testdata/insights/multiple_users_and_workspaces_week_third_template.json.golden rename to coderd/testdata/insights/template/multiple_users_and_workspaces_week_third_template.json.golden diff --git a/coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden b/coderd/testdata/insights/template/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden similarity index 100% rename from coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden rename to coderd/testdata/insights/template/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden diff --git a/coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden b/coderd/testdata/insights/template/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden similarity index 100% rename from coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden rename to coderd/testdata/insights/template/multiple_users_and_workspaces_weekly_aggregated_first_template.json.golden diff --git a/coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden b/coderd/testdata/insights/template/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden similarity index 100% rename from coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden rename to coderd/testdata/insights/template/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden diff --git a/coderd/testdata/insights/parameters_two_days_ago,_no_data.json.golden b/coderd/testdata/insights/template/parameters_two_days_ago,_no_data.json.golden similarity index 100% rename from coderd/testdata/insights/parameters_two_days_ago,_no_data.json.golden rename to coderd/testdata/insights/template/parameters_two_days_ago,_no_data.json.golden diff --git a/coderd/testdata/insights/parameters_yesterday_and_today_deployment_wide.json.golden b/coderd/testdata/insights/template/parameters_yesterday_and_today_deployment_wide.json.golden similarity index 100% rename from coderd/testdata/insights/parameters_yesterday_and_today_deployment_wide.json.golden rename to coderd/testdata/insights/template/parameters_yesterday_and_today_deployment_wide.json.golden From 854cf31c700c2d6f71d1086bc27a4c1b944774b5 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 26 Sep 2023 15:12:21 +0200 Subject: [PATCH 21/23] Try: truncate 1 min --- coderd/database/dbfake/dbfake.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 522106412b0a9..c7cc31ef243bb 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -2872,7 +2872,7 @@ func (q *FakeQuerier) GetUserActivityInsights(ctx context.Context, arg database. combinedStats[key] = make(map[time.Time]int64) } - t := s.SessionStartedAt.Truncate(5 * time.Minute) + t := s.SessionStartedAt.Truncate(time.Minute) if t.Before(arg.StartTime) { t = arg.StartTime } From f955f7f129887609eb6cb35a8de660ff37ec90e9 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 26 Sep 2023 18:16:05 +0200 Subject: [PATCH 22/23] Note --- coderd/database/querier.go | 4 ++++ coderd/database/queries.sql.go | 4 ++++ coderd/database/queries/insights.sql | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 676b20f1536bc..a9b3489d1f86b 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -156,6 +156,10 @@ type sqlcQuerier interface { // GetUserActivityInsights returns the ranking with top active users. // The result can be filtered on template_ids, meaning only user data from workspaces // based on those templates will be included. + // Note: When selecting data from multiple templates or the entire deployment, + // be aware that it may lead to an increase in "usage" numbers (cumulative). In such cases, + // users may be counted multiple times for the same time interval if they have used multiple templates + // simultaneously. GetUserActivityInsights(ctx context.Context, arg GetUserActivityInsightsParams) ([]GetUserActivityInsightsRow, error) GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 9145517c8097a..70930cb120038 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2110,6 +2110,10 @@ type GetUserActivityInsightsRow struct { // GetUserActivityInsights returns the ranking with top active users. // The result can be filtered on template_ids, meaning only user data from workspaces // based on those templates will be included. +// Note: When selecting data from multiple templates or the entire deployment, +// be aware that it may lead to an increase in "usage" numbers (cumulative). In such cases, +// users may be counted multiple times for the same time interval if they have used multiple templates +// simultaneously. func (q *sqlQuerier) GetUserActivityInsights(ctx context.Context, arg GetUserActivityInsightsParams) ([]GetUserActivityInsightsRow, error) { rows, err := q.db.QueryContext(ctx, getUserActivityInsights, pq.Array(arg.TemplateIDs), arg.StartTime, arg.EndTime) if err != nil { diff --git a/coderd/database/queries/insights.sql b/coderd/database/queries/insights.sql index ba73e4feb6bc5..7fb48100d5d8a 100644 --- a/coderd/database/queries/insights.sql +++ b/coderd/database/queries/insights.sql @@ -25,6 +25,10 @@ ORDER BY user_id ASC; -- GetUserActivityInsights returns the ranking with top active users. -- The result can be filtered on template_ids, meaning only user data from workspaces -- based on those templates will be included. +-- Note: When selecting data from multiple templates or the entire deployment, +-- be aware that it may lead to an increase in "usage" numbers (cumulative). In such cases, +-- users may be counted multiple times for the same time interval if they have used multiple templates +-- simultaneously. WITH app_stats AS ( SELECT s.start_time, From 3f1baa9ade50228a127d12cb799f9e3cd815903c Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 26 Sep 2023 18:19:12 +0200 Subject: [PATCH 23/23] Agent --- coderd/insights_test.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 5f32f647dc87f..49b7af0841a5a 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -136,15 +136,7 @@ func TestUserActivityInsights_SanityCheck(t *testing.T) { coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) // Start an agent so that we can generate stats. - agentClient := agentsdk.New(client.URL) - agentClient.SetSessionToken(authToken) - agentCloser := agent.New(agent.Options{ - Logger: logger.Named("agent"), - Client: agentClient, - }) - defer func() { - _ = agentCloser.Close() - }() + _ = agenttest.New(t, client.URL, authToken) resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) // Start must be at the beginning of the day, initialize it early in case