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

jeffersongoncalves/laravel-posthog

A Laravel integration for PostHog that makes it easy to send analytics events, identify users, and track feature usage from your PHP application through a simple, configurable package API.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install and wire up two credentials, not one. The package deliberately mirrors PostHog's split auth model, and almost every setup failure comes from mixing them up:

    POSTHOG_API_KEY=phc_...           # project key — ingestion and flag evaluation payloads
    POSTHOG_PERSONAL_API_KEY=phx_...  # personal key — Bearer token for the project API
    POSTHOG_HOST=https://us.i.posthog.com
    

    Use the EU host (https://eu.i.posthog.com) if your project lives there; a wrong host looks like an auth failure.

  2. Bind the client as a singleton in a service provider (or rely on the package's auto-discovered provider if it ships one). Read config once, not per call:

    $this->app->singleton(PostHog::class, fn () => new PostHog(
        apiKey: config('services.posthog.key'),
        personalApiKey: config('services.posthog.personal_key'),
        host: config('services.posthog.host'),
    ));
    

    Check the actual factory/constructor signature in the README — this is the one place the release notes don't spell out.

  3. First use case: capture a real business event and read a flag in the same request path. After registration, identify the user, then check a multivariate onboarding flag to pick a flow. That exercises ingestion, identity, and flag evaluation in one go and proves both keys are correct.

  4. Where to look first: the client's public methods map almost 1:1 to the release notes (capture, identify, alias, getAllFlags, getFeatureFlag, isFeatureEnabled, person lookup, query/insights/recordings). Then read PostHogException — it carries the raw API error body, which is your primary debugging tool for 4xx responses.

Implementation Patterns

  • Never capture inline in a hot request path. Dispatch a queued job or listener instead; analytics latency and outages must not affect checkout or login.

    final class CapturePostHogEvent implements ShouldQueue
    {
        public function __construct(public array $payload) {}
    
        public function handle(PostHog $posthog): void
        {
            $posthog->capture($this->payload);
        }
    }
    

    Use the batch ingestion path for backfills and imports (historical orders, migrated users) rather than looping single captures.

  • Fetch flags once per request, then evaluate locally. getAllFlags($distinctId, $groups) is the workhorse: one round trip, then read variants out of the array in middleware, policies, and Blade/Inertia props.

    $flags = Cache::remember("posthog:flags:{$user->id}", 60, fn () =>
        $posthog->getAllFlags((string) $user->id, ['company' => (string) $user->company_id])
    );
    
    $variant = $flags['new-checkout'] ?? 'control';
    

    Expose them to the frontend as a single shared prop instead of letting JS call PostHog separately.

  • Keep distinct_id stable and non-PII. Use your database user ID (cast to string), not email. Generate an anonymous ID for guests, then alias on signup so pre-login behaviour stitches onto the real person. Call identify on registration and on profile changes worth syncing.

  • Pass groups consistently everywhere. If you send ['company' => $id] on capture but omit it on flag calls, group-targeted flags silently won't match. Centralise this in a small PostHogContext helper that derives distinct_id and groups from the authenticated user.

  • Use the project API surface for internal tooling, not user-facing code. Person lookup by distinct_id belongs in an admin/support screen; HogQL queries and insights fit scheduled Artisan commands (daily metrics to Slack, cohort exports); session recording listings fit an internal QA dashboard. All of these need the personal API key, so they must stay server-side.

  • Wrap the client in a thin decorator that catches PostHogException, logs the body at warning level, and returns safe defaults (e.g. false for flag checks). Bind the decorator in the container so call sites stay clean and tests can swap in a fake.

  • Test with a fake, never the network. Bind an in-memory implementation in tests/TestCase.php and assert on captured payloads; add one optional integration test gated behind an env flag for the real client.

Gotchas and Tips

  • Two keys, two failure modes. Ingestion and (typically) /decide-style flag calls use the project key in the payload; everything under the project API needs Authorization: Bearer <personal key>. A 401 on a flag check usually means the method is hitting the project API, not that your key is wrong. Verify per-method which credential is required before debugging blind.
  • The personal API key is a broad-scope secret. Keep it in the server environment only, rotate it, and prefer scoped keys where PostHog offers them. Never let it reach a browser bundle or a mobile app.
  • Don't assume sessions are recorded. The SDK exposes session recording retrieval; actual recording is done by the frontend snippet. Plan accordingly if you're building a "watch replay" link in admin.
  • Multivariate vs boolean. getFeatureFlag returns a string variant (and null/false when absent or disabled), while isFeatureEnabled collapses to a boolean. Decide once which one your codebase uses per flag and don't mix — "false"-as-string and missing-flag cases are the classic bug.
  • Flags are network calls until proven otherwise. Unless you've explicitly enabled local evaluation, each flag check costs a request. Cache aggressively, always supply a fallback default, and consider a hard kill switch so a PostHog outage downgrades everyone to control.
  • Handle 429 and 5xx with backoff. Because PostHogException preserves the response body, you can distinguish rate limits from bad payloads. Retry only the transient class; drop and log the rest rather than blocking a queue worker.
  • Flush semantics matter in long-running processes. Batch ingestion buffers events; under Octane or queue workers, register a shutdown/termination flush and avoid holding client state across requests. In standard PHP-FPM it's largely handled for you.
  • Region and trailing slashes. A mismatched host (US key to EU host, or vice versa) surfaces as an auth error, not a network error. Normalise the host in config.
  • php artisan config:cache will freeze your keys — remember to re-cache after changing env vars in deploy pipelines.
  • PII and consent. Filter properties before capture (no emails, names, or free-text in event payloads), and honour opt-outs at the decorator layer so a single check disables all outbound calls.
  • First release, so pin it. ^1.0 is fine, but read the changelog on every minor bump and keep your decorator as the single integration seam — that's where upstream API changes will land.
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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin