diff --git a/app/Facades/TrackingAnalytics.php b/app/Facades/TrackingAnalytics.php new file mode 100644 index 0000000..0e48be7 --- /dev/null +++ b/app/Facades/TrackingAnalytics.php @@ -0,0 +1,24 @@ +onQueue('tracking'); + } + + public function handle(): void + { + try { + TrackingContentSelection::create([ + 'device_id' => $this->deviceId, + 'content_type' => $this->contentType, + 'content_id' => $this->contentId, + 'content_name' => $this->contentName, + 'selection_method' => $this->selectionMethod, + 'search_query' => $this->searchQuery, + 'action_at' => $this->actionAt, + 'user_agent' => $this->userAgent, + 'ip_address' => $this->ipAddress, + 'platform' => $this->platform, + ]); + } catch (\Exception $e) { + Log::error('Failed to track content selection', [ + 'device_id' => $this->deviceId, + 'content_type' => $this->contentType, + 'content_id' => $this->contentId, + 'error' => $e->getMessage(), + ]); + + throw $e; + } + } + + public function failed(\Throwable $exception): void + { + Log::error('TrackContentSelectionJob failed permanently', [ + 'device_id' => $this->deviceId, + 'content_type' => $this->contentType, + 'content_id' => $this->contentId, + 'exception' => $exception->getMessage(), + ]); + } +} \ No newline at end of file diff --git a/app/Jobs/TrackExportJob.php b/app/Jobs/TrackExportJob.php new file mode 100644 index 0000000..fb6f84a --- /dev/null +++ b/app/Jobs/TrackExportJob.php @@ -0,0 +1,74 @@ +onQueue('tracking'); + } + + public function handle(): int + { + try { + $trackingExport = TrackingExport::create([ + 'device_id' => $this->deviceId, + 'meme_id' => $this->memeId, + 'meme_media_id' => $this->memeMediaId, + 'background_media_id' => $this->backgroundMediaId, + 'caption_texts' => $this->captionTexts, + 'export_format' => $this->exportFormat, + 'export_quality' => $this->exportQuality, + 'export_status' => 'initiated', + 'action_at' => $this->actionAt, + 'user_agent' => $this->userAgent, + 'ip_address' => $this->ipAddress, + 'platform' => $this->platform, + ]); + + return $trackingExport->id; + } catch (\Exception $e) { + Log::error('Failed to track export', [ + 'device_id' => $this->deviceId, + 'meme_id' => $this->memeId, + 'export_format' => $this->exportFormat, + 'error' => $e->getMessage(), + ]); + + throw $e; + } + } + + public function failed(\Throwable $exception): void + { + Log::error('TrackExportJob failed permanently', [ + 'device_id' => $this->deviceId, + 'meme_id' => $this->memeId, + 'export_format' => $this->exportFormat, + 'exception' => $exception->getMessage(), + ]); + } +} \ No newline at end of file diff --git a/app/Jobs/TrackSearchJob.php b/app/Jobs/TrackSearchJob.php new file mode 100644 index 0000000..075bb19 --- /dev/null +++ b/app/Jobs/TrackSearchJob.php @@ -0,0 +1,65 @@ +onQueue('tracking'); + } + + public function handle(): void + { + try { + TrackingSearch::create([ + 'device_id' => $this->deviceId, + 'search_type' => $this->searchType, + 'search_query' => $this->searchQuery, + 'search_filters' => $this->searchFilters, + 'action_at' => $this->actionAt, + 'user_agent' => $this->userAgent, + 'ip_address' => $this->ipAddress, + 'platform' => $this->platform, + ]); + } catch (\Exception $e) { + Log::error('Failed to track search', [ + 'device_id' => $this->deviceId, + 'search_type' => $this->searchType, + 'search_query' => $this->searchQuery, + 'error' => $e->getMessage(), + ]); + + throw $e; + } + } + + public function failed(\Throwable $exception): void + { + Log::error('TrackSearchJob failed permanently', [ + 'device_id' => $this->deviceId, + 'search_type' => $this->searchType, + 'search_query' => $this->searchQuery, + 'exception' => $exception->getMessage(), + ]); + } +} \ No newline at end of file diff --git a/app/Jobs/UpdateExportStatusJob.php b/app/Jobs/UpdateExportStatusJob.php new file mode 100644 index 0000000..ea98b55 --- /dev/null +++ b/app/Jobs/UpdateExportStatusJob.php @@ -0,0 +1,66 @@ +onQueue('tracking'); + } + + public function handle(): void + { + try { + $trackingExport = TrackingExport::findOrFail($this->trackingExportId); + + $updateData = [ + 'export_status' => $this->status, + ]; + + if ($this->errorMessage !== null) { + $updateData['error_message'] = $this->errorMessage; + } + + if ($this->completedAt !== null) { + $updateData['completed_at'] = $this->completedAt; + } elseif (in_array($this->status, ['completed', 'failed'])) { + $updateData['completed_at'] = now(); + } + + $trackingExport->update($updateData); + } catch (\Exception $e) { + Log::error('Failed to update export status', [ + 'tracking_export_id' => $this->trackingExportId, + 'status' => $this->status, + 'error' => $e->getMessage(), + ]); + + throw $e; + } + } + + public function failed(\Throwable $exception): void + { + Log::error('UpdateExportStatusJob failed permanently', [ + 'tracking_export_id' => $this->trackingExportId, + 'status' => $this->status, + 'exception' => $exception->getMessage(), + ]); + } +} \ No newline at end of file diff --git a/app/Models/TrackingContentSelection.php b/app/Models/TrackingContentSelection.php new file mode 100644 index 0000000..680e1e1 --- /dev/null +++ b/app/Models/TrackingContentSelection.php @@ -0,0 +1,51 @@ + 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + protected $attributes = [ + 'platform' => 'web', + ]; + + /** + * Get the related content (polymorphic relationship) + */ + public function content() + { + if ($this->content_type === 'meme') { + return $this->belongsTo(MemeMedia::class, 'content_id'); + } + + if ($this->content_type === 'background') { + return $this->belongsTo(BackgroundMedia::class, 'content_id'); + } + + return null; + } +} \ No newline at end of file diff --git a/app/Models/TrackingExport.php b/app/Models/TrackingExport.php new file mode 100644 index 0000000..b3a8cfd --- /dev/null +++ b/app/Models/TrackingExport.php @@ -0,0 +1,91 @@ + 'array', + 'action_at' => 'datetime', + 'completed_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + protected $attributes = [ + 'platform' => 'web', + 'export_quality' => 'standard', + 'export_status' => 'initiated', + ]; + + /** + * Get the associated meme + */ + public function meme(): BelongsTo + { + return $this->belongsTo(Meme::class); + } + + /** + * Get the associated meme media + */ + public function memeMedia(): BelongsTo + { + return $this->belongsTo(MemeMedia::class, 'meme_media_id'); + } + + /** + * Get the associated background media + */ + public function backgroundMedia(): BelongsTo + { + return $this->belongsTo(BackgroundMedia::class, 'background_media_id'); + } + + /** + * Scope for completed exports + */ + public function scopeCompleted($query) + { + return $query->where('export_status', 'completed'); + } + + /** + * Scope for failed exports + */ + public function scopeFailed($query) + { + return $query->where('export_status', 'failed'); + } + + /** + * Scope for processing exports + */ + public function scopeProcessing($query) + { + return $query->where('export_status', 'processing'); + } +} \ No newline at end of file diff --git a/app/Models/TrackingSearch.php b/app/Models/TrackingSearch.php new file mode 100644 index 0000000..24b2bb9 --- /dev/null +++ b/app/Models/TrackingSearch.php @@ -0,0 +1,33 @@ + 'array', + 'action_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + protected $attributes = [ + 'platform' => 'web', + ]; +} \ No newline at end of file diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 1aeb057..0520d5c 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,6 +4,7 @@ use App\Models\MemeMedia; use App\Observers\MemeMediaObserver; +use App\Services\TrackingAnalyticsService; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -13,7 +14,9 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // + $this->app->singleton('tracking-analytics', function ($app) { + return new TrackingAnalyticsService(); + }); } /** diff --git a/app/Services/TrackingAnalyticsService.php b/app/Services/TrackingAnalyticsService.php new file mode 100644 index 0000000..7486324 --- /dev/null +++ b/app/Services/TrackingAnalyticsService.php @@ -0,0 +1,205 @@ + $request->userAgent(), + 'ip_address' => $request->ip(), + 'platform' => 'web', // Default for now, can be enhanced for mobile detection + ]; + } + + /** + * Generate a device ID from request + */ + public function generateDeviceId(): string + { + $request = request(); + + // Generate a consistent device ID based on session or create new one + if ($request->session()->has('device_id')) { + return $request->session()->get('device_id'); + } + + $deviceId = str()->uuid()->toString(); + $request->session()->put('device_id', $deviceId); + + return $deviceId; + } + + /** + * Quick track methods with auto device context + */ + public function quickTrackSearch(string $searchType, string $searchQuery, ?array $searchFilters = null): void + { + $context = $this->getDeviceContext(); + $deviceId = $this->generateDeviceId(); + + $this->trackSearch( + $deviceId, + $searchType, + $searchQuery, + $searchFilters, + null, + $context['user_agent'], + $context['ip_address'], + $context['platform'] + ); + } + + public function quickTrackContentSelection(string $contentType, int $contentId, string $contentName, string $selectionMethod, ?string $searchQuery = null): void + { + $context = $this->getDeviceContext(); + $deviceId = $this->generateDeviceId(); + + $this->trackContentSelection( + $deviceId, + $contentType, + $contentId, + $contentName, + $selectionMethod, + $searchQuery, + null, + $context['user_agent'], + $context['ip_address'], + $context['platform'] + ); + } + + public function quickTrackExport(?int $memeId, ?int $memeMediaId, ?int $backgroundMediaId, array $captionTexts, string $exportFormat, string $exportQuality = 'standard'): int + { + $context = $this->getDeviceContext(); + $deviceId = $this->generateDeviceId(); + + return $this->trackExport( + $deviceId, + $memeId, + $memeMediaId, + $backgroundMediaId, + $captionTexts, + $exportFormat, + $exportQuality, + null, + $context['user_agent'], + $context['ip_address'], + $context['platform'] + ); + } +} \ No newline at end of file diff --git a/config/horizon.php b/config/horizon.php index 2691d37..2b77625 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -85,6 +85,7 @@ 'waits' => [ 'redis:default' => 60, + 'redis:tracking' => 30, ], /* @@ -253,6 +254,20 @@ 'nice' => 0, 'rest' => 0, ], + 'supervisor-tracking' => [ + 'connection' => 'redis', + 'queue' => ['tracking'], + 'balance' => 'auto', + 'autoScalingStrategy' => 'time', + 'maxProcesses' => 2, + 'maxTime' => 0, + 'maxJobs' => 0, + 'memory' => 256, + 'tries' => 3, + 'timeout' => 30, + 'nice' => 0, + 'rest' => 0, + ], ], 'local' => [ @@ -312,6 +327,20 @@ 'nice' => 0, 'rest' => 0, ], + 'supervisor-tracking' => [ + 'connection' => 'redis', + 'queue' => ['tracking'], + 'balance' => 'auto', + 'autoScalingStrategy' => 'time', + 'maxProcesses' => 2, + 'maxTime' => 0, + 'maxJobs' => 0, + 'memory' => 256, + 'tries' => 3, + 'timeout' => 30, + 'nice' => 0, + 'rest' => 0, + ], ], ], ]; diff --git a/database/migrations/2025_07_16_034613_create_tracking_searches_table.php b/database/migrations/2025_07_16_034613_create_tracking_searches_table.php new file mode 100644 index 0000000..b7ce4d8 --- /dev/null +++ b/database/migrations/2025_07_16_034613_create_tracking_searches_table.php @@ -0,0 +1,46 @@ +id(); + + // Common fields + $table->string('device_id'); + $table->text('user_agent')->nullable(); + $table->string('ip_address')->nullable(); + $table->enum('platform', ['web', 'ios', 'android'])->default('web'); + + // Search-specific fields + $table->enum('search_type', ['meme', 'background']); + $table->text('search_query'); + $table->json('search_filters')->nullable(); + + // Timestamps + $table->timestamp('action_at'); + $table->timestamps(); + + // Indexes + $table->index(['device_id', 'action_at']); + $table->index(['search_type', 'action_at']); + $table->index('platform'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('tracking_searches'); + } +}; diff --git a/database/migrations/2025_07_16_034628_create_tracking_content_selections_table.php b/database/migrations/2025_07_16_034628_create_tracking_content_selections_table.php new file mode 100644 index 0000000..82f417e --- /dev/null +++ b/database/migrations/2025_07_16_034628_create_tracking_content_selections_table.php @@ -0,0 +1,49 @@ +id(); + + // Common fields + $table->string('device_id'); + $table->text('user_agent')->nullable(); + $table->string('ip_address')->nullable(); + $table->enum('platform', ['web', 'ios', 'android'])->default('web'); + + // Content selection fields + $table->enum('content_type', ['meme', 'background']); + $table->unsignedBigInteger('content_id'); + $table->string('content_name'); + $table->text('search_query')->nullable(); + $table->enum('selection_method', ['search', 'browse', 'featured', 'recent']); + + // Timestamps + $table->timestamp('action_at'); + $table->timestamps(); + + // Indexes + $table->index(['device_id', 'action_at']); + $table->index(['content_type', 'content_id']); + $table->index(['selection_method', 'action_at']); + $table->index('platform'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('tracking_content_selections'); + } +}; diff --git a/database/migrations/2025_07_16_034642_create_tracking_exports_table.php b/database/migrations/2025_07_16_034642_create_tracking_exports_table.php new file mode 100644 index 0000000..c08596a --- /dev/null +++ b/database/migrations/2025_07_16_034642_create_tracking_exports_table.php @@ -0,0 +1,58 @@ +id(); + + // Common fields + $table->string('device_id'); + $table->text('user_agent')->nullable(); + $table->string('ip_address')->nullable(); + $table->enum('platform', ['web', 'ios', 'android'])->default('web'); + + // Export-specific fields + $table->unsignedBigInteger('meme_id')->nullable(); + $table->unsignedBigInteger('meme_media_id')->nullable(); + $table->unsignedBigInteger('background_media_id')->nullable(); + $table->json('caption_texts'); + $table->enum('export_format', ['mov', 'webm', 'gif', 'webp']); + $table->enum('export_quality', ['standard', 'premium'])->default('standard'); + $table->enum('export_status', ['initiated', 'processing', 'completed', 'failed'])->default('initiated'); + $table->text('error_message')->nullable(); + + // Timestamps + $table->timestamp('action_at'); + $table->timestamp('completed_at')->nullable(); + $table->timestamps(); + + // Foreign key constraints + $table->foreign('meme_id')->references('id')->on('memes')->onDelete('set null'); + $table->foreign('meme_media_id')->references('id')->on('meme_medias')->onDelete('set null'); + $table->foreign('background_media_id')->references('id')->on('background_medias')->onDelete('set null'); + + // Indexes + $table->index(['device_id', 'action_at']); + $table->index(['export_status', 'action_at']); + $table->index(['export_format', 'action_at']); + $table->index('platform'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('tracking_exports'); + } +};