Skip to content

feat(ffe): PHP FFE implementation#3630

Draft
leoromanovsky wants to merge 26 commits intomasterfrom
feature/ffe-feature-flagging
Draft

feat(ffe): PHP FFE implementation#3630
leoromanovsky wants to merge 26 commits intomasterfrom
feature/ffe-feature-flagging

Conversation

@leoromanovsky
Copy link
Copy Markdown

@leoromanovsky leoromanovsky commented Feb 7, 2026

Motivation

Add FFE (Feature Flags & Experimentation) 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. Per the RFC "Flag evaluations tracking for APM tracers", we also emit a feature_flag.evaluations OTel counter metric on each evaluation so flag usage can be tracked in Datadog.

Changes

Core FFE Implementation

  • 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; enforces reason=ERROR for any non-zero error code per OpenFeature spec
  • 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
  • Build: Add datadog-ffe to RUST_FILES in Makefile for PECL packaging
  • 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

OTel Flag Evaluation Metrics (new)

  • src/DDTrace/FeatureFlags/FlagEvalMetrics.php: OTel counter hook that emits feature_flag.evaluations (count=1) after each evaluation. Tags: feature_flag.key, feature_flag.provider.name=datadog, feature_flag.result.variant, feature_flag.result.reason, feature_flag.result.allocation_key, and on error: error.type (type_mismatch / parse_error / flag_not_found).
  • Error code mapping corrected: errorCodeToTag() now maps Rust constants correctly: 1=ERROR_TYPE_MISMATCH→type_mismatch, 2=ERROR_CONFIG_PARSE→parse_error, 3=ERROR_FLAG_UNRECOGNIZED→flag_not_found (was transposed in initial implementation).
  • Provider.php: Forces reason=ERROR when error_code != 0. The Rust layer returns REASON_DEFAULT for FlagUnrecognizedOrDisabled but the OpenFeature spec requires ERROR to be surfaced to callers.

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.

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.

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

FlagEvalMetrics via OTel SDK. Uses open-telemetry/api + OTLP exporter. Recorded after the type-specific evaluation method returns (not inside core evaluate()) so type mismatch errors are captured with reason=error rather than the intermediate targeting_match.

ERROR reason forced for non-zero error codes. FlagUnrecognizedOrDisabled returns REASON_DEFAULT from Rust (the default value was served) but the OpenFeature spec requires reason=ERROR whenever an error code is set. Provider.php overrides the Rust-reported reason in this case.

Test Results

Unit tests

Unit tests (local, no extension): 243 tests, 75 assertions, 1 skipped
Unit tests (Docker with extension): 243 tests, 735 assertions — all pass

System tests — all FFE tests pass (PHP 8.0-apache)

Scenario: FEATURE_FLAGGING_AND_EXPERIMENTATION
Library: [email protected]

tests/ffe/test_dynamic_evaluation.py ..                                  [ 11%]
tests/ffe/test_exposures.py ...........                                  [ 76%]
tests/ffe/test_flag_eval_metrics.py .....                                [100%]

=============== 17 passed, 1 xfailed in 0:04:12 ================

The 1 xfail is test_cross_request_dedup — expected because PHP's shared-nothing architecture prevents cross-request LRU state.

OTel metrics tests (all 5 pass)

PASSED Test_FFE_Eval_Metric_Basic        — reason=static, variant=on, allocation_key=default-allocation
PASSED Test_FFE_Eval_Metric_Count        — 5 evaluations → metric count ≥ 5
PASSED Test_FFE_Eval_Metric_Different_Flags — two flags → two metric series
PASSED Test_FFE_Eval_Metric_Error        — reason=error, error.type=flag_not_found
PASSED Test_FFE_Eval_Metric_Type_Mismatch — reason=error, error.type=type_mismatch

LOC (excluding Cargo.lock + JSON fixtures)

Category Lines
Rust FFI 323
C extension 126
PHP source (FFE + metrics) 973
PHP tests 559
Build/wiring 8
Total 1,989

