mirror of
https://github.com/coder/coder.git
synced 2025-07-08 11:39:50 +00:00
feat(coderd): add parameter insights to template insights (#8656)
This commit is contained in:
committed by
GitHub
parent
2ed453035e
commit
d3991fac26
47
coderd/apidoc/docs.go
generated
47
coderd/apidoc/docs.go
generated
@ -9546,6 +9546,12 @@ const docTemplate = `{
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"parameters_usage": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.TemplateParameterUsage"
|
||||
}
|
||||
},
|
||||
"start_time": {
|
||||
"type": "string",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
47
coderd/apidoc/swagger.json
generated
47
coderd/apidoc/swagger.json
generated
@ -8626,6 +8626,12 @@
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"parameters_usage": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.TemplateParameterUsage"
|
||||
}
|
||||
},
|
||||
"start_time": {
|
||||
"type": "string",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -1227,6 +1227,25 @@ func (q *querier) GetTemplateInsights(ctx context.Context, arg database.GetTempl
|
||||
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) {
|
||||
tv, err := q.db.GetTemplateVersionByID(ctx, tvid)
|
||||
if err != nil {
|
||||
|
@ -596,6 +596,21 @@ func isNotNull(v interface{}) bool {
|
||||
// these methods remain unimplemented in the FakeQuerier.
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
@ -634,6 +634,13 @@ func (m metricsStore) GetTemplateInsights(ctx context.Context, arg database.GetT
|
||||
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) {
|
||||
start := time.Now()
|
||||
version, err := m.s.GetTemplateVersionByID(ctx, id)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (m *MockStore) GetTemplateUserRoles(arg0 context.Context, arg1 uuid.UUID) ([]database.TemplateUser, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -116,6 +116,11 @@ type sqlcQuerier interface {
|
||||
// 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).
|
||||
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)
|
||||
GetTemplateVersionByJobID(ctx context.Context, jobID uuid.UUID) (TemplateVersion, error)
|
||||
GetTemplateVersionByTemplateIDAndName(ctx context.Context, arg GetTemplateVersionByTemplateIDAndNameParams) (TemplateVersion, error)
|
||||
|
@ -1553,6 +1553,108 @@ func (q *sqlQuerier) GetTemplateInsights(ctx context.Context, arg GetTemplateIns
|
||||
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
|
||||
SELECT
|
||||
workspace_agent_stats.user_id,
|
||||
|
@ -114,3 +114,56 @@ SELECT
|
||||
COUNT(DISTINCT user_id) AS active_users
|
||||
FROM usage_by_day
|
||||
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;
|
||||
|
@ -2,8 +2,10 @@ package coderd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"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
|
||||
// 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
|
||||
|
||||
if interval != "" {
|
||||
dailyUsage, err = db.GetTemplateDailyInsights(ctx, database.GetTemplateDailyInsightsParams{
|
||||
dailyUsage, err = tx.GetTemplateDailyInsights(ctx, database.GetTemplateDailyInsightsParams{
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
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,
|
||||
EndTime: endTime,
|
||||
TemplateIDs: templateIDs,
|
||||
@ -227,13 +229,38 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
|
||||
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{
|
||||
Report: codersdk.TemplateInsightsReport{
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
TemplateIDs: usage.TemplateIDs,
|
||||
ActiveUsers: usage.ActiveUsers,
|
||||
AppsUsage: convertTemplateInsightsBuiltinApps(usage),
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
TemplateIDs: usage.TemplateIDs,
|
||||
ActiveUsers: usage.ActiveUsers,
|
||||
AppsUsage: convertTemplateInsightsBuiltinApps(usage),
|
||||
ParametersUsage: parametersUsage,
|
||||
},
|
||||
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
|
||||
// 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
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/codersdk/agentsdk"
|
||||
"github.com/coder/coder/provisioner/echo"
|
||||
"github.com/coder/coder/provisionersdk/proto"
|
||||
"github.com/coder/coder/testutil"
|
||||
)
|
||||
|
||||
@ -231,6 +232,32 @@ func TestUserLatencyInsights_BadRequest(t *testing.T) {
|
||||
func TestTemplateInsights(t *testing.T) {
|
||||
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)
|
||||
opts := &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
@ -241,15 +268,39 @@ func TestTemplateInsights(t *testing.T) {
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
authToken := uuid.NewString()
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.ProvisionComplete,
|
||||
Parse: echo.ParseComplete,
|
||||
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),
|
||||
})
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
// 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.
|
||||
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.EndTime, resp.IntervalReports[0].EndTime, 0)
|
||||
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) {
|
||||
|
Reference in New Issue
Block a user