Skip to content

Commit f62f45a

Browse files
authored
feat!: add sections parameter to template insights (#10010)
1 parent cb60409 commit f62f45a

9 files changed

+227
-26
lines changed

coderd/insights.go

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
257257
endTimeString = p.String(vals, "", "end_time")
258258
intervalString = p.String(vals, "", "interval")
259259
templateIDs = p.UUIDs(vals, []uuid.UUID{}, "template_ids")
260+
sectionStrings = p.Strings(vals, templateInsightsSectionAsStrings(codersdk.TemplateInsightsSectionIntervalReports, codersdk.TemplateInsightsSectionReport), "sections")
260261
)
261262
p.ErrorExcessParams(vals)
262263
if len(p.Errors) > 0 {
@@ -275,6 +276,10 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
275276
if !ok {
276277
return
277278
}
279+
sections, ok := parseTemplateInsightsSections(ctx, rw, sectionStrings)
280+
if !ok {
281+
return
282+
}
278283

279284
var usage database.GetTemplateInsightsRow
280285
var appUsage []database.GetTemplateAppInsightsRow
@@ -289,7 +294,7 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
289294
// overhead from a transaction is not worth it.
290295
eg.Go(func() error {
291296
var err error
292-
if interval != "" {
297+
if interval != "" && slices.Contains(sections, codersdk.TemplateInsightsSectionIntervalReports) {
293298
dailyUsage, err = api.Database.GetTemplateInsightsByInterval(egCtx, database.GetTemplateInsightsByIntervalParams{
294299
StartTime: startTime,
295300
EndTime: endTime,
@@ -303,6 +308,10 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
303308
return nil
304309
})
305310
eg.Go(func() error {
311+
if !slices.Contains(sections, codersdk.TemplateInsightsSectionReport) {
312+
return nil
313+
}
314+
306315
var err error
307316
usage, err = api.Database.GetTemplateInsights(egCtx, database.GetTemplateInsightsParams{
308317
StartTime: startTime,
@@ -315,6 +324,10 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
315324
return nil
316325
})
317326
eg.Go(func() error {
327+
if !slices.Contains(sections, codersdk.TemplateInsightsSectionReport) {
328+
return nil
329+
}
330+
318331
var err error
319332
appUsage, err = api.Database.GetTemplateAppInsights(egCtx, database.GetTemplateAppInsightsParams{
320333
StartTime: startTime,
@@ -330,6 +343,10 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
330343
// Template parameter insights have no risk of inconsistency with the other
331344
// insights.
332345
eg.Go(func() error {
346+
if !slices.Contains(sections, codersdk.TemplateInsightsSectionReport) {
347+
return nil
348+
}
349+
333350
var err error
334351
parameterRows, err = api.Database.GetTemplateParameterInsights(ctx, database.GetTemplateParameterInsightsParams{
335352
StartTime: startTime,
@@ -365,16 +382,20 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
365382
}
366383

367384
resp := codersdk.TemplateInsightsResponse{
368-
Report: codersdk.TemplateInsightsReport{
385+
IntervalReports: []codersdk.TemplateInsightsIntervalReport{},
386+
}
387+
388+
if slices.Contains(sections, codersdk.TemplateInsightsSectionReport) {
389+
resp.Report = &codersdk.TemplateInsightsReport{
369390
StartTime: startTime,
370391
EndTime: endTime,
371392
TemplateIDs: convertTemplateInsightsTemplateIDs(usage, appUsage),
372393
ActiveUsers: convertTemplateInsightsActiveUsers(usage, appUsage),
373394
AppsUsage: convertTemplateInsightsApps(usage, appUsage),
374395
ParametersUsage: parametersUsage,
375-
},
376-
IntervalReports: []codersdk.TemplateInsightsIntervalReport{},
396+
}
377397
}
398+
378399
for _, row := range dailyUsage {
379400
resp.IntervalReports = append(resp.IntervalReports, codersdk.TemplateInsightsIntervalReport{
380401
// NOTE(mafredri): This might not be accurate over DST since the
@@ -654,3 +675,33 @@ func lastReportIntervalHasAtLeastSixDays(startTime, endTime time.Time) bool {
654675
// when the duration can be shorter than 6 days: 5 days 23 hours.
655676
return lastReportIntervalDays >= 6*24*time.Hour || startTime.AddDate(0, 0, 6).Equal(endTime)
656677
}
678+
679+
func templateInsightsSectionAsStrings(sections ...codersdk.TemplateInsightsSection) []string {
680+
t := make([]string, len(sections))
681+
for i, s := range sections {
682+
t[i] = string(s)
683+
}
684+
return t
685+
}
686+
687+
func parseTemplateInsightsSections(ctx context.Context, rw http.ResponseWriter, sections []string) ([]codersdk.TemplateInsightsSection, bool) {
688+
t := make([]codersdk.TemplateInsightsSection, len(sections))
689+
for i, s := range sections {
690+
switch v := codersdk.TemplateInsightsSection(s); v {
691+
case codersdk.TemplateInsightsSectionIntervalReports, codersdk.TemplateInsightsSectionReport:
692+
t[i] = v
693+
default:
694+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
695+
Message: "Query parameter has invalid value.",
696+
Validations: []codersdk.ValidationError{
697+
{
698+
Field: "sections",
699+
Detail: fmt.Sprintf("must be one of %v", []codersdk.TemplateInsightsSection{codersdk.TemplateInsightsSectionIntervalReports, codersdk.TemplateInsightsSectionReport}),
700+
},
701+
},
702+
})
703+
return nil, false
704+
}
705+
}
706+
return t, true
707+
}

coderd/insights_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,6 +1135,30 @@ func TestTemplateInsights_Golden(t *testing.T) {
11351135
}
11361136
},
11371137
},
1138+
{
1139+
name: "three weeks second template only report",
1140+
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
1141+
return codersdk.TemplateInsightsRequest{
1142+
TemplateIDs: []uuid.UUID{templates[1].id},
1143+
StartTime: frozenWeekAgo.AddDate(0, 0, -14),
1144+
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
1145+
Interval: codersdk.InsightsReportIntervalWeek,
1146+
Sections: []codersdk.TemplateInsightsSection{codersdk.TemplateInsightsSectionReport},
1147+
}
1148+
},
1149+
},
1150+
{
1151+
name: "three weeks second template only interval reports",
1152+
makeRequest: func(templates []*testTemplate) codersdk.TemplateInsightsRequest {
1153+
return codersdk.TemplateInsightsRequest{
1154+
TemplateIDs: []uuid.UUID{templates[1].id},
1155+
StartTime: frozenWeekAgo.AddDate(0, 0, -14),
1156+
EndTime: frozenWeekAgo.AddDate(0, 0, 7),
1157+
Interval: codersdk.InsightsReportIntervalWeek,
1158+
Sections: []codersdk.TemplateInsightsSection{codersdk.TemplateInsightsSectionIntervalReports},
1159+
}
1160+
},
1161+
},
11381162
},
11391163
},
11401164
{
@@ -2049,6 +2073,14 @@ func TestTemplateInsights_BadRequest(t *testing.T) {
20492073
Interval: codersdk.InsightsReportIntervalWeek,
20502074
})
20512075
assert.Error(t, err, "last report interval must have at least 6 days")
2076+
2077+
_, err = client.TemplateInsights(ctx, codersdk.TemplateInsightsRequest{
2078+
StartTime: today.AddDate(0, 0, -1),
2079+
EndTime: today,
2080+
Interval: codersdk.InsightsReportIntervalWeek,
2081+
Sections: []codersdk.TemplateInsightsSection{"invalid"},
2082+
})
2083+
assert.Error(t, err, "want error for bad section")
20522084
}
20532085

20542086
func TestTemplateInsights_RBAC(t *testing.T) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"interval_reports": [
3+
{
4+
"start_time": "2023-08-01T00:00:00Z",
5+
"end_time": "2023-08-08T00:00:00Z",
6+
"template_ids": [
7+
"00000000-0000-0000-0000-000000000002"
8+
],
9+
"interval": "week",
10+
"active_users": 1
11+
},
12+
{
13+
"start_time": "2023-08-08T00:00:00Z",
14+
"end_time": "2023-08-15T00:00:00Z",
15+
"template_ids": [],
16+
"interval": "week",
17+
"active_users": 0
18+
},
19+
{
20+
"start_time": "2023-08-15T00:00:00Z",
21+
"end_time": "2023-08-22T00:00:00Z",
22+
"template_ids": [
23+
"00000000-0000-0000-0000-000000000002"
24+
],
25+
"interval": "week",
26+
"active_users": 1
27+
}
28+
]
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{
2+
"report": {
3+
"start_time": "2023-08-01T00:00:00Z",
4+
"end_time": "2023-08-22T00:00:00Z",
5+
"template_ids": [
6+
"00000000-0000-0000-0000-000000000002"
7+
],
8+
"active_users": 1,
9+
"apps_usage": [
10+
{
11+
"template_ids": [
12+
"00000000-0000-0000-0000-000000000002"
13+
],
14+
"type": "builtin",
15+
"display_name": "Visual Studio Code",
16+
"slug": "vscode",
17+
"icon": "/icon/code.svg",
18+
"seconds": 3600
19+
},
20+
{
21+
"template_ids": [
22+
"00000000-0000-0000-0000-000000000002"
23+
],
24+
"type": "builtin",
25+
"display_name": "JetBrains",
26+
"slug": "jetbrains",
27+
"icon": "/icon/intellij.svg",
28+
"seconds": 0
29+
},
30+
{
31+
"template_ids": [
32+
"00000000-0000-0000-0000-000000000002"
33+
],
34+
"type": "builtin",
35+
"display_name": "Web Terminal",
36+
"slug": "reconnecting-pty",
37+
"icon": "/icon/terminal.svg",
38+
"seconds": 7200
39+
},
40+
{
41+
"template_ids": [
42+
"00000000-0000-0000-0000-000000000002"
43+
],
44+
"type": "builtin",
45+
"display_name": "SSH",
46+
"slug": "ssh",
47+
"icon": "/icon/terminal.svg",
48+
"seconds": 10800
49+
},
50+
{
51+
"template_ids": [
52+
"00000000-0000-0000-0000-000000000002"
53+
],
54+
"type": "app",
55+
"display_name": "app1",
56+
"slug": "app1",
57+
"icon": "/icon1.png",
58+
"seconds": 21600
59+
}
60+
],
61+
"parameters_usage": []
62+
}
63+
}

coderd/testdata/insights/template/parameters_two_days_ago,_no_data.json.golden

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,5 @@
3939
}
4040
],
4141
"parameters_usage": []
42-
},
43-
"interval_reports": []
42+
}
4443
}

coderd/testdata/insights/template/parameters_yesterday_and_today_deployment_wide.json.golden

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,5 @@
160160
]
161161
}
162162
]
163-
},
164-
"interval_reports": []
163+
}
165164
}

codersdk/insights.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ const (
3838
InsightsReportIntervalWeek InsightsReportInterval = "week"
3939
)
4040

41+
// TemplateInsightsSection defines the section to be included in the template insights response.
42+
type TemplateInsightsSection string
43+
44+
// TemplateInsightsSection enums.
45+
const (
46+
TemplateInsightsSectionIntervalReports TemplateInsightsSection = "interval_reports"
47+
TemplateInsightsSectionReport TemplateInsightsSection = "report"
48+
)
49+
4150
// UserLatencyInsightsResponse is the response from the user latency insights
4251
// endpoint.
4352
type UserLatencyInsightsResponse struct {
@@ -158,8 +167,8 @@ func (c *Client) UserActivityInsights(ctx context.Context, req UserActivityInsig
158167

159168
// TemplateInsightsResponse is the response from the template insights endpoint.
160169
type TemplateInsightsResponse struct {
161-
Report TemplateInsightsReport `json:"report"`
162-
IntervalReports []TemplateInsightsIntervalReport `json:"interval_reports"`
170+
Report *TemplateInsightsReport `json:"report,omitempty"`
171+
IntervalReports []TemplateInsightsIntervalReport `json:"interval_reports,omitempty"`
163172
}
164173

165174
// TemplateInsightsReport is the report from the template insights endpoint.
@@ -221,10 +230,11 @@ type TemplateParameterValue struct {
221230
}
222231

223232
type TemplateInsightsRequest struct {
224-
StartTime time.Time `json:"start_time" format:"date-time"`
225-
EndTime time.Time `json:"end_time" format:"date-time"`
226-
TemplateIDs []uuid.UUID `json:"template_ids" format:"uuid"`
227-
Interval InsightsReportInterval `json:"interval" example:"day"`
233+
StartTime time.Time `json:"start_time" format:"date-time"`
234+
EndTime time.Time `json:"end_time" format:"date-time"`
235+
TemplateIDs []uuid.UUID `json:"template_ids" format:"uuid"`
236+
Interval InsightsReportInterval `json:"interval" example:"day"`
237+
Sections []TemplateInsightsSection `json:"sections" example:"report"`
228238
}
229239

230240
func (c *Client) TemplateInsights(ctx context.Context, req TemplateInsightsRequest) (TemplateInsightsResponse, error) {
@@ -241,6 +251,13 @@ func (c *Client) TemplateInsights(ctx context.Context, req TemplateInsightsReque
241251
if req.Interval != "" {
242252
qp.Add("interval", string(req.Interval))
243253
}
254+
if len(req.Sections) > 0 {
255+
var sections []string
256+
for _, sec := range req.Sections {
257+
sections = append(sections, string(sec))
258+
}
259+
qp.Add("sections", strings.Join(sections, ","))
260+
}
244261

245262
reqURL := fmt.Sprintf("/api/v2/insights/templates?%s", qp.Encode())
246263
resp, err := c.Request(ctx, http.MethodGet, reqURL, nil)

site/src/api/typesGenerated.ts

Lines changed: 10 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)