diff --git a/config/pixelify.php b/config/pixelify.php index 3fe472c..f80f610 100644 --- a/config/pixelify.php +++ b/config/pixelify.php @@ -53,4 +53,16 @@ | */ 'debug' => env('FACEBOOK_PIXEL_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Queued Listener Mode + |-------------------------------------------------------------------------- + | + | If true, the SendPixelEvent listener will be queued, allowing you to handle pixel events asynchronously. + | Make sure to set up a queue worker to process the jobs. If false, the listener will execute synchronously, + | which may impact performance if there are many events. + | + */ + 'queued_listener_mode' => env('FACEBOOK_QUEUED_LISTENER_MODE', false), ]; diff --git a/src/Listeners/SendPixelEvent.php b/src/Listeners/SendPixelEvent.php index 56120f0..ec27014 100644 --- a/src/Listeners/SendPixelEvent.php +++ b/src/Listeners/SendPixelEvent.php @@ -4,10 +4,11 @@ namespace Revoltify\Pixelify\Listeners; +use Illuminate\Contracts\Queue\ShouldQueue; use Revoltify\Pixelify\Events\PixelEventOccurred; use Revoltify\Pixelify\Http\Client\FacebookClient; -final readonly class SendPixelEvent +final readonly class SendPixelEvent implements ShouldQueue { public function __construct(private FacebookClient $client) {} @@ -15,4 +16,9 @@ public function handle(PixelEventOccurred $event): void { $this->client->sendEvent($event->eventData); } + + public function shouldQueue(PixelEventOccurred $event): bool + { + return config()->boolean('pixelify.queued_listener_mode', false); + } } diff --git a/tests/Feature/PixelifyTest.php b/tests/Feature/PixelifyTest.php index bb729f1..a3210b5 100644 --- a/tests/Feature/PixelifyTest.php +++ b/tests/Feature/PixelifyTest.php @@ -2,78 +2,85 @@ declare(strict_types=1); +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Cookie; use Illuminate\Support\Facades\Event; +use Revoltify\Pixelify\DTO\EventData; use Revoltify\Pixelify\DTO\ProductData; use Revoltify\Pixelify\DTO\UserData; use Revoltify\Pixelify\Events\PixelEventOccurred; use Revoltify\Pixelify\Facades\Pixelify; - -beforeEach(function (): void { - Event::fake(); -}); - -test('it can track page view event', function (): void { - $userData = new UserData( - firstName: 'John', - lastName: 'Doe', - email: 'test@example.com' - ); - - Pixelify::pageView($userData); - - Event::assertDispatched(PixelEventOccurred::class, fn ($event): bool => $event->eventData->eventName === 'PageView'); -}); - -test('it can track view content event', function (): void { - $productData = new ProductData( - productId: '123', - price: 99.99, - ); - - Pixelify::viewContent($productData); - - Event::assertDispatched(PixelEventOccurred::class, fn ($event): bool => $event->eventData->eventName === 'ViewContent' - && $event->eventData->productData->productId === '123'); +use Revoltify\Pixelify\Http\Middleware\FacebookTrackingMiddleware; +use Revoltify\Pixelify\Listeners\SendPixelEvent; + +dataset('listener modes', [ + 'queued mode' => [true], + 'sync mode' => [false], +]); + +dataset('tracking scenarios', [ + 'page view' => [ + fn (): mixed => Pixelify::pageView(new UserData(firstName: 'John', lastName: 'Doe', email: 'test@example.com')), + fn (PixelEventOccurred $event): bool => $event->eventData->eventName === 'PageView', + ], + 'view content' => [ + fn (): mixed => Pixelify::viewContent(new ProductData(productId: '123', price: 99.99)), + fn (PixelEventOccurred $event): bool => $event->eventData->eventName === 'ViewContent' + && $event->eventData->productData->productId === '123', + ], + 'add to cart' => [ + fn (): mixed => Pixelify::addToCart(new ProductData(productId: '123', price: 99.99, quantity: 2)), + fn (PixelEventOccurred $event): bool => $event->eventData->eventName === 'AddToCart' + && $event->eventData->productData->quantity === 2, + ], + 'initiate checkout' => [ + fn (): mixed => Pixelify::initiateCheckout( + new ProductData(productId: '123', price: 99.99), + new UserData(email: 'test@example.com') + ), + fn (PixelEventOccurred $event): bool => $event->eventData->eventName === 'InitiateCheckout' + && $event->eventData->userData->email === 'test@example.com', + ], + 'purchase' => [ + fn (): mixed => Pixelify::purchase(new ProductData(productId: '123', price: 99.99, currency: 'USD')), + fn (PixelEventOccurred $event): bool => $event->eventData->eventName === 'Purchase' + && $event->eventData->productData->currency === 'USD', + ], +]); + +describe('event tracking', function (): void { + beforeEach(function (): void { + Event::fake(); + }); + + test('it dispatches the expected pixel event', function (bool $isQueued, Closure $trackEvent, Closure $assertEvent): void { + config()->set('pixelify.queued_listener_mode', $isQueued); + + $trackEvent(); + + Event::assertDispatched(PixelEventOccurred::class, fn (PixelEventOccurred $event): bool => $assertEvent($event)); + })->with('listener modes')->with('tracking scenarios'); }); -test('it can track add to cart event', function (): void { - $productData = new ProductData( - productId: '123', - price: 99.99, - quantity: 2 - ); - - Pixelify::addToCart($productData); - - Event::assertDispatched(PixelEventOccurred::class, fn ($event): bool => $event->eventData->eventName === 'AddToCart' - && $event->eventData->productData->quantity === 2); -}); - -test('it can track initiate checkout event', function (): void { - $productData = new ProductData( - productId: '123', - price: 99.99 - ); - - $userData = new UserData( - email: 'test@example.com' - ); +describe('listener queueing', function (): void { + test('it follows the configured queue mode', function (bool $isQueued): void { + config()->set('pixelify.queued_listener_mode', $isQueued); - Pixelify::initiateCheckout($productData, $userData); + $listener = app(SendPixelEvent::class); + $event = new PixelEventOccurred(new EventData(eventName: 'PageView', eventId: 'test-id')); - Event::assertDispatched(PixelEventOccurred::class, fn ($event): bool => $event->eventData->eventName === 'InitiateCheckout' - && $event->eventData->userData->email === 'test@example.com'); + expect($listener->shouldQueue($event))->toBe($isQueued); + })->with('listener modes'); }); -test('it can track purchase event', function (): void { - $productData = new ProductData( - productId: '123', - price: 99.99, - currency: 'USD' - ); +describe('middleware tracking', function (): void { + test('it queues tracking cookies', function (bool $isQueued): void { + config()->set('pixelify.queued_listener_mode', $isQueued); - Pixelify::purchase($productData); + $middleware = app(FacebookTrackingMiddleware::class); + $middleware->handle(Request::create('/?fbclid=test123'), fn ($request) => response('ok')); - Event::assertDispatched(PixelEventOccurred::class, fn ($event): bool => $event->eventData->eventName === 'Purchase' - && $event->eventData->productData->currency === 'USD'); + expect(Cookie::queued('_fbc'))->not->toBeNull(); + expect(Cookie::queued('_fbp'))->not->toBeNull(); + })->with('listener modes'); });