keen-io/keen-io-bundle
Symfony bundle for integrating Keen IO analytics into your application. Configure project credentials and access Keen IO event tracking and querying services through the container, simplifying analytics instrumentation in Symfony apps.
Read this first: it is not a Laravel package. keen-io/keen-io-bundle is a Symfony2 bundle — it ships a Bundle class, a DependencyInjection/Extension, and service definitions in YAML/XML that only ever get compiled by Symfony's container. Laravel's container will not touch it, and there is no service provider, no facade, no config publishing, and composer require will not trigger auto-discovery. The practical move for a Laravel app is to use the bundle as documentation and depend on the underlying client (keen-io/keen-io, the Keen IO PHP client) directly.
Minimal steps if you are porting the idea:
composer require keen-io/keen-io — you want the client library, not the bundle. Check composer why-not keen-io/keen-io if resolution fails; old Keen clients pinned Guzzle 3/5 and will fight Laravel's Guzzle 7.config/services.php (project ID, write key, read key, optional master key) sourced from .env. Never commit keys; the write key is public-ish, the master key is full account access.addEvent() + sendQueuedEvents() against a throwaway collection before wiring it into anything real.Where to look first: open the bundle's DependencyInjection/Configuration.php and its service definitions. That is the authoritative list of config keys, service IDs, and constructor arguments — the README is thin and the exact key names have drifted between releases. Mirror those key names in your config/services.php so the port stays recognisable.
First use case that pays for itself: server-side event tracking for things you cannot see in the browser — signups, subscription changes, webhook receipts, background job completions. One collection, three or four events, queried through Keen's analysis endpoints or streamed into a dashboard.
Bind an interface, not the vendor client. The client's API surface is small (addEvent, addEvents, sendQueuedEvents, plus read/query methods), so wrap it. This buys you testability, and it buys you an exit ramp — Keen IO is a Scale AI property and the hosted service's long-term availability is worth pricing in.
// app/Services/Analytics/AnalyticsClient.php
interface AnalyticsClient
{
public function record(string $collection, array $event, array $keen = []): void;
public function flush(): void;
}
Respect the batching lifecycle. Keen's write API is queue-based: addEvent() only appends to an in-memory batch; nothing leaves the process until sendQueuedEvents(). In a Laravel request cycle that means either flushing in a terminable middleware, or flushing in a queued job's finally block. If you flush at the end of every request you have thrown away the reason batching exists; if you never flush, you silently lose everything.
// app/Http/Middleware/FlushAnalytics.php (registered as terminable)
public function terminate($request, $response): void
{
try {
app(AnalyticsClient::class)->flush();
} catch (\Throwable $e) {
report($e); // never let analytics break a response
}
}
Prefer a queued job for high-volume or latency-sensitive paths. A RecordEvent job on a dedicated analytics queue, with a batch accumulator that flushes on JobProcessed or when the payload hits ~1000 events, keeps the flush out of the user-facing request entirely. This is also where you get retries for free, which the raw client does not give you.
Attach keen metadata deliberately. The third argument carries timestamp, created_at, and — importantly — keen.addons for IP-to-geo enrichment. Setting an explicit timestamp server-side is essential when events are queued, otherwise you are analysing arrival time rather than occurrence time.
Use consistent, pre-aggregated event shapes. Keen's strength is ad-hoc analysis over flat JSON, so flatten aggressively: no nested objects, no polymorphic properties blob. plan, mrr_cents, currency, source as top-level scalars will save you a query rewrite later. Store money in integer minor units.
Split read and write clients. The write key cannot query and the read key cannot write. In Laravel, that usually means the write client in your service container and read/query access confined to an artisan command or a reporting service — never reachable from a controller that a user can influence. The master key belongs nowhere in application code; if you need collection management, do it from an operator-run command.
Testing. Because you bound an interface, tests get a FakeAnalyticsClient that records events into an array and asserts on them. Do not point tests at the live API; do not rely on the client's own batch state in unit tests.
The dependency chain is the real blocker. The bundle's supported client version historically pins an old Guzzle, which collides with Laravel's HTTP client — v1.8.0 does nothing to relax that pin. If composer require fails or you end up with two Guzzle majors installed, stop — the bundle is not going to work for you and you should integrate against Keen's HTTP API directly with Http::withToken() and a couple of thin methods. That is genuinely less code than untangling the transitive graph.
No auto-discovery, no php artisan vendor:publish, no config stub. Any guide you find will use Symfony YAML (keen_io: in config.yml) and $container->get('keen_io.client'). Translate mentally; do not copy service IDs.
Failures are quiet. The client's error handling swallows or throws late depending on version, and events queued but not flushed vanish without a trace. Wrap every call site so that an analytics failure is logged and reported but never propagates into a user request or a job that matters. Add a smoke test in CI that writes one event to a staging collection and asserts a 200.
Watch the flush boundary in long-running processes. Under Octane, queue workers, or Swoole, a singleton client accumulates a batch across many requests/jobs in the same process. Either flush in finally, or scope the client per-request, or you will ship one enormous batch at an arbitrary moment with stale timestamps.
Package health is the headline risk. v1.8.0 is a single commit dropping the deprecated HttpKernel\DependencyInjection\Extension base class in favour of the DI component's own extension — Symfony 7/8 compatibility housekeeping, and nothing more. It adds no service provider, no Laravel support, and no change to your integration story. Low stars, no dependents, and a Symfony2-era bundle kept on life support; the release cadence confirms maintenance rather than development. Treat it as a spec for how someone else wired Keen into a DI container, not as a dependency you adopt. The maintenance cost sits with you regardless.
Extension points, such as they are. You get the underlying client's HTTP transport and event-enrichment hooks — that is where you would inject a correlation ID, a request ID, or a user context. The bundle adds nothing beyond container wiring, so do not go looking for middleware, event subscribers, or a query builder that isn't there. The v1.8.0 refactor is internal DI plumbing and exposes no new hook.
Verify the service before you build on it. Keen IO was acquired by Scale AI and the product's roadmap has been uncertain for years. Before committing to it as your analytics store, confirm your account still works, confirm pricing and data retention for your region, and sketch the migration to a plain Postgres events table or a warehouse. If your event schema is flat JSON and your client is behind an interface, that migration is a weekend, not a quarter — which is the strongest argument for the interface-first pattern above.
How can I help you explore Laravel packages today?