where('is_active', true) ->get(); $triggered = 0; foreach ($alerts as $alert) { // Skip if triggered recently (within the period to avoid spam) if ($this->wasRecentlyTriggered($alert)) { continue; } $currentValue = $this->getCurrentValue($alert); $shouldTrigger = $this->evaluate($alert, $currentValue); if ($shouldTrigger) { $this->trigger($alert, $currentValue); $triggered++; } } $this->info("Checked {$alerts->count()} alerts, triggered {$triggered}."); return self::SUCCESS; } private function getCurrentValue(Alert $alert): float { $to = Carbon::now(); $from = $alert->period === 'hourly' ? Carbon::now()->subHour() : Carbon::today()->startOfDay(); $service = AnalyticsQueryService::for($alert->website, $from, $to); $summary = $service->summary(); return match ($alert->metric) { 'page_views' => $summary['page_views'], 'visitors' => $summary['visitors'], 'sessions' => $summary['sessions'], 'bounce_rate' => $summary['bounce_rate'], default => 0, }; } private function evaluate(Alert $alert, float $currentValue): bool { return match ($alert->condition) { 'above' => $currentValue > $alert->threshold, 'below' => $currentValue < $alert->threshold, 'change_up' => $this->percentageChange($alert) > $alert->threshold, 'change_down' => $this->percentageChange($alert) < -$alert->threshold, default => false, }; } private function percentageChange(Alert $alert): float { if ($alert->period === 'hourly') { // Compare current hour with previous hour $currentTo = Carbon::now(); $currentFrom = Carbon::now()->subHour(); $prevTo = Carbon::now()->subHour(); $prevFrom = Carbon::now()->subHours(2); $current = $this->getValueForPeriod($alert, $currentFrom, $currentTo); $previous = $this->getValueForPeriod($alert, $prevFrom, $prevTo); } else { // Compare today so far with yesterday same time $currentTo = Carbon::now(); $currentFrom = Carbon::today()->startOfDay(); $prevTo = Carbon::yesterday()->setTime($currentTo->hour, $currentTo->minute, $currentTo->second); $prevFrom = Carbon::yesterday()->startOfDay(); $current = $this->getValueForPeriod($alert, $currentFrom, $currentTo); $previous = $this->getValueForPeriod($alert, $prevFrom, $prevTo); } if ($previous == 0) { return $current > 0 ? 100 : 0; } return (($current - $previous) / $previous) * 100; } private function getValueForPeriod(Alert $alert, Carbon $from, Carbon $to): float { $service = AnalyticsQueryService::for($alert->website, $from, $to); $summary = $service->summary(); return $summary[$alert->metric] ?? 0; } private function wasRecentlyTriggered(Alert $alert): bool { if (!$alert->last_triggered_at) { return false; } $cooldown = $alert->period === 'hourly' ? 1 : 24; // hours $threshold = Carbon::now()->subHours($cooldown); return $alert->last_triggered_at->isAfter($threshold); } private function trigger(Alert $alert, float $currentValue): void { $conditionLabel = Alert::$conditions[$alert->condition] ?? $alert->condition; $metricLabel = Alert::$metrics[$alert->metric] ?? $alert->metric; $message = "{$metricLabel} {$conditionLabel} {$alert->threshold} (current: {$currentValue}) for {$alert->website->name}"; $notification = $alert->notifications()->create([ 'value' => $currentValue, 'message' => $message, ]); $alert->update(['last_triggered_at' => now()]); if (in_array($alert->channel, ['email', 'both'])) { Mail::to($alert->website->user)->send(new AlertTriggered($alert, $notification)); } $this->line(" → Triggered: {$alert->name} — {$message}"); } }