Companion PRs

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

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

Tests

🎉 All green!

❄️ No new flaky tests detected
🧪 All tests passed

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 60.68% (-0.01%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 11de6c2 | Docs | Datadog PR Page | 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
Copy Markdown

codecov-commenter commented Feb 7, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 62.39%. Comparing base (237080d) to head (c77115f).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #3630      +/-   ##
==========================================
+ Coverage   62.32%   62.39%   +0.07%     
==========================================
  Files         142      142              
  Lines       13586    13586              
  Branches     1775     1775              
==========================================
+ Hits         8467     8477      +10     
+ Misses       4311     4304       -7     
+ Partials      808      805       -3     

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 237080d...c77115f. 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
Copy Markdown

pr-commenter Bot commented Feb 7, 2026

Benchmarks [ tracer ]

Benchmark execution time: 2026-04-23 02:00:54

Comparing candidate commit 11de6c2 in PR branch feature/ffe-feature-flagging with baseline commit 3df62fd in branch master.

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

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 <[email protected]>
@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
@leoromanovsky leoromanovsky force-pushed the feature/ffe-feature-flagging branch from a24b1ae to 24e4304 Compare February 11, 2026 13:41
@leoromanovsky leoromanovsky force-pushed the feature/ffe-feature-flagging branch from 2a5d688 to 1ec323b Compare February 12, 2026 00:52
@leoromanovsky leoromanovsky changed the title port ffe feature flagging sdk to php feat(ffe): PHP FFE implementation with OTel flag evaluation metrics Mar 11, 2026
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

Build:
- Add datadog-ffe to RUST_FILES in Makefile for PECL packaging
- Cargo.lock: minimal additions only (73 new crates, no gratuitous bumps)
- Bump libdatadog to ed316b638 (FFE RC support, libdatadog#1532)

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)
…OpenFeature layers

- Add null check for flag_key in ddog_ffe_evaluate Rust FFI
- Set variant in OpenFeature ResolutionDetails (was always null)
- Replace magic numbers with named constants for reason/error codes
- Fix json_decode null ambiguity in JSON type parsing with json_last_error()
- Add dropped event tracking and curl failure logging (DD_TRACE_DEBUG)
- Guard against missing curl extension in ExposureWriter flush
- Fix buildEvent docblock parameter order
Add FlagEvalMetrics OpenFeature hook that records an OTel counter
(feature_flag.evaluations) after every flag evaluation.

Attributes: feature_flag.key, feature_flag.result.variant,
feature_flag.result.reason (lowercase), error.type (on error).

Enabled only when DD_METRICS_OTEL_ENABLED=true and open-telemetry/sdk
is installed. Is a complete noop otherwise.

Register hook in DataDogProvider constructor via setHooks().
Add FlagEvalMetrics.php to _files_tracer.php autoloader.
…-zero error codes

- Fix errorCodeToTag() to match actual Rust constants:
  1=ERROR_TYPE_MISMATCH→type_mismatch, 2=ERROR_CONFIG_PARSE→parse_error,
  3=ERROR_FLAG_UNRECOGNIZED→flag_not_found (was transposed)
- Per OpenFeature spec, force reason=ERROR for any non-zero error_code.
  FlagUnrecognizedOrDisabled returns REASON_DEFAULT from the Rust layer
  but must be reported as ERROR to callers.
- FlagEvalMetrics.php: class constant visibility (private const) requires
  PHP 7.1+; drop to bare const for PHP 7.0 compatibility. The class is
  already internal so visibility adds nothing.
- metadata/supported-configurations.json: regenerate after adding
  DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED to configuration.h.
  Fixes the "Configuration Consistency" CI check.
…tements

Windows MSVC compiles PHP extensions in C89 mode (no /std:c11),
which requires all variable declarations to precede executable
statements within a block scope.

The ffe_evaluate handler had three violations:
- c_attrs and attrs_count declared after the targeting_key if-statement
- idx/key/val declared after the c_attrs = ecalloc() call
- val/var/ak declared after array_init(return_value)

Moves all declarations to the top of their respective blocks.
@leoromanovsky leoromanovsky changed the title feat(ffe): PHP FFE implementation with OTel flag evaluation metrics feat(ffe): PHP FFE implementation Mar 11, 2026
- Add feature_flag.provider.name=datadog to OTel metric attributes
- Propagate error codes to OpenFeature ResolutionDetails via withError()
- Prefer dd_trace_env_config over getenv in isFeatureFlagEnabled()
- Log non-2xx HTTP status codes in ExposureWriter debug mode
- Clarify continue-in-switch comment in ddtrace.c
Comment thread src/DDTrace/FeatureFlags/ExposureWriter.php Outdated
Comment thread src/DDTrace/FeatureFlags/FlagEvalMetrics.php Outdated
Comment thread src/DDTrace/FeatureFlags/Provider.php Outdated
Comment thread tests/phpunit.xml
@bwoebi
Copy link
Copy Markdown
Collaborator

bwoebi commented Mar 17, 2026

Also please don't split this PR. I want to ensure that the whole functionality works properly and shall be included in a single step.

Comment thread components-rs/ffe.rs Outdated
/// `config` updated but `changed` still false (or vice-versa).
///
/// A `RwLock` would be more appropriate here (many readers via `ddog_ffe_evaluate`,
/// rare writer via `store_config`), but PHP is single-threaded per process so
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

PHP is single-threaded per process

Thats only true for NTS builds of PHP, not for ZTS builds of PHP

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Can ZTS builds be something we follow up with?

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.

No. ZTS support is often structural and I don't recommend squeezing it in (and half the users are probably using ZTS).

Comment thread components-rs/ffe.rs
}

