close
Skip to content

feat(ffe): add runtime-backed PHP feature flag evaluation#3906

Open
leoromanovsky wants to merge 19 commits into
masterfrom
leo.romanovsky/milestone-1-runtime-evaluation
Open

feat(ffe): add runtime-backed PHP feature flag evaluation#3906
leoromanovsky wants to merge 19 commits into
masterfrom
leo.romanovsky/milestone-1-runtime-evaluation

Conversation

@leoromanovsky
Copy link
Copy Markdown

@leoromanovsky leoromanovsky commented May 22, 2026

Motivation

This PR adds real server-side PHP feature flag evaluation backed by Remote Config and the libdatadog native evaluator. PHP 7 gets a Datadog API, PHP 8 can use the optional OpenFeature bridge, and both paths use the same live runtime evaluator.

This is intentionally the evaluation layer only. Exposure delivery, exposure caching, and evaluation metrics land in the later hook, exposure, and metrics PRs.

Shared planning doc: https://docs.google.com/document/d/1NvMfTpZWLBlFmEFNjdnlMyeVpy5l7KD8qujGFco6w2w/edit?tab=t.0

Decisions

  • Production clients/providers read only from the live Remote Config-backed native runtime.
  • The root package remains PHP 7 installable; the OpenFeature SDK is used only in PHP 8 test/app environments and is not a root dependency.
  • productionRuntime=false remains a temporary guardrail while the hook, exposure delivery, and evaluation metrics layers are incomplete. The final production-ready PHP FFE stack should not ship with that warning enabled.
  • The PHP/OpenFeature bridge passes a targeting key plus flat primitive attributes (bool, int, float, string) into the native evaluator. Nested arrays, objects, and null attribute values are dropped at the boundary.
  • DDTrace\Testing\ffe_load_config exists only for local/canonical fixture tests.

Where this PR fits in the stack

This is the bottom layer of the 4-PR stack. #3909 adds the shared hook, while #3910 (EVP exposures) and #3911 (OTLP metrics) build on top of that hook.

pr3906-runtime-evaluation-stack-position

Where this PR fits in the target system

This PR contributes the in-PHP evaluation surface: UserCode -> OpenFeature Client (PHP 8) / DDTrace FeatureFlags Client (PHP 7 + 8), the DDTrace OpenFeature DataDogProvider, the NativeEvaluator FFI bridge into libdatadog, and the Remote Config client for the FFE_FLAGS product.

pr3906-runtime-evaluation-system-scope

