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

Event Tracker Bundle Laravel Package

happyr/event-tracker-bundle

HappyR EventTrackerBundle is a Symfony bundle for recording and managing application events. It integrates with Doctrine to persist tracked events and provides services for capturing user activity, logging actions, and building simple analytics.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the reality check: this is a Symfony bundle, not a Laravel package. No extra.laravel.providers, no Illuminate\Support\ServiceProvider, no facade. The 0.6.1 release adds exactly one thing — Symfony 5.0 support — which is a compatibility bump, not a Laravel feature. Four stars, zero dependents. Treat it as a reference implementation of a small tracker service that you wire up yourself — not as a dependency you require and forget.

Read these four files before writing any code (in this order):

  1. composer.json — the real constraint list. With 0.6.1 the symfony/* constraints now admit ^4.3|^5.0, which implies a PHP 7.2.5 floor and rules out anything relying on the pre-4.3 dispatcher API. Confirm whether it hard-requires symfony/framework-bundle, doctrine/orm, or psr/log.
  2. DependencyInjection/Configuration.php — the config tree. This tells you what is actually tunable (storage driver, enabled flag, logger channel, table/prefix names).
  3. Resources/config/services.xml — your real API surface. Service IDs, constructor arguments, tagged listeners. Everything you need to instantiate by hand lives here.
  4. The tracker class itself (usually Tracker / EventTracker) — the public methods are typically a track(string $name, array $context = []) shape plus maybe identify().

Minimal wiring in Laravel — skip the bundle, or bind it manually:

composer require happyr/event-tracker-bundle:0.6.1
// app/Providers/EventTrackerServiceProvider.php
public function register(): void
{
    $this->app->singleton(EventTracker::class, function ($app) {
        // Inspect services.xml for the real constructor signature.
        return new EventTracker(
            $app['log']->channel(config('event-tracker.channel', 'events')),
        );
    });

    $this->app->alias(EventTracker::class, 'event-tracker');
}

Because there is no Symfony Kernel::registerBundles(), the bundle's Extension never runs — the Configuration tree is never processed. You are responsible for building any config array yourself and passing it into constructors.

First use case: structured domain-event logging — order.paid, user.invited, webhook.failed — with JSON context, written to a dedicated log channel or an event_log table, then queried by name/date. That is the whole value proposition; everything else is your own code.

Implementation Patterns

1. Adapter boundary — never leak the bundle's namespace into app code.

// app/Contracts/Tracker.php
interface Tracker { public function track(string $name, array $context = []): void; }

// app/Services/BundleTrackerAdapter.php
final class BundleTrackerAdapter implements Tracker
{
    public function __construct(private EventTracker $inner) {}

    public function track(string $name, array $context = []): void
    {
        $this->inner->track($name, $context + [
            'user_id'    => auth()->id(),
            'request_id' => request()->header('X-Request-Id'),
            'env'        => app()->environment(),
        ]);
    }
}

This is the single most important pattern: if the bundle dies (still likely — 0.6.1 is a compat bump, not a revival), you swap one class. If you inject EventTracker directly into 40 controllers, you are rewriting 40 files.

2. Bridge Laravel events into the tracker. One listener beats instrumenting every call site.

// app/Providers/EventServiceProvider.php
Event::listen('*', function (string $event, array $payload) {
    if (is_subclass_of($event, TrackableEvent::class)) {
        app(Tracker::class)->track($event, $payload[0]->trackingContext());
    }
});

Prefer an explicit TrackableEvent marker interface over wildcard-matching everything — the '*' listener fires for framework-internal events too and will flood your log.

3. Swap or duplicate storage deliberately. If the bundle ships Doctrine entities, do not try to bring in doctrine/orm alongside Eloquent. Instead re-bind the storage side to an Eloquent model and keep only the tracker's dispatch/formatting logic:

tracked_events: id, name (indexed), context (json), user_id (nullable, indexed), occurred_at (indexed)

Index (name, occurred_at) — every real query is "how many checkout.failed since Tuesday".

4. Queue it. Tracking should never add latency to the request or fail the operation:

final class QueuedTracker implements Tracker, ShouldQueue
{
    public function track(string $name, array $context = []): void
    {
        TrackEventJob::dispatch($name, $context)->onQueue('telemetry');
    }
}

Use Bus::batch() for backfills and ->delay() for coalescing high-frequency events.

5. Enrich context in middleware, not in call sites. A PushTrackingContext middleware that sets user/tenant/request-id once keeps every track() call two arguments wide.

6. The Symfony 5 bump actually matters here. Symfony 5 ships only the PSR-14 dispatcher signature — dispatch(object $event, string $eventName = null). The legacy dispatch($eventName, Event $event) form from 4.2 and earlier is gone. Two consequences:

  • If the bundle is a Symfony EventSubscriber, its internals are now written against the listener object shape (__invoke(object $event, string $eventName, EventDispatcherInterface $dispatcher)), which is exactly what you want to mirror.
  • Illuminate\Events\Dispatcher implements PSR-14 on Laravel 9+. You can bind Symfony\Component\EventDispatcher\EventDispatcherInterface to a small forwarder that re-dispatches into Laravel's dispatcher:
$this->app->bind(SymfonyDispatcherInterface::class, fn ($app) =>
    new class($app['events']) implements SymfonyDispatcherInterface {
        public function __construct(private IlluminateDispatcher $events) {}
        public function dispatch(object $event, ?string $eventName = null): object
        {
            $this->events->dispatch($eventName ?? $event::class, $event);
            return $event;
        }
        // addListener/removeListener/getListeners delegate to $this->events
    });

Verify with app(SymfonyDispatcherInterface::class) in tinker before committing to this; it remains the most fragile part of the integration.

7. Testing. Assert on your Tracker interface, never the bundle:

$this->mock(Tracker::class)->shouldReceive('track')->once()->with('order.paid', Mockery::type('array'));
Log::spy(); // if you kept the log-channel backend

Event::fake() will not intercept the bundle's internal dispatcher — another reason for the adapter layer.

Gotchas and Tips

  • 0.6.1 is a one-line compat release, nothing more. No Laravel provider, no config tree support outside Symfony, no new tracking features. Do not read "Symfony 5.0 support" as "actively maintained" — the package is still effectively dormant, and the release date still sits in 2020. Pin an exact commit in composer.json (or | a fork under repositories) rather than ^1.0 / ^0.6 — a stray minor bump can break you silently.
  • The dispatcher signature is now unambiguous. Because 0.6.1 targets Symfony 5, you can rely on PSR-14 semantics when bridging (dispatch(object $event, ?string $eventName)). But the flip side: 0.5.x and earlier used the legacy two-string signature. Never mix code written against both — a dispatch('name', $event) call copied from an old blog post will silently misbehave.
  • PHP floor moved with Symfony 5. Expect php: ^7.2.5 in practice. Modern PHP 8.3 stacks will emit deprecation notices from untyped properties and the Event class hierarchy; #[\ReturnTypeWillChange] patches may be needed if you touch the internals.
  • The bundle's DI extension never executes under Laravel. No config tree, no %parameter% resolution, no tagged-service collection. Any service that expects an injected array $config from Configuration.php must be constructed by hand with the values you choose.
  • Service IDs are your contract. Read services.xml; don't guess class names. Symfony 5 changed default service visibility (private-by-default, .public aliases), so new-ing the class yourself is usually safer than trying to reach into a container that isn't there.
  • Context must be JSON-serializable. Closures, Eloquent models, and DateTime objects will fail on encode or produce useless output. Normalize: Carbon → toIso8601String(), models → ['id' => $m->id]. Add a json_encode guard in the adapter that logs and drops unencodable context rather than throwing mid-request.
  • Double-tracking. If the bundle registers its own listener and you wire a wildcard listener, high-traffic events get written twice. Gate the second path behind a config flag and check row counts with groupBy('name') after rollout.
  • PII and retention. Event tables are append-only and grow fast. Names like user.email_changed with a full payload are a GDPR liability. Ship a model:prune / scheduled DELETE WHERE occurred_at < now()->subDays(90) from day one, and allow-list which context keys are persisted.
  • Timezones. Store UTC (occurred_at) and keep the display conversion in the read layer; mixing now() local and UTC rows makes every "last 24h" query wrong.
  • Debugging the wiring: php artisan tinker → app(Tracker::class), then ->track('test.ping', ['a' => 1]). If that throws a constructor-argument error, the class's real signature is in services.xml — you read the wrong service. composer why-not happyr/event-tracker-bundle 0.6.1 is the fastest way to find out which package is still holding you on Symfony 4.
  • Extension points are limited to what you build. There is no documented plugin/decoration system here. The practical extension surface is: (a) decorate your own Tracker interface, (b) swap the storage backend by re-binding, (c) subclass the bundle's tracker if its methods aren't final. Check final and private before planning either.
  • Consider not using it. The classes are small; reading them and reimplementing a ~60-line tracker with a log channel gives you PHP 8.3 types, no dead dependency, and no integration archaeology. The Symfony 5 bump does not change that calculus — it only means the source is slightly less likely to explode on install. If you do keep it, wrap it and budget a removal path.
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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect