This commit is contained in:
ct
2025-07-16 12:01:39 +08:00
parent 326c6e3507
commit d4b69df538
14 changed files with 864 additions and 1 deletions

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
/**
* @method static void trackSearch(string $deviceId, string $searchType, string $searchQuery, ?array $searchFilters = null, ?\Carbon\Carbon $actionAt = null, ?string $userAgent = null, ?string $ipAddress = null, string $platform = 'web')
* @method static void trackContentSelection(string $deviceId, string $contentType, int $contentId, string $contentName, string $selectionMethod, ?string $searchQuery = null, ?\Carbon\Carbon $actionAt = null, ?string $userAgent = null, ?string $ipAddress = null, string $platform = 'web')
* @method static int trackExport(string $deviceId, ?int $memeId, ?int $memeMediaId, ?int $backgroundMediaId, array $captionTexts, string $exportFormat, string $exportQuality = 'standard', ?\Carbon\Carbon $actionAt = null, ?string $userAgent = null, ?string $ipAddress = null, string $platform = 'web')
* @method static void updateExportStatus(int $trackingExportId, string $status, ?string $errorMessage = null, ?\Carbon\Carbon $completedAt = null)
* @method static array getDeviceContext()
* @method static string generateDeviceId()
* @method static void quickTrackSearch(string $searchType, string $searchQuery, ?array $searchFilters = null)
* @method static void quickTrackContentSelection(string $contentType, int $contentId, string $contentName, string $selectionMethod, ?string $searchQuery = null)
* @method static int quickTrackExport(?int $memeId, ?int $memeMediaId, ?int $backgroundMediaId, array $captionTexts, string $exportFormat, string $exportQuality = 'standard')
*/
class TrackingAnalytics extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'tracking-analytics';
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Jobs;
use App\Models\TrackingContentSelection;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class TrackContentSelectionJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
private string $deviceId,
private string $contentType,
private int $contentId,
private string $contentName,
private string $selectionMethod,
private ?string $searchQuery,
private Carbon $actionAt,
private ?string $userAgent,
private ?string $ipAddress,
private string $platform
) {
$this->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(),
]);
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Jobs;
use App\Models\TrackingExport;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class TrackExportJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
private string $deviceId,
private ?int $memeId,
private ?int $memeMediaId,
private ?int $backgroundMediaId,
private array $captionTexts,
private string $exportFormat,
private string $exportQuality,
private Carbon $actionAt,
private ?string $userAgent,
private ?string $ipAddress,
private string $platform
) {
$this->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(),
]);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Jobs;
use App\Models\TrackingSearch;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class TrackSearchJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
private string $deviceId,
private string $searchType,
private string $searchQuery,
private ?array $searchFilters,
private Carbon $actionAt,
private ?string $userAgent,
private ?string $ipAddress,
private string $platform
) {
$this->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(),
]);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Jobs;
use App\Models\TrackingExport;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class UpdateExportStatusJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
private int $trackingExportId,
private string $status,
private ?string $errorMessage,
private ?Carbon $completedAt
) {
$this->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(),
]);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TrackingContentSelection extends Model
{
use HasFactory;
protected $fillable = [
'device_id',
'user_agent',
'ip_address',
'platform',
'content_type',
'content_id',
'content_name',
'search_query',
'selection_method',
'action_at',
];
protected $casts = [
'action_at' => '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;
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TrackingExport extends Model
{
use HasFactory;
protected $fillable = [
'device_id',
'user_agent',
'ip_address',
'platform',
'meme_id',
'meme_media_id',
'background_media_id',
'caption_texts',
'export_format',
'export_quality',
'export_status',
'error_message',
'action_at',
'completed_at',
];
protected $casts = [
'caption_texts' => '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');
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class TrackingSearch extends Model
{
use HasFactory;
protected $fillable = [
'device_id',
'user_agent',
'ip_address',
'platform',
'search_type',
'search_query',
'search_filters',
'action_at',
];
protected $casts = [
'search_filters' => 'array',
'action_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
protected $attributes = [
'platform' => 'web',
];
}

View File

@@ -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();
});
}
/**

View File

@@ -0,0 +1,205 @@
<?php
namespace App\Services;
use App\Jobs\TrackSearchJob;
use App\Jobs\TrackContentSelectionJob;
use App\Jobs\TrackExportJob;
use App\Jobs\UpdateExportStatusJob;
use Carbon\Carbon;
class TrackingAnalyticsService
{
/**
* Track a search action
*/
public function trackSearch(
string $deviceId,
string $searchType,
string $searchQuery,
?array $searchFilters = null,
?Carbon $actionAt = null,
?string $userAgent = null,
?string $ipAddress = null,
string $platform = 'web'
): void {
TrackSearchJob::dispatch(
$deviceId,
$searchType,
$searchQuery,
$searchFilters,
$actionAt ?? now(),
$userAgent,
$ipAddress,
$platform
);
}
/**
* Track a content selection action
*/
public function trackContentSelection(
string $deviceId,
string $contentType,
int $contentId,
string $contentName,
string $selectionMethod,
?string $searchQuery = null,
?Carbon $actionAt = null,
?string $userAgent = null,
?string $ipAddress = null,
string $platform = 'web'
): void {
TrackContentSelectionJob::dispatch(
$deviceId,
$contentType,
$contentId,
$contentName,
$selectionMethod,
$searchQuery,
$actionAt ?? now(),
$userAgent,
$ipAddress,
$platform
);
}
/**
* Track an export action
*/
public function trackExport(
string $deviceId,
?int $memeId,
?int $memeMediaId,
?int $backgroundMediaId,
array $captionTexts,
string $exportFormat,
string $exportQuality = 'standard',
?Carbon $actionAt = null,
?string $userAgent = null,
?string $ipAddress = null,
string $platform = 'web'
): int {
return TrackExportJob::dispatchSync(
$deviceId,
$memeId,
$memeMediaId,
$backgroundMediaId,
$captionTexts,
$exportFormat,
$exportQuality,
$actionAt ?? now(),
$userAgent,
$ipAddress,
$platform
);
}
/**
* Update export status
*/
public function updateExportStatus(
int $trackingExportId,
string $status,
?string $errorMessage = null,
?Carbon $completedAt = null
): void {
UpdateExportStatusJob::dispatch(
$trackingExportId,
$status,
$errorMessage,
$completedAt
);
}
/**
* Get device context from request
*/
public function getDeviceContext(): array
{
$request = request();
return [
'user_agent' => $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']
);
}
}