Skip to content

Conversation

@leoromanovsky
Copy link

@leoromanovsky leoromanovsky commented Feb 7, 2026

Motivation

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

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, 232 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. This avoids collision from delimiters appearing in flag/subject strings — a subtle issue the simpler | separator has.

ExposureWriter caps at 1000 events per request. Matches Ruby and Python. Events beyond the cap are silently dropped. Flush happens via register_shutdown_function at request end.

Companion PRs

@datadog-datadog-prod-us1

This comment has been minimized.

@leoromanovsky leoromanovsky force-pushed the feature/ffe-feature-flagging branch from 18f7f00 to 508a8c8 Compare February 7, 2026 03:32
@codecov-commenter
Copy link

codecov-commenter commented Feb 7, 2026

Codecov Report

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

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #3630      +/-   ##
==========================================
- Coverage   62.21%   62.11%   -0.11%     
==========================================
  Files         141      141              
  Lines       13387    13387              
  Branches     1753     1753              
==========================================
- Hits         8329     8315      -14     
- Misses       4260     4273      +13     
- Partials      798      799       +1     

see 3 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 e88099a...a24b1ae. 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.

@pr-commenter
Copy link

pr-commenter bot commented Feb 7, 2026

Benchmarks [ tracer ]

Benchmark execution time: 2026-02-10 03:08:01

Comparing candidate commit a24b1ae in PR branch feature/ffe-feature-flagging with baseline commit e88099a in branch master.

Found 3 performance improvements and 40 performance regressions! Performance is the same for 150 metrics, 1 unstable metrics.

scenario:ComposerTelemetryBench/benchTelemetryParsing

  • 🟥 mem_peak [+146.904KB; +146.904KB] or [+3.676%; +3.676%]
  • 🟩 execution_time [-2.106µs; -1.094µs] or [-17.263%; -8.967%]

scenario:ContextPropagationBench/benchExtractHeaders128Bit

  • 🟥 mem_peak [+146.904KB; +146.904KB] or [+3.675%; +3.675%]

scenario:ContextPropagationBench/benchExtractHeaders128Bit-opcache

  • 🟩 execution_time [-887.327ns; -840.673ns] or [-51.330%; -48.631%]

scenario:ContextPropagationBench/benchExtractHeaders64Bit

  • 🟥 mem_peak [+146.904KB; +146.904KB] or [+3.675%; +3.675%]

scenario:ContextPropagationBench/benchExtractTraceContext128Bit

  • 🟥 mem_peak [+146.904KB; +146.904KB] or [+3.675%; +3.675%]

scenario:ContextPropagationBench/benchExtractTraceContext128Bit-opcache

  • 🟩 execution_time [-963.450ns; -830.550ns] or [-35.047%; -30.213%]

scenario:ContextPropagationBench/benchExtractTraceContext64Bit

  • 🟥 mem_peak [+146.904KB; +146.904KB] or [+3.675%; +3.675%]

scenario:ContextPropagationBench/benchInject128Bit

  • 🟥 mem_peak [+146.904KB; +146.904KB] or [+3.675%; +3.675%]

scenario:ContextPropagationBench/benchInject64Bit

  • 🟥 mem_peak [+146.904KB; +146.904KB] or [+3.675%; +3.675%]

scenario:EmptyFileBench/benchEmptyFileBaseline

  • 🟥 mem_peak [+146.912KB; +146.912KB] or [+2.838%; +2.838%]

scenario:EmptyFileBench/benchEmptyFileDdprof

  • 🟥 mem_peak [+147.683KB; +149.745KB] or [+2.849%; +2.889%]

scenario:EmptyFileBench/benchEmptyFileOverhead

  • 🟥 mem_peak [+146.912KB; +146.912KB] or [+2.838%; +2.838%]

scenario:HookBench/benchHookOverheadInstallHookOnFunction

  • 🟥 mem_peak [+146.904KB; +146.904KB] or [+3.675%; +3.675%]

scenario:HookBench/benchHookOverheadInstallHookOnMethod

  • 🟥 mem_peak [+146.904KB; +146.904KB] or [+3.675%; +3.675%]

scenario:HookBench/benchHookOverheadTraceFunction

  • 🟥 mem_peak [+146.992KB; +146.992KB] or [+3.308%; +3.308%]

scenario:HookBench/benchHookOverheadTraceMethod

  • 🟥 mem_peak [+146.990KB; +146.993KB] or [+3.261%; +3.261%]

scenario:HookBench/benchWithoutHook

  • 🟥 mem_peak [+146.904KB; +146.904KB] or [+3.676%; +3.676%]

