Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,23 @@ final class AddCustomerToConversionsApiEvent
You can also replace a step instead of adding to it: alias `PixelProviderInterface`, `FbpContextInterface` or
`FbcContextInterface` to your own service, or register a listener above the corresponding populate priority.

### Events that are not raised in a browser request

The pipeline assumes the event belongs to the request being handled. `PopulateRequestPropertiesSubscriber` therefore
fills in the source url, client ip and user agent of the current request, and the bot and user agent filters only
apply to events whose `actionSource` is `website` (the default).

For an event raised from a console command, a message handler or an incoming webhook, set another action source so
the filters leave it alone:

```php
$event = new Event(Event::EVENT_PURCHASE, Event::ACTION_SOURCE_SYSTEM_GENERATED);
```

If such an event is raised while handling an HTTP request, for instance a webhook from your payment provider, the
request properties still describe *that* request, not the customer. Overwrite them in a listener above
`PRIORITY_POPULATE` when they matter.

## Graph API version

Events are posted to the Graph API version of the installed `facebook/php-business-sdk` package (the SDK reads
Expand Down
7 changes: 7 additions & 0 deletions src/EventSubscriber/FilterBotsSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Setono\MetaConversionsApiBundle\EventSubscriber;

use Setono\BotDetectionBundle\BotDetector\BotDetectorInterface;
use Setono\MetaConversionsApi\Event\Event;
use Setono\MetaConversionsApiBundle\Event\ConversionsApiEventRaised;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

Expand All @@ -23,6 +24,12 @@ public static function getSubscribedEvents(): array

public function filter(ConversionsApiEventRaised $event): void
{
// A bot check is about the visitor behind the current request, which says nothing about an event raised
// from a console command or a message handler
if (Event::ACTION_SOURCE_WEBSITE !== $event->event->actionSource) {
return;
}

if ($this->botDetector->isBotRequest()) {
$event->stopPropagation();
}
Expand Down
11 changes: 10 additions & 1 deletion src/EventSubscriber/FilterEmptyUserAgentSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Setono\MetaConversionsApiBundle\EventSubscriber;

use Setono\MetaConversionsApi\Event\Event;
use Setono\MetaConversionsApiBundle\Event\ConversionsApiEventRaised;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

Expand All @@ -18,7 +19,15 @@ public static function getSubscribedEvents(): array

public function filter(ConversionsApiEventRaised $event): void
{
if (null === $event->event->userData->clientUserAgent || '' === $event->event->userData->clientUserAgent) {
// Meta only expects a client user agent for website events. An event raised from a console command, a
// message handler or a webhook legitimately has none, and dropping those would make the other action
// sources the SDK supports unusable
if (Event::ACTION_SOURCE_WEBSITE !== $event->event->actionSource) {
return;
}

$userAgent = $event->event->userData->clientUserAgent;
if (null === $userAgent || '' === $userAgent) {
$event->stopPropagation();
}
}
Expand Down
14 changes: 14 additions & 0 deletions tests/Unit/EventSubscriber/FilterBotsSubscriberTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ static function () use (&$enriched): void {
self::assertFalse($enriched);
}

/**
* An event raised from a console command or a message handler is not a request, so the bot check does not
* apply to it
*/
#[Test]
public function it_does_not_stop_a_non_website_event(): void
{
$event = new ConversionsApiEventRaised(new Event(Event::EVENT_PURCHASE, Event::ACTION_SOURCE_SYSTEM_GENERATED));

(new FilterBotsSubscriber(self::botDetector(true)))->filter($event);

self::assertFalse($event->isPropagationStopped());
}

private static function botDetector(bool $isBot): BotDetectorInterface
{
return new class($isBot) implements BotDetectorInterface {
Expand Down
80 changes: 80 additions & 0 deletions tests/Unit/EventSubscriber/FilterEmptyUserAgentSubscriberTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

declare(strict_types=1);

namespace Setono\MetaConversionsApiBundle\Tests\Unit\EventSubscriber;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Setono\MetaConversionsApi\Event\Event;
use Setono\MetaConversionsApiBundle\Event\ConversionsApiEventRaised;
use Setono\MetaConversionsApiBundle\EventSubscriber\FilterEmptyUserAgentSubscriber;

#[CoversClass(FilterEmptyUserAgentSubscriber::class)]
final class FilterEmptyUserAgentSubscriberTest extends TestCase
{
#[Test]
public function it_stops_a_website_event_without_a_user_agent(): void
{
$event = new ConversionsApiEventRaised(new Event(Event::EVENT_VIEW_CONTENT));

(new FilterEmptyUserAgentSubscriber())->filter($event);

self::assertTrue($event->isPropagationStopped());
}

#[Test]
public function it_stops_a_website_event_with_an_empty_user_agent(): void
{
$metaEvent = new Event(Event::EVENT_VIEW_CONTENT);
$metaEvent->userData->clientUserAgent = '';

$event = new ConversionsApiEventRaised($metaEvent);

(new FilterEmptyUserAgentSubscriber())->filter($event);

self::assertTrue($event->isPropagationStopped());
}

#[Test]
public function it_does_not_stop_a_website_event_with_a_user_agent(): void
{
$metaEvent = new Event(Event::EVENT_VIEW_CONTENT);
$metaEvent->userData->clientUserAgent = 'Chrome';

$event = new ConversionsApiEventRaised($metaEvent);

(new FilterEmptyUserAgentSubscriber())->filter($event);

self::assertFalse($event->isPropagationStopped());
}

/**
* Events raised from a console command, a message handler or a webhook have no user agent by definition
*/
#[Test]
#[DataProvider('nonWebsiteActionSources')]
public function it_does_not_stop_a_non_website_event(string $actionSource): void
{
$event = new ConversionsApiEventRaised(new Event(Event::EVENT_PURCHASE, $actionSource));

(new FilterEmptyUserAgentSubscriber())->filter($event);

self::assertFalse($event->isPropagationStopped());
}

/**
* @return iterable<string, array{string}>
*/
public static function nonWebsiteActionSources(): iterable
{
yield 'system generated' => [Event::ACTION_SOURCE_SYSTEM_GENERATED];
yield 'physical store' => [Event::ACTION_SOURCE_PHYSICAL_STORE];
yield 'email' => [Event::ACTION_SOURCE_EMAIL];
yield 'phone call' => [Event::ACTION_SOURCE_PHONE_CALL];
yield 'chat' => [Event::ACTION_SOURCE_CHAT];
yield 'other' => [Event::ACTION_SOURCE_OTHER];
}
}
Loading