slashequip/laravel-segment
slashequip/laravel-segment provides a simple Laravel integration for Segment analytics, with a facade and helpers for identifying users and tracking events server-side, plus configuration for your Segment write key and client-side snippet.
Install the package with Composer alongside your existing Laravel app:
composer require slashequip/laravel-segment
Publish the config so you can see what knobs are exposed (write key, endpoint, queueing, etc.):
php artisan vendor:publish --provider="Slashequip\LaravelSegment\SegmentServiceProvider"
Set at least your Segment write key in .env — this is the source write key, not a personal API token:
SEGMENT_WRITE_KEY=your_write_key
Then clear config if you're in a cached environment:
php artisan config:clear
Where to look first
config/segment.php — read it end-to-end. It tells you whether the package queues, how it resolves user identity, and what HTTP client/transport it uses.SegmentServiceProvider — it reveals the binding name behind the facade and any middleware or macro registrations.config/app.php. Most day-to-day usage goes through it.Your first use case
The fastest meaningful win is instrumenting a server-side track call for a critical conversion event (signup, subscription started, checkout completed). Resolve the facade and fire the event:
use Slashequip\LaravelSegment\Facades\Segment;
Segment::track('User Signed Up', [
'plan' => $user->plan,
'source' => 'web',
], [
'userId' => (string) $user->id,
]);
If the event shows up in Segment's Debugger within a few seconds, the wiring is correct and you can move on to identifying users and building reusable patterns around it.
Segment works best when identify provides context and track calls reference a userId. The least noisy approach is a small middleware that identifies the authenticated user after auth resolves:
class IdentifyWithSegment
{
public function handle($request, Closure $next)
{
if ($user = $request->user()) {
Segment::identify((string) $user->id, [
'email' => $user->email,
'name' => $user->name,
'plan' => $user->plan,
'created_at' => $user->created_at->toIso8601String(),
]);
}
return $next($request);
}
}
Register it in the web group so every authenticated request carries identity. Keep the trait payload stable — changing trait keys frequently makes downstream warehouse schemas angry.
Do not sprinkle Segment::track(...) through controllers. Introduce a thin domain service so event names and shapes live in one place:
class Analytics
{
public function __construct(private SegmentClient $segment) {}
public function orderPlaced(Order $order): void
{
$this->segment->track('Order Placed', [
'order_id' => $order->id,
'revenue' => $order->total_cents,
'currency' => $order->currency,
'product_count' => $order->items()->count(),
], ['userId' => (string) $order->user_id]);
}
}
This gives you a single file to audit for PII, a single place to add consent checks, and a trivial seam for tests.
For CRUD-heavy apps, hooking domain events to a listener is the cleanest pattern:
// app/Providers/EventServiceProvider.php
protected $listen = [
\App\Events\SubscriptionStarted::class => [
\App\Listeners\TrackSubscriptionStarted::class,
],
];
The listener calls Analytics, not Segment directly. This keeps transport (Segment) decoupled from domain events — if you ever swap to a different CDP, only the Analytics service changes.
Segment's API is remote HTTP. Even at ~100ms, it will noticeably lengthen p95 request latency under load. Always:
queue_events/queue in config/segment.php).dispatch(new TrackSegmentEvent('Order Placed', $properties, $options))
->onQueue('analytics');
Run a dedicated analytics queue worker with an aggressive concurrency setting — these jobs are I/O-bound and short.
Instead of passing ip, userAgent, locale, app_version on every call, wrap the client in a decorator that merges a default context:
class SegmentContextEnricher
{
public function track(string $event, array $props, array $options = []): void
{
$options['context'] = array_merge($options['context'] ?? [], [
'ip' => request()->ip(),
'userAgent' => request()->userAgent(),
'locale' => app()->getLocale(),
'app_version' => config('app.version'),
'environment' => app()->environment(),
]);
$this->inner->track($event, $props, $options);
}
}
Bind this in the container and inject it everywhere instead of the raw facade.
Never let tests hit Segment. Two proven approaches:
Analytics contract in the container.Segment::shouldReceive('track')->once()->with('Order Placed', Mockery::any(), Mockery::any());
Even better: assert on your Analytics contract ($analytics->assertTracked('Order Placed')) and keep Segment out of your test surface entirely.
Use the package for authoritative server events (payments, entitlement changes) and use analytics.js client-side for UI intent. Do not double-track the same event from both sides — Segment will fire two events, and your funnels will be wrong. A good rule: if the server is the source of truth for the fact, track server-side; if it's a UI interaction, track client-side.
identify() before track() matters. If you fire track without userId or anonymousId, Segment drops or fragments the event depending on your destination config. Always pass at least one identity key in the options array.track calls will show up in your APM traces as outbound HTTP. If you see them adding latency, that's a config flag, not a code change — check config/segment.php for a queue/batch option first.$user->hasAnalyticsConsent() or a setting), and be strict about never sending email/phone in track properties — those belong in identify traits.php artisan config:cache almost certainly ran and cached a stale/missing SEGMENT_WRITE_KEY. Add the env var to your deploy checklist.SEGMENT_ENABLED=false (or the package's equivalent) in .env.testing and .env.local. Don't rely on your dev write key pointing to a sandbox — someone always forgets.Title Case vs snake_case on day one and enforce it in your Analytics service. Segment allows both, but your dashboards won't thank you for the mixture.segmentio/analytics-php client or the transport binding — not wrapping each call in try/catch. Bind your configured client in a service provider rather than forking the package.identify before an track, either batch them or include the relevant traits in every track call's traits. Don't assume the middleware ran first for background jobs.How can I help you explore Laravel packages today?