lazy_static::lazy_static! {
static ref FFE_STATE: Mutex<FfeState> = Mutex::new(FfeState {
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 FFE config truly global across services/envs etc.? Or is it computed depending on the specific target tuple (service, env, version)?
Note that every thread has its own remote config receiver, with its own target tuple.

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.

If it's the latter, you will have to carry the state in DDTRACE_G() (i.e. the thread-local state managed by the PHP runtime).

@bwoebi
Copy link
Copy Markdown
Collaborator

bwoebi commented Mar 18, 2026

I also question the OpenTelemetry metrics a bit. Like, how important are these? The vast majority of our users will not have OpenTelemetry installed or enabled.
Submission of OpenTelemetry metrics will also require synchronous flushing of these metrics (especially if these are the only metrics).

I would encourage aggregating these in tracers directly.

@leoromanovsky
Copy link
Copy Markdown
Author

I also question the OpenTelemetry metrics a bit. Like, how important are these? The vast majority of our users will not have OpenTelemetry installed or enabled. Submission of OpenTelemetry metrics will also require synchronous flushing of these metrics (especially if these are the only metrics).

I would encourage aggregating these in tracers directly.

Thanks for asking, some details below:

https://docs.google.com/document/d/1yOdu6FU3Fw-PMhNlSfqawJ5r6f3tSGomjlJVwdtqpZc/edit?tab=t.0#heading=h.yx4xd78f94da

  • importance: they are not "core functionality" but extremely useful for understanding how SDKs behave; useful when making changes to flag state
  • flush: understood; we will work these into our onboarding documents

@leoromanovsky
Copy link
Copy Markdown
Author

Also please don't split this PR. I want to ensure that the whole functionality works properly and shall be included in a single step.

Thanks! That's my intuition as well for this first landing.

@bwoebi
Copy link
Copy Markdown
Collaborator

bwoebi commented Apr 8, 2026

@leoromanovsky

I mean, in particular this section from the doc:

The agent metric pipelines team suggested using OpenTelemetry Metrics instead, which does aggregation in tracers so as to not overload the agent.

is at least for dd-trace-php not true (at least not on the scale where it would be needed). dogstatsd metrics however, are actually aggregated internally in the php tracer.

…-flagging

# Conflicts:
#	ext/configuration.h
…er PR #3630 review

Addresses all outstanding reviewer feedback from #3630:

bwoebi:
- Blocker #1: Route exposures through sidecar, zero HTTP from PHP main thread.
  Implements Rust ExposureState with 65K-entry LRU dedup + 1000-event batch
  buffer. PHP ExposureWriter uses injectable sidecarCallable routing to
  DDTrace\ffe_send_exposure() — fire-and-forget, never blocks evaluation.
- Blocker #2: Proper documented functions in ddtrace.stub.php (not
  dd_trace_internal_fn). Eight DDTrace\ffe_* functions declared with PHPDoc
  @internal: ffe_evaluate, ffe_has_config, ffe_config_changed, ffe_load_config,
  ffe_send_exposure, ffe_flush_exposures, ffe_set_service_context,
  ffe_reset_exposure_state. Each has PHP_FUNCTION impl + arginfo + registration.
- emalloc/ecalloc memory hygiene and C99 brace style consistent with existing
  dd-trace-php conventions.
- Fire-and-forget: DataDogProvider::resolveViaFfe() invokes exposureWriter->send()
  and ignores return. No exception propagates from exposure path to evaluation.
- Primitive-only attribute filtering in EvaluationContextNormalizer mirrors the
  C-side filter in ffe_evaluate (drops arrays, nulls, objects, resources).

dd-oleksii:
- Null targeting key preserved end-to-end (not coerced to empty string).
  EvaluationContextNormalizer returns [?string, array].
- do_log per-allocation hard gate: ExposureContext::fromBridgeResult() returns
  null when do_log=false; provider skips exposure send.
- OTel feature_flag.evaluations counter with OBSV-02 attributes (key, variant,
  reason, error.type, allocation_key). MetricsCounter injectable callable,
  no open-telemetry/api composer dep per CLAUDE.md. Gated on
  DD_METRICS_OTEL_ENABLED. Fires on every evaluation including errors.
- PR split into four independently-reviewable phases (roadmap in staging repo).

realFlowControl:
- ZTS per-thread FFE state: declined for v1 (NTS-only per PROJECT.md).
  Global Mutex<FfeState> is correct for NTS. Tracked as v2 requirement.

Architecture changes:
- Remove src/DDTrace/FeatureFlags/ (old in-process HTTP architecture).
  LRUCache.php, ExposureCache.php, ExposureWriter.php, Provider.php,
  FlagEvalMetrics.php — all replaced by Rust-sidecar pipeline.
- Remove tests/FeatureFlags/ (tests covered by staging PHPUnit suite).
- Add src/DDTrace/OpenFeature/ with 9 classes:
  BridgeResultMapper, ContextFlattener, DataDogProvider (rewritten),
  EvaluationContextNormalizer, ExposureContext, ExposureWriter,
  MetricsCounter, OpenFeatureLifecycleCompatibility, ProviderLifecycle.
- Add tests/OpenFeature/ with 7 test files, 143 tests.
- components-rs/ffe.rs: add ExposureState + extern C functions for
  enqueue_exposure, flush_exposures, set_service_context, reset_exposure_state,
  free_flush_result. Uses lru crate 0.12.
- components-rs/Cargo.toml: add lru = "0.12".
- composer.json: add open-feature/sdk ^2.1 as require-dev.
- src/bridge/_files_tracer.php: load new OpenFeature files, remove FeatureFlags.

Verification: 143/143 staging PHPUnit tests pass.
Two Rust compile errors from PR CI:
1. E0599: as_bytes() unresolved on Slice<'static, i8> because AsBytes trait
   not in scope. Add `use libdd_common_ffi::slice::AsBytes;` inside
   ddog_ffe_free_flush_result.
2. E0133: CharSlice::from_raw_parts is unsafe, requires unsafe block in
   ddog_ffe_flush_exposures return path.
…enFeature/

Composer PSR-4 autoload only mapped DDTrace\\ to src/api/, so tests and
host code that reference DDTrace\\OpenFeature\\BridgeResultMapper etc
failed to autoload. Add explicit DDTrace\\OpenFeature\\ mapping before
the catch-all DDTrace\\ entry so PSR-4 resolution hits it first.

Verified: vendor/bin/phpunit tests/OpenFeature passes 143/143.
Extension autoloader already handles DDTrace\\OpenFeature\\* via
_files_tracer.php include list when datadog.trace.sources_path is set,
matching how DDTrace\\OpenTelemetry\\* is wired. Keeping the explicit
psr-4 entry split OpenFeature from the OpenTelemetry/OpenTracer pattern.

Test runners must set `-d datadog.trace.sources_path=/path/to/src`
(set automatically by Makefile's TRACER_SOURCES_INI).
…scenario

PHP 7.x CI (api unit) failed because open-feature/sdk ^2.1 requires PHP ^8.0,
and composer resolves require-dev during install — breaking root vendor/bin/phpunit
setup on 7.x before any tests ran.

Move the dep to an `openfeature` entry under extra.scenarios (same pattern as
`opentelemetry1`), so PHP 7.x default scenario installs clean and 8+ CI jobs
opt in by running `composer scenario openfeature` before the featureflags
testsuite.
Suite pointed at ./FeatureFlags/ which does not exist. OpenFeature adapter
tests live at tests/OpenFeature/ (matching src/DDTrace/OpenFeature/).
… compat)

_files_tracer.php eagerly includes OpenFeature bridge files, which are
concatenated into _generated_tracer.php and autoloaded on every request.
readonly is PHP 8.1+, so PHP 8.0 parsed "public readonly ?string" and died:

  ParseError: syntax error, unexpected token "?", expecting variable

Drop readonly on promoted constructor params. Immutability was a nice-to-have
only; no code outside the class mutates these fields.

Follow-up: the OpenFeature adapter still uses other PHP 8.0+ features (match,
constructor promotion, union types), so PHP 7.x will hit parse errors once
the composer scenario fix unblocks its build. Needs lazy-load gate in
ext/autoload_php_files.c mirroring the OpenTelemetry pattern.
Wires the `featureflags` phpunit testsuite (tests/OpenFeature) into CI per bwoebi's PR #3630 feedback. Gated to PHP 8.0+ because open-feature/sdk requires PHP 8.
internal-api-stress-test.php only uses ReflectionFunction::getNumberOfRequiredParameters() on PHP 8.1+; on older versions the fuzzer starts enumerating from zero args. For the 5-required-arg ffe_send_exposure that explodes to ~15^5 permutations and exhausts PHP's 128MB memory limit before ArgumentCountError pruning runs. Skip the function on PHP < 8.1 so the randomized stress test stays green on 7.x and 8.0.
components-rs/Cargo.toml added datadog-ffe and lru in ccaad89 but the lockfile regeneration from that commit was dropped during the later master merge. Local cargo build repopulated the two entries; check them in so the lockfile matches the manifest.
@leoromanovsky leoromanovsky requested a review from a team as a code owner April 22, 2026 02:03
…+ gate)

OpenFeature adapter code uses PHP 8.0+ syntax (match, union types,
constructor promotion). Baking it into _files_tracer.php caused the
concatenated _generated_tracer.php to fail parsing on PHP 7.x, which
cascaded into autoload failures like "add DDTrace\\Transport to bridge/
_files.php" because the whole file aborted before registering earlier
classes.

Mirror the OpenTelemetry lazy-load pattern:
  - Move OpenFeature entries from _files_tracer.php into new
    _files_openfeature.php.
  - Add openfeature_is_loaded state flag (DDTRACE_G + preload save/restore).
  - In dd_perform_autoload, match ddtrace\\openfeature\\ before the
    legacy-tracer branch, gated on PHP_VERSION_ID >= 80000. On 7.x
    return NULL — the adapter is unavailable but does not break the
    rest of the tracer.
  - Wire _generated_openfeature.php into tooling/generation so the
    compiled bridge exists alongside the non-compiled _files_ fallback.
The featureflags suite loads ddtrace\openfeature\* classes via the
extension's autoloader, which reads the compiled _generated_openfeature.php
bridge. That file is produced by the "Prepare code" stage (make generate)
but the job was only listing "compile extension: debug" under needs:, so
the bridge artifact was never pulled into the test container — 142/143
tests failed with "Class DDTrace\\OpenFeature\\X not found".

Add "Prepare code" to the needs list so src/bridge/_generated_*.php ships
alongside the extension .so.
test_featureflags fails on CI with "Class OpenFeature\interfaces\provider\Reason
not found" and downstream "Class DDTrace\OpenFeature\DataDogProvider not
found" because the SDK is never installed. The bridge autoload of
_generated_openfeature.php declares DataDogProvider extends AbstractProvider;
without the SDK on the autoloader path, the parent lookup fails and the whole
bridge file aborts mid-load.

- Add tests/OpenFeature/composer.json requiring open-feature/sdk ^2.1 so the
  existing tests/%/composer.lock-php pattern rule installs it into
  tests/OpenFeature/vendor/.
- Wire test_featureflags to depend on
  tests/OpenFeature/composer.lock-php$(PHP_MAJOR_MINOR) so the install runs
  before phpunit is invoked.

The bootstrap_common walkup autoloader picks up tests/OpenFeature/vendor/
when loading OpenFeature test files, registering the SDK autoloader before
the extension autoloads the adapter bridge.
…bundle RC init flags in a struct

Addresses two dd-oleksii review threads on PR #3630:

1) components-rs/ffe.rs: change `FfeState.changed: bool` (drain-on-read) to
   `FfeState.version: u64`. Consumers track their last observed value and
   compare; multiple independent subscribers can detect transitions without
   racing each other (previous CAS semantics meant only the first reader saw
   a transition).

   - ddog_ffe_config_changed() -> bool  REMOVED
   - ddog_ffe_config_version() -> u64   ADDED
   - PHP: DDTrace\ffe_config_changed  ->  DDTrace\ffe_config_version(): int
   - ProviderLifecycle tracks $lastSeenVersion, compares on checkForConfigChange,
     and syncs on transitionToReady so the transition itself is not observed
     as another change.
   - Tests rewritten to drive the version counter.

