mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(coderd): add user latency and template insights endpoints (#8519)
Part of #8514 Refs #8109
This commit is contained in:
@@ -1173,6 +1173,22 @@ func (q *querier) GetTemplateDAUs(ctx context.Context, arg database.GetTemplateD
|
||||
return q.db.GetTemplateDAUs(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetTemplateDailyInsights(ctx context.Context, arg database.GetTemplateDailyInsightsParams) ([]database.GetTemplateDailyInsightsRow, error) {
|
||||
// FIXME: this should maybe be READ rbac.ResourceTemplate or it's own resource.
|
||||
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetTemplateDailyInsights(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetTemplateInsights(ctx context.Context, arg database.GetTemplateInsightsParams) (database.GetTemplateInsightsRow, error) {
|
||||
// FIXME: this should maybe be READ rbac.ResourceTemplate or it's own resource.
|
||||
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil {
|
||||
return database.GetTemplateInsightsRow{}, err
|
||||
}
|
||||
return q.db.GetTemplateInsights(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetTemplateVersionByID(ctx context.Context, tvid uuid.UUID) (database.TemplateVersion, error) {
|
||||
tv, err := q.db.GetTemplateVersionByID(ctx, tvid)
|
||||
if err != nil {
|
||||
@@ -1339,6 +1355,13 @@ func (q *querier) GetUserCount(ctx context.Context) (int64, error) {
|
||||
return q.db.GetUserCount(ctx)
|
||||
}
|
||||
|
||||
func (q *querier) GetUserLatencyInsights(ctx context.Context, arg database.GetUserLatencyInsightsParams) ([]database.GetUserLatencyInsightsRow, error) {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetUserLatencyInsights(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetUserLinkByLinkedID(ctx context.Context, linkedID string) (database.UserLink, error) {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionRead, rbac.ResourceSystem); err != nil {
|
||||
return database.UserLink{}, err
|
||||
|
||||
@@ -1944,6 +1944,129 @@ func (q *FakeQuerier) GetTemplateDAUs(_ context.Context, arg database.GetTemplat
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetTemplateDailyInsights(_ context.Context, arg database.GetTemplateDailyInsightsParams) ([]database.GetTemplateDailyInsightsRow, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type dailyStat struct {
|
||||
startTime, endTime time.Time
|
||||
userSet map[uuid.UUID]struct{}
|
||||
templateIDSet map[uuid.UUID]struct{}
|
||||
}
|
||||
dailyStats := []dailyStat{{arg.StartTime, arg.StartTime.AddDate(0, 0, 1), make(map[uuid.UUID]struct{}), make(map[uuid.UUID]struct{})}}
|
||||
for dailyStats[len(dailyStats)-1].endTime.Before(arg.EndTime) {
|
||||
dailyStats = append(dailyStats, dailyStat{dailyStats[len(dailyStats)-1].endTime, dailyStats[len(dailyStats)-1].endTime.AddDate(0, 0, 1), make(map[uuid.UUID]struct{}), make(map[uuid.UUID]struct{})})
|
||||
}
|
||||
if dailyStats[len(dailyStats)-1].endTime.After(arg.EndTime) {
|
||||
dailyStats[len(dailyStats)-1].endTime = arg.EndTime
|
||||
}
|
||||
|
||||
for _, s := range q.workspaceAgentStats {
|
||||
if s.CreatedAt.Before(arg.StartTime) || s.CreatedAt.Equal(arg.EndTime) || s.CreatedAt.After(arg.EndTime) {
|
||||
continue
|
||||
}
|
||||
if len(arg.TemplateIDs) > 0 && !slices.Contains(arg.TemplateIDs, s.TemplateID) {
|
||||
continue
|
||||
}
|
||||
if s.ConnectionCount == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ds := range dailyStats {
|
||||
if s.CreatedAt.Before(ds.startTime) || s.CreatedAt.Equal(ds.endTime) || s.CreatedAt.After(ds.endTime) {
|
||||
continue
|
||||
}
|
||||
ds.userSet[s.UserID] = struct{}{}
|
||||
ds.templateIDSet[s.TemplateID] = struct{}{}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var result []database.GetTemplateDailyInsightsRow
|
||||
for _, ds := range dailyStats {
|
||||
templateIDs := make([]uuid.UUID, 0, len(ds.templateIDSet))
|
||||
for templateID := range ds.templateIDSet {
|
||||
templateIDs = append(templateIDs, templateID)
|
||||
}
|
||||
slices.SortFunc(templateIDs, func(a, b uuid.UUID) bool {
|
||||
return a.String() < b.String()
|
||||
})
|
||||
result = append(result, database.GetTemplateDailyInsightsRow{
|
||||
StartTime: ds.startTime,
|
||||
EndTime: ds.endTime,
|
||||
TemplateIDs: templateIDs,
|
||||
ActiveUsers: int64(len(ds.userSet)),
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetTemplateInsights(_ context.Context, arg database.GetTemplateInsightsParams) (database.GetTemplateInsightsRow, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return database.GetTemplateInsightsRow{}, err
|
||||
}
|
||||
|
||||
templateIDSet := make(map[uuid.UUID]struct{})
|
||||
appUsageIntervalsByUser := make(map[uuid.UUID]map[time.Time]*database.GetTemplateInsightsRow)
|
||||
for _, s := range q.workspaceAgentStats {
|
||||
if s.CreatedAt.Before(arg.StartTime) || s.CreatedAt.Equal(arg.EndTime) || s.CreatedAt.After(arg.EndTime) {
|
||||
continue
|
||||
}
|
||||
if len(arg.TemplateIDs) > 0 && !slices.Contains(arg.TemplateIDs, s.TemplateID) {
|
||||
continue
|
||||
}
|
||||
if s.ConnectionCount == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
templateIDSet[s.TemplateID] = struct{}{}
|
||||
if appUsageIntervalsByUser[s.UserID] == nil {
|
||||
appUsageIntervalsByUser[s.UserID] = make(map[time.Time]*database.GetTemplateInsightsRow)
|
||||
}
|
||||
t := s.CreatedAt.Truncate(5 * time.Minute)
|
||||
if _, ok := appUsageIntervalsByUser[s.UserID][t]; !ok {
|
||||
appUsageIntervalsByUser[s.UserID][t] = &database.GetTemplateInsightsRow{}
|
||||
}
|
||||
|
||||
if s.SessionCountJetBrains > 0 {
|
||||
appUsageIntervalsByUser[s.UserID][t].UsageJetbrainsSeconds = 300
|
||||
}
|
||||
if s.SessionCountVSCode > 0 {
|
||||
appUsageIntervalsByUser[s.UserID][t].UsageVscodeSeconds = 300
|
||||
}
|
||||
if s.SessionCountReconnectingPTY > 0 {
|
||||
appUsageIntervalsByUser[s.UserID][t].UsageReconnectingPtySeconds = 300
|
||||
}
|
||||
if s.SessionCountSSH > 0 {
|
||||
appUsageIntervalsByUser[s.UserID][t].UsageSshSeconds = 300
|
||||
}
|
||||
}
|
||||
|
||||
templateIDs := make([]uuid.UUID, 0, len(templateIDSet))
|
||||
for templateID := range templateIDSet {
|
||||
templateIDs = append(templateIDs, templateID)
|
||||
}
|
||||
slices.SortFunc(templateIDs, func(a, b uuid.UUID) bool {
|
||||
return a.String() < b.String()
|
||||
})
|
||||
result := database.GetTemplateInsightsRow{
|
||||
TemplateIDs: templateIDs,
|
||||
ActiveUsers: int64(len(appUsageIntervalsByUser)),
|
||||
}
|
||||
for _, intervals := range appUsageIntervalsByUser {
|
||||
for _, interval := range intervals {
|
||||
result.UsageJetbrainsSeconds += interval.UsageJetbrainsSeconds
|
||||
result.UsageVscodeSeconds += interval.UsageVscodeSeconds
|
||||
result.UsageReconnectingPtySeconds += interval.UsageReconnectingPtySeconds
|
||||
result.UsageSshSeconds += interval.UsageSshSeconds
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetTemplateVersionByID(ctx context.Context, templateVersionID uuid.UUID) (database.TemplateVersion, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
@@ -2188,6 +2311,74 @@ func (q *FakeQuerier) GetUserCount(_ context.Context) (int64, error) {
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetUserLatencyInsights(_ context.Context, arg database.GetUserLatencyInsightsParams) ([]database.GetUserLatencyInsightsRow, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
latenciesByUserID := make(map[uuid.UUID][]float64)
|
||||
seenTemplatesByUserID := make(map[uuid.UUID]map[uuid.UUID]struct{})
|
||||
for _, s := range q.workspaceAgentStats {
|
||||
if len(arg.TemplateIDs) > 0 && !slices.Contains(arg.TemplateIDs, s.TemplateID) {
|
||||
continue
|
||||
}
|
||||
if !arg.StartTime.Equal(s.CreatedAt) && !(s.CreatedAt.After(arg.StartTime) && s.CreatedAt.Before(arg.EndTime)) {
|
||||
continue
|
||||
}
|
||||
if s.ConnectionCount == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
latenciesByUserID[s.UserID] = append(latenciesByUserID[s.UserID], s.ConnectionMedianLatencyMS)
|
||||
if seenTemplatesByUserID[s.UserID] == nil {
|
||||
seenTemplatesByUserID[s.UserID] = make(map[uuid.UUID]struct{})
|
||||
}
|
||||
seenTemplatesByUserID[s.UserID][s.TemplateID] = struct{}{}
|
||||
}
|
||||
|
||||
tryPercentile := func(fs []float64, p float64) float64 {
|
||||
if len(fs) == 0 {
|
||||
return -1
|
||||
}
|
||||
sort.Float64s(fs)
|
||||
return fs[int(float64(len(fs))*p/100)]
|
||||
}
|
||||
|
||||
var rows []database.GetUserLatencyInsightsRow
|
||||
for userID, latencies := range latenciesByUserID {
|
||||
sort.Float64s(latencies)
|
||||
templateIDSet := seenTemplatesByUserID[userID]
|
||||
templateIDs := make([]uuid.UUID, 0, len(templateIDSet))
|
||||
for templateID := range templateIDSet {
|
||||
templateIDs = append(templateIDs, templateID)
|
||||
}
|
||||
slices.SortFunc(templateIDs, func(a, b uuid.UUID) bool {
|
||||
return a.String() < b.String()
|
||||
})
|
||||
user, err := q.getUserByIDNoLock(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row := database.GetUserLatencyInsightsRow{
|
||||
UserID: userID,
|
||||
Username: user.Username,
|
||||
TemplateIDs: templateIDs,
|
||||
WorkspaceConnectionLatency50: tryPercentile(latencies, 50),
|
||||
WorkspaceConnectionLatency95: tryPercentile(latencies, 95),
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
slices.SortFunc(rows, func(a, b database.GetUserLatencyInsightsRow) bool {
|
||||
return a.UserID.String() < b.UserID.String()
|
||||
})
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetUserLinkByLinkedID(_ context.Context, id string) (database.UserLink, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
@@ -5333,9 +5524,9 @@ func (q *FakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.
|
||||
}
|
||||
}
|
||||
|
||||
if len(arg.TemplateIds) > 0 {
|
||||
if len(arg.TemplateIDs) > 0 {
|
||||
match := false
|
||||
for _, id := range arg.TemplateIds {
|
||||
for _, id := range arg.TemplateIDs {
|
||||
if workspace.TemplateID == id {
|
||||
match = true
|
||||
break
|
||||
|
||||
@@ -599,6 +599,20 @@ func (m metricsStore) GetTemplateDAUs(ctx context.Context, arg database.GetTempl
|
||||
return daus, err
|
||||
}
|
||||
|
||||
func (m metricsStore) GetTemplateDailyInsights(ctx context.Context, arg database.GetTemplateDailyInsightsParams) ([]database.GetTemplateDailyInsightsRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetTemplateDailyInsights(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetTemplateDailyInsights").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) GetTemplateInsights(ctx context.Context, arg database.GetTemplateInsightsParams) (database.GetTemplateInsightsRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetTemplateInsights(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetTemplateInsights").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (database.TemplateVersion, error) {
|
||||
start := time.Now()
|
||||
version, err := m.s.GetTemplateVersionByID(ctx, id)
|
||||
@@ -697,6 +711,13 @@ func (m metricsStore) GetUserCount(ctx context.Context) (int64, error) {
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (m metricsStore) GetUserLatencyInsights(ctx context.Context, arg database.GetUserLatencyInsightsParams) ([]database.GetUserLatencyInsightsRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetUserLatencyInsights(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetUserLatencyInsights").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) GetUserLinkByLinkedID(ctx context.Context, linkedID string) (database.UserLink, error) {
|
||||
start := time.Now()
|
||||
link, err := m.s.GetUserLinkByLinkedID(ctx, linkedID)
|
||||
|
||||
@@ -1196,6 +1196,21 @@ func (mr *MockStoreMockRecorder) GetTemplateDAUs(arg0, arg1 interface{}) *gomock
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateDAUs", reflect.TypeOf((*MockStore)(nil).GetTemplateDAUs), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetTemplateDailyInsights mocks base method.
|
||||
func (m *MockStore) GetTemplateDailyInsights(arg0 context.Context, arg1 database.GetTemplateDailyInsightsParams) ([]database.GetTemplateDailyInsightsRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetTemplateDailyInsights", arg0, arg1)
|
||||
ret0, _ := ret[0].([]database.GetTemplateDailyInsightsRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetTemplateDailyInsights indicates an expected call of GetTemplateDailyInsights.
|
||||
func (mr *MockStoreMockRecorder) GetTemplateDailyInsights(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateDailyInsights", reflect.TypeOf((*MockStore)(nil).GetTemplateDailyInsights), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetTemplateGroupRoles mocks base method.
|
||||
func (m *MockStore) GetTemplateGroupRoles(arg0 context.Context, arg1 uuid.UUID) ([]database.TemplateGroup, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -1211,6 +1226,21 @@ func (mr *MockStoreMockRecorder) GetTemplateGroupRoles(arg0, arg1 interface{}) *
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateGroupRoles", reflect.TypeOf((*MockStore)(nil).GetTemplateGroupRoles), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetTemplateInsights mocks base method.
|
||||
func (m *MockStore) GetTemplateInsights(arg0 context.Context, arg1 database.GetTemplateInsightsParams) (database.GetTemplateInsightsRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetTemplateInsights", arg0, arg1)
|
||||
ret0, _ := ret[0].(database.GetTemplateInsightsRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetTemplateInsights indicates an expected call of GetTemplateInsights.
|
||||
func (mr *MockStoreMockRecorder) GetTemplateInsights(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateInsights", reflect.TypeOf((*MockStore)(nil).GetTemplateInsights), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetTemplateUserRoles mocks base method.
|
||||
func (m *MockStore) GetTemplateUserRoles(arg0 context.Context, arg1 uuid.UUID) ([]database.TemplateUser, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -1436,6 +1466,21 @@ func (mr *MockStoreMockRecorder) GetUserCount(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserCount", reflect.TypeOf((*MockStore)(nil).GetUserCount), arg0)
|
||||
}
|
||||
|
||||
// GetUserLatencyInsights mocks base method.
|
||||
func (m *MockStore) GetUserLatencyInsights(arg0 context.Context, arg1 database.GetUserLatencyInsightsParams) ([]database.GetUserLatencyInsightsRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetUserLatencyInsights", arg0, arg1)
|
||||
ret0, _ := ret[0].([]database.GetUserLatencyInsightsRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetUserLatencyInsights indicates an expected call of GetUserLatencyInsights.
|
||||
func (mr *MockStoreMockRecorder) GetUserLatencyInsights(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserLatencyInsights", reflect.TypeOf((*MockStore)(nil).GetUserLatencyInsights), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetUserLinkByLinkedID mocks base method.
|
||||
func (m *MockStore) GetUserLinkByLinkedID(arg0 context.Context, arg1 string) (database.UserLink, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -213,7 +213,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
|
||||
arg.OwnerID,
|
||||
arg.OwnerUsername,
|
||||
arg.TemplateName,
|
||||
pq.Array(arg.TemplateIds),
|
||||
pq.Array(arg.TemplateIDs),
|
||||
arg.Name,
|
||||
arg.HasAgent,
|
||||
arg.AgentInactiveDisconnectTimeoutSeconds,
|
||||
|
||||
@@ -105,6 +105,14 @@ type sqlcQuerier interface {
|
||||
GetTemplateByID(ctx context.Context, id uuid.UUID) (Template, error)
|
||||
GetTemplateByOrganizationAndName(ctx context.Context, arg GetTemplateByOrganizationAndNameParams) (Template, error)
|
||||
GetTemplateDAUs(ctx context.Context, arg GetTemplateDAUsParams) ([]GetTemplateDAUsRow, error)
|
||||
// GetTemplateDailyInsights returns all daily intervals between start and end
|
||||
// time, if end time is a partial day, it will be included in the results and
|
||||
// that interval will be less than 24 hours. If there is no data for a selected
|
||||
// interval/template, it will be included in the results with 0 active users.
|
||||
GetTemplateDailyInsights(ctx context.Context, arg GetTemplateDailyInsightsParams) ([]GetTemplateDailyInsightsRow, error)
|
||||
// GetTemplateInsights has a granularity of 5 minutes where if a session/app was
|
||||
// in use, we will add 5 minutes to the total usage for that session (per user).
|
||||
GetTemplateInsights(ctx context.Context, arg GetTemplateInsightsParams) (GetTemplateInsightsRow, error)
|
||||
GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (TemplateVersion, error)
|
||||
GetTemplateVersionByJobID(ctx context.Context, jobID uuid.UUID) (TemplateVersion, error)
|
||||
GetTemplateVersionByTemplateIDAndName(ctx context.Context, arg GetTemplateVersionByTemplateIDAndNameParams) (TemplateVersion, error)
|
||||
@@ -119,6 +127,11 @@ type sqlcQuerier interface {
|
||||
GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error)
|
||||
GetUserByID(ctx context.Context, id uuid.UUID) (User, error)
|
||||
GetUserCount(ctx context.Context) (int64, error)
|
||||
// GetUserLatencyInsights returns the median and 95th percentile connection
|
||||
// latency that users have experienced. The result can be filtered on
|
||||
// template_ids, meaning only user data from workspaces based on those templates
|
||||
// will be included.
|
||||
GetUserLatencyInsights(ctx context.Context, arg GetUserLatencyInsightsParams) ([]GetUserLatencyInsightsRow, error)
|
||||
GetUserLinkByLinkedID(ctx context.Context, linkedID string) (UserLink, error)
|
||||
GetUserLinkByUserIDLoginType(ctx context.Context, arg GetUserLinkByUserIDLoginTypeParams) (UserLink, error)
|
||||
// This will never return deleted users.
|
||||
|
||||
@@ -1375,6 +1375,228 @@ func (q *sqlQuerier) UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDPar
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getTemplateDailyInsights = `-- name: GetTemplateDailyInsights :many
|
||||
WITH d AS (
|
||||
-- sqlc workaround, use SELECT generate_series instead of SELECT * FROM generate_series.
|
||||
SELECT generate_series($1::timestamptz, $2::timestamptz, '1 day'::interval) AS d
|
||||
), ts AS (
|
||||
SELECT
|
||||
d::timestamptz AS from_,
|
||||
CASE WHEN (d + '1 day'::interval)::timestamptz <= $2::timestamptz THEN (d + '1 day'::interval)::timestamptz ELSE $2::timestamptz END AS to_
|
||||
FROM d
|
||||
), usage_by_day AS (
|
||||
SELECT
|
||||
ts.from_, ts.to_,
|
||||
was.user_id,
|
||||
array_agg(was.template_id) AS template_ids
|
||||
FROM ts
|
||||
LEFT JOIN workspace_agent_stats was ON (
|
||||
was.created_at >= ts.from_
|
||||
AND was.created_at < ts.to_
|
||||
AND was.connection_count > 0
|
||||
AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN was.template_id = ANY($3::uuid[]) ELSE TRUE END
|
||||
)
|
||||
GROUP BY ts.from_, ts.to_, was.user_id
|
||||
), template_ids AS (
|
||||
SELECT array_agg(DISTINCT template_id) AS ids
|
||||
FROM usage_by_day, unnest(template_ids) template_id
|
||||
WHERE template_id IS NOT NULL
|
||||
)
|
||||
|
||||
SELECT
|
||||
from_ AS start_time,
|
||||
to_ AS end_time,
|
||||
COALESCE((SELECT ids FROM template_ids), '{}')::uuid[] AS template_ids,
|
||||
COUNT(DISTINCT user_id) AS active_users
|
||||
FROM usage_by_day, unnest(template_ids) as template_id
|
||||
GROUP BY from_, to_
|
||||
`
|
||||
|
||||
type GetTemplateDailyInsightsParams struct {
|
||||
StartTime time.Time `db:"start_time" json:"start_time"`
|
||||
EndTime time.Time `db:"end_time" json:"end_time"`
|
||||
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
|
||||
}
|
||||
|
||||
type GetTemplateDailyInsightsRow struct {
|
||||
StartTime time.Time `db:"start_time" json:"start_time"`
|
||||
EndTime time.Time `db:"end_time" json:"end_time"`
|
||||
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
|
||||
ActiveUsers int64 `db:"active_users" json:"active_users"`
|
||||
}
|
||||
|
||||
// GetTemplateDailyInsights returns all daily intervals between start and end
|
||||
// time, if end time is a partial day, it will be included in the results and
|
||||
// that interval will be less than 24 hours. If there is no data for a selected
|
||||
// interval/template, it will be included in the results with 0 active users.
|
||||
func (q *sqlQuerier) GetTemplateDailyInsights(ctx context.Context, arg GetTemplateDailyInsightsParams) ([]GetTemplateDailyInsightsRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getTemplateDailyInsights, arg.StartTime, arg.EndTime, pq.Array(arg.TemplateIDs))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetTemplateDailyInsightsRow
|
||||
for rows.Next() {
|
||||
var i GetTemplateDailyInsightsRow
|
||||
if err := rows.Scan(
|
||||
&i.StartTime,
|
||||
&i.EndTime,
|
||||
pq.Array(&i.TemplateIDs),
|
||||
&i.ActiveUsers,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getTemplateInsights = `-- name: GetTemplateInsights :one
|
||||
WITH d AS (
|
||||
SELECT generate_series($1::timestamptz, $2::timestamptz, '5 minute'::interval) AS d
|
||||
), ts AS (
|
||||
SELECT
|
||||
d::timestamptz AS from_,
|
||||
(d + '5 minute'::interval)::timestamptz AS to_,
|
||||
EXTRACT(epoch FROM '5 minute'::interval) AS seconds
|
||||
FROM d
|
||||
), usage_by_user AS (
|
||||
SELECT
|
||||
ts.from_,
|
||||
ts.to_,
|
||||
was.user_id,
|
||||
array_agg(was.template_id) AS template_ids,
|
||||
CASE WHEN SUM(was.session_count_vscode) > 0 THEN ts.seconds ELSE 0 END AS usage_vscode_seconds,
|
||||
CASE WHEN SUM(was.session_count_jetbrains) > 0 THEN ts.seconds ELSE 0 END AS usage_jetbrains_seconds,
|
||||
CASE WHEN SUM(was.session_count_reconnecting_pty) > 0 THEN ts.seconds ELSE 0 END AS usage_reconnecting_pty_seconds,
|
||||
CASE WHEN SUM(was.session_count_ssh) > 0 THEN ts.seconds ELSE 0 END AS usage_ssh_seconds
|
||||
FROM ts
|
||||
JOIN workspace_agent_stats was ON (
|
||||
was.created_at >= ts.from_
|
||||
AND was.created_at < ts.to_
|
||||
AND was.connection_count > 0
|
||||
AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN was.template_id = ANY($3::uuid[]) ELSE TRUE END
|
||||
)
|
||||
GROUP BY ts.from_, ts.to_, ts.seconds, was.user_id
|
||||
), template_ids AS (
|
||||
SELECT array_agg(DISTINCT template_id) AS ids
|
||||
FROM usage_by_user, unnest(template_ids) template_id
|
||||
WHERE template_id IS NOT NULL
|
||||
)
|
||||
|
||||
SELECT
|
||||
COALESCE((SELECT ids FROM template_ids), '{}')::uuid[] AS template_ids,
|
||||
COUNT(DISTINCT user_id) AS active_users,
|
||||
COALESCE(SUM(usage_vscode_seconds), 0)::bigint AS usage_vscode_seconds,
|
||||
COALESCE(SUM(usage_jetbrains_seconds), 0)::bigint AS usage_jetbrains_seconds,
|
||||
COALESCE(SUM(usage_reconnecting_pty_seconds), 0)::bigint AS usage_reconnecting_pty_seconds,
|
||||
COALESCE(SUM(usage_ssh_seconds), 0)::bigint AS usage_ssh_seconds
|
||||
FROM usage_by_user
|
||||
`
|
||||
|
||||
type GetTemplateInsightsParams struct {
|
||||
StartTime time.Time `db:"start_time" json:"start_time"`
|
||||
EndTime time.Time `db:"end_time" json:"end_time"`
|
||||
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
|
||||
}
|
||||
|
||||
type GetTemplateInsightsRow struct {
|
||||
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
|
||||
ActiveUsers int64 `db:"active_users" json:"active_users"`
|
||||
UsageVscodeSeconds int64 `db:"usage_vscode_seconds" json:"usage_vscode_seconds"`
|
||||
UsageJetbrainsSeconds int64 `db:"usage_jetbrains_seconds" json:"usage_jetbrains_seconds"`
|
||||
UsageReconnectingPtySeconds int64 `db:"usage_reconnecting_pty_seconds" json:"usage_reconnecting_pty_seconds"`
|
||||
UsageSshSeconds int64 `db:"usage_ssh_seconds" json:"usage_ssh_seconds"`
|
||||
}
|
||||
|
||||
// GetTemplateInsights has a granularity of 5 minutes where if a session/app was
|
||||
// in use, we will add 5 minutes to the total usage for that session (per user).
|
||||
func (q *sqlQuerier) GetTemplateInsights(ctx context.Context, arg GetTemplateInsightsParams) (GetTemplateInsightsRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getTemplateInsights, arg.StartTime, arg.EndTime, pq.Array(arg.TemplateIDs))
|
||||
var i GetTemplateInsightsRow
|
||||
err := row.Scan(
|
||||
pq.Array(&i.TemplateIDs),
|
||||
&i.ActiveUsers,
|
||||
&i.UsageVscodeSeconds,
|
||||
&i.UsageJetbrainsSeconds,
|
||||
&i.UsageReconnectingPtySeconds,
|
||||
&i.UsageSshSeconds,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserLatencyInsights = `-- name: GetUserLatencyInsights :many
|
||||
SELECT
|
||||
workspace_agent_stats.user_id,
|
||||
users.username,
|
||||
array_agg(DISTINCT template_id)::uuid[] AS template_ids,
|
||||
coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50,
|
||||
coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95
|
||||
FROM workspace_agent_stats
|
||||
JOIN users ON (users.id = workspace_agent_stats.user_id)
|
||||
WHERE
|
||||
workspace_agent_stats.created_at >= $1
|
||||
AND workspace_agent_stats.created_at < $2
|
||||
AND workspace_agent_stats.connection_median_latency_ms > 0
|
||||
AND workspace_agent_stats.connection_count > 0
|
||||
AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN template_id = ANY($3::uuid[]) ELSE TRUE END
|
||||
GROUP BY workspace_agent_stats.user_id, users.username
|
||||
ORDER BY user_id ASC
|
||||
`
|
||||
|
||||
type GetUserLatencyInsightsParams struct {
|
||||
StartTime time.Time `db:"start_time" json:"start_time"`
|
||||
EndTime time.Time `db:"end_time" json:"end_time"`
|
||||
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
|
||||
}
|
||||
|
||||
type GetUserLatencyInsightsRow struct {
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
Username string `db:"username" json:"username"`
|
||||
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
|
||||
WorkspaceConnectionLatency50 float64 `db:"workspace_connection_latency_50" json:"workspace_connection_latency_50"`
|
||||
WorkspaceConnectionLatency95 float64 `db:"workspace_connection_latency_95" json:"workspace_connection_latency_95"`
|
||||
}
|
||||
|
||||
// GetUserLatencyInsights returns the median and 95th percentile connection
|
||||
// latency that users have experienced. The result can be filtered on
|
||||
// template_ids, meaning only user data from workspaces based on those templates
|
||||
// will be included.
|
||||
func (q *sqlQuerier) GetUserLatencyInsights(ctx context.Context, arg GetUserLatencyInsightsParams) ([]GetUserLatencyInsightsRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getUserLatencyInsights, arg.StartTime, arg.EndTime, pq.Array(arg.TemplateIDs))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetUserLatencyInsightsRow
|
||||
for rows.Next() {
|
||||
var i GetUserLatencyInsightsRow
|
||||
if err := rows.Scan(
|
||||
&i.UserID,
|
||||
&i.Username,
|
||||
pq.Array(&i.TemplateIDs),
|
||||
&i.WorkspaceConnectionLatency50,
|
||||
&i.WorkspaceConnectionLatency95,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const deleteLicense = `-- name: DeleteLicense :one
|
||||
DELETE
|
||||
FROM licenses
|
||||
@@ -8536,7 +8758,7 @@ type GetWorkspacesParams struct {
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||
OwnerUsername string `db:"owner_username" json:"owner_username"`
|
||||
TemplateName string `db:"template_name" json:"template_name"`
|
||||
TemplateIds []uuid.UUID `db:"template_ids" json:"template_ids"`
|
||||
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
|
||||
Name string `db:"name" json:"name"`
|
||||
HasAgent string `db:"has_agent" json:"has_agent"`
|
||||
AgentInactiveDisconnectTimeoutSeconds int64 `db:"agent_inactive_disconnect_timeout_seconds" json:"agent_inactive_disconnect_timeout_seconds"`
|
||||
@@ -8571,7 +8793,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
|
||||
arg.OwnerID,
|
||||
arg.OwnerUsername,
|
||||
arg.TemplateName,
|
||||
pq.Array(arg.TemplateIds),
|
||||
pq.Array(arg.TemplateIDs),
|
||||
arg.Name,
|
||||
arg.HasAgent,
|
||||
arg.AgentInactiveDisconnectTimeoutSeconds,
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
-- name: GetUserLatencyInsights :many
|
||||
-- GetUserLatencyInsights returns the median and 95th percentile connection
|
||||
-- latency that users have experienced. The result can be filtered on
|
||||
-- template_ids, meaning only user data from workspaces based on those templates
|
||||
-- will be included.
|
||||
SELECT
|
||||
workspace_agent_stats.user_id,
|
||||
users.username,
|
||||
array_agg(DISTINCT template_id)::uuid[] AS template_ids,
|
||||
coalesce((PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_50,
|
||||
coalesce((PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY connection_median_latency_ms)), -1)::FLOAT AS workspace_connection_latency_95
|
||||
FROM workspace_agent_stats
|
||||
JOIN users ON (users.id = workspace_agent_stats.user_id)
|
||||
WHERE
|
||||
workspace_agent_stats.created_at >= @start_time
|
||||
AND workspace_agent_stats.created_at < @end_time
|
||||
AND workspace_agent_stats.connection_median_latency_ms > 0
|
||||
AND workspace_agent_stats.connection_count > 0
|
||||
AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
|
||||
GROUP BY workspace_agent_stats.user_id, users.username
|
||||
ORDER BY user_id ASC;
|
||||
|
||||
-- name: GetTemplateInsights :one
|
||||
-- GetTemplateInsights has a granularity of 5 minutes where if a session/app was
|
||||
-- in use, we will add 5 minutes to the total usage for that session (per user).
|
||||
WITH d AS (
|
||||
SELECT generate_series(@start_time::timestamptz, @end_time::timestamptz, '5 minute'::interval) AS d
|
||||
), ts AS (
|
||||
SELECT
|
||||
d::timestamptz AS from_,
|
||||
(d + '5 minute'::interval)::timestamptz AS to_,
|
||||
EXTRACT(epoch FROM '5 minute'::interval) AS seconds
|
||||
FROM d
|
||||
), usage_by_user AS (
|
||||
SELECT
|
||||
ts.from_,
|
||||
ts.to_,
|
||||
was.user_id,
|
||||
array_agg(was.template_id) AS template_ids,
|
||||
CASE WHEN SUM(was.session_count_vscode) > 0 THEN ts.seconds ELSE 0 END AS usage_vscode_seconds,
|
||||
CASE WHEN SUM(was.session_count_jetbrains) > 0 THEN ts.seconds ELSE 0 END AS usage_jetbrains_seconds,
|
||||
CASE WHEN SUM(was.session_count_reconnecting_pty) > 0 THEN ts.seconds ELSE 0 END AS usage_reconnecting_pty_seconds,
|
||||
CASE WHEN SUM(was.session_count_ssh) > 0 THEN ts.seconds ELSE 0 END AS usage_ssh_seconds
|
||||
FROM ts
|
||||
JOIN workspace_agent_stats was ON (
|
||||
was.created_at >= ts.from_
|
||||
AND was.created_at < ts.to_
|
||||
AND was.connection_count > 0
|
||||
AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN was.template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
|
||||
)
|
||||
GROUP BY ts.from_, ts.to_, ts.seconds, was.user_id
|
||||
), template_ids AS (
|
||||
SELECT array_agg(DISTINCT template_id) AS ids
|
||||
FROM usage_by_user, unnest(template_ids) template_id
|
||||
WHERE template_id IS NOT NULL
|
||||
)
|
||||
|
||||
SELECT
|
||||
COALESCE((SELECT ids FROM template_ids), '{}')::uuid[] AS template_ids,
|
||||
COUNT(DISTINCT user_id) AS active_users,
|
||||
COALESCE(SUM(usage_vscode_seconds), 0)::bigint AS usage_vscode_seconds,
|
||||
COALESCE(SUM(usage_jetbrains_seconds), 0)::bigint AS usage_jetbrains_seconds,
|
||||
COALESCE(SUM(usage_reconnecting_pty_seconds), 0)::bigint AS usage_reconnecting_pty_seconds,
|
||||
COALESCE(SUM(usage_ssh_seconds), 0)::bigint AS usage_ssh_seconds
|
||||
FROM usage_by_user;
|
||||
|
||||
-- name: GetTemplateDailyInsights :many
|
||||
-- GetTemplateDailyInsights returns all daily intervals between start and end
|
||||
-- time, if end time is a partial day, it will be included in the results and
|
||||
-- that interval will be less than 24 hours. If there is no data for a selected
|
||||
-- interval/template, it will be included in the results with 0 active users.
|
||||
WITH d AS (
|
||||
-- sqlc workaround, use SELECT generate_series instead of SELECT * FROM generate_series.
|
||||
SELECT generate_series(@start_time::timestamptz, @end_time::timestamptz, '1 day'::interval) AS d
|
||||
), ts AS (
|
||||
SELECT
|
||||
d::timestamptz AS from_,
|
||||
CASE WHEN (d + '1 day'::interval)::timestamptz <= @end_time::timestamptz THEN (d + '1 day'::interval)::timestamptz ELSE @end_time::timestamptz END AS to_
|
||||
FROM d
|
||||
), usage_by_day AS (
|
||||
SELECT
|
||||
ts.*,
|
||||
was.user_id,
|
||||
array_agg(was.template_id) AS template_ids
|
||||
FROM ts
|
||||
LEFT JOIN workspace_agent_stats was ON (
|
||||
was.created_at >= ts.from_
|
||||
AND was.created_at < ts.to_
|
||||
AND was.connection_count > 0
|
||||
AND CASE WHEN COALESCE(array_length(@template_ids::uuid[], 1), 0) > 0 THEN was.template_id = ANY(@template_ids::uuid[]) ELSE TRUE END
|
||||
)
|
||||
GROUP BY ts.from_, ts.to_, was.user_id
|
||||
), template_ids AS (
|
||||
SELECT array_agg(DISTINCT template_id) AS ids
|
||||
FROM usage_by_day, unnest(template_ids) template_id
|
||||
WHERE template_id IS NOT NULL
|
||||
)
|
||||
|
||||
SELECT
|
||||
from_ AS start_time,
|
||||
to_ AS end_time,
|
||||
COALESCE((SELECT ids FROM template_ids), '{}')::uuid[] AS template_ids,
|
||||
COUNT(DISTINCT user_id) AS active_users
|
||||
FROM usage_by_day, unnest(template_ids) as template_id
|
||||
GROUP BY from_, to_;
|
||||
@@ -69,6 +69,7 @@ overrides:
|
||||
inactivity_ttl: InactivityTTL
|
||||
eof: EOF
|
||||
locked_ttl: LockedTTL
|
||||
template_ids: TemplateIDs
|
||||
|
||||
sql:
|
||||
- schema: "./dump.sql"
|
||||
|
||||
Reference in New Issue
Block a user