storing analytics data

This commit is contained in:
2026-02-27 09:46:51 +01:00
parent 15b7b77d20
commit f0cca76eb3
57 changed files with 3478 additions and 466 deletions

View File

@@ -23,26 +23,56 @@ class ArtworkStatsService
/**
* Increment views for an artwork.
* Set $defer=true to push to Redis for async processing when available.
* Both all-time (views) and windowed (views_24h, views_7d) are updated.
*/
public function incrementViews(int $artworkId, int $by = 1, bool $defer = false): void
{
if ($defer && $this->redisAvailable()) {
$this->pushDelta($artworkId, 'views', $by);
$this->pushDelta($artworkId, 'views', $by);
$this->pushDelta($artworkId, 'views_24h', $by);
$this->pushDelta($artworkId, 'views_7d', $by);
return;
}
$this->applyDelta($artworkId, ['views' => $by]);
$this->applyDelta($artworkId, ['views' => $by, 'views_24h' => $by, 'views_7d' => $by]);
}
/**
* Increment downloads for an artwork.
* Both all-time (downloads) and windowed (downloads_24h, downloads_7d) are updated.
*/
public function incrementDownloads(int $artworkId, int $by = 1, bool $defer = false): void
{
if ($defer && $this->redisAvailable()) {
$this->pushDelta($artworkId, 'downloads', $by);
$this->pushDelta($artworkId, 'downloads', $by);
$this->pushDelta($artworkId, 'downloads_24h', $by);
$this->pushDelta($artworkId, 'downloads_7d', $by);
return;
}
$this->applyDelta($artworkId, ['downloads' => $by]);
$this->applyDelta($artworkId, ['downloads' => $by, 'downloads_24h' => $by, 'downloads_7d' => $by]);
}
/**
* Write one row to artwork_view_events (the persistent event log).
*
* Called from ArtworkViewController after session dedup passes.
* Guests (unauthenticated) are recorded with user_id = null.
* Rows are pruned after 90 days by skinbase:prune-view-events.
*/
public function logViewEvent(int $artworkId, ?int $userId): void
{
try {
DB::table('artwork_view_events')->insert([
'artwork_id' => $artworkId,
'user_id' => $userId,
'viewed_at' => now(),
]);
} catch (Throwable $e) {
Log::warning('Failed to write artwork_view_events row', [
'artwork_id' => $artworkId,
'user_id' => $userId,
'error' => $e->getMessage(),
]);
}
}
/**
@@ -75,17 +105,21 @@ class ArtworkStatsService
DB::transaction(function () use ($artworkId, $deltas) {
// Ensure a stats row exists — insert default zeros if missing.
DB::table('artwork_stats')->insertOrIgnore([
'artwork_id' => $artworkId,
'views' => 0,
'downloads' => 0,
'favorites' => 0,
'rating_avg' => 0,
'rating_count' => 0,
'artwork_id' => $artworkId,
'views' => 0,
'views_24h' => 0,
'views_7d' => 0,
'downloads' => 0,
'downloads_24h' => 0,
'downloads_7d' => 0,
'favorites' => 0,
'rating_avg' => 0,
'rating_count' => 0,
]);
foreach ($deltas as $column => $value) {
// Only allow known columns to avoid SQL injection.
if (! in_array($column, ['views', 'downloads', 'favorites', 'rating_count'], true)) {
if (! in_array($column, ['views', 'views_24h', 'views_7d', 'downloads', 'downloads_24h', 'downloads_7d', 'favorites', 'rating_count'], true)) {
continue;
}