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
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,75 @@ final class YourService
}
```

## How it works

Dispatching a `ConversionsApiEventRaised` runs the event through a pipeline of listeners. The bundle populates the
event first, then leaves a gap for your own listeners, then filters and sends:

| Priority | Listener | What it does |
|---------------------------------------|---------------------------------------------------|---------------------------------------------------------|
| `PRIORITY_POPULATE` (1000) | `PopulateRequestPropertiesSubscriber` | Source url, client ip and user agent from the request |
| 900 | `PopulateFbpAndFbcPropertiesSubscriber` | `fbp` and `fbc` |
| 800 | `PopulateTestEventCodePropertySubscriber` | Test event code |
| 700 | `PopulatePixelsSubscriber` | Pixels from the pixel provider |
| **`PRIORITY_ENRICH` (0)** | **your listeners** | **Email, phone, external id, custom data** |
| -850 | `FilterEmptyUserAgentSubscriber` | Stops events without a user agent |
| -875 | `FilterConfiguredUserAgentsSubscriber` | Stops events matching `filters.user_agent` |
| `PRIORITY_FILTER` (-900) | `FilterBotsSubscriber` | Stops events from bots |
| -950 | `StopPropagationIfNoPixelsHasBeenAddedSubscriber` | Stops events without pixels |
| `PRIORITY_SEND` (-1000) | `AddEventToTagBagSubscriber` | Renders the `fbq()` calls (client side) |
| `PRIORITY_SEND` (-1000) | `DispatchOnCommandBusSubscriber` | Dispatches `SendEvent` (server side) |

Two things follow from this:

- **Enrich at `PRIORITY_ENRICH`**, which is the default priority of any listener. Everything the bundle knows about
the request is populated by then, and nothing has been filtered or sent yet.
- **A listener below `PRIORITY_FILTER` may never run**, because the filters stop propagation.

The constants live on `ConversionsApiEventRaised`, so you can position your listener without hard coding a number.

### Enriching an event

Everything the Conversions API can do beyond the browser pixel comes from the user data you attach server side. Meta
normalises and hashes it for you, so set the raw values:

```php
<?php

declare(strict_types=1);

namespace App\EventListener;

use Setono\MetaConversionsApiBundle\Event\ConversionsApiEventRaised;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener(priority: ConversionsApiEventRaised::PRIORITY_ENRICH)]
final class AddCustomerToConversionsApiEvent
{
public function __construct(private readonly Security $security)
{
}

public function __invoke(ConversionsApiEventRaised $event): void
{
$user = $this->security->getUser();
if (!$user instanceof User) {
return;
}

$userData = $event->event->userData;
$userData->email[] = $user->getEmail();
$userData->firstName[] = $user->getFirstName();
$userData->lastName[] = $user->getLastName();
$userData->externalId[] = (string) $user->getId();
}
}
```

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.

## Graph API version

Events are posted to the Graph API version of the installed `facebook/php-business-sdk` package (the SDK reads
Expand Down
33 changes: 33 additions & 0 deletions src/Event/ConversionsApiEventRaised.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,42 @@

/**
* Dispatch this event onto the EventDispatcher and everything will be handled for you
*
* The bundle's own listeners run in four bands. Use the constants below to position your own listener relative to
* them instead of hard coding a number:
*
* | Priority | What happens |
* |--------------------------------|-----------------------------------------------------------------------|
* | PRIORITY_POPULATE (and below) | The bundle fills in request properties, fbp/fbc, test event code, pixels |
* | PRIORITY_ENRICH | Your listeners add user data and custom data |
* | PRIORITY_FILTER | The bundle drops events it should not track (bots, filtered user agents) |
* | PRIORITY_SEND | The bundle renders the client side tags and dispatches the command |
*
* A listener below PRIORITY_FILTER may never run, because the filters stop propagation
*/
final class ConversionsApiEventRaised extends StoppableEvent
{
/**
* The bundle populates the event from the current request at this priority and just below it
*/
public const PRIORITY_POPULATE = 1000;

/**
* The priority your own listeners should use. Everything the bundle knows about the request is populated by
* now, and nothing has been filtered or sent yet. This is the default priority of an event listener
*/
public const PRIORITY_ENRICH = 0;

/**
* The bundle decides here whether the event should be tracked at all
*/
public const PRIORITY_FILTER = -900;

/**
* The bundle hands the event to the tag bag and the command bus at this priority
*/
public const PRIORITY_SEND = -1000;

/**
* @param array<string, mixed> $context
*/
Expand Down
2 changes: 1 addition & 1 deletion src/EventSubscriber/AddEventToTagBagSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public function __construct(
public static function getSubscribedEvents(): array
{
return [
ConversionsApiEventRaised::class => ['add', -1000],
ConversionsApiEventRaised::class => ['add', ConversionsApiEventRaised::PRIORITY_SEND],
];
}

Expand Down
2 changes: 1 addition & 1 deletion src/EventSubscriber/DispatchOnCommandBusSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function __construct(
public static function getSubscribedEvents(): array
{
return [
ConversionsApiEventRaised::class => ['dispatch', -1000],
ConversionsApiEventRaised::class => ['dispatch', ConversionsApiEventRaised::PRIORITY_SEND],
];
}

Expand Down
2 changes: 1 addition & 1 deletion src/EventSubscriber/FilterBotsSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public function __construct(private readonly BotDetectorInterface $botDetector)
public static function getSubscribedEvents(): array
{
return [
ConversionsApiEventRaised::class => ['filter', -900],
ConversionsApiEventRaised::class => ['filter', ConversionsApiEventRaised::PRIORITY_FILTER],
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public function __construct(array $userAgents)
public static function getSubscribedEvents(): array
{
return [
ConversionsApiEventRaised::class => ['filter', -875],
ConversionsApiEventRaised::class => ['filter', ConversionsApiEventRaised::PRIORITY_FILTER + 25],
];
}

Expand Down
2 changes: 1 addition & 1 deletion src/EventSubscriber/FilterEmptyUserAgentSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ final class FilterEmptyUserAgentSubscriber implements EventSubscriberInterface
public static function getSubscribedEvents(): array
{
return [
ConversionsApiEventRaised::class => ['filter', -850],
ConversionsApiEventRaised::class => ['filter', ConversionsApiEventRaised::PRIORITY_FILTER + 50],
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public function __construct(
public static function getSubscribedEvents(): array
{
return [
ConversionsApiEventRaised::class => ['populate', 900],
ConversionsApiEventRaised::class => ['populate', ConversionsApiEventRaised::PRIORITY_POPULATE - 100],
];
}

Expand Down
2 changes: 1 addition & 1 deletion src/EventSubscriber/PopulatePixelsSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public function __construct(private readonly PixelProviderInterface $pixelProvid
public static function getSubscribedEvents(): array
{
return [
ConversionsApiEventRaised::class => ['populate', 700],
ConversionsApiEventRaised::class => ['populate', ConversionsApiEventRaised::PRIORITY_POPULATE - 300],
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public function __construct(private readonly RequestStack $requestStack)
public static function getSubscribedEvents(): array
{
return [
ConversionsApiEventRaised::class => ['populate', 1000],
ConversionsApiEventRaised::class => ['populate', ConversionsApiEventRaised::PRIORITY_POPULATE],
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public function __construct(
public static function getSubscribedEvents(): array
{
return [
ConversionsApiEventRaised::class => ['populate', 800],
ConversionsApiEventRaised::class => ['populate', ConversionsApiEventRaised::PRIORITY_POPULATE - 200],
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ final class StopPropagationIfNoPixelsHasBeenAddedSubscriber implements EventSubs
public static function getSubscribedEvents(): array
{
return [
ConversionsApiEventRaised::class => ['filter', -950],
ConversionsApiEventRaised::class => ['filter', ConversionsApiEventRaised::PRIORITY_SEND + 50],
];
}

Expand Down
122 changes: 122 additions & 0 deletions tests/Unit/Event/ConversionsApiEventRaisedTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?php

declare(strict_types=1);

namespace Setono\MetaConversionsApiBundle\Tests\Unit\Event;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Setono\MetaConversionsApi\Event\Event;
use Setono\MetaConversionsApiBundle\Event\ConversionsApiEventRaised;
use Setono\MetaConversionsApiBundle\EventSubscriber\AddEventToTagBagSubscriber;
use Setono\MetaConversionsApiBundle\EventSubscriber\DispatchOnCommandBusSubscriber;
use Setono\MetaConversionsApiBundle\EventSubscriber\FilterBotsSubscriber;
use Setono\MetaConversionsApiBundle\EventSubscriber\FilterConfiguredUserAgentsSubscriber;
use Setono\MetaConversionsApiBundle\EventSubscriber\FilterEmptyUserAgentSubscriber;
use Setono\MetaConversionsApiBundle\EventSubscriber\PopulateFbpAndFbcPropertiesSubscriber;
use Setono\MetaConversionsApiBundle\EventSubscriber\PopulatePixelsSubscriber;
use Setono\MetaConversionsApiBundle\EventSubscriber\PopulateRequestPropertiesSubscriber;
use Setono\MetaConversionsApiBundle\EventSubscriber\PopulateTestEventCodePropertySubscriber;
use Setono\MetaConversionsApiBundle\EventSubscriber\StopPropagationIfNoPixelsHasBeenAddedSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

#[CoversClass(ConversionsApiEventRaised::class)]
final class ConversionsApiEventRaisedTest extends TestCase
{
/**
* The subscribers the bundle registers, in the order the event dispatcher must call them
*
* @var list<class-string<EventSubscriberInterface>>
*/
private const PIPELINE = [
PopulateRequestPropertiesSubscriber::class,
PopulateFbpAndFbcPropertiesSubscriber::class,
PopulateTestEventCodePropertySubscriber::class,
PopulatePixelsSubscriber::class,
FilterEmptyUserAgentSubscriber::class,
FilterConfiguredUserAgentsSubscriber::class,
FilterBotsSubscriber::class,
StopPropagationIfNoPixelsHasBeenAddedSubscriber::class,
AddEventToTagBagSubscriber::class,
DispatchOnCommandBusSubscriber::class,
];

#[Test]
public function it_has_context(): void
{
$event = new ConversionsApiEventRaised(new Event(Event::EVENT_VIEW_CONTENT), ['order' => 1]);

self::assertTrue($event->hasContext('order'));
self::assertFalse($event->hasContext('customer'));
}

/**
* The documented pipeline only holds as long as the bundle's own listeners keep their relative order, so this
* pins it down. It is the contract integrators position their own listeners against
*/
#[Test]
public function the_pipeline_runs_in_the_documented_order(): void
{
$previous = null;

foreach (self::PIPELINE as $subscriber) {
$priority = self::priority($subscriber);

if (null !== $previous) {
self::assertLessThanOrEqual($previous, $priority, sprintf('%s runs out of order', $subscriber));
}

$previous = $priority;
}
}

#[Test]
public function everything_is_populated_before_your_listeners_run(): void
{
foreach ([
PopulateRequestPropertiesSubscriber::class,
PopulateFbpAndFbcPropertiesSubscriber::class,
PopulateTestEventCodePropertySubscriber::class,
PopulatePixelsSubscriber::class,
] as $subscriber) {
self::assertGreaterThan(ConversionsApiEventRaised::PRIORITY_ENRICH, self::priority($subscriber));
}
}

#[Test]
public function filtering_and_sending_happen_after_your_listeners(): void
{
foreach ([
FilterEmptyUserAgentSubscriber::class,
FilterConfiguredUserAgentsSubscriber::class,
FilterBotsSubscriber::class,
StopPropagationIfNoPixelsHasBeenAddedSubscriber::class,
AddEventToTagBagSubscriber::class,
DispatchOnCommandBusSubscriber::class,
] as $subscriber) {
self::assertLessThan(ConversionsApiEventRaised::PRIORITY_ENRICH, self::priority($subscriber));
}
}

#[Test]
public function the_sinks_run_last(): void
{
self::assertSame(ConversionsApiEventRaised::PRIORITY_SEND, self::priority(AddEventToTagBagSubscriber::class));
self::assertSame(ConversionsApiEventRaised::PRIORITY_SEND, self::priority(DispatchOnCommandBusSubscriber::class));
}

/**
* @param class-string<EventSubscriberInterface> $subscriber
*/
private static function priority(string $subscriber): int
{
$listener = $subscriber::getSubscribedEvents()[ConversionsApiEventRaised::class] ?? null;

self::assertIsArray($listener);
self::assertArrayHasKey(1, $listener);
self::assertIsInt($listener[1]);

return $listener[1];
}
}
Loading