fluffydiscord / sylius-google-analytics-bundle
Sylius integration for fluffydiscord/google-analytics-bundle: maps the Sylius checkout and catalogue flow onto GA4 events.
Package info
github.com/FluffyDiscord/sylius-google-analytics-bundle
Type:sylius-bundle
pkg:composer/fluffydiscord/sylius-google-analytics-bundle
Requires
- php: >=8.3
- fluffydiscord/google-analytics-bundle: ^3.0
- sylius/sylius: ^2.2
- symfony/config: ^7.4
- symfony/dependency-injection: ^7.4
- symfony/event-dispatcher: ^7.4
- symfony/http-foundation: ^7.4
- symfony/http-kernel: ^7.4
Requires (Dev)
- phpunit/phpunit: ^10.5
README
Sylius integration for fluffydiscord/google-analytics-bundle. It maps the Sylius checkout and catalogue flow onto GA4 e-commerce events, so no tracking calls are written into templates or controllers.
Requirements
- PHP >= 8.3
- Symfony 7.4 (LTS)
- Sylius ^2.2
- fluffydiscord/google-analytics-bundle ^3.0
Installation
composer require fluffydiscord/sylius-google-analytics-bundle
Register the bundle in config/bundles.php:
return [ // ... FluffyDiscord\SyliusGoogleAnalyticsBundle\FluffyDiscordSyliusGoogleAnalyticsBundle::class => ['all' => true], ];
The bundle wires its subscribers automatically. One optional key controls how order phone numbers
are written into the purchase payload:
fluffy_discord_sylius_google_analytics: default_country_calling_code: '+420' # prepended to numbers stored without an international prefix user_data_needs_consent: true # omit userData unless ad_user_data is granted contact_request_lead: enabled: true # generate_lead when the contact form sends its e-mail event_category: 'contact form'
With no calling code configured, a national number is emitted as stored. The default formatter is
deliberately naive: it strips separators, rewrites a leading 00, and prepends the calling code to
a number that does not already start with it. It cannot tell a national number from one that
carries the calling code without a +, and it does not strip a foreign trunk prefix — implement
PhoneNumberFormatterInterface with libphonenumber when the shop takes numbers from several
countries.
Emitted events
| GA4 event | Sylius trigger | Delivery |
|---|---|---|
view_item |
kernel.request on route sylius_shop_product_show |
dataLayer |
add_to_cart |
sylius.cart_item_add (listener priority -10, after Sylius' merge and any add guard) |
dataLayer |
view_cart |
kernel.request on route sylius_shop_cart_summary |
dataLayer |
begin_checkout |
kernel.request on route sylius_shop_checkout_start |
dataLayer |
add_shipping_info |
sylius.order.post_select_shipping |
dataLayer |
add_payment_info |
sylius.order.post_payment |
dataLayer |
purchase |
sylius.order.post_complete |
dataLayer + server-side (Measurement Protocol) |
generate_lead |
the shop contact form sending its e-mail | dataLayer |
Client-side events are pushed to the data layer collector provided by the base bundle. Every
subscriber catches and logs its own errors (channel analytics) so a tracking failure never
breaks the shop flow.
Sylius 2 adds to the cart through a Live Component, and a shop may add through its own AJAX
endpoint, so most add_to_cart events are collected on a response that renders no <head>. They
travel on the X-Analytics-Events header instead. Register the base bundle's Stimulus controller
(see its README) — it hooks every Live Component render and pushes those events into the
dataLayer. Without it they are silently lost, and a shop that adds to the cart through its own
fetch hands the response to pushAnalyticsEvents() as well.
Purchase
purchase reaches the data layer like every other event, and is additionally sent through the
Measurement Protocol. Its client-side payload carries ecommerce.userData — the customer e-mail
and the order phone number in E.164 form, taken from the billing address, then the shipping
address, then the customer — for Tag Manager to hash into an enhanced conversion. Personal data
never reaches the Measurement Protocol parameters. Replace PhoneNumberFormatterInterface to plug
in a full libphonenumber formatter.
The Measurement Protocol snapshot is built
inside the HTTP request — where the _ga / _ga_* cookies and the consent state are readable —
and dispatched to messenger.default_bus for an async worker to deliver. It is only sent when:
purchaseis listed in the base bundle'sfluffy_discord_google_analytics.server_side_events,- analytics consent is granted, and
- a client id can be parsed from the
_gacookie.
Contact form leads
Sylius dispatches no event when the shop contact form is submitted, so the lead is collected by
decorating sylius_shop.mailer.contact_email_manager: the event is collected only once the contact
e-mail has actually been handed to the mailer, and a tracking failure is caught and logged like
everywhere else. The API contact endpoint uses a different mailer service and is left alone — it
serves clients with no data layer.
The form POSTs and redirects, so the push is deferred by the base bundle and renders on the page the visitor lands on:
dataLayer.push({ event: 'generate_lead', event_category: 'contact form', userData: { email: '…' } });
generate_lead is not an ecommerce event, so its parameters are not nested under ecommerce.
The e-mail is trimmed and lower-cased, and honours user_data_needs_consent exactly as the purchase
userData does. Set contact_request_lead.enabled: false to drop the decorator entirely.
Amounts
All amounts reach GA4 in integer minor units and are reported excluding tax. They come from a
single source, Ga4AmountResolverInterface, implemented by SyliusAmountResolver:
- order and item amounts:
total - taxTotal; - shipping:
shippingTotalminus only the neutral tax adjustments attached to a shipment (so a payment-fee tax on the same order is not subtracted); - catalogue price: Sylius'
sylius.calculator.product_variant_price.
To change the money model (net-price catalogues, VAT-exempt groups, gross reporting), decorate the interface rather than recomputing totals at each call site:
#[AsDecorator('fluffy_discord_sylius_google_analytics.resolver.amount')] final class MyAmountResolver implements Ga4AmountResolverInterface { // ... }
Upgrading from 2.x
3.0 tracks the base bundle's 3.0: login, sign_up and search are no longer wrapped in an
ecommerce object. The ecommerce events this bundle emits are unchanged. See the base bundle's
upgrade note.