2) components-rs/remote_config.rs: replace 4-bool positional args of
   ddog_init_remote_config(...) with a `DdogRemoteConfigFlags` #[repr(C)]
   struct. Caller in ext/sidecar.c uses C designated initializers, making
   the subscription flags self-documenting at the call site.

Also clarifies the FfeState doc comment (components-rs/ffe.rs:12-22) to note
v1 is NTS-only per PROJECT.md, with ZTS support tracked as a follow-up phase
where per-thread state moves into DDTRACE_G().
Comment thread components-rs/ffe.rs
@leoromanovsky leoromanovsky marked this pull request as draft April 22, 2026 13:00
Import ffe-system-test-data (ufc-config + 24 evaluation-case JSON files) into
tests/OpenFeature/testdata/ and add FfeFixturesTest that drives each case
through the PHP FFE bridge (DDTrace\ffe_load_config + DDTrace\ffe_evaluate).

Mirrors dd-trace-go's TestEvaluateFlag_JSONFixtures and dd-trace-java's
DDEvaluatorTest. 220 sub-cases, 1320 assertions, all pass.

Value parity is enforced strictly. Reason classification treats
{STATIC, TARGETING_MATCH, SPLIT} as interchangeable ("successful match") and
{DEFAULT, DISABLED, ERROR} as interchangeable (all produce defaultValue
through the OpenFeature provider). libdatadog and the canonical fixtures
currently classify a few cases differently (start/end-date allocations and
one multi-split flag) — value correctness holds across the split; only the
reason taxonomy drifts. Captured in the test docblock for follow-up.
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.

5 participants