Skip to content

feat: add a runnable Laravel SDK example - #286

Merged
marandaneto merged 70 commits into
mainfrom
feat/laravel-example
Nov 3, 2026
Merged

marandaneto merged 70 commits into
mainfrom
feat/laravel-example

Conversation

@marandaneto

@marandaneto marandaneto commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Synced with main after #289

Signed merge 40948d8 integrates main at bc7d741, including the URL-host fix from merged PR #289 and the core 4.20.1 version bump. This PR's diff now contains only examples/laravel and its lint configuration. No core SDK, core regression test, release intent, dependency, or API snapshot changes remain in the diff.

Validation after the merge passes: 2,537 core tests / 11,427 assertions, 151 Laravel package tests / 961 assertions, 29 example tests / 163 assertions, and 16 adapter/checker tests. Both public API snapshots, Composer validation, and PHP formatting checks pass. Live validation and MCP ingestion verification are summarized in the PR comment below. No project token or secret is committed.

💡 Motivation and Context

This follows the Pennant integration PR and adds a runnable Laravel 12/13 application under examples/laravel using both local Composer packages.

The dashboard demonstrates explicit/ambient capture, identify/alias/groups, manual and framework errors, flag variants/payloads/snapshots/unresolved reasons, Pennant reads, auth/header precedence, queue context and overrides, model/domain events, and explicit flush. Artisan and scheduled commands exercise the console paths. Environment toggles enable each optional collector independently.

The baseline needs no npm, external database, queue server or PostHog credentials. It uses SQLite and defaults to disabled telemetry with a blank key. Composer path repositories are development-only. No production package metadata is changed here.

This application is for local development only. Its synthetic login intentionally has no password. It must not be exposed publicly. Browser setup is documented separately, with secret-key and duplicate-pageview warnings.

💚 How did you test it?

  • 29 tests, 163 assertions on both Laravel 12 and 13. There are no skipped collector tests.
  • Real SQLite commit/rollback, actual Artisan/scheduler execution, fake capture assertions, and mock-server flag/event serialization and 503 failure cases.
  • Composer installation, config cache/clear and a served HTTP dashboard smoke test pass. The dashboard has 12 forms, includes Pennant and loads no browser script.
  • The full-stack helper tests a clean committed-source copy, not the developer's installed vendor directories.
  • Initial isolated review reports no actionable findings. An earlier claim that after_commit=true broke the queue tests was rejected: Laravel's testing transaction manager deliberately allows callbacks outside application transactions. Added coverage verifies real application commit/rollback behavior without disabling that setting.

📝 Checklist

  • All commits are signed.
  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Implemented with Pi, PHP/Composer, Git and GitHub CLI in an isolated worktree. The application exercises public package APIs and uses synthetic data only. CLI tests explicitly enable Symfony event rerouting because Laravel suppresses it during unit tests. The initial implementation used no live PostHog project. Follow-up validation used synthetic events in a development project. Human review is required before merge.

Reviewed commit: 0ce49ab1ed24c6f08f29ab73f551dc1379e0060c. Reviewed base: 0377f640e79547ded44cc36b2e38cbbb26c68685. Initial isolated review has no actionable findings.

The example CI follow-up uses repository-standard camel-case test names and PHP formatting. Blade templates are excluded from the PHP-only linter, and the conventional Laravel front controller is exempt only from the declarations/side-effects rule. All example tests still execute.

Stack

Merge bottom-up:

  1. feat: add PHP framework integration primitives #282: PHP framework support
  2. feat: add the Laravel runtime package #283: Laravel runtime
  3. feat: add opt-in Laravel autocapture #284: Optional server autocapture
  4. feat: integrate PostHog feature flags with Laravel Pennant #285: Read-only Pennant integration
  5. feat: add a runnable Laravel SDK example #286: Runnable Laravel example
  6. ci: release the Laravel package independently #287: Independent CI and publishing

Laravel source lives in this repository but its Composer package is released separately. Publish core ^4.20 first. The distribution repository, protected release environment and Packagist setup remain administrator prerequisites. No release has been published.

Review feedback follow-up

Historical review follow-up head: 212980ce70e36e7395709b701c88e4824fb6a5d7.

The Pennant form is disabled and server-guarded in POSTHOG_LOCAL_ONLY mode with explicit skipped feedback. A real SDK/mock transport regression verifies zero remote flag requests, while the remote-enabled control still returns boolean and variant values. Latest upstream fixes and internal metadata hooks are integrated.

