Skip to content
This repository has been archived by the owner on Jul 12, 2023. It is now read-only.

Commit

Permalink
Add new stats for codes_invalid and tokens to responses (#1631)
Browse files Browse the repository at this point in the history
* Add new stats for codes_invalid and tokens to responses

These statistics already exist and are collected in 0.20. This adds them to the response payload.

* I don't know how this tick keeps getting removed, but it's starting to annoy me
  • Loading branch information
sethvargo authored Jan 19, 2021
1 parent 7f4039a commit b5b150c
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 76 deletions.
4 changes: 2 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ eg.
error: "the first code failed",
errorCode: "missing_date",
}
``
```

**BatchIssueCodeRequest**

Expand Down Expand Up @@ -468,7 +468,7 @@ notice!**
This path includes realm-level statistics for the past 30 days.

- `/api/stats/realm.{csv,json}` - Daily statistics for the realm, including
codes issued, codes claimed, and daily active users (if enabled).
codes issued, codes claimed, tokens claimed, and invalid attempts.

- `/api/stats/realm/users.{csv,json}` - Daily statistics for codes issued by
realm user. These statistics only include codes issued by humans logged into
Expand Down
24 changes: 1 addition & 23 deletions pkg/controller/apikey/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,18 @@
package apikey

import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"time"

"github.com/google/exposure-notifications-verification-server/internal/project"
"github.com/google/exposure-notifications-verification-server/pkg/cache"
"github.com/google/exposure-notifications-verification-server/pkg/controller"
"github.com/google/exposure-notifications-verification-server/pkg/database"
"github.com/google/exposure-notifications-verification-server/pkg/rbac"
"github.com/gorilla/mux"
)

const statsCacheTimeout = 30 * time.Minute

func (c *Controller) HandleStats() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
Expand Down Expand Up @@ -66,7 +61,7 @@ func (c *Controller) HandleStats() http.Handler {
return
}

stats, err := c.getStats(ctx, authApp, currentRealm)
stats, err := authApp.StatsCached(ctx, c.db, c.cacher)
if err != nil {
controller.InternalError(w, r, c.h, err)
return
Expand All @@ -88,20 +83,3 @@ func (c *Controller) HandleStats() http.Handler {
}
})
}

func (c *Controller) getStats(ctx context.Context, authApp *database.AuthorizedApp, realm *database.Realm) (database.AuthorizedAppStats, error) {
now := time.Now().UTC()
past := now.Add(-30 * 24 * time.Hour)

var stats database.AuthorizedAppStats
cacheKey := &cache.Key{
Namespace: "stats:app",
Key: strconv.FormatUint(uint64(authApp.ID), 10),
}
if err := c.cacher.Fetch(ctx, cacheKey, &stats, statsCacheTimeout, func() (interface{}, error) {
return authApp.Stats(c.db, past, now)
}); err != nil {
return nil, err
}
return stats, nil
}
34 changes: 29 additions & 5 deletions pkg/database/authorized_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package database

import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha512"
Expand All @@ -27,6 +28,7 @@ import (
"github.com/google/exposure-notifications-server/pkg/base64util"
"github.com/google/exposure-notifications-server/pkg/timeutils"
"github.com/google/exposure-notifications-verification-server/internal/project"
"github.com/google/exposure-notifications-verification-server/pkg/cache"
"github.com/jinzhu/gorm"
)

Expand Down Expand Up @@ -192,10 +194,9 @@ func (db *Database) FindAuthorizedAppByAPIKey(apiKey string) (*AuthorizedApp, er

// Stats returns the usage statistics for this app. If no stats exist, it
// returns an empty array.
func (a *AuthorizedApp) Stats(db *Database, start, stop time.Time) (AuthorizedAppStats, error) {
start = timeutils.UTCMidnight(start)
stop = timeutils.UTCMidnight(stop)

func (a *AuthorizedApp) Stats(db *Database) (AuthorizedAppStats, error) {
stop := timeutils.Midnight(time.Now().UTC())
start := stop.Add(30 * -24 * time.Hour)
if start.After(stop) {
return nil, ErrBadDateRange
}
Expand All @@ -208,7 +209,11 @@ func (a *AuthorizedApp) Stats(db *Database, start, stop time.Time) (AuthorizedAp
d.date AS date,
$1 AS authorized_app_id,
$2 AS authorized_app_name,
COALESCE(s.codes_issued, 0) AS codes_issued
COALESCE(s.codes_issued, 0) AS codes_issued,
COALESCE(s.codes_claimed, 0) AS codes_claimed,
COALESCE(s.codes_invalid, 0) AS codes_invalid,
COALESCE(s.tokens_claimed, 0) AS tokens_claimed,
COALESCE(s.tokens_invalid, 0) AS tokens_invalid
FROM (
SELECT date::date FROM generate_series($3, $4, '1 day'::interval) date
) d
Expand All @@ -225,6 +230,25 @@ func (a *AuthorizedApp) Stats(db *Database, start, stop time.Time) (AuthorizedAp
return stats, nil
}

// StatsCached is stats, but cached.
func (a *AuthorizedApp) StatsCached(ctx context.Context, db *Database, cacher cache.Cacher) (AuthorizedAppStats, error) {
if cacher == nil {
return nil, fmt.Errorf("cacher cannot be nil")
}

var stats AuthorizedAppStats
cacheKey := &cache.Key{
Namespace: "stats:app",
Key: strconv.FormatUint(uint64(a.ID), 10),
}
if err := cacher.Fetch(ctx, cacheKey, &stats, 30*time.Minute, func() (interface{}, error) {
return a.Stats(db)
}); err != nil {
return nil, err
}
return stats, nil
}

// SaveAuthorizedApp saves the authorized app.
func (db *Database) SaveAuthorizedApp(a *AuthorizedApp, actor Auditable) error {
if a == nil {
Expand Down
26 changes: 23 additions & 3 deletions pkg/database/authorized_app_stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ func (s AuthorizedAppStats) MarshalCSV() ([]byte, error) {
var b bytes.Buffer
w := csv.NewWriter(&b)

if err := w.Write([]string{"date", "authorized_app_id", "authorized_app_name", "codes_issued"}); err != nil {
if err := w.Write([]string{
"date", "authorized_app_id", "authorized_app_name",
"codes_issued", "codes_claimed", "codes_invalid",
"tokens_claimed", "tokens_invalid",
}); err != nil {
return nil, fmt.Errorf("failed to write CSV header: %w", err)
}

Expand All @@ -76,6 +80,10 @@ func (s AuthorizedAppStats) MarshalCSV() ([]byte, error) {
strconv.FormatUint(uint64(stat.AuthorizedAppID), 10),
stat.AuthorizedAppName,
strconv.FormatUint(uint64(stat.CodesIssued), 10),
strconv.FormatUint(uint64(stat.CodesClaimed), 10),
strconv.FormatUint(uint64(stat.CodesInvalid), 10),
strconv.FormatUint(uint64(stat.TokensClaimed), 10),
strconv.FormatUint(uint64(stat.TokensInvalid), 10),
}); err != nil {
return nil, fmt.Errorf("failed to write CSV entry %d: %w", i, err)
}
Expand All @@ -101,7 +109,11 @@ type jsonAuthorizedAppStatstats struct {
}

type jsonAuthorizedAppStatstatsData struct {
CodesIssued uint `json:"codes_issued"`
CodesIssued uint `json:"codes_issued"`
CodesClaimed uint `json:"codes_claimed"`
CodesInvalid uint `json:"codes_invalid"`
TokensClaimed uint `json:"tokens_claimed"`
TokensInvalid uint `json:"tokens_invalid"`
}

// MarshalJSON is a custom JSON marshaller.
Expand All @@ -116,7 +128,11 @@ func (s AuthorizedAppStats) MarshalJSON() ([]byte, error) {
stats = append(stats, &jsonAuthorizedAppStatstats{
Date: stat.Date,
Data: &jsonAuthorizedAppStatstatsData{
CodesIssued: stat.CodesIssued,
CodesIssued: stat.CodesIssued,
CodesClaimed: stat.CodesClaimed,
CodesInvalid: stat.CodesInvalid,
TokensClaimed: stat.TokensClaimed,
TokensInvalid: stat.TokensInvalid,
},
})
}
Expand Down Expand Up @@ -154,6 +170,10 @@ func (s *AuthorizedAppStats) UnmarshalJSON(b []byte) error {
AuthorizedAppID: result.AuthorizedAppID,
AuthorizedAppName: result.AuthorizedAppName,
CodesIssued: stat.Data.CodesIssued,
CodesClaimed: stat.Data.CodesClaimed,
CodesInvalid: stat.Data.CodesInvalid,
TokensClaimed: stat.Data.TokensClaimed,
TokensInvalid: stat.Data.TokensInvalid,
})
}

Expand Down
28 changes: 22 additions & 6 deletions pkg/database/authorized_app_stats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,15 @@ func TestAuthorizedAppStats_MarshalCSV(t *testing.T) {
Date: time.Date(2020, 2, 3, 0, 0, 0, 0, time.UTC),
AuthorizedAppID: 1,
CodesIssued: 10,
CodesClaimed: 4,
CodesInvalid: 2,
TokensClaimed: 3,
TokensInvalid: 1,
AuthorizedAppName: "Appy",
},
},
exp: `date,authorized_app_id,authorized_app_name,codes_issued
2020-02-03,1,Appy,10
exp: `date,authorized_app_id,authorized_app_name,codes_issued,codes_claimed,codes_invalid,tokens_claimed,tokens_invalid
2020-02-03,1,Appy,10,4,2,3,1
`,
},
{
Expand All @@ -55,25 +59,37 @@ func TestAuthorizedAppStats_MarshalCSV(t *testing.T) {
Date: time.Date(2020, 2, 3, 0, 0, 0, 0, time.UTC),
AuthorizedAppID: 1,
CodesIssued: 10,
CodesClaimed: 10,
CodesInvalid: 2,
TokensClaimed: 4,
TokensInvalid: 2,
AuthorizedAppName: "Appy",
},
{
Date: time.Date(2020, 2, 4, 0, 0, 0, 0, time.UTC),
AuthorizedAppID: 1,
CodesIssued: 45,
CodesClaimed: 44,
CodesInvalid: 5,
TokensClaimed: 3,
TokensInvalid: 2,
AuthorizedAppName: "Mc",
},
{
Date: time.Date(2020, 2, 5, 0, 0, 0, 0, time.UTC),
AuthorizedAppID: 1,
CodesIssued: 15,
CodesClaimed: 13,
CodesInvalid: 4,
TokensClaimed: 6,
TokensInvalid: 2,
AuthorizedAppName: "Apperson",
},
},
exp: `date,authorized_app_id,authorized_app_name,codes_issued
2020-02-03,1,Appy,10
2020-02-04,1,Mc,45
2020-02-05,1,Apperson,15
exp: `date,authorized_app_id,authorized_app_name,codes_issued,codes_claimed,codes_invalid,tokens_claimed,tokens_invalid
2020-02-03,1,Appy,10,10,2,4,2
2020-02-04,1,Mc,45,44,5,3,2
2020-02-05,1,Apperson,15,13,4,6,2
`,
},
}
Expand Down
7 changes: 2 additions & 5 deletions pkg/database/authorized_app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"testing"
"time"

"github.com/google/exposure-notifications-server/pkg/timeutils"
"github.com/google/exposure-notifications-verification-server/pkg/pagination"
"github.com/jinzhu/gorm"
)
Expand Down Expand Up @@ -164,13 +163,11 @@ func TestAuthorizedApp_Stats(t *testing.T) {

// Ensure graph is contiguous.
{
stop := timeutils.Midnight(time.Now().UTC())
start := stop.Add(6 * -24 * time.Hour)
stats, err := authorizedApp.Stats(db, start, stop)
stats, err := authorizedApp.Stats(db)
if err != nil {
t.Fatal(err)
}
if got, want := len(stats), 7; got != want {
if got, want := len(stats), 30; got < want {
t.Errorf("expected stats for %d days, got %d", want, got)
}
}
Expand Down
5 changes: 4 additions & 1 deletion pkg/database/realm.go
Original file line number Diff line number Diff line change
Expand Up @@ -1443,7 +1443,10 @@ func (r *Realm) Stats(db *Database) (RealmStats, error) {
d.date AS date,
$1 AS realm_id,
COALESCE(s.codes_issued, 0) AS codes_issued,
COALESCE(s.codes_claimed, 0) AS codes_claimed
COALESCE(s.codes_claimed, 0) AS codes_claimed,
COALESCE(s.codes_invalid, 0) AS codes_invalid,
COALESCE(s.tokens_claimed, 0) AS tokens_claimed,
COALESCE(s.tokens_invalid, 0) AS tokens_invalid
FROM (
SELECT date::date FROM generate_series($2, $3, '1 day'::interval) date
) d
Expand Down
34 changes: 25 additions & 9 deletions pkg/database/realm_stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ func (s RealmStats) MarshalCSV() ([]byte, error) {
var b bytes.Buffer
w := csv.NewWriter(&b)

if err := w.Write([]string{"date", "codes_issued", "codes_claimed"}); err != nil {
if err := w.Write([]string{
"date",
"codes_issued", "codes_claimed", "codes_invalid",
"tokens_claimed", "tokens_invalid",
}); err != nil {
return nil, fmt.Errorf("failed to write CSV header: %w", err)
}

Expand All @@ -70,6 +74,9 @@ func (s RealmStats) MarshalCSV() ([]byte, error) {
stat.Date.Format(project.RFC3339Date),
strconv.FormatUint(uint64(stat.CodesIssued), 10),
strconv.FormatUint(uint64(stat.CodesClaimed), 10),
strconv.FormatUint(uint64(stat.CodesInvalid), 10),
strconv.FormatUint(uint64(stat.TokensClaimed), 10),
strconv.FormatUint(uint64(stat.TokensInvalid), 10),
}); err != nil {
return nil, fmt.Errorf("failed to write CSV entry %d: %w", i, err)
}
Expand All @@ -94,8 +101,11 @@ type jsonRealmStatStats struct {
}

type jsonRealmStatStatsData struct {
CodesIssued uint `json:"codes_issued"`
CodesClaimed uint `json:"codes_claimed"`
CodesIssued uint `json:"codes_issued"`
CodesClaimed uint `json:"codes_claimed"`
CodesInvalid uint `json:"codes_invalid"`
TokensClaimed uint `json:"tokens_claimed"`
TokensInvalid uint `json:"tokens_invalid"`
}

// MarshalJSON is a custom JSON marshaller.
Expand All @@ -110,8 +120,11 @@ func (s RealmStats) MarshalJSON() ([]byte, error) {
stats = append(stats, &jsonRealmStatStats{
Date: stat.Date,
Data: &jsonRealmStatStatsData{
CodesIssued: stat.CodesIssued,
CodesClaimed: stat.CodesClaimed,
CodesIssued: stat.CodesIssued,
CodesClaimed: stat.CodesClaimed,
CodesInvalid: stat.CodesInvalid,
TokensClaimed: stat.TokensClaimed,
TokensInvalid: stat.TokensInvalid,
},
})
}
Expand Down Expand Up @@ -144,10 +157,13 @@ func (s *RealmStats) UnmarshalJSON(b []byte) error {

for _, stat := range result.Stats {
*s = append(*s, &RealmStat{
Date: stat.Date,
RealmID: result.RealmID,
CodesIssued: stat.Data.CodesIssued,
CodesClaimed: stat.Data.CodesClaimed,
Date: stat.Date,
RealmID: result.RealmID,
CodesIssued: stat.Data.CodesIssued,
CodesClaimed: stat.Data.CodesClaimed,
CodesInvalid: stat.Data.CodesInvalid,
TokensClaimed: stat.Data.TokensClaimed,
TokensInvalid: stat.Data.TokensInvalid,
})
}

Expand Down
Loading

0 comments on commit b5b150c

Please sign in to comment.