Issues (510)

app/Console/Commands/BackfillUserActivityStats.php (2 issues)

Labels
Severity
1
<?php
2
3
namespace App\Console\Commands;
4
5
use App\Models\UserActivityStat;
6
use App\Models\UserDownload;
7
use App\Models\UserRequest;
8
use Carbon\Carbon;
9
use Illuminate\Console\Command;
10
use Illuminate\Support\Facades\DB;
11
12
class BackfillUserActivityStats extends Command
13
{
14
    /**
15
     * The name and signature of the console command.
16
     *
17
     * @var string
18
     */
19
    protected $signature = 'nntmux:backfill-user-activity-stats
20
                            {--type=daily : Type of stats to backfill (daily or hourly)}
21
                            {--days=30 : Number of days to backfill (for daily stats)}
22
                            {--hours=24 : Number of hours to backfill (for hourly stats)}
23
                            {--force : Force backfill even if data already exists}';
24
25
    /**
26
     * The console command description.
27
     *
28
     * @var string
29
     */
30
    protected $description = 'Backfill user activity stats (daily or hourly) from existing user_downloads and user_requests data';
31
32
    /**
33
     * Execute the console command.
34
     */
35
    public function handle(): int
36
    {
37
        $type = $this->option('type');
38
        $force = $this->option('force');
39
40
        if (! in_array($type, ['daily', 'hourly'])) {
41
            $this->error("Invalid type '{$type}'. Must be 'daily' or 'hourly'.");
42
43
            return Command::FAILURE;
44
        }
45
46
        if ($type === 'daily') {
47
            return $this->backfillDaily($force);
0 ignored issues
show
$force of type string is incompatible with the type boolean expected by parameter $force of App\Console\Commands\Bac...yStats::backfillDaily(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

47
            return $this->backfillDaily(/** @scrutinizer ignore-type */ $force);
Loading history...
48
        } else {
49
            return $this->backfillHourly($force);
0 ignored issues
show
$force of type string is incompatible with the type boolean expected by parameter $force of App\Console\Commands\Bac...Stats::backfillHourly(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

49
            return $this->backfillHourly(/** @scrutinizer ignore-type */ $force);
Loading history...
50
        }
51
    }
52
53
    /**
54
     * Backfill daily stats
55
     */
56
    protected function backfillDaily(bool $force): int
57
    {
58
        $days = (int) $this->option('days');
59
60
        $this->info("Backfilling daily user activity stats for the last {$days} days...");
61
62
        $progressBar = $this->output->createProgressBar($days);
63
64
        $statsCollected = 0;
65
        $statsSkipped = 0;
66
67
        for ($i = $days - 1; $i >= 0; $i--) {
68
            $date = Carbon::now()->subDays($i)->format('Y-m-d');
69
70
            // Check if stats already exist for this date
71
            if (! $force && UserActivityStat::where('stat_date', $date)->exists()) {
72
                $statsSkipped++;
73
                $progressBar->advance();
74
75
                continue;
76
            }
77
78
            // Count downloads for the date
79
            $downloadsCount = UserDownload::query()
80
                ->whereRaw('DATE(timestamp) = ?', [$date])
81
                ->count();
82
83
            // Count API hits for the date
84
            $apiHitsCount = UserRequest::query()
85
                ->whereRaw('DATE(timestamp) = ?', [$date])
86
                ->count();
87
88
            // Store or update the stats
89
            UserActivityStat::updateOrCreate(
90
                ['stat_date' => $date],
91
                [
92
                    'downloads_count' => $downloadsCount,
93
                    'api_hits_count' => $apiHitsCount,
94
                ]
95
            );
96
97
            $statsCollected++;
98
            $progressBar->advance();
99
        }
100
101
        $progressBar->finish();
102
        $this->newLine(2);
103
104
        $this->info('Daily backfill complete!');
105
        $this->info("Stats collected: {$statsCollected}");
106
        if ($statsSkipped > 0) {
107
            $this->info("Stats skipped (already existed): {$statsSkipped}");
108
        }
109
110
        return Command::SUCCESS;
111
    }
112
113
    /**
114
     * Backfill hourly stats
115
     */
116
    protected function backfillHourly(bool $force): int
117
    {
118
        $hours = (int) $this->option('hours');
119
120
        $this->info("Backfilling hourly user activity stats for the last {$hours} hours...");
121
122
        $progressBar = $this->output->createProgressBar($hours);
123
124
        $statsCollected = 0;
125
        $statsSkipped = 0;
126
127
        for ($i = $hours - 1; $i >= 0; $i--) {
128
            $hour = Carbon::now()->subHours($i)->startOfHour()->format('Y-m-d H:00:00');
129
130
            // Check if stats already exist for this hour
131
            if (! $force && DB::table('user_activity_stats_hourly')->where('stat_hour', $hour)->exists()) {
132
                $statsSkipped++;
133
                $progressBar->advance();
134
135
                continue;
136
            }
137
138
            // Count downloads for the hour
139
            $downloadsCount = UserDownload::query()
140
                ->where('timestamp', '>=', $hour)
141
                ->where('timestamp', '<', Carbon::parse($hour)->addHour()->format('Y-m-d H:00:00'))
142
                ->count();
143
144
            // Count API hits for the hour
145
            $apiHitsCount = UserRequest::query()
146
                ->where('timestamp', '>=', $hour)
147
                ->where('timestamp', '<', Carbon::parse($hour)->addHour()->format('Y-m-d H:00:00'))
148
                ->count();
149
150
            // Store or update the stats
151
            DB::table('user_activity_stats_hourly')->updateOrInsert(
152
                ['stat_hour' => $hour],
153
                [
154
                    'downloads_count' => $downloadsCount,
155
                    'api_hits_count' => $apiHitsCount,
156
                    'updated_at' => now(),
157
                    'created_at' => DB::raw('COALESCE(created_at, NOW())'),
158
                ]
159
            );
160
161
            $statsCollected++;
162
            $progressBar->advance();
163
        }
164
165
        $progressBar->finish();
166
        $this->newLine(2);
167
168
        $this->info('Hourly backfill complete!');
169
        $this->info("Stats collected: {$statsCollected}");
170
        if ($statsSkipped > 0) {
171
            $this->info("Stats skipped (already existed): {$statsSkipped}");
172
        }
173
174
        return Command::SUCCESS;
175
    }
176
}
177