Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Utm Cookie Bundle Laravel Package

tattali/utm-cookie-bundle

tattali/utm-cookie-bundle captures UTM parameters from incoming URLs and stores them in cookies, so your app can attribute visits, signups, or conversions to the right campaign long after the first click. Simple setup and configurable cookie lifetime for PHP apps.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the important framing: tattali/utm-cookie-bundle is a Symfony bundle, not a Laravel package. It ships no service provider, no facade, no publishable config, and no package auto-discovery. composer require tattali/utm-cookie-bundle inside a Laravel app will install the files but nothing will wire itself up — no request listener runs, no cookie is ever written. Treat this as a reference implementation to read and port (it is small — a request listener, a service, a Twig extension, and a DI extension), not as a drop-in dependency.

That said, reading it is a good 20-minute exercise, because the problem it solves is one every marketing-facing Laravel app has.

Where to look first, in order:

  1. The event listener subscribed to kernel.request — this is the entire capture mechanism. It reads the query string, filters to known utm_* keys, and sets a cookie.
  2. The service class (the thing the Twig extension wraps) — this defines the read API: get-all vs. get-one.
  3. The DI extension / Configuration class — this is where the cookie name, lifetime, and captured key list are defined. Version drift lives here.

Minimal Symfony setup (if you're actually in a Symfony app):

// config/bundles.php
return [
    // ...
    Tattali\UtmCookieBundle\TattaliUtmCookieBundle::class => ['all' => true],
];
# config/packages/tattali_utm_cookie.yaml
tattali_utm_cookie:
    cookie_name: utm   # verify exact key names against Configuration.php
    cookie_duration: 30 # days — check whether this is days, seconds, or a date modifier

First use case: a paid or email campaign lands on https://app.test/?utm_source=newsletter&utm_medium=email&utm_campaign=spring. The listener writes those values to a cookie. Three pages later the user registers; you read the cookie and stamp utm_source, utm_medium, utm_campaign onto the users or leads row. That's it — that's the whole package.

Minimal Laravel equivalent, if you want the behavior today without waiting on a port:

// app/Http/Middleware/CaptureUtm.php
public function handle(Request $request, Closure $next)
{
    $keys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];
    $utm  = array_filter($request->only($keys));

    if ($utm !== []) {
        cookie()->queue(config('marketing.utm_cookie', 'utm'), json_encode($utm), 60 * 24 * 30);
    }

    return $next($request);
}

Read it back with json_decode($request->cookie('utm', '{}'), true).

Implementation Patterns

Read-all at conversion time. The dominant pattern is: capture passively on every request, read once at the moment of value. Inject the service into your registration/checkout controller and copy the full array onto the record. Don't read the cookie all over the codebase — one read point keeps attribution consistent and testable.

Read-one for hidden form fields. The Twig extension exists so templates can drop attribution into a form without a controller round-trip:

<input type="hidden" name="utm_source" value="{{ utm_cookie('utm_source') }}">

In Laravel, the equivalent is a view composer or View::share so the values are available to every Blade view:

View::composer('*', function ($view) use ($request) {
    $view->with('utm', json_decode($request->cookie('utm', '{}'), true));
});

First-touch vs. last-touch is a product decision, not a code detail. The bundle writes on every UTM-bearing request, which means last-touch wins — an email click overwrites the Google Ads click that got the user there. If marketing wants first-touch, guard the write:

if ($utm !== [] && ! $request->hasCookie('utm')) {
    cookie()->queue('utm', json_encode($utm), 60 * 24 * 30);
}

Decide this before launch. Retrofitting it later means your historical attribution data is inconsistent.

Attribute to the entity, not the session. Cookies expire, users switch devices, and consent banners delete them. The durable pattern is: cookie → hidden form field → leads.utm_source / orders.utm_json column on first meaningful action. After that, read from the database, never from the cookie.

Inertia / Livewire / SPA note. If you're server-rendering JSON props, share the UTM array as a page prop on first load only, then persist it in the payload of the conversion request. Don't expect the cookie to be readable from JavaScript unless you deliberately excluded it from HttpOnly.

Extension points. In Symfony, the intended extension points are: decorating the UTM service to add keys (e.g. gclid, fbclid, msclkid), or replacing the listener. In a Laravel port, the equivalent is making the key list config-driven rather than hardcoded — the bundle's fixed list is its main functional limitation.

Gotchas and Tips

It will silently do nothing in Laravel. No bundle registration, no listener, no cookie. This is the single biggest trap — you'll see the class exists, assume it works, and lose a week of attribution data before someone notices the Set-Cookie header is missing. If you install it in a Laravel app, you're porting it. Budget an afternoon and write tests.

Consent and GDPR. This bundle sets a cookie on the first request, before any consent interaction. In the EU that is a compliance problem, not a style preference. If you have a consent manager, gate the write behind it — which, in practice, means your own middleware anyway.

"Why is the cookie empty on the same request?" The capture listener reads the query string; the cookie is only visible on subsequent requests. In Laravel, cookie()->queue() writes to the response — $request->cookie('utm') will be null on the landing request itself. Always fall back to $request->only($keys) when reading on the first hit:

$utm = array_filter($request->only($keys)) ?: json_decode($request->cookie('utm', '{}'), true);

Only utm_* is captured. Click IDs (gclid, fbclid, ttclid, msclkid) and custom params are ignored by default. For paid social and Google Ads these are frequently more valuable than the UTM triplet. Extend the key list deliberately.

Cookie attributes need review. Domain, path, Secure, SameSite, and HttpOnly all matter here. Defaults often give you a host-only cookie that silently fails to travel between www. and app. subdomains. Check the Set-Cookie header in devtools on the landing request — that's the only source of truth. If the header looks right and the value is still missing, you almost certainly have a domain/path mismatch.

Config key names and units drift between versions. The repo has effectively zero community scrutiny (no dependents, no stars), so treat the README as aspirational. Read DependencyInjection/Configuration.php and use exactly what's there — in particular, confirm whether the expiration value is days, seconds, or a strtotime modifier.

Debugging checklist, in order: (1) does the landing URL actually contain the params? (2) does the response have a Set-Cookie? (3) does the next request send it back? (4) is the cookie name colliding with something else? Each step isolates a different layer, and skipping to step 4 wastes time.

Vendor it, don't depend on it. Given the near-total absence of adoption and the tiny surface area, the sane move is to copy the listener logic into your own middleware, keep the behavior you need, and drop the dependency. You get the extension points the bundle lacks and you don't inherit its maintenance risk.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment