From c87fffcdb61deca15d0f70aa0c1ec31cc1f44d88 Mon Sep 17 00:00:00 2001 From: Asher Date: Mon, 24 Nov 2025 12:33:38 -0900 Subject: [PATCH 1/9] feat: Add a flag to disable storing agent and app stats This effectively disables template insights, as there will be no data. Prometheus remains unaffected; this only prevents our own storage of the stats. --- cli/testdata/coder_server_--help.golden | 6 + cli/testdata/server-config.yaml.golden | 4 + coderd/agentapi/stats_test.go | 131 ++++++++++++++++- coderd/apidoc/docs.go | 3 + coderd/apidoc/swagger.json | 3 + coderd/coderd.go | 17 ++- coderd/insights_test.go | 139 ++++++++++++------ .../disabled_week_deployment_wide.json.golden | 107 ++++++++++++++ .../disabled_week_deployment_wide.json.golden | 8 + coderd/workspacestats/reporter.go | 41 ++++-- codersdk/deployment.go | 11 ++ docs/reference/api/general.md | 1 + docs/reference/api/schemas.md | 3 + docs/reference/cli/server.md | 11 ++ .../cli/testdata/coder_server_--help.golden | 6 + site/src/api/typesGenerated.ts | 1 + 16 files changed, 425 insertions(+), 67 deletions(-) create mode 100644 coderd/testdata/insights/template/disabled_week_deployment_wide.json.golden create mode 100644 coderd/testdata/insights/user-activity/disabled_week_deployment_wide.json.golden diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index aa318a5f857c7..9042b7b54622a 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -223,6 +223,12 @@ Configure TLS for your SMTP server target. --email-tls-starttls bool, $CODER_EMAIL_TLS_STARTTLS Enable STARTTLS to upgrade insecure SMTP connections using TLS. +INTROSPECTION OPTIONS: +Configure logging, tracing, and metrics exporting. + + --disable-template-insights bool, $CODER_DISABLE_TEMPLATE_INSIGHTS (default: false) + Disable storage and display of template insights. + INTROSPECTION / HEALTH CHECK OPTIONS: --health-check-refresh duration, $CODER_HEALTH_CHECK_REFRESH (default: 10m0s) Refresh interval for healthchecks. diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index a9e6058a3eef2..d1c10e42ab854 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -190,7 +190,11 @@ autobuildPollInterval: 1m0s # Interval to poll for hung and pending jobs and automatically terminate them. # (default: 1m0s, type: duration) jobHangDetectorInterval: 1m0s +# Configure logging, tracing, and metrics exporting. introspection: + # Disable storage and display of template insights. + # (default: false, type: bool) + disableTemplateInsights: false prometheus: # Serve prometheus metrics on the address defined by prometheus address. # (default: , type: bool) diff --git a/coderd/agentapi/stats_test.go b/coderd/agentapi/stats_test.go index c5cc2bd262114..75d8795d21ab6 100644 --- a/coderd/agentapi/stats_test.go +++ b/coderd/agentapi/stats_test.go @@ -28,7 +28,7 @@ import ( "github.com/coder/coder/v2/testutil" ) -func TestUpdateStates(t *testing.T) { +func TestUpdateStats(t *testing.T) { t.Parallel() var ( @@ -542,6 +542,135 @@ func TestUpdateStates(t *testing.T) { } require.True(t, updateAgentMetricsFnCalled) }) + + t.Run("DropStats", func(t *testing.T) { + t.Parallel() + + var ( + now = dbtime.Now() + dbM = dbmock.NewMockStore(gomock.NewController(t)) + ps = pubsub.NewInMemory() + + templateScheduleStore = schedule.MockTemplateScheduleStore{ + GetFn: func(context.Context, database.Store, uuid.UUID) (schedule.TemplateScheduleOptions, error) { + panic("should not be called") + }, + SetFn: func(context.Context, database.Store, database.Template, schedule.TemplateScheduleOptions) (database.Template, error) { + panic("not implemented") + }, + } + updateAgentMetricsFnCalled = false + tickCh = make(chan time.Time) + flushCh = make(chan int, 1) + wut = workspacestats.NewTracker(dbM, + workspacestats.TrackerWithTickFlush(tickCh, flushCh), + ) + + req = &agentproto.UpdateStatsRequest{ + Stats: &agentproto.Stats{ + ConnectionsByProto: map[string]int64{ + "tcp": 1, + "dean": 2, + }, + ConnectionCount: 3, + ConnectionMedianLatencyMs: 23, + RxPackets: 120, + RxBytes: 1000, + TxPackets: 130, + TxBytes: 2000, + SessionCountVscode: 1, + SessionCountJetbrains: 2, + SessionCountReconnectingPty: 3, + SessionCountSsh: 4, + Metrics: []*agentproto.Stats_Metric{ + { + Name: "awesome metric", + Value: 42, + }, + { + Name: "uncool metric", + Value: 0, + }, + }, + }, + } + ) + api := agentapi.StatsAPI{ + AgentFn: func(context.Context) (database.WorkspaceAgent, error) { + return agent, nil + }, + Workspace: &workspaceAsCacheFields, + Database: dbM, + StatsReporter: workspacestats.NewReporter(workspacestats.ReporterOptions{ + Database: dbM, + Pubsub: ps, + StatsBatcher: nil, // Should not be called. + UsageTracker: wut, + TemplateScheduleStore: templateScheduleStorePtr(templateScheduleStore), + UpdateAgentMetricsFn: func(ctx context.Context, labels prometheusmetrics.AgentMetricLabels, metrics []*agentproto.Stats_Metric) { + updateAgentMetricsFnCalled = true + assert.Equal(t, prometheusmetrics.AgentMetricLabels{ + Username: user.Username, + WorkspaceName: workspace.Name, + AgentName: agent.Name, + TemplateName: template.Name, + }, labels) + assert.Equal(t, req.Stats.Metrics, metrics) + }, + DisableDatabaseStorage: true, + }), + AgentStatsRefreshInterval: 10 * time.Second, + TimeNowFn: func() time.Time { + return now + }, + } + defer wut.Close() + + // We expect an activity bump because ConnectionCount > 0. + dbM.EXPECT().ActivityBumpWorkspace(gomock.Any(), database.ActivityBumpWorkspaceParams{ + WorkspaceID: workspace.ID, + NextAutostart: time.Time{}.UTC(), + }).Return(nil) + + // Workspace last used at gets bumped. + dbM.EXPECT().BatchUpdateWorkspaceLastUsedAt(gomock.Any(), database.BatchUpdateWorkspaceLastUsedAtParams{ + IDs: []uuid.UUID{workspace.ID}, + LastUsedAt: now, + }).Return(nil) + + // Ensure that pubsub notifications are sent. + notifyDescription := make(chan struct{}) + ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID), + wspubsub.HandleWorkspaceEvent( + func(_ context.Context, e wspubsub.WorkspaceEvent, err error) { + if err != nil { + return + } + if e.Kind == wspubsub.WorkspaceEventKindStatsUpdate && e.WorkspaceID == workspace.ID { + go func() { + notifyDescription <- struct{}{} + }() + } + })) + + resp, err := api.UpdateStats(context.Background(), req) + require.NoError(t, err) + require.Equal(t, &agentproto.UpdateStatsResponse{ + ReportInterval: durationpb.New(10 * time.Second), + }, resp) + + tickCh <- now + count := <-flushCh + require.Equal(t, 1, count, "expected one flush with one id") + + ctx := testutil.Context(t, testutil.WaitShort) + select { + case <-ctx.Done(): + t.Error("timed out while waiting for pubsub notification") + case <-notifyDescription: + } + require.True(t, updateAgentMetricsFnCalled) + }) } func templateScheduleStorePtr(store schedule.TemplateScheduleStore) *atomic.Pointer[schedule.TemplateScheduleStore] { diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index b8e3331ecd1f2..fe2e6102a328f 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -14214,6 +14214,9 @@ const docTemplate = `{ "disable_path_apps": { "type": "boolean" }, + "disable_template_insights": { + "type": "boolean" + }, "disable_workspace_sharing": { "type": "boolean" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 396a704a06119..ab71f64e333bd 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -12798,6 +12798,9 @@ "disable_path_apps": { "type": "boolean" }, + "disable_template_insights": { + "type": "boolean" + }, "disable_workspace_sharing": { "type": "boolean" }, diff --git a/coderd/coderd.go b/coderd/coderd.go index b356f372dc56c..c3a4a50408834 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -768,14 +768,15 @@ func New(options *Options) *API { } api.statsReporter = workspacestats.NewReporter(workspacestats.ReporterOptions{ - Database: options.Database, - Logger: options.Logger.Named("workspacestats"), - Pubsub: options.Pubsub, - TemplateScheduleStore: options.TemplateScheduleStore, - StatsBatcher: options.StatsBatcher, - UsageTracker: options.WorkspaceUsageTracker, - UpdateAgentMetricsFn: options.UpdateAgentMetrics, - AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize, + Database: options.Database, + Logger: options.Logger.Named("workspacestats"), + Pubsub: options.Pubsub, + TemplateScheduleStore: options.TemplateScheduleStore, + StatsBatcher: options.StatsBatcher, + UsageTracker: options.WorkspaceUsageTracker, + UpdateAgentMetricsFn: options.UpdateAgentMetrics, + AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize, + DisableDatabaseStorage: options.DeploymentValues.DisableTemplateInsights.Value(), }) workspaceAppsLogger := options.Logger.Named("workspaceapps") if options.WorkspaceAppsStatsCollectorOptions.Logger == nil { diff --git a/coderd/insights_test.go b/coderd/insights_test.go index a4a47bea396a6..221d702eb5e3d 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -520,7 +520,7 @@ func TestTemplateInsights_Golden(t *testing.T) { return templates, users, testData } - prepare := func(t *testing.T, templates []*testTemplate, users []*testUser, testData map[*testWorkspace]testDataGen) (*codersdk.Client, chan dbrollup.Event) { + prepare := func(t *testing.T, templates []*testTemplate, users []*testUser, testData map[*testWorkspace]testDataGen, disableStorage bool) (*codersdk.Client, chan dbrollup.Event) { logger := testutil.Logger(t) db, ps := dbtestutil.NewDB(t) events := make(chan dbrollup.Event) @@ -706,22 +706,24 @@ func TestTemplateInsights_Golden(t *testing.T) { require.NoError(t, err) defer batcherCloser() // Flushes the stats, this is to ensure they're written. - for workspace, data := range testData { - for _, stat := range data.agentStats { - createdAt := stat.startedAt - connectionCount := int64(1) - if stat.noConnections { - connectionCount = 0 - } - for createdAt.Before(stat.endedAt) { - batcher.Add(createdAt, workspace.agentID, workspace.template.id, workspace.user.(*testUser).sdk.ID, workspace.id, &agentproto.Stats{ - ConnectionCount: connectionCount, - SessionCountVscode: stat.sessionCountVSCode, - SessionCountJetbrains: stat.sessionCountJetBrains, - SessionCountReconnectingPty: stat.sessionCountReconnectingPTY, - SessionCountSsh: stat.sessionCountSSH, - }, false) - createdAt = createdAt.Add(30 * time.Second) + if !disableStorage { + for workspace, data := range testData { + for _, stat := range data.agentStats { + createdAt := stat.startedAt + connectionCount := int64(1) + if stat.noConnections { + connectionCount = 0 + } + for createdAt.Before(stat.endedAt) { + batcher.Add(createdAt, workspace.agentID, workspace.template.id, workspace.user.(*testUser).sdk.ID, workspace.id, &agentproto.Stats{ + ConnectionCount: connectionCount, + SessionCountVscode: stat.sessionCountVSCode, + SessionCountJetbrains: stat.sessionCountJetBrains, + SessionCountReconnectingPty: stat.sessionCountReconnectingPTY, + SessionCountSsh: stat.sessionCountSSH, + }, false) + createdAt = createdAt.Add(30 * time.Second) + } } } } @@ -750,8 +752,9 @@ func TestTemplateInsights_Golden(t *testing.T) { } } reporter := workspacestats.NewReporter(workspacestats.ReporterOptions{ - Database: db, - AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize, + Database: db, + AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize, + DisableDatabaseStorage: disableStorage, }) err = reporter.ReportAppStats(dbauthz.AsSystemRestricted(ctx), stats) require.NoError(t, err, "want no error inserting app stats") @@ -1057,10 +1060,11 @@ func TestTemplateInsights_Golden(t *testing.T) { ignoreTimes bool } tests := []struct { - name string - makeFixture func() ([]*testTemplate, []*testUser) - makeTestData func([]*testTemplate, []*testUser) map[*testWorkspace]testDataGen - requests []testRequest + name string + makeFixture func() ([]*testTemplate, []*testUser) + makeTestData func([]*testTemplate, []*testUser) map[*testWorkspace]testDataGen + disableStorage bool + requests []testRequest }{ { name: "multiple users and workspaces", @@ -1237,6 +1241,24 @@ func TestTemplateInsights_Golden(t *testing.T) { }, }, }, + { + name: "disabled", + makeFixture: baseTemplateAndUserFixture, + makeTestData: makeBaseTestData, + disableStorage: true, + requests: []testRequest{ + { + name: "week deployment wide", + makeRequest: func(_ []*testTemplate) codersdk.TemplateInsightsRequest { + return codersdk.TemplateInsightsRequest{ + StartTime: frozenWeekAgo, + EndTime: frozenWeekAgo.AddDate(0, 0, 7), + Interval: codersdk.InsightsReportIntervalDay, + } + }, + }, + }, + }, } for _, tt := range tests { @@ -1246,7 +1268,7 @@ func TestTemplateInsights_Golden(t *testing.T) { require.NotNil(t, tt.makeFixture, "test bug: makeFixture must be set") require.NotNil(t, tt.makeTestData, "test bug: makeTestData must be set") templates, users, testData := prepareFixtureAndTestData(t, tt.makeFixture, tt.makeTestData) - client, events := prepare(t, templates, users, testData) + client, events := prepare(t, templates, users, testData, tt.disableStorage) // Drain two events, the first one resumes rolluper // operation and the second one waits for the rollup @@ -1431,7 +1453,7 @@ func TestUserActivityInsights_Golden(t *testing.T) { return templates, users, testData } - prepare := func(t *testing.T, templates []*testTemplate, users []*testUser, testData map[*testWorkspace]testDataGen) (*codersdk.Client, chan dbrollup.Event) { + prepare := func(t *testing.T, templates []*testTemplate, users []*testUser, testData map[*testWorkspace]testDataGen, disableStorage bool) (*codersdk.Client, chan dbrollup.Event) { logger := testutil.Logger(t) db, ps := dbtestutil.NewDB(t) events := make(chan dbrollup.Event) @@ -1595,22 +1617,24 @@ func TestUserActivityInsights_Golden(t *testing.T) { require.NoError(t, err) defer batcherCloser() // Flushes the stats, this is to ensure they're written. - for workspace, data := range testData { - for _, stat := range data.agentStats { - createdAt := stat.startedAt - connectionCount := int64(1) - if stat.noConnections { - connectionCount = 0 - } - for createdAt.Before(stat.endedAt) { - batcher.Add(createdAt, workspace.agentID, workspace.template.id, workspace.user.(*testUser).sdk.ID, workspace.id, &agentproto.Stats{ - ConnectionCount: connectionCount, - SessionCountVscode: stat.sessionCountVSCode, - SessionCountJetbrains: stat.sessionCountJetBrains, - SessionCountReconnectingPty: stat.sessionCountReconnectingPTY, - SessionCountSsh: stat.sessionCountSSH, - }, false) - createdAt = createdAt.Add(30 * time.Second) + if !disableStorage { + for workspace, data := range testData { + for _, stat := range data.agentStats { + createdAt := stat.startedAt + connectionCount := int64(1) + if stat.noConnections { + connectionCount = 0 + } + for createdAt.Before(stat.endedAt) { + batcher.Add(createdAt, workspace.agentID, workspace.template.id, workspace.user.(*testUser).sdk.ID, workspace.id, &agentproto.Stats{ + ConnectionCount: connectionCount, + SessionCountVscode: stat.sessionCountVSCode, + SessionCountJetbrains: stat.sessionCountJetBrains, + SessionCountReconnectingPty: stat.sessionCountReconnectingPTY, + SessionCountSsh: stat.sessionCountSSH, + }, false) + createdAt = createdAt.Add(30 * time.Second) + } } } } @@ -1639,8 +1663,9 @@ func TestUserActivityInsights_Golden(t *testing.T) { } } reporter := workspacestats.NewReporter(workspacestats.ReporterOptions{ - Database: db, - AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize, + Database: db, + AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize, + DisableDatabaseStorage: disableStorage, }) err = reporter.ReportAppStats(dbauthz.AsSystemRestricted(ctx), stats) require.NoError(t, err, "want no error inserting app stats") @@ -1902,10 +1927,11 @@ func TestUserActivityInsights_Golden(t *testing.T) { ignoreTimes bool } tests := []struct { - name string - makeFixture func() ([]*testTemplate, []*testUser) - makeTestData func([]*testTemplate, []*testUser) map[*testWorkspace]testDataGen - requests []testRequest + name string + makeFixture func() ([]*testTemplate, []*testUser) + makeTestData func([]*testTemplate, []*testUser) map[*testWorkspace]testDataGen + disableStorage bool + requests []testRequest }{ { name: "multiple users and workspaces", @@ -2013,6 +2039,23 @@ func TestUserActivityInsights_Golden(t *testing.T) { }, }, }, + { + name: "disabled", + makeFixture: baseTemplateAndUserFixture, + makeTestData: makeBaseTestData, + disableStorage: true, + requests: []testRequest{ + { + name: "week deployment wide", + makeRequest: func(templates []*testTemplate) codersdk.UserActivityInsightsRequest { + return codersdk.UserActivityInsightsRequest{ + StartTime: frozenWeekAgo, + EndTime: frozenWeekAgo.AddDate(0, 0, 7), + } + }, + }, + }, + }, } for _, tt := range tests { @@ -2022,7 +2065,7 @@ func TestUserActivityInsights_Golden(t *testing.T) { require.NotNil(t, tt.makeFixture, "test bug: makeFixture must be set") require.NotNil(t, tt.makeTestData, "test bug: makeTestData must be set") templates, users, testData := prepareFixtureAndTestData(t, tt.makeFixture, tt.makeTestData) - client, events := prepare(t, templates, users, testData) + client, events := prepare(t, templates, users, testData, tt.disableStorage) // Drain two events, the first one resumes rolluper // operation and the second one waits for the rollup diff --git a/coderd/testdata/insights/template/disabled_week_deployment_wide.json.golden b/coderd/testdata/insights/template/disabled_week_deployment_wide.json.golden new file mode 100644 index 0000000000000..0d2a4870c4d30 --- /dev/null +++ b/coderd/testdata/insights/template/disabled_week_deployment_wide.json.golden @@ -0,0 +1,107 @@ +{ + "report": { + "start_time": "2023-08-15T00:00:00Z", + "end_time": "2023-08-22T00:00:00Z", + "template_ids": [], + "active_users": 0, + "apps_usage": [ + { + "template_ids": [], + "type": "builtin", + "display_name": "Visual Studio Code", + "slug": "vscode", + "icon": "/icon/code.svg", + "seconds": 0, + "times_used": 0 + }, + { + "template_ids": [], + "type": "builtin", + "display_name": "JetBrains", + "slug": "jetbrains", + "icon": "/icon/intellij.svg", + "seconds": 0, + "times_used": 0 + }, + { + "template_ids": [], + "type": "builtin", + "display_name": "Web Terminal", + "slug": "reconnecting-pty", + "icon": "/icon/terminal.svg", + "seconds": 0, + "times_used": 0 + }, + { + "template_ids": [], + "type": "builtin", + "display_name": "SSH", + "slug": "ssh", + "icon": "/icon/terminal.svg", + "seconds": 0, + "times_used": 0 + }, + { + "template_ids": [], + "type": "builtin", + "display_name": "SFTP", + "slug": "sftp", + "icon": "/icon/terminal.svg", + "seconds": 0, + "times_used": 0 + } + ], + "parameters_usage": [] + }, + "interval_reports": [ + { + "start_time": "2023-08-15T00:00:00Z", + "end_time": "2023-08-16T00:00:00Z", + "template_ids": [], + "interval": "day", + "active_users": 0 + }, + { + "start_time": "2023-08-16T00:00:00Z", + "end_time": "2023-08-17T00:00:00Z", + "template_ids": [], + "interval": "day", + "active_users": 0 + }, + { + "start_time": "2023-08-17T00:00:00Z", + "end_time": "2023-08-18T00:00:00Z", + "template_ids": [], + "interval": "day", + "active_users": 0 + }, + { + "start_time": "2023-08-18T00:00:00Z", + "end_time": "2023-08-19T00:00:00Z", + "template_ids": [], + "interval": "day", + "active_users": 0 + }, + { + "start_time": "2023-08-19T00:00:00Z", + "end_time": "2023-08-20T00:00:00Z", + "template_ids": [], + "interval": "day", + "active_users": 0 + }, + { + "start_time": "2023-08-20T00:00:00Z", + "end_time": "2023-08-21T00:00:00Z", + "template_ids": [], + "interval": "day", + "active_users": 0 + }, + { + "start_time": "2023-08-21T00:00:00Z", + "end_time": "2023-08-22T00:00:00Z", + "template_ids": [], + "interval": "day", + "active_users": 0 + } + ] +} diff --git a/coderd/testdata/insights/user-activity/disabled_week_deployment_wide.json.golden b/coderd/testdata/insights/user-activity/disabled_week_deployment_wide.json.golden new file mode 100644 index 0000000000000..a02a67d7be491 --- /dev/null +++ b/coderd/testdata/insights/user-activity/disabled_week_deployment_wide.json.golden @@ -0,0 +1,8 @@ +{ + "report": { + "start_time": "2023-08-15T00:00:00Z", + "end_time": "2023-08-22T00:00:00Z", + "template_ids": [], + "users": [] + } +} diff --git a/coderd/workspacestats/reporter.go b/coderd/workspacestats/reporter.go index ea81843488e82..1810c46ce0cdd 100644 --- a/coderd/workspacestats/reporter.go +++ b/coderd/workspacestats/reporter.go @@ -22,6 +22,21 @@ import ( "github.com/coder/coder/v2/coderd/wspubsub" ) +// TODO: There are currently two paths for reporting activity, both of which are +// tied up with stat collection: +// +// 1. The workspace agent periodically POSTs stats to coderd. On receiving +// this POST, if there is an active SSH or web terminal session, bump both +// the workspace's last_used_at and the deadline. +// 2. The coderd app proxy and wsproxy will periodically report app status +// (coderd calls directly, wsproxy POSTs). This only bumps the workspace's +// last_used_at, as only SSH and web terminal sessions count as activity. +// +// Ideally we would have a single code path for this and we may want to untangle +// activity bumping from stat reporting so we can disable stats collection +// entirely when template insights are disabled rather than having to still +// collect stats but then drop them here. + type ReporterOptions struct { Database database.Store Logger slog.Logger @@ -31,6 +46,10 @@ type ReporterOptions struct { UsageTracker *UsageTracker UpdateAgentMetricsFn func(ctx context.Context, labels prometheusmetrics.AgentMetricLabels, metrics []*agentproto.Stats_Metric) + // DisableDatabaseStorage prevents storing stats in the database. The + // reporter will still call UpdateAgentMetricsFn and bump workspace activity. + DisableDatabaseStorage bool + AppStatBatchSize int } @@ -93,15 +112,12 @@ func (r *Reporter) ReportAppStats(ctx context.Context, stats []workspaceapps.Sta return nil } - if err := tx.InsertWorkspaceAppStats(ctx, batch); err != nil { - return err + if !r.opts.DisableDatabaseStorage { + if err := tx.InsertWorkspaceAppStats(ctx, batch); err != nil { + return err + } } - // TODO: We currently measure workspace usage based on when we get stats from it. - // There are currently two paths for this: - // 1) From SSH -> workspace agent stats POSTed from agent - // 2) From workspace apps / rpty -> workspace app stats (from coderd / wsproxy) - // Ideally we would have a single code path for this. uniqueIDs := slice.Unique(batch.WorkspaceID) if err := tx.BatchUpdateWorkspaceLastUsedAt(ctx, database.BatchUpdateWorkspaceLastUsedAtParams{ IDs: uniqueIDs, @@ -122,9 +138,11 @@ func (r *Reporter) ReportAppStats(ctx context.Context, stats []workspaceapps.Sta // nolint:revive // usage is a control flag while we have the experiment func (r *Reporter) ReportAgentStats(ctx context.Context, now time.Time, workspace database.WorkspaceIdentity, workspaceAgent database.WorkspaceAgent, stats *agentproto.Stats, usage bool) error { // update agent stats - r.opts.StatsBatcher.Add(now, workspaceAgent.ID, workspace.TemplateID, workspace.OwnerID, workspace.ID, stats, usage) + if !r.opts.DisableDatabaseStorage { + r.opts.StatsBatcher.Add(now, workspaceAgent.ID, workspace.TemplateID, workspace.OwnerID, workspace.ID, stats, usage) + } - // update prometheus metrics + // update prometheus metrics (even if template insights are disabled) if r.opts.UpdateAgentMetricsFn != nil { r.opts.UpdateAgentMetricsFn(ctx, prometheusmetrics.AgentMetricLabels{ Username: workspace.OwnerUsername, @@ -135,7 +153,10 @@ func (r *Reporter) ReportAgentStats(ctx context.Context, now time.Time, workspac } // workspace activity: if no sessions we do not bump activity - if usage && stats.SessionCountVscode == 0 && stats.SessionCountJetbrains == 0 && stats.SessionCountReconnectingPty == 0 && stats.SessionCountSsh == 0 { + if usage && stats.SessionCountVscode == 0 && + stats.SessionCountJetbrains == 0 && + stats.SessionCountReconnectingPty == 0 && + stats.SessionCountSsh == 0 { return nil } diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 0dd082ab5eebc..55c5654dbc526 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -511,6 +511,7 @@ type DeploymentValues struct { Prebuilds PrebuildsConfig `json:"workspace_prebuilds,omitempty" typescript:",notnull"` HideAITasks serpent.Bool `json:"hide_ai_tasks,omitempty" typescript:",notnull"` AI AIConfig `json:"ai,omitempty"` + DisableTemplateInsights serpent.Bool `json:"disable_template_insights,omitempty" typescript:",notnull"` Config serpent.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"` WriteConfig serpent.Bool `json:"write_config,omitempty" typescript:",notnull"` @@ -1701,6 +1702,16 @@ func (c *DeploymentValues) Options() serpent.OptionSet { Group: &deploymentGroupNetworkingDERP, YAML: "configPath", }, + { + Name: "Disable Template Insights", + Description: "Disable storage and display of template insights.", + Flag: "disable-template-insights", + Env: "CODER_DISABLE_TEMPLATE_INSIGHTS", + Default: "false", + Value: &c.DisableTemplateInsights, + Group: &deploymentGroupIntrospection, + YAML: "disableTemplateInsights", + }, // TODO: support Git Auth settings. // Prometheus settings { diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index 3ea0180ae1454..34d9e1a8feaa5 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -233,6 +233,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "disable_owner_workspace_exec": true, "disable_password_auth": true, "disable_path_apps": true, + "disable_template_insights": true, "disable_workspace_sharing": true, "docs_url": { "forceQuery": true, diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index bd00d79c4b40b..4724f52cc9ae9 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -2917,6 +2917,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "disable_owner_workspace_exec": true, "disable_password_auth": true, "disable_path_apps": true, + "disable_template_insights": true, "disable_workspace_sharing": true, "docs_url": { "forceQuery": true, @@ -3440,6 +3441,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "disable_owner_workspace_exec": true, "disable_password_auth": true, "disable_path_apps": true, + "disable_template_insights": true, "disable_workspace_sharing": true, "docs_url": { "forceQuery": true, @@ -3795,6 +3797,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `disable_owner_workspace_exec` | boolean | false | | | | `disable_password_auth` | boolean | false | | | | `disable_path_apps` | boolean | false | | | +| `disable_template_insights` | boolean | false | | | | `disable_workspace_sharing` | boolean | false | | | | `docs_url` | [serpent.URL](#serpenturl) | false | | | | `enable_authz_recording` | boolean | false | | | diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md index 4ba8c026fb299..e2f44f6969614 100644 --- a/docs/reference/cli/server.md +++ b/docs/reference/cli/server.md @@ -269,6 +269,17 @@ URL to fetch a DERP mapping on startup. See: https://tailscale.com/kb/1118/custo Path to read a DERP mapping from. See: https://tailscale.com/kb/1118/custom-derp-servers/. +### --disable-template-insights + +| | | +|-------------|----------------------------------------------------| +| Type | bool | +| Environment | $CODER_DISABLE_TEMPLATE_INSIGHTS | +| YAML | introspection.disableTemplateInsights | +| Default | false | + +Disable storage and display of template insights. + ### --prometheus-enable | | | diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index 32db725d93f77..4ce380e9b3fae 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -224,6 +224,12 @@ Configure TLS for your SMTP server target. --email-tls-starttls bool, $CODER_EMAIL_TLS_STARTTLS Enable STARTTLS to upgrade insecure SMTP connections using TLS. +INTROSPECTION OPTIONS: +Configure logging, tracing, and metrics exporting. + + --disable-template-insights bool, $CODER_DISABLE_TEMPLATE_INSIGHTS (default: false) + Disable storage and display of template insights. + INTROSPECTION / HEALTH CHECK OPTIONS: --health-check-refresh duration, $CODER_HEALTH_CHECK_REFRESH (default: 10m0s) Refresh interval for healthchecks. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 6cb14744035cc..2d20247f61796 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1788,6 +1788,7 @@ export interface DeploymentValues { readonly workspace_prebuilds?: PrebuildsConfig; readonly hide_ai_tasks?: boolean; readonly ai?: AIConfig; + readonly disable_template_insights?: boolean; readonly config?: string; readonly write_config?: boolean; /** From deb0bd062072107632ae7bb45284510087584049 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 25 Nov 2025 16:25:42 -0900 Subject: [PATCH 2/9] Fix codersdk get user status counts It takes a timezone offset, not a date. --- codersdk/insights.go | 7 +++++-- site/src/api/typesGenerated.ts | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/codersdk/insights.go b/codersdk/insights.go index ef44b6b8d013e..301411d412c49 100644 --- a/codersdk/insights.go +++ b/codersdk/insights.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/url" + "strconv" "strings" "time" @@ -293,12 +294,14 @@ type UserStatusChangeCount struct { } type GetUserStatusCountsRequest struct { - Offset time.Time `json:"offset" format:"date-time"` + // Timezone offset in hours. Use 0 for UTC, and TimezoneOffsetHour(time.Local) + // for the local timezone. + Offset int `json:"offset"` } func (c *Client) GetUserStatusCounts(ctx context.Context, req GetUserStatusCountsRequest) (GetUserStatusCountsResponse, error) { qp := url.Values{} - qp.Add("offset", req.Offset.Format(insightsTimeLayout)) + qp.Add("tz_offset", strconv.Itoa(req.Offset)) reqURL := fmt.Sprintf("/api/v2/insights/user-status-counts?%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 2d20247f61796..e7ccc18cf79aa 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2178,7 +2178,11 @@ export interface GetInboxNotificationResponse { // From codersdk/insights.go export interface GetUserStatusCountsRequest { - readonly offset: string; + /** + * Timezone offset in hours. Use 0 for UTC, and TimezoneOffsetHour(time.Local) + * for the local timezone. + */ + readonly offset: number; } // From codersdk/insights.go From 93e6c388542df196882c61e8c1060f89e86b0a99 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 25 Nov 2025 15:50:47 -0900 Subject: [PATCH 3/9] Disable /insights endpoint according to flag --- coderd/coderd.go | 25 +++++++++-- coderd/insights_test.go | 92 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 4 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index c3a4a50408834..e84e63063ee44 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1529,11 +1529,28 @@ func New(options *Options) *API { }) r.Route("/insights", func(r chi.Router) { r.Use(apiKeyMiddleware) - r.Get("/daus", api.deploymentDAUs) - r.Get("/user-activity", api.insightsUserActivity) + r.Group(func(r chi.Router) { + r.Use( + func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + if options.DeploymentValues.DisableTemplateInsights.Value() { + httpapi.Write(context.Background(), rw, http.StatusForbidden, codersdk.Response{ + Message: "Forbidden.", + Detail: "Template insights are disabled.", + }) + return + } + + next.ServeHTTP(rw, r) + }) + }, + ) + r.Get("/daus", api.deploymentDAUs) + r.Get("/user-activity", api.insightsUserActivity) + r.Get("/user-latency", api.insightsUserLatency) + r.Get("/templates", api.insightsTemplates) + }) r.Get("/user-status-counts", api.insightsUserStatusCounts) - r.Get("/user-latency", api.insightsUserLatency) - r.Get("/templates", api.insightsTemplates) }) r.Route("/debug", func(r chi.Router) { r.Use( diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 221d702eb5e3d..4f42f61b60477 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -2389,3 +2389,95 @@ func TestGenericInsights_RBAC(t *testing.T) { }) } } + +func TestGenericInsights_Disabled(t *testing.T) { + t.Parallel() + + db, ps := dbtestutil.NewDB(t) + logger := testutil.Logger(t) + client := coderdtest.New(t, &coderdtest.Options{ + Database: db, + Pubsub: ps, + Logger: &logger, + IncludeProvisionerDaemon: true, + AgentStatsRefreshInterval: time.Millisecond * 100, + DatabaseRolluper: dbrollup.New( + logger.Named("dbrollup"), + db, + dbrollup.WithInterval(time.Millisecond*100), + ), + DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) { + dv.DisableTemplateInsights = true + }), + }) + user := coderdtest.CreateFirstUser(t, client) + _, _ = coderdtest.CreateAnotherUser(t, client, user.OrganizationID) + + tests := []struct { + name string + fn func(ctx context.Context) error + // ok means there should be no error, otherwise assume forbidden due to + // being disabled. + ok bool + }{ + { + name: "DAUS", + fn: func(ctx context.Context) error { + _, err := client.DeploymentDAUs(ctx, 0) + return err + }, + }, + { + name: "UserActivity", + fn: func(ctx context.Context) error { + _, err := client.UserActivityInsights(ctx, codersdk.UserActivityInsightsRequest{}) + return err + }, + }, + { + name: "UserLatency", + fn: func(ctx context.Context) error { + _, err := client.UserLatencyInsights(ctx, codersdk.UserLatencyInsightsRequest{}) + return err + }, + }, + { + name: "UserStatusCounts", + fn: func(ctx context.Context) error { + _, err := client.GetUserStatusCounts(ctx, codersdk.GetUserStatusCountsRequest{ + Offset: 0, + }) + return err + }, + // Status count is not derived from template insights, so it should not be + // disabled. + ok: true, + }, + { + name: "Templates", + fn: func(ctx context.Context) error { + _, err := client.TemplateInsights(ctx, codersdk.TemplateInsightsRequest{}) + return err + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + err := tt.fn(ctx) + if tt.ok { + require.NoError(t, err) + } else { + require.Error(t, err) + cerr := coderdtest.SDKError(t, err) + require.Contains(t, cerr.Error(), "disabled") + require.Equal(t, http.StatusForbidden, cerr.StatusCode()) + } + }) + } +} From 092cca42ddac5050e179b7772fbaf748d56a9903 Mon Sep 17 00:00:00 2001 From: Asher Date: Tue, 25 Nov 2025 16:57:06 -0900 Subject: [PATCH 4/9] Handle /insights errors on dashboard Now errors show up in the middle of the panel. This will include if the insights are disabled. --- .../TemplateInsightsPage.stories.tsx | 1625 +++++++++-------- .../TemplateInsightsPage.tsx | 233 ++- 2 files changed, 978 insertions(+), 880 deletions(-) diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx index 37b7b89a4c0b2..8af224bd663e7 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx @@ -1,4 +1,5 @@ import { chromatic } from "testHelpers/chromatic"; +import { mockApiError } from "testHelpers/entities"; import type { Meta, StoryObj } from "@storybook/react-vite"; import { TemplateInsightsPageView } from "./TemplateInsightsPage"; @@ -13,39 +14,80 @@ type Story = StoryObj; export const Loading: Story = { args: { - templateInsights: undefined, - userLatency: undefined, + templateInsights: { + data: undefined, + error: null, + }, + userLatency: { + data: undefined, + error: null, + }, + userActivity: { + data: undefined, + error: null, + }, + }, +}; + +const forbidden = mockApiError({ + message: "Forbidden.", + detail: "Template insights are disabled.", +}); + +export const LoadingError: Story = { + args: { + templateInsights: { + data: undefined, + error: forbidden, + }, + userLatency: { + data: undefined, + error: forbidden, + }, + userActivity: { + data: undefined, + error: forbidden, + }, }, }; export const Empty: Story = { args: { templateInsights: { - interval_reports: [], - report: { - active_users: 0, - end_time: "", - start_time: "", - template_ids: [], - apps_usage: [], - parameters_usage: [], + data: { + interval_reports: [], + report: { + active_users: 0, + end_time: "", + start_time: "", + template_ids: [], + apps_usage: [], + parameters_usage: [], + }, }, + error: null, }, userLatency: { - report: { - end_time: "", - start_time: "", - template_ids: [], - users: [], + data: { + report: { + end_time: "", + start_time: "", + template_ids: [], + users: [], + }, }, + error: null, }, userActivity: { - report: { - end_time: "", - start_time: "", - template_ids: [], - users: [], + data: { + report: { + end_time: "", + start_time: "", + template_ids: [], + users: [], + }, }, + error: null, }, }, }; @@ -54,816 +96,837 @@ export const Loaded: Story = { args: { // Got from dev.coder.com network calls templateInsights: { - report: { - start_time: "2023-07-18T00:00:00Z", - end_time: "2023-07-25T00:00:00Z", - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - active_users: 14, - apps_usage: [ + data: { + report: { + start_time: "2023-07-18T00:00:00Z", + end_time: "2023-07-25T00:00:00Z", + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + active_users: 14, + apps_usage: [ + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + type: "builtin", + display_name: "Visual Studio Code", + slug: "vscode", + icon: "/icon/code.svg", + seconds: 2513400, + times_used: 0, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + type: "builtin", + display_name: "JetBrains", + slug: "jetbrains", + icon: "/icon/intellij.svg", + seconds: 2013400, + times_used: 20, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + type: "builtin", + display_name: "Web Terminal", + slug: "reconnecting-pty", + icon: "/icon/terminal.svg", + seconds: 110400, + times_used: 0, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + type: "builtin", + display_name: "SSH", + slug: "ssh", + icon: "/icon/terminal.svg", + seconds: 1020900, + times_used: 0, + }, + ], + parameters_usage: [ + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "", + name: "Compute instances", + type: "number", + description: "Let's set the expected number of instances.", + values: [ + { + value: "3", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "", + name: "Docker Image", + type: "string", + description: "Docker image for the development container", + values: [ + { + value: "ghcr.io/harrison-ai/coder-dev:base", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "Very random string", + name: "Optional random string", + type: "string", + description: "This string is optional", + values: [ + { + value: "ksjdlkajs;djálskd'l ;a k;aosdk ;oaids ;li", + count: 1, + }, + { + value: "some other any string here", + count: 1, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "", + name: "Region", + type: "string", + description: "These are options.", + options: [ + { + name: "US Central", + description: "Select for central!", + value: "us-central1-a", + icon: "/icon/goland.svg", + }, + { + name: "US East", + description: "Select for east!", + value: "us-east1-a", + icon: "/icon/folder.svg", + }, + { + name: "US West", + description: "Select for west!", + value: "us-west2-a", + icon: "", + }, + ], + values: [ + { + value: "us-central1-a", + count: 1, + }, + { + value: "us-west2-a", + count: 1, + }, + // Test orphan values + { + value: "us-west-orphan", + count: 1, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "", + name: "Security groups", + type: "list(string)", + description: "Select appropriate security groups.", + values: [ + { + value: + '["Web Server Security Group","Database Security Group","Backend Security Group"]', + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "Very random string", + name: "buggy-1", + type: "string", + description: "This string is buggy", + values: [ + { + value: "", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "Force rebuild", + name: "force-rebuild", + type: "bool", + description: "Rebuild the project code", + values: [ + { + value: "false", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "Location", + name: "location", + type: "string", + description: "What location should your workspace live in?", + options: [ + { + name: "US (Virginia)", + description: "", + value: "eastus", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "US (Virginia) 2", + description: "", + value: "eastus2", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "US (Texas)", + description: "", + value: "southcentralus", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "US (Washington)", + description: "", + value: "westus2", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "US (Arizona)", + description: "", + value: "westus3", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "US (Iowa)", + description: "", + value: "centralus", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "Canada (Toronto)", + description: "", + value: "canadacentral", + icon: "/emojis/1f1e8-1f1e6.png", + }, + { + name: "Brazil (Sao Paulo)", + description: "", + value: "brazilsouth", + icon: "/emojis/1f1e7-1f1f7.png", + }, + { + name: "East Asia (Hong Kong)", + description: "", + value: "eastasia", + icon: "/emojis/1f1f0-1f1f7.png", + }, + { + name: "Southeast Asia (Singapore)", + description: "", + value: "southeastasia", + icon: "/emojis/1f1f0-1f1f7.png", + }, + { + name: "Australia (New South Wales)", + description: "", + value: "australiaeast", + icon: "/emojis/1f1e6-1f1fa.png", + }, + { + name: "China (Hebei)", + description: "", + value: "chinanorth3", + icon: "/emojis/1f1e8-1f1f3.png", + }, + { + name: "India (Pune)", + description: "", + value: "centralindia", + icon: "/emojis/1f1ee-1f1f3.png", + }, + { + name: "Japan (Tokyo)", + description: "", + value: "japaneast", + icon: "/emojis/1f1ef-1f1f5.png", + }, + { + name: "Korea (Seoul)", + description: "", + value: "koreacentral", + icon: "/emojis/1f1f0-1f1f7.png", + }, + { + name: "Europe (Ireland)", + description: "", + value: "northeurope", + icon: "/emojis/1f1ea-1f1fa.png", + }, + { + name: "Europe (Netherlands)", + description: "", + value: "westeurope", + icon: "/emojis/1f1ea-1f1fa.png", + }, + { + name: "France (Paris)", + description: "", + value: "francecentral", + icon: "/emojis/1f1eb-1f1f7.png", + }, + { + name: "Germany (Frankfurt)", + description: "", + value: "germanywestcentral", + icon: "/emojis/1f1e9-1f1ea.png", + }, + { + name: "Norway (Oslo)", + description: "", + value: "norwayeast", + icon: "/emojis/1f1f3-1f1f4.png", + }, + { + name: "Sweden (Gävle)", + description: "", + value: "swedencentral", + icon: "/emojis/1f1f8-1f1ea.png", + }, + { + name: "Switzerland (Zurich)", + description: "", + value: "switzerlandnorth", + icon: "/emojis/1f1e8-1f1ed.png", + }, + { + name: "Qatar (Doha)", + description: "", + value: "qatarcentral", + icon: "/emojis/1f1f6-1f1e6.png", + }, + { + name: "UAE (Dubai)", + description: "", + value: "uaenorth", + icon: "/emojis/1f1e6-1f1ea.png", + }, + { + name: "South Africa (Johannesburg)", + description: "", + value: "southafricanorth", + icon: "/emojis/1f1ff-1f1e6.png", + }, + { + name: "UK (London)", + description: "", + value: "uksouth", + icon: "/emojis/1f1ec-1f1e7.png", + }, + ], + values: [ + { + value: "brazilsouth", + count: 1, + }, + { + value: "switzerlandnorth", + count: 1, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "", + name: "mtojek_region", + type: "string", + description: "What region should your workspace live in?", + options: [ + { + name: "Los Angeles, CA", + description: "", + value: "Los Angeles, CA", + icon: "", + }, + { + name: "Moncks Corner, SC", + description: "", + value: "Moncks Corner, SC", + icon: "", + }, + { + name: "Eemshaven, NL", + description: "", + value: "Eemshaven, NL", + icon: "", + }, + ], + values: [ + { + value: "Los Angeles, CA", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "My Project ID", + name: "project_id", + type: "string", + description: "This is the Project ID.", + values: [ + { + value: "12345", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "Force devcontainer rebuild", + name: "rebuild_devcontainer", + type: "bool", + description: "", + values: [ + { + value: "false", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "Git Repo URL", + name: "repo_url", + type: "string", + description: + "See sample projects (https://github.com/microsoft/vscode-dev-containers#sample-projects)", + values: [ + { + value: "https://github.com/mtojek/coder", + count: 2, + }, + ], + }, + ], + }, + interval_reports: [ { + start_time: "2023-07-18T00:00:00Z", + end_time: "2023-07-19T00:00:00Z", template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - type: "builtin", - display_name: "Visual Studio Code", - slug: "vscode", - icon: "/icon/code.svg", - seconds: 2513400, - times_used: 0, + interval: "day", + active_users: 13, }, { + start_time: "2023-07-19T00:00:00Z", + end_time: "2023-07-20T00:00:00Z", template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - type: "builtin", - display_name: "JetBrains", - slug: "jetbrains", - icon: "/icon/intellij.svg", - seconds: 2013400, - times_used: 20, + interval: "day", + active_users: 11, }, { + start_time: "2023-07-20T00:00:00Z", + end_time: "2023-07-21T00:00:00Z", template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - type: "builtin", - display_name: "Web Terminal", - slug: "reconnecting-pty", - icon: "/icon/terminal.svg", - seconds: 110400, - times_used: 0, + interval: "day", + active_users: 11, }, { + start_time: "2023-07-21T00:00:00Z", + end_time: "2023-07-22T00:00:00Z", template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - type: "builtin", - display_name: "SSH", - slug: "ssh", - icon: "/icon/terminal.svg", - seconds: 1020900, - times_used: 0, - }, - ], - parameters_usage: [ - { - template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], - display_name: "", - name: "Compute instances", - type: "number", - description: "Let's set the expected number of instances.", - values: [ - { - value: "3", - count: 2, - }, - ], - }, - { - template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], - display_name: "", - name: "Docker Image", - type: "string", - description: "Docker image for the development container", - values: [ - { - value: "ghcr.io/harrison-ai/coder-dev:base", - count: 2, - }, - ], - }, - { - template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], - display_name: "Very random string", - name: "Optional random string", - type: "string", - description: "This string is optional", - values: [ - { - value: "ksjdlkajs;djálskd'l ;a k;aosdk ;oaids ;li", - count: 1, - }, - { - value: "some other any string here", - count: 1, - }, - ], + interval: "day", + active_users: 13, }, { - template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], - display_name: "", - name: "Region", - type: "string", - description: "These are options.", - options: [ - { - name: "US Central", - description: "Select for central!", - value: "us-central1-a", - icon: "/icon/goland.svg", - }, - { - name: "US East", - description: "Select for east!", - value: "us-east1-a", - icon: "/icon/folder.svg", - }, - { - name: "US West", - description: "Select for west!", - value: "us-west2-a", - icon: "", - }, - ], - values: [ - { - value: "us-central1-a", - count: 1, - }, - { - value: "us-west2-a", - count: 1, - }, - // Test orphan values - { - value: "us-west-orphan", - count: 1, - }, - ], - }, - { - template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], - display_name: "", - name: "Security groups", - type: "list(string)", - description: "Select appropriate security groups.", - values: [ - { - value: - '["Web Server Security Group","Database Security Group","Backend Security Group"]', - count: 2, - }, - ], + start_time: "2023-07-22T00:00:00Z", + end_time: "2023-07-23T00:00:00Z", + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + interval: "day", + active_users: 7, }, { - template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], - display_name: "Very random string", - name: "buggy-1", - type: "string", - description: "This string is buggy", - values: [ - { - value: "", - count: 2, - }, - ], + start_time: "2023-07-23T00:00:00Z", + end_time: "2023-07-24T00:00:00Z", + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + interval: "day", + active_users: 5, }, { - template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], - display_name: "Force rebuild", - name: "force-rebuild", - type: "bool", - description: "Rebuild the project code", - values: [ - { - value: "false", - count: 2, - }, - ], + start_time: "2023-07-24T00:00:00Z", + end_time: "2023-07-25T00:00:00Z", + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + interval: "day", + active_users: 16, }, - { - template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], - display_name: "Location", - name: "location", - type: "string", - description: "What location should your workspace live in?", - options: [ - { - name: "US (Virginia)", - description: "", - value: "eastus", - icon: "/emojis/1f1fa-1f1f8.png", - }, - { - name: "US (Virginia) 2", - description: "", - value: "eastus2", - icon: "/emojis/1f1fa-1f1f8.png", - }, - { - name: "US (Texas)", - description: "", - value: "southcentralus", - icon: "/emojis/1f1fa-1f1f8.png", - }, - { - name: "US (Washington)", - description: "", - value: "westus2", - icon: "/emojis/1f1fa-1f1f8.png", - }, - { - name: "US (Arizona)", - description: "", - value: "westus3", - icon: "/emojis/1f1fa-1f1f8.png", - }, - { - name: "US (Iowa)", - description: "", - value: "centralus", - icon: "/emojis/1f1fa-1f1f8.png", - }, - { - name: "Canada (Toronto)", - description: "", - value: "canadacentral", - icon: "/emojis/1f1e8-1f1e6.png", - }, - { - name: "Brazil (Sao Paulo)", - description: "", - value: "brazilsouth", - icon: "/emojis/1f1e7-1f1f7.png", - }, - { - name: "East Asia (Hong Kong)", - description: "", - value: "eastasia", - icon: "/emojis/1f1f0-1f1f7.png", - }, - { - name: "Southeast Asia (Singapore)", - description: "", - value: "southeastasia", - icon: "/emojis/1f1f0-1f1f7.png", - }, - { - name: "Australia (New South Wales)", - description: "", - value: "australiaeast", - icon: "/emojis/1f1e6-1f1fa.png", - }, - { - name: "China (Hebei)", - description: "", - value: "chinanorth3", - icon: "/emojis/1f1e8-1f1f3.png", - }, - { - name: "India (Pune)", - description: "", - value: "centralindia", - icon: "/emojis/1f1ee-1f1f3.png", - }, - { - name: "Japan (Tokyo)", - description: "", - value: "japaneast", - icon: "/emojis/1f1ef-1f1f5.png", - }, - { - name: "Korea (Seoul)", - description: "", - value: "koreacentral", - icon: "/emojis/1f1f0-1f1f7.png", - }, - { - name: "Europe (Ireland)", - description: "", - value: "northeurope", - icon: "/emojis/1f1ea-1f1fa.png", - }, - { - name: "Europe (Netherlands)", - description: "", - value: "westeurope", - icon: "/emojis/1f1ea-1f1fa.png", - }, - { - name: "France (Paris)", - description: "", - value: "francecentral", - icon: "/emojis/1f1eb-1f1f7.png", - }, - { - name: "Germany (Frankfurt)", - description: "", - value: "germanywestcentral", - icon: "/emojis/1f1e9-1f1ea.png", - }, - { - name: "Norway (Oslo)", - description: "", - value: "norwayeast", - icon: "/emojis/1f1f3-1f1f4.png", - }, - { - name: "Sweden (Gävle)", - description: "", - value: "swedencentral", - icon: "/emojis/1f1f8-1f1ea.png", - }, - { - name: "Switzerland (Zurich)", - description: "", - value: "switzerlandnorth", - icon: "/emojis/1f1e8-1f1ed.png", + ], + }, + error: null, + }, + userLatency: { + data: { + report: { + start_time: "2023-07-18T00:00:00Z", + end_time: "2023-07-25T00:00:00Z", + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + users: [ + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "0bac0dfd-b086-4b6d-b8ba-789e0eca7451", + username: "kylecarbs", + avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4", + latency_ms: { + p50: 63.826, + p95: 139.328, }, - { - name: "Qatar (Doha)", - description: "", - value: "qatarcentral", - icon: "/emojis/1f1f6-1f1e6.png", + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "12b03f43-1bb7-4fca-967a-585c97f31682", + username: "coadler", + avatar_url: "https://avatars.githubusercontent.com/u/6332295?v=4", + latency_ms: { + p50: 51.0745, + p95: 54.62562499999999, }, - { - name: "UAE (Dubai)", - description: "", - value: "uaenorth", - icon: "/emojis/1f1e6-1f1ea.png", + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "15890ddb-142c-443d-8fd5-cd8307256ab1", + username: "jsjoeio", + avatar_url: "https://avatars.githubusercontent.com/u/3806031?v=4", + latency_ms: { + p50: 37.444, + p95: 37.8488, }, - { - name: "South Africa (Johannesburg)", - description: "", - value: "southafricanorth", - icon: "/emojis/1f1ff-1f1e6.png", + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "3f8c0eef-6a45-4759-a4d6-d00bbffb1369", + username: "dean", + avatar_url: + "https://avatars.githubusercontent.com/u/11241812?v=4", + latency_ms: { + p50: 7.1295, + p95: 70.34084999999999, }, - { - name: "UK (London)", - description: "", - value: "uksouth", - icon: "/emojis/1f1ec-1f1e7.png", + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "59da0bfe-9c99-47fa-a563-f9fdb18449d0", + username: "cian", + avatar_url: + "https://lh3.googleusercontent.com/a/AAcHTtdsYrtIfkXU52rHXhY9DHehpw-slUKe9v6UELLJgXT2mDM=s96-c", + latency_ms: { + p50: 42.14975, + p95: 125.5441, }, - ], - values: [ - { - value: "brazilsouth", - count: 1, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "5ccd3128-cbbb-4cfb-8139-5a1edbb60c71", + username: "bpmct", + avatar_url: + "https://avatars.githubusercontent.com/u/22407953?v=4", + latency_ms: { + p50: 42.175, + p95: 43.437599999999996, }, - { - value: "switzerlandnorth", - count: 1, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "631f78f6-098e-4cb0-ae4f-418fafb0a406", + username: "matifali", + avatar_url: + "https://avatars.githubusercontent.com/u/10648092?v=4", + latency_ms: { + p50: 78.02, + p95: 86.3328, }, - ], - }, - { - template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], - display_name: "", - name: "mtojek_region", - type: "string", - description: "What region should your workspace live in?", - options: [ - { - name: "Los Angeles, CA", - description: "", - value: "Los Angeles, CA", - icon: "", + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "740bba7f-356d-4203-8f15-03ddee381998", + username: "eric", + avatar_url: "https://avatars.githubusercontent.com/u/9683576?v=4", + latency_ms: { + p50: 34.533, + p95: 110.52659999999999, }, - { - name: "Moncks Corner, SC", - description: "", - value: "Moncks Corner, SC", - icon: "", + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "78dd2361-4a5a-42b0-9ec3-3eea23af1094", + username: "code-asher", + avatar_url: + "https://avatars.githubusercontent.com/u/45609798?v=4", + latency_ms: { + p50: 74.78875, + p95: 114.80699999999999, }, - { - name: "Eemshaven, NL", - description: "", - value: "Eemshaven, NL", - icon: "", + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "7f5cc5e9-20ee-48ce-959d-081b3f52273e", + username: "mafredri", + avatar_url: "https://avatars.githubusercontent.com/u/147409?v=4", + latency_ms: { + p50: 19.2115, + p95: 96.44249999999992, }, - ], - values: [ - { - value: "Los Angeles, CA", - count: 2, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "9ed91bb9-db45-4cef-b39c-819856e98c30", + username: "jon", + avatar_url: + "https://lh3.googleusercontent.com/a/AAcHTtddhPxiGYniy6_rFhdAi2C1YwKvDButlCvJ6G-166mG=s96-c", + latency_ms: { + p50: 42.0445, + p95: 133.846, }, - ], - }, - { - template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], - display_name: "My Project ID", - name: "project_id", - type: "string", - description: "This is the Project ID.", - values: [ - { - value: "12345", - count: 2, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "a73425d1-53a7-43d3-b6ae-cae9ba59b92b", + username: "ammar", + avatar_url: "https://avatars.githubusercontent.com/u/7416144?v=4", + latency_ms: { + p50: 49.249, + p95: 56.773250000000004, }, - ], - }, - { - template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], - display_name: "Force devcontainer rebuild", - name: "rebuild_devcontainer", - type: "bool", - description: "", - values: [ - { - value: "false", - count: 2, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "af657bc3-6949-4b1b-bc2d-d41a40b546a4", + username: "BrunoQuaresma", + avatar_url: "https://avatars.githubusercontent.com/u/3165839?v=4", + latency_ms: { + p50: 82.97, + p95: 147.3868, }, - ], - }, - { - template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], - display_name: "Git Repo URL", - name: "repo_url", - type: "string", - description: - "See sample projects (https://github.com/microsoft/vscode-dev-containers#sample-projects)", - values: [ - { - value: "https://github.com/mtojek/coder", - count: 2, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "b006209d-fdd2-4716-afb2-104dafb32dfb", + username: "mtojek", + avatar_url: + "https://avatars.githubusercontent.com/u/14044910?v=4", + latency_ms: { + p50: 36.758, + p95: 101.31679999999983, }, - ], - }, - ], - }, - interval_reports: [ - { - start_time: "2023-07-18T00:00:00Z", - end_time: "2023-07-19T00:00:00Z", - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - interval: "day", - active_users: 13, - }, - { - start_time: "2023-07-19T00:00:00Z", - end_time: "2023-07-20T00:00:00Z", - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - interval: "day", - active_users: 11, - }, - { - start_time: "2023-07-20T00:00:00Z", - end_time: "2023-07-21T00:00:00Z", - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - interval: "day", - active_users: 11, - }, - { - start_time: "2023-07-21T00:00:00Z", - end_time: "2023-07-22T00:00:00Z", - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - interval: "day", - active_users: 13, - }, - { - start_time: "2023-07-22T00:00:00Z", - end_time: "2023-07-23T00:00:00Z", - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - interval: "day", - active_users: 7, - }, - { - start_time: "2023-07-23T00:00:00Z", - end_time: "2023-07-24T00:00:00Z", - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - interval: "day", - active_users: 5, - }, - { - start_time: "2023-07-24T00:00:00Z", - end_time: "2023-07-25T00:00:00Z", - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - interval: "day", - active_users: 16, + }, + ], }, - ], + }, + error: null, }, - userLatency: { - report: { - start_time: "2023-07-18T00:00:00Z", - end_time: "2023-07-25T00:00:00Z", - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - users: [ - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "0bac0dfd-b086-4b6d-b8ba-789e0eca7451", - username: "kylecarbs", - avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4", - latency_ms: { - p50: 63.826, - p95: 139.328, + userActivity: { + data: { + report: { + start_time: "2023-09-03T00:00:00-03:00", + end_time: "2023-10-01T00:00:00-03:00", + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + users: [ + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "0bac0dfd-b086-4b6d-b8ba-789e0eca7451", + username: "kylecarbs", + avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4", + seconds: 671040, }, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "12b03f43-1bb7-4fca-967a-585c97f31682", - username: "coadler", - avatar_url: "https://avatars.githubusercontent.com/u/6332295?v=4", - latency_ms: { - p50: 51.0745, - p95: 54.62562499999999, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "12b03f43-1bb7-4fca-967a-585c97f31682", + username: "coadler", + avatar_url: "https://avatars.githubusercontent.com/u/6332295?v=4", + seconds: 1487460, }, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "15890ddb-142c-443d-8fd5-cd8307256ab1", - username: "jsjoeio", - avatar_url: "https://avatars.githubusercontent.com/u/3806031?v=4", - latency_ms: { - p50: 37.444, - p95: 37.8488, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "15890ddb-142c-443d-8fd5-cd8307256ab1", + username: "jsjoeio", + avatar_url: "https://avatars.githubusercontent.com/u/3806031?v=4", + seconds: 6600, }, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "3f8c0eef-6a45-4759-a4d6-d00bbffb1369", - username: "dean", - avatar_url: "https://avatars.githubusercontent.com/u/11241812?v=4", - latency_ms: { - p50: 7.1295, - p95: 70.34084999999999, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "1c3e3fff-6a0e-4179-9ba3-27f5443e6fce", + username: "Kira-Pilot", + avatar_url: + "https://avatars.githubusercontent.com/u/19142439?v=4", + seconds: 195240, }, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "59da0bfe-9c99-47fa-a563-f9fdb18449d0", - username: "cian", - avatar_url: - "https://lh3.googleusercontent.com/a/AAcHTtdsYrtIfkXU52rHXhY9DHehpw-slUKe9v6UELLJgXT2mDM=s96-c", - latency_ms: { - p50: 42.14975, - p95: 125.5441, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "2e1e7f76-ae77-424a-a209-f35a99731ec9", + username: "phorcys420", + avatar_url: + "https://avatars.githubusercontent.com/u/57866459?v=4", + seconds: 16320, }, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "5ccd3128-cbbb-4cfb-8139-5a1edbb60c71", - username: "bpmct", - avatar_url: "https://avatars.githubusercontent.com/u/22407953?v=4", - latency_ms: { - p50: 42.175, - p95: 43.437599999999996, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "3f8c0eef-6a45-4759-a4d6-d00bbffb1369", + username: "dean", + avatar_url: + "https://avatars.githubusercontent.com/u/11241812?v=4", + seconds: 533520, }, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "631f78f6-098e-4cb0-ae4f-418fafb0a406", - username: "matifali", - avatar_url: "https://avatars.githubusercontent.com/u/10648092?v=4", - latency_ms: { - p50: 78.02, - p95: 86.3328, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "59da0bfe-9c99-47fa-a563-f9fdb18449d0", + username: "cian", + avatar_url: + "https://lh3.googleusercontent.com/a/ACg8ocKKaBWosY_nuQvecIaUPh5RYjxkEN-C8FNGVPlC0Ch2fx0=s96-c", + seconds: 607080, }, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "740bba7f-356d-4203-8f15-03ddee381998", - username: "eric", - avatar_url: "https://avatars.githubusercontent.com/u/9683576?v=4", - latency_ms: { - p50: 34.533, - p95: 110.52659999999999, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "5ccd3128-cbbb-4cfb-8139-5a1edbb60c71", + username: "bpmct", + avatar_url: + "https://avatars.githubusercontent.com/u/22407953?v=4", + seconds: 161340, }, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "78dd2361-4a5a-42b0-9ec3-3eea23af1094", - username: "code-asher", - avatar_url: "https://avatars.githubusercontent.com/u/45609798?v=4", - latency_ms: { - p50: 74.78875, - p95: 114.80699999999999, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "631f78f6-098e-4cb0-ae4f-418fafb0a406", + username: "matifali", + avatar_url: + "https://avatars.githubusercontent.com/u/10648092?v=4", + seconds: 202500, }, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "7f5cc5e9-20ee-48ce-959d-081b3f52273e", - username: "mafredri", - avatar_url: "https://avatars.githubusercontent.com/u/147409?v=4", - latency_ms: { - p50: 19.2115, - p95: 96.44249999999992, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "740bba7f-356d-4203-8f15-03ddee381998", + username: "eric", + avatar_url: "https://avatars.githubusercontent.com/u/9683576?v=4", + seconds: 352680, }, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "9ed91bb9-db45-4cef-b39c-819856e98c30", - username: "jon", - avatar_url: - "https://lh3.googleusercontent.com/a/AAcHTtddhPxiGYniy6_rFhdAi2C1YwKvDButlCvJ6G-166mG=s96-c", - latency_ms: { - p50: 42.0445, - p95: 133.846, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "78dd2361-4a5a-42b0-9ec3-3eea23af1094", + username: "code-asher", + avatar_url: + "https://avatars.githubusercontent.com/u/45609798?v=4", + seconds: 518640, }, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "a73425d1-53a7-43d3-b6ae-cae9ba59b92b", - username: "ammar", - avatar_url: "https://avatars.githubusercontent.com/u/7416144?v=4", - latency_ms: { - p50: 49.249, - p95: 56.773250000000004, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "7f5cc5e9-20ee-48ce-959d-081b3f52273e", + username: "mafredri", + avatar_url: "https://avatars.githubusercontent.com/u/147409?v=4", + seconds: 218100, }, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "af657bc3-6949-4b1b-bc2d-d41a40b546a4", - username: "BrunoQuaresma", - avatar_url: "https://avatars.githubusercontent.com/u/3165839?v=4", - latency_ms: { - p50: 82.97, - p95: 147.3868, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "8b474a55-d414-4b53-a6ba-760f3d4eed7b", + username: "kirby", + avatar_url: + "https://lh3.googleusercontent.com/a-/ALV-UjUHd9l3CaO99BfVlP8L9D9HqKFOUac7zVCA_Bb_2lj0hcPkQvHkMk4HRaMw4b1YF7E-uHnJO-w8sXf3pqRA2EUP9sDvX6ITd2S2YN23kttVCJKTiI-YEIS8eVDfrF8YLqjfKL3PWsxyiPcgtcdfmPiEnlh4mpUMRXZudwtINfk0W3B9KEpwJTpipdlb57HdYO-mD3DEfmwnpZIO_iVjwnpWZZimXH5g15NVregb8VH_vlsW-vHrMsZ1fRGpm6GWnTcWx2rTImz5Qq5dd15MPKYUxc4wpyYImg07eD41ShzHDJhmDaj_n3hjOwFLuyloLBck-t9skQLWf2r7Voq42jVhzJ2-GAv9atC41_ohG1kq8TpCf9ak6S4hE3xMIB4yzDC0VZxl-BlsBHCuKBRTwC-58yTL2GZI31a0Q9PpR720AyiZaOWhX1QOVZmPZey8b8SG7jWTOfzNa9Shf9E0pz3yyIxFx7KSY5Qeye5AmO1au-rXuWr4whXXY6fsn0tnG4nxdyetCiXd0mOmvYHoJuuQFfqYNjdObduRD0yaVZGL-hPFDYH6K-wiedT1y-66jKXcqjVqe0Rwo7YzcVcP-IeV5RGuJ36TEpC1lhi2V-AnG7pmvIn_4AmXfycclrISO10LgQsrx8bxeBW61t9oTFTZCXXBDAd9bLRxndLi_mWYEfOSnWODgfCrapL_GNZsV0tkQ9x-zvlSXQXtze5bg__uAo7CEnZ20yWT5Gr25_NPsH6vyR3hplKn67qBti5_rKzFQ1sVbcuab2BRmF_Al9MTQw-R2gmd0mle9JRr8tyuwCYh82mBrM-dGebXSdqvabws7_WmF5TNwDHHzeeiHq1_6FYB0tBldx3yWk3U8olZ3SiPAe_NRnY0vUKI3ZANOA-IRYxyTAfjShJE0fRMCe70BsqzJj3RDAciqt5IaP2vZQeImjPZLd2NGo-Bbw=s96-c", + seconds: 543960, }, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "b006209d-fdd2-4716-afb2-104dafb32dfb", - username: "mtojek", - avatar_url: "https://avatars.githubusercontent.com/u/14044910?v=4", - latency_ms: { - p50: 36.758, - p95: 101.31679999999983, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "9ed91bb9-db45-4cef-b39c-819856e98c30", + username: "jon", + avatar_url: + "https://lh3.googleusercontent.com/a/ACg8ocJEE9R4__Pdh40DHGD-3noKezyw-1qo2auV_cb2gxBg=s96-c", + seconds: 464100, }, - }, - ], - }, - }, - userActivity: { - report: { - start_time: "2023-09-03T00:00:00-03:00", - end_time: "2023-10-01T00:00:00-03:00", - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - users: [ - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "0bac0dfd-b086-4b6d-b8ba-789e0eca7451", - username: "kylecarbs", - avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4", - seconds: 671040, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "12b03f43-1bb7-4fca-967a-585c97f31682", - username: "coadler", - avatar_url: "https://avatars.githubusercontent.com/u/6332295?v=4", - seconds: 1487460, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "15890ddb-142c-443d-8fd5-cd8307256ab1", - username: "jsjoeio", - avatar_url: "https://avatars.githubusercontent.com/u/3806031?v=4", - seconds: 6600, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "1c3e3fff-6a0e-4179-9ba3-27f5443e6fce", - username: "Kira-Pilot", - avatar_url: "https://avatars.githubusercontent.com/u/19142439?v=4", - seconds: 195240, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "2e1e7f76-ae77-424a-a209-f35a99731ec9", - username: "phorcys420", - avatar_url: "https://avatars.githubusercontent.com/u/57866459?v=4", - seconds: 16320, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "3f8c0eef-6a45-4759-a4d6-d00bbffb1369", - username: "dean", - avatar_url: "https://avatars.githubusercontent.com/u/11241812?v=4", - seconds: 533520, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "59da0bfe-9c99-47fa-a563-f9fdb18449d0", - username: "cian", - avatar_url: - "https://lh3.googleusercontent.com/a/ACg8ocKKaBWosY_nuQvecIaUPh5RYjxkEN-C8FNGVPlC0Ch2fx0=s96-c", - seconds: 607080, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "5ccd3128-cbbb-4cfb-8139-5a1edbb60c71", - username: "bpmct", - avatar_url: "https://avatars.githubusercontent.com/u/22407953?v=4", - seconds: 161340, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "631f78f6-098e-4cb0-ae4f-418fafb0a406", - username: "matifali", - avatar_url: "https://avatars.githubusercontent.com/u/10648092?v=4", - seconds: 202500, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "740bba7f-356d-4203-8f15-03ddee381998", - username: "eric", - avatar_url: "https://avatars.githubusercontent.com/u/9683576?v=4", - seconds: 352680, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "78dd2361-4a5a-42b0-9ec3-3eea23af1094", - username: "code-asher", - avatar_url: "https://avatars.githubusercontent.com/u/45609798?v=4", - seconds: 518640, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "7f5cc5e9-20ee-48ce-959d-081b3f52273e", - username: "mafredri", - avatar_url: "https://avatars.githubusercontent.com/u/147409?v=4", - seconds: 218100, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "8b474a55-d414-4b53-a6ba-760f3d4eed7b", - username: "kirby", - avatar_url: - "https://lh3.googleusercontent.com/a-/ALV-UjUHd9l3CaO99BfVlP8L9D9HqKFOUac7zVCA_Bb_2lj0hcPkQvHkMk4HRaMw4b1YF7E-uHnJO-w8sXf3pqRA2EUP9sDvX6ITd2S2YN23kttVCJKTiI-YEIS8eVDfrF8YLqjfKL3PWsxyiPcgtcdfmPiEnlh4mpUMRXZudwtINfk0W3B9KEpwJTpipdlb57HdYO-mD3DEfmwnpZIO_iVjwnpWZZimXH5g15NVregb8VH_vlsW-vHrMsZ1fRGpm6GWnTcWx2rTImz5Qq5dd15MPKYUxc4wpyYImg07eD41ShzHDJhmDaj_n3hjOwFLuyloLBck-t9skQLWf2r7Voq42jVhzJ2-GAv9atC41_ohG1kq8TpCf9ak6S4hE3xMIB4yzDC0VZxl-BlsBHCuKBRTwC-58yTL2GZI31a0Q9PpR720AyiZaOWhX1QOVZmPZey8b8SG7jWTOfzNa9Shf9E0pz3yyIxFx7KSY5Qeye5AmO1au-rXuWr4whXXY6fsn0tnG4nxdyetCiXd0mOmvYHoJuuQFfqYNjdObduRD0yaVZGL-hPFDYH6K-wiedT1y-66jKXcqjVqe0Rwo7YzcVcP-IeV5RGuJ36TEpC1lhi2V-AnG7pmvIn_4AmXfycclrISO10LgQsrx8bxeBW61t9oTFTZCXXBDAd9bLRxndLi_mWYEfOSnWODgfCrapL_GNZsV0tkQ9x-zvlSXQXtze5bg__uAo7CEnZ20yWT5Gr25_NPsH6vyR3hplKn67qBti5_rKzFQ1sVbcuab2BRmF_Al9MTQw-R2gmd0mle9JRr8tyuwCYh82mBrM-dGebXSdqvabws7_WmF5TNwDHHzeeiHq1_6FYB0tBldx3yWk3U8olZ3SiPAe_NRnY0vUKI3ZANOA-IRYxyTAfjShJE0fRMCe70BsqzJj3RDAciqt5IaP2vZQeImjPZLd2NGo-Bbw=s96-c", - seconds: 543960, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "9ed91bb9-db45-4cef-b39c-819856e98c30", - username: "jon", - avatar_url: - "https://lh3.googleusercontent.com/a/ACg8ocJEE9R4__Pdh40DHGD-3noKezyw-1qo2auV_cb2gxBg=s96-c", - seconds: 464100, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "a73425d1-53a7-43d3-b6ae-cae9ba59b92b", - username: "ammar", - avatar_url: "https://avatars.githubusercontent.com/u/7416144?v=4", - seconds: 316200, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "af657bc3-6949-4b1b-bc2d-d41a40b546a4", - username: "BrunoQuaresma", - avatar_url: "https://avatars.githubusercontent.com/u/3165839?v=4", - seconds: 329100, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "b006209d-fdd2-4716-afb2-104dafb32dfb", - username: "mtojek", - avatar_url: "https://avatars.githubusercontent.com/u/14044910?v=4", - seconds: 11520, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "b3e1b884-1a5b-44eb-b8b3-423f8eddc503", - username: "spikecurtis", - avatar_url: "https://avatars.githubusercontent.com/u/5375600?v=4", - seconds: 523140, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "baf63990-16a9-472f-b715-64e24c6bcefb", - username: "atif", - avatar_url: - "https://lh3.googleusercontent.com/a-/ALV-UjVWiI2I5XOkxxi5KwAyfzZlfcOSYlMw8dIwJVwg2satTlOaLUy2PXcFcHCYtMg41DImXlB4F7YFIEW-CR_ANiCol7LnHTFomTyeh5N4ZvVQ4rx_sCl3PARywl0-UBW6usVGRVB8CnHve95q4ZDzJA6wJGVvr7gceCpgGe2A2597_KM1L5KIWKr5SAn41AZgQHZc7pgYJtiyKNleDN8LYzmceOtR3GJgFKjMrSOczNLNI3S2TrRPmIBIr_pZFDI3_npKDmQu9fPiVip5RDTAsuP9PdqruNJ4rB0rBae4Gog-RhqUV4L_i01-bJ6aepjH9gqxEkHHkXi7W0ldH8uV2fsQ4Eul78OQp0NrWxx9xZmFseTPK0toiop3EAWnuyp5ikaAnLodtvJ8L3iZXh45LvDv1ADESYPVAeuyHY5eee54O5xy72HABVB_UTE45Zhq086i4zaTNZoObXPrgiU3uNo0EhDQKa2jPNY2oQO0oZa991Oo9zCT9AULz5RP_3GTnfRMgD8ofCKr8Y3dVmSGI0RYOMI5Yqi76sEROCT5LqwAqRTFeGSMIF7-VI9qCctCtZ50n0OVtbFjPCgUGFVN1gZxe2qb66XCQnZOklTaMadj7KvtgIIJFlBSZJLkoPhSyIdiUAOp3VpDn8jOuEI0109YHzEM7l5KFNL-cHxQQyYB9hquld6y6EVRJdro8uVQdwkZ-_Yu4oD70A-WLb-Gi5RLdbB1iFwr99Lg-l4HNDWhh0h1wT5yhn4kgjPMgeTNT7F6fkiteAIvK_jJjVVh-PtKTt48kPv9c7rbc_jCBP70zUQ9X4Xxf9917BPUfvMgLk0gShSaFXxAGTgA7TzRaEsWSi9_DuJ0Q-yQZXwCJ1Y_1VrSF9B2FKsrugotVoC5BORu9tiaWi9jRP6RymM2X0HxsLv0lUFFVjgV0SZnynBNCgqyS02xAs8vEYpw-T7RJg=s96-c", - seconds: 2040, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "c0240345-f14a-4632-b713-a0f09c2ed927", - username: "asher-user", - avatar_url: "", - seconds: 0, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "c5eb8310-cf4f-444c-b223-0e991f828b40", - username: "Emyrk", - avatar_url: "https://avatars.githubusercontent.com/u/5446298?v=4", - seconds: 24540, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "d96bf761-3f94-46b3-a1da-6316e2e4735d", - username: "aslilac", - avatar_url: "https://avatars.githubusercontent.com/u/418348?v=4", - seconds: 824820, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "db38462b-e63d-4304-87e9-2640eea4a3b8", - username: "marc", - avatar_url: "", - seconds: 120, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "e9b24091-a633-4ba0-9746-ca325a86f0f5", - username: "michaelsmith", - avatar_url: - "https://lh3.googleusercontent.com/a-/ALV-UjXDMd9gEl4aMyM5ENfj4Ruzzn57AETWW6UuNC3Od03Y3AjvrCDhp8iE4I8L0393C_peQF9PZQyVklGCW-FCzODkvyVojUFqafbFi6AtvxjKn59ZyUVtG0ELoDNZOtRQaqUuMNIjtafNQ19LgwYm7LSB47My__oafDZ6jw6Kd_H-qtx19Vh62t3ACoJBHpDrF0BdDxWGBCkUAlC8aJcnqdRqPbKB5WGGcEfwLzrhLc5REN4CuXzm09_ZpU2jdvMUKCBX9H_8j2wcPwtgY0JG0DfIOX_VgTdM6Zy7BLiVQHSjD-uSkwqOEoXvsuKWlEBt74rqjyNDjjM1NyHiUdKpUd26hI2jcro_yrf4Jli7MCf5SjnkGMxQCgrD6-D9bcyBNzXpc1_5mDWrGpSh0X6pVK6GsmuYAc68hfTIHYVs-jB97mls9ClOJ2m51AdOAlizT80Ram2yJ09l-YbTVd4fG3L9FajMsvRhcvwwvN5tGcOk36KcIm0wFy9NQyH09QP3M1Rr2kDn9MzYYuyAZ9Um0tZydrPN9FA59JUytq8GtwnZZVmlZk2X2fXsCgJBv3dCwuF3THqSvL0M3lQa89-slrp2qgSRekiCmbb0-b62T413mOA9KNXcCvct_NN-JAE0b6o7To8B1WW8-AZiFQ2DesSEXL-CWYfqfecs4hoIrSBnQLa3Pm2Q5O-R7R99eRD7H3EqPihl_TiG2s_8gvLUF7ft55hYkV0j-YzTS4nOnUtEAXSqN-JYAd_BTJPJ0kyJLGIScwUQGoNFUQYs5nmlKPepeNpoQYYpQe0zK4ZVYm6fnRXUgv1cWvkD5RuxbBs1kgoVyZrZSNco8apuIjg6sBejRJFre_m0N6emp-Jn5wIkFB1f6IRb7S1aPvCqrqgqI8mTcI6Z-4Z3E3YwiYsn8_zVF9EPa1f1zpzeoppGd_YKaAxLjyOv_nC15bN3eio43A=s96-c", - seconds: 449820, - }, - { - template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], - user_id: "fdc2dab9-dabd-4980-843f-2e93042db566", - username: "sharkymark", - avatar_url: "https://avatars.githubusercontent.com/u/2022166?v=4", - seconds: 124440, - }, - ], + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "a73425d1-53a7-43d3-b6ae-cae9ba59b92b", + username: "ammar", + avatar_url: "https://avatars.githubusercontent.com/u/7416144?v=4", + seconds: 316200, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "af657bc3-6949-4b1b-bc2d-d41a40b546a4", + username: "BrunoQuaresma", + avatar_url: "https://avatars.githubusercontent.com/u/3165839?v=4", + seconds: 329100, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "b006209d-fdd2-4716-afb2-104dafb32dfb", + username: "mtojek", + avatar_url: + "https://avatars.githubusercontent.com/u/14044910?v=4", + seconds: 11520, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "b3e1b884-1a5b-44eb-b8b3-423f8eddc503", + username: "spikecurtis", + avatar_url: "https://avatars.githubusercontent.com/u/5375600?v=4", + seconds: 523140, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "baf63990-16a9-472f-b715-64e24c6bcefb", + username: "atif", + avatar_url: + "https://lh3.googleusercontent.com/a-/ALV-UjVWiI2I5XOkxxi5KwAyfzZlfcOSYlMw8dIwJVwg2satTlOaLUy2PXcFcHCYtMg41DImXlB4F7YFIEW-CR_ANiCol7LnHTFomTyeh5N4ZvVQ4rx_sCl3PARywl0-UBW6usVGRVB8CnHve95q4ZDzJA6wJGVvr7gceCpgGe2A2597_KM1L5KIWKr5SAn41AZgQHZc7pgYJtiyKNleDN8LYzmceOtR3GJgFKjMrSOczNLNI3S2TrRPmIBIr_pZFDI3_npKDmQu9fPiVip5RDTAsuP9PdqruNJ4rB0rBae4Gog-RhqUV4L_i01-bJ6aepjH9gqxEkHHkXi7W0ldH8uV2fsQ4Eul78OQp0NrWxx9xZmFseTPK0toiop3EAWnuyp5ikaAnLodtvJ8L3iZXh45LvDv1ADESYPVAeuyHY5eee54O5xy72HABVB_UTE45Zhq086i4zaTNZoObXPrgiU3uNo0EhDQKa2jPNY2oQO0oZa991Oo9zCT9AULz5RP_3GTnfRMgD8ofCKr8Y3dVmSGI0RYOMI5Yqi76sEROCT5LqwAqRTFeGSMIF7-VI9qCctCtZ50n0OVtbFjPCgUGFVN1gZxe2qb66XCQnZOklTaMadj7KvtgIIJFlBSZJLkoPhSyIdiUAOp3VpDn8jOuEI0109YHzEM7l5KFNL-cHxQQyYB9hquld6y6EVRJdro8uVQdwkZ-_Yu4oD70A-WLb-Gi5RLdbB1iFwr99Lg-l4HNDWhh0h1wT5yhn4kgjPMgeTNT7F6fkiteAIvK_jJjVVh-PtKTt48kPv9c7rbc_jCBP70zUQ9X4Xxf9917BPUfvMgLk0gShSaFXxAGTgA7TzRaEsWSi9_DuJ0Q-yQZXwCJ1Y_1VrSF9B2FKsrugotVoC5BORu9tiaWi9jRP6RymM2X0HxsLv0lUFFVjgV0SZnynBNCgqyS02xAs8vEYpw-T7RJg=s96-c", + seconds: 2040, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "c0240345-f14a-4632-b713-a0f09c2ed927", + username: "asher-user", + avatar_url: "", + seconds: 0, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "c5eb8310-cf4f-444c-b223-0e991f828b40", + username: "Emyrk", + avatar_url: "https://avatars.githubusercontent.com/u/5446298?v=4", + seconds: 24540, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "d96bf761-3f94-46b3-a1da-6316e2e4735d", + username: "aslilac", + avatar_url: "https://avatars.githubusercontent.com/u/418348?v=4", + seconds: 824820, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "db38462b-e63d-4304-87e9-2640eea4a3b8", + username: "marc", + avatar_url: "", + seconds: 120, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "e9b24091-a633-4ba0-9746-ca325a86f0f5", + username: "michaelsmith", + avatar_url: + "https://lh3.googleusercontent.com/a-/ALV-UjXDMd9gEl4aMyM5ENfj4Ruzzn57AETWW6UuNC3Od03Y3AjvrCDhp8iE4I8L0393C_peQF9PZQyVklGCW-FCzODkvyVojUFqafbFi6AtvxjKn59ZyUVtG0ELoDNZOtRQaqUuMNIjtafNQ19LgwYm7LSB47My__oafDZ6jw6Kd_H-qtx19Vh62t3ACoJBHpDrF0BdDxWGBCkUAlC8aJcnqdRqPbKB5WGGcEfwLzrhLc5REN4CuXzm09_ZpU2jdvMUKCBX9H_8j2wcPwtgY0JG0DfIOX_VgTdM6Zy7BLiVQHSjD-uSkwqOEoXvsuKWlEBt74rqjyNDjjM1NyHiUdKpUd26hI2jcro_yrf4Jli7MCf5SjnkGMxQCgrD6-D9bcyBNzXpc1_5mDWrGpSh0X6pVK6GsmuYAc68hfTIHYVs-jB97mls9ClOJ2m51AdOAlizT80Ram2yJ09l-YbTVd4fG3L9FajMsvRhcvwwvN5tGcOk36KcIm0wFy9NQyH09QP3M1Rr2kDn9MzYYuyAZ9Um0tZydrPN9FA59JUytq8GtwnZZVmlZk2X2fXsCgJBv3dCwuF3THqSvL0M3lQa89-slrp2qgSRekiCmbb0-b62T413mOA9KNXcCvct_NN-JAE0b6o7To8B1WW8-AZiFQ2DesSEXL-CWYfqfecs4hoIrSBnQLa3Pm2Q5O-R7R99eRD7H3EqPihl_TiG2s_8gvLUF7ft55hYkV0j-YzTS4nOnUtEAXSqN-JYAd_BTJPJ0kyJLGIScwUQGoNFUQYs5nmlKPepeNpoQYYpQe0zK4ZVYm6fnRXUgv1cWvkD5RuxbBs1kgoVyZrZSNco8apuIjg6sBejRJFre_m0N6emp-Jn5wIkFB1f6IRb7S1aPvCqrqgqI8mTcI6Z-4Z3E3YwiYsn8_zVF9EPa1f1zpzeoppGd_YKaAxLjyOv_nC15bN3eio43A=s96-c", + seconds: 449820, + }, + { + template_ids: ["0d286645-29aa-4eaf-9b52-cc5d2740c90b"], + user_id: "fdc2dab9-dabd-4980-843f-2e93042db566", + username: "sharkymark", + avatar_url: "https://avatars.githubusercontent.com/u/2022166?v=4", + seconds: 124440, + }, + ], + }, }, + error: null, }, }, }; diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index 47e8f951eb369..58fcfc8bb5e33 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -1,6 +1,7 @@ import { useTheme } from "@emotion/react"; import LinearProgress from "@mui/material/LinearProgress"; import Link from "@mui/material/Link"; +import { getErrorDetail, getErrorMessage } from "api/errors"; import { entitlements } from "api/queries/entitlements"; import { insightsTemplate, @@ -95,9 +96,9 @@ export default function TemplateInsightsPage() { }; const insightsFilter = { ...commonFilters, interval }; - const { data: templateInsights } = useQuery(insightsTemplate(insightsFilter)); - const { data: userLatency } = useQuery(insightsUserLatency(commonFilters)); - const { data: userActivity } = useQuery(insightsUserActivity(commonFilters)); + const templateInsights = useQuery(insightsTemplate(insightsFilter)); + const userLatency = useQuery(insightsUserLatency(commonFilters)); + const userActivity = useQuery(insightsUserActivity(commonFilters)); const { metadata } = useEmbeddedMetadata(); const { data: entitlementsQuery } = useQuery( @@ -202,9 +203,18 @@ const getDateRange = ( }; interface TemplateInsightsPageViewProps { - templateInsights: TemplateInsightsResponse | undefined; - userLatency: UserLatencyInsightsResponse | undefined; - userActivity: UserActivityInsightsResponse | undefined; + templateInsights: { + data: TemplateInsightsResponse | undefined; + error: unknown; + }; + userLatency: { + data: UserLatencyInsightsResponse | undefined; + error: unknown; + }; + userActivity: { + data: UserActivityInsightsResponse | undefined; + error: unknown; + }; entitlements: Entitlements | undefined; controls: ReactNode; interval: InsightsInterval; @@ -246,17 +256,26 @@ export const TemplateInsightsPageView: FC = ({ ? entitlements?.features.user_limit.limit : undefined } - data={templateInsights?.interval_reports} + data={templateInsights?.data?.interval_reports} + error={templateInsights?.error} + /> + - + - @@ -265,12 +284,14 @@ export const TemplateInsightsPageView: FC = ({ interface ActiveUsersPanelProps extends PanelProps { data: TemplateInsightsResponse["interval_reports"] | undefined; + error: unknown; interval: InsightsInterval; userLimit: number | undefined; } const ActiveUsersPanel: FC = ({ data, + error, interval, userLimit, ...panelProps @@ -283,8 +304,8 @@ const ActiveUsersPanel: FC = ({ - {!data && } - {data && data.length === 0 && } + {!error && !data && } + {(error || data?.length === 0) && } {data && data.length > 0 && ( ({ @@ -300,10 +321,12 @@ const ActiveUsersPanel: FC = ({ interface UsersLatencyPanelProps extends PanelProps { data: UserLatencyInsightsResponse | undefined; + error: unknown; } const UsersLatencyPanel: FC = ({ data, + error, ...panelProps }) => { const theme = useTheme(); @@ -327,8 +350,8 @@ const UsersLatencyPanel: FC = ({ - {!data && } - {users && users.length === 0 && } + {!error && !users && } + {(error || users?.length === 0) && } {users && [...users] .sort((a, b) => b.latency_ms.p50 - a.latency_ms.p50) @@ -367,10 +390,12 @@ const UsersLatencyPanel: FC = ({ interface UsersActivityPanelProps extends PanelProps { data: UserActivityInsightsResponse | undefined; + error: unknown; } const UsersActivityPanel: FC = ({ data, + error, ...panelProps }) => { const theme = useTheme(); @@ -395,8 +420,8 @@ const UsersActivityPanel: FC = ({ - {!data && } - {users && users.length === 0 && } + {!error && !users && } + {(error || users?.length === 0) && } {users && [...users] .sort((a, b) => b.seconds - a.seconds) @@ -434,13 +459,16 @@ const UsersActivityPanel: FC = ({ interface TemplateUsagePanelProps extends PanelProps { data: readonly TemplateAppUsage[] | undefined; + error: unknown; } const TemplateUsagePanel: FC = ({ data, + error, ...panelProps }) => { const theme = useTheme(); + // The API returns a row for each app, even if the user didn't use it. const validUsage = data ?.filter((u) => u.seconds > 0) .sort((a, b) => b.seconds - a.seconds); @@ -450,8 +478,6 @@ const TemplateUsagePanel: FC = ({ .scale([theme.roles.success.fill.solid, theme.roles.warning.fill.solid]) .mode("lch") .colors(validUsage?.length ?? 0); - // The API returns a row for each app, even if the user didn't use it. - const hasDataAvailable = validUsage && validUsage.length > 0; return ( @@ -459,9 +485,11 @@ const TemplateUsagePanel: FC = ({ App & IDE Usage - {!data && } - {data && !hasDataAvailable && } - {data && hasDataAvailable && ( + {!error && !data && } + {(error || validUsage?.length === 0) && ( + + )} + {validUsage && validUsage.length > 0 && (
= ({ interface TemplateParametersUsagePanelProps extends PanelProps { data: readonly TemplateParameterUsage[] | undefined; + error: unknown; } const TemplateParametersUsagePanel: FC = ({ data, + error, ...panelProps }) => { const theme = useTheme(); @@ -570,82 +600,80 @@ const TemplateParametersUsagePanel: FC = ({ Parameters usage - {!data && } - {data && data.length === 0 && } - {data && - data.length > 0 && - data.map((parameter, parameterIndex) => { - const label = - parameter.display_name !== "" - ? parameter.display_name - : parameter.name; - return ( -
-
-
{label}
-

- {parameter.description} -

-
-
- -
Value
- - -
Count
-
- - The number of workspaces using this value - -
-
- {[...parameter.values] - .sort((a, b) => b.count - a.count) - .filter((usage) => filterOrphanValues(usage, parameter)) - .map((usage, usageIndex) => ( - - -
{usage.count}
-
- ))} -
+ {!error && !data && } + {(error || data?.length === 0) && ( + + )} + {data?.map((parameter, parameterIndex) => { + const label = + parameter.display_name !== "" + ? parameter.display_name + : parameter.name; + return ( +
+
+
{label}
+

+ {parameter.description} +

+
+
+ +
Value
+ + +
Count
+
+ + The number of workspaces using this value + +
+
+ {[...parameter.values] + .sort((a, b) => b.count - a.count) + .filter((usage) => filterOrphanValues(usage, parameter)) + .map((usage, usageIndex) => ( + + +
{usage.count}
+
+ ))}
- ); - })} +
+ ); + })} ); @@ -850,7 +878,11 @@ const PanelContent: FC> = ({ ); }; -const NoDataAvailable = (props: HTMLAttributes) => { +interface NoDataAvailableProps extends HTMLAttributes { + error: unknown; +} + +const NoDataAvailable: FC = ({ error, ...props }) => { const theme = useTheme(); return ( @@ -866,7 +898,10 @@ const NoDataAvailable = (props: HTMLAttributes) => { justifyContent: "center", }} > - No data available + {error + ? getErrorDetail(error) || + getErrorMessage(error, "Unable to fetch insights") + : "No data available"}
); }; From 7076956b24efcecb44b375afb9ac5b43d31d3d1e Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 10 Dec 2025 16:55:43 -0900 Subject: [PATCH 5/9] Rename to templateInsights.enable and improve description --- cli/testdata/coder_server_--help.golden | 16 ++++++----- cli/testdata/server-config.yaml.golden | 13 ++++++--- coderd/apidoc/docs.go | 14 +++++++--- coderd/apidoc/swagger.json | 14 +++++++--- coderd/coderd.go | 4 +-- coderd/insights_test.go | 4 ++- codersdk/deployment.go | 27 ++++++++++++------- docs/reference/api/general.md | 4 ++- docs/reference/api/schemas.md | 24 ++++++++++++++--- docs/reference/cli/server.md | 10 +++---- .../cli/testdata/coder_server_--help.golden | 16 ++++++----- site/src/api/typesGenerated.ts | 7 ++++- 12 files changed, 109 insertions(+), 44 deletions(-) diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 9042b7b54622a..13e51ad4c0459 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -223,12 +223,6 @@ Configure TLS for your SMTP server target. --email-tls-starttls bool, $CODER_EMAIL_TLS_STARTTLS Enable STARTTLS to upgrade insecure SMTP connections using TLS. -INTROSPECTION OPTIONS: -Configure logging, tracing, and metrics exporting. - - --disable-template-insights bool, $CODER_DISABLE_TEMPLATE_INSIGHTS (default: false) - Disable storage and display of template insights. - INTROSPECTION / HEALTH CHECK OPTIONS: --health-check-refresh duration, $CODER_HEALTH_CHECK_REFRESH (default: 10m0s) Refresh interval for healthchecks. @@ -275,6 +269,16 @@ INTROSPECTION / PROMETHEUS OPTIONS: --prometheus-enable bool, $CODER_PROMETHEUS_ENABLE Serve prometheus metrics on the address defined by prometheus address. +INTROSPECTION / TEMPLATE INSIGHTS OPTIONS: + --template-insights-enable bool, $CODER_TEMPLATE_INSIGHTS_ENABLE (default: true) + Enable the collection and display of template insights along with the + associated API endpoints. This will also enable aggregating these + insights into daily active users, application usage, and transmission + rates for overall deployment stats. When disabled, these values will + be zero, which will also affect what the bottom deployment overview + bar displays. Disabling will also prevent Prometheus collection of + these values. + INTROSPECTION / TRACING OPTIONS: --trace-logs bool, $CODER_TRACE_LOGS Enables capturing of logs as events in traces. This is useful for diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index d1c10e42ab854..f9cede4035125 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -190,11 +190,16 @@ autobuildPollInterval: 1m0s # Interval to poll for hung and pending jobs and automatically terminate them. # (default: 1m0s, type: duration) jobHangDetectorInterval: 1m0s -# Configure logging, tracing, and metrics exporting. introspection: - # Disable storage and display of template insights. - # (default: false, type: bool) - disableTemplateInsights: false + templateInsights: + # Enable the collection and display of template insights along with the associated + # API endpoints. This will also enable aggregating these insights into daily + # active users, application usage, and transmission rates for overall deployment + # stats. When disabled, these values will be zero, which will also affect what the + # bottom deployment overview bar displays. Disabling will also prevent Prometheus + # collection of these values. + # (default: true, type: bool) + enable: true prometheus: # Serve prometheus metrics on the address defined by prometheus address. # (default: , type: bool) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index fe2e6102a328f..cb3639791d9cd 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -14214,9 +14214,6 @@ const docTemplate = `{ "disable_path_apps": { "type": "boolean" }, - "disable_template_insights": { - "type": "boolean" - }, "disable_workspace_sharing": { "type": "boolean" }, @@ -14344,6 +14341,9 @@ const docTemplate = `{ "telemetry": { "$ref": "#/definitions/codersdk.TelemetryConfig" }, + "template_insights": { + "$ref": "#/definitions/codersdk.TemplateInsightsConfig" + }, "terms_of_service_url": { "type": "string" }, @@ -18593,6 +18593,14 @@ const docTemplate = `{ } } }, + "codersdk.TemplateInsightsConfig": { + "type": "object", + "properties": { + "enable": { + "type": "boolean" + } + } + }, "codersdk.TemplateInsightsIntervalReport": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index ab71f64e333bd..fe5448d370849 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -12798,9 +12798,6 @@ "disable_path_apps": { "type": "boolean" }, - "disable_template_insights": { - "type": "boolean" - }, "disable_workspace_sharing": { "type": "boolean" }, @@ -12928,6 +12925,9 @@ "telemetry": { "$ref": "#/definitions/codersdk.TelemetryConfig" }, + "template_insights": { + "$ref": "#/definitions/codersdk.TemplateInsightsConfig" + }, "terms_of_service_url": { "type": "string" }, @@ -17029,6 +17029,14 @@ } } }, + "codersdk.TemplateInsightsConfig": { + "type": "object", + "properties": { + "enable": { + "type": "boolean" + } + } + }, "codersdk.TemplateInsightsIntervalReport": { "type": "object", "properties": { diff --git a/coderd/coderd.go b/coderd/coderd.go index e84e63063ee44..857a80b850343 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -776,7 +776,7 @@ func New(options *Options) *API { UsageTracker: options.WorkspaceUsageTracker, UpdateAgentMetricsFn: options.UpdateAgentMetrics, AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize, - DisableDatabaseStorage: options.DeploymentValues.DisableTemplateInsights.Value(), + DisableDatabaseStorage: !options.DeploymentValues.TemplateInsights.Enable.Value(), }) workspaceAppsLogger := options.Logger.Named("workspaceapps") if options.WorkspaceAppsStatsCollectorOptions.Logger == nil { @@ -1533,7 +1533,7 @@ func New(options *Options) *API { r.Use( func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - if options.DeploymentValues.DisableTemplateInsights.Value() { + if !options.DeploymentValues.TemplateInsights.Enable.Value() { httpapi.Write(context.Background(), rw, http.StatusForbidden, codersdk.Response{ Message: "Forbidden.", Detail: "Template insights are disabled.", diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 4f42f61b60477..4438dce8ea344 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -2407,7 +2407,9 @@ func TestGenericInsights_Disabled(t *testing.T) { dbrollup.WithInterval(time.Millisecond*100), ), DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) { - dv.DisableTemplateInsights = true + dv.TemplateInsights = codersdk.TemplateInsightsConfig{ + Enable: false, + } }), }) user := coderdtest.CreateFirstUser(t, client) diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 55c5654dbc526..bb4a882d9859c 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -511,7 +511,7 @@ type DeploymentValues struct { Prebuilds PrebuildsConfig `json:"workspace_prebuilds,omitempty" typescript:",notnull"` HideAITasks serpent.Bool `json:"hide_ai_tasks,omitempty" typescript:",notnull"` AI AIConfig `json:"ai,omitempty"` - DisableTemplateInsights serpent.Bool `json:"disable_template_insights,omitempty" typescript:",notnull"` + TemplateInsights TemplateInsightsConfig `json:"template_insights,omitempty" typescript:",notnull"` Config serpent.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"` WriteConfig serpent.Bool `json:"write_config,omitempty" typescript:",notnull"` @@ -611,6 +611,10 @@ type DERPConfig struct { Path serpent.String `json:"path" typescript:",notnull"` } +type TemplateInsightsConfig struct { + Enable serpent.Bool `json:"enable" typescript:",notnull"` +} + type PrometheusConfig struct { Enable serpent.Bool `json:"enable" typescript:",notnull"` Address serpent.HostPort `json:"address" typescript:",notnull"` @@ -1081,6 +1085,11 @@ func (c *DeploymentValues) Options() serpent.OptionSet { Name: "pprof", YAML: "pprof", } + deploymentGroupIntrospectionTemplateInsights = serpent.Group{ + Parent: &deploymentGroupIntrospection, + Name: "Template Insights", + YAML: "templateInsights", + } deploymentGroupIntrospectionPrometheus = serpent.Group{ Parent: &deploymentGroupIntrospection, Name: "Prometheus", @@ -1703,14 +1712,14 @@ func (c *DeploymentValues) Options() serpent.OptionSet { YAML: "configPath", }, { - Name: "Disable Template Insights", - Description: "Disable storage and display of template insights.", - Flag: "disable-template-insights", - Env: "CODER_DISABLE_TEMPLATE_INSIGHTS", - Default: "false", - Value: &c.DisableTemplateInsights, - Group: &deploymentGroupIntrospection, - YAML: "disableTemplateInsights", + Name: "Enable Template Insights", + Description: "Enable the collection and display of template insights along with the associated API endpoints. This will also enable aggregating these insights into daily active users, application usage, and transmission rates for overall deployment stats. When disabled, these values will be zero, which will also affect what the bottom deployment overview bar displays. Disabling will also prevent Prometheus collection of these values.", + Flag: "template-insights-enable", + Env: "CODER_TEMPLATE_INSIGHTS_ENABLE", + Default: "true", + Value: &c.TemplateInsights.Enable, + Group: &deploymentGroupIntrospectionTemplateInsights, + YAML: "enable", }, // TODO: support Git Auth settings. // Prometheus settings diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index 34d9e1a8feaa5..0170577e45dd2 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -233,7 +233,6 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "disable_owner_workspace_exec": true, "disable_password_auth": true, "disable_path_apps": true, - "disable_template_insights": true, "disable_workspace_sharing": true, "docs_url": { "forceQuery": true, @@ -517,6 +516,9 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "user": {} } }, + "template_insights": { + "enable": true + }, "terms_of_service_url": "string", "tls": { "address": { diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 4724f52cc9ae9..3ea5895761ab8 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -2917,7 +2917,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "disable_owner_workspace_exec": true, "disable_password_auth": true, "disable_path_apps": true, - "disable_template_insights": true, "disable_workspace_sharing": true, "docs_url": { "forceQuery": true, @@ -3201,6 +3200,9 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "user": {} } }, + "template_insights": { + "enable": true + }, "terms_of_service_url": "string", "tls": { "address": { @@ -3441,7 +3443,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "disable_owner_workspace_exec": true, "disable_password_auth": true, "disable_path_apps": true, - "disable_template_insights": true, "disable_workspace_sharing": true, "docs_url": { "forceQuery": true, @@ -3725,6 +3726,9 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "user": {} } }, + "template_insights": { + "enable": true + }, "terms_of_service_url": "string", "tls": { "address": { @@ -3797,7 +3801,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `disable_owner_workspace_exec` | boolean | false | | | | `disable_password_auth` | boolean | false | | | | `disable_path_apps` | boolean | false | | | -| `disable_template_insights` | boolean | false | | | | `disable_workspace_sharing` | boolean | false | | | | `docs_url` | [serpent.URL](#serpenturl) | false | | | | `enable_authz_recording` | boolean | false | | | @@ -3835,6 +3838,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `support` | [codersdk.SupportConfig](#codersdksupportconfig) | false | | | | `swagger` | [codersdk.SwaggerConfig](#codersdkswaggerconfig) | false | | | | `telemetry` | [codersdk.TelemetryConfig](#codersdktelemetryconfig) | false | | | +| `template_insights` | [codersdk.TemplateInsightsConfig](#codersdktemplateinsightsconfig) | false | | | | `terms_of_service_url` | string | false | | | | `tls` | [codersdk.TLSConfig](#codersdktlsconfig) | false | | | | `trace` | [codersdk.TraceConfig](#codersdktraceconfig) | false | | | @@ -8510,6 +8514,20 @@ Restarts will only happen on weekdays in this list on weeks which line up with W | `role` | `admin` | | `role` | `use` | +## codersdk.TemplateInsightsConfig + +```json +{ + "enable": true +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|----------|---------|----------|--------------|-------------| +| `enable` | boolean | false | | | + ## codersdk.TemplateInsightsIntervalReport ```json diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md index e2f44f6969614..fa2de052d175e 100644 --- a/docs/reference/cli/server.md +++ b/docs/reference/cli/server.md @@ -269,16 +269,16 @@ URL to fetch a DERP mapping on startup. See: https://tailscale.com/kb/1118/custo Path to read a DERP mapping from. See: https://tailscale.com/kb/1118/custom-derp-servers/. -### --disable-template-insights +### --template-insights-enable | | | |-------------|----------------------------------------------------| | Type | bool | -| Environment | $CODER_DISABLE_TEMPLATE_INSIGHTS | -| YAML | introspection.disableTemplateInsights | -| Default | false | +| Environment | $CODER_TEMPLATE_INSIGHTS_ENABLE | +| YAML | introspection.templateInsights.enable | +| Default | true | -Disable storage and display of template insights. +Enable the collection and display of template insights along with the associated API endpoints. This will also enable aggregating these insights into daily active users, application usage, and transmission rates for overall deployment stats. When disabled, these values will be zero, which will also affect what the bottom deployment overview bar displays. Disabling will also prevent Prometheus collection of these values. ### --prometheus-enable diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index 4ce380e9b3fae..439cfe5330e11 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -224,12 +224,6 @@ Configure TLS for your SMTP server target. --email-tls-starttls bool, $CODER_EMAIL_TLS_STARTTLS Enable STARTTLS to upgrade insecure SMTP connections using TLS. -INTROSPECTION OPTIONS: -Configure logging, tracing, and metrics exporting. - - --disable-template-insights bool, $CODER_DISABLE_TEMPLATE_INSIGHTS (default: false) - Disable storage and display of template insights. - INTROSPECTION / HEALTH CHECK OPTIONS: --health-check-refresh duration, $CODER_HEALTH_CHECK_REFRESH (default: 10m0s) Refresh interval for healthchecks. @@ -276,6 +270,16 @@ INTROSPECTION / PROMETHEUS OPTIONS: --prometheus-enable bool, $CODER_PROMETHEUS_ENABLE Serve prometheus metrics on the address defined by prometheus address. +INTROSPECTION / TEMPLATE INSIGHTS OPTIONS: + --template-insights-enable bool, $CODER_TEMPLATE_INSIGHTS_ENABLE (default: true) + Enable the collection and display of template insights along with the + associated API endpoints. This will also enable aggregating these + insights into daily active users, application usage, and transmission + rates for overall deployment stats. When disabled, these values will + be zero, which will also affect what the bottom deployment overview + bar displays. Disabling will also prevent Prometheus collection of + these values. + INTROSPECTION / TRACING OPTIONS: --trace-logs bool, $CODER_TRACE_LOGS Enables capturing of logs as events in traces. This is useful for diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index e7ccc18cf79aa..e000b9b2f2fd2 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1788,7 +1788,7 @@ export interface DeploymentValues { readonly workspace_prebuilds?: PrebuildsConfig; readonly hide_ai_tasks?: boolean; readonly ai?: AIConfig; - readonly disable_template_insights?: boolean; + readonly template_insights?: TemplateInsightsConfig; readonly config?: string; readonly write_config?: boolean; /** @@ -5079,6 +5079,11 @@ export interface TemplateGroup extends Group { readonly role: TemplateRole; } +// From codersdk/deployment.go +export interface TemplateInsightsConfig { + readonly enable: boolean; +} + // From codersdk/insights.go /** * TemplateInsightsIntervalReport is the report from the template insights From aa07160f913ac99f8fd99dd1d41a8d6e92b06e51 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 10 Dec 2025 16:56:28 -0900 Subject: [PATCH 6/9] Link GH issue to TODO --- coderd/workspacestats/reporter.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/coderd/workspacestats/reporter.go b/coderd/workspacestats/reporter.go index 1810c46ce0cdd..4c92dabc3aacb 100644 --- a/coderd/workspacestats/reporter.go +++ b/coderd/workspacestats/reporter.go @@ -36,6 +36,8 @@ import ( // activity bumping from stat reporting so we can disable stats collection // entirely when template insights are disabled rather than having to still // collect stats but then drop them here. +// +// https://github.com/coder/internal/issues/196 type ReporterOptions struct { Database database.Store From be20e54e4f2912286eda7c10fce735629374c357 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 10 Dec 2025 16:57:43 -0900 Subject: [PATCH 7/9] s/DisableDatabaseStorage/DisableDatabaseInserts --- coderd/agentapi/stats_test.go | 2 +- coderd/coderd.go | 2 +- coderd/insights_test.go | 4 ++-- coderd/workspacestats/reporter.go | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/coderd/agentapi/stats_test.go b/coderd/agentapi/stats_test.go index 75d8795d21ab6..c4e0e370db870 100644 --- a/coderd/agentapi/stats_test.go +++ b/coderd/agentapi/stats_test.go @@ -617,7 +617,7 @@ func TestUpdateStats(t *testing.T) { }, labels) assert.Equal(t, req.Stats.Metrics, metrics) }, - DisableDatabaseStorage: true, + DisableDatabaseInserts: true, }), AgentStatsRefreshInterval: 10 * time.Second, TimeNowFn: func() time.Time { diff --git a/coderd/coderd.go b/coderd/coderd.go index 857a80b850343..7990516105e04 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -776,7 +776,7 @@ func New(options *Options) *API { UsageTracker: options.WorkspaceUsageTracker, UpdateAgentMetricsFn: options.UpdateAgentMetrics, AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize, - DisableDatabaseStorage: !options.DeploymentValues.TemplateInsights.Enable.Value(), + DisableDatabaseInserts: !options.DeploymentValues.TemplateInsights.Enable.Value(), }) workspaceAppsLogger := options.Logger.Named("workspaceapps") if options.WorkspaceAppsStatsCollectorOptions.Logger == nil { diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 4438dce8ea344..7be09e40dae4d 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -754,7 +754,7 @@ func TestTemplateInsights_Golden(t *testing.T) { reporter := workspacestats.NewReporter(workspacestats.ReporterOptions{ Database: db, AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize, - DisableDatabaseStorage: disableStorage, + DisableDatabaseInserts: disableStorage, }) err = reporter.ReportAppStats(dbauthz.AsSystemRestricted(ctx), stats) require.NoError(t, err, "want no error inserting app stats") @@ -1665,7 +1665,7 @@ func TestUserActivityInsights_Golden(t *testing.T) { reporter := workspacestats.NewReporter(workspacestats.ReporterOptions{ Database: db, AppStatBatchSize: workspaceapps.DefaultStatsDBReporterBatchSize, - DisableDatabaseStorage: disableStorage, + DisableDatabaseInserts: disableStorage, }) err = reporter.ReportAppStats(dbauthz.AsSystemRestricted(ctx), stats) require.NoError(t, err, "want no error inserting app stats") diff --git a/coderd/workspacestats/reporter.go b/coderd/workspacestats/reporter.go index 4c92dabc3aacb..650c6b0bc7a86 100644 --- a/coderd/workspacestats/reporter.go +++ b/coderd/workspacestats/reporter.go @@ -48,9 +48,9 @@ type ReporterOptions struct { UsageTracker *UsageTracker UpdateAgentMetricsFn func(ctx context.Context, labels prometheusmetrics.AgentMetricLabels, metrics []*agentproto.Stats_Metric) - // DisableDatabaseStorage prevents storing stats in the database. The + // DisableDatabaseInserts prevents inserting stats in the database. The // reporter will still call UpdateAgentMetricsFn and bump workspace activity. - DisableDatabaseStorage bool + DisableDatabaseInserts bool AppStatBatchSize int } @@ -114,7 +114,7 @@ func (r *Reporter) ReportAppStats(ctx context.Context, stats []workspaceapps.Sta return nil } - if !r.opts.DisableDatabaseStorage { + if !r.opts.DisableDatabaseInserts { if err := tx.InsertWorkspaceAppStats(ctx, batch); err != nil { return err } @@ -140,7 +140,7 @@ func (r *Reporter) ReportAppStats(ctx context.Context, stats []workspaceapps.Sta // nolint:revive // usage is a control flag while we have the experiment func (r *Reporter) ReportAgentStats(ctx context.Context, now time.Time, workspace database.WorkspaceIdentity, workspaceAgent database.WorkspaceAgent, stats *agentproto.Stats, usage bool) error { // update agent stats - if !r.opts.DisableDatabaseStorage { + if !r.opts.DisableDatabaseInserts { r.opts.StatsBatcher.Add(now, workspaceAgent.ID, workspace.TemplateID, workspace.OwnerID, workspace.ID, stats, usage) } From 353504834bca5729a3de0c14813ee959921c4204 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 10 Dec 2025 17:31:50 -0900 Subject: [PATCH 8/9] Use 404 instead of 403 for disabled endpoint --- coderd/coderd.go | 4 ++-- coderd/insights_test.go | 6 +++--- .../TemplateInsightsPage.stories.tsx | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 7990516105e04..e08a2a3036885 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1534,8 +1534,8 @@ func New(options *Options) *API { func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { if !options.DeploymentValues.TemplateInsights.Enable.Value() { - httpapi.Write(context.Background(), rw, http.StatusForbidden, codersdk.Response{ - Message: "Forbidden.", + httpapi.Write(context.Background(), rw, http.StatusNotFound, codersdk.Response{ + Message: "Not Found.", Detail: "Template insights are disabled.", }) return diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 7be09e40dae4d..b960ab000a8eb 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -2418,8 +2418,8 @@ func TestGenericInsights_Disabled(t *testing.T) { tests := []struct { name string fn func(ctx context.Context) error - // ok means there should be no error, otherwise assume forbidden due to - // being disabled. + // ok means there should be no error, otherwise assume 404 due to being + // disabled. ok bool }{ { @@ -2478,7 +2478,7 @@ func TestGenericInsights_Disabled(t *testing.T) { require.Error(t, err) cerr := coderdtest.SDKError(t, err) require.Contains(t, cerr.Error(), "disabled") - require.Equal(t, http.StatusForbidden, cerr.StatusCode()) + require.Equal(t, http.StatusNotFound, cerr.StatusCode()) } }) } diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx index 8af224bd663e7..fbabe7f2b7b58 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx @@ -29,8 +29,8 @@ export const Loading: Story = { }, }; -const forbidden = mockApiError({ - message: "Forbidden.", +const notFound = mockApiError({ + message: "Not Found.", detail: "Template insights are disabled.", }); @@ -38,15 +38,15 @@ export const LoadingError: Story = { args: { templateInsights: { data: undefined, - error: forbidden, + error: notFound, }, userLatency: { data: undefined, - error: forbidden, + error: notFound, }, userActivity: { data: undefined, - error: forbidden, + error: notFound, }, }, }; From 54e725eb28d0e04acf21108d8b1876d43e5a4dd8 Mon Sep 17 00:00:00 2001 From: Asher Date: Sat, 13 Dec 2025 17:37:19 -0900 Subject: [PATCH 9/9] Remove unnecessary ? --- .../TemplateInsightsPage.tsx | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index 58fcfc8bb5e33..c9f91e3392c81 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -256,26 +256,23 @@ export const TemplateInsightsPageView: FC = ({ ? entitlements?.features.user_limit.limit : undefined } - data={templateInsights?.data?.interval_reports} - error={templateInsights?.error} - /> - +