website/pkg/models/usage_statistic.go
Noah Petherbridge 2f31d678d0 Usage Statistics and Website Demographics
Adds two new features to collect and show useful analytics.

Usage Statistics:
* Begin tracking daily active users who log in and interact with major features
  of the website each day, such as the chat room, forum and gallery.

Demographics page:
* For marketing, the home page now shows live statistics about the breakdown of
  content (explicit vs. non-explicit) on the site, and the /insights page gives
  a lot more data in detail.
* Show the percent split in photo gallery content and how many users opt-in or
  share explicit content on the site.
* Show high-level demographics of the members (by age range, gender, orientation)

Misc cleanup:
* Rearrange model list in data export to match the auto-create statements.
* In data exports, include the forum_memberships, push_notifications and
  usage_statistics tables.
2024-09-11 19:28:52 -07:00

106 lines
3.5 KiB
Go

package models
import "time"
/*
UsageStatistic holds basic analytics points for things like daily/monthly active user counts.
Generally, there will be one UserStatistic row for each combination of a UserID and Type for
each calendar day of the year. Type names may be like "dau" to log daily logins (Daily Active User),
or "chat" to log daily chat room users.
If a user logs in multiple times in the same day, their existing UsageStatistic for that day
is reused and the Counter is incremented. So if a user joins chat 3 times on the same day, there
will be a single row for that date for that user, but with a Counter of 3 in that case.
This makes it easier to query for aggregate reports on daily/monthly active users since each
row/event type combo only appears once per user per day.
*/
type UsageStatistic struct {
ID uint64 `gorm:"primaryKey"`
UserID uint64 `gorm:"uniqueIndex:idx_usage_statistics"`
Type string `gorm:"uniqueIndex:idx_usage_statistics"`
Date string `gorm:"uniqueIndex:idx_usage_statistics"` // unique days, yyyy-mm-dd format.
Counter uint64
CreatedAt time.Time `gorm:"index"` // full timestamps
UpdatedAt time.Time `gorm:"index"`
}
// Options for UsageStatistic Type values.
const (
UsageStatisticDailyVisit = "dau" // daily active user counter
UsageStatisticChatEntry = "chat" // daily chat room users
UsageStatisticForumUser = "forum" // daily forum users (when they open a thread)
UsageStatisticGalleryUser = "gallery" // daily Site Gallery user (when viewing the site gallery)
)
// LogDailyActiveUser will ping a UserStatistic for the current user to mark them present for the day.
func LogDailyActiveUser(user *User) error {
var (
date = time.Now().Format(time.DateOnly)
_, err = IncrementUsageStatistic(user, UsageStatisticDailyVisit, date)
)
return err
}
// LogDailyChatUser will ping a UserStatistic for the current user to mark them as having used the chat room today.
func LogDailyChatUser(user *User) error {
var (
date = time.Now().Format(time.DateOnly)
_, err = IncrementUsageStatistic(user, UsageStatisticChatEntry, date)
)
return err
}
// LogDailyForumUser will ping a UserStatistic for the current user to mark them as having used the forums today.
func LogDailyForumUser(user *User) error {
var (
date = time.Now().Format(time.DateOnly)
_, err = IncrementUsageStatistic(user, UsageStatisticForumUser, date)
)
return err
}
// LogDailyGalleryUser will ping a UserStatistic for the current user to mark them as having used the site gallery today.
func LogDailyGalleryUser(user *User) error {
var (
date = time.Now().Format(time.DateOnly)
_, err = IncrementUsageStatistic(user, UsageStatisticGalleryUser, date)
)
return err
}
// GetUsageStatistic looks up a user statistic.
func GetUsageStatistic(user *User, statType, date string) (*UsageStatistic, error) {
var (
result = &UsageStatistic{}
res = DB.Model(&UsageStatistic{}).Where(
"user_id = ? AND type = ? AND date = ?",
user.ID, statType, date,
).First(&result)
)
return result, res.Error
}
// IncrementUsageStatistic finds or creates a UserStatistic type and increments the counter.
func IncrementUsageStatistic(user *User, statType, date string) (*UsageStatistic, error) {
user.muStatistic.Lock()
defer user.muStatistic.Unlock()
// Is there an existing row?
stat, err := GetUsageStatistic(user, statType, date)
if err != nil {
stat = &UsageStatistic{
UserID: user.ID,
Type: statType,
Counter: 0,
Date: date,
}
}
// Update and save it.
stat.Counter++
err = DB.Save(stat).Error
return stat, err
}