Skip to content

Conversation

@leoromanovsky
Copy link

@leoromanovsky leoromanovsky commented Feb 7, 2026

Motivation

PHP is the only major Datadog tracer without Feature Flagging and Experimentation (FFE) support. This PR adds it, matching the behavior of Go, Java, and Python. PHP applications can now evaluate feature flags delivered by the Datadog platform using the UFC v1 protocol.

📖 RFC - Feature Flags & Experiments in APM

Feature Flag Implementations Across dd-trace- Libraries*

Go (dd-trace-go)

Java (dd-trace-java)

Python (dd-trace-py)

Node.js (dd-trace-js)

Ruby (dd-trace-rb)

.NET (dd-trace-dotnet)

Changes

  • Add a PHP-based UFC v1 flag evaluation engine that handles targeting rules, condition operators, shard-based rollouts, and time-windowed allocations
  • Add an exposure event writer that batches and sends flag evaluation results to the Datadog Agent's EVP proxy for analytics
  • Add an LRU deduplication cache (65,536 entries) to avoid sending duplicate exposure events
  • Wire FFE into PHP's existing Remote Config pipeline so flag configurations arrive through the same sidecar mechanism used by APM Tracing and Live Debugger
  • Add C FFI bindings for the shared datadog-ffe native evaluation crate (ready for future use)
  • Register DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED as a new tracer configuration variable
  • Update the libdatadog submodule to include the FFE_FLAGS RC product and capability

System diagram

flowchart TB
    Agent["Datadog Agent\n(Remote Config endpoint)"]
    Sidecar["Sidecar\n(polls agent, shared memory)"]
    Rust["Rust: ddog_process_remote_configs()\nRemoteConfigData::FfeFlags(bytes)\nstored in FFE_CONFIG"]
    Provider["PHP: Provider.php"]
    Evaluator["Evaluator\n(UFC v1 engine)"]
    Cache["LRU Cache\n(65K dedup)"]
    Writer["Exposure Writer"]
    EVP["Agent EVP Proxy\n/evp_proxy/v2/api/v2/exposures"]
    App["PHP Application\nevaluate(flag, type, default, targetingKey, attrs)"]

    Agent -- "FFE_FLAGS configs" --> Sidecar
    Sidecar -- "SIGVTALRM → VM interrupt" --> Rust
    Rust -- "dd_trace_internal_fn('get_ffe_config')" --> Provider
    App --> Provider
    Provider --> Evaluator
    Provider --> Cache
    Provider --> Writer
    Evaluator -- "flag value + variant" --> Provider
    Cache -- "skip duplicates" --> Writer
    Writer -- "POST exposures" --> EVP
Loading

Companion PRs

Decisions

  • Reuses existing RC pipeline: No new polling mechanism. FFE configs flow through the same sidecar path as all other RC products.
  • PHP evaluator as primary path: A pure PHP evaluator handles flag resolution today. The native datadog-ffe Rust crate is linked with C FFI bindings ready, and can replace the PHP evaluator later for performance.
  • Vec<u8> for RC data: The RC layer stores raw JSON bytes rather than parsing UFC types, keeping datadog-remote-config decoupled from datadog-ffe.
  • Shard computation matches Go reference: MD5(salt + "-" + targetingKey), first 8 hex chars as big-endian uint32, modulo totalShards.
  • Attribute lookup checks context first: Falls back to targeting key for "id" only if not in attributes, matching Go behavior.

@datadog-datadog-prod-us1
Copy link

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

⚠️ Tests

Fix all issues with Cursor

⚠️ Warnings

🧪 1038 Tests failed

telemetry log for failed application of config() from com.datadog.appsec.php.integration.TelemetryTests (Datadog) (Fix with Cursor)
groovy.lang.GroovyRuntimeException: Could not find named-arg compatible constructor. Expecting one of:
com.datadog.appsec.php.TelemetryHelpers$Logs(org.apache.groovy.json.internal.LazyMap)
com.datadog.appsec.php.TelemetryHelpers$Logs()

groovy.lang.GroovyRuntimeException: Could not find named-arg compatible constructor. Expecting one of:
com.datadog.appsec.php.TelemetryHelpers$Logs(org.apache.groovy.json.internal.LazyMap)
com.datadog.appsec.php.TelemetryHelpers$Logs()
	at groovy.lang.MetaClassImpl.retrieveNamedArgCompatibleConstructor(MetaClassImpl.java:1878)
	at groovy.lang.MetaClassImpl.invokeConstructor(MetaClassImpl.java:1898)
	at groovy.lang.MetaClassImpl.invokeConstructor(MetaClassImpl.java:1679)
...

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

testSimplePushAndProcess from laravel-58-test.DDTrace\Tests\Integrations\Laravel\V5_8\QueueTest (Datadog) (Fix with Cursor)
Risky Test
phpvfscomposer://tests/vendor/phpunit/phpunit/phpunit:97
View all

ℹ️ Info

❄️ No new flaky tests detected

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

@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>
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
@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
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
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