fluffydiscord/sylius-google-analytics-bundle

Sylius integration for fluffydiscord/google-analytics-bundle: maps the Sylius checkout and catalogue flow onto GA4 events.

Maintainers

Package info

github.com/FluffyDiscord/sylius-google-analytics-bundle

Type:sylius-bundle

pkg:composer/fluffydiscord/sylius-google-analytics-bundle

Transparency log

Statistics

Installs: 29

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v3.0.0 2026-08-24 09:36 UTC

This package is auto-updated.

Last update: 2026-08-24 09:36:59 UTC


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:

  • purchase is listed in the base bundle's fluffy_discord_google_analytics.server_side_events,
  • analytics consent is granted, and
  • a client id can be parsed from the _ga cookie.

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: shippingTotal minus 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.

License

MIT