Skip to content

feat: integrate PostHog feature flags with Laravel Pennant - #285

Draft
marandaneto wants to merge 19 commits into
feat/laravel-autocapturefrom
feat/laravel-pennant
Draft

marandaneto wants to merge 19 commits into
feat/laravel-autocapturefrom
feat/laravel-pennant

Conversation

@marandaneto

@marandaneto marandaneto commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

💡 Motivation and Context

This follows the Laravel collectors PR in the stack and adds an optional, read-only PostHog store for Laravel Pennant. Pennant remains an application dependency, not a required runtime dependency of the Laravel package.

Boolean and variant reads delegate to the native SDK snapshots. Unknown results are inactive. User/model scopes use the same identifier as normal SDK capture, and Pennant's own scope contract is resolved before the driver receives it. Write operations fail explicitly instead of pretending to change remote PostHog flags.

The adapter resolves the current application rather than retaining a request manager in a singleton. Its HTTP, queue and command hooks clear Pennant's separate cache. Same-scope targeting changes within one request still require Feature::flushCache(), as documented in PENNANT.md. Advanced flag options and payloads remain available through evaluateFlags().

The evaluate-flags and exposure contracts apply through the PHP SDK. There is no separate Pennant-specific SDK spec.

💚 How did you test it?

  • 25 Pennant test cases use the real Pennant package and mock SDK HTTP traffic. They check one snapshot per identity, variants, unknown/disabled states, payload access, exposure deduplication, two requests, model IDs, custom scope contracts and rejected mutations.
  • The integrated package passes 106 tests, 664 assertions on Laravel 12 and 13.
  • An initial isolated-review finding claimed Pennant passed unnormalized scope objects. Source inspection showed Decorator::get() normalizes them first. Two real public-API tests prove both custom objects and models honor FeatureScopeable; both passed without changing production code. That finding was rejected with this evidence.
  • PSR-12 and git diff --check pass. Initial exact-head isolated review reports no actionable findings.

📝 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

  • Recorded a Laravel-only initial release intent in .laravel-changes/.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Implemented with Pi, PHP/Composer, Git and GitHub CLI in a dedicated worktree. The adapter uses the SDK's evaluator rather than implementing flag logic. Public Pennant integration tests validate the framework contract instead of relying on assumptions from a bundle-only review. Human review is required before merge.

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

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

Current head: bffd72ae717f224d264b73300e8a3ce4b63409a1.

The driver implements scoped external flag discovery for all()/loadAll(); defined() is a non-networking local catalog, preventing accidental ambient-identity evaluation. PostHog-backed Pennant caches are flushed after actual context restoration, including sync-job failure paths and late end-event reads.

Final clean-source Laravel 12 and 13 stack (PHP 8.5 locally): 106 package tests / 664 assertions and 29 example tests / 163 assertions per version. Regression coverage includes scoped enumeration, exactly one explicit-identity request, silent listing, and four real sync-job restoration cases. Independent scoped source/vendor-aware review found no remaining actionable findings.

All three Pennant review concerns addressed. The intentional defined() semantics are documented; no mutable cross-request catalog or new store policy was added.

Direct-command context follow-up

The new upstream opt-in command trait is integrated. Pennant's independent cache now clears on console-context entry and after actual caller restoration, including direct child failures. Four new anonymous/identified parent × success/throw cases failed before and pass afterward. An isolated mutation removing only exit cleanup reproduces child-cache leakage in all four cases. Parent native snapshots are preserved without extra flag requests. Both scoped independent reviews are clean.

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.

@posthog posthog Bot 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.

PostHog Review · Standard · reviewed 0377f64: 3 Should fix.

This is the only automatic review while this pull request is a draft. Comment @posthog review to review new changes sooner. Otherwise, a review of the new changes runs when you mark it ready for review.

See this review and its findings in PostHog Review. Comment @posthog review --deep for a Deep review. A Deep review takes longer and looks deeper than a Standard review.

Comment thread packages/laravel/src/Features/PostHogDriver.php Outdated
Comment thread packages/laravel/src/Features/PennantServiceProvider.php
Comment thread packages/laravel/src/Features/PostHogDriver.php Outdated
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

posthog-php-lib_curl Compliance Report

Date: 2026-10-30T08:47:21.708263+00:00
Duration: 116164ms

⚠️ Some Tests Failed

46/47 tests passed, 1 failed


Capture Tests

