phase 7
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ApiLog;
|
||||
use App\Models\JobFetchLog;
|
||||
use App\Services\MetricsService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private MetricsService $metricsService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display the admin dashboard
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$days = $request->input('days', 7);
|
||||
|
||||
$metrics = [
|
||||
'api' => $this->metricsService->getApiMetrics($days),
|
||||
'fetch' => $this->metricsService->getJobFetchMetrics($days),
|
||||
'analytics' => $this->metricsService->getAnalyticsMetrics($days),
|
||||
'health' => $this->metricsService->getSystemHealth(),
|
||||
];
|
||||
|
||||
return view('admin.dashboard', compact('metrics', 'days'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display API logs
|
||||
*/
|
||||
public function apiLogs(Request $request)
|
||||
{
|
||||
$query = ApiLog::with('source')
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
if ($request->has('source_id')) {
|
||||
$query->where('job_source_id', $request->source_id);
|
||||
}
|
||||
|
||||
if ($request->has('status')) {
|
||||
$status = $request->status === 'success';
|
||||
$query->where('success', $status);
|
||||
}
|
||||
|
||||
$logs = $query->paginate(50);
|
||||
|
||||
return view('admin.api-logs', compact('logs'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display job fetch logs
|
||||
*/
|
||||
public function fetchLogs(Request $request)
|
||||
{
|
||||
$query = JobFetchLog::with('source')
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
if ($request->has('source_id')) {
|
||||
$query->where('job_source_id', $request->source_id);
|
||||
}
|
||||
|
||||
if ($request->has('status')) {
|
||||
$status = $request->status === 'success';
|
||||
$query->where('success', $status);
|
||||
}
|
||||
|
||||
$logs = $query->paginate(50);
|
||||
|
||||
return view('admin.fetch-logs', compact('logs'));
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\AnalyticsEvent;
|
||||
use App\Models\Job;
|
||||
use App\Models\JobCategory;
|
||||
use App\Repositories\JobRepository;
|
||||
@@ -35,6 +36,11 @@ class JobController extends Controller
|
||||
'sort',
|
||||
]);
|
||||
|
||||
// Track search event if keywords are provided
|
||||
if (!empty($filters['keywords'])) {
|
||||
AnalyticsEvent::logEvent(AnalyticsEvent::EVENT_SEARCH, null, $filters);
|
||||
}
|
||||
|
||||
$perPage = $request->input('per_page', 20);
|
||||
$jobs = $this->searchService->search($filters, $perPage);
|
||||
|
||||
@@ -52,6 +58,9 @@ class JobController extends Controller
|
||||
$job = Job::with(['source', 'category', 'jobLocation'])
|
||||
->findOrFail($id);
|
||||
|
||||
// Track job view event
|
||||
AnalyticsEvent::logEvent(AnalyticsEvent::EVENT_JOB_VIEW, $job->id);
|
||||
|
||||
$similarJobs = $this->jobRepository->getSimilarJobs($job, 5);
|
||||
|
||||
return view('jobs.show', compact('job', 'similarJobs'));
|
||||
@@ -64,6 +73,9 @@ class JobController extends Controller
|
||||
{
|
||||
$category = JobCategory::where('slug', $slug)->firstOrFail();
|
||||
|
||||
// Track category view event
|
||||
AnalyticsEvent::logEvent(AnalyticsEvent::EVENT_CATEGORY_VIEW, null, ['category' => $category->name]);
|
||||
|
||||
$filters = $request->only([
|
||||
'keywords',
|
||||
'location',
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class AnalyticsEvent extends Model
|
||||
{
|
||||
const UPDATED_AT = null; // Only created_at timestamp
|
||||
|
||||
protected $fillable = [
|
||||
'event_type',
|
||||
'job_id',
|
||||
'session_id',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
'referer',
|
||||
'event_data',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'event_data' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
// Event types
|
||||
const EVENT_JOB_VIEW = 'job_view';
|
||||
const EVENT_JOB_APPLY_CLICK = 'job_apply_click';
|
||||
const EVENT_SEARCH = 'search';
|
||||
const EVENT_FILTER = 'filter';
|
||||
const EVENT_CATEGORY_VIEW = 'category_view';
|
||||
|
||||
public function job(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Job::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get events by type
|
||||
*/
|
||||
public function scopeOfType($query, string $type)
|
||||
{
|
||||
return $query->where('event_type', $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get events within a date range
|
||||
*/
|
||||
public function scopeWithinDays($query, int $days)
|
||||
{
|
||||
return $query->where('created_at', '>=', now()->subDays($days));
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an event
|
||||
*/
|
||||
public static function logEvent(string $eventType, ?int $jobId = null, array $eventData = []): void
|
||||
{
|
||||
self::create([
|
||||
'event_type' => $eventType,
|
||||
'job_id' => $jobId,
|
||||
'session_id' => session()->getId(),
|
||||
'ip_address' => request()->ip(),
|
||||
'user_agent' => request()->userAgent(),
|
||||
'referer' => request()->header('referer'),
|
||||
'event_data' => $eventData,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ApiLog extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'job_source_id',
|
||||
'endpoint',
|
||||
'method',
|
||||
'status_code',
|
||||
'response_time_ms',
|
||||
'success',
|
||||
'error_message',
|
||||
'request_params',
|
||||
'response_data',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'success' => 'boolean',
|
||||
'request_params' => 'array',
|
||||
'response_data' => 'array',
|
||||
];
|
||||
|
||||
public function source(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(JobSource::class, 'job_source_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get only successful logs
|
||||
*/
|
||||
public function scopeSuccessful($query)
|
||||
{
|
||||
return $query->where('success', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get only failed logs
|
||||
*/
|
||||
public function scopeFailed($query)
|
||||
{
|
||||
return $query->where('success', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get logs within a date range
|
||||
*/
|
||||
public function scopeWithinDays($query, int $days)
|
||||
{
|
||||
return $query->where('created_at', '>=', now()->subDays($days));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class JobFetchLog extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'job_source_id',
|
||||
'success',
|
||||
'jobs_fetched',
|
||||
'jobs_created',
|
||||
'jobs_updated',
|
||||
'jobs_skipped',
|
||||
'duration_seconds',
|
||||
'error_message',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'success' => 'boolean',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
|
||||
public function source(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(JobSource::class, 'job_source_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get only successful logs
|
||||
*/
|
||||
public function scopeSuccessful($query)
|
||||
{
|
||||
return $query->where('success', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get only failed logs
|
||||
*/
|
||||
public function scopeFailed($query)
|
||||
{
|
||||
return $query->where('success', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get logs within a date range
|
||||
*/
|
||||
public function scopeWithinDays($query, int $days)
|
||||
{
|
||||
return $query->where('created_at', '>=', now()->subDays($days));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total jobs processed
|
||||
*/
|
||||
public function getTotalJobsProcessedAttribute(): int
|
||||
{
|
||||
return $this->jobs_created + $this->jobs_updated + $this->jobs_skipped;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ApiLog;
|
||||
use App\Models\JobSource;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ApiClientService
|
||||
{
|
||||
/**
|
||||
* Make an API request with logging and retry logic
|
||||
*/
|
||||
public function makeRequest(
|
||||
JobSource $source,
|
||||
string $endpoint,
|
||||
string $method = 'GET',
|
||||
array $params = [],
|
||||
int $maxRetries = 3
|
||||
): array {
|
||||
$startTime = microtime(true);
|
||||
$attempt = 0;
|
||||
$lastException = null;
|
||||
|
||||
while ($attempt < $maxRetries) {
|
||||
$attempt++;
|
||||
|
||||
try {
|
||||
$response = $this->executeRequest($method, $endpoint, $params);
|
||||
$responseTime = (int)((microtime(true) - $startTime) * 1000);
|
||||
|
||||
// Log successful request
|
||||
$this->logApiRequest($source, $endpoint, $method, $params, $response, $responseTime, true);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $response->json(),
|
||||
'status_code' => $response->status(),
|
||||
'response_time' => $responseTime,
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
$lastException = $e;
|
||||
|
||||
// Log failed attempt
|
||||
Log::warning("API request failed (attempt {$attempt}/{$maxRetries})", [
|
||||
'source' => $source->name,
|
||||
'endpoint' => $endpoint,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
if ($attempt < $maxRetries) {
|
||||
// Exponential backoff: wait 2^attempt seconds
|
||||
sleep(pow(2, $attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$responseTime = (int)((microtime(true) - $startTime) * 1000);
|
||||
|
||||
// Log final failure
|
||||
$this->logApiRequest(
|
||||
$source,
|
||||
$endpoint,
|
||||
$method,
|
||||
$params,
|
||||
null,
|
||||
$responseTime,
|
||||
false,
|
||||
$lastException->getMessage() ?? 'Unknown error'
|
||||
);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $lastException->getMessage() ?? 'Unknown error',
|
||||
'response_time' => $responseTime,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the HTTP request
|
||||
*/
|
||||
private function executeRequest(string $method, string $endpoint, array $params): \Illuminate\Http\Client\Response
|
||||
{
|
||||
$http = Http::timeout(30)
|
||||
->retry(1, 100)
|
||||
->withHeaders([
|
||||
'User-Agent' => 'JobifyGarden/1.0',
|
||||
'Accept' => 'application/json',
|
||||
]);
|
||||
|
||||
return match (strtoupper($method)) {
|
||||
'GET' => $http->get($endpoint, $params),
|
||||
'POST' => $http->post($endpoint, $params),
|
||||
'PUT' => $http->put($endpoint, $params),
|
||||
'DELETE' => $http->delete($endpoint, $params),
|
||||
default => throw new \InvalidArgumentException("Unsupported HTTP method: {$method}"),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Log API request to database
|
||||
*/
|
||||
private function logApiRequest(
|
||||
JobSource $source,
|
||||
string $endpoint,
|
||||
string $method,
|
||||
array $params,
|
||||
$response,
|
||||
int $responseTime,
|
||||
bool $success,
|
||||
?string $errorMessage = null
|
||||
): void {
|
||||
try {
|
||||
ApiLog::create([
|
||||
'job_source_id' => $source->id,
|
||||
'endpoint' => $endpoint,
|
||||
'method' => $method,
|
||||
'status_code' => $response?->status(),
|
||||
'response_time_ms' => $responseTime,
|
||||
'success' => $success,
|
||||
'error_message' => $errorMessage,
|
||||
'request_params' => !empty($params) ? $params : null,
|
||||
'response_data' => $success && $response ? [
|
||||
'status' => $response->status(),
|
||||
'size' => strlen($response->body()),
|
||||
] : null,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to log API request', [
|
||||
'error' => $e->getMessage(),
|
||||
'endpoint' => $endpoint,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if source is rate limited
|
||||
*/
|
||||
public function isRateLimited(JobSource $source): bool
|
||||
{
|
||||
if (!$source->rate_limit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check requests in the last hour
|
||||
$recentRequests = ApiLog::where('job_source_id', $source->id)
|
||||
->where('created_at', '>=', now()->subHour())
|
||||
->count();
|
||||
|
||||
return $recentRequests >= $source->rate_limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rate limit info for a source
|
||||
*/
|
||||
public function getRateLimitInfo(JobSource $source): array
|
||||
{
|
||||
$recentRequests = ApiLog::where('job_source_id', $source->id)
|
||||
->where('created_at', '>=', now()->subHour())
|
||||
->count();
|
||||
|
||||
return [
|
||||
'limit' => $source->rate_limit,
|
||||
'used' => $recentRequests,
|
||||
'remaining' => max(0, $source->rate_limit - $recentRequests),
|
||||
'reset_at' => now()->addHour()->startOfHour(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ApiLog;
|
||||
use App\Models\AnalyticsEvent;
|
||||
use App\Models\JobFetchLog;
|
||||
use App\Models\JobSource;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class MetricsService
|
||||
{
|
||||
/**
|
||||
* Get API performance metrics
|
||||
*/
|
||||
public function getApiMetrics(int $days = 7): array
|
||||
{
|
||||
$startDate = now()->subDays($days);
|
||||
|
||||
return [
|
||||
'total_requests' => ApiLog::where('created_at', '>=', $startDate)->count(),
|
||||
'successful_requests' => ApiLog::successful()->where('created_at', '>=', $startDate)->count(),
|
||||
'failed_requests' => ApiLog::failed()->where('created_at', '>=', $startDate)->count(),
|
||||
'average_response_time' => ApiLog::where('created_at', '>=', $startDate)
|
||||
->whereNotNull('response_time_ms')
|
||||
->avg('response_time_ms'),
|
||||
'success_rate' => $this->calculateSuccessRate($days),
|
||||
'by_source' => $this->getApiMetricsBySource($days),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get job fetch metrics
|
||||
*/
|
||||
public function getJobFetchMetrics(int $days = 7): array
|
||||
{
|
||||
$startDate = now()->subDays($days);
|
||||
|
||||
return [
|
||||
'total_fetches' => JobFetchLog::where('created_at', '>=', $startDate)->count(),
|
||||
'successful_fetches' => JobFetchLog::successful()->where('created_at', '>=', $startDate)->count(),
|
||||
'failed_fetches' => JobFetchLog::failed()->where('created_at', '>=', $startDate)->count(),
|
||||
'total_jobs_fetched' => JobFetchLog::where('created_at', '>=', $startDate)->sum('jobs_fetched'),
|
||||
'total_jobs_created' => JobFetchLog::where('created_at', '>=', $startDate)->sum('jobs_created'),
|
||||
'total_jobs_updated' => JobFetchLog::where('created_at', '>=', $startDate)->sum('jobs_updated'),
|
||||
'total_jobs_skipped' => JobFetchLog::where('created_at', '>=', $startDate)->sum('jobs_skipped'),
|
||||
'average_duration' => JobFetchLog::where('created_at', '>=', $startDate)
|
||||
->whereNotNull('duration_seconds')
|
||||
->avg('duration_seconds'),
|
||||
'by_source' => $this->getJobFetchMetricsBySource($days),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get analytics metrics
|
||||
*/
|
||||
public function getAnalyticsMetrics(int $days = 7): array
|
||||
{
|
||||
$startDate = now()->subDays($days);
|
||||
|
||||
return [
|
||||
'total_events' => AnalyticsEvent::where('created_at', '>=', $startDate)->count(),
|
||||
'job_views' => AnalyticsEvent::ofType(AnalyticsEvent::EVENT_JOB_VIEW)
|
||||
->where('created_at', '>=', $startDate)->count(),
|
||||
'job_apply_clicks' => AnalyticsEvent::ofType(AnalyticsEvent::EVENT_JOB_APPLY_CLICK)
|
||||
->where('created_at', '>=', $startDate)->count(),
|
||||
'searches' => AnalyticsEvent::ofType(AnalyticsEvent::EVENT_SEARCH)
|
||||
->where('created_at', '>=', $startDate)->count(),
|
||||
'unique_sessions' => AnalyticsEvent::where('created_at', '>=', $startDate)
|
||||
->distinct('session_id')->count('session_id'),
|
||||
'click_through_rate' => $this->calculateClickThroughRate($days),
|
||||
'popular_jobs' => $this->getPopularJobs($days),
|
||||
'popular_searches' => $this->getPopularSearches($days),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system health metrics
|
||||
*/
|
||||
public function getSystemHealth(): array
|
||||
{
|
||||
$recentFailures = ApiLog::failed()
|
||||
->where('created_at', '>=', now()->subHours(1))
|
||||
->count();
|
||||
|
||||
$recentFetchFailures = JobFetchLog::failed()
|
||||
->where('created_at', '>=', now()->subHours(24))
|
||||
->count();
|
||||
|
||||
return [
|
||||
'status' => $this->determineSystemStatus($recentFailures, $recentFetchFailures),
|
||||
'api_failures_last_hour' => $recentFailures,
|
||||
'fetch_failures_last_24h' => $recentFetchFailures,
|
||||
'last_successful_fetch' => JobFetchLog::successful()
|
||||
->latest()
|
||||
->first()?->created_at,
|
||||
'sources_status' => $this->getSourcesStatus(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate API success rate
|
||||
*/
|
||||
private function calculateSuccessRate(int $days): float
|
||||
{
|
||||
$total = ApiLog::withinDays($days)->count();
|
||||
if ($total === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$successful = ApiLog::successful()->withinDays($days)->count();
|
||||
return round(($successful / $total) * 100, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API metrics by source
|
||||
*/
|
||||
private function getApiMetricsBySource(int $days): array
|
||||
{
|
||||
return ApiLog::select('job_source_id')
|
||||
->selectRaw('COUNT(*) as total')
|
||||
->selectRaw('SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successful')
|
||||
->selectRaw('AVG(response_time_ms) as avg_response_time')
|
||||
->withinDays($days)
|
||||
->with('source:id,name')
|
||||
->groupBy('job_source_id')
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'source' => $item->source->name ?? 'Unknown',
|
||||
'total_requests' => $item->total,
|
||||
'successful' => $item->successful,
|
||||
'success_rate' => $item->total > 0 ? round(($item->successful / $item->total) * 100, 2) : 0,
|
||||
'avg_response_time' => round($item->avg_response_time, 2),
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get job fetch metrics by source
|
||||
*/
|
||||
private function getJobFetchMetricsBySource(int $days): array
|
||||
{
|
||||
return JobFetchLog::select('job_source_id')
|
||||
->selectRaw('COUNT(*) as total_fetches')
|
||||
->selectRaw('SUM(jobs_fetched) as jobs_fetched')
|
||||
->selectRaw('SUM(jobs_created) as jobs_created')
|
||||
->selectRaw('SUM(jobs_updated) as jobs_updated')
|
||||
->selectRaw('AVG(duration_seconds) as avg_duration')
|
||||
->withinDays($days)
|
||||
->with('source:id,name')
|
||||
->groupBy('job_source_id')
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'source' => $item->source->name ?? 'Unknown',
|
||||
'total_fetches' => $item->total_fetches,
|
||||
'jobs_fetched' => $item->jobs_fetched,
|
||||
'jobs_created' => $item->jobs_created,
|
||||
'jobs_updated' => $item->jobs_updated,
|
||||
'avg_duration' => round($item->avg_duration, 2),
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate click-through rate
|
||||
*/
|
||||
private function calculateClickThroughRate(int $days): float
|
||||
{
|
||||
$views = AnalyticsEvent::ofType(AnalyticsEvent::EVENT_JOB_VIEW)
|
||||
->withinDays($days)
|
||||
->count();
|
||||
|
||||
if ($views === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$clicks = AnalyticsEvent::ofType(AnalyticsEvent::EVENT_JOB_APPLY_CLICK)
|
||||
->withinDays($days)
|
||||
->count();
|
||||
|
||||
return round(($clicks / $views) * 100, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get popular jobs
|
||||
*/
|
||||
private function getPopularJobs(int $days, int $limit = 10): array
|
||||
{
|
||||
return AnalyticsEvent::select('job_id')
|
||||
->selectRaw('COUNT(*) as view_count')
|
||||
->selectRaw('SUM(CASE WHEN event_type = ? THEN 1 ELSE 0 END) as apply_clicks', [AnalyticsEvent::EVENT_JOB_APPLY_CLICK])
|
||||
->whereNotNull('job_id')
|
||||
->withinDays($days)
|
||||
->with('job:id,title,company')
|
||||
->groupBy('job_id')
|
||||
->orderByDesc('view_count')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'job_id' => $item->job_id,
|
||||
'title' => $item->job->title ?? 'Unknown',
|
||||
'company' => $item->job->company ?? 'Unknown',
|
||||
'views' => $item->view_count,
|
||||
'apply_clicks' => $item->apply_clicks,
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get popular search terms
|
||||
*/
|
||||
private function getPopularSearches(int $days, int $limit = 10): array
|
||||
{
|
||||
return AnalyticsEvent::select(DB::raw('JSON_EXTRACT(event_data, "$.keywords") as search_term'))
|
||||
->selectRaw('COUNT(*) as search_count')
|
||||
->ofType(AnalyticsEvent::EVENT_SEARCH)
|
||||
->withinDays($days)
|
||||
->whereNotNull(DB::raw('JSON_EXTRACT(event_data, "$.keywords")'))
|
||||
->groupBy('search_term')
|
||||
->orderByDesc('search_count')
|
||||
->limit($limit)
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'term' => trim($item->search_term, '"'),
|
||||
'count' => $item->search_count,
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine system status
|
||||
*/
|
||||
private function determineSystemStatus(int $apiFailures, int $fetchFailures): string
|
||||
{
|
||||
if ($apiFailures > 10 || $fetchFailures > 5) {
|
||||
return 'critical';
|
||||
}
|
||||
|
||||
if ($apiFailures > 5 || $fetchFailures > 2) {
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
return 'healthy';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status of all sources
|
||||
*/
|
||||
private function getSourcesStatus(): array
|
||||
{
|
||||
return JobSource::get()->map(function ($source) {
|
||||
$lastFetch = JobFetchLog::where('job_source_id', $source->id)
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
return [
|
||||
'id' => $source->id,
|
||||
'name' => $source->name,
|
||||
'is_active' => $source->is_active,
|
||||
'last_fetch_success' => $lastFetch?->success ?? null,
|
||||
'last_fetch_at' => $lastFetch?->created_at ?? null,
|
||||
'jobs_in_last_fetch' => $lastFetch?->jobs_created ?? 0,
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Models\JobFetchLog;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
trait HandlesJobErrors
|
||||
{
|
||||
/**
|
||||
* Handle job fetch errors gracefully
|
||||
*/
|
||||
protected function handleFetchError(
|
||||
int $sourceId,
|
||||
\Exception $exception,
|
||||
array $metadata = []
|
||||
): void {
|
||||
// Log the error
|
||||
Log::error('Job fetch failed', [
|
||||
'source_id' => $sourceId,
|
||||
'error' => $exception->getMessage(),
|
||||
'trace' => $exception->getTraceAsString(),
|
||||
'metadata' => $metadata,
|
||||
]);
|
||||
|
||||
// Record in database
|
||||
JobFetchLog::create([
|
||||
'job_source_id' => $sourceId,
|
||||
'success' => false,
|
||||
'jobs_fetched' => 0,
|
||||
'jobs_created' => 0,
|
||||
'jobs_updated' => 0,
|
||||
'jobs_skipped' => 0,
|
||||
'error_message' => $exception->getMessage(),
|
||||
'metadata' => array_merge($metadata, [
|
||||
'exception_class' => get_class($exception),
|
||||
'file' => $exception->getFile(),
|
||||
'line' => $exception->getLine(),
|
||||
]),
|
||||
]);
|
||||
|
||||
// Check if we need to disable the source due to repeated failures
|
||||
$this->checkAndDisableSource($sourceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for repeated failures and disable source if necessary
|
||||
*/
|
||||
protected function checkAndDisableSource(int $sourceId): void
|
||||
{
|
||||
$recentFailures = JobFetchLog::where('job_source_id', $sourceId)
|
||||
->where('success', false)
|
||||
->where('created_at', '>=', now()->subHours(24))
|
||||
->count();
|
||||
|
||||
// Disable source after 5 consecutive failures in 24 hours
|
||||
if ($recentFailures >= 5) {
|
||||
$source = \App\Models\JobSource::find($sourceId);
|
||||
if ($source && $source->is_active) {
|
||||
$source->update(['is_active' => false]);
|
||||
|
||||
Log::warning("Source {$source->name} has been disabled due to repeated failures", [
|
||||
'source_id' => $sourceId,
|
||||
'failures' => $recentFailures,
|
||||
]);
|
||||
|
||||
// You could send a notification to admins here
|
||||
$this->notifyAdmins($source, $recentFailures);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log successful job fetch
|
||||
*/
|
||||
protected function logSuccessfulFetch(
|
||||
int $sourceId,
|
||||
int $jobsFetched,
|
||||
int $jobsCreated,
|
||||
int $jobsUpdated,
|
||||
int $jobsSkipped,
|
||||
int $durationSeconds,
|
||||
array $metadata = []
|
||||
): void {
|
||||
JobFetchLog::create([
|
||||
'job_source_id' => $sourceId,
|
||||
'success' => true,
|
||||
'jobs_fetched' => $jobsFetched,
|
||||
'jobs_created' => $jobsCreated,
|
||||
'jobs_updated' => $jobsUpdated,
|
||||
'jobs_skipped' => $jobsSkipped,
|
||||
'duration_seconds' => $durationSeconds,
|
||||
'metadata' => $metadata,
|
||||
]);
|
||||
|
||||
Log::info('Job fetch completed successfully', [
|
||||
'source_id' => $sourceId,
|
||||
'jobs_fetched' => $jobsFetched,
|
||||
'jobs_created' => $jobsCreated,
|
||||
'jobs_updated' => $jobsUpdated,
|
||||
'duration' => $durationSeconds,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify admins about source failure (placeholder)
|
||||
*/
|
||||
protected function notifyAdmins($source, int $failureCount): void
|
||||
{
|
||||
// TODO: Implement notification logic
|
||||
// Could use Laravel notifications, email, Slack, etc.
|
||||
Log::critical("ADMIN ALERT: Source {$source->name} disabled after {$failureCount} failures");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user