Ship production optimization M1-M12.5A: queues, metrics, HTTP observability, and vector search reliability.
Keep similar-ai from tripping the global circuit on a lone URL 502, clamp Qdrant search to 100, and add Server-Timing plus slow-request logging. Studio shared props, Academy S3 exists caching, heat chunking, and Redis/scheduler hygiene stay in this rollout.
This commit is contained in:
@@ -6,6 +6,7 @@ namespace App\Services\Studio;
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ArtworkStats;
|
||||
use App\Services\Metrics\ArtworkHourlySnapshotWindow;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
@@ -16,10 +17,26 @@ final class StudioMetricsService
|
||||
{
|
||||
private const CACHE_TTL = 300; // 5 minutes
|
||||
|
||||
public function __construct(
|
||||
private readonly ArtworkHourlySnapshotWindow $snapshotWindow,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dashboard KPI metrics for a creator.
|
||||
*
|
||||
* @return array{total_artworks: int, views_30d: int, favourites_30d: int, shares_30d: int, followers: int}
|
||||
* 30-day counters are latest-minus-baseline deltas of cumulative hourly snapshots, never SUM.
|
||||
*
|
||||
* @return array{
|
||||
* total_artworks: int,
|
||||
* views_30d: int,
|
||||
* favourites_30d: int,
|
||||
* shares_30d: int,
|
||||
* downloads_30d: int,
|
||||
* comments_30d: int,
|
||||
* followers: int,
|
||||
* snapshot_window: array<string, mixed>
|
||||
* }
|
||||
*/
|
||||
public function getDashboardKpis(int $userId): array
|
||||
{
|
||||
@@ -30,35 +47,7 @@ final class StudioMetricsService
|
||||
->whereNull('deleted_at')
|
||||
->count();
|
||||
|
||||
// Aggregate stats from artwork_stats for this user's artworks
|
||||
$statsAgg = DB::table('artwork_stats')
|
||||
->join('artworks', 'artworks.id', '=', 'artwork_stats.artwork_id')
|
||||
->where('artworks.user_id', $userId)
|
||||
->whereNull('artworks.deleted_at')
|
||||
->selectRaw('
|
||||
COALESCE(SUM(artwork_stats.views), 0) as total_views,
|
||||
COALESCE(SUM(artwork_stats.favorites), 0) as total_favourites,
|
||||
COALESCE(SUM(artwork_stats.shares_count), 0) as total_shares
|
||||
')
|
||||
->first();
|
||||
|
||||
// Views in last 30 days from hourly snapshots if available, fallback to totals
|
||||
$views30d = 0;
|
||||
try {
|
||||
if (\Illuminate\Support\Facades\Schema::hasTable('artwork_metric_snapshots_hourly')) {
|
||||
$views30d = (int) DB::table('artwork_metric_snapshots_hourly')
|
||||
->join('artworks', 'artworks.id', '=', 'artwork_metric_snapshots_hourly.artwork_id')
|
||||
->where('artworks.user_id', $userId)
|
||||
->where('artwork_metric_snapshots_hourly.bucket_hour', '>=', now()->subDays(30))
|
||||
->sum('artwork_metric_snapshots_hourly.views_count');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Table or column doesn't exist — fall back to totals
|
||||
}
|
||||
|
||||
if ($views30d === 0) {
|
||||
$views30d = (int) ($statsAgg->total_views ?? 0);
|
||||
}
|
||||
$window = $this->snapshotWindow->userWindow($userId, 30);
|
||||
|
||||
$followers = DB::table('user_followers')
|
||||
->where('user_id', $userId)
|
||||
@@ -66,10 +55,20 @@ final class StudioMetricsService
|
||||
|
||||
return [
|
||||
'total_artworks' => $totalArtworks,
|
||||
'views_30d' => $views30d,
|
||||
'favourites_30d' => (int) ($statsAgg->total_favourites ?? 0),
|
||||
'shares_30d' => (int) ($statsAgg->total_shares ?? 0),
|
||||
'followers' => $followers,
|
||||
'views_30d' => $window['views'],
|
||||
'favourites_30d' => $window['favourites'],
|
||||
'shares_30d' => $window['shares'],
|
||||
'downloads_30d' => $window['downloads'],
|
||||
'comments_30d' => $window['comments'],
|
||||
'followers' => $followers,
|
||||
'snapshot_window' => [
|
||||
'requested_days' => $window['requested_days'],
|
||||
'table_coverage_days' => $window['table_coverage_days'],
|
||||
'user_coverage_days' => $window['user_coverage_days'],
|
||||
'window_complete' => $window['window_complete'],
|
||||
'oldest_bucket' => $window['oldest_bucket'],
|
||||
'newest_bucket' => $window['newest_bucket'],
|
||||
],
|
||||
];
|
||||
});
|
||||
}
|
||||
@@ -145,7 +144,6 @@ final class StudioMetricsService
|
||||
$cacheKey = "studio.analytics_overview.{$userId}";
|
||||
|
||||
return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($userId) {
|
||||
// Totals
|
||||
$totals = DB::table('artwork_stats')
|
||||
->join('artworks', 'artworks.id', '=', 'artwork_stats.artwork_id')
|
||||
->where('artworks.user_id', $userId)
|
||||
@@ -161,7 +159,6 @@ final class StudioMetricsService
|
||||
')
|
||||
->first();
|
||||
|
||||
// Top 10 artworks by ranking score
|
||||
$topArtworks = Artwork::where('user_id', $userId)
|
||||
->whereNull('deleted_at')
|
||||
->where('is_public', true)
|
||||
@@ -188,7 +185,6 @@ final class StudioMetricsService
|
||||
'heat_score' => (float) ($art->stats?->heat_score ?? 0),
|
||||
]);
|
||||
|
||||
// Content type breakdown
|
||||
$contentBreakdown = DB::table('artworks')
|
||||
->join('artwork_category', 'artwork_category.artwork_id', '=', 'artworks.id')
|
||||
->join('categories', 'categories.id', '=', 'artwork_category.category_id')
|
||||
|
||||
Reference in New Issue
Block a user