⚠️ 29/30 tests passed, 1 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 28ms
Format Validation.Event Has Uuid ✅ 523ms
Format Validation.Event Has Lib Properties ✅ 525ms
Format Validation.Distinct Id Is String ✅ 524ms
Format Validation.Token Is Present ✅ 524ms
Format Validation.Custom Properties Preserved ✅ 524ms
Format Validation.Event Has Timestamp ✅ 526ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 524ms
Retry Behavior.Retries On 503 ✅ 5833ms
Retry Behavior.Does Not Retry On 400 ✅ 2526ms
Retry Behavior.Does Not Retry On 401 ✅ 2525ms
Retry Behavior.Respects Retry After Header ❌ 6336ms
Retry Behavior.Implements Backoff ✅ 16240ms
Retry Behavior.Retries On 500 ✅ 5138ms
Retry Behavior.Retries On 502 ✅ 5632ms
Retry Behavior.Retries On 504 ✅ 5632ms
Retry Behavior.Max Retries Respected ✅ 17047ms
Deduplication.Generates Unique Uuids ✅ 231ms
Deduplication.Preserves Uuid On Retry ✅ 5631ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10841ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5634ms
Deduplication.No Duplicate Events In Batch ✅ 531ms
Deduplication.Different Events Have Different Uuids ✅ 526ms
Compression.Sends Gzip When Enabled ✅ 525ms
Batch Format.Uses Proper Batch Structure ✅ 524ms
Batch Format.Flush With No Events Sends Nothing ✅ 520ms
Batch Format.Multiple Events Batched Together ✅ 513ms
Error Handling.Does Not Retry On 403 ✅ 2528ms
Error Handling.Does Not Retry On 413 ✅ 2527ms
Error Handling.Retries On 408 ✅ 5632ms

Failures

retry_behavior.respects_retry_after_header

Retry delay too short: 803ms < 2500ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 523ms
Request Payload.Flags Request Uses V2 Query Param ✅ 522ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 523ms
Request Payload.Flags Request Omits Authorization Header ✅ 523ms
Request Payload.Token In Flags Body Matches Init ✅ 523ms
Request Payload.Groups Round Trip ✅ 522ms
Request Payload.Groups Default To Empty Object ✅ 522ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 522ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 523ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 526ms
Request Lifecycle.No Flags Request On Init Alone ✅ 517ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 509ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 526ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 524ms
Retry Behavior.Retries Flags On 502 ✅ 624ms
Retry Behavior.Retries Flags On 504 ✅ 624ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 525ms

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

posthog-php-socket Compliance Report

Date: 2026-10-30T08:46:52.198026+00:00
Duration: 115700ms

⚠️ Some Tests Failed

46/47 tests passed, 1 failed


Capture Tests

⚠️ 29/30 tests passed, 1 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 29ms
Format Validation.Event Has Uuid ✅ 523ms
Format Validation.Event Has Lib Properties ✅ 524ms
Format Validation.Distinct Id Is String ✅ 525ms
Format Validation.Token Is Present ✅ 525ms
Format Validation.Custom Properties Preserved ✅ 525ms
Format Validation.Event Has Timestamp ✅ 525ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 525ms
Retry Behavior.Retries On 503 ✅ 5833ms
Retry Behavior.Does Not Retry On 400 ✅ 2528ms
Retry Behavior.Does Not Retry On 401 ✅ 2529ms
Retry Behavior.Respects Retry After Header ❌ 6334ms
Retry Behavior.Implements Backoff ✅ 16250ms
Retry Behavior.Retries On 500 ✅ 5633ms
Retry Behavior.Retries On 502 ✅ 5635ms
Retry Behavior.Retries On 504 ✅ 5633ms
Retry Behavior.Max Retries Respected ✅ 16248ms
Deduplication.Generates Unique Uuids ✅ 533ms
Deduplication.Preserves Uuid On Retry ✅ 5634ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10832ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5143ms
Deduplication.No Duplicate Events In Batch ✅ 533ms
Deduplication.Different Events Have Different Uuids ✅ 527ms
Compression.Sends Gzip When Enabled ✅ 525ms
Batch Format.Uses Proper Batch Structure ✅ 526ms
Batch Format.Flush With No Events Sends Nothing ✅ 521ms
Batch Format.Multiple Events Batched Together ✅ 512ms
Error Handling.Does Not Retry On 403 ✅ 2528ms
Error Handling.Does Not Retry On 413 ✅ 2528ms
Error Handling.Retries On 408 ✅ 5633ms

Failures

retry_behavior.respects_retry_after_header

