diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 60ecb02..281146f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,21 +13,14 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest] - php: [8.1, 8.0, 7.4] - laravel: [9.*, 8.*, 7.*] + php: [8.1, 8.0] + laravel: [9.*, 8.*] stability: [prefer-lowest, prefer-stable] include: - laravel: 9.* testbench: 7.* - laravel: 8.* testbench: ^6.23 - - laravel: 7.* - testbench: ^5.20 - exclude: - - laravel: 9.* - php: 7.4 - - laravel: 7.* - php: 8.1 name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} diff --git a/README.md b/README.md index 82126ad..58bceea 100644 --- a/README.md +++ b/README.md @@ -32,89 +32,355 @@ return [ */ 'pixel_id' => env('CONVERSIONS_API_PIXEL_ID'), + /** + * The Google Tag Manager container ID used in case you're deduplicating + * events through Google Tag Manager instead of Facebook Pixel directly. + * Should look something like "GTM-XXXXXX". + */ + 'gtm_id' => env('GOOGLE_TAG_MANAGER_ID'), + /** * The Conversions API comes with a nice way to test your events. * You may use this config variable to set your test code. */ - 'test_code' => null, + 'test_code' => env('CONVERSIONS_API_TEST_CODE'), ]; ``` ## Conversions API -This package allows you to set the user data and events that will be sent to the Conversions API. +### Events +To add events to the conversions API you may use the `addEvent`, `addEvents` or `setEvents` methods. +Retrieving or clearing events may be done using the `getEvents` and `clearEvents` methods: ```php use Esign\ConversionsApi\Facades\ConversionsApi; use FacebookAds\Object\ServerSide\UserData; use FacebookAds\Object\ServerSide\Event; -ConversionsApi::setUserData( - (new UserData())->setFirstName('John')->setLastName('Doe') -); -ConversionsApi::setEvent( +ConversionsApi::addEvent( (new Event())->setEventName('PageView')->setEventId('abc') ); + +ConversionsApi::setEvents([ + (new Event())->setEventName('PageView')->setEventId('abc'), + (new Event())->setEventName('Purchase')->setEventId('xyz'), +]); + +ConversionsApi::getEvents(); +ConversionsApi::clearEvents(); +``` + +Adding events won't cause them to be sent to the Conversions API. +To actually send the events you must call the `sendEvents` method: +```php +use Esign\ConversionsApi\Facades\ConversionsApi; + +ConversionsApi::sendEvents(); ``` -To actually send the data you must call the `execute` method. +#### Creating event classes +To make things a bit cleaner you may extend Facebook's default event class: + ```php use Esign\ConversionsApi\Facades\ConversionsApi; +use FacebookAds\Object\ServerSide\ActionSource; +use FacebookAds\Object\ServerSide\Event; -ConversionsApi::execute(); +class PurchaseEvent extends Event +{ + public static function create(): static + { + return (new static()) + ->setActionSource(ActionSource::WEBSITE) + ->setEventName('Purchase') + ->setEventTime(time()) + ->setEventSourceUrl(request()->fullUrl()) + ->setEventId((string) Str::uuid()) + ->setUserData(ConversionsApi::getUserData()); + } +} +``` +```php +ConversionsApi::addEvent( + PurchaseEvent::create() +); ``` -This package also comes with a nice helper to send `PageView` events. -By including the `@conversionsApiPageView` directive on a page, an event with the minimum required data (ip address, user agent and request url) will be sent to the Conversions API: +### User Data +This package also comes with a way to define default user data for the user of the current request. +You may do so by calling the `setUserData` method, this is typically done in your `AppServiceProvider`: ```php -@conversionsApiPageView +use Esign\ConversionsApi\Facades\ConversionsApi; +use Esign\ConversionsApi\Objects\DefaultUserData; + +ConversionsApi::setUserData( + DefaultUserData::create() + ->setEmail(auth()->user()?->email) +); ``` -### +You may now pass the user data along with your events: +```php +use Esign\ConversionsApi\Facades\ConversionsApi; +use FacebookAds\Object\ServerSide\Event; + +ConversionsApi::addEvent( + (new Event())->setUserData(ConversionsApi::getUserData()) +); +``` -## Facebook Pixel -To [deduplicate browser and server events](https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events/) this package will automatically generate a unique event ID for every request. -This event ID should be passed along with your Facebook Pixel. -This package comes with a few ways to do this: +## Event deduplication +This package comes with a few ways to assist you in [deduplicating browser and server events](https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events/). This can either be done using the Facebook Pixel directly or through Google Tag Manager's data layer. ### Facebook Pixel -In case you want to directly load the Facebook Pixel script you may use the `@conversionsApiFacebookPixelScript` directive or directly include it. +Before attempting to send events through Facebook Pixel make sure to load the pixel script: +```blade + +``` + +This will render the following html: +```html + +``` + +This package will attempt to provide as much advanced matching data as possible by using user data from the `ConversionsApi`. +For example when an email address is set, it will automatically be provided to the init method: ```php -@conversionsApiFacebookPixelScript -@include('conversions-api::facebook-pixel-script') +ConversionsApi::setUserData( + (new UserData())->setEmail('john@example.com') +); +``` +```js +fbq('init', 'your-configured-pixel-id', {"em": "john@example.com"}); ``` -### Google Tag Manager -A convenient dataLayer helper is included in case you want to load the Facebook Pixel through Google Tag Manager. -By default a variable name `conversionsApiEventId` will be used: +Now that your Pixel is correctly initialized, it's time to send some events. +Sadly the parameters between the Conversions API and Facebook Pixel are not identical, so they must be mapped to the [correct format](https://developers.facebook.com/docs/meta-pixel/reference). +An easy way of doing this is by extending the `FacebookAds\Object\ServerSide\Event` class and implementing the `Esign\ConversionsApi\Contracts\MapsToFacebookPixel` interface on it: ```php -@conversionsApiDataLayer -@include('conversions-api::data-layer') +use Esign\ConversionsApi\Contracts\MapsToFacebookPixel; +use Esign\ConversionsApi\Facades\ConversionsApi; +use FacebookAds\Object\ServerSide\ActionSource; +use FacebookAds\Object\ServerSide\Event; + +class PurchaseEvent extends Event implements MapsToFacebookPixel +{ + public static function create(): static + { + return (new static()) + ->setActionSource(ActionSource::WEBSITE) + ->setEventName('Purchase') + ->setEventTime(time()) + ->setEventSourceUrl(request()->fullUrl()) + ->setEventId((string) Str::uuid()) + ->setUserData(ConversionsApi::getUserData()); + } + + public function getFacebookPixelEventType(): string + { + return 'track'; + } + + public function getFacebookPixelEventName(): string + { + return $this->getEventName(); + } + + public function getFacebookPixelCustomData(): array + { + $customData = $this->getCustomData(); + + return array_filter([ + 'currency' => $customData?->getCurrency(), + 'value' => $customData?->getValue(), + ]); + } + + public function getFacebookPixelEventData(): array + { + return array_filter(['eventID' => $this->getEventId()]); + } +} ``` -You may also pass a custom variable name: +You may now pass any class that implements the `MapsToFacebookPixel` interface to the view component responsible for tracking Facebook Pixel events: ```php -@conversionsApiDataLayer('yourDataLayerVariableName') -@include('conversions-api::data-layer', ['dataLayerVariableName' => 'yourDataLayerVariableName']) +use FacebookAds\Object\ServerSide\CustomData; +use Illuminate\Support\Str; + +$event = PurchaseEvent::create()->setCustomData( + (new CustomData())->setCurrency('EUR')->setValue(10) +); +``` + +```blade + +``` + +This will render the following script tag: +```html + ``` -#### Configuring Google Tag Manager -First off, you should add a new `Data Layer Variable` to your Google Tag Manager workspace. -![1](docs/images/gtm-step-1.png) +To retrieve a list of all events that implement the `MapsToFacebookPixel` interface you may call the `filterFacebookPixelEvents` method: +```blade +@foreach(ConversionsApi::getEvents()->filterFacebookPixelEvents() as $event) + +@endforeach +``` + +In case you want more control over what's being rendered, you may always use the anonymous component: +```blade + +``` + +### Google Tag Manager +Before attempting to deduplicate events through GTM make sure to configure your GTM container id and include the necessary scripts: + +```env +GOOGLE_TAG_MANAGER_ID=GTM-XXXXXX +``` -Next up you should use the variable name that was passed along to the data layer view. -![2](docs/images/gtm-step-2.png) +```blade + + + + {{-- ... --}} + + + + {{-- ... --}} + + +``` -After saving the variable you should be able to use it in your Facebook Pixel script using the double bracket syntax: `{{ Name of your variable }}`. -![3](docs/images/gtm-step-3.png) +This package comes with a view component that will map all user data from the `ConversionsApi` to dataLayer variables: +```blade + +``` +For example when an email address is set, it will be automatically mapped to a dataLayer variable. +Check the [source](src/View/Components/DataLayerUserDataVariable.php) of the view component to see a list of all possible variables. +```php +ConversionsApi::setUserData( + (new UserData())->setEmail('john@example.com') +); +``` +```js +window.dataLayer.push({"conversionsApiUserEmail": "john@example.com"}); +``` -### Manually retrieving the event ID -In case you want to use another strategy to deduplicate your events you can do so by manually retrieving the event ID: +Now that your Pixel through GTM is correctly initialized, it's time to send some events. +An easy way of doing this is by extending the `FacebookAds\Object\ServerSide\Event` class and implementing the `Esign\ConversionsApi\Contracts\MapsToDataLayer` interface on it: ```php +use Esign\ConversionsApi\Contracts\MapsToDataLayer; use Esign\ConversionsApi\Facades\ConversionsApi; +use FacebookAds\Object\ServerSide\ActionSource; +use FacebookAds\Object\ServerSide\Event; + +class PurchaseEvent extends Event implements MapsToDataLayer +{ + public static function create(): static + { + return (new static()) + ->setActionSource(ActionSource::WEBSITE) + ->setEventName('Purchase') + ->setEventTime(time()) + ->setEventSourceUrl(request()->fullUrl()) + ->setEventId((string) Str::uuid()) + ->setUserData(ConversionsApi::getUserData()); + } -ConversionsApi::getEventId(); + public function getDataLayerArguments(): array + { + $customData = $this->getCustomData(); + + return [ + 'event' => 'conversionsApiPurchase', + 'conversionsApiPurchaseEventId' => $this->getEventId(), + 'conversionsApiPurchaseCurrency' => $customData?->getCurrency(), + 'conversionsApiPurchaseValue' => $customData?->getValue(), + ]; + } +} +``` + +You may now pass any class that implements the `MapsToDataLayer` interface to the view component responsible for tracking Facebook Pixel events: +```php +use FacebookAds\Object\ServerSide\CustomData; +use Illuminate\Support\Str; + +$event = PurchaseEvent::create()->setCustomData( + (new CustomData())->setCurrency('EUR')->setValue(10) +); ``` +```blade + +``` + +This will render the following script tag: +```html + +``` + +To retrieve a list of all events that implement the `MapsToDataLayer` interface you may call the `filterDataLayerEvents` method: +```blade +@foreach(ConversionsApi::getEvents()->filterDataLayerEvents() as $event) + +@endforeach +``` + +In case you want more control over what's being rendered, you may always use the anonymous component: +```blade + +``` + +## PageView Events +This package ships with some helpers to track `PageView` events out of the box. +These helpers will automatically send both Conversions API & Facebook Pixel events and provide event deduplication. +> **Note** +> Make sure to always include these view components after you've already looped over any other events currently defined on the ConversionsApi. Including these view components will clear any existing events. + +In case you're using the Facebook Pixel directly: +```blade + +``` +Or by using Google Tag Manager. The data-layer variable to deduplicate events is called `conversionsApiPageViewEventId`. +```blade + +``` + +## Troubleshooting +### PageView events are not shown as deduplicated in the test events dashboard +Event deduplication for PageView events should be fine out of the box, since the event name and event id parameters have been provided. +However, when serving your application locally the ip address returned by Laravel's `request()->ip()` will be `127.0.0.1`. +This is different from the ip address sent through Facebook Pixel, causing the Conversions API and Facebook Pixel events to not be deduplicated. +This issue should solve itself once the application will be ran in production. + + + ## Testing ```bash diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000..8683fc1 --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,20 @@ +## From v1 to v2 +This package now supports to send multiple events at once to the Conversions API, instead of sending them one by one. While the previous version was mainly focused on sending `PageView` events, v2 now supports all kinds of events. + +### Method changes +- The `setEvent` method has been replaced by `addEvent`, `addEvents` and `setEvents` methods. +- The `getEvent` method has been removed in favor of `getEvents`. +- The `execute` method has been replaced by `sendEvents`. +- The `setEventByName` method has been removed, it's recommended to create [dedicated event classes](README.MD#creating-event-classes) for this. +- The `executePageViewEvent` has been removed and is now handled in dedicated view components, see [PageView Events](README.md#pageview-events) +- The `getEventId` and `setEventId` methods have been removed, since this package now supports multiple events, that each have their own unique event id. +You may use Laravel's built-in `Str::uuid()` helper to create unique event id's. +See the [docs](README.md#creating-event-classes) for an example on this. + +### Directive changes +- The `@conversionsApiPageView` directive has been replaced by 2 view components, depending on your deduplication preference. +In case you're using the Facebook Pixel directly, use ``. +If you're sending them through Google Tag Manager, use ``. +Note that the dataLayer variable name has been changed from `conversionsApiEventId` to `conversionsApiPageViewEventId`. +- The `@conversionsApiFacebookPixelScript` directive has been replaced with ``. +- The `@conversionsApiDataLayer` has been removed. Support for dataLayer events is possible through `` or ``. See the [docs](README.md#google-tag-manager) for an example on this. \ No newline at end of file diff --git a/composer.json b/composer.json index 5a79a70..a9a4e30 100644 --- a/composer.json +++ b/composer.json @@ -17,14 +17,14 @@ } ], "require": { - "php": "^7.4|^8.0", + "php": "^8.0", "facebook/php-business-sdk": "^12.0", - "illuminate/http": "^7.0|^8.0|^9.0", - "illuminate/support": "^7.0|^8.0|^9.0" + "illuminate/http": "^8.0|^9.0", + "illuminate/support": "^8.0|^9.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.3", - "orchestra/testbench": "^5.0|^6.0|^7.0", + "orchestra/testbench": "^6.0|^7.0", "phpunit/phpunit": "^9.0" }, "autoload": { diff --git a/config/conversions-api.php b/config/conversions-api.php index 7ae3695..b8867e1 100644 --- a/config/conversions-api.php +++ b/config/conversions-api.php @@ -11,9 +11,16 @@ */ 'pixel_id' => env('CONVERSIONS_API_PIXEL_ID'), + /** + * The Google Tag Manager container ID used in case you're deduplicating + * events through Google Tag Manager instead of Facebook Pixel directly. + * Should look something like "GTM-XXXXXX". + */ + 'gtm_id' => env('GOOGLE_TAG_MANAGER_ID'), + /** * The Conversions API comes with a nice way to test your events. * You may use this config variable to set your test code. */ - 'test_code' => null, + 'test_code' => env('CONVERSIONS_API_TEST_CODE'), ]; diff --git a/docs/images/gtm-step-1.png b/docs/images/gtm-step-1.png deleted file mode 100644 index 0fdb8a7..0000000 Binary files a/docs/images/gtm-step-1.png and /dev/null differ diff --git a/docs/images/gtm-step-2.png b/docs/images/gtm-step-2.png deleted file mode 100644 index a3a49b0..0000000 Binary files a/docs/images/gtm-step-2.png and /dev/null differ diff --git a/docs/images/gtm-step-3.png b/docs/images/gtm-step-3.png deleted file mode 100644 index 83968b6..0000000 Binary files a/docs/images/gtm-step-3.png and /dev/null differ diff --git a/resources/views/components/data-layer-variable.blade.php b/resources/views/components/data-layer-variable.blade.php new file mode 100644 index 0000000..9f84b36 --- /dev/null +++ b/resources/views/components/data-layer-variable.blade.php @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/resources/views/components/facebook-pixel-script.blade.php b/resources/views/components/facebook-pixel-script.blade.php new file mode 100644 index 0000000..42f03b4 --- /dev/null +++ b/resources/views/components/facebook-pixel-script.blade.php @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/resources/views/components/facebook-pixel-tracking-event.blade.php b/resources/views/components/facebook-pixel-tracking-event.blade.php new file mode 100644 index 0000000..2db7195 --- /dev/null +++ b/resources/views/components/facebook-pixel-tracking-event.blade.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/resources/views/components/google-tag-manager-body.blade.php b/resources/views/components/google-tag-manager-body.blade.php new file mode 100644 index 0000000..2390f8a --- /dev/null +++ b/resources/views/components/google-tag-manager-body.blade.php @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/resources/views/components/google-tag-manager-head.blade.php b/resources/views/components/google-tag-manager-head.blade.php new file mode 100644 index 0000000..a26ddf9 --- /dev/null +++ b/resources/views/components/google-tag-manager-head.blade.php @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/resources/views/data-layer.blade.php b/resources/views/data-layer.blade.php deleted file mode 100644 index a9a1d0a..0000000 --- a/resources/views/data-layer.blade.php +++ /dev/null @@ -1,4 +0,0 @@ - \ No newline at end of file diff --git a/resources/views/facebook-pixel-script.blade.php b/resources/views/facebook-pixel-script.blade.php deleted file mode 100644 index 789232c..0000000 --- a/resources/views/facebook-pixel-script.blade.php +++ /dev/null @@ -1,10 +0,0 @@ - \ No newline at end of file diff --git a/src/Collections/EventCollection.php b/src/Collections/EventCollection.php new file mode 100644 index 0000000..209a2ae --- /dev/null +++ b/src/Collections/EventCollection.php @@ -0,0 +1,20 @@ +filter(fn ($event) => $event instanceof MapsToFacebookPixel); + } + + public function filterDataLayerEvents(): static + { + return $this->filter(fn ($event) => $event instanceof MapsToDataLayer); + } +} diff --git a/src/Contracts/MapsToDataLayer.php b/src/Contracts/MapsToDataLayer.php new file mode 100644 index 0000000..1860fed --- /dev/null +++ b/src/Contracts/MapsToDataLayer.php @@ -0,0 +1,8 @@ +request = request(); - $this->userData = $this->getDefaultUserData(); - $this->eventId = (string) Str::uuid(); + $this->events = new EventCollection(); + $this->setUserData(DefaultUserData::create()); Api::init(null, null, config('conversions-api.access_token'), false); } - protected function getDefaultUserData(): UserData - { - return (new UserData()) - ->setClientIpAddress($this->request->ip()) - ->setClientUserAgent($this->request->userAgent()); - } - public function setUserData(UserData $userData): self { $this->userData = $userData; @@ -44,47 +34,41 @@ public function getUserData(): UserData return $this->userData; } - public function setEvent(Event $event): self + public function addEvent(Event $event): self { - $this->event = $event; + $this->events->push($event); return $this; } - public function getEvent(): ?Event + public function addEvents(iterable $events): self { - return $this->event; - } + $this->events = $this->events->merge($events); - public function getEventId(): string - { - return $this->eventId; + return $this; } - public function setEventId(string $eventId): self + public function setEvents(iterable $events): self { - $this->eventId = $eventId; + $this->events = new EventCollection($events); return $this; } - public function setEventByName(string $eventName): self + public function getEvents(): EventCollection { - $event = (new Event()) - ->setActionSource('website') - ->setEventName($eventName) - ->setEventId($this->getEventId()) - ->setEventTime(time()) - ->setEventSourceUrl($this->request->url()) - ->setUserData($this->userData); - - return $this->setEvent($event); + return $this->events; } - public function execute(): PromiseInterface + public function clearEvents(): self + { + return $this->setEvents([]); + } + + public function sendEvents(): PromiseInterface { $eventRequest = (new EventRequestAsync(config('conversions-api.pixel_id'))) - ->setEvents([$this->event]); + ->setEvents($this->events); if ($testCode = config('conversions-api.test_code')) { $eventRequest->setTestEventCode($testCode); @@ -92,9 +76,4 @@ public function execute(): PromiseInterface return $eventRequest->execute(); } - - public function executePageViewEvent(): PromiseInterface - { - return $this->setEventByName('PageView')->execute(); - } } diff --git a/src/ConversionsApiServiceProvider.php b/src/ConversionsApiServiceProvider.php index 2aaa46f..b908b87 100644 --- a/src/ConversionsApiServiceProvider.php +++ b/src/ConversionsApiServiceProvider.php @@ -2,7 +2,15 @@ namespace Esign\ConversionsApi; -use Illuminate\Support\Facades\Blade; +use Esign\ConversionsApi\Facades\ConversionsApi; +use Esign\ConversionsApi\View\Components\DataLayerPageView; +use Esign\ConversionsApi\View\Components\DataLayerUserDataVariable; +use Esign\ConversionsApi\View\Components\DataLayerVariable; +use Esign\ConversionsApi\View\Components\FacebookPixelPageView; +use Esign\ConversionsApi\View\Components\FacebookPixelScript; +use Esign\ConversionsApi\View\Components\FacebookPixelTrackingEvent; +use Esign\ConversionsApi\View\Components\GoogleTagManagerBody; +use Esign\ConversionsApi\View\Components\GoogleTagManagerHead; use Illuminate\Support\ServiceProvider; class ConversionsApiServiceProvider extends ServiceProvider @@ -10,7 +18,16 @@ class ConversionsApiServiceProvider extends ServiceProvider public function boot() { $this->loadViewsFrom($this->viewPath(), 'conversions-api'); - $this->registerBladeDirectives(); + $this->loadViewComponentsAs('conversions-api', [ + 'data-layer-page-view' => DataLayerPageView::class, + 'data-layer-variable' => DataLayerVariable::class, + 'data-layer-user-variable' => DataLayerUserDataVariable::class, + 'facebook-pixel-script' => FacebookPixelScript::class, + 'facebook-pixel-page-view' => FacebookPixelPageView::class, + 'facebook-pixel-tracking-event' => FacebookPixelTrackingEvent::class, + 'google-tag-manager-body' => GoogleTagManagerBody::class, + 'google-tag-manager-head' => GoogleTagManagerHead::class, + ]); if ($this->app->runningInConsole()) { $this->publishes([$this->configPath() => config_path('conversions-api.php')], 'config'); @@ -23,23 +40,6 @@ public function register() $this->app->singleton(ConversionsApi::class); } - protected function registerBladeDirectives(): void - { - Blade::directive('conversionsApiDataLayer', function (?string $dataLayerVariableName = null) { - if (! $dataLayerVariableName) { - return ""; - } - - return " $dataLayerVariableName]); ?>"; - }); - Blade::directive('conversionsApiFacebookPixelScript', function () { - return ""; - }); - Blade::directive('conversionsApiPageView', function () { - return "executePageViewEvent(); ?>"; - }); - } - protected function configPath(): string { return __DIR__ . '/../config/conversions-api.php'; diff --git a/src/Facades/ConversionsApi.php b/src/Facades/ConversionsApi.php index 2ee9a56..9e88e2a 100644 --- a/src/Facades/ConversionsApi.php +++ b/src/Facades/ConversionsApi.php @@ -5,6 +5,15 @@ use Illuminate\Support\Facades\Facade; /** + * @method static self setUserData(\FacebookAds\Object\ServerSide\UserData $userData) + * @method static \FacebookAds\Object\ServerSide\UserData getUserData() + * @method static self addEvent(\FacebookAds\Object\ServerSide\Event $event) + * @method static self addEvents(iterable $events) + * @method static self setEvents(iterable $events) + * @method static \Illuminate\Support\Collection getEvents() + * @method static self clearEvents() + * @method static \GuzzleHttp\Promise\PromiseInterface sendEvents() + * * @see \Esign\ConversionsApi\ConversionsApi */ class ConversionsApi extends Facade diff --git a/src/Objects/DefaultUserData.php b/src/Objects/DefaultUserData.php new file mode 100644 index 0000000..0b2fc71 --- /dev/null +++ b/src/Objects/DefaultUserData.php @@ -0,0 +1,17 @@ +setFbp(request()->cookie('_fbp')) + ->setFbc(request()->cookie('_fbc')) + ->setClientIpAddress(request()->ip()) + ->setClientUserAgent(request()->userAgent()); + } +} diff --git a/src/Objects/PageViewEvent.php b/src/Objects/PageViewEvent.php new file mode 100644 index 0000000..029b7ab --- /dev/null +++ b/src/Objects/PageViewEvent.php @@ -0,0 +1,52 @@ +setActionSource(ActionSource::WEBSITE) + ->setEventName('PageView') + ->setEventId((string) Str::uuid()) + ->setEventTime(time()) + ->setEventSourceUrl(request()->fullUrl()) + ->setUserData(ConversionsApi::getUserData()); + } + + public function getFacebookPixelEventType(): string + { + return 'track'; + } + + public function getFacebookPixelEventName(): string + { + return $this->getEventName(); + } + + public function getFacebookPixelCustomData(): array + { + return []; + } + + public function getFacebookPixelEventData(): array + { + return ['eventID' => $this->getEventId()]; + } + + public function getDataLayerArguments(): array + { + return [ + 'event' => 'conversionsApiPageView', + 'conversionsApiPageViewEventId' => $this->getEventId() + ]; + } +} diff --git a/src/View/Components/DataLayerPageView.php b/src/View/Components/DataLayerPageView.php new file mode 100644 index 0000000..f95d22d --- /dev/null +++ b/src/View/Components/DataLayerPageView.php @@ -0,0 +1,17 @@ +addEvent($pageViewEvent)->sendEvents(); + + parent::__construct($pageViewEvent); + } +} diff --git a/src/View/Components/DataLayerUserDataVariable.php b/src/View/Components/DataLayerUserDataVariable.php new file mode 100644 index 0000000..384a1cc --- /dev/null +++ b/src/View/Components/DataLayerUserDataVariable.php @@ -0,0 +1,33 @@ + $userData->getEmail(), + 'conversionsApiUserFirstName' => $userData->getFirstName(), + 'conversionsApiUserLastName' => $userData->getLastName(), + 'conversionsApiUserPhone' => $userData->getPhone(), + 'conversionsApiUserExternalId' => $userData->getExternalId(), + 'conversionsApiUserGender' => $userData->getGender(), + 'conversionsApiUserDateOfBirth' => $userData->getDateOfBirth(), + 'conversionsApiUserCity' => $userData->getCity(), + 'conversionsApiUserState' => $userData->getState(), + 'conversionsApiUserZipCode' => $userData->getZipCode(), + 'conversionsApiUserCountry' => $userData->getCountryCode(), + ]); + } +} \ No newline at end of file diff --git a/src/View/Components/DataLayerVariable.php b/src/View/Components/DataLayerVariable.php new file mode 100644 index 0000000..76d5922 --- /dev/null +++ b/src/View/Components/DataLayerVariable.php @@ -0,0 +1,21 @@ + $this->event->getDataLayerArguments(), + ]); + } +} diff --git a/src/View/Components/FacebookPixelPageView.php b/src/View/Components/FacebookPixelPageView.php new file mode 100644 index 0000000..4dbbccd --- /dev/null +++ b/src/View/Components/FacebookPixelPageView.php @@ -0,0 +1,17 @@ +addEvent($pageViewEvent)->sendEvents(); + + parent::__construct($pageViewEvent); + } +} diff --git a/src/View/Components/FacebookPixelScript.php b/src/View/Components/FacebookPixelScript.php new file mode 100644 index 0000000..6c87e36 --- /dev/null +++ b/src/View/Components/FacebookPixelScript.php @@ -0,0 +1,52 @@ +pixelId = $pixelId ?? config('conversions-api.pixel_id'); + $this->advancedMatchingData = $advancedMatchingData ?? $this->getAdvancedMatchingDataFromConversionsApiUserData(); + } + + protected function getAdvancedMatchingDataFromConversionsApiUserData(): array + { + $userData = ConversionsApi::getUserData(); + + return array_filter([ + 'em' => $userData->getEmail(), + 'fn' => $userData->getFirstName(), + 'ln' => $userData->getLastName(), + 'ph' => $userData->getPhone(), + 'external_id' => $userData->getExternalId(), + 'ge' => $userData->getGender(), + 'db' => $userData->getDateOfBirth(), + 'ct' => $userData->getCity(), + 'st' => $userData->getState(), + 'zp' => $userData->getZipCode(), + 'country' => $userData->getCountryCode(), + ]); + } + + public function render() + { + return view('conversions-api::components.facebook-pixel-script', [ + 'pixelId' => $this->pixelId, + 'advancedMatchingData' => $this->advancedMatchingData, + ]); + } +} diff --git a/src/View/Components/FacebookPixelTrackingEvent.php b/src/View/Components/FacebookPixelTrackingEvent.php new file mode 100644 index 0000000..a3ca96c --- /dev/null +++ b/src/View/Components/FacebookPixelTrackingEvent.php @@ -0,0 +1,24 @@ + $this->event->getFacebookPixelEventType(), + 'eventName' => $this->event->getFacebookPixelEventName(), + 'customData' => $this->event->getFacebookPixelCustomData(), + 'eventData' => $this->event->getFacebookPixelEventData(), + ]); + } +} diff --git a/src/View/Components/GoogleTagManagerBody.php b/src/View/Components/GoogleTagManagerBody.php new file mode 100644 index 0000000..2009a9b --- /dev/null +++ b/src/View/Components/GoogleTagManagerBody.php @@ -0,0 +1,22 @@ +gtmId = $gtmId ?? config('conversions-api.gtm_id'); + } + + public function render() + { + return view('conversions-api::components.google-tag-manager-body', [ + 'gtmId' => $this->gtmId, + ]); + } +} \ No newline at end of file diff --git a/src/View/Components/GoogleTagManagerHead.php b/src/View/Components/GoogleTagManagerHead.php new file mode 100644 index 0000000..c820e47 --- /dev/null +++ b/src/View/Components/GoogleTagManagerHead.php @@ -0,0 +1,22 @@ +gtmId = $gtmId ?? config('conversions-api.gtm_id'); + } + + public function render() + { + return view('conversions-api::components.google-tag-manager-head', [ + 'gtmId' => $this->gtmId, + ]); + } +} \ No newline at end of file diff --git a/tests/ConversionsApiTest.php b/tests/ConversionsApiTest.php deleted file mode 100644 index 968d9c6..0000000 --- a/tests/ConversionsApiTest.php +++ /dev/null @@ -1,75 +0,0 @@ -assertTrue(Str::isUuid(ConversionsApi::getEventId())); - } - - /** @test */ - public function it_can_set_an_event_id() - { - ConversionsApi::setEventId('abc'); - - $this->assertEquals('abc', ConversionsApi::getEventId()); - } - - /** @test */ - public function it_can_set_user_data_by_default() - { - request()->headers->set('USER_AGENT', 'Esign Agent'); - request()->server->set('REMOTE_ADDR', '0.0.0.0'); - - $this->assertEquals('0.0.0.0', ConversionsApi::getUserData()->getClientIpAddress()); - $this->assertEquals('Esign Agent', ConversionsApi::getUserData()->getClientUserAgent()); - } - - /** @test */ - public function it_can_set_user_data() - { - ConversionsApi::setUserData( - (new UserData())->setFirstName('John')->setLastName('Doe') - ); - - $this->assertEquals('John', ConversionsApi::getUserData()->getFirstName()); - $this->assertEquals('Doe', ConversionsApi::getUserData()->getLastName()); - } - - /** @test */ - public function it_wont_have_an_event_by_default() - { - $this->assertNull(ConversionsApi::getEvent()); - } - - /** @test */ - public function it_can_set_an_event() - { - ConversionsApi::setEvent( - (new Event())->setEventName('PageView')->setEventId('abc') - ); - - $this->assertEquals('PageView', ConversionsApi::getEvent()->getEventName()); - $this->assertEquals('abc', ConversionsApi::getEvent()->getEventId()); - } - - /** @test */ - public function it_can_set_an_event_by_name() - { - request()->headers->set('HOST', 'www.esign.eu'); - request()->server->set('HTTPS', true); - ConversionsApi::setEventByName('Contact'); - - $this->assertEquals('Contact', ConversionsApi::getEvent()->getEventName()); - $this->assertEquals('website', ConversionsApi::getEvent()->getActionSource()); - $this->assertEquals('https://www.esign.eu', ConversionsApi::getEvent()->getEventSourceUrl()); - } -} diff --git a/tests/ConversionsApiViewTest.php b/tests/ConversionsApiViewTest.php deleted file mode 100644 index 822db87..0000000 --- a/tests/ConversionsApiViewTest.php +++ /dev/null @@ -1,89 +0,0 @@ -assertSame( - 'executePageViewEvent(); ?>', - Blade::compileString('@conversionsApiPageView'), - ); - } - - /** @test */ - public function it_can_render_the_data_layer_view() - { - $this->assertStringContainsString( - ConversionsApi::getEventId(), - view('conversions-api::data-layer')->render() - ); - } - - /** @test */ - public function it_can_contain_a_default_data_layer_variable_name() - { - $this->assertStringContainsString( - 'conversionsApiEventId', - view('conversions-api::data-layer')->render() - ); - } - - /** @test */ - public function it_can_use_a_custom_data_layer_variable_name() - { - $this->assertStringContainsString( - 'customDataLayerVariableName', - view('conversions-api::data-layer', ['dataLayerVariableName' => 'customDataLayerVariableName'])->render() - ); - } - - /** @test */ - public function it_can_render_the_facebook_pixel_script_view() - { - Config::set('conversions-api.pixel_id', 'your-pixel-id'); - - $this->assertStringContainsString( - ConversionsApi::getEventId(), - view('conversions-api::facebook-pixel-script')->render() - ); - - $this->assertStringContainsString( - 'your-pixel-id', - view('conversions-api::facebook-pixel-script')->render() - ); - } - - /** @test */ - public function it_can_render_the_facebook_pixel_script_directive() - { - $this->assertStringContainsString( - "", - Blade::compileString('@conversionsApiFacebookPixelScript'), - ); - } - - /** @test */ - public function it_can_render_the_data_layer_directive() - { - $this->assertStringContainsString( - " 'customDataLayerVariableName']); ?>", - Blade::compileString("@conversionsApiDataLayer('customDataLayerVariableName')"), - ); - } - - /** @test */ - public function it_can_render_the_data_layer_directive_when_no_arguments_are_provided() - { - $this->assertStringContainsString( - "", - Blade::compileString("@conversionsApiDataLayer"), - ); - } -} diff --git a/tests/Feature/ConversionsApiTest.php b/tests/Feature/ConversionsApiTest.php new file mode 100644 index 0000000..b47db12 --- /dev/null +++ b/tests/Feature/ConversionsApiTest.php @@ -0,0 +1,100 @@ +headers->set('USER_AGENT', 'Esign Agent'); + request()->server->set('REMOTE_ADDR', '0.0.0.0'); + + $this->assertEquals('0.0.0.0', ConversionsApi::getUserData()->getClientIpAddress()); + $this->assertEquals('Esign Agent', ConversionsApi::getUserData()->getClientUserAgent()); + } + + /** @test */ + public function it_can_set_user_data() + { + ConversionsApi::setUserData( + (new UserData())->setFirstName('John')->setLastName('Doe') + ); + + $this->assertEquals('John', ConversionsApi::getUserData()->getFirstName()); + $this->assertEquals('Doe', ConversionsApi::getUserData()->getLastName()); + } + + /** @test */ + public function it_can_add_an_event() + { + ConversionsApi::addEvent( + (new Event())->setEventName('PageView')->setEventId('abc') + ); + + $this->assertCount(1, ConversionsApi::getEvents()); + $this->assertEquals('PageView', ConversionsApi::getEvents()->first()->getEventName()); + $this->assertEquals('abc', ConversionsApi::getEvents()->first()->getEventId()); + } + + /** @test */ + public function it_can_add_multiple_events() + { + ConversionsApi::addEvent( + (new Event())->setEventName('PageView')->setEventId('abc') + ); + ConversionsApi::addEvents([ + (new Event())->setEventName('PageView')->setEventId('xyz'), + ]); + + $this->assertCount(2, ConversionsApi::getEvents()); + $this->assertEquals('PageView', ConversionsApi::getEvents()->first()->getEventName()); + $this->assertEquals('abc', ConversionsApi::getEvents()->first()->getEventId()); + } + + /** @test */ + public function it_can_set_an_array_of_events() + { + ConversionsApi::addEvent( + (new Event())->setEventName('PageView')->setEventId('abc') + ); + ConversionsApi::setEvents([ + (new Event())->setEventName('PageView')->setEventId('xyz'), + ]); + + $this->assertCount(1, ConversionsApi::getEvents()); + $this->assertEquals('PageView', ConversionsApi::getEvents()->first()->getEventName()); + $this->assertEquals('xyz', ConversionsApi::getEvents()->first()->getEventId()); + } + + /** @test */ + public function it_can_clear_events() + { + ConversionsApi::setEvents([ + (new Event())->setEventName('PageView')->setEventId('abc'), + ]); + + ConversionsApi::clearEvents(); + + $this->assertCount(0, ConversionsApi::getEvents()); + } + + /** @test */ + public function it_can_get_events() + { + ConversionsApi::setEvents([ + (new Event())->setEventName('PageView')->setEventId('abc'), + ]); + + $events = ConversionsApi::getEvents(); + + $this->assertInstanceOf(EventCollection::class, $events); + $this->assertCount(1, $events); + } +} diff --git a/tests/Feature/View/Components/DataLayerPageViewTest.php b/tests/Feature/View/Components/DataLayerPageViewTest.php new file mode 100644 index 0000000..2cce774 --- /dev/null +++ b/tests/Feature/View/Components/DataLayerPageViewTest.php @@ -0,0 +1,40 @@ + 'b13ddf8f-df2d-4554-9ae6-a1a73861b0ad'); + $component = $this->component(DataLayerPageView::class); + + $component->assertSee( + 'window.dataLayer.push({"event":"conversionsApiPageView","conversionsApiPageViewEventId":"b13ddf8f-df2d-4554-9ae6-a1a73861b0ad"});', + false + ); + } + + /** @test */ + public function it_can_execute_a_page_view_event() + { + $this->mock(ConversionsApi::class, function (MockInterface $mock) { + $mock->shouldReceive('getUserData')->once(); + $mock->shouldReceive('clearEvents')->once()->andReturnSelf(); + $mock->shouldReceive('addEvent')->once()->andReturnSelf(); + $mock->shouldReceive('sendEvents')->once(); + }); + + $this->component(DataLayerPageView::class); + } +} diff --git a/tests/Feature/View/Components/DataLayerUserDataVariableTest.php b/tests/Feature/View/Components/DataLayerUserDataVariableTest.php new file mode 100644 index 0000000..1684bc8 --- /dev/null +++ b/tests/Feature/View/Components/DataLayerUserDataVariableTest.php @@ -0,0 +1,30 @@ +setEmail('test@test.com') + ); + $component = $this->component(DataLayerUserDataVariable::class); + + $component->assertSee( + 'window.dataLayer.push({"conversionsApiUserEmail":"test@test.com"});', + false + ); + } +} diff --git a/tests/Feature/View/Components/DataLayerVariableTest.php b/tests/Feature/View/Components/DataLayerVariableTest.php new file mode 100644 index 0000000..e0e6f88 --- /dev/null +++ b/tests/Feature/View/Components/DataLayerVariableTest.php @@ -0,0 +1,39 @@ +setEventId('9a97e3f0-3dbb-4d74-bf05-a42f330f843d'); + $component = $this->component(DataLayerVariable::class, [ + 'event' => $event, + ]); + + $component->assertSee( + 'window.dataLayer.push({"event":"Contact","conversionsApiContactEventId":"9a97e3f0-3dbb-4d74-bf05-a42f330f843d"});', + false + ); + } + + /** @test */ + public function it_can_render_anonymously() + { + $view = $this->blade(' + + '); + + $view->assertSee('window.dataLayer.push({"event":"contact"});', false); + } +} diff --git a/tests/Feature/View/Components/FacebookPixelPageViewTest.php b/tests/Feature/View/Components/FacebookPixelPageViewTest.php new file mode 100644 index 0000000..7560e34 --- /dev/null +++ b/tests/Feature/View/Components/FacebookPixelPageViewTest.php @@ -0,0 +1,40 @@ + 'b13ddf8f-df2d-4554-9ae6-a1a73861b0ad'); + $component = $this->component(FacebookPixelPageView::class); + + $component->assertSee( + "fbq('track', 'PageView', {}, {\"eventID\":\"b13ddf8f-df2d-4554-9ae6-a1a73861b0ad\"}", + false + ); + } + + /** @test */ + public function it_can_execute_a_page_view_event() + { + $this->mock(ConversionsApi::class, function (MockInterface $mock) { + $mock->shouldReceive('getUserData')->once(); + $mock->shouldReceive('clearEvents')->once()->andReturnSelf(); + $mock->shouldReceive('addEvent')->once()->andReturnSelf(); + $mock->shouldReceive('sendEvents')->once(); + }); + + $this->component(FacebookPixelPageView::class); + } +} diff --git a/tests/Feature/View/Components/FacebookPixelScriptTest.php b/tests/Feature/View/Components/FacebookPixelScriptTest.php new file mode 100644 index 0000000..64bd643 --- /dev/null +++ b/tests/Feature/View/Components/FacebookPixelScriptTest.php @@ -0,0 +1,45 @@ +setEmail('test@test.com')); + $component = $this->component(FacebookPixelScript::class); + + $component->assertSee("fbq('init', '414800860114807', {\"em\":\"test@test.com\"});", false); + } + + /** @test */ + public function it_can_render_an_empty_object_for_advanced_matching_data() + { + Config::set('conversions-api.pixel_id', '414800860114807'); + $component = $this->component(FacebookPixelScript::class); + + $component->assertSee("fbq('init', '414800860114807', {});", false); + } + + /** @test */ + public function it_can_render_the_view_passing_custom_data() + { + $component = $this->component(FacebookPixelScript::class, [ + 'pixelId' => '744689831385767', + 'advancedMatchingData' => ['em' => 'test@test.com'], + ]); + + $component->assertSee("fbq('init', '744689831385767', {\"em\":\"test@test.com\"});", false); + } +} diff --git a/tests/Feature/View/Components/FacebookPixelTrackingEventTest.php b/tests/Feature/View/Components/FacebookPixelTrackingEventTest.php new file mode 100644 index 0000000..2d7e5f2 --- /dev/null +++ b/tests/Feature/View/Components/FacebookPixelTrackingEventTest.php @@ -0,0 +1,72 @@ +component(FacebookPixelTrackingEvent::class, [ + 'event' => $event, + ]); + + $component->assertSee('track'); + $component->assertSee('Purchase'); + } + + /** @test */ + public function it_can_encode_custom_data_and_event_data_as_objects_when_they_are_empty_arrays() + { + $event = (new PurchaseEvent()); + $component = $this->component(FacebookPixelTrackingEvent::class, [ + 'event' => $event, + ]); + + $component->assertSee( + "fbq('track', 'Purchase', {}, {});", + false + ); + } + + /** @test */ + public function it_can_json_encode_custom_data_and_event_data() + { + $contents = (new Content())->setProductId('10')->setQuantity(2); + $customData = (new CustomData())->setValue(120)->setCurrency('GBP')->setContents([$contents]); + $event = (new PurchaseEvent())->setCustomData($customData)->setEventId('123'); + $component = $this->component(FacebookPixelTrackingEvent::class, [ + 'event' => $event, + ]); + + $component->assertSee( + "fbq('track', 'Purchase', {\"value\":120,\"currency\":\"GBP\",\"contents\":[{\"id\":\"10\",\"quantity\":2}]}, {\"eventID\":\"123\"});", + false + ); + } + + /** @test */ + public function it_can_render_anonymously() + { + $view = $this->blade(' + + '); + + $view->assertSee("fbq('track', 'Purchase', {}, {});", false); + } +} diff --git a/tests/Feature/View/Components/GoogleTagManagerBodyTest.php b/tests/Feature/View/Components/GoogleTagManagerBodyTest.php new file mode 100644 index 0000000..2140ca5 --- /dev/null +++ b/tests/Feature/View/Components/GoogleTagManagerBodyTest.php @@ -0,0 +1,33 @@ +component(GoogleTagManagerBody::class); + + $component->assertSee('https://www.googletagmanager.com/ns.html?id=GTM-123456'); + } + + /** @test */ + public function it_can_render_the_view_using_custom_data() + { + Config::set('conversions-api.gtm_id', null); + $component = $this->component(GoogleTagManagerBody::class, [ + 'gtmId' => 'GTM-123456', + ]); + + $component->assertSee('https://www.googletagmanager.com/ns.html?id=GTM-123456'); + } +} \ No newline at end of file diff --git a/tests/Feature/View/Components/GoogleTagManagerHeadTest.php b/tests/Feature/View/Components/GoogleTagManagerHeadTest.php new file mode 100644 index 0000000..b1360a3 --- /dev/null +++ b/tests/Feature/View/Components/GoogleTagManagerHeadTest.php @@ -0,0 +1,33 @@ +component(GoogleTagManagerHead::class); + + $component->assertSee("(window,document,'script','dataLayer','GTM-123456')", false); + } + + /** @test */ + public function it_can_render_the_view_using_custom_data() + { + Config::set('conversions-api.gtm_id', null); + $component = $this->component(GoogleTagManagerHead::class, [ + 'gtmId' => 'GTM-123456', + ]); + + $component->assertSee("(window,document,'script','dataLayer','GTM-123456')", false); + } +} \ No newline at end of file diff --git a/tests/Support/Events/ContactEvent.php b/tests/Support/Events/ContactEvent.php new file mode 100644 index 0000000..befc699 --- /dev/null +++ b/tests/Support/Events/ContactEvent.php @@ -0,0 +1,17 @@ + 'Contact', + 'conversionsApiContactEventId' => $this->getEventId(), + ]; + } +} diff --git a/tests/Support/Events/PurchaseEvent.php b/tests/Support/Events/PurchaseEvent.php new file mode 100644 index 0000000..2826313 --- /dev/null +++ b/tests/Support/Events/PurchaseEvent.php @@ -0,0 +1,41 @@ +getCustomData(); + + return array_filter([ + 'value' => $customData?->getValue(), + 'currency' => $customData?->getCurrency(), + 'contents' => array_map(function (Content $content) { + return [ + 'id' => $content->getProductId(), + 'quantity' => $content->getQuantity(), + ]; + }, $customData?->getContents() ?? []), + ]); + } + + public function getFacebookPixelEventData(): array + { + return array_filter(['eventID' => $this->getEventId()]); + } +} diff --git a/tests/Unit/Collections/EventCollectionTest.php b/tests/Unit/Collections/EventCollectionTest.php new file mode 100644 index 0000000..2b4e982 --- /dev/null +++ b/tests/Unit/Collections/EventCollectionTest.php @@ -0,0 +1,39 @@ +filterFacebookPixelEvents(); + + $this->assertTrue($facebookPixelEvents->contains($purchaseEvent)); + $this->assertFalse($facebookPixelEvents->contains($contactEvent)); + } + + /** @test */ + public function it_can_filter_data_layer_events() + { + $eventCollection = new EventCollection([ + $purchaseEvent = new PurchaseEvent(), + $contactEvent = new ContactEvent(), + ]); + + $dataLayerEvents = $eventCollection->filterDataLayerEvents(); + + $this->assertFalse($dataLayerEvents->contains($purchaseEvent)); + $this->assertTrue($dataLayerEvents->contains($contactEvent)); + } +} diff --git a/tests/Unit/Objects/PageViewEventTest.php b/tests/Unit/Objects/PageViewEventTest.php new file mode 100644 index 0000000..eb35637 --- /dev/null +++ b/tests/Unit/Objects/PageViewEventTest.php @@ -0,0 +1,27 @@ +get('posts?title=abc'); + Str::createUuidsUsing(fn () => 'b13ddf8f-df2d-4554-9ae6-a1a73861b0ad'); + + $event = PageViewEvent::create(); + + $this->assertEquals(ActionSource::WEBSITE, $event->getActionSource()); + $this->assertEquals('PageView', $event->getEventName()); + $this->assertEquals('b13ddf8f-df2d-4554-9ae6-a1a73861b0ad', $event->getEventId()); + $this->assertEquals(ConversionsApi::getUserData(), $event->getUserData()); + $this->assertEquals('http://localhost/posts?title=abc', $event->getEventSourceUrl()); + } +} \ No newline at end of file