mirror of https://github.com/coder/coder.git
refactor: define insights interval (#9693)
This commit is contained in:
parent
65db7a71b7
commit
d0d64bbdca
|
@ -1247,25 +1247,6 @@ 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) {
|
||||
for _, templateID := range arg.TemplateIDs {
|
||||
template, err := q.db.GetTemplateByID(ctx, templateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(arg.TemplateIDs) == 0 {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return q.db.GetTemplateDailyInsights(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetTemplateInsights(ctx context.Context, arg database.GetTemplateInsightsParams) (database.GetTemplateInsightsRow, error) {
|
||||
for _, templateID := range arg.TemplateIDs {
|
||||
template, err := q.db.GetTemplateByID(ctx, templateID)
|
||||
|
@ -1285,6 +1266,25 @@ func (q *querier) GetTemplateInsights(ctx context.Context, arg database.GetTempl
|
|||
return q.db.GetTemplateInsights(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetTemplateInsightsByInterval(ctx context.Context, arg database.GetTemplateInsightsByIntervalParams) ([]database.GetTemplateInsightsByIntervalRow, error) {
|
||||
for _, templateID := range arg.TemplateIDs {
|
||||
template, err := q.db.GetTemplateByID(ctx, templateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := q.authorizeContext(ctx, rbac.ActionUpdate, template); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(arg.TemplateIDs) == 0 {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceTemplate.All()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return q.db.GetTemplateInsightsByInterval(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetTemplateParameterInsights(ctx context.Context, arg database.GetTemplateParameterInsightsParams) ([]database.GetTemplateParameterInsightsRow, error) {
|
||||
for _, templateID := range arg.TemplateIDs {
|
||||
template, err := q.db.GetTemplateByID(ctx, templateID)
|
||||
|
|
|
@ -2340,92 +2340,6 @@ func (q *FakeQuerier) GetTemplateDAUs(_ context.Context, arg database.GetTemplat
|
|||
return rs, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetTemplateDailyInsights(ctx context.Context, arg database.GetTemplateDailyInsightsParams) ([]database.GetTemplateDailyInsightsRow, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
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{}{}
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range q.workspaceAppStats {
|
||||
w, err := q.getWorkspaceByIDNoLock(ctx, s.WorkspaceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(arg.TemplateIDs) > 0 && !slices.Contains(arg.TemplateIDs, w.TemplateID) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ds := range dailyStats {
|
||||
// (was.session_started_at >= ts.from_ AND was.session_started_at < ts.to_)
|
||||
// OR (was.session_ended_at > ts.from_ AND was.session_ended_at < ts.to_)
|
||||
// OR (was.session_started_at < ts.from_ AND was.session_ended_at >= ts.to_)
|
||||
if !(((s.SessionStartedAt.After(ds.startTime) || s.SessionStartedAt.Equal(ds.startTime)) && s.SessionStartedAt.Before(ds.endTime)) ||
|
||||
(s.SessionEndedAt.After(ds.startTime) && s.SessionEndedAt.Before(ds.endTime)) ||
|
||||
(s.SessionStartedAt.Before(ds.startTime) && (s.SessionEndedAt.After(ds.endTime) || s.SessionEndedAt.Equal(ds.endTime)))) {
|
||||
continue
|
||||
}
|
||||
|
||||
ds.userSet[s.UserID] = struct{}{}
|
||||
ds.templateIDSet[w.TemplateID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
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) int {
|
||||
return slice.Ascending(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 {
|
||||
|
@ -2495,6 +2409,93 @@ func (q *FakeQuerier) GetTemplateInsights(_ context.Context, arg database.GetTem
|
|||
return result, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetTemplateInsightsByInterval(ctx context.Context, arg database.GetTemplateInsightsByIntervalParams) ([]database.GetTemplateInsightsByIntervalRow, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
type statByInterval struct {
|
||||
startTime, endTime time.Time
|
||||
userSet map[uuid.UUID]struct{}
|
||||
templateIDSet map[uuid.UUID]struct{}
|
||||
}
|
||||
|
||||
statsByInterval := []statByInterval{{arg.StartTime, arg.StartTime.AddDate(0, 0, int(arg.IntervalDays)), make(map[uuid.UUID]struct{}), make(map[uuid.UUID]struct{})}}
|
||||
for statsByInterval[len(statsByInterval)-1].endTime.Before(arg.EndTime) {
|
||||
statsByInterval = append(statsByInterval, statByInterval{statsByInterval[len(statsByInterval)-1].endTime, statsByInterval[len(statsByInterval)-1].endTime.AddDate(0, 0, int(arg.IntervalDays)), make(map[uuid.UUID]struct{}), make(map[uuid.UUID]struct{})})
|
||||
}
|
||||
if statsByInterval[len(statsByInterval)-1].endTime.After(arg.EndTime) {
|
||||
statsByInterval[len(statsByInterval)-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 statsByInterval {
|
||||
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{}{}
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range q.workspaceAppStats {
|
||||
w, err := q.getWorkspaceByIDNoLock(ctx, s.WorkspaceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(arg.TemplateIDs) > 0 && !slices.Contains(arg.TemplateIDs, w.TemplateID) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ds := range statsByInterval {
|
||||
// (was.session_started_at >= ts.from_ AND was.session_started_at < ts.to_)
|
||||
// OR (was.session_ended_at > ts.from_ AND was.session_ended_at < ts.to_)
|
||||
// OR (was.session_started_at < ts.from_ AND was.session_ended_at >= ts.to_)
|
||||
if !(((s.SessionStartedAt.After(ds.startTime) || s.SessionStartedAt.Equal(ds.startTime)) && s.SessionStartedAt.Before(ds.endTime)) ||
|
||||
(s.SessionEndedAt.After(ds.startTime) && s.SessionEndedAt.Before(ds.endTime)) ||
|
||||
(s.SessionStartedAt.Before(ds.startTime) && (s.SessionEndedAt.After(ds.endTime) || s.SessionEndedAt.Equal(ds.endTime)))) {
|
||||
continue
|
||||
}
|
||||
|
||||
ds.userSet[s.UserID] = struct{}{}
|
||||
ds.templateIDSet[w.TemplateID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
var result []database.GetTemplateInsightsByIntervalRow
|
||||
for _, ds := range statsByInterval {
|
||||
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) int {
|
||||
return slice.Ascending(a.String(), b.String())
|
||||
})
|
||||
result = append(result, database.GetTemplateInsightsByIntervalRow{
|
||||
StartTime: ds.startTime,
|
||||
EndTime: ds.endTime,
|
||||
TemplateIDs: templateIDs,
|
||||
ActiveUsers: int64(len(ds.userSet)),
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetTemplateParameterInsights(ctx context.Context, arg database.GetTemplateParameterInsightsParams) ([]database.GetTemplateParameterInsightsRow, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
|
|
|
@ -655,13 +655,6 @@ 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)
|
||||
|
@ -669,6 +662,13 @@ func (m metricsStore) GetTemplateInsights(ctx context.Context, arg database.GetT
|
|||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) GetTemplateInsightsByInterval(ctx context.Context, arg database.GetTemplateInsightsByIntervalParams) ([]database.GetTemplateInsightsByIntervalRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetTemplateInsightsByInterval(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetTemplateInsightsByInterval").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) GetTemplateParameterInsights(ctx context.Context, arg database.GetTemplateParameterInsightsParams) ([]database.GetTemplateParameterInsightsRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetTemplateParameterInsights(ctx, arg)
|
||||
|
|
|
@ -1315,21 +1315,6 @@ 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()
|
||||
|
@ -1360,6 +1345,21 @@ func (mr *MockStoreMockRecorder) GetTemplateInsights(arg0, arg1 interface{}) *go
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateInsights", reflect.TypeOf((*MockStore)(nil).GetTemplateInsights), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetTemplateInsightsByInterval mocks base method.
|
||||
func (m *MockStore) GetTemplateInsightsByInterval(arg0 context.Context, arg1 database.GetTemplateInsightsByIntervalParams) ([]database.GetTemplateInsightsByIntervalRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetTemplateInsightsByInterval", arg0, arg1)
|
||||
ret0, _ := ret[0].([]database.GetTemplateInsightsByIntervalRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetTemplateInsightsByInterval indicates an expected call of GetTemplateInsightsByInterval.
|
||||
func (mr *MockStoreMockRecorder) GetTemplateInsightsByInterval(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateInsightsByInterval", reflect.TypeOf((*MockStore)(nil).GetTemplateInsightsByInterval), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetTemplateParameterInsights mocks base method.
|
||||
func (m *MockStore) GetTemplateParameterInsights(arg0 context.Context, arg1 database.GetTemplateParameterInsightsParams) ([]database.GetTemplateParameterInsightsRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
@ -124,15 +124,15 @@ 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 during a minute, we will add 5 minutes to the total usage for that
|
||||
// session/app (per user).
|
||||
GetTemplateInsights(ctx context.Context, arg GetTemplateInsightsParams) (GetTemplateInsightsRow, error)
|
||||
// GetTemplateInsightsByInterval returns all intervals between start and end
|
||||
// time, if end time is a partial interval, it will be included in the results and
|
||||
// that interval will be shorter than a full one. If there is no data for a selected
|
||||
// interval/template, it will be included in the results with 0 active users.
|
||||
GetTemplateInsightsByInterval(ctx context.Context, arg GetTemplateInsightsByIntervalParams) ([]GetTemplateInsightsByIntervalRow, error)
|
||||
// GetTemplateParameterInsights does for each template in a given timeframe,
|
||||
// look for the latest workspace build (for every workspace) that has been
|
||||
// created in the timeframe and return the aggregate usage counts of parameter
|
||||
|
|
|
@ -1738,112 +1738,6 @@ func (q *sqlQuerier) GetTemplateAppInsights(ctx context.Context, arg GetTemplate
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const getTemplateDailyInsights = `-- name: GetTemplateDailyInsights :many
|
||||
WITH ts AS (
|
||||
SELECT
|
||||
d::timestamptz AS from_,
|
||||
CASE
|
||||
WHEN (d::timestamptz + '1 day'::interval) <= $1::timestamptz
|
||||
THEN (d::timestamptz + '1 day'::interval)
|
||||
ELSE $1::timestamptz
|
||||
END AS to_
|
||||
FROM
|
||||
-- Subtract 1 second from end_time to avoid including the next interval in the results.
|
||||
generate_series($2::timestamptz, ($1::timestamptz) - '1 second'::interval, '1 day'::interval) AS d
|
||||
), unflattened_usage_by_day AS (
|
||||
-- We select data from both workspace agent stats and workspace app stats to
|
||||
-- get a complete picture of usage. This matches how usage is calculated by
|
||||
-- the combination of GetTemplateInsights and GetTemplateAppInsights. We use
|
||||
-- a union all to avoid a costly distinct operation.
|
||||
--
|
||||
-- Note that one query must perform a left join so that all intervals are
|
||||
-- present at least once.
|
||||
SELECT
|
||||
ts.from_, ts.to_,
|
||||
was.template_id,
|
||||
was.user_id
|
||||
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.template_id, was.user_id
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
ts.from_, ts.to_,
|
||||
w.template_id,
|
||||
was.user_id
|
||||
FROM ts
|
||||
JOIN workspace_app_stats was ON (
|
||||
(was.session_started_at >= ts.from_ AND was.session_started_at < ts.to_)
|
||||
OR (was.session_ended_at > ts.from_ AND was.session_ended_at < ts.to_)
|
||||
OR (was.session_started_at < ts.from_ AND was.session_ended_at >= ts.to_)
|
||||
)
|
||||
JOIN workspaces w ON (
|
||||
w.id = was.workspace_id
|
||||
AND CASE WHEN COALESCE(array_length($3::uuid[], 1), 0) > 0 THEN w.template_id = ANY($3::uuid[]) ELSE TRUE END
|
||||
)
|
||||
GROUP BY ts.from_, ts.to_, w.template_id, was.user_id
|
||||
)
|
||||
|
||||
SELECT
|
||||
from_ AS start_time,
|
||||
to_ AS end_time,
|
||||
array_remove(array_agg(DISTINCT template_id), NULL)::uuid[] AS template_ids,
|
||||
COUNT(DISTINCT user_id) AS active_users
|
||||
FROM unflattened_usage_by_day
|
||||
GROUP BY from_, to_
|
||||
`
|
||||
|
||||
type GetTemplateDailyInsightsParams struct {
|
||||
EndTime time.Time `db:"end_time" json:"end_time"`
|
||||
StartTime time.Time `db:"start_time" json:"start_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.EndTime, arg.StartTime, 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 agent_stats_by_interval_and_user AS (
|
||||
SELECT
|
||||
|
@ -1910,6 +1804,118 @@ func (q *sqlQuerier) GetTemplateInsights(ctx context.Context, arg GetTemplateIns
|
|||
return i, err
|
||||
}
|
||||
|
||||
const getTemplateInsightsByInterval = `-- name: GetTemplateInsightsByInterval :many
|
||||
WITH ts AS (
|
||||
SELECT
|
||||
d::timestamptz AS from_,
|
||||
CASE
|
||||
WHEN (d::timestamptz + ($1::int || ' day')::interval) <= $2::timestamptz
|
||||
THEN (d::timestamptz + ($1::int || ' day')::interval)
|
||||
ELSE $2::timestamptz
|
||||
END AS to_
|
||||
FROM
|
||||
-- Subtract 1 microsecond from end_time to avoid including the next interval in the results.
|
||||
generate_series($3::timestamptz, ($2::timestamptz) - '1 microsecond'::interval, ($1::int || ' day')::interval) AS d
|
||||
), unflattened_usage_by_interval AS (
|
||||
-- We select data from both workspace agent stats and workspace app stats to
|
||||
-- get a complete picture of usage. This matches how usage is calculated by
|
||||
-- the combination of GetTemplateInsights and GetTemplateAppInsights. We use
|
||||
-- a union all to avoid a costly distinct operation.
|
||||
--
|
||||
-- Note that one query must perform a left join so that all intervals are
|
||||
-- present at least once.
|
||||
SELECT
|
||||
ts.from_, ts.to_,
|
||||
was.template_id,
|
||||
was.user_id
|
||||
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($4::uuid[], 1), 0) > 0 THEN was.template_id = ANY($4::uuid[]) ELSE TRUE END
|
||||
)
|
||||
GROUP BY ts.from_, ts.to_, was.template_id, was.user_id
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
ts.from_, ts.to_,
|
||||
w.template_id,
|
||||
was.user_id
|
||||
FROM ts
|
||||
JOIN workspace_app_stats was ON (
|
||||
(was.session_started_at >= ts.from_ AND was.session_started_at < ts.to_)
|
||||
OR (was.session_ended_at > ts.from_ AND was.session_ended_at < ts.to_)
|
||||
OR (was.session_started_at < ts.from_ AND was.session_ended_at >= ts.to_)
|
||||
)
|
||||
JOIN workspaces w ON (
|
||||
w.id = was.workspace_id
|
||||
AND CASE WHEN COALESCE(array_length($4::uuid[], 1), 0) > 0 THEN w.template_id = ANY($4::uuid[]) ELSE TRUE END
|
||||
)
|
||||
GROUP BY ts.from_, ts.to_, w.template_id, was.user_id
|
||||
)
|
||||
|
||||
SELECT
|
||||
from_ AS start_time,
|
||||
to_ AS end_time,
|
||||
array_remove(array_agg(DISTINCT template_id), NULL)::uuid[] AS template_ids,
|
||||
COUNT(DISTINCT user_id) AS active_users
|
||||
FROM unflattened_usage_by_interval
|
||||
GROUP BY from_, to_
|
||||
`
|
||||
|
||||
type GetTemplateInsightsByIntervalParams struct {
|
||||
IntervalDays int32 `db:"interval_days" json:"interval_days"`
|
||||
EndTime time.Time `db:"end_time" json:"end_time"`
|
||||
StartTime time.Time `db:"start_time" json:"start_time"`
|
||||
TemplateIDs []uuid.UUID `db:"template_ids" json:"template_ids"`
|
||||
}
|
||||
|
||||
type GetTemplateInsightsByIntervalRow 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"`
|
||||
}
|
||||
|
||||
// GetTemplateInsightsByInterval returns all intervals between start and end
|
||||
// time, if end time is a partial interval, it will be included in the results and
|
||||
// that interval will be shorter than a full one. If there is no data for a selected
|
||||
// interval/template, it will be included in the results with 0 active users.
|
||||
func (q *sqlQuerier) GetTemplateInsightsByInterval(ctx context.Context, arg GetTemplateInsightsByIntervalParams) ([]GetTemplateInsightsByIntervalRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getTemplateInsightsByInterval,
|
||||
arg.IntervalDays,
|
||||
arg.EndTime,
|
||||
arg.StartTime,
|
||||
pq.Array(arg.TemplateIDs),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetTemplateInsightsByIntervalRow
|
||||
for rows.Next() {
|
||||
var i GetTemplateInsightsByIntervalRow
|
||||
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 getTemplateParameterInsights = `-- name: GetTemplateParameterInsights :many
|
||||
WITH latest_workspace_builds AS (
|
||||
SELECT
|
||||
|
|
|
@ -113,23 +113,23 @@ SELECT
|
|||
FROM app_stats_by_user_and_agent
|
||||
GROUP BY access_method, slug_or_port, display_name, icon, is_app;
|
||||
|
||||
-- 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
|
||||
-- name: GetTemplateInsightsByInterval :many
|
||||
-- GetTemplateInsightsByInterval returns all intervals between start and end
|
||||
-- time, if end time is a partial interval, it will be included in the results and
|
||||
-- that interval will be shorter than a full one. If there is no data for a selected
|
||||
-- interval/template, it will be included in the results with 0 active users.
|
||||
WITH ts AS (
|
||||
SELECT
|
||||
d::timestamptz AS from_,
|
||||
CASE
|
||||
WHEN (d::timestamptz + '1 day'::interval) <= @end_time::timestamptz
|
||||
THEN (d::timestamptz + '1 day'::interval)
|
||||
WHEN (d::timestamptz + (@interval_days::int || ' day')::interval) <= @end_time::timestamptz
|
||||
THEN (d::timestamptz + (@interval_days::int || ' day')::interval)
|
||||
ELSE @end_time::timestamptz
|
||||
END AS to_
|
||||
FROM
|
||||
-- Subtract 1 second from end_time to avoid including the next interval in the results.
|
||||
generate_series(@start_time::timestamptz, (@end_time::timestamptz) - '1 second'::interval, '1 day'::interval) AS d
|
||||
), unflattened_usage_by_day AS (
|
||||
-- Subtract 1 microsecond from end_time to avoid including the next interval in the results.
|
||||
generate_series(@start_time::timestamptz, (@end_time::timestamptz) - '1 microsecond'::interval, (@interval_days::int || ' day')::interval) AS d
|
||||
), unflattened_usage_by_interval AS (
|
||||
-- We select data from both workspace agent stats and workspace app stats to
|
||||
-- get a complete picture of usage. This matches how usage is calculated by
|
||||
-- the combination of GetTemplateInsights and GetTemplateAppInsights. We use
|
||||
|
@ -174,7 +174,7 @@ SELECT
|
|||
to_ AS end_time,
|
||||
array_remove(array_agg(DISTINCT template_id), NULL)::uuid[] AS template_ids,
|
||||
COUNT(DISTINCT user_id) AS active_users
|
||||
FROM unflattened_usage_by_day
|
||||
FROM unflattened_usage_by_interval
|
||||
GROUP BY from_, to_;
|
||||
|
||||
-- name: GetTemplateParameterInsights :many
|
||||
|
|
|
@ -191,7 +191,7 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
|
|||
|
||||
var usage database.GetTemplateInsightsRow
|
||||
var appUsage []database.GetTemplateAppInsightsRow
|
||||
var dailyUsage []database.GetTemplateDailyInsightsRow
|
||||
var dailyUsage []database.GetTemplateInsightsByIntervalRow
|
||||
var parameterRows []database.GetTemplateParameterInsightsRow
|
||||
|
||||
eg, egCtx := errgroup.WithContext(ctx)
|
||||
|
@ -203,10 +203,11 @@ func (api *API) insightsTemplates(rw http.ResponseWriter, r *http.Request) {
|
|||
eg.Go(func() error {
|
||||
var err error
|
||||
if interval != "" {
|
||||
dailyUsage, err = api.Database.GetTemplateDailyInsights(egCtx, database.GetTemplateDailyInsightsParams{
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
TemplateIDs: templateIDs,
|
||||
dailyUsage, err = api.Database.GetTemplateInsightsByInterval(egCtx, database.GetTemplateInsightsByIntervalParams{
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
TemplateIDs: templateIDs,
|
||||
IntervalDays: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get template daily insights: %w", err)
|
||||
|
|
Loading…
Reference in New Issue