Skip to content

Commit 4543b21

Browse files
SasSwartmafredri
andauthored
feat(coderd/database): track user status changes over time (#16019)
RE: #15740, #15297 In order to add a graph to the coder frontend to show user status over time as an indicator of license usage, this PR adds the following: * a new `api.insightsUserStatusCountsOverTime` endpoint to the API * which calls a new `GetUserStatusCountsOverTime` query from postgres * which relies on two new tables `user_status_changes` and `user_deleted` * which are populated by a new trigger and function that tracks updates to the users table The chart itself will be added in a subsequent PR --------- Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
1 parent 73d8dde commit 4543b21

25 files changed

+1456
-3
lines changed

Makefile

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,7 @@ lint/markdown: node_modules/.installed
521521
# All files generated by the database should be added here, and this can be used
522522
# as a target for jobs that need to run after the database is generated.
523523
DB_GEN_FILES := \
524+
coderd/database/dump.sql \
524525
coderd/database/querier.go \
525526
coderd/database/unique_constraint.go \
526527
coderd/database/dbmem/dbmem.go \
@@ -540,8 +541,6 @@ GEN_FILES := \
540541
provisionersdk/proto/provisioner.pb.go \
541542
provisionerd/proto/provisionerd.pb.go \
542543
vpn/vpn.pb.go \
543-
coderd/database/dump.sql \
544-
$(DB_GEN_FILES) \
545544
site/src/api/typesGenerated.ts \
546545
coderd/rbac/object_gen.go \
547546
codersdk/rbacresources_gen.go \
@@ -559,7 +558,7 @@ GEN_FILES := \
559558
coderd/database/pubsub/psmock/psmock.go
560559

561560
# all gen targets should be added here and to gen/mark-fresh
562-
gen: $(GEN_FILES)
561+
gen: gen/db $(GEN_FILES)
563562
.PHONY: gen
564563

565564
gen/db: $(DB_GEN_FILES)

coderd/apidoc/docs.go

Lines changed: 61 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 57 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,6 +1281,7 @@ func New(options *Options) *API {
12811281
r.Use(apiKeyMiddleware)
12821282
r.Get("/daus", api.deploymentDAUs)
12831283
r.Get("/user-activity", api.insightsUserActivity)
1284+
r.Get("/user-status-counts", api.insightsUserStatusCounts)
12841285
r.Get("/user-latency", api.insightsUserLatency)
12851286
r.Get("/templates", api.insightsTemplates)
12861287
})

coderd/database/dbauthz/dbauthz.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2421,6 +2421,13 @@ func (q *querier) GetUserNotificationPreferences(ctx context.Context, userID uui
24212421
return q.db.GetUserNotificationPreferences(ctx, userID)
24222422
}
24232423

2424+
func (q *querier) GetUserStatusCounts(ctx context.Context, arg database.GetUserStatusCountsParams) ([]database.GetUserStatusCountsRow, error) {
2425+
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUser); err != nil {
2426+
return nil, err
2427+
}
2428+
return q.db.GetUserStatusCounts(ctx, arg)
2429+
}
2430+
24242431
func (q *querier) GetUserWorkspaceBuildParameters(ctx context.Context, params database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
24252432
u, err := q.db.GetUserByID(ctx, params.OwnerID)
24262433
if err != nil {

coderd/database/dbauthz/dbauthz_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1708,6 +1708,12 @@ func (s *MethodTestSuite) TestUser() {
17081708
rbac.ResourceTemplate.InOrg(orgID), policy.ActionRead,
17091709
)
17101710
}))
1711+
s.Run("GetUserStatusCounts", s.Subtest(func(db database.Store, check *expects) {
1712+
check.Args(database.GetUserStatusCountsParams{
1713+
StartTime: time.Now().Add(-time.Hour * 24 * 30),
1714+
EndTime: time.Now(),
1715+
}).Asserts(rbac.ResourceUser, policy.ActionRead)
1716+
}))
17111717
}
17121718

