mirror of https://github.com/coder/coder.git
feat(coderd): support weekly aggregated insights (#9684)
This commit is contained in:
parent
b358e3d558
commit
b0e3daa120
|
@ -8438,10 +8438,12 @@ const docTemplate = `{
|
|||
"codersdk.InsightsReportInterval": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"day"
|
||||
"day",
|
||||
"week"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"InsightsReportIntervalDay"
|
||||
"InsightsReportIntervalDay",
|
||||
"InsightsReportIntervalWeek"
|
||||
]
|
||||
},
|
||||
"codersdk.IssueReconnectingPTYSignedTokenRequest": {
|
||||
|
@ -9732,7 +9734,12 @@ const docTemplate = `{
|
|||
"format": "date-time"
|
||||
},
|
||||
"interval": {
|
||||
"$ref": "#/definitions/codersdk.InsightsReportInterval"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.InsightsReportInterval"
|
||||
}
|
||||
],
|
||||
"example": "week"
|
||||
},
|
||||
"start_time": {
|
||||
"type": "string",
|
||||
|
|
|
@ -7578,8 +7578,11 @@
|
|||
},
|
||||
"codersdk.InsightsReportInterval": {
|
||||
"type": "string",
|
||||
"enum": ["day"],
|
||||
"x-enum-varnames": ["InsightsReportIntervalDay"]
|
||||
"enum": ["day", "week"],
|
||||
"x-enum-varnames": [
|
||||
"InsightsReportIntervalDay",
|
||||
"InsightsReportIntervalWeek"
|
||||
]
|
||||
},
|
||||
"codersdk.IssueReconnectingPTYSignedTokenRequest": {
|
||||
"type": "object",
|
||||
|
@ -8794,7 +8797,12 @@
|
|||
"format": "date-time"
|
||||
},
|
||||
"interval": {
|
||||
"$ref": "#/definitions/codersdk.InsightsReportInterval"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.InsightsReportInterval"
|
||||
}
|
||||
],
|
||||
"example": "week"
|
||||
},
|
||||
"start_time": {
|
||||
"type": "string",
|
||||
|
|
|
@ -7518,7 +7518,7 @@ func (q *sqlQuerier) UpdateWorkspaceAgentStartupByID(ctx context.Context, arg Up
|
|||
}
|
||||
|
||||
const deleteOldWorkspaceAgentStats = `-- name: DeleteOldWorkspaceAgentStats :exec
|
||||
DELETE FROM workspace_agent_stats WHERE created_at < NOW() - INTERVAL '30 days'
|
||||
DELETE FROM workspace_agent_stats WHERE created_at < NOW() - INTERVAL '6 months'
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) DeleteOldWorkspaceAgentStats(ctx context.Context) error {
|
||||
|
|
|
@ -90,7 +90,7 @@ ORDER BY
|
|||
date ASC;
|
||||
|
||||
-- name: DeleteOldWorkspaceAgentStats :exec
|
||||
DELETE FROM workspace_agent_stats WHERE created_at < NOW() - INTERVAL '30 days';
|
||||
DELETE FROM workspace_agent_stats WHERE created_at < NOW() - INTERVAL '6 months';
|
||||
|
||||
-- name: GetDeploymentWorkspaceAgentStats :one
|
||||
WITH agent_stats AS (
|
||||
|
|
|
@ -184,7 +184,7 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
|
|||
if !ok {
|
||||
return
|
||||
}
|
||||
interval, ok := verifyInsightsInterval(ctx, rw, intervalString)
|
||||
interval, ok := parseInsightsInterval(ctx, rw, intervalString, startTime, endTime)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
@ -198,7 +198,7 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
|
|||
eg.SetLimit(4)
|
||||
|
||||
// The following insights data queries have a theoretical chance to be
|
||||
// inconsistent between eachother when looking at "today", however, the
|
||||
// inconsistent between each other when looking at "today", however, the
|
||||
// overhead from a transaction is not worth it.
|
||||
eg.Go(func() error {
|
||||
var err error
|
||||
|
@ -207,7 +207,7 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
|
|||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
TemplateIDs: templateIDs,
|
||||
IntervalDays: 1,
|
||||
IntervalDays: interval.Days(),
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get template daily insights: %w", err)
|
||||
|
@ -531,20 +531,39 @@ func parseInsightsStartAndEndTime(ctx context.Context, rw http.ResponseWriter, s
|
|||
return startTime, endTime, true
|
||||
}
|
||||
|
||||
func verifyInsightsInterval(ctx context.Context, rw http.ResponseWriter, intervalString string) (codersdk.InsightsReportInterval, bool) {
|
||||
func parseInsightsInterval(ctx context.Context, rw http.ResponseWriter, intervalString string, startTime, endTime time.Time) (codersdk.InsightsReportInterval, bool) {
|
||||
switch v := codersdk.InsightsReportInterval(intervalString); v {
|
||||
case codersdk.InsightsReportIntervalDay, "":
|
||||
return v, true
|
||||
case codersdk.InsightsReportIntervalWeek:
|
||||
if !lastReportIntervalHasAtLeastSixDays(startTime, endTime) {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Query parameter has invalid value.",
|
||||
Detail: "Last report interval should have at least 6 days.",
|
||||
})
|
||||
return "", false
|
||||
}
|
||||
return v, true
|
||||
default:
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Query parameter has invalid value.",
|
||||
Validations: []codersdk.ValidationError{
|
||||
{
|
||||
Field: "interval",
|
||||
Detail: fmt.Sprintf("must be one of %v", []codersdk.InsightsReportInterval{codersdk.InsightsReportIntervalDay}),
|
||||
Detail: fmt.Sprintf("must be one of %v", []codersdk.InsightsReportInterval{codersdk.InsightsReportIntervalDay, codersdk.InsightsReportIntervalWeek}),
|
||||
},
|
||||
},
|
||||
})
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func lastReportIntervalHasAtLeastSixDays(startTime, endTime time.Time) bool {
|
||||
lastReportIntervalDays := endTime.Sub(startTime) % (7 * 24 * time.Hour)
|
||||
if lastReportIntervalDays == 0 {
|
||||
return true // this is a perfectly full week!
|
||||
}
|
||||
// Ensure that the last interval has at least 6 days, or check the special case, forward DST change,
|
||||
// when the duration can be shorter than 6 days: 5 days 23 hours.
|
||||
return lastReportIntervalDays >= 6*24*time.Hour || startTime.AddDate(0, 0, 6).Equal(endTime)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import (
|
|||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
func Test_parseInsightsStartAndEndTime(t *testing.T) {
|
||||
|
@ -148,3 +150,156 @@ func Test_parseInsightsStartAndEndTime(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseInsightsInterval_week(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
layout := insightsTimeLayout
|
||||
sydneyLoc, err := time.LoadLocation("Australia/Sydney") // Random location
|
||||
require.NoError(t, err)
|
||||
|
||||
now := time.Now()
|
||||
y, m, d := now.Date()
|
||||
today := time.Date(y, m, d, 0, 0, 0, 0, sydneyLoc)
|
||||
|
||||
thisHour := time.Date(y, m, d, now.Hour(), 0, 0, 0, sydneyLoc)
|
||||
twoHoursAgo := thisHour.Add(-2 * time.Hour)
|
||||
thirteenDaysAgo := today.AddDate(0, 0, -13)
|
||||
|
||||
sixDaysAgo := today.AddDate(0, 0, -6)
|
||||
nineDaysAgo := today.AddDate(0, 0, -9)
|
||||
|
||||
type args struct {
|
||||
startTime string
|
||||
endTime string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantOk bool
|
||||
}{
|
||||
{
|
||||
name: "Two full weeks",
|
||||
args: args{
|
||||
startTime: "2023-08-10T00:00:00+02:00",
|
||||
endTime: "2023-08-24T00:00:00+02:00",
|
||||
},
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "Two weeks",
|
||||
args: args{
|
||||
startTime: thirteenDaysAgo.Format(layout),
|
||||
endTime: twoHoursAgo.Format(layout),
|
||||
},
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "One full week",
|
||||
args: args{
|
||||
startTime: "2023-09-06T00:00:00+02:00",
|
||||
endTime: "2023-09-13T00:00:00+02:00",
|
||||
},
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "6 days are acceptable",
|
||||
args: args{
|
||||
startTime: sixDaysAgo.Format(layout),
|
||||
endTime: thisHour.Format(layout),
|
||||
},
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "Shorter than a full week",
|
||||
args: args{
|
||||
startTime: "2023-09-08T00:00:00+02:00",
|
||||
endTime: "2023-09-13T00:00:00+02:00",
|
||||
},
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "9 days (7 + 2) are not acceptable",
|
||||
args: args{
|
||||
startTime: nineDaysAgo.Format(layout),
|
||||
endTime: thisHour.Format(layout),
|
||||
},
|
||||
wantOk: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
startTime, endTime, ok := parseInsightsStartAndEndTime(context.Background(), rw, tt.args.startTime, tt.args.endTime)
|
||||
if !ok {
|
||||
//nolint:bodyclose
|
||||
t.Log("Status: ", rw.Result().StatusCode)
|
||||
t.Log("Body: ", rw.Body.String())
|
||||
}
|
||||
require.True(t, ok, "start_time and end_time must be valid")
|
||||
|
||||
parsedInterval, gotOk := parseInsightsInterval(context.Background(), rw, "week", startTime, endTime)
|
||||
if !assert.Equal(t, tt.wantOk, gotOk) {
|
||||
//nolint:bodyclose
|
||||
t.Log("Status: ", rw.Result().StatusCode)
|
||||
t.Log("Body: ", rw.Body.String())
|
||||
}
|
||||
if tt.wantOk {
|
||||
assert.Equal(t, codersdk.InsightsReportIntervalWeek, parsedInterval)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastReportIntervalHasAtLeastSixDays(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
loc, err := time.LoadLocation("Europe/Warsaw")
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
startTime time.Time
|
||||
endTime time.Time
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "perfectly full week",
|
||||
startTime: time.Date(2023, time.September, 11, 12, 0, 0, 0, loc),
|
||||
endTime: time.Date(2023, time.September, 18, 12, 0, 0, 0, loc),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "exactly 6 days apart",
|
||||
startTime: time.Date(2023, time.September, 11, 12, 0, 0, 0, loc),
|
||||
endTime: time.Date(2023, time.September, 17, 12, 0, 0, 0, loc),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "less than 6 days apart",
|
||||
startTime: time.Date(2023, time.September, 11, 12, 0, 0, 0, time.UTC),
|
||||
endTime: time.Date(2023, time.September, 17, 11, 0, 0, 0, time.UTC),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "forward DST change, 5 days and 23 hours apart",
|
||||
startTime: time.Date(2023, time.March, 22, 12, 0, 0, 0, loc), // A day before DST starts
|
||||
endTime: time.Date(2023, time.March, 28, 12, 0, 0, 0, loc), // Exactly 6 "days" apart
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
result := lastReportIntervalHasAtLeastSixDays(tc.startTime, tc.endTime)
|
||||
if result != tc.expected {
|
||||
t.Errorf("Expected %v, but got %v for start time %v and end time %v", tc.expected, result, tc.startTime, tc.endTime)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -810,6 +810,18 @@ func TestTemplateInsights_Golden(t *testing.T) {
|
|||
sessionCountVSCode: 1,
|
||||
sessionCountSSH: 1,
|
||||
},
|
||||
{ // One hour of usage.
|
||||
startedAt: frozenWeekAgo.AddDate(0, 0, -12),
|
||||
endedAt: frozenWeekAgo.AddDate(0, 0, -12).Add(time.Hour),
|
||||
sessionCountSSH: 1,
|
||||
sessionCountReconnectingPTY: 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
|
||||
|
@ -922,6 +934,16 @@ func TestTemplateInsights_Golden(t *testing.T) {
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "weekly aggregated deployment wide",
|
||||
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
||||
return codersdk.TemplateInsightsRequest{
|
||||
StartTime: frozenWeekAgo.AddDate(0, 0, -3),
|
||||
EndTime: frozenWeekAgo.AddDate(0, 0, 4),
|
||||
Interval: codersdk.InsightsReportIntervalWeek,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "week all templates",
|
||||
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
||||
|
@ -933,6 +955,17 @@ func TestTemplateInsights_Golden(t *testing.T) {
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "weekly aggregated templates",
|
||||
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
||||
return codersdk.TemplateInsightsRequest{
|
||||
TemplateIDs: []uuid.UUID{templates[0].id, templates[1].id, templates[2].id},
|
||||
StartTime: frozenWeekAgo.AddDate(0, 0, -1),
|
||||
EndTime: frozenWeekAgo.AddDate(0, 0, 6),
|
||||
Interval: codersdk.InsightsReportIntervalWeek,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "week first template",
|
||||
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
||||
|
@ -944,6 +977,17 @@ func TestTemplateInsights_Golden(t *testing.T) {
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "weekly aggregated first template",
|
||||
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
||||
return codersdk.TemplateInsightsRequest{
|
||||
TemplateIDs: []uuid.UUID{templates[0].id},
|
||||
StartTime: frozenWeekAgo,
|
||||
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
|
||||
Interval: codersdk.InsightsReportIntervalWeek,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "week second template",
|
||||
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
||||
|
@ -955,6 +999,17 @@ func TestTemplateInsights_Golden(t *testing.T) {
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "three weeks second template",
|
||||
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
||||
return codersdk.TemplateInsightsRequest{
|
||||
TemplateIDs: []uuid.UUID{templates[1].id},
|
||||
StartTime: frozenWeekAgo.AddDate(0, 0, -14),
|
||||
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
|
||||
Interval: codersdk.InsightsReportIntervalWeek,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "week third template",
|
||||
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
|
||||
|
@ -1115,6 +1170,13 @@ func TestTemplateInsights_BadRequest(t *testing.T) {
|
|||
Interval: "invalid",
|
||||
})
|
||||
assert.Error(t, err, "want error for bad interval")
|
||||
|
||||
_, err = client.TemplateInsights(ctx, codersdk.TemplateInsightsRequest{
|
||||
StartTime: today.AddDate(0, 0, -5),
|
||||
EndTime: today,
|
||||
Interval: codersdk.InsightsReportIntervalWeek,
|
||||
})
|
||||
assert.Error(t, err, "last report interval must have at least 6 days")
|
||||
}
|
||||
|
||||
func TestTemplateInsights_RBAC(t *testing.T) {
|
||||
|
|
90
coderd/testdata/insights/multiple_users_and_workspaces_three_weeks_second_template.json.golden
vendored
Normal file
90
coderd/testdata/insights/multiple_users_and_workspaces_three_weeks_second_template.json.golden
vendored
Normal file
|
@ -0,0 +1,90 @@
|
|||
{
|
||||
"report": {
|
||||
"start_time": "2023-08-01T00:00:00Z",
|
||||
"end_time": "2023-08-22T00:00:00Z",
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000002"
|
||||
],
|
||||
"active_users": 1,
|
||||
"apps_usage": [
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000002"
|
||||
],
|
||||
"type": "builtin",
|
||||
"display_name": "Visual Studio Code",
|
||||
"slug": "vscode",
|
||||
"icon": "/icon/code.svg",
|
||||
"seconds": 3600
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000002"
|
||||
],
|
||||
"type": "builtin",
|
||||
"display_name": "JetBrains",
|
||||
"slug": "jetbrains",
|
||||
"icon": "/icon/intellij.svg",
|
||||
"seconds": 0
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000002"
|
||||
],
|
||||
"type": "builtin",
|
||||
"display_name": "Web Terminal",
|
||||
"slug": "reconnecting-pty",
|
||||
"icon": "/icon/terminal.svg",
|
||||
"seconds": 7200
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000002"
|
||||
],
|
||||
"type": "builtin",
|
||||
"display_name": "SSH",
|
||||
"slug": "ssh",
|
||||
"icon": "/icon/terminal.svg",
|
||||
"seconds": 10800
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000002"
|
||||
],
|
||||
"type": "app",
|
||||
"display_name": "app1",
|
||||
"slug": "app1",
|
||||
"icon": "/icon1.png",
|
||||
"seconds": 21600
|
||||
}
|
||||
],
|
||||
"parameters_usage": []
|
||||
},
|
||||
"interval_reports": [
|
||||
{
|
||||
"start_time": "2023-08-01T00:00:00Z",
|
||||
"end_time": "2023-08-08T00:00:00Z",
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000002"
|
||||
],
|
||||
"interval": "week",
|
||||
"active_users": 1
|
||||
},
|
||||
{
|
||||
"start_time": "2023-08-08T00:00:00Z",
|
||||
"end_time": "2023-08-15T00:00:00Z",
|
||||
"template_ids": [],
|
||||
"interval": "week",
|
||||
"active_users": 0
|
||||
},
|
||||
{
|
||||
"start_time": "2023-08-15T00:00:00Z",
|
||||
"end_time": "2023-08-22T00:00:00Z",
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000002"
|
||||
],
|
||||
"interval": "week",
|
||||
"active_users": 1
|
||||
}
|
||||
]
|
||||
}
|
107
coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden
vendored
Normal file
107
coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_deployment_wide.json.golden
vendored
Normal file
|
@ -0,0 +1,107 @@
|
|||
{
|
||||
"report": {
|
||||
"start_time": "2023-08-12T00:00:00Z",
|
||||
"end_time": "2023-08-19T00:00:00Z",
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
"00000000-0000-0000-0000-000000000002",
|
||||
"00000000-0000-0000-0000-000000000003"
|
||||
],
|
||||
"active_users": 3,
|
||||
"apps_usage": [
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
"00000000-0000-0000-0000-000000000002",
|
||||
"00000000-0000-0000-0000-000000000003"
|
||||
],
|
||||
"type": "builtin",
|
||||
"display_name": "Visual Studio Code",
|
||||
"slug": "vscode",
|
||||
"icon": "/icon/code.svg",
|
||||
"seconds": 7200
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
"00000000-0000-0000-0000-000000000002",
|
||||
"00000000-0000-0000-0000-000000000003"
|
||||
],
|
||||
"type": "builtin",
|
||||
"display_name": "JetBrains",
|
||||
"slug": "jetbrains",
|
||||
"icon": "/icon/intellij.svg",
|
||||
"seconds": 120
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
"00000000-0000-0000-0000-000000000002",
|
||||
"00000000-0000-0000-0000-000000000003"
|
||||
],
|
||||
"type": "builtin",
|
||||
"display_name": "Web Terminal",
|
||||
"slug": "reconnecting-pty",
|
||||
"icon": "/icon/terminal.svg",
|
||||
"seconds": 3600
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
"00000000-0000-0000-0000-000000000002",
|
||||
"00000000-0000-0000-0000-000000000003"
|
||||
],
|
||||
"type": "builtin",
|
||||
"display_name": "SSH",
|
||||
"slug": "ssh",
|
||||
"icon": "/icon/terminal.svg",
|
||||
"seconds": 15120
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
"00000000-0000-0000-0000-000000000002"
|
||||
],
|
||||
"type": "app",
|
||||
"display_name": "app1",
|
||||
"slug": "app1",
|
||||
"icon": "/icon1.png",
|
||||
"seconds": 25380
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001"
|
||||
],
|
||||
"type": "app",
|
||||
"display_name": "app3",
|
||||
"slug": "app3",
|
||||
"icon": "/icon2.png",
|
||||
"seconds": 3600
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000003"
|
||||
],
|
||||
"type": "app",
|
||||
"display_name": "otherapp1",
|
||||
"slug": "otherapp1",
|
||||
"icon": "/icon1.png",
|
||||
"seconds": 300
|
||||
}
|
||||
],
|
||||
"parameters_usage": []
|
||||
},
|
||||
"interval_reports": [
|
||||
{
|
||||
"start_time": "2023-08-12T00:00:00Z",
|
||||
"end_time": "2023-08-19T00:00:00Z",
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
"00000000-0000-0000-0000-000000000002",
|
||||
"00000000-0000-0000-0000-000000000003"
|
||||
],
|
||||
"interval": "week",
|
||||
"active_users": 3
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"report": {
|
||||
"start_time": "2023-08-15T00:00:00Z",
|
||||
"end_time": "2023-08-22T00:00:00Z",
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001"
|
||||
],
|
||||
"active_users": 2,
|
||||
"apps_usage": [
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001"
|
||||
],
|
||||
"type": "builtin",
|
||||
"display_name": "Visual Studio Code",
|
||||
"slug": "vscode",
|
||||
"icon": "/icon/code.svg",
|
||||
"seconds": 3600
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001"
|
||||
],
|
||||
"type": "builtin",
|
||||
"display_name": "JetBrains",
|
||||
"slug": "jetbrains",
|
||||
"icon": "/icon/intellij.svg",
|
||||
"seconds": 120
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001"
|
||||
],
|
||||
"type": "builtin",
|
||||
"display_name": "Web Terminal",
|
||||
"slug": "reconnecting-pty",
|
||||
"icon": "/icon/terminal.svg",
|
||||
"seconds": 0
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001"
|
||||
],
|
||||
"type": "builtin",
|
||||
"display_name": "SSH",
|
||||
"slug": "ssh",
|
||||
"icon": "/icon/terminal.svg",
|
||||
"seconds": 7920
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001"
|
||||
],
|
||||
"type": "app",
|
||||
"display_name": "app1",
|
||||
"slug": "app1",
|
||||
"icon": "/icon1.png",
|
||||
"seconds": 3780
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001"
|
||||
],
|
||||
"type": "app",
|
||||
"display_name": "app3",
|
||||
"slug": "app3",
|
||||
"icon": "/icon2.png",
|
||||
"seconds": 720
|
||||
}
|
||||
],
|
||||
"parameters_usage": []
|
||||
},
|
||||
"interval_reports": [
|
||||
{
|
||||
"start_time": "2023-08-15T00:00:00Z",
|
||||
"end_time": "2023-08-22T00:00:00Z",
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001"
|
||||
],
|
||||
"interval": "week",
|
||||
"active_users": 2
|
||||
}
|
||||
]
|
||||
}
|
107
coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden
vendored
Normal file
107
coderd/testdata/insights/multiple_users_and_workspaces_weekly_aggregated_templates.json.golden
vendored
Normal file
|
@ -0,0 +1,107 @@
|
|||
{
|
||||
"report": {
|
||||
"start_time": "2023-08-14T00:00:00Z",
|
||||
"end_time": "2023-08-21T00:00:00Z",
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
"00000000-0000-0000-0000-000000000002",
|
||||
"00000000-0000-0000-0000-000000000003"
|
||||
],
|
||||
"active_users": 3,
|
||||
"apps_usage": [
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
"00000000-0000-0000-0000-000000000002",
|
||||
"00000000-0000-0000-0000-000000000003"
|
||||
],
|
||||
"type": "builtin",
|
||||
"display_name": "Visual Studio Code",
|
||||
"slug": "vscode",
|
||||
"icon": "/icon/code.svg",
|
||||
"seconds": 7200
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
"00000000-0000-0000-0000-000000000002",
|
||||
"00000000-0000-0000-0000-000000000003"
|
||||
],
|
||||
"type": "builtin",
|
||||
"display_name": "JetBrains",
|
||||
"slug": "jetbrains",
|
||||
"icon": "/icon/intellij.svg",
|
||||
"seconds": 120
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
"00000000-0000-0000-0000-000000000002",
|
||||
"00000000-0000-0000-0000-000000000003"
|
||||
],
|
||||
"type": "builtin",
|
||||
"display_name": "Web Terminal",
|
||||
"slug": "reconnecting-pty",
|
||||
"icon": "/icon/terminal.svg",
|
||||
"seconds": 3600
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
"00000000-0000-0000-0000-000000000002",
|
||||
"00000000-0000-0000-0000-000000000003"
|
||||
],
|
||||
"type": "builtin",
|
||||
"display_name": "SSH",
|
||||
"slug": "ssh",
|
||||
"icon": "/icon/terminal.svg",
|
||||
"seconds": 15120
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
"00000000-0000-0000-0000-000000000002"
|
||||
],
|
||||
"type": "app",
|
||||
"display_name": "app1",
|
||||
"slug": "app1",
|
||||
"icon": "/icon1.png",
|
||||
"seconds": 25380
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001"
|
||||
],
|
||||
"type": "app",
|
||||
"display_name": "app3",
|
||||
"slug": "app3",
|
||||
"icon": "/icon2.png",
|
||||
"seconds": 3600
|
||||
},
|
||||
{
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000003"
|
||||
],
|
||||
"type": "app",
|
||||
"display_name": "otherapp1",
|
||||
"slug": "otherapp1",
|
||||
"icon": "/icon1.png",
|
||||
"seconds": 300
|
||||
}
|
||||
],
|
||||
"parameters_usage": []
|
||||
},
|
||||
"interval_reports": [
|
||||
{
|
||||
"start_time": "2023-08-14T00:00:00Z",
|
||||
"end_time": "2023-08-21T00:00:00Z",
|
||||
"template_ids": [
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
"00000000-0000-0000-0000-000000000002",
|
||||
"00000000-0000-0000-0000-000000000003"
|
||||
],
|
||||
"interval": "week",
|
||||
"active_users": 3
|
||||
}
|
||||
]
|
||||
}
|
|
@ -20,9 +20,22 @@ const insightsTimeLayout = time.RFC3339
|
|||
// smaller insights report within a time range.
|
||||
type InsightsReportInterval string
|
||||
|
||||
// Days returns the duration of the interval in days.
|
||||
func (interval InsightsReportInterval) Days() int32 {
|
||||
switch interval {
|
||||
case InsightsReportIntervalDay:
|
||||
return 1
|
||||
case InsightsReportIntervalWeek:
|
||||
return 7
|
||||
default:
|
||||
panic("developer error: unsupported report interval")
|
||||
}
|
||||
}
|
||||
|
||||
// InsightsReportInterval enums.
|
||||
const (
|
||||
InsightsReportIntervalDay InsightsReportInterval = "day"
|
||||
InsightsReportIntervalDay InsightsReportInterval = "day"
|
||||
InsightsReportIntervalWeek InsightsReportInterval = "week"
|
||||
)
|
||||
|
||||
// UserLatencyInsightsResponse is the response from the user latency insights
|
||||
|
@ -109,7 +122,7 @@ type TemplateInsightsIntervalReport struct {
|
|||
StartTime time.Time `json:"start_time" format:"date-time"`
|
||||
EndTime time.Time `json:"end_time" format:"date-time"`
|
||||
TemplateIDs []uuid.UUID `json:"template_ids" format:"uuid"`
|
||||
Interval InsightsReportInterval `json:"interval"`
|
||||
Interval InsightsReportInterval `json:"interval" example:"week"`
|
||||
ActiveUsers int64 `json:"active_users" example:"14"`
|
||||
}
|
||||
|
||||
|
@ -155,7 +168,7 @@ type TemplateInsightsRequest struct {
|
|||
StartTime time.Time `json:"start_time" format:"date-time"`
|
||||
EndTime time.Time `json:"end_time" format:"date-time"`
|
||||
TemplateIDs []uuid.UUID `json:"template_ids" format:"uuid"`
|
||||
Interval InsightsReportInterval `json:"interval"`
|
||||
Interval InsightsReportInterval `json:"interval" example:"day"`
|
||||
}
|
||||
|
||||
func (c *Client) TemplateInsights(ctx context.Context, req TemplateInsightsRequest) (TemplateInsightsResponse, error) {
|
||||
|
|
|
@ -60,7 +60,7 @@ curl -X GET http://coder-server:8080/api/v2/insights/templates \
|
|||
{
|
||||
"active_users": 14,
|
||||
"end_time": "2019-08-24T14:15:22Z",
|
||||
"interval": "day",
|
||||
"interval": "week",
|
||||
"start_time": "2019-08-24T14:15:22Z",
|
||||
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"]
|
||||
}
|
||||
|
|
|
@ -3085,9 +3085,10 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
|
||||
#### Enumerated Values
|
||||
|
||||
| Value |
|
||||
| ----- |
|
||||
| `day` |
|
||||
| Value |
|
||||
| ------ |
|
||||
| `day` |
|
||||
| `week` |
|
||||
|
||||
## codersdk.IssueReconnectingPTYSignedTokenRequest
|
||||
|
||||
|
@ -4421,7 +4422,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
{
|
||||
"active_users": 14,
|
||||
"end_time": "2019-08-24T14:15:22Z",
|
||||
"interval": "day",
|
||||
"interval": "week",
|
||||
"start_time": "2019-08-24T14:15:22Z",
|
||||
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"]
|
||||
}
|
||||
|
@ -4500,7 +4501,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
{
|
||||
"active_users": 14,
|
||||
"end_time": "2019-08-24T14:15:22Z",
|
||||
"interval": "day",
|
||||
"interval": "week",
|
||||
"start_time": "2019-08-24T14:15:22Z",
|
||||
"template_ids": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"]
|
||||
}
|
||||
|
|
|
@ -1654,8 +1654,11 @@ export type GroupSource = "oidc" | "user";
|
|||
export const GroupSources: GroupSource[] = ["oidc", "user"];
|
||||
|
||||
// From codersdk/insights.go
|
||||
export type InsightsReportInterval = "day";
|
||||
export const InsightsReportIntervals: InsightsReportInterval[] = ["day"];
|
||||
export type InsightsReportInterval = "day" | "week";
|
||||
export const InsightsReportIntervals: InsightsReportInterval[] = [
|
||||
"day",
|
||||
"week",
|
||||
];
|
||||
|
||||
// From codersdk/provisionerdaemons.go
|
||||
export type JobErrorCode = "REQUIRED_TEMPLATE_VARIABLES";
|
||||
|
|
Loading…
Reference in New Issue