wi3edigital/laravel-posthog-tracker (1.1.0)
Installation
{
"repositories": [{
"type": "composer",
"url": " "
}
]
}composer require wi3edigital/laravel-posthog-tracker:1.1.0About this package
Laravel PostHog Tracker
PostHog analytics for Laravel. Server-side event capture with optional JS client-side proxy support.
Requirements
- PHP 8.4+
- Laravel 12+
Installation
composer require wi3edigital/laravel-posthog-tracker
Package auto-discovers via Laravel's package discovery.
Configuration
Publish the config file:
php artisan vendor:publish --tag=posthog-config
Add to .env:
POSTHOG_API_KEY=phc_your_key_here
POSTHOG_HOST=https://us.i.posthog.com
Config reference
// config/posthog.php
'api_key' => env('POSTHOG_API_KEY'),
'host' => env('POSTHOG_HOST', 'https://us.i.posthog.com'),
// null = auto (only in production when api_key is set)
// true = always enabled (when api_key is set)
// false = always disabled
'enabled' => env('POSTHOG_ENABLED'),
// Tracking only fires once the visitor has granted consent (e.g. via a
// cookie banner). Consent is read from a plain-text cookie, so it must be
// excluded from Laravel's cookie encryption (`encryptCookies(except: [...])`
// in bootstrap/app.php) — both client-side JS and PHP need to read the same
// raw value.
'consent' => [
'required' => env('POSTHOG_CONSENT_REQUIRED', true), // false = skip the consent check entirely
'cookie' => env('POSTHOG_CONSENT_COOKIE', 'posthog_consent'),
'granted_value' => 'all',
],
'route' => [
'prefix' => env('POSTHOG_ROUTE_PREFIX', 'analytics'),
'middleware' => ['web'],
'name' => 'analytics.capture',
],
Consent
capture() and identify() no-op until the visitor has consented — checked live per call via PosthogTracker::hasConsent(), which reads the posthog.consent.cookie cookie for the granted_value. The host application is responsible for the consent UI (e.g. a cookie banner) that sets this cookie, and for excluding it from cookie encryption so client-side JS can read/write it too.
Set posthog.consent.required to false to skip the check entirely (e.g. for internal/staging environments).
Usage
Capture events
use Wi3eDigital\PosthogTracker\Facades\PosthogTracker;
// Authenticated user (auto-resolved from Auth::user() if null)
PosthogTracker::capture(user: $user, event: 'order.placed', properties: [
'order_id' => $order->id,
'total' => $order->total,
]);
// Anonymous / guest (uses session ID as distinct ID)
PosthogTracker::capture(user: null, event: 'page.viewed');
// Custom distinct ID
PosthogTracker::capture(user: null, event: 'signup.started', options: [
'distinct_id' => 'temp-uuid-or-token',
]);
// Group analytics
PosthogTracker::capture(user: $user, event: 'invoice.sent', options: [
'groups' => ['company' => $company->id],
]);
Events are dispatched via defer() — no blocking HTTP calls during the request.
Identify users
PosthogTracker::identify(user: $user, traits: [
'name' => $user->name,
'email' => $user->email,
'plan' => $user->plan,
]);
Custom properties / traits resolvers
Register in a ServiceProvider::boot() to automatically attach properties to every event:
use Wi3eDigital\PosthogTracker\PosthogTracker;
// Extra properties on every capture
PosthogTracker::resolvePropertiesUsing(function ($user) {
return [
'tenant_id' => $user->tenant_id,
'plan' => $user->plan,
];
});
// Default traits on every identify
PosthogTracker::resolveTraitsUsing(function ($user) {
return [
'name' => $user->name,
'email' => $user->email,
];
});
Default properties included on every event
| Property | Value |
|---|---|
user_id |
Authenticated user's ID |
environment |
app()->environment() |
app_version |
config('app.version') |
request_path |
Current request path |
user_agent_hash |
SHA-256 hash of User-Agent |
JavaScript setup
Client-side PostHog (optional)
Publish the JS stubs:
php artisan vendor:publish --tag=posthog-stubs
This publishes two files to resources/js/:
| File | Purpose |
|---|---|
posthog.js |
Initialises PostHog JS SDK (production only) |
analytics-tracker.js |
DOM attribute tracker + server-side proxy |
Add to .env:
VITE_POSTHOG_API_KEY=phc_your_key_here
VITE_POSTHOG_HOST=https://us.i.posthog.com
VITE_POSTHOG_UI_HOST=https://us.posthog.com
VITE_APP_ENV=production
Import in your JS entry point:
import './posthog.js';
import './analytics-tracker.js';
Server-side capture route
The package registers POST /analytics/capture (name: analytics.capture). The JS tracker proxies client-side events through this route so your PostHog API key stays server-side.
Add to your Blade layout to support a custom route prefix:
<meta name="posthog-capture-url" content="{{ route('analytics.capture') }}">
HTML attribute tracking
Capture events declaratively in HTML without writing JS:
<!-- Simple event -->
<button data-analytics-event="cta.clicked">Sign up</button>
<!-- With properties -->
<button
data-analytics-event="plan.selected"
data-analytics-properties='{"plan":"pro","billing":"monthly"}'
>
Choose Pro
</button>
<!-- With custom distinct ID -->
<button
data-analytics-event="share.clicked"
data-analytics-distinct-id="{{ $shareToken }}"
>
Share
</button>
JS API
window.analyticsTracker.capture('video.played', { video_id: 123 });
Testing
use Wi3eDigital\PosthogTracker\Facades\PosthogTracker;
$fake = PosthogTracker::fake();
// ... trigger code that calls PosthogTracker::capture()
$fake->assertCaptured('order.placed');
$fake->assertCaptured('order.placed', function (array $payload): bool {
return $payload['properties']['order_id'] === 42;
});
$fake->assertNotCaptured('payment.failed');
$fake->assertNothingCaptured();
$fake->assertIdentified($user);
$fake->assertIdentified($user, function (array $payload): bool {
return $payload['traits']['plan'] === 'pro';
});
License
MIT
Dependencies
Dependencies
| ID | Version |
|---|---|
| illuminate/contracts | ^12.0 |
| illuminate/http | ^12.0 |
| illuminate/support | ^12.0 |
| php | ^8.4 |
| posthog/posthog-php | ^4.0 |
Development dependencies
| ID | Version |
|---|---|
| larastan/larastan | ^3.0 |
| laravel/pint | ^1.0 |
| orchestra/testbench | ^10.0 |
| pestphp/pest | ^4.0 |
| pestphp/pest-plugin-laravel | ^4.0 |