alt-three/segment
Laravel integration for Segment analytics that makes it simple to identify users and track events from your application. Includes a service provider, facade, and configuration for sending server-side analytics data to Segment.
1. Install and check constraints first. The package is archived (last release July 2020), so treat installation as a compatibility exercise:
composer require alt-three/segment
It pulls in Segment's server-side PHP library and declares Laravel/PHP constraints from the 5.x–7.x era. On modern PHP (8.1+) and Laravel 9/10/11 you will likely need --ignore-platform-reqs or a fork. If Composer resolves cleanly, run the test suite immediately — deprecation notices from the vendored analytics library are the first thing you'll hit.
2. Wire up the provider. On Laravel 5.5+ the service provider is auto-discovered via extra.laravel.providers. On older versions add to config/app.php:
'providers' => [
AltThree\Segment\SegmentServiceProvider::class,
],
'aliases' => [
'Segment' => AltThree\Segment\Facades\Segment::class,
],
3. Publish the config and set the write key.
php artisan vendor:publish --provider="AltThree\Segment\SegmentServiceProvider"
This creates config/segment.php. Put the key in .env — never in the config file:
SEGMENT_WRITE_KEY=xxxxxxxxxxxxxxxxxxxxxxxx
Review the consumer and debug keys in that file before going further (see Gotchas).
4. First use case — identify on login, track on the action.
use AltThree\Segment\Facades\Segment;
// After authentication
Segment::identify($user->id, [
'email' => $user->email,
'name' => $user->name,
'created_at' => $user->created_at->toIso8601String(),
]);
// Anywhere in the request lifecycle
Segment::track($user->id, 'Order Placed', [
'order_id' => $order->id,
'revenue' => $order->total_cents / 100,
'currency' => 'USD',
]);
Where to look first: config/segment.php for the consumer/debug knobs, then vendor/alt-three/segment/src/ — the wrapper is thin, so reading the two or three classes gives you the full API surface (identify, track, page, screen, group, alias, flush).
Wrap the facade behind your own interface. Given the package is unmaintained with zero dependents, do not let Segment::track() calls spread across controllers. Define a small contract and bind it:
interface Analytics
{
public function identify(User $user, array $traits = []): void;
public function track(?string $userId, string $event, array $props = [], array $options = []): void;
public function flush(): void;
}
final class SegmentAnalytics implements Analytics
{
public function track(?string $userId, string $event, array $props = [], array $options = []): void
{
Segment::track($userId, $event, $props, $options + ['anonymousId' => request()->cookie('ajs_anonymous_id')]);
}
}
This gives you one swap point when the package dies (or when you move to segmentio/analytics-php v2 or direct Http::post('https://api.segment.io/v1/track')) and a trivially fakeable dependency in tests.
Drive it from domain events, not controllers. Business events (UserRegistered, SubscriptionCancelled, OrderShipped) already exist in most Laravel apps. Add listeners that translate them into Segment calls. You get a clean tracking plan, and you can queue the listener so analytics never blocks a checkout:
class TrackOrderPlaced implements ShouldQueue
{
public $afterCommit = true; // don't emit for a transaction that rolled back
public function handle(OrderPlaced $event): void
{
app(Analytics::class)->track((string) $event->order->user_id, 'Order Placed', [...]);
}
}
afterCommit (or DB::afterCommit(fn () => ...)) is the single most important detail here — firing a Segment call inside a transaction produces events for orders that were later rolled back.
Build context once, in middleware. ip, userAgent, locale, and anonymousId are per-request and easy to forget. Resolve them in a middleware and stash them in a request-scoped context object your Analytics implementation merges into $options:
Segment::track($userId, 'Report Exported', $props, [
'context' => [
'ip' => request()->ip(),
'userAgent' => request()->userAgent(),
'locale' => app()->getLocale(),
],
]);
Handle the anonymous → identified transition explicitly. Segment's PHP library does not do this for you. On registration, read ajs_anonymous_id from the cookie, identify() with it, then alias($userId, $anonymousId) so pre-signup activity stitches to the new user:
Segment::identify($anonymousId, [], ['anonymousId' => $anonymousId]);
Segment::alias($user->id, $anonymousId);
Use page() server-side for non-browser clients. Invoices, emails, webhooks, and mobile backends have no analytics.js. A server-side page($userId, 'Invoice View', $props) keeps funnels complete across surfaces.
Flush on long-running processes. Queue workers and Laravel Octane hold the client alive for hours. Register a flush() on the worker's Looping/terminating hook, or every N events, otherwise buffered messages are lost on a hard restart.
Testing. Because it's a facade, Mockery fakes work directly:
Segment::shouldReceive('track')
->once()
->with($user->id, 'Order Placed', Mockery::on(fn ($p) => $p['order_id'] === $order->id));
$this->post('/orders', [...])->assertOk();
For listener tests, assert on your Analytics interface instead — it survives a package swap.
Analytics interface is the cheap insurance.ext-pcntl. Under php-fpm, Octane, or inside a queue worker, pcntl_fork is either unavailable or leaks state. Prefer the curl consumer (simple, blocking, works everywhere) or file with an out-of-band flush. Set this in config/segment.php and verify in staging, not production.debug locally, and in production monitor for absence of events rather than presence of errors.config:cache freezes the write key. php artisan config:cache during deploy bakes whatever SEGMENT_WRITE_KEY was present at cache time. If you cache before injecting env vars (common with Docker entrypoints), you ship a null key. Re-cache after env injection, and never call env() outside config files.$request->all() or $order->toArray() will blow past Segment's ~32KB per-message limit and leak PII into your warehouse. Explicitly whitelist the handful of properties the tracking plan specifies.'Order Placed' vs 'Order placed' vs 'order_placed' are three different events. Centralize names as constants in your Analytics implementation or an enum.analytics.js. If the browser SDK is also loaded, server-side calls duplicate page/track. Use the $options integrations block to suppress specific downstream tools (['integrations' => ['Mixpanel' => false]]), or decide firmly which events are server-authoritative.['timestamp' => $occurredAt->toIso8601String()] in $options; otherwise the event is stamped when the queue drains, which wrecks cohort queries.context.consent field) so a single policy change doesn't require hunting down every call site.consumer/debug/write_key, and the facade accessor (override for a test double). Everything else lives in the vendored analytics library — patch there only via a fork you control.How can I help you explore Laravel packages today?