See where customers are coming from by attributing conversions directly in your Sylius store
Upgrading from 1.x? See UPGRADE.md.
composer require setono/sylius-conversion-attribution-pluginThis also installs setono/client-bundle, which identifies
visitors with the setono_client_id cookie, and setono/bot-detection-bundle.
The tracking script tag is added to your pages through setono/tag-bag-bundle
(3.1 or later), so install it too and follow its installation instructions:
composer require setono/tag-bag-bundleIf you'd rather render the script tag yourself, disable the javascript feature instead (see Configuration).
Symfony Flex registers the client, bot detection and tag bag bundles, but not this plugin, so add it to
config/bundles.php:
<?php
return [
// ...
Setono\SyliusConversionAttributionPlugin\SetonoSyliusConversionAttributionPlugin::class => ['all' => true],
];# config/routes/setono_sylius_conversion_attribution.yaml
setono_sylius_conversion_attribution:
resource: "@SetonoSyliusConversionAttributionPlugin/Resources/config/routes.yaml"<?php
declare(strict_types=1);
namespace App\Entity\Customer;
use Doctrine\ORM\Mapping as ORM;
use Setono\SyliusConversionAttributionPlugin\Model\CustomerInterface;
use Setono\SyliusConversionAttributionPlugin\Model\CustomerTrait;
use Sylius\Component\Core\Model\Customer as BaseCustomer;
/**
* @ORM\Entity
*
* @ORM\Table(name="sylius_customer")
*/
class Customer extends BaseCustomer implements CustomerInterface
{
use CustomerTrait;
}<?php
declare(strict_types=1);
namespace App\Entity\Order;
use Doctrine\ORM\Mapping as ORM;
use Setono\SyliusConversionAttributionPlugin\Model\OrderInterface;
use Setono\SyliusConversionAttributionPlugin\Model\OrderTrait;
use Sylius\Component\Core\Model\Order as BaseOrder;
/**
* @ORM\Entity
*
* @ORM\Table(name="sylius_order")
*/
class Order extends BaseOrder implements OrderInterface
{
use OrderTrait;
}php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrateThe tracking script is shipped as a bundle asset, so make sure assets are installed (Sylius' installation and deployment already do this):
php bin/console assets:installIf the script isn't installed, the browser gets a 404 for it and nothing is tracked.
Every tracked visit creates a source row, so the table grows continuously. Schedule the prune
command (for example daily via cron) to delete old rows. It keeps 180 days:
php bin/console setono:sylius-conversion-attribution:pruneAll options are optional. These are the defaults:
# config/packages/setono_sylius_conversion_attribution.yaml
setono_sylius_conversion_attribution:
# Adds the tracking script tag to HTML pages (requires setono/tag-bag-bundle)
javascript: true
# Seconds of inactivity after which a visitor's next page view is tracked again
session_timeout: 1800query_parameters maps query parameters, typically ad click ids, to a traffic source. The defaults are:
| Key | Query parameters | Source | Medium |
|---|---|---|---|
facebook |
fbclid |
facebook |
cpc |
google |
gclid, gbraid, wbraid |
google |
cpc |
microsoft |
msclkid |
bing |
cpc |
generic |
source, ref |
the parameter's value | |
tiktok |
ttclid |
tiktok |
cpc |
x |
twclid |
x |
cpc |
Add your own, or disable a default by its key:
setono_sylius_conversion_attribution:
query_parameters:
pinterest:
matches: [epik]
source: pinterest # without a source, the parameter's value is used
medium: cpc # optional, like campaign
generic:
enabled: falseOn every HTML page the plugin adds this tag (through the tag bag):
<script src="/bundles/setonosyliusconversionattributionplugin/js/track.js?v=…" id="ssca" defer
data-endpoint="/track" data-session-timeout="1800"></script>The script POSTs the page URL and the referrer to /track, where everything else is resolved server
side: the client id from the setono_client_id cookie riding on the request, and the traffic source
(utm parameters, click ids such as gclid/fbclid, the referrer) by the source matchers. To keep
the source table small, a visitor isn't tracked again within session_timeout seconds (a rolling
localStorage timestamp): only the first page view of a session is tracked, with its URL and
referrer, since that's where the visitor came from. Later page views within the session aren't, not
even with campaign parameters or a referrer from another site, so a customer returning from a payment
gateway (PayPal, Stripe, …) is never recorded as a referral. Prerendered pages are tracked when they are shown, not before.
A tracked page view gets its source from the first of these that matches:
- A query parameter from
query_parameters(gclid,fbclid, …, see Configuration) utm_source, together withutm_mediumandutm_campaign- The referrer. Search engines, social networks and email providers are looked up in
Snowplow's referer database, which is
downloaded when the cache is warmed up (if the download fails, a warning is logged and the lookup
is skipped until the next warmup). A site the database lists without a medium (e.g. Google Maps)
gets the medium
referral, and any other site becomes its host with the mediumreferral. Referrers from your own host are ignored. - Otherwise the source is
direct.
To add your own rule, implement SourceMatcherInterface and tag the service. The priority decides
the order: the built-in matchers have 300 (query parameters), 200 (utm) and 100 (referrer).
<?php
declare(strict_types=1);
namespace App\Attribution;
use Setono\SyliusConversionAttributionPlugin\Matcher\PageView;
use Setono\SyliusConversionAttributionPlugin\Matcher\Source;
use Setono\SyliusConversionAttributionPlugin\Matcher\SourceMatcherInterface;
final class NewsletterSourceMatcher implements SourceMatcherInterface
{
public function match(PageView $pageView): ?Source
{
// $pageView->url, $pageView->referrer, $pageView->host and $pageView->referrerHost are available
// too. Values come trimmed, and a blank query parameter or referrer is null
$issue = $pageView->getQueryParameter('newsletter');
if (null === $issue) {
return null;
}
return new Source('newsletter', 'email', $issue);
}
}# config/services.yaml
services:
App\Attribution\NewsletterSourceMatcher:
tags:
- { name: setono_sylius_conversion_attribution.source_matcher, priority: 250 }When a customer completes checkout, the visitor's client id is saved on the order and the customer. The order page in the admin then shows, in the sidebar, the newest non-direct source tracked for that client id up to checkout completion, with its medium, campaign, referrer, landing page and time. Visits after checkout, like the customer returning from a payment gateway, don't take the credit.
- The default JavaScript injection requires
setono/tag-bag-bundle. Without it, the container fails to build while thejavascriptfeature is enabled (the default). Install it, or setsetono_sylius_conversion_attribution.javascript: falseand render the script tag above yourself. - Bot filtering (for the
/trackendpoint) is provided bysetono/bot-detection-bundle, which is installed automatically as a dependency. Most bots are filtered structurally anyway: they don't execute JavaScript, so they never POST to/track. Automated browsers that do execute JavaScript (Selenium, Puppeteer, Playwright, Cypress, headless Chrome, …) are skipped by the script itself via the standardizednavigator.webdriverflag (and Cypress'window.Cypress). This also keeps your own end-to-end test runs out of the attribution data.
The script tag is static (identical for every visitor), so pages carrying it are safe to store in a full page / HTTP cache (Varnish, a CDN, Symfony HttpCache, …). Nothing per-visitor is rendered into the HTML. Make sure your layout renders the tag bag on cacheable pages: if a page never renders it, the tag bag stores the pending tag in the session, which can make the response uncacheable.
If your cache strips Set-Cookie from cached responses (it generally should), first-time visitors
get their client cookie on the uncached POST /track response instead, so attribution still works.
The script is a same-origin file, not an inline script, so a strict CSP needs no 'unsafe-inline',
hashes or nonces (nonces couldn't work with full page caching anyway). Allow the script in
script-src ('self', or your asset CDN if you serve assets from one) and the /track endpoint in
connect-src:
Content-Security-Policy: script-src 'self'; connect-src 'self'
A source row stores the visitor's IP address and user agent, which are personal data in many
jurisdictions. Keep retention short by scheduling the prune command, and ensure your privacy policy
and consent handling cover this collection (you may need to anonymize or omit the IP depending on your
requirements). On the visitor's device, the client id lives in the setono_client_id cookie and the
script keeps its session timestamp in localStorage (ssca_last_seen).