medelse/utm-cookie-bundle
A Symfony bundle that stores UTM campaign parameters in a cookie so attribution data survives across requests and redirects. It captures traffic source details and keeps them available for later tracking, lead attribution, or analytics.
This is a Symfony bundle, not a Laravel package — there is no service provider auto-discovery. If you are working in Laravel, the practical move is to read the bundle's ~100 lines of source and port the listener into a Middleware (see Implementation Patterns). If you are in a Symfony app:
composer require medelse/utm-cookie-bundleconfig/bundles.php:
Medelse\UtmCookieBundle\MedelseUtmCookieBundle::class => ['all' => true],
DependencyInjection/Configuration.php in vendor/medelse/utm-cookie-bundle to confirm the config alias and options (cookie_name, cookie_lifetime, utm_keys, overwrite are the usual suspects). Add a config/packages/*.yaml block if you need to change the cookie name, TTL, or the list of tracked keys.?utm_source=newsletter&utm_medium=email&utm_campaign=spring). Hit it in a browser, open DevTools → Application → Cookies, and confirm the UTM values were persisted. Then load a second page without the query string and confirm the cookie is still there — that persistence is the entire value of the package.Where to look first: the bundle's event subscriber (it hooks kernel.request), because that determines when the cookie is written and whether it overwrites an existing first-touch value.
First-touch vs. last-touch attribution. The single most important decision. The bundle writes when UTM params are present in the query string; whether it clobbers an existing cookie depends on its overwrite behavior/config. Decide explicitly: first-touch (don't overwrite once set) is usually what marketing wants for signup attribution; last-touch is what ad platforms want. Verify the behavior in the listener and, if needed, decorate or replace it.
Reading the cookie server-side for conversion events. On signup, checkout, or lead-form submission, read the cookie from the request and persist it onto your domain entity:
// Symfony controller
$utms = $request->cookies->all();
// filter to your tracked keys, then:
$lead->setUtmSource($utms['utm_source'] ?? null);
$lead->setUtmCampaign($utms['utm_campaign'] ?? null);
Copy the values onto the order/lead at creation time — do not rely on the cookie surviving to fulfillment, because cookie TTLs, Safari ITP, and consent resets will eat it.
Hidden form fields. If forms submit cross-domain or you need attribution on a POST that may not carry the cookie, inject the UTM values into hidden inputs at render time. Cheap and survives more edge cases than the cookie alone.
Laravel port (recommended if you're in Laravel). The package's logic is trivial to reimplement as middleware:
public function handle($request, Closure $next)
{
$response = $next($request);
$keys = ['utm_source','utm_medium','utm_campaign','utm_term','utm_content'];
foreach ($keys as $key) {
if ($request->filled($key)) {
$response->cookie($key, $request->query($key), 60 * 24 * 30);
}
}
return $response;
}
This gives you full control over overwrite rules, consent gating, and cookie flags — all things the bundle makes awkward to customize.
Integration with analytics. Pair server-side persistence with a small JS read of document.cookie to forward UTMs into GA4/Matomo on pages that don't re-receive the query string. Keep the key names identical between the bundle config and your JS.
kernel.request will put you on the wrong side of ePrivacy. Gate the listener behind your consent manager, or switch to a session-scoped server-side store until consent is granted. The bundle has no built-in consent hook — you'll need to short-circuit the listener yourself.App\Utm\...) or fork it — the maintenance cost of owning 100 lines is lower than the cost of an abandoned bundle in composer.json.SameSite, Secure, and HttpOnly in the emitted Set-Cookie. Defaults from 2021-era Symfony may be laxer than your current policy, and HttpOnly will block the JS-read pattern above.curl -i "https://app.test/?utm_source=x" and inspect the Set-Cookie header — faster than fighting browser cache. If the cookie isn't appearing, check that your reverse proxy/CDN isn't stripping query strings, and that the listener is actually registered (bin/console debug:event-dispatcher kernel.request).services.yaml. If neither is ergonomic (likely, given the package's age), copy the class into your app namespace — you lose nothing.How can I help you explore Laravel packages today?