feat(coderd): add parameter insights to template insights (#8656)

This commit is contained in:
Mathias Fredriksson
2023-08-03 14:43:23 +00:00
committed by GitHub
parent 2ed453035e
commit d3991fac26
16 changed files with 713 additions and 37 deletions
+47
View File
@@ -9546,6 +9546,12 @@ const docTemplate = `{
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
}, },
"parameters_usage": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.TemplateParameterUsage"
}
},
"start_time": { "start_time": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -9573,6 +9579,47 @@ const docTemplate = `{
} }
} }
}, },
"codersdk.TemplateParameterUsage": {
"type": "object",
"properties": {
"display_name": {
"type": "string"
},
"name": {
"type": "string"
},
"options": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.TemplateVersionParameterOption"
}
},
"template_ids": {
"type": "array",
"items": {
"type": "string",
"format": "uuid"
}
},
"values": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.TemplateParameterValue"
}
}
}
},
"codersdk.TemplateParameterValue": {
"type": "object",
"properties": {
"count": {
"type": "integer"
},
"value": {
"type": "string"
}
}
},
"codersdk.TemplateRestartRequirement": { "codersdk.TemplateRestartRequirement": {
"type": "object", "type": "object",
"properties": { "properties": {
+47
View File
@@ -8626,6 +8626,12 @@
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
}, },
"parameters_usage": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.TemplateParameterUsage"
}
},
"start_time": { "start_time": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@@ -8653,6 +8659,47 @@
} }
} }
}, },
"codersdk.TemplateParameterUsage": {
"type": "object",
"properties": {
"display_name": {
"type": "string"
},
"name": {
"type": "string"
},
"options": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.TemplateVersionParameterOption"
}
},
"template_ids": {
"type": "array",
"items": {
"type": "string",
"format": "uuid"
}
},
"values": {
"type": "array",
"items": {
"$ref": "#/definitions/codersdk.TemplateParameterValue"
}
}
}
},
"codersdk.TemplateParameterValue": {
"type": "object",
"properties": {
"count": {
"type": "integer"
},
"value": {
"type": "string"
}
}
},
"codersdk.TemplateRestartRequirement": { "codersdk.TemplateRestartRequirement": {
"type": "object", "type": "object",
"properties": { "properties": {
+19
View File
@@ -1227,6 +1227,25 @@ func (q *querier) GetTemplateInsights(ctx context.Context, arg database.GetTempl
return q.db.GetTemplateInsights(ctx, arg) return q.db.GetTemplateInsights(ctx, arg)
} }
func (q *querier) GetTemplateParameterInsights(ctx context.Context, arg database.GetTemplateParameterInsightsParams) ([]database.GetTemplateParameterInsightsRow, error) {
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.GetTemplateParameterInsights(ctx, arg)
}
func (q *querier) GetTemplateVersionByID(ctx context.Context, tvid uuid.UUID) (database.TemplateVersion, error) { func (q *querier) GetTemplateVersionByID(ctx context.Context, tvid uuid.UUID) (database.TemplateVersion, error) {
tv, err := q.db.GetTemplateVersionByID(ctx, tvid) tv, err := q.db.GetTemplateVersionByID(ctx, tvid)
if err != nil { if err != nil {
+109
View File
@@ -596,6 +596,21 @@ func isNotNull(v interface{}) bool {
// these methods remain unimplemented in the FakeQuerier. // these methods remain unimplemented in the FakeQuerier.
var ErrUnimplemented = xerrors.New("unimplemented") var ErrUnimplemented = xerrors.New("unimplemented")
func uniqueSortedUUIDs(uuids []uuid.UUID) []uuid.UUID {
set := make(map[uuid.UUID]struct{})
for _, id := range uuids {
set[id] = struct{}{}
}
unique := make([]uuid.UUID, 0, len(set))
for id := range set {
unique = append(unique, id)
}
slices.SortFunc(unique, func(a, b uuid.UUID) bool {
return a.String() < b.String()
})
return unique
}
func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error { func (*FakeQuerier) AcquireLock(_ context.Context, _ int64) error {
return xerrors.New("AcquireLock must only be called within a transaction") return xerrors.New("AcquireLock must only be called within a transaction")
} }
@@ -2122,6 +2137,100 @@ func (q *FakeQuerier) GetTemplateInsights(_ context.Context, arg database.GetTem
return result, nil return result, nil
} }
func (q *FakeQuerier) GetTemplateParameterInsights(ctx context.Context, arg database.GetTemplateParameterInsightsParams) ([]database.GetTemplateParameterInsightsRow, error) {
err := validateDatabaseType(arg)
if err != nil {
return nil, err
}
q.mutex.RLock()
defer q.mutex.RUnlock()
// WITH latest_workspace_builds ...
latestWorkspaceBuilds := make(map[uuid.UUID]database.WorkspaceBuildTable)
for _, wb := range q.workspaceBuilds {
if wb.CreatedAt.Before(arg.StartTime) || wb.CreatedAt.Equal(arg.EndTime) || wb.CreatedAt.After(arg.EndTime) {
continue
}
if latestWorkspaceBuilds[wb.WorkspaceID].BuildNumber < wb.BuildNumber {
latestWorkspaceBuilds[wb.WorkspaceID] = wb
}
}
if len(arg.TemplateIDs) > 0 {
for wsID := range latestWorkspaceBuilds {
ws, err := q.getWorkspaceByIDNoLock(ctx, wsID)
if err != nil {
return nil, err
}
if slices.Contains(arg.TemplateIDs, ws.TemplateID) {
delete(latestWorkspaceBuilds, wsID)
}
}
}
// WITH unique_template_params ...
num := int64(0)
uniqueTemplateParams := make(map[string]*database.GetTemplateParameterInsightsRow)
uniqueTemplateParamWorkspaceBuildIDs := make(map[string][]uuid.UUID)
for _, wb := range latestWorkspaceBuilds {
tv, err := q.getTemplateVersionByIDNoLock(ctx, wb.TemplateVersionID)
if err != nil {
return nil, err
}
for _, tvp := range q.templateVersionParameters {
if tvp.TemplateVersionID != tv.ID {
continue
}
key := fmt.Sprintf("%s:%s:%s:%s", tvp.Name, tvp.DisplayName, tvp.Description, tvp.Options)
if _, ok := uniqueTemplateParams[key]; !ok {
num++
uniqueTemplateParams[key] = &database.GetTemplateParameterInsightsRow{
Num: num,
Name: tvp.Name,
DisplayName: tvp.DisplayName,
Description: tvp.Description,
Options: tvp.Options,
}
}
uniqueTemplateParams[key].TemplateIDs = append(uniqueTemplateParams[key].TemplateIDs, tv.TemplateID.UUID)
uniqueTemplateParamWorkspaceBuildIDs[key] = append(uniqueTemplateParamWorkspaceBuildIDs[key], wb.ID)
}
}
// SELECT ...
counts := make(map[string]map[string]int64)
for key, utp := range uniqueTemplateParams {
for _, wbp := range q.workspaceBuildParameters {
if !slices.Contains(uniqueTemplateParamWorkspaceBuildIDs[key], wbp.WorkspaceBuildID) {
continue
}
if wbp.Name != utp.Name {
continue
}
if counts[key] == nil {
counts[key] = make(map[string]int64)
}
counts[key][wbp.Value]++
}
}
var rows []database.GetTemplateParameterInsightsRow
for key, utp := range uniqueTemplateParams {
for value, count := range counts[key] {
rows = append(rows, database.GetTemplateParameterInsightsRow{
Num: utp.Num,
TemplateIDs: uniqueSortedUUIDs(utp.TemplateIDs),
Name: utp.Name,
DisplayName: utp.DisplayName,
Description: utp.Description,
Options: utp.Options,
Value: value,
Count: count,
})
}
}
return rows, nil
}
func (q *FakeQuerier) GetTemplateVersionByID(ctx context.Context, templateVersionID uuid.UUID) (database.TemplateVersion, error) { func (q *FakeQuerier) GetTemplateVersionByID(ctx context.Context, templateVersionID uuid.UUID) (database.TemplateVersion, error) {
q.mutex.RLock() q.mutex.RLock()
defer q.mutex.RUnlock() defer q.mutex.RUnlock()
+7
View File
@@ -634,6 +634,13 @@ func (m metricsStore) GetTemplateInsights(ctx context.Context, arg database.GetT
return r0, r1 return r0, r1
} }
func (m metricsStore) GetTemplateParameterInsights(ctx context.Context, arg database.GetTemplateParameterInsightsParams) ([]database.GetTemplateParameterInsightsRow, error) {
start := time.Now()
r0, r1 := m.s.GetTemplateParameterInsights(ctx, arg)
m.queryLatencies.WithLabelValues("GetTemplateParameterInsights").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m metricsStore) GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (database.TemplateVersion, error) { func (m metricsStore) GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (database.TemplateVersion, error) {
start := time.Now() start := time.Now()
version, err := m.s.GetTemplateVersionByID(ctx, id) version, err := m.s.GetTemplateVersionByID(ctx, id)
+15
View File
@@ -1286,6 +1286,21 @@ func (mr *MockStoreMockRecorder) GetTemplateInsights(arg0, arg1 interface{}) *go
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateInsights", reflect.TypeOf((*MockStore)(nil).GetTemplateInsights), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateInsights", reflect.TypeOf((*MockStore)(nil).GetTemplateInsights), arg0, arg1)
} }
// GetTemplateParameterInsights mocks base method.
func (m *MockStore) GetTemplateParameterInsights(arg0 context.Context, arg1 database.GetTemplateParameterInsightsParams) ([]database.GetTemplateParameterInsightsRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTemplateParameterInsights", arg0, arg1)
ret0, _ := ret[0].([]database.GetTemplateParameterInsightsRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetTemplateParameterInsights indicates an expected call of GetTemplateParameterInsights.
func (mr *MockStoreMockRecorder) GetTemplateParameterInsights(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateParameterInsights", reflect.TypeOf((*MockStore)(nil).GetTemplateParameterInsights), arg0, arg1)
}
// GetTemplateUserRoles mocks base method. // GetTemplateUserRoles mocks base method.
func (m *MockStore) GetTemplateUserRoles(arg0 context.Context, arg1 uuid.UUID) ([]database.TemplateUser, error) { func (m *MockStore) GetTemplateUserRoles(arg0 context.Context, arg1 uuid.UUID) ([]database.TemplateUser, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
+5
View File
@@ -116,6 +116,11 @@ type sqlcQuerier interface {
// GetTemplateInsights has a granularity of 5 minutes where if a session/app was // GetTemplateInsights has a granularity of 5 minutes where if a session/app was
// in use, we will add 5 minutes to the total usage for that session (per user). // in use, we will add 5 minutes to the total usage for that session (per user).
GetTemplateInsights(ctx context.Context, arg GetTemplateInsightsParams) (GetTemplateInsightsRow, error) GetTemplateInsights(ctx context.Context, arg GetTemplateInsightsParams) (GetTemplateInsightsRow, error)
// GetTemplateParameterInsights does for each template in a given timeframe,
// look for the latest workspace build (for every workspace) that has been
// created in the timeframe and return the aggregate usage counts of parameter
// values.
GetTemplateParameterInsights(ctx context.Context, arg GetTemplateParameterInsightsParams) ([]GetTemplateParameterInsightsRow, error)
GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (TemplateVersion, error) GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (TemplateVersion, error)
GetTemplateVersionByJobID(ctx context.Context, jobID uuid.UUID) (TemplateVersion, error) GetTemplateVersionByJobID(ctx context.Context, jobID uuid.UUID) (TemplateVersion, error)
GetTemplateVersionByTemplateIDAndName(ctx context.Context, arg GetTemplateVersionByTemplateIDAndNameParams) (TemplateVersion, error) GetTemplateVersionByTemplateIDAndName(ctx context.Context, arg GetTemplateVersionByTemplateIDAndNameParams) (TemplateVersion, error)
+102
View File
@@ -1553,6 +1553,108 @@ func (q *sqlQuerier) GetTemplateInsights(ctx context.Context, arg GetTemplateIns
return i, err return i, err
} }
const getTemplateParameterInsights = `-- name: GetTemplateParameterInsights :many
WITH latest_workspace_builds AS (
SELECT
wb.id,
wbmax.template_id,
wb.template_version_id
FROM (
SELECT
tv.template_id, wbmax.workspace_id, MAX(wbmax.build_number) as max_build_number
FROM workspace_builds wbmax
JOIN template_versions tv ON (tv.id = wbmax.template_version_id)
WHERE
wbmax.created_at >= $1::timestamptz
AND wbmax.created_at < $2::timestamptz
AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN tv.template_id = ANY($3::uuid[]) ELSE TRUE END
GROUP BY tv.template_id, wbmax.workspace_id
) wbmax
JOIN workspace_builds wb ON (
wb.workspace_id = wbmax.workspace_id
AND wb.build_number = wbmax.max_build_number
)
), unique_template_params AS (
SELECT
ROW_NUMBER() OVER () AS num,
array_agg(DISTINCT wb.template_id)::uuid[] AS template_ids,
array_agg(wb.id)::uuid[] AS workspace_build_ids,
tvp.name,
tvp.display_name,
tvp.description,
tvp.options
FROM latest_workspace_builds wb
JOIN template_version_parameters tvp ON (tvp.template_version_id = wb.template_version_id)
GROUP BY tvp.name, tvp.display_name, tvp.description, tvp.options
)
SELECT
utp.num,
utp.template_ids,
utp.name,
utp.display_name,
utp.description,
utp.options,
wbp.value,
COUNT(wbp.value) AS count
FROM unique_template_params utp
JOIN workspace_build_parameters wbp ON (utp.workspace_build_ids @> ARRAY[wbp.workspace_build_id] AND utp.name = wbp.name)
GROUP BY utp.num, utp.name, utp.display_name, utp.description, utp.options, utp.template_ids, wbp.value
`
type GetTemplateParameterInsightsParams struct {
StartTime time.Time `db:"start_time" json:"start_time"`
EndTime time.Time `db:"end_time" json:"end_time"`
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
}
type GetTemplateParameterInsightsRow struct {
Num int64 `db:"num" json:"num"`
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
Name string `db:"name" json:"name"`
DisplayName string `db:"display_name" json:"display_name"`
Description string `db:"description" json:"description"`
Options json.RawMessage `db:"options" json:"options"`
Value string `db:"value" json:"value"`
Count int64 `db:"count" json:"count"`
}
// GetTemplateParameterInsights does for each template in a given timeframe,
// look for the latest workspace build (for every workspace) that has been
// created in the timeframe and return the aggregate usage counts of parameter
// values.
func (q *sqlQuerier) GetTemplateParameterInsights(ctx context.Context, arg GetTemplateParameterInsightsParams) ([]GetTemplateParameterInsightsRow, error) {
rows, err := q.db.QueryContext(ctx, getTemplateParameterInsights, arg.StartTime, arg.EndTime, pq.Array(arg.TemplateIDs))
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTemplateParameterInsightsRow
for rows.Next() {
var i GetTemplateParameterInsightsRow
if err := rows.Scan(
&i.Num,
pq.Array(&i.TemplateIDs),
&i.Name,
&i.DisplayName,
&i.Description,
&i.Options,
&i.Value,
&i.Count,
); 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 const getUserLatencyInsights = `-- name: GetUserLatencyInsights :many
SELECT SELECT
workspace_agent_stats.user_id, workspace_agent_stats.user_id,
+53
View File
@@ -114,3 +114,56 @@ SELECT
COUNT(DISTINCT user_id) AS active_users COUNT(DISTINCT user_id) AS active_users
FROM usage_by_day FROM usage_by_day
GROUP BY from_, to_; GROUP BY from_, to_;
-- name: GetTemplateParameterInsights :many
-- GetTemplateParameterInsights does for each template in a given timeframe,
-- look for the latest workspace build (for every workspace) that has been
-- created in the timeframe and return the aggregate usage counts of parameter
-- values.
WITH latest_workspace_builds AS (
SELECT
wb.id,
wbmax.template_id,
wb.template_version_id
FROM (
SELECT
tv.template_id, wbmax.workspace_id, MAX(wbmax.build_number) as max_build_number
FROM workspace_builds wbmax
JOIN template_versions tv ON (tv.id = wbmax.template_version_id)
WHERE
wbmax.created_at >= @start_time::timestamptz
AND wbmax.created_at < @end_time::timestamptz
AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN tv.template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
GROUP BY tv.template_id, wbmax.workspace_id
) wbmax
JOIN workspace_builds wb ON (
wb.workspace_id = wbmax.workspace_id
AND wb.build_number = wbmax.max_build_number
)
), unique_template_params AS (
SELECT
ROW_NUMBER() OVER () AS num,
array_agg(DISTINCT wb.template_id)::uuid[] AS template_ids,
array_agg(wb.id)::uuid[] AS workspace_build_ids,
tvp.name,
tvp.display_name,
tvp.description,
tvp.options
FROM latest_workspace_builds wb
JOIN template_version_parameters tvp ON (tvp.template_version_id = wb.template_version_id)
GROUP BY tvp.name, tvp.display_name, tvp.description, tvp.options
)
SELECT
utp.num,
utp.template_ids,
utp.name,
utp.display_name,
utp.description,
utp.options,
wbp.value,
COUNT(wbp.value) AS count
FROM unique_template_params utp
JOIN workspace_build_parameters wbp ON (utp.workspace_build_ids @> ARRAY[wbp.workspace_build_id] AND utp.name = wbp.name)
GROUP BY utp.num, utp.name, utp.display_name, utp.description, utp.options, utp.template_ids, wbp.value;
+68 -8
View File
@@ -2,8 +2,10 @@ package coderd
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"sort"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
@@ -190,11 +192,11 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
// Use a transaction to ensure that we get consistent data between // Use a transaction to ensure that we get consistent data between
// the full and interval report. // the full and interval report.
err := api.Database.InTx(func(db database.Store) error { err := api.Database.InTx(func(tx database.Store) error {
var err error var err error
if interval != "" { if interval != "" {
dailyUsage, err = db.GetTemplateDailyInsights(ctx, database.GetTemplateDailyInsightsParams{ dailyUsage, err = tx.GetTemplateDailyInsights(ctx, database.GetTemplateDailyInsightsParams{
StartTime: startTime, StartTime: startTime,
EndTime: endTime, EndTime: endTime,
TemplateIDs: templateIDs, TemplateIDs: templateIDs,
@@ -204,7 +206,7 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
} }
} }
usage, err = db.GetTemplateInsights(ctx, database.GetTemplateInsightsParams{ usage, err = tx.GetTemplateInsights(ctx, database.GetTemplateInsightsParams{
StartTime: startTime, StartTime: startTime,
EndTime: endTime, EndTime: endTime,
TemplateIDs: templateIDs, TemplateIDs: templateIDs,
@@ -227,13 +229,38 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
return return
} }
// Template parameter insights have no risk of inconsistency with the other
// insights, so we don't need to perform this in a transaction.
parameterRows, err := api.Database.GetTemplateParameterInsights(ctx, database.GetTemplateParameterInsightsParams{
StartTime: startTime,
EndTime: endTime,
TemplateIDs: templateIDs,
})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching template parameter insights.",
Detail: err.Error(),
})
return
}
parametersUsage, err := convertTemplateInsightsParameters(parameterRows)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error converting template parameter insights.",
Detail: err.Error(),
})
return
}
resp := codersdk.TemplateInsightsResponse{ resp := codersdk.TemplateInsightsResponse{
Report: codersdk.TemplateInsightsReport{ Report: codersdk.TemplateInsightsReport{
StartTime: startTime, StartTime: startTime,
EndTime: endTime, EndTime: endTime,
TemplateIDs: usage.TemplateIDs, TemplateIDs: usage.TemplateIDs,
ActiveUsers: usage.ActiveUsers, ActiveUsers: usage.ActiveUsers,
AppsUsage: convertTemplateInsightsBuiltinApps(usage), AppsUsage: convertTemplateInsightsBuiltinApps(usage),
ParametersUsage: parametersUsage,
}, },
IntervalReports: []codersdk.TemplateInsightsIntervalReport{}, IntervalReports: []codersdk.TemplateInsightsIntervalReport{},
} }
@@ -288,6 +315,39 @@ func convertTemplateInsightsBuiltinApps(usage database.GetTemplateInsightsRow) [
} }
} }
func convertTemplateInsightsParameters(parameterRows []database.GetTemplateParameterInsightsRow) ([]codersdk.TemplateParameterUsage, error) {
parametersByNum := make(map[int64]*codersdk.TemplateParameterUsage)
for _, param := range parameterRows {
if _, ok := parametersByNum[param.Num]; !ok {
var opts []codersdk.TemplateVersionParameterOption
err := json.Unmarshal(param.Options, &opts)
if err != nil {
return nil, xerrors.Errorf("unmarshal template parameter options: %w", err)
}
parametersByNum[param.Num] = &codersdk.TemplateParameterUsage{
TemplateIDs: param.TemplateIDs,
Name: param.Name,
DisplayName: param.DisplayName,
Options: opts,
}
}
parametersByNum[param.Num].Values = append(parametersByNum[param.Num].Values, codersdk.TemplateParameterValue{
Value: param.Value,
Count: param.Count,
})
}
parametersUsage := []codersdk.TemplateParameterUsage{}
for _, param := range parametersByNum {
parametersUsage = append(parametersUsage, *param)
}
sort.Slice(parametersUsage, func(i, j int) bool {
return parametersUsage[i].Name < parametersUsage[j].Name
})
return parametersUsage, nil
}
// parseInsightsStartAndEndTime parses the start and end time query parameters // parseInsightsStartAndEndTime parses the start and end time query parameters
// and returns the parsed values. The client provided timezone must be preserved // and returns the parsed values. The client provided timezone must be preserved
// when parsing the time. Verification is performed so that the start and end // when parsing the time. Verification is performed so that the start and end
+89 -5
View File
@@ -20,6 +20,7 @@ import (
"github.com/coder/coder/codersdk" "github.com/coder/coder/codersdk"
"github.com/coder/coder/codersdk/agentsdk" "github.com/coder/coder/codersdk/agentsdk"
"github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
"github.com/coder/coder/testutil" "github.com/coder/coder/testutil"
) )
@@ -231,6 +232,32 @@ func TestUserLatencyInsights_BadRequest(t *testing.T) {
func TestTemplateInsights(t *testing.T) { func TestTemplateInsights(t *testing.T) {
t.Parallel() t.Parallel()
const (
firstParameterName = "first_parameter"
firstParameterDisplayName = "First PARAMETER"
firstParameterType = "string"
firstParameterDescription = "This is first parameter"
firstParameterValue = "abc"
secondParameterName = "second_parameter"
secondParameterDisplayName = "Second PARAMETER"
secondParameterType = "number"
secondParameterDescription = "This is second parameter"
secondParameterValue = "123"
thirdParameterName = "third_parameter"
thirdParameterDisplayName = "Third PARAMETER"
thirdParameterType = "string"
thirdParameterDescription = "This is third parameter"
thirdParameterValue = "bbb"
thirdParameterOptionName1 = "This is AAA"
thirdParameterOptionValue1 = "aaa"
thirdParameterOptionName2 = "This is BBB"
thirdParameterOptionValue2 = "bbb"
thirdParameterOptionName3 = "This is CCC"
thirdParameterOptionValue3 = "ccc"
)
logger := slogtest.Make(t, nil) logger := slogtest.Make(t, nil)
opts := &coderdtest.Options{ opts := &coderdtest.Options{
IncludeProvisionerDaemon: true, IncludeProvisionerDaemon: true,
@@ -241,15 +268,39 @@ func TestTemplateInsights(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client) user := coderdtest.CreateFirstUser(t, client)
authToken := uuid.NewString() authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionPlan: echo.ProvisionComplete, ProvisionPlan: []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Parameters: []*proto.RichParameter{
{Name: firstParameterName, DisplayName: firstParameterDisplayName, Type: firstParameterType, Description: firstParameterDescription, Required: true},
{Name: secondParameterName, DisplayName: secondParameterDisplayName, Type: secondParameterType, Description: secondParameterDescription, Required: true},
{Name: thirdParameterName, DisplayName: thirdParameterDisplayName, Type: thirdParameterType, Description: thirdParameterDescription, Required: true, Options: []*proto.RichParameterOption{
{Name: thirdParameterOptionName1, Value: thirdParameterOptionValue1},
{Name: thirdParameterOptionName2, Value: thirdParameterOptionValue2},
{Name: thirdParameterOptionName3, Value: thirdParameterOptionValue3},
}},
},
},
},
},
},
ProvisionApply: echo.ProvisionApplyWithAgent(authToken), ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
}) })
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart]) require.Empty(t, template.BuildTimeStats[codersdk.WorkspaceTransitionStart])
coderdtest.AwaitTemplateVersionJob(t, client, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
buildParameters := []codersdk.WorkspaceBuildParameter{
{Name: firstParameterName, Value: firstParameterValue},
{Name: secondParameterName, Value: secondParameterValue},
{Name: thirdParameterName, Value: thirdParameterValue},
}
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.RichParameterValues = buildParameters
})
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
// Start an agent so that we can generate stats. // Start an agent so that we can generate stats.
@@ -346,10 +397,43 @@ func TestTemplateInsights(t *testing.T) {
} }
} }
// The full timeframe is <= 24h, so the interval matches exactly. // The full timeframe is <= 24h, so the interval matches exactly.
assert.Len(t, resp.IntervalReports, 1, "want one interval report") require.Len(t, resp.IntervalReports, 1, "want one interval report")
assert.WithinDuration(t, req.StartTime, resp.IntervalReports[0].StartTime, 0) assert.WithinDuration(t, req.StartTime, resp.IntervalReports[0].StartTime, 0)
assert.WithinDuration(t, req.EndTime, resp.IntervalReports[0].EndTime, 0) assert.WithinDuration(t, req.EndTime, resp.IntervalReports[0].EndTime, 0)
assert.Equal(t, resp.IntervalReports[0].ActiveUsers, int64(1), "want one active user in the interval report") assert.Equal(t, resp.IntervalReports[0].ActiveUsers, int64(1), "want one active user in the interval report")
// The workspace uses 3 parameters
require.Len(t, resp.Report.ParametersUsage, 3)
assert.Equal(t, firstParameterName, resp.Report.ParametersUsage[0].Name)
assert.Equal(t, firstParameterDisplayName, resp.Report.ParametersUsage[0].DisplayName)
assert.Contains(t, resp.Report.ParametersUsage[0].Values, codersdk.TemplateParameterValue{
Value: firstParameterValue,
Count: 1,
})
assert.Contains(t, resp.Report.ParametersUsage[0].TemplateIDs, template.ID)
assert.Empty(t, resp.Report.ParametersUsage[0].Options)
assert.Equal(t, secondParameterName, resp.Report.ParametersUsage[1].Name)
assert.Equal(t, secondParameterDisplayName, resp.Report.ParametersUsage[1].DisplayName)
assert.Contains(t, resp.Report.ParametersUsage[1].Values, codersdk.TemplateParameterValue{
Value: secondParameterValue,
Count: 1,
})
assert.Contains(t, resp.Report.ParametersUsage[1].TemplateIDs, template.ID)
assert.Empty(t, resp.Report.ParametersUsage[1].Options)
assert.Equal(t, thirdParameterName, resp.Report.ParametersUsage[2].Name)
assert.Equal(t, thirdParameterDisplayName, resp.Report.ParametersUsage[2].DisplayName)
assert.Contains(t, resp.Report.ParametersUsage[2].Values, codersdk.TemplateParameterValue{
Value: thirdParameterValue,
Count: 1,
})
assert.Contains(t, resp.Report.ParametersUsage[2].TemplateIDs, template.ID)
assert.Equal(t, []codersdk.TemplateVersionParameterOption{
{Name: thirdParameterOptionName1, Value: thirdParameterOptionValue1},
{Name: thirdParameterOptionName2, Value: thirdParameterOptionValue2},
{Name: thirdParameterOptionName3, Value: thirdParameterOptionValue3},
}, resp.Report.ParametersUsage[2].Options)
} }
func TestTemplateInsights_BadRequest(t *testing.T) { func TestTemplateInsights_BadRequest(t *testing.T) {
+13 -17
View File
@@ -94,13 +94,12 @@ type TemplateInsightsResponse struct {
// TemplateInsightsReport is the report from the template insights endpoint. // TemplateInsightsReport is the report from the template insights endpoint.
type TemplateInsightsReport struct { type TemplateInsightsReport struct {
StartTime time.Time `json:"start_time" format:"date-time"` StartTime time.Time `json:"start_time" format:"date-time"`
EndTime time.Time `json:"end_time" format:"date-time"` EndTime time.Time `json:"end_time" format:"date-time"`
TemplateIDs []uuid.UUID `json:"template_ids" format:"uuid"` TemplateIDs []uuid.UUID `json:"template_ids" format:"uuid"`
ActiveUsers int64 `json:"active_users" example:"22"` ActiveUsers int64 `json:"active_users" example:"22"`
AppsUsage []TemplateAppUsage `json:"apps_usage"` AppsUsage []TemplateAppUsage `json:"apps_usage"`
// TODO(mafredri): To be introduced in a future pull request. ParametersUsage []TemplateParameterUsage `json:"parameters_usage"`
// TemplateParametersUsage []TemplateParameterUsage `json:"parameters_usage"`
} }
// TemplateInsightsIntervalReport is the report from the template insights // TemplateInsightsIntervalReport is the report from the template insights
@@ -133,25 +132,22 @@ type TemplateAppUsage struct {
Seconds int64 `json:"seconds" example:"80500"` Seconds int64 `json:"seconds" example:"80500"`
} }
// TODO(mafredri): To be introduced in a future pull request.
/*
// TemplateParameterUsage shows the usage of a parameter for one or more // TemplateParameterUsage shows the usage of a parameter for one or more
// templates. // templates.
type TemplateParameterUsage struct { type TemplateParameterUsage struct {
TemplateIDs []uuid.UUID `json:"template_ids" format:"uuid"` TemplateIDs []uuid.UUID `json:"template_ids" format:"uuid"`
DisplayName string `json:"display_name"` DisplayName string `json:"display_name"`
Name string `json:"name"` Name string `json:"name"`
Values []TemplateParameterValue `json:"values"` Options []TemplateVersionParameterOption `json:"options,omitempty"`
Values []TemplateParameterValue `json:"values"`
} }
// TemplateParameterValue shows the usage of a parameter value for one or more // TemplateParameterValue shows the usage of a parameter value for one or more
// templates. // templates.
type TemplateParameterValue struct { type TemplateParameterValue struct {
Value *string `json:"value"` Value string `json:"value"`
Icon string `json:"icon"` Count int64 `json:"count"`
Count int64 `json:"count"`
} }
*/
type TemplateInsightsRequest struct { type TemplateInsightsRequest struct {
StartTime time.Time `json:"start_time" format:"date-time"` StartTime time.Time `json:"start_time" format:"date-time"`
+21
View File
@@ -78,6 +78,27 @@ curl -X GET http://coder-server:8080/api/v2/insights/templates \
} }
], ],
"end_time": "2019-08-24T14:15:22Z", "end_time": "2019-08-24T14:15:22Z",
"parameters_usage": [
{
"display_name": "string",
"name": "string",
"options": [
{
"description": "string",
"icon": "string",
"name": "string",
"value": "string"
}
],
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"values": [
{
"count": 0,
"value": "string"
}
]
}
],
"start_time": "2019-08-24T14:15:22Z", "start_time": "2019-08-24T14:15:22Z",
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"] "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"]
} }
+100 -7
View File
@@ -4315,6 +4315,27 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
} }
], ],
"end_time": "2019-08-24T14:15:22Z", "end_time": "2019-08-24T14:15:22Z",
"parameters_usage": [
{
"display_name": "string",
"name": "string",
"options": [
{
"description": "string",
"icon": "string",
"name": "string",
"value": "string"
}
],
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"values": [
{
"count": 0,
"value": "string"
}
]
}
],
"start_time": "2019-08-24T14:15:22Z", "start_time": "2019-08-24T14:15:22Z",
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"] "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"]
} }
@@ -4322,13 +4343,14 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
### Properties ### Properties
| Name | Type | Required | Restrictions | Description | | Name | Type | Required | Restrictions | Description |
| -------------- | --------------------------------------------------------------- | -------- | ------------ | ----------- | | ------------------ | --------------------------------------------------------------------------- | -------- | ------------ | ----------- |
| `active_users` | integer | false | | | | `active_users` | integer | false | | |
| `apps_usage` | array of [codersdk.TemplateAppUsage](#codersdktemplateappusage) | false | | | | `apps_usage` | array of [codersdk.TemplateAppUsage](#codersdktemplateappusage) | false | | |
| `end_time` | string | false | | | | `end_time` | string | false | | |
| `start_time` | string | false | | | | `parameters_usage` | array of [codersdk.TemplateParameterUsage](#codersdktemplateparameterusage) | false | | |
| `template_ids` | array of string | false | | | | `start_time` | string | false | | |
| `template_ids` | array of string | false | | |
## codersdk.TemplateInsightsResponse ## codersdk.TemplateInsightsResponse
@@ -4356,6 +4378,27 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
} }
], ],
"end_time": "2019-08-24T14:15:22Z", "end_time": "2019-08-24T14:15:22Z",
"parameters_usage": [
{
"display_name": "string",
"name": "string",
"options": [
{
"description": "string",
"icon": "string",
"name": "string",
"value": "string"
}
],
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"values": [
{
"count": 0,
"value": "string"
}
]
}
],
"start_time": "2019-08-24T14:15:22Z", "start_time": "2019-08-24T14:15:22Z",
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"] "template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"]
} }
@@ -4369,6 +4412,56 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `interval_reports` | array of [codersdk.TemplateInsightsIntervalReport](#codersdktemplateinsightsintervalreport) | false | | | | `interval_reports` | array of [codersdk.TemplateInsightsIntervalReport](#codersdktemplateinsightsintervalreport) | false | | |
| `report` | [codersdk.TemplateInsightsReport](#codersdktemplateinsightsreport) | false | | | | `report` | [codersdk.TemplateInsightsReport](#codersdktemplateinsightsreport) | false | | |
## codersdk.TemplateParameterUsage
```json
{
"display_name": "string",
"name": "string",
"options": [
{
"description": "string",
"icon": "string",
"name": "string",
"value": "string"
}
],
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"],
"values": [
{
"count": 0,
"value": "string"
}
]
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| -------------- | ------------------------------------------------------------------------------------------- | -------- | ------------ | ----------- |
| `display_name` | string | false | | |
| `name` | string | false | | |
| `options` | array of [codersdk.TemplateVersionParameterOption](#codersdktemplateversionparameteroption) | false | | |
| `template_ids` | array of string | false | | |
| `values` | array of [codersdk.TemplateParameterValue](#codersdktemplateparametervalue) | false | | |
## codersdk.TemplateParameterValue
```json
{
"count": 0,
"value": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| ------- | ------- | -------- | ------------ | ----------- |
| `count` | integer | false | | |
| `value` | string | false | | |
## codersdk.TemplateRestartRequirement ## codersdk.TemplateRestartRequirement
```json ```json
+16
View File
@@ -963,6 +963,7 @@ export interface TemplateInsightsReport {
readonly template_ids: string[] readonly template_ids: string[]
readonly active_users: number readonly active_users: number
readonly apps_usage: TemplateAppUsage[] readonly apps_usage: TemplateAppUsage[]
readonly parameters_usage: TemplateParameterUsage[]
} }
// From codersdk/insights.go // From codersdk/insights.go
@@ -979,6 +980,21 @@ export interface TemplateInsightsResponse {
readonly interval_reports: TemplateInsightsIntervalReport[] readonly interval_reports: TemplateInsightsIntervalReport[]
} }
// From codersdk/insights.go
export interface TemplateParameterUsage {
readonly template_ids: string[]
readonly display_name: string
readonly name: string
readonly options?: TemplateVersionParameterOption[]
readonly values: TemplateParameterValue[]
}
// From codersdk/insights.go
export interface TemplateParameterValue {
readonly value: string
readonly count: number
}
// From codersdk/templates.go // From codersdk/templates.go
export interface TemplateRestartRequirement { export interface TemplateRestartRequirement {
readonly days_of_week: string[] readonly days_of_week: string[]
@@ -26,6 +26,7 @@ export const Empty: Story = {
start_time: "", start_time: "",
template_ids: [], template_ids: [],
apps_usage: [], apps_usage: [],
parameters_usage: [],
}, },
}, },
userLatency: { userLatency: {
@@ -82,6 +83,7 @@ export const Loaded: Story = {
seconds: 1020900, seconds: 1020900,
}, },
], ],
parameters_usage: [],
}, },
interval_reports: [ interval_reports: [
{ {