scenario:LaravelBench/benchLaravelBaseline

  • 🟥 mem_peak [+146.944KB; +146.944KB] or [+2.838%; +2.838%]

scenario:LaravelBench/benchLaravelDdprof

  • 🟥 mem_peak [+146.220KB; +148.324KB] or [+2.820%; +2.861%]

scenario:LaravelBench/benchLaravelOverhead

  • 🟥 mem_peak [+146.944KB; +146.944KB] or [+2.838%; +2.838%]

scenario:MessagePackSerializationBench/benchMessagePackSerialization

  • 🟥 mem_peak [+146.992KB; +146.992KB] or [+3.455%; +3.455%]

scenario:PDOBench/benchPDOBaseline

  • 🟥 mem_peak [+146.992KB; +146.992KB] or [+3.643%; +3.643%]

scenario:PHPRedisBench/benchRedisBaseline

  • 🟥 mem_peak [+146.904KB; +146.904KB] or [+3.675%; +3.675%]

scenario:SamplingRuleMatchingBench/benchGlobMatching1

  • 🟥 mem_peak [+146.904KB; +146.904KB] or [+3.676%; +3.676%]

scenario:SamplingRuleMatchingBench/benchGlobMatching2

  • 🟥 mem_peak [+146.904KB; +146.904KB] or [+3.676%; +3.676%]

scenario:SamplingRuleMatchingBench/benchGlobMatching3

  • 🟥 mem_peak [+146.904KB; +146.904KB] or [+3.676%; +3.676%]

scenario:SamplingRuleMatchingBench/benchGlobMatching4

  • 🟥 mem_peak [+146.904KB; +146.904KB] or [+3.676%; +3.676%]

scenario:SamplingRuleMatchingBench/benchRegexMatching1

  • 🟥 execution_time [+78.275ns; +135.525ns] or [+6.750%; +11.687%]
  • 🟥 mem_peak [+146.904KB; +146.904KB] or [+3.675%; +3.675%]

scenario:SamplingRuleMatchingBench/benchRegexMatching2

  • 🟥 execution_time [+119.760ns; +178.040ns] or [+10.483%; +15.585%]
  • 🟥 mem_peak [+146.904KB; +146.904KB] or [+3.675%; +3.675%]

scenario:SamplingRuleMatchingBench/benchRegexMatching3

  • 🟥 execution_time [+47.380ns; +121.420ns] or [+3.974%; +10.184%]
  • 🟥 mem_peak [+146.904KB; +146.904KB] or [+3.675%; +3.675%]

scenario:SamplingRuleMatchingBench/benchRegexMatching4

  • 🟥 execution_time [+100.317ns; +145.283ns] or [+8.673%; +12.560%]
  • 🟥 mem_peak [+146.904KB; +146.904KB] or [+3.675%; +3.675%]

scenario:SpanBench/benchDatadogAPI

  • 🟥 mem_peak [+146.904KB; +146.904KB] or [+3.676%; +3.676%]

scenario:SymfonyBench/benchSymfonyBaseline

  • 🟥 mem_peak [+146.944KB; +146.944KB] or [+2.838%; +2.838%]

scenario:SymfonyBench/benchSymfonyDdprof

  • 🟥 mem_peak [+144.917KB; +147.332KB] or [+2.795%; +2.841%]

scenario:SymfonyBench/benchSymfonyOverhead

  • 🟥 mem_peak [+146.944KB; +146.944KB] or [+2.838%; +2.838%]

scenario:TraceAnnotationsBench/benchTraceAnnotationOverhead

  • 🟥 mem_peak [+146.990KB; +146.995KB] or [+3.255%; +3.256%]

scenario:TraceFlushBench/benchFlushTrace

  • 🟥 mem_peak [+146.992KB; +146.992KB] or [+3.604%; +3.604%]

scenario:TraceSerializationBench/benchSerializeTrace

  • 🟥 mem_peak [+146.992KB; +146.992KB] or [+3.522%; +3.522%]

gh-worker-dd-mergequeue-cf854d bot pushed a commit to DataDog/libdatadog that referenced this pull request Feb 9, 2026
## Motivation

Add Feature Flagging and Experimentation (FFE) support to the remote config infrastructure, enabling tracers to subscribe to FFE_FLAGS configurations via the sidecar.