Retry delay too short: 802ms < 2500ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 524ms
Request Payload.Flags Request Uses V2 Query Param ✅ 523ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 522ms
Request Payload.Flags Request Omits Authorization Header ✅ 523ms
Request Payload.Token In Flags Body Matches Init ✅ 523ms
Request Payload.Groups Round Trip ✅ 524ms
Request Payload.Groups Default To Empty Object ✅ 524ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 523ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 522ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 523ms
Request Lifecycle.No Flags Request On Init Alone ✅ 518ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 508ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 526ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 524ms
Retry Behavior.Retries Flags On 502 ✅ 626ms
Retry Behavior.Retries Flags On 504 ✅ 624ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 526ms

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

posthog-php-fork_curl Compliance Report

Date: 2026-10-30T08:47:13.911553+00:00
Duration: 116682ms

⚠️ Some Tests Failed

46/47 tests passed, 1 failed


Capture Tests

⚠️ 29/30 tests passed, 1 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 39ms
Format Validation.Event Has Uuid ✅ 532ms
Format Validation.Event Has Lib Properties ✅ 536ms
Format Validation.Distinct Id Is String ✅ 537ms
Format Validation.Token Is Present ✅ 535ms
Format Validation.Custom Properties Preserved ✅ 535ms
Format Validation.Event Has Timestamp ✅ 534ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 536ms
Retry Behavior.Retries On 503 ✅ 5861ms
Retry Behavior.Does Not Retry On 400 ✅ 2537ms
Retry Behavior.Does Not Retry On 401 ✅ 2536ms
Retry Behavior.Respects Retry After Header ❌ 6350ms
Retry Behavior.Implements Backoff ✅ 16283ms
Retry Behavior.Retries On 500 ✅ 5650ms
Retry Behavior.Retries On 502 ✅ 5654ms
Retry Behavior.Retries On 504 ✅ 5649ms
Retry Behavior.Max Retries Respected ✅ 16286ms
Deduplication.Generates Unique Uuids ✅ 542ms
Deduplication.Preserves Uuid On Retry ✅ 5648ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10870ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5654ms
Deduplication.No Duplicate Events In Batch ✅ 541ms
Deduplication.Different Events Have Different Uuids ✅ 535ms
Compression.Sends Gzip When Enabled ✅ 536ms
Batch Format.Uses Proper Batch Structure ✅ 535ms
Batch Format.Flush With No Events Sends Nothing ✅ 522ms
Batch Format.Multiple Events Batched Together ✅ 519ms
Error Handling.Does Not Retry On 403 ✅ 2534ms
Error Handling.Does Not Retry On 413 ✅ 2535ms
Error Handling.Retries On 408 ✅ 5653ms

Failures

retry_behavior.respects_retry_after_header

Retry delay too short: 812ms < 2500ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 526ms
Request Payload.Flags Request Uses V2 Query Param ✅ 523ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 524ms
Request Payload.Flags Request Omits Authorization Header ✅ 527ms
Request Payload.Token In Flags Body Matches Init ✅ 526ms
Request Payload.Groups Round Trip ✅ 524ms
Request Payload.Groups Default To Empty Object ✅ 524ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 525ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 531ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 526ms
Request Lifecycle.No Flags Request On Init Alone ✅ 520ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 518ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 527ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 526ms
Retry Behavior.Retries Flags On 502 ✅ 629ms
Retry Behavior.Retries Flags On 504 ✅ 629ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 539ms

marandaneto and others added 13 commits October 29, 2026 12:29
* feat: add PHP framework integration primitives

* fix: address framework integration review feedback

* fix(flags): retain snapshot context for lazy exposure events

* fix: make library metadata and release id internal integration overrides

Remove library_name, library_version and release_id from the public client
options. Framework integrations override internal Client methods instead, so
customers keep POSTHOG_RELEASE_ID as the only documented release id input.

* docs: explain when to use withContext versus enterContext

* fix: address framework primitive review feedback
Bumps the github-actions group with 3 updates: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml](https://github.com/posthog/posthog-sdk-test-harness).


Updates `github/codeql-action/init` from 4.38.3 to 4.39.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@24c5418...6f2ac81)

Updates `github/codeql-action/analyze` from 4.38.3 to 4.39.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@24c5418...6f2ac81)

Updates `PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml` from 1.15.0 to 1.19.0
- [Release notes](https://github.com/posthog/posthog-sdk-test-harness/releases)
- [Changelog](https://github.com/PostHog/posthog-sdk-test-harness/blob/main/CHANGELOG.md)
- [Commits](PostHog/posthog-sdk-test-harness@64ffa6a...129fbb4)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml
  dependency-version: 1.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Manoel Aranda Neto <[email protected]>

This branch has not been deployed

No deployments
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.

1 participant