diff --git a/management/server/file_store.go b/management/server/file_store.go index ad514781f..5d8ddba4b 100644 --- a/management/server/file_store.go +++ b/management/server/file_store.go @@ -676,28 +676,33 @@ func (s *FileStore) CalculateUsageStats(_ context.Context, accountID string, sta return nil, fmt.Errorf("account not found") } - stats := &AccountUsageStats{ - TotalUsers: 0, - TotalPeers: int64(len(account.Peers)), - } + stats := &AccountUsageStats{} + + activeUsers := make(map[string]struct{}) + for userID, user := range account.Users { + if user.LastLogin.After(start) && user.LastLogin.Before(end) && !user.IsServiceUser { + activeUsers[userID] = struct{}{} + } - for _, user := range account.Users { if !user.IsServiceUser { stats.TotalUsers++ } } - activeUsers := make(map[string]bool) for _, peer := range account.Peers { - lastSeen := peer.Status.LastSeen - if lastSeen.Compare(start) >= 0 && lastSeen.Compare(end) <= 0 { - if _, exists := account.Users[peer.UserID]; exists && !activeUsers[peer.UserID] { - activeUsers[peer.UserID] = true - stats.ActiveUsers++ - } + if peer.Status.LastSeen.After(start) && peer.Status.LastSeen.Before(end) { stats.ActivePeers++ + + // service users don't have peers, but we check regardless. + if _, exists := account.Users[peer.UserID]; exists && !account.Users[peer.UserID].IsServiceUser { + activeUsers[peer.UserID] = struct{}{} + } } } + stats.ActiveUsers = int64(len(activeUsers)) + + stats.TotalPeers = int64(len(account.Peers)) + return stats, nil } diff --git a/management/server/file_store_test.go b/management/server/file_store_test.go index 083a062b6..5c0c26b24 100644 --- a/management/server/file_store_test.go +++ b/management/server/file_store_test.go @@ -674,15 +674,15 @@ func TestFileStore_CalculateUsageStats(t *testing.T) { stats1, err := store.CalculateUsageStats(context.TODO(), "account-1", startDate, endDate) require.NoError(t, err) - assert.Equal(t, int64(2), stats1.ActiveUsers) - assert.Equal(t, int64(4), stats1.TotalUsers) + assert.Equal(t, int64(3), stats1.ActiveUsers) + assert.Equal(t, int64(5), stats1.TotalUsers) assert.Equal(t, int64(3), stats1.ActivePeers) - assert.Equal(t, int64(7), stats1.TotalPeers) + assert.Equal(t, int64(8), stats1.TotalPeers) stats2, err := store.CalculateUsageStats(context.TODO(), "account-2", startDate, endDate) require.NoError(t, err) - assert.Equal(t, int64(1), stats2.ActiveUsers) + assert.Equal(t, int64(2), stats2.ActiveUsers) assert.Equal(t, int64(2), stats2.TotalUsers) assert.Equal(t, int64(1), stats2.ActivePeers) assert.Equal(t, int64(2), stats2.TotalPeers) diff --git a/management/server/sqlite_store.go b/management/server/sqlite_store.go index c12ad1edd..bd3cf5f09 100644 --- a/management/server/sqlite_store.go +++ b/management/server/sqlite_store.go @@ -505,10 +505,13 @@ func (s *SqliteStore) CalculateUsageStats(ctx context.Context, accountID string, stats := &AccountUsageStats{} err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - err := tx.Model(&nbpeer.Peer{}). - Where("account_id = ? AND peer_status_last_seen BETWEEN ? AND ?", accountID, start, end). - Distinct("user_id"). - Count(&stats.ActiveUsers).Error + err := tx.Raw(` + SELECT COUNT(DISTINCT user_id) FROM ( + SELECT id as user_id FROM users WHERE account_id = ? AND last_login BETWEEN ? AND ? AND is_service_user = ? + UNION + SELECT user_id FROM peers WHERE account_id = ? AND peer_status_last_seen BETWEEN ? AND ? + ) AS combined`, accountID, start, end, false, accountID, start, end).Scan(&stats.ActiveUsers).Error + if err != nil { return fmt.Errorf("get active users: %w", err) } diff --git a/management/server/sqlite_store_test.go b/management/server/sqlite_store_test.go index e85ee211f..6e70fc165 100644 --- a/management/server/sqlite_store_test.go +++ b/management/server/sqlite_store_test.go @@ -360,15 +360,15 @@ func TestSqliteStore_CalculateUsageStats(t *testing.T) { stats1, err := store.CalculateUsageStats(context.TODO(), "account-1", startDate, endDate) require.NoError(t, err) - assert.Equal(t, int64(2), stats1.ActiveUsers) - assert.Equal(t, int64(4), stats1.TotalUsers) + assert.Equal(t, int64(3), stats1.ActiveUsers) + assert.Equal(t, int64(5), stats1.TotalUsers) assert.Equal(t, int64(3), stats1.ActivePeers) - assert.Equal(t, int64(7), stats1.TotalPeers) + assert.Equal(t, int64(8), stats1.TotalPeers) stats2, err := store.CalculateUsageStats(context.TODO(), "account-2", startDate, endDate) require.NoError(t, err) - assert.Equal(t, int64(1), stats2.ActiveUsers) + assert.Equal(t, int64(2), stats2.ActiveUsers) assert.Equal(t, int64(2), stats2.TotalUsers) assert.Equal(t, int64(1), stats2.ActivePeers) assert.Equal(t, int64(2), stats2.TotalPeers) diff --git a/management/server/testdata/store_stats.json b/management/server/testdata/store_stats.json index 747916370..36adabfb7 100644 --- a/management/server/testdata/store_stats.json +++ b/management/server/testdata/store_stats.json @@ -13,20 +13,29 @@ }, "Users": { "user-1-account-1": { - "Id": "user-1-account-1" + "Id": "user-1-account-1", + "LastLogin": "2023-01-15T00:00:00Z" }, "user-2-account-1": { - "Id": "user-2-account-1" + "Id": "user-2-account-1", + "LastLogin": "2024-01-02T00:00:00Z" }, "user-3-account-1": { - "Id": "user-3-account-1" + "Id": "user-3-account-1", + "LastLogin": "2024-02-05T00:00:00Z" }, "user-4-account-1": { - "Id": "user-4-account-1" + "Id": "user-4-account-1", + "LastLogin": "2024-01-20T00:00:00Z" }, "user-5-account-1": { "Id": "user-5-account-1", - "IsServiceUser": true + "IsServiceUser": true, + "LastLogin": "2024-02-15T00:00:00Z" + }, + "user-6-account-1": { + "Id": "user-6-account-1", + "LastLogin": "2024-02-10T00:00:00Z" } }, "Peers": { @@ -106,6 +115,17 @@ "Meta": { "Hostname": "peer7-host" } + }, + "peer-8-account-1": { + "ID": "peer-8-account-1", + "UserID": "user-6-account-1", + "Status": { + "LastSeen": "2024-01-01T00:00:00Z" + }, + "Name": "Peer Eight", + "Meta": { + "Hostname": "peer8-host" + } } } }, @@ -122,14 +142,17 @@ }, "Users": { "user-1-account-2": { - "Id": "user-1-account-2" + "Id": "user-1-account-2", + "LastLogin": "2023-12-15T00:00:00Z" }, "user-2-account-2": { - "Id": "user-1-account-2" + "Id": "user-2-account-2", + "LastLogin": "2024-02-28T00:00:00Z" }, "user-3-account-2": { "Id": "user-3-account-2", - "IsServiceUser": true + "IsServiceUser": true, + "LastLogin": "2024-01-30T00:00:00Z" } }, "Peers": {