WIP: php tracer changes (DataDog/dd-trace-php#3630)

## Changes

- Add `FfeFlags` variant to `RemoteConfigProduct` enum
- Add `"FFE_FLAGS"` string mapping in Display and FromStr
- Add `FfeFlagConfigurationRules = 46` to `RemoteConfigCapabilities`
- Add `FfeFlags(Vec<u8>)` variant to `RemoteConfigData` to preserve raw config bytes

## Decisions

- Raw bytes are preserved (not parsed) in `FfeFlags(Vec<u8>)` since each tracer handles evaluation with the `datadog-ffe` crate directly
- Capability bit 46 matches the server-side FFE capability definition

Co-authored-by: leo.romanovsky <leo.romanovsky@datadoghq.com>
@leoromanovsky leoromanovsky force-pushed the feature/ffe-feature-flagging branch from 1990188 to 867337e Compare February 9, 2026 19:24
@leoromanovsky leoromanovsky changed the title Feature/ffe feature flagging port ffe feature flagging sdk to php Feb 9, 2026
Implement UFCv1 evaluation engine, exposure event reporting, and
Remote Config integration for the FFE product.

- Add DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED config
- Add FFE RC product subscription and config delivery via sidecar
- Add PHP evaluator with full condition/sharding support (217/217 tests pass)
- Add exposure event writer with LRU deduplication cache
- Add Provider API with singleton pattern
- Add datadog-ffe native crate dependency and C FFI bindings
- Wire get_ffe_config/ffe_config_changed internal functions
- Fix EvaluationContext construction (no Deserialize/Default)
- Fix AssignmentValue::Json struct pattern
- Add parse_evaluation_context helper
- Resolve workspace inheritance in datadog-ffe Cargo.toml
- Remove unused imports
Remove stale git references to libdatadog v25.0.0 and use local
submodule path dependencies for datadog-ffe and datadog-ffe-ffi.
Add DDTrace\OpenFeature\DataDogProvider that implements the
OpenFeature PHP SDK's AbstractProvider interface, wrapping the
internal FFE evaluation engine.
Base FFE changes on the original pinned commit (534d009c) instead
of latest main, to avoid pulling in unrelated debugger/telemetry
changes that break existing tests.
Use master Cargo.lock as base and only add new workspace crates
(datadog-ffe and dependencies) to avoid bumping existing crates
to versions requiring edition 2024 (rmp, rmpv, time).
…DER_ENABLED

The INI config defaults to false and shadows the env var in some
SAPIs. Check getenv() first for reliability across all SAPIs.
Replace the pure PHP UFC evaluator with the native datadog-ffe
engine from libdatadog. The evaluation path is now:

  RC config bytes → ddog_ffe_load_config() → native Configuration
  evaluate() → ddog_ffe_evaluate() → native get_assignment()

- Remove Evaluator.php (482 lines of PHP reimplementation)
- Remove EvaluatorTest.php
- Add ffe_load_config, ffe_has_config, ffe_evaluate internal functions
- Update Provider to call native engine via dd_trace_internal_fn
- Add C header declarations for all ddog_ffe_* functions
Add ddog_ffe_load_config() FFI function to load UFC JSON config directly
into the Rust FFE engine without Remote Config, enabling test-time config
injection. Add 220 parametric evaluation tests driven by shared cross-tracer
JSON fixtures (merged from dd-trace-py and dd-trace-java configs) and 8
LRU cache unit tests covering eviction, promotion, and edge cases.
- Fix exposure dedup cache to match Java canonical impl: create
  ExposureCache class with length-prefixed composite keys (no collision),
  add() returns bool like Java's LRUExposureCache.add(), always promotes
  LRU position even for duplicates
- Add 12 ExposureCache tests matching Java's LRUExposureCacheTest
- Add LRUCache.put() (returns old value) and size() methods with tests
- Fix Provider to handle config removal via ffe_config_changed(), clear
  exposure cache when RC removes config
- Auto-flush exposures on request shutdown via register_shutdown_function
- Reduce ExposureWriter curl timeout to 500ms/100ms (was 5s/2s)
- Combine FFE_CONFIG + FFE_CONFIG_CHANGED into single FfeState struct
  behind one Mutex for atomic updates, add RwLock justification comment
- Remove unused datadog-ffe-ffi dependency from Cargo.toml
- Change LRUCache eviction from while to if (only one entry added)
- Clarify continue-in-switch comment in ddtrace.c ffe_evaluate handler
- Cap ExposureWriter buffer at 1000 events (matches Ruby/Python)
- Return variant key and allocation_key from Provider::evaluate()
  so callers can distinguish the variant identifier from the value
- Regenerate Cargo.lock after rebase on master
@leoromanovsky leoromanovsky force-pushed the feature/ffe-feature-flagging branch from a24b1ae to 24e4304 Compare February 11, 2026 13:41
The bootstrap stubs dd_trace_internal_fn(), so function_exists()
always returned true even without the extension. Use
extension_loaded('ddtrace') instead to correctly skip when the
real extension isn't available.
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