Repository navigation
feat: integrate PostHog feature flags with Laravel Pennant #285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
marandaneto
wants to merge
55
commits into
main
Choose a base branch
from
feat/laravel-pennant
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+638
−1
Open
Changes from all commits
Commits
Show all changes
55 commits
Select commit
Hold shift + click to select a range
34e0710
feat: add PHP framework integration primitives
marandaneto 3501917
Merge framework integration APIs for Laravel runtime
marandaneto bec01c1
feat: add Laravel runtime foundation
marandaneto 1536956
feat: propagate safe Laravel queue context and prepare package distri…
marandaneto 18d9ac6
feat: enrich Laravel captures with safe request metadata
marandaneto 04967bf
feat: add opt-in Laravel server event collectors
marandaneto 53cdbc5
fix: preserve explicit and nested Laravel scopes through lifecycle ev…
marandaneto 460d2d7
fix: retain failed command context until Laravel reports the exception
marandaneto 33a5dc3
Merge branch 'feat/laravel-package-runtime' into feat/laravel-autocap…
marandaneto caa7a73
fix: preserve collector privacy defaults and session correlation
marandaneto 47bec03
feat: add an optional read-only Laravel Pennant driver
marandaneto 3f4e01d
Merge branch 'feat/laravel-autocapture' into feat/laravel-pennant
marandaneto 3b2ad5a
fix: use explicit HTTP and console execution lifecycle boundaries
marandaneto f375d53
Merge branch 'feat/laravel-package-runtime' into feat/laravel-autocap…
marandaneto 8480a6f
Merge branch 'feat/laravel-autocapture' into feat/laravel-pennant
marandaneto 5465c69
test: construct synthetic URL credentials without secret-scan false p…
marandaneto 2f07fde
test: verify Pennant normalizes custom and model scope contracts
marandaneto ce94a3f
Merge branch 'feat/laravel-autocapture' into feat/laravel-pennant
marandaneto 0377f64
Merge branch 'feat/laravel-autocapture' into feat/laravel-pennant
marandaneto 0e3243e
fix: capture explicitly failed Laravel jobs as failures
marandaneto 8773c43
fix: address framework integration review feedback
marandaneto 5e93cc5
fix(laravel): discover Pennant flags for the requested scope
marandaneto ecbc8cc
fix(flags): retain snapshot context for lazy exposure events
marandaneto d9d9897
fix(laravel): bridge runtime scopes and isolate diagnostics
marandaneto 07e50c1
Merge signed core flag exposure origin-context dependency
marandaneto 26ced2f
fix(laravel): clear Pennant cache after restoring queue context
marandaneto 8526b32
fix(laravel): preserve raw context precedence in manager getter
marandaneto 9601245
fix(laravel): bound deferred model events to their owning transactions
marandaneto 893c8c5
fix(laravel): omit outcomes for overlap-skipped schedules
marandaneto 4387cf8
Merge commit '8526b3285e78d06b33ae1895fa72f86d353b8f8c' into feat/lar…
marandaneto 4e953ec
Merge branch 'feat/laravel-autocapture' into feat/laravel-pennant
marandaneto a33384a
fix: make library metadata and release id internal integration overrides
marandaneto 66063ab
Merge internal core integration overrides for Laravel runtime
marandaneto 5a9958f
fix(laravel): use internal client overrides for library metadata and …
marandaneto b63cd37
fix(laravel): release model buffer on commit failure retry
marandaneto 71ac3bc
Merge commit '5a9958f239dfe68744597c5df894cd6aca4fac11' into feat/lar…
marandaneto b2d9730
Merge branch 'feat/laravel-autocapture' into feat/laravel-pennant
marandaneto dc28ca9
docs: explain when to use withContext versus enterContext
marandaneto 4848a82
Merge commit 'dc28ca9bb1e36de1c377017243391d41aac246a2' into feat/lar…
marandaneto bffd72a
Merge branch 'feat/laravel-autocapture' into feat/laravel-pennant
marandaneto 8ffd0ad
Merge branch 'feat/laravel-package-runtime' into feat/laravel-autocap…
marandaneto e23f1fc
feat(laravel): isolate direct child command calls with opt-in trait
marandaneto d6848b6
Merge command isolation into Pennant integration
marandaneto c8cf8b0
Merge runtime command isolation into collectors
marandaneto 3321cfb
fix(laravel): clear Pennant cache across direct command scopes
marandaneto 858c204
fix: address framework primitive review feedback
marandaneto 944a3bd
Merge framework review fixes into runtime
marandaneto c70c493
Merge framework review fixes into autocapture
marandaneto 31abada
Merge framework review fixes into pennant
marandaneto 4383d7f
Merge remote-tracking branch 'origin/main' into feat/laravel-pennant
marandaneto 370eb1a
chore: drop changeset already released in posthog-php 4.20.0
marandaneto f79cf5f
Merge remote-tracking branch 'origin/main' into feat/laravel-pennant
marandaneto fbd861a
chore: remove stale Laravel runtime release intent
marandaneto 1f4138e
fix(laravel): keep Pennant implementation classes internal
marandaneto 0dca396
chore: defer Laravel release intents
marandaneto File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,3 +15,4 @@ xdebug.log | |
| .DS_Store | ||
| .env | ||
| node_modules/ | ||
| /.laravel-changes/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| # Laravel Pennant | ||
|
|
||
| The optional, read-only driver uses the PHP SDK's feature flag snapshots. Install | ||
| Pennant in your application, then select the store: | ||
|
|
||
| ```sh | ||
| composer require laravel/pennant:^1.26 | ||
| ``` | ||
|
|
||
| ```dotenv | ||
| PENNANT_STORE=posthog | ||
| ``` | ||
|
|
||
| Package discovery registers a `posthog` store if the application has not already | ||
| configured one. No Pennant migration or database-backed flag store is needed. | ||
|
|
||
| ```php | ||
| use Laravel\Pennant\Feature; | ||
|
|
||
| Feature::for('account-123')->active('new-checkout'); | ||
| $variant = Feature::for('account-123')->value('checkout-layout'); | ||
| ``` | ||
|
|
||
| Values retain their boolean or variant-string type. Unknown or unresolved flags | ||
| return `false`, not an accidentally active `null`. Reads use the SDK's normal | ||
| exposure events; repeated cached reads do not generate duplicate exposures. | ||
|
|
||
| String and integer scopes are distinct IDs. Authenticated user/model scopes use | ||
| their auth identifier or model key, without a class-name prefix, to match ordinary | ||
| SDK capture. Models are not serialized and their properties are not sent. | ||
| Pennant normally uses the authenticated user as its default scope. When that | ||
| scope is null, the driver uses the runtime's current analytics context. Tracing | ||
| headers remain untrusted analytics input, never authorization evidence. | ||
|
|
||
| For custom scope types, use Pennant's scope-identifier contract to resolve an | ||
| explicit string/integer identifier. Configure runtime group resolvers for group | ||
| flag targeting. Use the native `evaluateFlags()` API for person/group properties, | ||
| local-only evaluation, payloads, key subsets and unresolved reasons: | ||
|
|
||
| ```php | ||
| $payload = \PostHog\Laravel\Facades\PostHog::evaluateFlags()->getFlagPayload('checkout-layout'); | ||
| ``` | ||
|
|
||
| ## Read-only behavior | ||
|
|
||
| Define and manage flags in PostHog. `define`, `activate`, `deactivate`, `delete`, | ||
| `purge` and other write operations throw `LogicException`; the driver never pretends | ||
| to modify remote flag definitions. Use remote flag key strings, not locally defined | ||
| PHP feature classes. | ||
|
|
||
| `Feature::defined()` intentionally returns an empty array: this read-only external | ||
| store has no local definitions. Pennant calls it during feature-name resolution, | ||
| before resolving the requested scope, so it never evaluates flags or performs I/O. | ||
| Use `Feature::for($scope)->all()` or `Feature::for($scope)->loadAll()` to discover | ||
| PostHog flags for a scope through Pennant's external-definition contract. Discovery | ||
| uses that scope's native snapshot without recording exposure; reading the discovered | ||
| values uses normal deduplicated exposures. Bulk and subsequent individual reads | ||
| reuse the same snapshot, preserving variants and disabled values. Unknown or | ||
| unresolved flags remain inactive. | ||
|
|
||
| ## Cache lifetime | ||
|
|
||
| The SDK reuses snapshots for identical complete evaluation inputs. Pennant also | ||
| has its own feature/scope cache. The integration clears PostHog stores around HTTP, | ||
| queue and command boundaries, including failed jobs. Pennant's own Octane lifecycle | ||
| integration remains active, and the driver resolves the current application rather | ||
| than retaining a request manager in a singleton. Calling commands that opt into | ||
| `IsolatesCommandContext` also clear Pennant's cache on direct child-call entry and | ||
| after restoring the caller, including failures. Uninstrumented direct calls are | ||
| not intercepted; explicit context scopes alone do not clear Pennant's cache. | ||
|
|
||
| If targeting properties/groups change for the **same scope within one request**, | ||
| call `Feature::flushCache()` before another Pennant read. Alternatively use native | ||
| `evaluateFlags()` directly, which keys its snapshot cache by all evaluation inputs. | ||
| Do not retain a scoped PostHog manager in an application singleton. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| <?php | ||
|
|
||
| namespace PostHog\Laravel\Features; | ||
|
|
||
| use Illuminate\Container\Container; | ||
| use Laravel\Pennant\FeatureManager; | ||
|
|
||
| /** @internal Pennant caches values independently of the SDK's scoped snapshots. */ | ||
| final class PennantCache | ||
| { | ||
| public static function flush(): void | ||
| { | ||
| $app = Container::getInstance(); | ||
| if (!$app->resolved(FeatureManager::class)) { | ||
| return; | ||
| } | ||
| $manager = $app->make(FeatureManager::class); | ||
| $manager->setContainer($app); | ||
| foreach ($app['config']->get('pennant.stores', []) as $name => $store) { | ||
| if (($store['driver'] ?? null) === 'posthog') { | ||
| $manager->store($name)->flushCache(); | ||
| } | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| <?php | ||
|
|
||
| namespace PostHog\Laravel\Features; | ||
|
|
||
| use Closure; | ||
|
|
||
| /** @internal */ | ||
| final class PennantMiddleware | ||
| { | ||
| public function handle($request, Closure $next): mixed | ||
| { | ||
| PennantCache::flush(); | ||
| try { | ||
| return $next($request); | ||
| } finally { | ||
| PennantCache::flush(); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| <?php | ||
|
|
||
| namespace PostHog\Laravel\Features; | ||
|
|
||
| use Illuminate\Contracts\Http\Kernel; | ||
| use Illuminate\Support\ServiceProvider; | ||
| use Laravel\Pennant\FeatureManager; | ||
|
|
||
| /** @internal */ | ||
| final class PennantServiceProvider extends ServiceProvider | ||
| { | ||
| public function boot(): void | ||
| { | ||
| if ($this->app['config']->get('pennant.stores.posthog') === null) { | ||
| $this->app['config']->set('pennant.stores.posthog', ['driver' => 'posthog']); | ||
| } | ||
| $this->callAfterResolving(FeatureManager::class, function ($manager) { | ||
| $manager->extend('posthog', function () { | ||
| return new PostHogDriver(); | ||
| }); | ||
| }); | ||
| $this->callAfterResolving(Kernel::class, fn ($kernel) => $kernel->prependMiddleware(PennantMiddleware::class)); | ||
| $this->app['events']->listen([ | ||
| \Illuminate\Queue\Events\JobProcessing::class, | ||
| \Illuminate\Queue\Events\JobProcessed::class, | ||
| \Illuminate\Queue\Events\JobExceptionOccurred::class, | ||
| \Illuminate\Console\Events\CommandStarting::class, | ||
| \Illuminate\Console\Events\CommandFinished::class, | ||
| ], fn () => PennantCache::flush()); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| <?php | ||
|
|
||
| namespace PostHog\Laravel\Features; | ||
|
|
||
| use Illuminate\Container\Container; | ||
| use Illuminate\Contracts\Auth\Authenticatable; | ||
| use Illuminate\Database\Eloquent\Model; | ||
| use InvalidArgumentException; | ||
| use Laravel\Pennant\Contracts\DefinesFeaturesExternally; | ||
| use Laravel\Pennant\Contracts\Driver; | ||
| use LogicException; | ||
| use PostHog\Laravel\PostHog; | ||
|
|
||
| /** @internal */ | ||
| final class PostHogDriver implements Driver, DefinesFeaturesExternally | ||
| { | ||
| public function get(string $feature, mixed $scope): mixed | ||
| { | ||
| $id = $this->identity($scope); | ||
| return Container::getInstance()->make(PostHog::class)->evaluateFlags($id)->getFlag($feature) ?? false; | ||
| } | ||
|
|
||
| public function getAll(array $features): array | ||
| { | ||
| $values = []; | ||
| foreach ($features as $feature => $scopes) { | ||
| $values[$feature] = []; | ||
| foreach ($scopes as $key => $scope) { | ||
| $values[$feature][$key] = $this->get($feature, $scope); | ||
| } | ||
| } | ||
| return $values; | ||
| } | ||
|
|
||
| public function defined(): array | ||
| { | ||
| // Pennant calls this before resolving the scope; there are no local definitions. | ||
| return []; | ||
| } | ||
|
|
||
| public function definedFeaturesForScope(mixed $scope): array | ||
| { | ||
| return Container::getInstance()->make(PostHog::class)->evaluateFlags($this->identity($scope))->getKeys(); | ||
| } | ||
|
|
||
| private function identity(mixed $scope): ?string | ||
| { | ||
| if ($scope === null) { | ||
| return null; | ||
| } | ||
| if ($scope instanceof Authenticatable) { | ||
| $scope = $scope->getAuthIdentifier(); | ||
| } elseif ($scope instanceof Model) { | ||
| $scope = $scope->getKey(); | ||
| } | ||
| if (is_string($scope) || is_int($scope)) { | ||
| return (string) $scope; | ||
| } | ||
| throw new InvalidArgumentException('PostHog Pennant scopes must have a string or integer identifier.'); | ||
| } | ||
|
|
||
| public function define(string $feature, callable $resolver): void | ||
| { | ||
| throw new LogicException('PostHog flags are read-only. Define them in PostHog.'); | ||
| } | ||
|
|
||
| public function set(string $feature, mixed $scope, mixed $value): void | ||
| { | ||
| throw new LogicException('PostHog flags are read-only. Update them in PostHog.'); | ||
| } | ||
|
|
||
| public function setForAllScopes(string $feature, mixed $value): void | ||
| { | ||
| throw new LogicException('PostHog flags are read-only. Update them in PostHog.'); | ||
| } | ||
|
|
||
| public function delete(string $feature, mixed $scope): void | ||
| { | ||
| throw new LogicException('PostHog flags are read-only. Remove them in PostHog.'); | ||
| } | ||
|
|
||
| public function purge(?array $features): void | ||
| { | ||
| throw new LogicException('PostHog flags are read-only. Remove them in PostHog.'); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
packages/laravel/tests/Fixtures/PennantEndEventProvider.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| <?php | ||
|
|
||
| namespace PostHog\Laravel\Tests\Fixtures; | ||
|
|
||
| use Illuminate\Queue\Events\JobExceptionOccurred; | ||
| use Illuminate\Queue\Events\JobProcessed; | ||
| use Illuminate\Support\ServiceProvider; | ||
| use Laravel\Pennant\Feature; | ||
|
|
||
| class PennantEndEventProvider extends ServiceProvider | ||
| { | ||
| public function boot(): void | ||
| { | ||
| $this->app['events']->listen([JobProcessed::class, JobExceptionOccurred::class], function (): void { | ||
| if ($this->app['config']->get('pennant-test.read-on-job-end', false)) { | ||
| $this->app['config']->set('pennant-test.job-value', Feature::value('layout')); | ||
| } | ||
| }); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.