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

Posthog Php Laravel Package

posthog/posthog-php

PostHog PHP SDK for server-side product analytics: capture events, identify users, group accounts, evaluate feature flags, and send data to PostHog from Laravel or plain PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

  • composer require posthog/posthog-php, then grab your project API key (phc_...) from PostHog → Project Settings. Note your region host — https://us.i.posthog.com or https://eu.i.posthog.com. The default is not always what you want.
  • Initialize once per process, normally from a PostHogServiceProvider (or a small singleton in App\Support\Analytics). Because the SDK is static, keep initialization in one place and out of config:cache bootstrapping logic:
// config/services.php
'posthog' => [
    'key'  => env('POSTHOG_API_KEY'),
    'host' => env('POSTHOG_HOST', 'https://us.i.posthog.com'),
],

// PostHogServiceProvider::register()
PostHog::init(config('services.posthog.key'), [
    'host'     => config('services.posthog.host'),
    'consumer' => 'lib-curl',
    'debug'    => config('app.debug'),
    'error_handler' => fn ($e) => report($e), // analytics must never break checkout
]);
  • First real use case: a server-side event on signup, where the server is the source of truth:
PostHog::capture([
    'distinctId' => (string) $user->id,
    'event'      => 'user_signed_up',
    'properties' => [
        'plan'   => $user->plan,
        '$set'   => ['email' => $user->email, 'created_at' => $user->created_at->toIso8601String()],
    ],
]);
PostHog::flush();
  • Where to look first: the README's usage block (it's essentially the whole API surface), the PostHog class for method signatures, the Consumer/* classes to understand delivery, and the config array in PostHog::init. There is no deep API to learn — capture, identify, alias, groupIdentify, isFeatureEnabled/getFeatureFlag/getAllFlags, flush, shutdown.
  • Verify it works: PostHog::flush(), then check Activity → Live events in the PostHog UI filtered by your event name. Don't trust a green test suite for this.

Implementation Patterns

Wrap it, don't sprinkle it. Put a thin Analytics class (or contract) in front of PostHog::* so you have one place to resolve the current distinctId, tenant group, and to no-op in tests. Calling the static class from 40 controllers is the fastest way to an unmaintainable integration.

Identity lifecycle.

  • Use your app's user primary key as distinctId (cast to string — integers work but string is the safe contract).
  • Call identify() on profile changes, not on every request; use $set for mutable attributes and $set_once for first-touch values.
  • For anonymous visitors: generate a UUID, store it in session/attribute, capture events with it, then PostHog::alias(['distinctId' => $userId, 'alias' => $anonId]) at login and switch to the user ID going forward.

Groups for multi-tenant apps. Do groupIdentify() when a tenant is created/updated, and then attach 'groups' => ['company' => $tenant->id] to every capture. Doing groups half-way (some events grouped, some not) produces dashboards that undercount.

Feature flags.

  • PostHog::isFeatureEnabled('new-checkout', (string) $user->id) is a network call by default. Don't put it inside a loop or a Blade template.
  • If flags are on a hot path, configure local evaluation: pass personalApiKey (the personal key, not the project key), the matching host, and featureFlagsPollingInterval. Then pass personProperties in the options so conditions that reference person props actually match.
  • Call getAllFlags($distinctId, ['personProperties' => [...]]) once per request and resolve flags from the returned array.
  • Use 'onlyEvaluateLocally' => true when you need to know a flag wasn't resolvable — otherwise the SDK silently falls back to a network evaluation.

Batching and lifecycle. The SDK queues events and flushes on a timer/destructor. In long-lived processes (queue workers, Octane, Horizon) flush explicitly:

  • HTTP: a terminable middleware that calls PostHog::flush() after the response is sent.
  • Jobs: PostHog::flush() in a finally block, or dispatch a small "flush" job at the end of a batch.

Non-critical work off the hot path. Emit analytics from queued event/listener classes (UserSignedUp → SendSignupAnalytics) rather than inline in the request. This also gives you retries for free and keeps p95 latency clean.

Testing. Point the consumer at file (or your own fake consumer), run the flow, then read the JSON lines and assert on event/distinctId/properties. Always flush() in tearDown(), and be aware that the static client persists across tests in the same process.

Gotchas and Tips

  • The static singleton is global state. PostHog::init() can't be called twice cleanly. Guard it with a hasInitialized check, and call PostHog::shutdown() between tests or in long-running processes where you need a fresh client. Test-suite cross-contamination ("events attributed to the wrong user") almost always traces back to this.
  • Events can be lost at process exit. The shutdown flush is a destructor — exit/die, fatal errors, uncaught signals, and pcntl_fork can drop the queue. If an event matters (e.g. subscription_started), flush() explicitly. If it doesn't, let it batch.
  • flush() blocks. Never call it per event in a web request. One flush in a terminable middleware or job finally is the pattern.
  • Consumer choice matters. lib-curl is the sane default. fork-curl requires pcntl and commonly isn't available in shared/FPM setups; file and socket are debugging aids, not production transports. Misconfigured consumers fail quietly, which is why 'debug' => true in local is worth it.
  • Wrong host = silent data loss (or wrong region). EU projects sending to the US host will either 404 or land in the wrong project. This is the single most common "the SDK is broken" report.
  • Property naming is camelCase and Segment-flavoured. distinctId, not distinct_id; $set/$set_once for person properties; anything else in properties is an event property. PostHog won't error — it just won't do what you meant.
  • Null vs false. isFeatureEnabled() returns false for both "flag is off" and "flag not found / evaluation failed". If that distinction matters, use getFeatureFlag() and treat null as unknown.
  • Local evaluation is strict. Missing personalApiKey, wrong host, stale polling interval, or missing personProperties all cause flags to fall back or evaluate incorrectly — and it looks like a flag-logic bug. Start with server-side evaluation on, confirm the flag, then move to local.
  • Never send PII or secrets. Passwords, tokens, raw request bodies, and card data will be ingested and visible in the UI forever. Add a beforeSend-style sanitizer in your Analytics wrapper if you can't trust callers.
  • Memory in batch jobs. A loop emitting 100k events with a large max_queue_size will balloon worker memory. Lower batch_size / flush periodically inside the loop.
  • Octane/Horizon persistence. The client and its queue survive between requests. Re-check that the distinctId you resolve per request isn't cached in the container, and flush between requests.
  • Extension points worth knowing: error_handler (log, never throw), timeout, ssl, and passing a pre-built Consumer instance to init — the cleanest way to inject a fake consumer in tests or route to a proxy.
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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
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
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core