henryavila/email-tracking
Laravel package for email tracking that monitors opens and link clicks by injecting tracking pixels, rewriting URLs, and recording engagement events, with per-message analytics to help measure campaign performance.
Environment check first. v7 requires PHP 8.2+ and Laravel 11+. Verify before touching composer:
php -v
php artisan --version
If you're still on Laravel 10 or PHP 8.1, upgrade Laravel and PHP first — the package will refuse to install otherwise. Laravel 10 is EOL and has known vulnerabilities, so treat this as a forcing function rather than an inconvenience.
Install:
composer require henryavila/email-tracking:^7.0
Publish and migrate. Publish the config and migrations, then run them. Note that v7 switched to Laravel's numeric-prefix migration naming convention, so if you have previously published migrations from v6, diff them before re-publishing — don't blindly overwrite.
php artisan vendor:publish --provider="HenryAvila\EmailTracking\EmailTrackingServiceProvider"
php artisan migrate
Register the listener — this is the v7 breaking change. Laravel 11 removed the auto-discovered EventServiceProvider::$listen pattern the package documented for v9/v10. Register explicitly in AppServiceProvider::boot():
public function boot(): void
{
Event::listen(
events: \Illuminate\Mail\Events\MessageSent::class,
listener: \HenryAvila\EmailTracking\Listeners\LogEmailSentListener::class,
);
}
If you skip this, emails send fine but nothing gets tracked — no error, no log entry. This is the single most common post-upgrade failure.
First use case: send any Mailable and confirm a tracking record is created. That validates the whole event → listener → persistence chain before you invest in webhooks or analytics.
Automatic tracking via the MessageSent event. Once the listener is registered, every outbound mail is captured automatically — no per-Mailable code. This is the default and preferred path; it keeps tracking concerns out of your Mailable classes and catches mail sent from anywhere in the app (jobs, notifications, third-party packages).
Manual tracking for targeted cases. When you need tracking context (custom metadata, user association, explicit type) that the global listener can't infer, attach it at the send site. Keep manual calls limited to cases where the automatic path genuinely can't resolve the data — mixing both indiscriminately makes it hard to reason about what's tracked.
Email type classification (new in v7). Classify each email as transactional, marketing, notification, etc. Two supported styles:
// BackedEnum — preferred for type safety and IDE support
enum EmailType: string
{
case Transactional = 'transactional';
case Marketing = 'marketing';
case Notification = 'notification';
}
or plain strings for quick adoption. Prefer the enum: it gives you a single source of truth, prevents typos in stored values, and makes filtering queries self-documenting. If you start with strings, migrate to an enum before the distinct values proliferate.
Use types to drive analytics and filtering — separating transactional from marketing volume is essential for deliverability analysis (marketing opens/clicks have very different baselines) and for compliance reporting.
Webhook ingestion for opens and clicks. Tracking data ultimately flows back via provider webhooks. Expose a route for your mail provider's callbacks, and keep it:
validateCsrfTokens exceptions in bootstrap/app.php (Laravel 11 style) or your VerifyCsrfToken middleware.200 immediately; providers will disable webhooks that time out.Job/queue interaction. Mail sent from queued jobs still fires MessageSent, so tracking works transparently. But be aware that MessageSent fires at send time, not delivery time — your tracking record exists before the provider confirms anything. Don't treat "tracked" as "delivered."
Testing. v7 ships Pest 3 and PHPUnit 11 support, which means your app's test suite is likely on the same stack. Use Mail::fake() plus Event::fake([MessageSent::class]) when you want to assert tracking behavior in isolation, and assert on MessageSent rather than on the listener directly.
The listener registration is silent when wrong. No exception, no warning — mail sends, nothing tracks. If tracking "stopped working" after the v7 upgrade, 90% of the time it's the EventServiceProvider → AppServiceProvider migration. Grep your codebase for the old $listen array and remove it.
Migration naming collisions. v7 adopts numeric-prefixed migration filenames. Re-publishing over an existing install can produce duplicate tables or ordering issues. Check database/migrations for the package's files before running vendor:publish again, and roll back stale copies if needed.
Enum vs. string inconsistency is a data problem, not a code problem. Stored values from string-based usage won't automatically match an enum's ->value. If you switch approaches, backfill existing rows — otherwise your filters silently miss historical records.
Version constraints cascade. ^7.0 pulls in updated dependencies; if another package in your app pins an older illuminate/* or PHPUnit, composer will conflict. The release notes call out CI/CD dependency conflicts resolved upstream — the same class of conflict can hit your app. Resolve by upgrading the conflicting package first, not by pinning the tracker lower.
Windows/multi-OS CI is covered (v7 tests on Ubuntu and Windows, PHP 8.2–8.4, Laravel 11 & 12), so if you develop on Windows and deploy to Linux, the package's own matrix supports you. Your app's coverage of that matrix is still your responsibility.
Coverage numbers are a floor, not a guarantee. ~92.8% coverage with reflection-based tests of private methods is genuinely good, but reflection tests are brittle against internal refactors — expect occasional churn in the package's private surface across minor releases. Not a reason to avoid it, just don't build workarounds on non-public APIs.
Extension points: prefer the config file and the event/listener boundary over subclassing package internals. Your own listeners on MessageSent compose cleanly alongside LogEmailSentListener, and running your logic after the tracking listener (via listener ordering) gives you guaranteed tracking records to enrich.
Upgrade sequence that actually works: PHP → Laravel 11 → run your existing test suite green → tracker ^7.0 → move listener registration → re-run tests. Doing the Laravel upgrade and the tracker upgrade in one commit makes failures ambiguous.
How can I help you explore Laravel packages today?