29 example tests / 163 assertions on Laravel 12 and 13; 106 package tests / 664 assertions in the same clean-source matrix. The local-only request-count regression failed before the guard and passes afterward. Independent scoped review is clean.

The single example review concern is addressed. Local-only means no remote flag fallback; definition fetching and separately enabled analytics collectors are not redefined as offline mode.

Core human-review integration

Signed merges integrate #282 commit 858c204: non-throwing instance group validation (static facade validation unchanged), explicit typed exception source independent of mechanism/handled, provenance protection and scope ownership docs. The core suite passes 2,532 tests / 11,366 assertions. Final committed-source Laravel 12/13 matrices remain green at 106 package tests / 664 assertions and 29 example tests / 163 assertions each. The core delta received a clean isolated review; no automatic Laravel hook-source inference was added.

Before the first Laravel release: server SDK registration

posthog-laravel is not yet registered on PostHog server master for PHP usage attribution, Python/Rust feature-flag library classification/parser, or SDK health frontend/backend registries. A separate backend registration/alias PR must land before the Laravel package ships, following the approach in PostHog/posthog#93911. Event ingestion itself is not rejected just because this library identifier is unclassified. SDK health/version handling must respect Laravel's independent package version, not compare 0.1.x against PHP core 4.x. No backend PR, repository or release was created by this SDK follow-up; this remains an explicit pre-release prerequisite alongside core ^4.20 publication and distribution/Packagist setup.

@marandaneto marandaneto self-assigned this Oct 29, 2026
Base automatically changed from feat/laravel-pennant to main November 3, 2026 09:27
@marandaneto

Copy link
Copy Markdown
Member Author

needs #289

@marandaneto
marandaneto marked this pull request as ready for review November 3, 2026 12:09
@marandaneto
marandaneto requested a review from a team as a code owner November 3, 2026 12:09
@marandaneto

Copy link
Copy Markdown
Member Author

Live validation completed successfully

We exercised the example against a real PostHog test project using synthetic data, then confirmed the ingested events through MCP. The tests below passed. Deliberate exceptions, failed jobs, and injected transport errors were expected test inputs, not unexpected failures.

Example application and collectors

  • Opened the dashboard in a dedicated Brave test profile and exercised all 12 forms over real HTTP with session cookies and CSRF protection.
  • Verified explicit and ambient capture, identify, alias, group identify, explicit identity precedence, authenticated-user precedence over analytics headers, and header fallback after logout.
  • Verified manual handled exceptions, Laravel report(), and an intentional unhandled HTTP 500. Exception handled state, mechanism, source, and identity were correct, and subsequent requests succeeded.
  • Exercised all eight optional collector families: requests, pageviews, auth, models, mapped domain events, jobs, commands, and scheduler.
  • Verified sanitized route/pageview metadata, health and unmatched-route exclusions, model attribute allowlists, and mapped domain-event properties.
  • Verified real transaction commit/rollback and model create/update/delete. Rolled-back state was not sent.
  • Verified before_send redaction. Synthetic query/body canaries and redacted email/password values were absent from ingested properties. No project or secret token appeared in ingested events or rendered HTML.
  • Ran the real Artisan demo and scheduler. Populated transport flushes succeeded. An empty standalone flush returning false was expected and is not a delivery failure.

Remote and local feature flags

  • Verified remote boolean evaluation, both blue and control variants, decoded JSON payloads, accessed-only event attachments, and agreement between the native API and Pennant.
  • Verified cold local-only evaluation with authenticated definition fetching, shared definition-cache reuse across fresh PHP requests, and lazy refresh.
  • Temporarily removed the secret and verified a fresh process could evaluate from shared cached definitions without a definition or remote evaluation request.
  • Verified native and Pennant local-first evaluation, the real Artisan command in local-only mode, and the final direct HTTPS configuration.
  • Instrumented local-evaluation scenarios made zero remote /flags/ evaluation calls. Outgoing exposures and the stored exposures inspected through MCP had locally_evaluated: true.
  • Verified the local-only Pennant form guard and disabled-SDK behavior. The disabled SDK sent neither analytics nor flag requests, even with collectors enabled.

Additional edge cases, tested sequentially