Changes

  • Imports the libdatadog FFE evaluator path and keeps PHP user-facing APIs as thin adapters over the native runtime.
  • Wires PHP 7-safe DDTrace\FeatureFlags evaluation through live Remote Config.
  • Adds a PHP 8-only DDTrace\OpenFeature provider bridge.
  • Adds the canonical DataDog/ffe-system-test-data submodule and a native-runtime PHPT loop over ufc-config.json and evaluation-cases/*.json.

Not Included

  • Exposure writer, exposure cache, or EVP forwarding.
  • Exposure flush/dedup tests.
  • Flag-evaluation metric emission.

Validation

  • php vendor/bin/phpunit --config phpunit.xml tests/api/Unit/FeatureFlags tests/OpenFeature/DataDogProviderTest.php: 29 tests / 87 assertions.
  • make test_featureflags: 8 tests / 29 assertions.
  • MAX_TEST_PARALLELISM=1 TESTS=tests/ext/ffe/native_bridge_evaluate.phpt make test_c: 1/1 passed.
  • MAX_TEST_PARALLELISM=1 TESTS=tests/ext/ffe/system_test_data_evaluate.phpt make test_c: 1/1 passed.
  • make test_internal_api_randomized: passed.

Dogfooding Validation

See DataDog/ffe-dogfooding#68.

Ran the PHP 7 and PHP 8 servers locally; observed correct evaluations.

Screenshot 2026-05-22 at 12 57 05 PM

@datadog-datadog-prod-us1-2
Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 60.70% (-0.03%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 782f622 | Docs | Datadog PR Page | Give us feedback!

@datadog-datadog-prod-us1-2
Copy link
Copy Markdown

datadog-datadog-prod-us1-2 Bot commented May 22, 2026

Pipelines  Tests

Fix all issues with BitsAI

⚠️ Warnings

🚦 10 Pipeline jobs failed

DataDog/apm-reliability/dd-trace-php | ASAN test_c: [8.2, arm64]   View in Datadog   GitLab

🔧 Fix in code (Fix with Cursor). Test dynamic config update failed in dynamic_config_auto_update.phpt.

DataDog/apm-reliability/dd-trace-php | check-big-regressions   View in Datadog   GitLab

🔧 Fix in code (Fix with Cursor). Regression Check failed for metrics. Scenario PHPRedisBench/benchRedisBaseline has regression +17.91%, exceeding threshold. Scenario ContextPropagationBench/benchInject128Bit-opcache has regression +42.55%, exceeding threshold. Scenario ContextPropagationBench/benchInject64Bit-opcache has regression +27.45%, exceeding threshold. Exited with code 5.

DataDog/apm-reliability/dd-trace-php | test_web_laravel_57: [7.3, cgi-fcgi]   View in Datadog   GitLab

🔧 Fix in code (Fix with Cursor). 2 failed tests. Error: Failed asserting that 0 matches expected 1 in tests/Integrations/Laravel/PathParamsTestSuite.php:19 and tests/Integrations/Laravel/PathParamsTestSuite.php:31.

View all 10 failed jobs.

1 Test performance regression detected

bug77270.phpt - Bug #77270 (imagecolormatch Out Of Bounds Write on Heap) from php-src.php-src.ext.gd.tests — 6.02s (+5.23s, +668%)   View in Datadog

ℹ️ Info

No other issues found (see more)

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 60.74% (+0.02%)

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: d82e50d | Docs | Datadog PR Page | Give us feedback!

@pr-commenter
Copy link
Copy Markdown

pr-commenter Bot commented May 22, 2026

Benchmarks [ tracer ]

Benchmark execution time: 2026-05-27 16:28:55

Comparing candidate commit d82e50d in PR branch leo.romanovsky/milestone-1-runtime-evaluation with baseline commit 396a51e in branch master.

Found 2 performance improvements and 6 performance regressions! Performance is the same for 186 metrics, 0 unstable metrics.

scenario:ComposerTelemetryBench/benchTelemetryParsing-opcache

  • 🟥 execution_time [+0.515µs; +1.885µs] or [+3.279%; +12.008%]

scenario:ContextPropagationBench/benchInject128Bit-opcache

  • 🟥 execution_time [+718.646ns; +787.354ns] or [+42.549%; +46.617%]

scenario:ContextPropagationBench/benchInject64Bit-opcache

  • 🟥 execution_time [+639.870ns; +742.130ns] or [+27.450%; +31.837%]

scenario:EmptyFileBench/benchEmptyFileBaseline

  • 🟩 execution_time [-300.519µs; -74.681µs] or [-9.205%; -2.288%]

scenario:LogsInjectionBench/benchLogsInfoBaseline-opcache

  • 🟥 execution_time [+135.185ns; +171.815ns] or [+8.202%; +10.425%]

scenario:LogsInjectionBench/benchLogsNullBaseline-opcache

  • 🟩 execution_time [-1.308µs; -0.962µs] or [-5.975%; -4.393%]

scenario:PHPRedisBench/benchRedisBaseline

  • 🟥 execution_time [+30.084µs; +31.074µs] or [+17.911%; +18.500%]

scenario:PHPRedisBench/benchRedisOverhead

  • 🟥 execution_time [+23.536µs; +37.680µs] or [+2.444%; +3.912%]

@leoromanovsky leoromanovsky marked this pull request as ready for review May 22, 2026 18:02
@leoromanovsky leoromanovsky requested review from a team as code owners May 22, 2026 18:02
@leoromanovsky leoromanovsky requested review from tabgok and removed request for a team May 22, 2026 18:02
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e55ebb976b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/bridge/_files_openfeature.php Outdated
Comment thread components-rs/ffe.rs
@leoromanovsky leoromanovsky marked this pull request as draft May 22, 2026 18:57
@leoromanovsky leoromanovsky marked this pull request as ready for review May 22, 2026 19:04
Brings the PHP FFE diagram convention to the M1 PR. Each subsequent PR
in the stack (#3909, #3910, #3911) already carried its own stack +
system diagram; #3906 was missing them.

Mirrors the format used by the rest of the stack:
- `stack-pr3906.mmd` — the 4-PR stack with #3906 badged as current
  and the downstream layers shown as "future".
- `system-pr3906.mmd` — the target end-to-end architecture with
  M1's scope (UserCode, OpenFeature Client, DataDogProvider,
  DDTrace FeatureFlags Client, NativeEvaluator, Remote Config client)
  highlighted, and everything from the Hook layer onward dashed.

All conventions match the other branches: quoted YAML titles (to keep
`#PR-number` out of the YAML comment parser), `flowchart TD`
orientation, rendered with `-w 2400 -H 2400 --scale 3 -b white`.
Comment thread ext/autoload_php_files.c Outdated
Comment thread components-rs/ffe.rs Outdated
Comment thread ext/ddtrace.c Outdated
Comment thread components-rs/ffe.rs Outdated
Comment thread ext/ddtrace.stub.php Outdated
Comment thread ext/ddtrace.c Outdated
Comment thread src/api/FeatureFlags/Client.php Outdated
Comment thread src/DDTrace/OpenFeature/DataDogProvider.php Outdated
Comment thread src/api/FeatureFlags/Internal/ResultMapper.php
Comment thread src/api/FeatureFlags/Internal/NativeEvaluator.php Outdated
Comment thread src/api/FeatureFlags/Internal/NativeEvaluator.php Outdated
Comment thread src/api/FeatureFlags/Internal/UnavailableEvaluator.php Outdated
@leoromanovsky
Copy link
Copy Markdown
Author

Feedback from @bwoebi addressed; retest with local build against ffe-dogfooding server with satisfactory results.

Comment thread components-rs/ffe.rs
Comment on lines +214 to +216
variant: Some(string_to_zend_string(
assignment.variation_key.as_str().to_string(),
)),
Copy link
Copy Markdown
Collaborator

@bwoebi bwoebi May 27, 2026

Choose a reason for hiding this comment

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

Suggested change
variant: Some(string_to_zend_string(
assignment.variation_key.as_str().to_string(),
)),
variant: Some(assignment.variation_key.as_str().into()),

should work, same for allocation_key and value_json in error branch

Comment thread ext/remote_config.c
}
}

void ddtrace_process_remote_config_now(void) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Leftover now

{
$this->client = $client ?: FeatureFlagsClient::createWithDependencies(
null,
new NullLogger(LogLevel::EMERGENCY)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this intentional that it doesn't pass $this->warningLogger here?

/**
* @internal Tests and Datadog-owned bridge adapters only.
*/
public static function createWithDependencies(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

redundant with constructor.

What you probably want is:

    public function __construct(?LoggerInterface $logger = null) // the public API, only has $logger
    {
        $this->warningLogger = $logger ?: new TriggerErrorLogger();
        $this->client = FeatureFlagsClient::createWithDependencies(
            null,
            $this->warningLogger
        );
    }

    public static function createWithDependencies(?FeatureFlagsClient $client = null, ?LoggerInterface $logger = null): self { // the internal test function, can swap the FeatureFlagsClient impl.
        $instance = new self($logger);
        if ($logger) {
            $instance->logger = $logger;
        }
    }

Or, honestly, it's only for testing, right? Strip createWithDependencies fully from the API and assign it in the test:
$provider = new DataDogProvider($logger);
(fn() => $this->client = new RecordingLogger)->call($provider); // bypass private state directly in the test.

That would be my favourite approach.

/**
* @internal Tests and Datadog-owned bridge adapters only.
*/
public static function createWithDependencies(
Copy link
Copy Markdown
Collaborator

@bwoebi bwoebi May 27, 2026

Choose a reason for hiding this comment

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

Same as for DataDogProvider, you probably should make the __construct public API, remove Evaluator as arg (logger becomes optional arg), and overwrite it in tests via Closure::call().

Comment thread .github/dependabot.yml

version: 2
updates:
- package-ecosystem: "gitsubmodule"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is global, I don't want that for other submodules. If you can restrict it to the ffe tests submodule, fine by me.

Comment thread ext/ddtrace.c
Comment on lines +2999 to +3001
if (targeting_key != NULL) {
targeting_key_slice = dd_zend_string_to_CharSlice(targeting_key);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: dd_zend_string_to_CharSlice handles NULL itself already, if is redundant.

Comment thread ext/ddtrace.stub.php
*
* @internal Used by the Datadog feature flag client.
*/
function ffe_evaluate(string $flagKey, int $expectedType, ?string $targetingKey, array $attributes): ?array {}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

minor suggestion, but feel free to ignore:

add

class FfeResult {
    public ?string $valueJson = null;
    public ?string $variant = null;
    public ?string $allocationKey = null;
    public int $reason;
    public int $errorCode;
    public bool $doLog;
}

and return that instead of array. Constructing/accessing objects is faster than arrays too, and better automcomplete etc. But not necessary.

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.

4 participants