diff --git a/coderd/insights.go b/coderd/insights.go
index a0113b15c1..714835db43 100644
--- a/coderd/insights.go
+++ b/coderd/insights.go
@@ -257,6 +257,7 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
endTimeString = p.String(vals, "", "end_time")
intervalString = p.String(vals, "", "interval")
templateIDs = p.UUIDs(vals, []uuid.UUID{}, "template_ids")
+ sectionStrings = p.Strings(vals, templateInsightsSectionAsStrings(codersdk.TemplateInsightsSectionIntervalReports, codersdk.TemplateInsightsSectionReport), "sections")
)
p.ErrorExcessParams(vals)
if len(p.Errors) > 0 {
@@ -275,6 +276,10 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
if !ok {
return
}
+ sections, ok := parseTemplateInsightsSections(ctx, rw, sectionStrings)
+ if !ok {
+ return
+ }
var usage database.GetTemplateInsightsRow
var appUsage []database.GetTemplateAppInsightsRow
@@ -289,7 +294,7 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
// overhead from a transaction is not worth it.
eg.Go(func() error {
var err error
- if interval != "" {
+ if interval != "" && slices.Contains(sections, codersdk.TemplateInsightsSectionIntervalReports) {
dailyUsage, err = api.Database.GetTemplateInsightsByInterval(egCtx, database.GetTemplateInsightsByIntervalParams{
StartTime: startTime,
EndTime: endTime,
@@ -303,6 +308,10 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
return nil
})
eg.Go(func() error {
+ if !slices.Contains(sections, codersdk.TemplateInsightsSectionReport) {
+ return nil
+ }
+
var err error
usage, err = api.Database.GetTemplateInsights(egCtx, database.GetTemplateInsightsParams{
StartTime: startTime,
@@ -315,6 +324,10 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
return nil
})
eg.Go(func() error {
+ if !slices.Contains(sections, codersdk.TemplateInsightsSectionReport) {
+ return nil
+ }
+
var err error
appUsage, err = api.Database.GetTemplateAppInsights(egCtx, database.GetTemplateAppInsightsParams{
StartTime: startTime,
@@ -330,6 +343,10 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
// Template parameter insights have no risk of inconsistency with the other
// insights.
eg.Go(func() error {
+ if !slices.Contains(sections, codersdk.TemplateInsightsSectionReport) {
+ return nil
+ }
+
var err error
parameterRows, err = api.Database.GetTemplateParameterInsights(ctx, database.GetTemplateParameterInsightsParams{
StartTime: startTime,
@@ -365,16 +382,20 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
}
resp := codersdk.TemplateInsightsResponse{
- Report: codersdk.TemplateInsightsReport{
+ IntervalReports: []codersdk.TemplateInsightsIntervalReport{},
+ }
+
+ if slices.Contains(sections, codersdk.TemplateInsightsSectionReport) {
+ resp.Report = &codersdk.TemplateInsightsReport{
StartTime: startTime,
EndTime: endTime,
TemplateIDs: convertTemplateInsightsTemplateIDs(usage, appUsage),
ActiveUsers: convertTemplateInsightsActiveUsers(usage, appUsage),
AppsUsage: convertTemplateInsightsApps(usage, appUsage),
ParametersUsage: parametersUsage,
- },
- IntervalReports: []codersdk.TemplateInsightsIntervalReport{},
+ }
}
+
for _, row := range dailyUsage {
resp.IntervalReports = append(resp.IntervalReports, codersdk.TemplateInsightsIntervalReport{
// NOTE(mafredri): This might not be accurate over DST since the
@@ -654,3 +675,33 @@ func lastReportIntervalHasAtLeastSixDays(startTime, endTime time.Time) bool {
// 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)
}
+
+func templateInsightsSectionAsStrings(sections ...codersdk.TemplateInsightsSection) []string {
+ t := make([]string, len(sections))
+ for i, s := range sections {
+ t[i] = string(s)
+ }
+ return t
+}
+
+func parseTemplateInsightsSections(ctx context.Context, rw http.ResponseWriter, sections []string) ([]codersdk.TemplateInsightsSection, bool) {
+ t := make([]codersdk.TemplateInsightsSection, len(sections))
+ for i, s := range sections {
+ switch v := codersdk.TemplateInsightsSection(s); v {
+ case codersdk.TemplateInsightsSectionIntervalReports, codersdk.TemplateInsightsSectionReport:
+ t[i] = v
+ default:
+ httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
+ Message: "Query parameter has invalid value.",
+ Validations: []codersdk.ValidationError{
+ {
+ Field: "sections",
+ Detail: fmt.Sprintf("must be one of %v", []codersdk.TemplateInsightsSection{codersdk.TemplateInsightsSectionIntervalReports, codersdk.TemplateInsightsSectionReport}),
+ },
+ },
+ })
+ return nil, false
+ }
+ }
+ return t, true
+}
diff --git a/coderd/insights_test.go b/coderd/insights_test.go
index 49b7af0841..6f1e365a9a 100644
--- a/coderd/insights_test.go
+++ b/coderd/insights_test.go
@@ -1135,6 +1135,30 @@ func TestTemplateInsights_Golden(t *testing.T) {
}
},
},
+ {
+ name: "three weeks second template only report",
+ 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,
+ Sections: []codersdk.TemplateInsightsSection{codersdk.TemplateInsightsSectionReport},
+ }
+ },
+ },
+ {
+ name: "three weeks second template only interval reports",
+ 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,
+ Sections: []codersdk.TemplateInsightsSection{codersdk.TemplateInsightsSectionIntervalReports},
+ }
+ },
+ },
},
},
{
@@ -2049,6 +2073,14 @@ func TestTemplateInsights_BadRequest(t *testing.T) {
Interval: codersdk.InsightsReportIntervalWeek,
})
assert.Error(t, err, "last report interval must have at least 6 days")
+
+ _, err = client.TemplateInsights(ctx, codersdk.TemplateInsightsRequest{
+ StartTime: today.AddDate(0, 0, -1),
+ EndTime: today,
+ Interval: codersdk.InsightsReportIntervalWeek,
+ Sections: []codersdk.TemplateInsightsSection{"invalid"},
+ })
+ assert.Error(t, err, "want error for bad section")
}
func TestTemplateInsights_RBAC(t *testing.T) {
diff --git a/coderd/testdata/insights/template/multiple_users_and_workspaces_three_weeks_second_template_only_interval_reports.json.golden b/coderd/testdata/insights/template/multiple_users_and_workspaces_three_weeks_second_template_only_interval_reports.json.golden
new file mode 100644
index 0000000000..34ffaeccf6
--- /dev/null
+++ b/coderd/testdata/insights/template/multiple_users_and_workspaces_three_weeks_second_template_only_interval_reports.json.golden
@@ -0,0 +1,29 @@
+{
+ "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
+ }
+ ]
+}
diff --git a/coderd/testdata/insights/template/multiple_users_and_workspaces_three_weeks_second_template_only_report.json.golden b/coderd/testdata/insights/template/multiple_users_and_workspaces_three_weeks_second_template_only_report.json.golden
new file mode 100644
index 0000000000..e3a1a2cd39
--- /dev/null
+++ b/coderd/testdata/insights/template/multiple_users_and_workspaces_three_weeks_second_template_only_report.json.golden
@@ -0,0 +1,63 @@
+{
+ "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": []
+ }
+}
diff --git a/coderd/testdata/insights/template/parameters_two_days_ago,_no_data.json.golden b/coderd/testdata/insights/template/parameters_two_days_ago,_no_data.json.golden
index e3875b2a34..f735c19be4 100644
--- a/coderd/testdata/insights/template/parameters_two_days_ago,_no_data.json.golden
+++ b/coderd/testdata/insights/template/parameters_two_days_ago,_no_data.json.golden
@@ -39,6 +39,5 @@
}
],
"parameters_usage": []
- },
- "interval_reports": []
+ }
}
diff --git a/coderd/testdata/insights/template/parameters_yesterday_and_today_deployment_wide.json.golden b/coderd/testdata/insights/template/parameters_yesterday_and_today_deployment_wide.json.golden
index fc7ccd8a50..f260941893 100644
--- a/coderd/testdata/insights/template/parameters_yesterday_and_today_deployment_wide.json.golden
+++ b/coderd/testdata/insights/template/parameters_yesterday_and_today_deployment_wide.json.golden
@@ -160,6 +160,5 @@
]
}
]
- },
- "interval_reports": []
+ }
}
diff --git a/codersdk/insights.go b/codersdk/insights.go
index 6312c6f2c4..047f35a4da 100644
--- a/codersdk/insights.go
+++ b/codersdk/insights.go
@@ -38,6 +38,15 @@ const (
InsightsReportIntervalWeek InsightsReportInterval = "week"
)
+// TemplateInsightsSection defines the section to be included in the template insights response.
+type TemplateInsightsSection string
+
+// TemplateInsightsSection enums.
+const (
+ TemplateInsightsSectionIntervalReports TemplateInsightsSection = "interval_reports"
+ TemplateInsightsSectionReport TemplateInsightsSection = "report"
+)
+
// UserLatencyInsightsResponse is the response from the user latency insights
// endpoint.
type UserLatencyInsightsResponse struct {
@@ -158,8 +167,8 @@ func (c *Client) UserActivityInsights(ctx context.Context, req UserActivityInsig
// TemplateInsightsResponse is the response from the template insights endpoint.
type TemplateInsightsResponse struct {
- Report TemplateInsightsReport `json:"report"`
- IntervalReports []TemplateInsightsIntervalReport `json:"interval_reports"`
+ Report *TemplateInsightsReport `json:"report,omitempty"`
+ IntervalReports []TemplateInsightsIntervalReport `json:"interval_reports,omitempty"`
}
// TemplateInsightsReport is the report from the template insights endpoint.
@@ -221,10 +230,11 @@ type TemplateParameterValue struct {
}
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" example:"day"`
+ 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" example:"day"`
+ Sections []TemplateInsightsSection `json:"sections" example:"report"`
}
func (c *Client) TemplateInsights(ctx context.Context, req TemplateInsightsRequest) (TemplateInsightsResponse, error) {
@@ -241,6 +251,13 @@ func (c *Client) TemplateInsights(ctx context.Context, req TemplateInsightsReque
if req.Interval != "" {
qp.Add("interval", string(req.Interval))
}
+ if len(req.Sections) > 0 {
+ var sections []string
+ for _, sec := range req.Sections {
+ sections = append(sections, string(sec))
+ }
+ qp.Add("sections", strings.Join(sections, ","))
+ }
reqURL := fmt.Sprintf("/api/v2/insights/templates?%s", qp.Encode())
resp, err := c.Request(ctx, http.MethodGet, reqURL, nil)
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index 0b7f0c4cde..3acced5ba7 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -964,12 +964,13 @@ export interface TemplateInsightsRequest {
readonly end_time: string;
readonly template_ids: string[];
readonly interval: InsightsReportInterval;
+ readonly sections: TemplateInsightsSection[];
}
// From codersdk/insights.go
export interface TemplateInsightsResponse {
- readonly report: TemplateInsightsReport;
- readonly interval_reports: TemplateInsightsIntervalReport[];
+ readonly report?: TemplateInsightsReport;
+ readonly interval_reports?: TemplateInsightsIntervalReport[];
}
// From codersdk/insights.go
@@ -1877,6 +1878,13 @@ export const ServerSentEventTypes: ServerSentEventType[] = [
export type TemplateAppsType = "app" | "builtin";
export const TemplateAppsTypes: TemplateAppsType[] = ["app", "builtin"];
+// From codersdk/insights.go
+export type TemplateInsightsSection = "interval_reports" | "report";
+export const TemplateInsightsSections: TemplateInsightsSection[] = [
+ "interval_reports",
+ "report",
+];
+
// From codersdk/templates.go
export type TemplateRole = "" | "admin" | "use";
export const TemplateRoles: TemplateRole[] = ["", "admin", "use"];
diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx
index e6ae5e642e..d9c1444af4 100644
--- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx
+++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx
@@ -20,6 +20,7 @@ import { getTemplatePageTitle } from "../utils";
import { Loader } from "components/Loader/Loader";
import {
DAUsResponse,
+ TemplateAppUsage,
TemplateInsightsResponse,
TemplateParameterUsage,
TemplateParameterValue,
@@ -105,11 +106,11 @@ export const TemplateInsightsPageView = ({
>
@@ -202,7 +203,7 @@ const TemplateUsagePanel = ({
data,
...panelProps
}: PanelProps & {
- data: TemplateInsightsResponse["report"]["apps_usage"] | undefined;
+ data: TemplateAppUsage[] | undefined;
}) => {
const validUsage = data?.filter((u) => u.seconds > 0);
const totalInSeconds =
@@ -293,7 +294,7 @@ const TemplateParametersUsagePanel = ({
data,
...panelProps
}: PanelProps & {
- data: TemplateInsightsResponse["report"]["parameters_usage"] | undefined;
+ data: TemplateParameterUsage[] | undefined;
}) => {
return (
@@ -588,12 +589,14 @@ function mapToDAUsResponse(
): DAUsResponse {
return {
tz_hour_offset: 0,
- entries: data.map((d) => {
- return {
- amount: d.active_users,
- date: d.start_time,
- };
- }),
+ entries: data
+ ? data.map((d) => {
+ return {
+ amount: d.active_users,
+ date: d.start_time,
+ };
+ })
+ : [],
};
}