Scenario Successful result
Actual local-to-remote fallback A temporary experience-continuity flag made exactly one remote evaluation request when allowed. Local-only mode returned no value with experience_continuity and made no remote evaluation request.
Changed definitions and cache expiry Cached values stayed unchanged until refresh. Refresh picked up a backend rollout change. After a real two-second cache expiry, a secretless local-only reader returned an empty snapshot without network calls, while remote fallback returned the current value.
Errors and recovery The running app tolerated injected flag HTTP 503 and a client read timeout, then recovered. A failing cache store did not crash the app. Failed definition refresh preserved valid in-memory definitions. Ingestion 503/429 tests recovered by flushing the same event UUID, forwarded successfully exactly once.
Persistent database queue Three real SQLite jobs ran in the same worker process. The first deliberately failed inside a nested context. The next two completed with clean, separate identities/sessions. The anonymous third job inherited no prior context. The exception retained its nested identity and laravel.queue_job source.
Queue context opt-out _posthog propagation metadata was absent. Caller identity/session did not propagate, while explicit job identity still worked.
Laravel 13 Laravel 13.39.0 passed every dashboard action, flags/Pennant/payload checks, intentional 500 followed by successful capture, config cache/clear, Artisan, scheduler, and the example suite: 29 tests / 163 assertions. The earlier live run used Laravel 12.70.2.
Real Redis and concurrency Redis 7.4 with Predis handled six simultaneous PHP processes. Alternating targeting properties produced the expected true/false values. The warm-cache refresh race made exactly one definition fetch and no remote evaluations. Evaluation recovered when Redis stopped and restarted.
Real Octane/RoadRunner Laravel Octane 2.22.0 with RoadRunner 3.0.1 used one persistent PHP worker. Request identities/sessions stayed isolated, an anonymous request remained anonymous, login in one cookie jar did not authenticate another, and an intentional HTTP 500 did not poison subsequent requests. This was a passing isolation test, not an Octane bug.

The extended matrix ran against a private source copy with separate dependencies, database, caches, ports, and test-only routes/jobs. Error responses and delays were injected by a loopback recorder forwarding successful requests to real PostHog endpoints. Redis and Octane were actual services, not mocked lifecycle events. PHP was 8.5.10.

MCP ingestion verification

Run Evidence confirmed through MCP
laravel-pr286-live-20261103T094239Z Audited all 178 stored events against recorded transport evidence or the direct-test script's expected fields. Checked identities, timestamps, properties, and 79 resolved exception frames. No unexplained checked-field mismatches. This historical run retains the known pre-fix flag_missing results; successful flag validation is in the later runs.
laravel-pr286-flags-fixed-20261103T1000Z Audited all 50 stored events against known scenario inputs and saved form/CLI outputs. Boolean true, both variants, payloads, exposure metadata, decision attachments, and identity/context checks passed, with no flag errors or null flag responses.
laravel-pr286-local-eval-20261103T1054Z* Inspected all 26 flag-related stored rows: 12 flag exposures, eight decisions, and six exposure companion rows. Every exposure was locally evaluated, with correct values and no flag errors.
laravel-pr286-matrix-20261103* Reconciled all 283 stored events from the completed sequential matrix. Successful forwarded batches contained 290 event records: 281 matched stored rows, nine repeated flag exposures were suppressed consistently with server deduplication, and two exposure companion rows accounted for the remaining stored rows. No non-exposure event was unexpectedly missing, and no checked payload field had an unexplained mismatch.

Payload comparisons account for timestamp precision, identify/alias normalization, and resolved exception frames. Separate raw-frame source-context storage was not independently retrieved. The fixed-run audit used saved scenario outputs rather than a byte-for-byte outgoing recording. PostHog adds the sender connection IP despite GeoIP enrichment being disabled; this was observed and accounted for.

No additional SDK defect was reproduced in the sequential matrix, so no further SDK fix was needed. Temporary services were stopped, the Redis container was removed, and the two uniquely named test flags were disabled. Testing credentials were removed from the private test copy. The normal sample remains running with local-first evaluation configured only in its ignored environment file. No credentials were committed.

@ioannisj ioannisj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 An AI agent wrote this on behalf of @ioannisj.

No findings. I went through the demo actions, collectors, Pennant wiring and feature tests. The lib/HttpClient.php change is identical to main now that #289 landed, so syncing main should drop it from this diff.

@marandaneto
marandaneto merged commit 97744ed into main Nov 3, 2026
27 checks passed
@marandaneto
marandaneto deleted the feat/laravel-example branch November 3, 2026 12:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants