Skip to content

Conversation

@leoromanovsky
Copy link

Motivation

Add FFE support to dd-trace-php. PHP applications can evaluate feature flags delivered via Remote Config using the same datadog-ffe Rust engine used by Ruby and Python.

Stacked on #3642 (libdatadog bump).

Changes

  • Rust FFI layer (components-rs/ffe.rs): C-callable bridge to datadog-ffe::rules_based — config store, evaluate, result accessors
  • C extension (ext/ddtrace.c): ffe_evaluate, ffe_has_config, ffe_config_changed, ffe_load_config internal functions that marshal PHP arrays to FfeAttribute structs
  • Remote Config (components-rs/remote_config.rs): Register FfeFlags product + FfeFlagConfigurationRules capability; handle add/remove of FFE configs
  • PHP Provider (src/DDTrace/FeatureFlags/Provider.php): Singleton that checks RC config state, calls native evaluate, parses JSON results, reports exposures
  • Exposure pipeline: LRU dedup cache (65K entries) + batched writer to /evp_proxy/v2/api/v2/exposures (1000 event buffer cap)
  • OpenFeature adapter (src/DDTrace/OpenFeature/DataDogProvider.php): Implements AbstractProvider for the open-feature/sdk composer package
  • Tests: LRU cache unit tests, exposure cache unit tests, 220 evaluation correctness tests from JSON fixtures
  • Config: DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED gating via X-macro in ext/configuration.h

Decisions

Evaluation in Rust, not PHP. All flag evaluation (UFC parsing, targeting rules, shard hashing, allocation resolution) happens in libdatadog's datadog-ffe crate via FFI. PHP only handles orchestration (config lifecycle, exposure dedup, HTTP transport). This matches Ruby and Python — no language re-implements evaluation logic.

Global config behind Mutex<FfeState>. The Rust FFE config is stored in a lazy_static global with a Mutex. PHP is single-threaded per process, so RwLock would be unnecessary complexity. The changed flag and config are bundled in one struct to avoid torn reads.

Reuses existing RC pipeline. FFE configs flow through the same sidecar → ddog_process_remote_configs() path as APM Tracing and Live Debugger. No new polling mechanism.

Structured attributes, not JSON blobs. The C extension converts PHP arrays into FfeAttribute structs (typed: string/number/bool) before calling Rust, avoiding JSON encode/decode overhead on the hot path.

Exposure dedup uses length-prefixed composite keys. Key = len(flag):flag:subject, value = len(variant):variant:allocation. Avoids collision from delimiters appearing in flag/subject strings.

ExposureWriter caps at 1000 events per request. Matches Ruby and Python. Flush via register_shutdown_function.

Test Results

Unit tests (local, no extension):

243 tests, 75 assertions, 1 skipped (EvaluationTest requires extension)

Unit tests (Docker with extension):

243 tests, 735 assertions — all pass

System tests (FEATURE_FLAGGING_AND_EXPERIMENTATION, apache-mod-8.0):

13 tests, 0 failures, 0 errors, 1 xfail
12 passed, 1 skipped (exposure cache cross-request dedup — PHP shared-nothing arch)

Cross-language dogfood parity (6 flags, 6 SDKs):

ALL MATCH: boolean, float, integer, JSON, string flags across Go, Java, Node, PHP, Python, Ruby

Companion PRs

LOC Breakdown (excluding Cargo.lock + JSON fixtures)

Category Lines
Rust FFI 323
C extension 125
PHP source 829
PHP tests 559
Wiring 7
Total 1,843

Update libdatadog submodule to include:
- feat: Add FFE_FLAGS remote config product and capability (#1532)

This adds RemoteConfigProduct::FfeFlags, RemoteConfigCapabilities::
FfeFlagConfigurationRules, and RemoteConfigData::FfeFlags to the
remote config layer. Required for the PHP FFE integration (PR #2).

Cargo.lock regenerated with pins for CI compatibility:
home→0.5.9, rmp→0.8.14, rmp-serde→1.3.0 (edition2024 crates).
Add FFE support to dd-trace-php. Flag evaluation is delegated to
libdatadog's datadog-ffe Rust crate via FFI. PHP handles orchestration:
config lifecycle, exposure dedup, and HTTP transport.

Rust FFI layer (components-rs/ffe.rs):
- C-callable bridge to datadog-ffe::rules_based
- Global config store behind Mutex<FfeState>
- Structured attribute passing (no JSON on hot path)

C extension (ext/ddtrace.c):
- ffe_evaluate, ffe_has_config, ffe_config_changed, ffe_load_config
- Marshals PHP arrays to FfeAttribute structs

Remote Config (components-rs/remote_config.rs):
- Register FfeFlags product + FfeFlagConfigurationRules capability
- Handle add/remove of FFE configs via sidecar

PHP Provider (src/DDTrace/FeatureFlags/Provider.php):
- Singleton checking RC config state
- Calls native evaluate, parses JSON results
- Reports exposures via LRU-deduplicated writer

Exposure pipeline:
- LRU cache (65K entries) with length-prefixed composite keys
- Batched writer to /evp_proxy/v2/api/v2/exposures (1000 cap)
- Auto-flush via register_shutdown_function

OpenFeature adapter (src/DDTrace/OpenFeature/DataDogProvider.php):
- Implements AbstractProvider for open-feature/sdk

Tests:
- LRU cache unit tests (11 tests)
- Exposure cache unit tests (12 tests)
- 220 evaluation correctness tests from JSON fixtures

Config: DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED (default: false)
@datadog-datadog-prod-us1
Copy link

datadog-datadog-prod-us1 bot commented Feb 12, 2026

⚠️ Tests

Fix all issues with Cursor

⚠️ Warnings

🧪 24 Tests failed

    testSearchPhpBinaries from integration.DDTrace\Tests\Integration\PHPInstallerTest (Fix with Cursor)

    testSimplePushAndProcess from laravel-58-test.DDTrace\Tests\Integrations\Laravel\V5_8\QueueTest (Fix with Cursor)

testSimplePushAndProcess from laravel-8x-test.DDTrace\Tests\Integrations\Laravel\V8_x\QueueTest (Datadog) (Fix with Cursor)
DDTrace\Tests\Integrations\Laravel\V8_x\QueueTest::testSimplePushAndProcess
Test code or tested code printed unexpected output: spanLinksTraceId: 698d2400000000009baafa01192f4f58
tid: 698d240000000000
hexProcessTraceId: 9baafa01192f4f58
hexProcessSpanId: 7e1b411444eb6e46
processTraceId: 11217052704544083800
processSpanId: 9086928228461538886
View all

ℹ️ Info

❄️ No new flaky tests detected

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

@leoromanovsky
Copy link
Author

Superseded — going with single PR #3630 instead of stacked approach.

@codecov-commenter
Copy link

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 62.11%. Comparing base (f3d286f) to head (d83199b).

Additional details and impacted files

Impacted file tree graph

@@                    Coverage Diff                    @@
##           ffe/1-libdatadog-bump    #3643      +/-   ##
=========================================================
- Coverage                  62.20%   62.11%   -0.09%     
=========================================================
  Files                        141      141              
  Lines                      13387    13387              
  Branches                    1753     1753              
=========================================================
- Hits                        8327     8315      -12     
- Misses                      4263     4273      +10     
- Partials                     797      799       +2     

see 2 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f3d286f...d83199b. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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