17131719
func (s *MethodTestSuite) TestWorkspace() {

coderd/database/dbmem/dbmem.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ func New() database.Store {
8888
customRoles: make([]database.CustomRole, 0),
8989
locks: map[int64]struct{}{},
9090
runtimeConfig: map[string]string{},
91+
userStatusChanges: make([]database.UserStatusChange, 0),
9192
},
9293
}
9394
// Always start with a default org. Matching migration 198.
@@ -256,6 +257,7 @@ type data struct {
256257
lastLicenseID int32
257258
defaultProxyDisplayName string
258259
defaultProxyIconURL string
260+
userStatusChanges []database.UserStatusChange
259261
}
260262

261263
func tryPercentile(fs []float64, p float64) float64 {
@@ -5669,6 +5671,42 @@ func (q *FakeQuerier) GetUserNotificationPreferences(_ context.Context, userID u
56695671
return out, nil
56705672
}
56715673

5674+
func (q *FakeQuerier) GetUserStatusCounts(_ context.Context, arg database.GetUserStatusCountsParams) ([]database.GetUserStatusCountsRow, error) {
5675+
q.mutex.RLock()
5676+
defer q.mutex.RUnlock()
5677+
5678+
err := validateDatabaseType(arg)
5679+
if err != nil {
5680+
return nil, err
5681+
}
5682+
5683+
result := make([]database.GetUserStatusCountsRow, 0)
5684+
for _, change := range q.userStatusChanges {
5685+
if change.ChangedAt.Before(arg.StartTime) || change.ChangedAt.After(arg.EndTime) {
5686+
continue
5687+
}
5688+
date := time.Date(change.ChangedAt.Year(), change.ChangedAt.Month(), change.ChangedAt.Day(), 0, 0, 0, 0, time.UTC)
5689+
if !slices.ContainsFunc(result, func(r database.GetUserStatusCountsRow) bool {
5690+
return r.Status == change.NewStatus && r.Date.Equal(date)
5691+
}) {
5692+
result = append(result, database.GetUserStatusCountsRow{
5693+
Status: change.NewStatus,
5694+
Date: date,
5695+
Count: 1,
5696+
})
5697+
} else {
5698+
for i, r := range result {
5699+
if r.Status == change.NewStatus && r.Date.Equal(date) {
5700+
result[i].Count++
5701+
break
5702+
}
5703+
}
5704+
}
5705+
}
5706+
5707+
return result, nil
5708+
}
5709+
56725710
func (q *FakeQuerier) GetUserWorkspaceBuildParameters(_ context.Context, params database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
56735711
q.mutex.RLock()
56745712
defer q.mutex.RUnlock()
@@ -8021,6 +8059,12 @@ func (q *FakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam
80218059
sort.Slice(q.users, func(i, j int) bool {
80228060
return q.users[i].CreatedAt.Before(q.users[j].CreatedAt)
80238061
})
8062+
8063+
q.userStatusChanges = append(q.userStatusChanges, database.UserStatusChange{
8064+
UserID: user.ID,
8065+
NewStatus: user.Status,
8066+
ChangedAt: user.UpdatedAt,
8067+
})
80248068
return user, nil
80258069
}
80268070

@@ -9062,12 +9106,18 @@ func (q *FakeQuerier) UpdateInactiveUsersToDormant(_ context.Context, params dat
90629106
Username: user.Username,
90639107
LastSeenAt: user.LastSeenAt,
90649108
})
9109+
q.userStatusChanges = append(q.userStatusChanges, database.UserStatusChange{
9110+
UserID: user.ID,
9111+
NewStatus: database.UserStatusDormant,
9112+
ChangedAt: params.UpdatedAt,
9113+
})
90659114
}
90669115
}
90679116

90689117
if len(updated) == 0 {
90699118
return nil, sql.ErrNoRows
90709119
}
9120+
90719121
return updated, nil
90729122
}
90739123

@@ -9868,6 +9918,12 @@ func (q *FakeQuerier) UpdateUserStatus(_ context.Context, arg database.UpdateUse
98689918
user.Status = arg.Status
98699919
user.UpdatedAt = arg.UpdatedAt
98709920
q.users[index] = user
9921+
9922+
q.userStatusChanges = append(q.userStatusChanges, database.UserStatusChange{
9923+
UserID: user.ID,
9924+
NewStatus: user.Status,
9925+
ChangedAt: user.UpdatedAt,
9926+
})
98719927
return user, nil
98729928
}
98739929
return database.User{}, sql.ErrNoRows

coderd/database/dbmetrics/querymetrics.go

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbmock/dbmock.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy