diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index aa318a5f857c7..13e51ad4c0459 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -269,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 a9e6058a3eef2..f9cede4035125 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -191,6 +191,15 @@ autobuildPollInterval: 1m0s # (default: 1m0s, type: duration) jobHangDetectorInterval: 1m0s introspection: + 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/agentapi/stats_test.go b/coderd/agentapi/stats_test.go index c5cc2bd262114..c4e0e370db870 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) + }, + DisableDatabaseInserts: 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..cb3639791d9cd 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -14341,6 +14341,9 @@ const docTemplate = `{ "telemetry": { "$ref": "#/definitions/codersdk.TelemetryConfig" }, + "template_insights": { + "$ref": "#/definitions/codersdk.TemplateInsightsConfig" + }, "terms_of_service_url": { "type": "string" }, @@ -18590,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 396a704a06119..fe5448d370849 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -12925,6 +12925,9 @@ "telemetry": { "$ref": "#/definitions/codersdk.TelemetryConfig" }, + "template_insights": { + "$ref": "#/definitions/codersdk.TemplateInsightsConfig" + }, "terms_of_service_url": { "type": "string" }, @@ -17026,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 b356f372dc56c..e08a2a3036885 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, + DisableDatabaseInserts: !options.DeploymentValues.TemplateInsights.Enable.Value(), }) workspaceAppsLogger := options.Logger.Named("workspaceapps") if options.WorkspaceAppsStatsCollectorOptions.Logger == nil { @@ -1528,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.TemplateInsights.Enable.Value() { + httpapi.Write(context.Background(), rw, http.StatusNotFound, codersdk.Response{ + Message: "Not Found.", + 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 a4a47bea396a6..b960ab000a8eb 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, + DisableDatabaseInserts: 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, + DisableDatabaseInserts: 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 @@ -2346,3 +2389,97 @@ 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.TemplateInsights = codersdk.TemplateInsightsConfig{ + Enable: false, + } + }), + }) + 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 404 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.StatusNotFound, cerr.StatusCode()) + } + }) + } +} 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..650c6b0bc7a86 100644 --- a/coderd/workspacestats/reporter.go +++ b/coderd/workspacestats/reporter.go @@ -22,6 +22,23 @@ 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. +// +// https://github.com/coder/internal/issues/196 + type ReporterOptions struct { Database database.Store Logger slog.Logger @@ -31,6 +48,10 @@ type ReporterOptions struct { UsageTracker *UsageTracker UpdateAgentMetricsFn func(ctx context.Context, labels prometheusmetrics.AgentMetricLabels, metrics []*agentproto.Stats_Metric) + // DisableDatabaseInserts prevents inserting stats in the database. The + // reporter will still call UpdateAgentMetricsFn and bump workspace activity. + DisableDatabaseInserts bool + AppStatBatchSize int } @@ -93,15 +114,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.DisableDatabaseInserts { + 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 +140,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.DisableDatabaseInserts { + 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 +155,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..bb4a882d9859c 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"` + 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"` @@ -610,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"` @@ -1080,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", @@ -1701,6 +1711,16 @@ func (c *DeploymentValues) Options() serpent.OptionSet { Group: &deploymentGroupNetworkingDERP, YAML: "configPath", }, + { + 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/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/docs/reference/api/general.md b/docs/reference/api/general.md index 3ea0180ae1454..0170577e45dd2 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -516,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 bd00d79c4b40b..3ea5895761ab8 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -3200,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": { @@ -3723,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": { @@ -3832,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 | | | @@ -8507,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 4ba8c026fb299..fa2de052d175e 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/. +### --template-insights-enable + +| | | +|-------------|----------------------------------------------------| +| Type | bool | +| Environment | $CODER_TEMPLATE_INSIGHTS_ENABLE | +| YAML | introspection.templateInsights.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. + ### --prometheus-enable | | | diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index 32db725d93f77..439cfe5330e11 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -270,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 6cb14744035cc..e000b9b2f2fd2 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 template_insights?: TemplateInsightsConfig; readonly config?: string; readonly write_config?: boolean; /** @@ -2177,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 @@ -5074,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 diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx index 37b7b89a4c0b2..fbabe7f2b7b58 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 notFound = mockApiError({ + message: "Not Found.", + detail: "Template insights are disabled.", +}); + +export const LoadingError: Story = { + args: { + templateInsights: { + data: undefined, + error: notFound, + }, + userLatency: { + data: undefined, + error: notFound, + }, + userActivity: { + data: undefined, + error: notFound, + }, }, }; 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..c9f91e3392c81 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,23 @@ export const TemplateInsightsPageView: FC = ({ ? entitlements?.features.user_limit.limit : undefined } - data={templateInsights?.interval_reports} + data={templateInsights.data?.interval_reports} + error={templateInsights.error} /> - + + - @@ -265,12 +281,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 +301,8 @@ const ActiveUsersPanel: FC = ({ - {!data && } - {data && data.length === 0 && } + {!error && !data && } + {(error || data?.length === 0) && } {data && data.length > 0 && ( ({ @@ -300,10 +318,12 @@ const ActiveUsersPanel: FC = ({ interface UsersLatencyPanelProps extends PanelProps { data: UserLatencyInsightsResponse | undefined; + error: unknown; } const UsersLatencyPanel: FC = ({ data, + error, ...panelProps }) => { const theme = useTheme(); @@ -327,8 +347,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 +387,12 @@ const UsersLatencyPanel: FC = ({ interface UsersActivityPanelProps extends PanelProps { data: UserActivityInsightsResponse | undefined; + error: unknown; } const UsersActivityPanel: FC = ({ data, + error, ...panelProps }) => { const theme = useTheme(); @@ -395,8 +417,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 +456,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 +475,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 +482,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 +597,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 +875,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 +895,10 @@ const NoDataAvailable = (props: HTMLAttributes) => { justifyContent: "center", }} > - No data available + {error + ? getErrorDetail(error) || + getErrorMessage(error, "Unable to fetch insights") + : "No data available"}
); };