feat: expose active user counts in API
This commit is contained in:
		
							parent
							
								
									e8d9ccb1ac
								
							
						
					
					
						commit
						de460720da
					
				
					 4 changed files with 79 additions and 12 deletions
				
			
		|  | @ -45,7 +45,35 @@ func (db *DB) initMetrics() (err error) { | ||||||
| 		Name: "pronouns_users_active", | 		Name: "pronouns_users_active", | ||||||
| 		Help: "The number of users active in the past 30 days", | 		Help: "The number of users active in the past 30 days", | ||||||
| 	}, func() float64 { | 	}, func() float64 { | ||||||
| 		count, err := db.ActiveUsers(context.Background()) | 		count, err := db.ActiveUsers(context.Background(), ActiveMonth) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Errorf("getting active user count for metrics: %v", err) | ||||||
|  | 		} | ||||||
|  | 		return float64(count) | ||||||
|  | 	})) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return errors.Wrap(err, "registering active user count gauge") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err = prometheus.Register(prometheus.NewGaugeFunc(prometheus.GaugeOpts{ | ||||||
|  | 		Name: "pronouns_users_active_week", | ||||||
|  | 		Help: "The number of users active in the past 7 days", | ||||||
|  | 	}, func() float64 { | ||||||
|  | 		count, err := db.ActiveUsers(context.Background(), ActiveWeek) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Errorf("getting active user count for metrics: %v", err) | ||||||
|  | 		} | ||||||
|  | 		return float64(count) | ||||||
|  | 	})) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return errors.Wrap(err, "registering active user count gauge") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	err = prometheus.Register(prometheus.NewGaugeFunc(prometheus.GaugeOpts{ | ||||||
|  | 		Name: "pronouns_users_active_day", | ||||||
|  | 		Help: "The number of users active in the past 1 day", | ||||||
|  | 	}, func() float64 { | ||||||
|  | 		count, err := db.ActiveUsers(context.Background(), ActiveDay) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Errorf("getting active user count for metrics: %v", err) | 			log.Errorf("getting active user count for metrics: %v", err) | ||||||
| 		} | 		} | ||||||
|  | @ -95,10 +123,14 @@ func (db *DB) TotalMemberCount(ctx context.Context) (numMembers int64, err error | ||||||
| 	return numMembers, nil | 	return numMembers, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const activeTime = 30 * 24 * time.Hour | const ( | ||||||
|  | 	ActiveMonth = 30 * 24 * time.Hour | ||||||
|  | 	ActiveWeek  = 7 * 24 * time.Hour | ||||||
|  | 	ActiveDay   = 24 * time.Hour | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| func (db *DB) ActiveUsers(ctx context.Context) (numUsers int64, err error) { | func (db *DB) ActiveUsers(ctx context.Context, dur time.Duration) (numUsers int64, err error) { | ||||||
| 	t := time.Now().Add(-activeTime) | 	t := time.Now().Add(-dur) | ||||||
| 	err = db.QueryRow(ctx, "SELECT COUNT(*) FROM users WHERE deleted_at IS NULL AND last_active > $1", t).Scan(&numUsers) | 	err = db.QueryRow(ctx, "SELECT COUNT(*) FROM users WHERE deleted_at IS NULL AND last_active > $1", t).Scan(&numUsers) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return 0, errors.Wrap(err, "querying active user count") | 		return 0, errors.Wrap(err, "querying active user count") | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"os" | 	"os" | ||||||
| 
 | 
 | ||||||
|  | 	"codeberg.org/u1f320/pronouns.cc/backend/db" | ||||||
| 	"codeberg.org/u1f320/pronouns.cc/backend/server" | 	"codeberg.org/u1f320/pronouns.cc/backend/server" | ||||||
| 	"emperror.dev/errors" | 	"emperror.dev/errors" | ||||||
| 	"github.com/go-chi/chi/v5" | 	"github.com/go-chi/chi/v5" | ||||||
|  | @ -21,11 +22,18 @@ func Mount(srv *server.Server, r chi.Router) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type MetaResponse struct { | type MetaResponse struct { | ||||||
| 	GitRepository string `json:"git_repository"` | 	GitRepository string    `json:"git_repository"` | ||||||
| 	GitCommit     string `json:"git_commit"` | 	GitCommit     string    `json:"git_commit"` | ||||||
| 	Users         int64  `json:"users"` | 	Users         MetaUsers `json:"users"` | ||||||
| 	Members       int64  `json:"members"` | 	Members       int64     `json:"members"` | ||||||
| 	RequireInvite bool   `json:"require_invite"` | 	RequireInvite bool      `json:"require_invite"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type MetaUsers struct { | ||||||
|  | 	Total       int64 `json:"total"` | ||||||
|  | 	ActiveMonth int64 `json:"active_month"` | ||||||
|  | 	ActiveWeek  int64 `json:"active_week"` | ||||||
|  | 	ActiveDay   int64 `json:"active_day"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *Server) meta(w http.ResponseWriter, r *http.Request) error { | func (s *Server) meta(w http.ResponseWriter, r *http.Request) error { | ||||||
|  | @ -36,6 +44,21 @@ func (s *Server) meta(w http.ResponseWriter, r *http.Request) error { | ||||||
| 		return errors.Wrap(err, "querying user count") | 		return errors.Wrap(err, "querying user count") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	activeMonth, err := s.DB.ActiveUsers(ctx, db.ActiveMonth) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return errors.Wrap(err, "querying user count") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	activeWeek, err := s.DB.ActiveUsers(ctx, db.ActiveWeek) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return errors.Wrap(err, "querying user count") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	activeDay, err := s.DB.ActiveUsers(ctx, db.ActiveDay) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return errors.Wrap(err, "querying user count") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	numMembers, err := s.DB.TotalMemberCount(ctx) | 	numMembers, err := s.DB.TotalMemberCount(ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return errors.Wrap(err, "querying user count") | 		return errors.Wrap(err, "querying user count") | ||||||
|  | @ -44,7 +67,12 @@ func (s *Server) meta(w http.ResponseWriter, r *http.Request) error { | ||||||
| 	render.JSON(w, r, MetaResponse{ | 	render.JSON(w, r, MetaResponse{ | ||||||
| 		GitRepository: server.Repository, | 		GitRepository: server.Repository, | ||||||
| 		GitCommit:     server.Revision, | 		GitCommit:     server.Revision, | ||||||
| 		Users:         numUsers, | 		Users: MetaUsers{ | ||||||
|  | 			Total:       numUsers, | ||||||
|  | 			ActiveMonth: activeMonth, | ||||||
|  | 			ActiveWeek:  activeWeek, | ||||||
|  | 			ActiveDay:   activeDay, | ||||||
|  | 		}, | ||||||
| 		Members:       numMembers, | 		Members:       numMembers, | ||||||
| 		RequireInvite: os.Getenv("REQUIRE_INVITE") == "true", | 		RequireInvite: os.Getenv("REQUIRE_INVITE") == "true", | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|  | @ -8,11 +8,18 @@ export interface SignupResponse { | ||||||
| export interface MetaResponse { | export interface MetaResponse { | ||||||
|   git_repository: string; |   git_repository: string; | ||||||
|   git_commit: string; |   git_commit: string; | ||||||
|   users: number; |   users: MetaUsers; | ||||||
|   members: number; |   members: number; | ||||||
|   require_invite: boolean; |   require_invite: boolean; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export interface MetaUsers { | ||||||
|  |   total: number; | ||||||
|  |   active_month: number; | ||||||
|  |   active_week: number; | ||||||
|  |   active_day: number; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export interface UrlsResponse { | export interface UrlsResponse { | ||||||
|   discord?: string; |   discord?: string; | ||||||
|   tumblr?: string; |   tumblr?: string; | ||||||
|  |  | ||||||
|  | @ -61,7 +61,7 @@ | ||||||
|         <a href="/page/privacy">Privacy policy</a> |         <a href="/page/privacy">Privacy policy</a> | ||||||
|       </p> |       </p> | ||||||
|       <p class="ms-auto"> |       <p class="ms-auto"> | ||||||
|         Users: <strong>{data.users}</strong> · Members: <strong>{data.members}</strong> |         Users: <strong>{data.users.total}</strong> · Members: <strong>{data.members}</strong> | ||||||
|       </p> |       </p> | ||||||
|     </div> |     </div> | ||||||
|   </footer> |   </footer> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue