diff --git a/README.md b/README.md index ba37294..8718f50 100644 --- a/README.md +++ b/README.md @@ -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 +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 diff --git a/src/Event/ConversionsApiEventRaised.php b/src/Event/ConversionsApiEventRaised.php index 3489281..b5a8f0a 100644 --- a/src/Event/ConversionsApiEventRaised.php +++ b/src/Event/ConversionsApiEventRaised.php @@ -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 $context */ diff --git a/src/EventSubscriber/AddEventToTagBagSubscriber.php b/src/EventSubscriber/AddEventToTagBagSubscriber.php index 14f9589..9394af4 100644 --- a/src/EventSubscriber/AddEventToTagBagSubscriber.php +++ b/src/EventSubscriber/AddEventToTagBagSubscriber.php @@ -25,7 +25,7 @@ public function __construct( public static function getSubscribedEvents(): array { return [ - ConversionsApiEventRaised::class => ['add', -1000], + ConversionsApiEventRaised::class => ['add', ConversionsApiEventRaised::PRIORITY_SEND], ]; } diff --git a/src/EventSubscriber/DispatchOnCommandBusSubscriber.php b/src/EventSubscriber/DispatchOnCommandBusSubscriber.php index ff79721..e52fe95 100644 --- a/src/EventSubscriber/DispatchOnCommandBusSubscriber.php +++ b/src/EventSubscriber/DispatchOnCommandBusSubscriber.php @@ -21,7 +21,7 @@ public function __construct( public static function getSubscribedEvents(): array { return [ - ConversionsApiEventRaised::class => ['dispatch', -1000], + ConversionsApiEventRaised::class => ['dispatch', ConversionsApiEventRaised::PRIORITY_SEND], ]; } diff --git a/src/EventSubscriber/FilterBotsSubscriber.php b/src/EventSubscriber/FilterBotsSubscriber.php index ff35c26..a407055 100644 --- a/src/EventSubscriber/FilterBotsSubscriber.php +++ b/src/EventSubscriber/FilterBotsSubscriber.php @@ -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], ]; } diff --git a/src/EventSubscriber/FilterConfiguredUserAgentsSubscriber.php b/src/EventSubscriber/FilterConfiguredUserAgentsSubscriber.php index 6ba48b2..4ce047c 100644 --- a/src/EventSubscriber/FilterConfiguredUserAgentsSubscriber.php +++ b/src/EventSubscriber/FilterConfiguredUserAgentsSubscriber.php @@ -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], ]; } diff --git a/src/EventSubscriber/FilterEmptyUserAgentSubscriber.php b/src/EventSubscriber/FilterEmptyUserAgentSubscriber.php index 834cfa4..7abec51 100644 --- a/src/EventSubscriber/FilterEmptyUserAgentSubscriber.php +++ b/src/EventSubscriber/FilterEmptyUserAgentSubscriber.php @@ -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], ]; } diff --git a/src/EventSubscriber/PopulateFbpAndFbcPropertiesSubscriber.php b/src/EventSubscriber/PopulateFbpAndFbcPropertiesSubscriber.php index 11886df..11de20e 100644 --- a/src/EventSubscriber/PopulateFbpAndFbcPropertiesSubscriber.php +++ b/src/EventSubscriber/PopulateFbpAndFbcPropertiesSubscriber.php @@ -20,7 +20,7 @@ public function __construct( public static function getSubscribedEvents(): array { return [ - ConversionsApiEventRaised::class => ['populate', 900], + ConversionsApiEventRaised::class => ['populate', ConversionsApiEventRaised::PRIORITY_POPULATE - 100], ]; } diff --git a/src/EventSubscriber/PopulatePixelsSubscriber.php b/src/EventSubscriber/PopulatePixelsSubscriber.php index b71bbba..f55c1b0 100644 --- a/src/EventSubscriber/PopulatePixelsSubscriber.php +++ b/src/EventSubscriber/PopulatePixelsSubscriber.php @@ -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], ]; } diff --git a/src/EventSubscriber/PopulateRequestPropertiesSubscriber.php b/src/EventSubscriber/PopulateRequestPropertiesSubscriber.php index 3f07711..afb22b3 100644 --- a/src/EventSubscriber/PopulateRequestPropertiesSubscriber.php +++ b/src/EventSubscriber/PopulateRequestPropertiesSubscriber.php @@ -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], ]; } diff --git a/src/EventSubscriber/PopulateTestEventCodePropertySubscriber.php b/src/EventSubscriber/PopulateTestEventCodePropertySubscriber.php index 5770ece..802dbb5 100644 --- a/src/EventSubscriber/PopulateTestEventCodePropertySubscriber.php +++ b/src/EventSubscriber/PopulateTestEventCodePropertySubscriber.php @@ -23,7 +23,7 @@ public function __construct( public static function getSubscribedEvents(): array { return [ - ConversionsApiEventRaised::class => ['populate', 800], + ConversionsApiEventRaised::class => ['populate', ConversionsApiEventRaised::PRIORITY_POPULATE - 200], ]; } diff --git a/src/EventSubscriber/StopPropagationIfNoPixelsHasBeenAddedSubscriber.php b/src/EventSubscriber/StopPropagationIfNoPixelsHasBeenAddedSubscriber.php index 5331399..d111744 100644 --- a/src/EventSubscriber/StopPropagationIfNoPixelsHasBeenAddedSubscriber.php +++ b/src/EventSubscriber/StopPropagationIfNoPixelsHasBeenAddedSubscriber.php @@ -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], ]; } diff --git a/tests/Unit/Event/ConversionsApiEventRaisedTest.php b/tests/Unit/Event/ConversionsApiEventRaisedTest.php new file mode 100644 index 0000000..a56624d --- /dev/null +++ b/tests/Unit/Event/ConversionsApiEventRaisedTest.php @@ -0,0 +1,122 @@ +> + */ + 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 $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]; + } +}