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

Laravel Posthog Laravel Package

qodenl/laravel-posthog

A Laravel package for integrating PostHog analytics into your app. Track events, identify users, and send data to PostHog through a simple, configurable API, making product analytics and feature flag workflows easier to add to Laravel projects.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and publish its config:

composer require qodenl/laravel-posthog
php artisan vendor:publish --tag=posthog-config

Add the minimum to .env:

POSTHOG_API_KEY=phc_...
POSTHOG_HOST=https://eu.i.posthog.com

The config file (config/posthog.php) is the first place to look — nearly every behaviour in this release (release ID, error tracking, IP capture, feature flags) is a config key with a matching env var. The Posthog facade is the day-to-day entry point.

A good first use case is capturing a server-side event and reading a feature flag in the same request:

use Posthog\Laravel\Facades\Posthog;

Posthog::capture('order_placed', [
    'distinct_id' => (string) $user->id,
    'properties'  => ['total' => $order->total, 'currency' => $order->currency],
]);

$variant = Posthog::getFeatureFlag('checkout-redesign', (string) $user->id, ['plan' => $user->plan]);

If you're upgrading from 1.4.x, check your Laravel version first: 1.5.0 removes Laravel 11 support and requires Laravel 12 or 13.

Implementation Patterns

Identify once per request, capture many times. Resolve the distinct ID in a middleware or a base controller and let callers pass it explicitly. The package will fall back to the authenticated user, but being explicit is what makes queued jobs and console commands behave predictably.

Release ID as a deploy marker. Set POSTHOG_RELEASE_ID to your git SHA in CI and it is attached to every event, including $feature_flag_called. This is the cheap way to correlate a regression with a deploy without touching event call sites. Because the package reads it from config rather than from the process environment, it survives config:cache and PHP-FPM's default clear_env = no — which is the main reason to use it over the SDK's built-in POSTHOG_RELEASE_ID.

Feature flags with payloads. The new getFeatureFlagPayload() reads from the same cached evaluation as getFeatureFlag(), so the idiomatic pattern is to resolve the flag and its payload together and avoid a second /flags round trip:

$distinctId = (string) $user->id;
$variant    = Posthog::getFeatureFlag('checkout-redesign', $distinctId);
$config     = Posthog::getFeatureFlagPayload('checkout-redesign', $distinctId);

// e.g. $config = ['headline' => 'Buy now', 'cta_color' => '#0af']

Treat the payload as untrusted, nullable config — always have a default variant path.

Error tracking. Enable it globally with POSTHOG_ERROR_TRACKING_ENABLED=true and every exception reaching Laravel's handler is reported with the current distinct ID and groups. For targeted reporting, call it yourself:

try {
    $this->charge($order);
} catch (PaymentException $e) {
    Posthog::captureException($e, ['distinct_id' => (string) $user->id]);
    throw $e;
}

Only enable the global handler once you've reviewed $dontReport in bootstrap/app.php — exceptions Laravel doesn't report (validation, 404s, auth) are skipped automatically, but anything you'd normally log is now also going to PostHog.

IP capture and GeoIP. Set POSTHOG_CAPTURE_IP=true so GeoIP resolves to the visitor rather than the queue worker's host. The IP is taken from request()->ip() at dispatch time, which is the right behaviour for queued events but means console commands and jobs have no IP. Behind Cloudflare or a load balancer, register a resolver in a service provider:

Posthog::resolveIpUsing(fn () => request()->header('CF-Connecting-IP'));

Groups. Pass groups on capture/identify when you care about organisation-level analytics; captureException() picks up the current groups, so the same context flows into error tracking.

Integration tips. Prefer the facade over injecting the SDK client directly; it keeps config-driven behaviour (release ID, IP, error tracking) consistent. In tests, swap the underlying client or assert on Posthog::capture() calls rather than hitting the flags endpoint.

Gotchas and Tips

  • Laravel 11 is gone. 1.5.0 requires Laravel 12 or 13. If you're still on 11, pin 1.4.* — Laravel 11 no longer receives security fixes, so the package's position is reasonable but will block upgrades.
  • Explicit properties always win. An explicit $release_id or $ip in event properties overrides the config-driven value. This is useful for backfills and for replaying events, but it also means a stray $ip in a properties array silently defeats POSTHOG_CAPTURE_IP.
  • IP is dispatch-time, not send-time. Queued events capture request()->ip() when the job is dispatched. If you dispatch from a console command or a job that was itself queued, there is no request and no IP — don't expect GeoIP to work there.
  • Payloads need an evaluation. getFeatureFlagPayload() reads the cached evaluation, so call getFeatureFlag() first (or accept that the payload call triggers the evaluation). Don't assume payloads are available for flags you never resolved.
  • Anonymous exceptions. When no distinct ID can be resolved, captureException() sends anonymously rather than dropping the event. That's usually what you want, but it means errors from unauthenticated paths show up as anonymous persons in PostHog — filter by $exception properties rather than by person.
  • Global error tracking multiplies volume. Turning on POSTHOG_ERROR_TRACKING_ENABLED sends every reported exception. Rate-limit noisy exceptions or narrow $dontReport before enabling in production; otherwise a single failing dependency can dominate your event quota.
  • config:cache is fine, but rebuild on deploy. The release ID and IP settings are read from config, so run php artisan config:cache as part of the deploy after setting POSTHOG_RELEASE_ID, or every event will carry the previous release's ID.
  • Local evaluation parity. The posthog-php 4.14 bump makes local evaluation match the flags service more closely and resolves payloads locally. If you rely on local evaluation, re-test flag payloads after upgrading — the values should now match what the /flags endpoint returns, which may change behaviour you previously worked around.
  • Debugging. Use Posthog::capture() with a known distinct ID and check the /flags response in PostHog's activity view; most "flag not resolving" issues are a missing or mismatched distinct ID rather than a package problem.
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.
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator