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

Email Tracking Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

Implementation Patterns

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:

  • Excluded from CSRF — it's a server-to-server POST, so add it to the validateCsrfTokens exceptions in bootstrap/app.php (Laravel 11 style) or your VerifyCsrfToken middleware.
  • Idempotent — providers retry aggressively. Deduplicate on the provider's event/message ID so re-delivery doesn't inflate open counts.
  • Fast — queue the processing and return 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.

Gotchas and Tips

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.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager