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

Bitter Laravel Package

free-agent/bitter

Bitter is a lightweight feature flag package for Laravel by FreeAgent. It lets you define and check feature flags in your application so you can safely enable or disable functionality without redeploying, with a simple API and Blade integration.

View on GitHub
Deep Wiki
Context7

Getting Started

Bitter is a thin analytics layer over a key/value store — you hit() a named event, and it increments a counter (optionally inside a time bucket) or adds a value to a unique set. There is no schema, no migrations, no dashboard; it is a few hundred lines of PHP around Redis (or another backend). Treat it as a counting primitive, not a product.

Install and pin. The package is archived, so Composer will resolve whatever the last tag was and never move again:

composer require free-agent/bitter

Because it is unmaintained, commit the lockfile entry and be prepared to path-repo or fork it if you need a PHP 8 fix. Check composer.json for the PHP constraint before assuming it runs on your app's runtime.

Pick your backend first. Bitter is constructor-injected with a client — typically a Predis\Client (or a phpredis \Redis connection through the matching adapter). The adapter is the only real configuration decision:

use Bitter\Bitter;
use Predis\Client;

$bitter = new Bitter(new Client([
    'scheme' => 'tcp',
    'host'   => env('REDIS_HOST', '127.0.0.1'),
    'port'   => 6379,
]));

Where to look in the source. Bitter.php is the entire public surface — hit(), set(), get(), and the BUCKET_* constants. Read it before the README; the README is short and the class is the real contract. Then read whichever adapter you use, because that is where key naming and TTL behaviour live.

First use case. A signup/view counter is the smallest thing that demonstrates value:

// in a controller / listener
$bitter->hit('user.registered', 1, Bitter::BUCKET_DAY);

// later, on a dashboard
$today = $bitter->get('user.registered', Bitter::BUCKET_DAY);

If that round-trips, you understand the library. Everything else is naming discipline.

Bind it once. Don't new Bitter(...) in controllers. Register it in a service provider and inject it:

// app/Providers/AnalyticsServiceProvider.php
public function register(): void
{
    $this->app->singleton(Bitter::class, function () {
        return new Bitter(new \Predis\Client(config('database.redis.default')));
    });
}

Implementation Patterns

Dotted, verb-first event keys. Bitter has no namespace concept — the string is the namespace. Adopt domain.action (post.read, order.paid, user.registered) and enforce it, because Redis keys are global and a typo silently creates a new metric that no query will ever find.

Two counters per feature, not one. Keep a lifetime counter (hit('post.read') with no bucket) and a bucketed one (hit('post.read', 1, Bitter::BUCKET_DAY)). The lifetime number is cheap to read for "total ever"; the bucketed one is what you actually chart. Don't try to reconstruct the total by summing buckets.

Buckets are a write-time decision. Bitter's bucket constants (BUCKET_HOUR, BUCKET_DAY, BUCKET_WEEK, BUCKET_MONTH, BUCKET_YEAR) fan a single hit() out into per-period keys. Pick at most two per metric — hour for operational dashboards, day for product reporting. Recording all five is how you turn a 50-byte metric into five keys that nobody queries.

Uniques via set(). When "how many" is really "how many distinct", use the set path instead of hit():

$bitter->set('post.read.unique', $postId, Bitter::BUCKET_DAY);

This is the pattern for unique visitors, distinct buyers, DAU. It is not idempotent-per-user across buckets — see gotchas.

Wrap it, don't sprinkle it. Put a small Analytics facade over Bitter so the rest of the app never sees the library:

class Analytics
{
    public function __construct(private Bitter $bitter) {}

    public function track(string $event, int $value = 1): void
    {
        $this->bitter->hit($event, $value, Bitter::BUCKET_DAY);
    }

    public function count(string $event): mixed
    {
        return $this->bitter->get($event, Bitter::BUCKET_DAY);
    }
}

This is the single highest-leverage thing you can do with an abandoned dependency: it gives you one file to change when you eventually swap Bitter for a maintained counter, and it gives you a place to add a prefix, a try/catch, and a feature flag.

Fire it from domain events, not controllers. Laravel event listeners are the natural seam — OrderPaid → Analytics::track('order.paid'). Controllers get reused (API, webhook, console), so counting there double-counts. Listeners also run in queues, which lets you defer the Redis round-trip.

Multi-tenant prefixing. Bitter doesn't know about tenants. Prefix at the wrapper level rather than trusting every call site:

$this->bitter->hit("tenant.{$tenantId}.{$event}", $value, Bitter::BUCKET_DAY);

Query cheaply, cache aggressively. get() is a Redis read; a dashboard pulling twenty metrics per page load is twenty round-trips. Cache the rendered numbers for 1–5 minutes — analytics tolerates staleness far better than your page load does.

Backfill and replay. Because keys are just strings, you can seed history by writing keys directly (SET with the computed bucket suffix) if you ever need to migrate from another system. That's a real advantage over hosted analytics, and worth exploiting during a migration.

Gotchas and Tips

It is archived — budget for it. No security patches, no PHP 8.x compatibility guarantee, zero dependents means zero community pressure to fix anything. Pin the version, wrap it, and assume you will replace it. Don't build a billing-critical metric on it.

Unbounded set growth is the real operational risk. set() keys with no expiry will grow forever on high-cardinality values (user IDs, session IDs, request IDs). Set a TTL on the underlying keys at the Redis level — either in the adapter/config or with a scheduled EXPIRE sweep — or your Redis memory graphs will become the incident.

Redis is not durable by default. If you're running the default RDB snapshot config, a crash loses recent counts. For counters this is usually acceptable; know that it is happening rather than discovering it.

Read-after-write is not guaranteed to be what you expect. If you write counters from a queued listener and read them on the next page load, you will occasionally see stale zeroes. Either write synchronously for metrics the UI reads immediately, or accept the lag explicitly.

Bucket keys multiply silently. Every hit() with a bucket writes a new key per period. A metric hit hourly across 5 bucket types is 5 keys per write, and hourly buckets across a year is 8,760 keys per metric. Enumerate your metrics × buckets on paper before shipping.

Key collisions with the rest of your Redis instance. Bitter writes into the same db as your cache and sessions unless you separate them. Give it its own database index or a dedicated Redis instance — FLUSHDB during debugging and a shared keyspace is a bad combination.

No Laravel auto-discovery. There's no service provider and no facade in the package. Bind it manually, and register the binding as a singleton so you don't spawn a new Redis connection per injection.

Predis vs phpredis matters. The adapter you choose must match the client object you pass to the constructor, and old Predis 0.x/1.x clients are not interchangeable with Predis 2.x. If you get a "call to undefined method" on construction, it's the client, not Bitter.

Testing. Don't mock the adapter — run against a real Redis database index in tests (db => 15) and flush it in setUp. The library has no in-memory backend, and faking it gives you false confidence about key names, which is the thing that actually breaks.

Debug by looking at raw keys. When a metric reads zero, redis-cli KEYS '*your.event*' tells you instantly whether the write path used a different key, a different bucket, or a different database. This is faster than any instrumentation.

Extension points are thin but real. The adapter is the seam: implement the same interface against a different store if you need to move off Redis, and the Bitter class itself is small enough to fork and keep the API. If you fork, the one change worth making first is a configurable key prefix — it solves the collision and multi-tenant problems in one place.

Verify the return type of get() before building on it. The reader in this library is terse; confirm whether you get a scalar or a metric object, and normalise it in your wrapper so the rest of the codebase never has to care.

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