diff --git a/.claude/ci/tracer-unit-tests.md b/.claude/ci/tracer-unit-tests.md index 9b7569d1b4d..1077dc6a7e0 100644 --- a/.claude/ci/tracer-unit-tests.md +++ b/.claude/ci/tracer-unit-tests.md @@ -1,5 +1,17 @@ # Tracer Unit Tests and Native Extension Tests +## Choosing a test suite + +- `tests/ext/` tests the extension's exposed API directly. Do not load `src/` + classes from these tests: packaged extension tests may run without the + repository's PHP sources or Composer dependencies. +- Tests of `src/api/` belong in the API PHPUnit suite (`tests/api/Unit/`). + OpenFeature integration tests belong in `tests/OpenFeature/`. + Using native extension functions does not make a PHP-class test an + extension-only test. PHPUnit can run `.phpt` files as well as test classes. +- When moving tests, check suite discovery and the CI PHP-version matrix. + Keep native-client coverage on PHP 7; OpenFeature's suite requires PHP 8+. + ## CI Jobs **Source:** diff --git a/Makefile b/Makefile index 653639ccb55..e4532b21f0e 100644 --- a/Makefile +++ b/Makefile @@ -1701,7 +1701,7 @@ merge_coverage_reports: API_TESTS_ROOT := ./tests/api test_api_unit: composer.lock global_test_run_dependencies - $(ENV_OVERRIDE) php $(TRACER_SOURCES_INI) vendor/bin/phpunit --config=phpunit.xml $(API_TESTS_ROOT)/Unit $(TESTS) + $(ENV_OVERRIDE) php $(TRACER_SOURCES_INI) vendor/bin/phpunit --config=phpunit.xml --test-suffix=Test.php,.phpt $(API_TESTS_ROOT)/Unit $(TESTS) # Just test it does not crash, i.e. the exit code test_internal_api_randomized: $(SO_FILE) diff --git a/components-rs/common.h b/components-rs/common.h index 9e02d299e5e..a2d16b78d6a 100644 --- a/components-rs/common.h +++ b/components-rs/common.h @@ -544,6 +544,7 @@ typedef struct ddog_FfeResult { int32_t serial_id; bool has_serial_id; bool do_log; + bool observe_full_evaluation_data; bool valid; } ddog_FfeResult; diff --git a/components-rs/ffe.rs b/components-rs/ffe.rs index 4964bc7b0b9..b3296bfb94a 100644 --- a/components-rs/ffe.rs +++ b/components-rs/ffe.rs @@ -106,6 +106,8 @@ pub struct FfeResult { pub serial_id: i32, pub has_serial_id: bool, pub do_log: bool, + // Consent belongs to the configuration snapshot used for this evaluation. + pub observe_full_evaluation_data: bool, pub valid: bool, } @@ -166,7 +168,12 @@ pub extern "C" fn ddog_ffe_evaluate( ffe::now(), ); - result_from_assignment(assignment) + let mut result = result_from_assignment(assignment); + result.observe_full_evaluation_data = state + .config + .as_ref() + .is_some_and(Configuration::observe_full_evaluation_data); + result }) } @@ -231,6 +238,7 @@ fn result_from_assignment(assignment: Result) serial_id: assignment.serial_id.unwrap_or(0), has_serial_id: assignment.serial_id.is_some(), do_log: assignment.do_log, + observe_full_evaluation_data: false, valid: true, } } @@ -257,6 +265,7 @@ fn result_from_assignment(assignment: Result) serial_id: 0, has_serial_id: false, do_log: false, + observe_full_evaluation_data: false, valid: true, } } @@ -273,6 +282,7 @@ fn invalid_result() -> FfeResult { serial_id: 0, has_serial_id: false, do_log: false, + observe_full_evaluation_data: false, valid: false, } } @@ -433,6 +443,45 @@ mod tests { clear_config(); } + #[test] + fn evaluation_captures_consent_from_its_configuration() { + setup_zend_string_functions(); + clear_config(); + let flag_key = CString::new("empty.targeting.shard.flag").unwrap(); + let evaluate = |value_type| { + ddog_ffe_evaluate( + char_slice(&flag_key), + value_type, + CharSlice::from(""), + std::ptr::null(), + 0, + ) + }; + assert!(!evaluate(TYPE_STRING).observe_full_evaluation_data); + + let mut config: serde_json::Value = + serde_json::from_str(EMPTY_TARGETING_KEY_CONFIG).unwrap(); + config["observeFullEvaluationData"] = true.into(); + let json = CString::new(config.to_string()).unwrap(); + assert!(ddog_ffe_load_config(char_slice(&json))); + let allowed = evaluate(TYPE_STRING); + assert!(allowed.valid); + assert!(allowed.observe_full_evaluation_data); + // Error observations use the same consent as successful assignments. + let mismatch = evaluate(TYPE_BOOLEAN); + assert_ne!(mismatch.error_code, ERROR_NONE); + assert!(mismatch.observe_full_evaluation_data); + + config["observeFullEvaluationData"] = false.into(); + let json = CString::new(config.to_string()).unwrap(); + assert!(ddog_ffe_load_config(char_slice(&json))); + assert!(!evaluate(TYPE_STRING).observe_full_evaluation_data); + // Reloading config must not retroactively change the prior outcome. + assert!(allowed.observe_full_evaluation_data); + clear_config(); + assert!(!evaluate(TYPE_STRING).observe_full_evaluation_data); + } + #[test] fn configuration_state_is_thread_local() { clear_config(); diff --git a/ext/sidecar.c b/ext/sidecar.c index 4fcfc9ccfde..20d4a13de32 100644 --- a/ext/sidecar.c +++ b/ext/sidecar.c @@ -126,7 +126,7 @@ static void dd_sidecar_arm_signal_transport(ddog_SidecarTransport *owner, ddog_S ddog_SignalFlush *flush = NULL; if (!datadog_ffi_try("Failed preparing sidecar signal flush", ddog_sidecar_prepare_signal_flush( - connection, (ddog_SidecarFlushOptions){.traces_and_stats = true}, &flush))) { + connection, (ddog_SidecarFlushOptions){.traces_and_stats = true, .flag_evaluations = true}, &flush))) { // Clears the stale flush while still the owner, then lets a later RINIT retry. datadog_signals_set_sidecar_flush(NULL, true); atomic_store(&dd_sidecar_signal_owner, 0); @@ -186,6 +186,20 @@ static void dd_sidecar_on_connect(ddog_SidecarTransport **transport) { } } +#ifdef DDTRACE +static void dd_sidecar_configure_ffe_transport(ddog_SidecarTransport **transport) { + // Both FFE writers share this target. Replace it when the test token changes + // so new observations use the new session and queued ones keep the old one. + const ddog_EvpProducerIdentity ffe_producer = { + .origin = DDOG_CHARSLICE_C("dd-trace-php"), + .version = DDOG_CHARSLICE_C(PHP_DDTRACE_VERSION), + }; + datadog_ffi_try("Failed configuring PHP FFE EVP transport", + ddog_sidecar_session_set_evp_transport(transport, DDOG_EVP_TRANSPORT_MODE_AGENT_ONLY, + datadog_endpoint, NULL, DDOG_CHARSLICE_C("event-platform-intake"), &ffe_producer)); +} +#endif + // Sends the identity and configuration. Needed once per transport: the transport replays them after reconnects. static void dd_sidecar_configure(ddog_SidecarTransport **transport) { #ifdef _WIN32 @@ -251,6 +265,13 @@ static void dd_sidecar_configure(ddog_SidecarTransport **transport) { ddog_endpoint_drop(otlp_metrics_endpoint); } +#ifdef DDTRACE + // Configure during ordinary connection setup, including fork recovery. + // The transport replays its configuration after reconnects. + // Remote Config keeps the fixed Agent EVP v2 route. + dd_sidecar_configure_ffe_transport(transport); +#endif + if (get_global_DD_INSTRUMENTATION_TELEMETRY_ENABLED()) { datadog_telemetry_register_services(transport); } @@ -1025,6 +1046,11 @@ bool datadog_alter_test_session_token(zval *old_value, zval *new_value, zend_str if (DATADOG_G(sidecar)) { datadog_ffi_try("Failed updating test session token", ddog_sidecar_set_test_session_token(&DATADOG_G(sidecar), dd_zend_string_to_CharSlice(Z_STR_P(new_value)))); +#ifdef DDTRACE + if (token_changed) { + dd_sidecar_configure_ffe_transport(&DATADOG_G(sidecar)); + } +#endif } #if !defined(_WIN32) && defined(DDTRACE) ddtrace_coms_set_test_session_token(Z_STRVAL_P(new_value), Z_STRLEN_P(new_value)); diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 72de20ca32f..01015c0577a 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -411,6 +411,13 @@ "default": "true" } ], + "DD_FLAGGING_EVALUATION_COUNTS_ENABLED": [ + { + "implementation": "A", + "type": "boolean", + "default": "true" + } + ], "DD_GIT_COMMIT_SHA": [ { "implementation": "B", diff --git a/src/DDTrace/OpenFeature/DataDogProvider.php b/src/DDTrace/OpenFeature/DataDogProvider.php index 122eb2ed2c3..931ba465e6a 100644 --- a/src/DDTrace/OpenFeature/DataDogProvider.php +++ b/src/DDTrace/OpenFeature/DataDogProvider.php @@ -34,16 +34,44 @@ final class DataDogProvider extends AbstractProvider private LoggerInterface $datadogLogger; private bool $warnedAboutRuntimeNotReady = false; private EvaluationMetricRecorder $metricRecorder; + private object $mainObservationScope; + private \WeakMap $fiberObservationScopes; public function __construct(?LoggerInterface $logger = null) { // Native evaluation metrics are disabled here because OpenFeature owns // the final provider outcome, including OF-level type mismatch mapping. - $this->evaluator = NativeEvaluator::create(false); + $this->mainObservationScope = (object) ['pending' => null, 'recorder' => null]; + $this->fiberObservationScopes = new \WeakMap(); + $this->evaluator = NativeEvaluator::create(false, function (array $observation): void { + $recorder = $this->observationScope()->recorder; + if ($recorder !== null) { + $recorder->capture($observation); + } else { + NativeEvaluator::submitFlagEvaluation($observation); + } + }); $this->datadogLogger = new NonThrowingLogger($logger ?: GlobalLogger::get()); $this->metricRecorder = EvaluationMetricRecorder::createDefault(); } + public function getHooks(): array + { + // The SDK asks once per invocation. A fresh hook keeps finalization + // state separate even when a before hook fails before ours runs. + return array_merge(parent::getHooks(), [new FlagEvaluationHook($this->observationScope())]); + } + + private function observationScope(): object + { + $fiber = class_exists(\Fiber::class, false) ? \Fiber::getCurrent() : null; + if ($fiber === null) { + return $this->mainObservationScope; + } + return $this->fiberObservationScopes[$fiber] + ?? ($this->fiberObservationScopes[$fiber] = (object) ['pending' => null, 'recorder' => null]); + } + /** * @internal Tests and Datadog-owned bridge adapters only. */ @@ -111,6 +139,26 @@ private function resolve( string $expectedType, mixed $defaultValue, ?EvaluationContext $context + ): ResolutionDetailsInterface { + $scope = $this->observationScope(); + $observation = $scope->pending; + if ($observation !== null) { + $observation->claim(); + } + $previous = $scope->recorder; + $scope->recorder = $observation; + try { + return $this->resolveWithObservation($flagKey, $expectedType, $defaultValue, $context); + } finally { + $scope->recorder = $previous; + } + } + + private function resolveWithObservation( + string $flagKey, + string $expectedType, + mixed $defaultValue, + ?EvaluationContext $context ): ResolutionDetailsInterface { $normalizedContext = $this->normalizeContext($context); $details = $this->evaluate($flagKey, $expectedType, $defaultValue, $normalizedContext); diff --git a/src/DDTrace/OpenFeature/FlagEvaluationHook.php b/src/DDTrace/OpenFeature/FlagEvaluationHook.php new file mode 100644 index 00000000000..ecb5aa7efb2 --- /dev/null +++ b/src/DDTrace/OpenFeature/FlagEvaluationHook.php @@ -0,0 +1,99 @@ +scope = $scope; + $this->startedAtMs = (int) (microtime(true) * 1000); + } + + public function supportsFlagValueType(string $flagValueType): bool + { + return true; + } + + public function before(HookContext $context, HookHints $hints): ?EvaluationContext + { + $this->previous = $this->scope->pending; + $this->scope->pending = $this; + return null; + } + + // The provider claims this invocation before resolving. Direct/nested + // provider calls must not overwrite its observation. + public function claim(): void + { + if ($this->scope->pending === $this) { + $this->scope->pending = $this->previous; + } + $this->previous = null; + } + + public function capture(array $observation): void + { + $this->observation = $observation; + } + + public function after(HookContext $context, ResolutionDetails $details, HookHints $hints): void + { + if ($this->observation === null) { + // No native snapshot (for example, an unavailable evaluator). + // Never infer consent from the current configuration. + $this->observation = $this->protectedObservation($context); + $this->observation[1] = $details->getVariant(); + $this->observation[5] = $details->getError() + ? $details->getError()->getResolutionErrorCode()->getValue() : null; + $this->observation[6] = $details->getVariant() === null; + } + } + + public function error(HookContext $context, \Throwable $error, HookHints $hints): void + { + $this->observation = $this->observation ?? $this->protectedObservation($context); + // Before/after failures replace the SDK result with the caller default. + // Keep evaluated consent/time, but not the successful variant/allocation. + $this->observation[1] = null; + $this->observation[2] = null; + $this->observation[5] = $error instanceof ThrowableWithResolutionError + ? $error->getResolutionError()->getResolutionErrorCode()->getValue() : 'GENERAL'; + $this->observation[6] = true; + } + + public function finally(HookContext $context, HookHints $hints): void + { + try { + if ($this->observation !== null) { + NativeEvaluator::submitFlagEvaluation($this->observation); + } + } finally { + $this->claim(); + $this->observation = null; + } + } + + private function protectedObservation(HookContext $context): array + { + return [$context->getFlagKey(), null, null, + $context->getEvaluationContext()->getTargetingKey(), [], 'GENERAL', true, + false, $this->startedAtMs, false]; + } +} diff --git a/src/api/FeatureFlags/Client.php b/src/api/FeatureFlags/Client.php index 7a8df017709..8661f289608 100644 --- a/src/api/FeatureFlags/Client.php +++ b/src/api/FeatureFlags/Client.php @@ -107,6 +107,7 @@ public function getObjectDetails($flagKey, array $defaultValue, array $context = private function evaluate($flagKey, $expectedType, $defaultValue, array $context) { $flagKey = $this->expectFlagKey($flagKey); + $targetingKeyInvalid = isset($context['targetingKey']) && !is_string($context['targetingKey']); list($targetingKey, $attributes) = $this->normalizeContext($context); $details = $this->evaluator->evaluate( @@ -114,7 +115,8 @@ private function evaluate($flagKey, $expectedType, $defaultValue, array $context $expectedType, $defaultValue, $targetingKey, - $attributes + $attributes, + $targetingKeyInvalid ); $this->warnIfRuntimeNotReady($details); diff --git a/src/api/FeatureFlags/Internal/Evaluator.php b/src/api/FeatureFlags/Internal/Evaluator.php index 132e26cbcb2..636638c275d 100644 --- a/src/api/FeatureFlags/Internal/Evaluator.php +++ b/src/api/FeatureFlags/Internal/Evaluator.php @@ -12,7 +12,8 @@ interface Evaluator * @param mixed $defaultValue * @param string|null $targetingKey * @param array $attributes + * @param bool $targetingKeyInvalid Whether evaluation coerced a non-string identity. * @return EvaluationDetails */ - public function evaluate($flagKey, $expectedType, $defaultValue, $targetingKey = null, array $attributes = array()); + public function evaluate($flagKey, $expectedType, $defaultValue, $targetingKey = null, array $attributes = array(), $targetingKeyInvalid = false); } diff --git a/src/api/FeatureFlags/Internal/NativeEvaluator.php b/src/api/FeatureFlags/Internal/NativeEvaluator.php index 929f058f733..b1984034af6 100644 --- a/src/api/FeatureFlags/Internal/NativeEvaluator.php +++ b/src/api/FeatureFlags/Internal/NativeEvaluator.php @@ -11,8 +11,9 @@ final class NativeEvaluator implements Evaluator private $mapper; private $recordMetrics; + private $flagEvaluationRecorder; - private function __construct($mapper = null, bool $recordMetrics = true) + private function __construct($mapper = null, bool $recordMetrics = true, $flagEvaluationRecorder = null) { if ($mapper !== null && !$mapper instanceof ResultMapper) { throw new \InvalidArgumentException('Expected a ResultMapper instance'); @@ -20,6 +21,7 @@ private function __construct($mapper = null, bool $recordMetrics = true) $this->mapper = $mapper ?: new ResultMapper(); $this->recordMetrics = $recordMetrics; + $this->flagEvaluationRecorder = $flagEvaluationRecorder; } public static function isAvailable() @@ -27,9 +29,10 @@ public static function isAvailable() return function_exists('DDTrace\\ffe_evaluate'); } - public static function create($recordMetrics = true) + /** @param callable|null $flagEvaluationRecorder Internal adapter for terminal OpenFeature reporting. */ + public static function create($recordMetrics = true, $flagEvaluationRecorder = null) { - return self::isAvailable() ? new self(null, $recordMetrics) : new UnavailableEvaluator(); + return self::isAvailable() ? new self(null, $recordMetrics, $flagEvaluationRecorder) : new UnavailableEvaluator(); } public function evaluate( @@ -37,13 +40,16 @@ public function evaluate( $expectedType, $defaultValue, $targetingKey = null, - array $attributes = array() + array $attributes = array(), + $targetingKeyInvalid = false ) { + $evaluationTimestampMs = (int) (microtime(true) * 1000); + $normalizedAttributes = $this->normalizeAttributes($attributes); $rawResult = \DDTrace\ffe_evaluate( $flagKey, $this->typeId($expectedType), $targetingKey, - $this->normalizeAttributes($attributes), + $normalizedAttributes, $this->recordMetrics ); @@ -53,6 +59,10 @@ public function evaluate( $details = $this->mapper->map($rawResult, $expectedType, $defaultValue); + // Native clients submit now. OpenFeature can retain this snapshot until + // its hooks finish, independently of native/OTLP metric recording. + $this->recordFlagEvaluation($flagKey, $targetingKey, $normalizedAttributes, $rawResult, $details, $evaluationTimestampMs, $targetingKeyInvalid); + // APM feature-flag span enrichment. This is the single choke point both // the native Client and the OpenFeature DataDogProvider evaluate through, // and it is only reachable with the extension loaded (UnavailableEvaluator @@ -64,6 +74,50 @@ public function evaluate( return $details; } + private function recordFlagEvaluation($flagKey, $targetingKey, array $attributes, $rawResult, $details, $evaluationTimestampMs, $targetingKeyInvalid) + { + try { + $exposure = $details->getExposureData(); + $consent = is_object($rawResult) + && isset($rawResult->observeFullEvaluationData) + && $rawResult->observeFullEvaluationData === true; + $observation = array( + $flagKey, + $details->getVariant(), + isset($exposure['allocationKey']) ? $exposure['allocationKey'] : null, + $targetingKeyInvalid ? null : $targetingKey, + $attributes, + $details->getErrorCode(), + $details->getVariant() === null && !isset($exposure['serialId']), + $consent, + $evaluationTimestampMs, + $targetingKeyInvalid + ); + if ($this->flagEvaluationRecorder !== null) { + // Keep only the native inspection budget plus one overflow + // marker while OpenFeature finishes its hooks. Scalars are + // already copied by normalizeAttributes; do not retain the + // customer's full context or any protected attributes. + $observation[4] = $consent ? array_slice($attributes, 0, 257, true) : array(); + ($this->flagEvaluationRecorder)($observation); + } else { + self::submitFlagEvaluation($observation); + } + } catch (\Throwable $ignored) { + // Best-effort telemetry must not change a flag's evaluation result. + } + } + + /** @internal Shared submission for immediate native and finalized OpenFeature observations. */ + public static function submitFlagEvaluation(array $observation) + { + try { + return \DDTrace\Internal\record_ffe_flag_evaluation(...$observation); + } catch (\Throwable $ignored) { + return false; + } + } + private function typeId($expectedType) { switch ($expectedType) { diff --git a/src/api/FeatureFlags/Internal/UnavailableEvaluator.php b/src/api/FeatureFlags/Internal/UnavailableEvaluator.php index fe1fde0c7d9..e77a0b53757 100644 --- a/src/api/FeatureFlags/Internal/UnavailableEvaluator.php +++ b/src/api/FeatureFlags/Internal/UnavailableEvaluator.php @@ -11,7 +11,7 @@ final class UnavailableEvaluator implements Evaluator const WARNING_MESSAGE = 'Datadog-backed PHP feature flag evaluation is unavailable for this client. ' . 'Returning default values because the ddtrace FFE runtime path was not available when the client was created.'; - public function evaluate($flagKey, $expectedType, $defaultValue, $targetingKey = null, array $attributes = array()) + public function evaluate($flagKey, $expectedType, $defaultValue, $targetingKey = null, array $attributes = array(), $targetingKeyInvalid = false) { return new EvaluationDetails( $defaultValue, diff --git a/src/bridge/_files_openfeature.php b/src/bridge/_files_openfeature.php index d46c20f4164..a452c3eb382 100644 --- a/src/bridge/_files_openfeature.php +++ b/src/bridge/_files_openfeature.php @@ -1,5 +1,6 @@ '2026-05-22T00:00:00Z', + 'environment' => array('name' => 'test'), + 'observeFullEvaluationData' => $consent, 'flags' => array()); + foreach (array('flag', 'inner') as $key) { + $config['flags'][$key] = array('key' => $key, 'enabled' => true, 'variationType' => 'STRING', + 'variations' => array('blue' => array('key' => 'blue', 'value' => 'blue')), + 'allocations' => array(array('key' => 'allocation', 'rules' => array(), 'doLog' => false, + 'splits' => array(array('variationKey' => 'blue', 'serialId' => 1, 'shards' => array()))))); + } + return json_encode($config); +} + +class FfeOutcomeHook extends \OpenFeature\implementation\hooks\StringHook { + public $onBefore; + public $onAfter; + public $onError; + public $onFinally; + public function before(\OpenFeature\interfaces\hooks\HookContext $context, + \OpenFeature\interfaces\hooks\HookHints $hints): ?\OpenFeature\interfaces\flags\EvaluationContext { + return $this->onBefore ? ($this->onBefore)($context) : null; + } + public function after(\OpenFeature\interfaces\hooks\HookContext $context, + \OpenFeature\interfaces\provider\ResolutionDetails $details, + \OpenFeature\interfaces\hooks\HookHints $hints): void { + if ($this->onAfter) ($this->onAfter)($context, $details); + } + public function error(\OpenFeature\interfaces\hooks\HookContext $context, \Throwable $error, + \OpenFeature\interfaces\hooks\HookHints $hints): void { + if ($this->onError) ($this->onError)(); + } + public function finally(\OpenFeature\interfaces\hooks\HookContext $context, + \OpenFeature\interfaces\hooks\HookHints $hints): void { + if ($this->onFinally) ($this->onFinally)(); + } +} + +$ffeRecords = array(); +\DDTrace\install_hook('DDTrace\\Internal\\record_ffe_flag_evaluation', function ($hook) use (&$ffeRecords) { + $ffeRecords[] = $hook->args; +}); +function ffeOutcomeClient($provider, $hook = null) { + $api = \OpenFeature\OpenFeatureAPI::getInstance(); + $api->setProvider($provider); + $client = $api->getClient('ffe-terminal-outcome'); + if ($hook) $client->addHooks($hook); + return $client; +} diff --git a/tests/Common/FeatureFlags/flag_evaluation_test_session_app.php b/tests/Common/FeatureFlags/flag_evaluation_test_session_app.php new file mode 100644 index 00000000000..0355ff1818b --- /dev/null +++ b/tests/Common/FeatureFlags/flag_evaluation_test_session_app.php @@ -0,0 +1,28 @@ + '2026-05-22T00:00:00Z', + 'environment' => array('name' => 'test'), + 'observeFullEvaluationData' => false, + 'flags' => array('flag' => array( + 'key' => 'flag', 'enabled' => true, 'variationType' => 'STRING', + 'variations' => array('blue' => array('key' => 'blue', 'value' => 'blue')), + 'allocations' => array(array( + 'key' => 'allocation', 'rules' => array(), 'doLog' => true, + 'splits' => array(array('variationKey' => 'blue', 'serialId' => 0, 'shards' => array())), + )), + )), +); +if (!\DDTrace\Testing\ffe_load_config(json_encode($config))) throw new RuntimeException('configuration rejected'); +$client = new \DDTrace\FeatureFlags\Client(); +foreach (array('session-a', 'session-b') as $token) { + ini_set('datadog.trace.agent_test_session_token', $token); + if ($client->getStringValue('flag', 'fallback', array('targetingKey' => 'subject')) !== 'blue') { + throw new RuntimeException('assignment changed'); + } + // Hand off exposures before switching sessions. Flagevaluations may + // still be queued and must retain their original destination. + if (!\DDTrace\Testing\flush_ffe_exposures()) throw new RuntimeException('exposure flush rejected'); +} +dd_trace_synchronous_flush(5000); +exit(0); diff --git a/tests/Common/FeatureFlags/flag_evaluation_wire.inc b/tests/Common/FeatureFlags/flag_evaluation_wire.inc new file mode 100644 index 00000000000..08ef6b7f631 --- /dev/null +++ b/tests/Common/FeatureFlags/flag_evaluation_wire.inc @@ -0,0 +1,205 @@ + 0.5) break; + $connection = @stream_socket_accept($listener, 0.1); + if (!$connection) { + continue; + } + stream_set_timeout($connection, 2); + $requestLine = trim(fgets($connection)); + $requests[] = $requestLine; + $headers = array(); + while (($line = fgets($connection)) !== false && trim($line) !== '') { + $parts = explode(':', $line, 2); + if (count($parts) === 2) { + $headers[strtolower($parts[0])] = trim($parts[1]); + } + } + $length = isset($headers['content-length']) ? (int) $headers['content-length'] : 0; + $body = ''; + while (strlen($body) < $length && !feof($connection)) { + $chunk = fread($connection, $length - strlen($body)); + if ($chunk === false || $chunk === '') break; + $body .= $chunk; + } + if (strpos($requestLine, '/evp_proxy/v2/api/v2/') !== false) { + $captures[] = array($requestLine, $headers, json_decode($body, true), $body); + $lastCaptureAt = microtime(true); + } + $response = '{}'; + fwrite($connection, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 2\r\nConnection: close\r\n\r\n" . $response); + fclose($connection); + } while (microtime(true) < $deadline); + $stdout .= ffeTestProcessRead($pipes[1]); + $stderr .= ffeTestProcessRead($pipes[2]); + if (!$captures) { + throw new RuntimeException('no EVP capture: ' . json_encode($requests) . ' ' . $stdout . ' ' . $stderr); + } + if ($exitCode !== 0) { + throw new RuntimeException('evaluation process failed or timed out: ' . $stdout . ' ' . $stderr); + } + $rows = array(); + $exposureCount = 0; + $identity = true; + $evaluationIdentityAbsent = true; + $protectedBytesAbsent = true; + foreach ($captures as $capture) { + list($request, $headers, $payload) = $capture; + $identity = $identity && !isset($headers['dd-api-key']) + && isset($headers['dd-evp-origin']) && $headers['dd-evp-origin'] === 'dd-trace-php' + && isset($headers['dd-evp-origin-version']) && $headers['dd-evp-origin-version'] === phpversion('ddtrace'); + if (!$identity) { + echo 'identity_headers=', json_encode(array_intersect_key($headers, + array('dd-evp-origin' => true, 'dd-evp-origin-version' => true))), "\n"; + } + if (strpos($request, '/flagevaluation ') !== false) { + $protectedBytesAbsent = $protectedBytesAbsent + && strpos($capture[3], 'ffe-protected-pii-canary@example.test') === false + && strpos($capture[3], 'ffe-context-pii-canary@example.test') === false; + if (strpos($capture[3], 'ffe-error-pii-canary@example.test') !== false) { + throw new RuntimeException('raw error text reached the payload'); + } + // Application attribution is deferred to FFL-3438/FFL-3439. + $evaluationIdentityAbsent = $evaluationIdentityAbsent && $payload['context'] === array(); + foreach ($payload['flagEvaluations'] as $row) { + $evaluationIdentityAbsent = $evaluationIdentityAbsent && !isset($row['context']['dd']); + $key = $row['flag']['key']; + if (isset($rows[$key])) { + $rows[$key]['evaluation_count'] += $row['evaluation_count']; + $rows[$key]['timestamp'] = max($rows[$key]['timestamp'], $row['timestamp']); + $rows[$key]['first_evaluation'] = min($rows[$key]['first_evaluation'], $row['first_evaluation']); + $rows[$key]['last_evaluation'] = max($rows[$key]['last_evaluation'], $row['last_evaluation']); + } else { + $rows[$key] = $row; + } + } + } else { + $context = $payload['context']; + ksort($context); + if ($context !== array('env' => 'test', 'service' => 'php-ffe-wire', 'version' => 'wire-version')) { + throw new RuntimeException('incorrect exposure context: ' . json_encode($payload['context'])); + } + $exposureCount += count($payload['exposures']); + } + } + echo 'exposure_service=php-ffe-wire', "\n"; + echo 'identity='; var_dump($identity); + echo 'evaluation_application_identity_absent='; var_dump($evaluationIdentityAbsent); + echo 'exposures='; var_dump($exposureCount); + echo 'consented_count='; var_dump($rows['consented']['evaluation_count']); + echo 'consented_subject='; var_dump($rows['consented']['targeting_key']); + echo 'consented_context='; var_dump($rows['consented']['context']['evaluation']); + echo 'protected_count='; var_dump($rows['protected']['evaluation_count']); + echo 'protected_subject='; var_dump($rows['protected']['targeting_key']); + echo 'protected_context_absent='; var_dump(!isset($rows['protected']['context']['evaluation'])); + echo 'protected_raw_bytes_absent='; var_dump($protectedBytesAbsent); + echo 'evaluation_times_preserved='; + var_dump($rows['timestamped']['evaluation_count'] === 2 + && $rows['timestamped']['timestamp'] === 1700000000456 + && $rows['timestamped']['first_evaluation'] === 1700000000123 + && $rows['timestamped']['last_evaluation'] === 1700000000456); + echo 'identity_omissions_preserve_evaluations='; + var_dump(!isset($rows['coerced']['targeting_key']) && $rows['coerced']['evaluation_count'] === 1 + && !isset($rows['missing']['targeting_key']) && $rows['missing']['evaluation_count'] === 1 + && !isset($rows['malformed']['targeting_key']) && $rows['malformed']['evaluation_count'] === 1 + && $rows['empty']['targeting_key'] === '' && $rows['empty']['evaluation_count'] === 1); + $bounded = $rows['bounded']['context']['evaluation']; + echo 'first_256_inspected='; + var_dump(count($bounded) === 255 && !isset($bounded['rejected']) + && !isset($bounded['beyond-limit']) && $bounded['field-1'] === 1 && $bounded['field-255'] === 255); + $unicode = $rows['unicode']['context']['evaluation']; + $expected = array(str_repeat('é', 256) => str_repeat('😀', 256), + 'bool' => false, 'integer' => -42, 'float' => 1.5, + 0 => 'zero-key', -1 => 'negative-key', 'empty' => ''); + ksort($unicode, SORT_STRING); + ksort($expected, SORT_STRING); + echo 'unicode_and_scalar_boundaries='; var_dump($unicode === $expected); + echo 'owned_snapshot='; + var_dump($rows['snapshot']['context']['evaluation'] === array('value' => 'before-submission')); + echo 'invalid_context_keeps_observation='; + var_dump($rows['all-invalid']['evaluation_count'] === 1 && $rows['all-invalid']['variant']['key'] === 'blue' + && $rows['all-invalid']['targeting_key'] === 'subject' && !isset($rows['all-invalid']['context']['evaluation'])); + echo 'recovered_connection_preserves_privacy='; + var_dump($rows['recovered']['evaluation_count'] === 1 + && $rows['recovered']['targeting_key'] === 'sha256_2234a4963a43ffe952df4e09b1fd0d36f8b49216e5cba4182ae4d01e67cf112a' + && !isset($rows['recovered']['context']['evaluation'])); + echo 'consented_identity_and_safe_error='; + var_dump($rows['consented-canary']['targeting_key'] === 'ffe-consented-pii-canary@example.test' + && $rows['consented-canary']['context']['evaluation']['email'] === 'ffe-consented-context-canary@example.test' + && $rows['consented-canary']['error']['message'] === 'GENERAL'); + if ($openFeature) { + $terminalErrors = true; + foreach (array('hook-before-error', 'hook-after-error') as $flag) { + $row = $rows[$flag]; + $terminalErrors = $terminalErrors && $row['evaluation_count'] === 1 + && !isset($row['variant']) && !isset($row['allocation']) + && $row['runtime_default_used'] === true && $row['error']['message'] === 'GENERAL' + && $row['targeting_key'] === 'sha256_2234a4963a43ffe952df4e09b1fd0d36f8b49216e5cba4182ae4d01e67cf112a' + && !isset($row['context']['evaluation']); + } + echo 'terminal_hook_errors='; var_dump($terminalErrors); + } + // TRACE logs can be large in CI. Inspect every byte without loading the + // entire log (and a second concatenated copy) into the PHP process. + $diagnostics = fopen($diagnosticLog, 'r'); + if ($diagnostics === false) throw new RuntimeException('cannot open diagnostic log'); + try { + echo 'diagnostics_exercised='; + var_dump(ffeTestStreamContains($diagnostics, 'FfeFlagEvaluationEvent') + || strpos($stderr, 'FfeFlagEvaluationEvent') !== false); + $piiAbsent = true; + foreach (array('ffe-protected-pii-canary@example.test', 'ffe-context-pii-canary@example.test', + 'ffe-consented-pii-canary@example.test', 'ffe-consented-context-canary@example.test', + 'ffe-error-pii-canary@example.test') as $canary) { + $piiAbsent = $piiAbsent && !ffeTestStreamContains($diagnostics, $canary) + && strpos($stderr, $canary) === false; + } + echo 'diagnostics_pii_absent='; var_dump($piiAbsent); + } finally { + fclose($diagnostics); + } +} finally { + if ($exitedAt === null) proc_terminate($process); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + fclose($listener); + @unlink($diagnosticLog); +} +?> diff --git a/tests/Common/FeatureFlags/flag_evaluation_wire_app.php b/tests/Common/FeatureFlags/flag_evaluation_wire_app.php new file mode 100644 index 00000000000..c1d9b7d77f2 --- /dev/null +++ b/tests/Common/FeatureFlags/flag_evaluation_wire_app.php @@ -0,0 +1,125 @@ +returned), "\n"; +}); + +$config = array( + 'createdAt' => '2026-05-22T00:00:00Z', + 'environment' => array('name' => 'test'), + 'observeFullEvaluationData' => true, + 'flags' => array(), +); +foreach (array('consented', 'protected', 'coerced', 'empty', 'missing', 'malformed', 'hook-after-error') as $key) { + $config['flags'][$key] = array( + 'key' => $key, 'enabled' => true, 'variationType' => 'STRING', + 'variations' => array('blue' => array('key' => 'blue', 'value' => 'blue')), + 'allocations' => array(array( + 'key' => 'allocation', 'rules' => array(), 'doLog' => in_array($key, array('consented', 'protected'), true), + 'splits' => array(array('variationKey' => 'blue', 'serialId' => 0, 'shards' => array())), + )), + ); +} +\DDTrace\Testing\ffe_load_config(json_encode($config)); +$client = new \DDTrace\FeatureFlags\Client(); +$context = array('targetingKey' => 'subject', 'attributes' => array('plan' => 'pro')); +if (getenv('PHP_FFE_WIRE_OPENFEATURE') === '1') { + require __DIR__ . '/flag_evaluation_openfeature.inc'; + $api = \OpenFeature\OpenFeatureAPI::getInstance(); + $api->setProvider(new \DDTrace\OpenFeature\DataDogProvider()); + $client = $api->getClient('php-ffe-wire'); + $context = new \OpenFeature\implementation\flags\EvaluationContext('subject', + new \OpenFeature\implementation\flags\Attributes(array('plan' => 'pro'))); +} +for ($i = 0; $i < 2; ++$i) { + if ($client->getStringValue('consented', 'fallback', $context) !== 'blue') { + throw new RuntimeException('assignment failed'); + } +} +$config['observeFullEvaluationData'] = false; +\DDTrace\Testing\ffe_load_config(json_encode($config)); +$protectedKey = 'ffe-protected-pii-canary@example.test'; +$protectedAttributes = array('email' => 'ffe-context-pii-canary@example.test'); +$context = array('targetingKey' => $protectedKey, 'attributes' => $protectedAttributes); +if (getenv('PHP_FFE_WIRE_OPENFEATURE') === '1') { + $context = new \OpenFeature\implementation\flags\EvaluationContext($protectedKey, + new \OpenFeature\implementation\flags\Attributes($protectedAttributes)); +} +if ($client->getStringValue('protected', 'fallback', $context) !== 'blue') { + throw new RuntimeException('assignment failed'); +} +if (getenv('PHP_FFE_WIRE_OPENFEATURE') === '1') { + $hook = new FfeOutcomeHook(); + $hook->onBefore = function ($context) { + if ($context->getFlagKey() === 'hook-before-error') throw new RuntimeException('ffe-error-pii-canary@example.test'); + return null; + }; + $hook->onAfter = function ($context) { + if ($context->getFlagKey() === 'hook-after-error') throw new RuntimeException('ffe-error-pii-canary@example.test'); + }; + $client->addHooks($hook); + foreach (array('hook-before-error', 'hook-after-error') as $flag) { + if ($client->getStringValue($flag, 'fallback', $context) !== 'fallback') { + throw new RuntimeException('hook failure did not return default'); + } + } +} +echo 'exposure_flush=', json_encode(\DDTrace\Testing\flush_ffe_exposures()), "\n"; +echo "evaluated\n"; +$nativeClient = new \DDTrace\FeatureFlags\Client(); +foreach (array('coerced' => 123, 'empty' => '', 'missing' => null, 'malformed' => "bad-\xff") as $flag => $key) { + if ($nativeClient->getStringValue($flag, 'fallback', array('targetingKey' => $key)) !== 'blue') { + throw new RuntimeException('identity handling changed assignment'); + } +} +// Fixed timestamps prove that submission preserves the evaluation time rather +// than reading the clock again when the C bridge or sidecar handles the event. +foreach (array(1700000000123, 1700000000456) as $timestamp) { + \DDTrace\Internal\record_ffe_flag_evaluation( + 'timestamped', 'blue', 'allocation', null, array(), null, false, false, $timestamp + ); +} +// Exercise the PHP/native snapshot boundary directly: the public evaluator +// already filters non-scalars before this boundary, but internal calls must +// still omit unsafe fields without losing the observation or invoking objects. +class FfeUnserializableContext { + public function __toString() { throw new RuntimeException('must not convert context'); } +} +$bounded = array('rejected' => str_repeat('x', 257)); +for ($i = 1; $i < 256; ++$i) $bounded['field-' . $i] = $i; +$bounded['beyond-limit'] = 'must-not-be-inspected'; +$unicode = array( + str_repeat('é', 256) => str_repeat('😀', 256), + str_repeat('é', 257) => 'overlong-key', + 'overlong-value' => str_repeat('😀', 257), + 'invalid-value' => "bad-\xff", "bad-\xff" => 'invalid-key', + 'nan' => NAN, 'infinity' => INF, + 'object' => new FfeUnserializableContext(), 'nested' => array('ignored'), + 'bool' => false, 'integer' => -42, 'float' => 1.5, + 0 => 'zero-key', -1 => 'negative-key', 'empty' => '', +); +$original = 'before-submission'; +$snapshot = array('value' => &$original); +foreach (array('bounded' => $bounded, 'unicode' => $unicode, 'snapshot' => $snapshot, + 'all-invalid' => array('bad' => NAN, 'object' => new FfeUnserializableContext())) as $flag => $attributes) { + if (!\DDTrace\Internal\record_ffe_flag_evaluation( + $flag, 'blue', 'allocation', 'subject', $attributes, null, false, true, 1700000000123 + )) throw new RuntimeException('context observation rejected'); +} +$original = 'after-submission'; +\DDTrace\Internal\record_ffe_flag_evaluation( + 'consented-canary', 'blue', null, 'ffe-consented-pii-canary@example.test', + array('email' => 'ffe-consented-context-canary@example.test'), + 'ffe-error-pii-canary@example.test', false, true, 1700000000123 +); +// Follow the existing broken-pipe telemetry test: a normal shared-sender +// operation notices the broken connection and replays its configuration. +dd_trace_internal_fn('break_sidecar_connection'); +dd_trace_internal_fn('stats_sidecar'); +if (!\DDTrace\Internal\record_ffe_flag_evaluation( + 'recovered', 'blue', null, $protectedKey, $protectedAttributes, null, false, false, 1700000000123 +)) throw new RuntimeException('submission after shared-sender recovery rejected'); +// Give the ordinary sidecar coalescer time to send while the parent captures. +// Shutdown delivery has its own lifecycle tests; this tests the live writer. +usleep(1000000); diff --git a/tests/OpenFeature/DataDogProviderTest.php b/tests/OpenFeature/DataDogProviderTest.php index 1404395c075..bb1d06dcbba 100644 --- a/tests/OpenFeature/DataDogProviderTest.php +++ b/tests/OpenFeature/DataDogProviderTest.php @@ -251,7 +251,7 @@ public function setFlagNotFound(string $flagKey): self return $this; } - public function evaluate($flagKey, $expectedType, $defaultValue, $targetingKey = null, array $attributes = []) + public function evaluate($flagKey, $expectedType, $defaultValue, $targetingKey = null, array $attributes = [], $targetingKeyInvalid = false) { $this->calls[] = [ 'flagKey' => $flagKey, diff --git a/tests/OpenFeature/OpenFeatureEvaluationMetricsTest.php b/tests/OpenFeature/OpenFeatureEvaluationMetricsTest.php index 75ca6a619bc..313acacfda7 100644 --- a/tests/OpenFeature/OpenFeatureEvaluationMetricsTest.php +++ b/tests/OpenFeature/OpenFeatureEvaluationMetricsTest.php @@ -185,7 +185,7 @@ public function setSuccess( return $this; } - public function evaluate($flagKey, $expectedType, $defaultValue, $targetingKey = null, array $attributes = []) + public function evaluate($flagKey, $expectedType, $defaultValue, $targetingKey = null, array $attributes = [], $targetingKeyInvalid = false) { if (!array_key_exists($flagKey, $this->details)) { return new EvaluationDetails( diff --git a/tests/OpenFeature/flag_evaluation_openfeature_nested.phpt b/tests/OpenFeature/flag_evaluation_openfeature_nested.phpt new file mode 100644 index 00000000000..c376e7b4377 --- /dev/null +++ b/tests/OpenFeature/flag_evaluation_openfeature_nested.phpt @@ -0,0 +1,75 @@ +--TEST-- +FFE OpenFeature observations keep nested calls and interleaved fibers separate +--SKIPIF-- + +--ENV-- +DD_METRICS_OTEL_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 +DD_REMOTE_CONFIG_ENABLED=0 +DD_TRACE_AGENT_URL=http://127.0.0.1:9 +--FILE-- +onAfter = function ($context) use ($client, $provider) { + if ($context->getFlagKey() !== 'flag') return; + $provider->resolveStringValue('inner', 'fallback'); + $client->getStringValue('inner', 'fallback'); + throw new RuntimeException('outer failed'); +}; +echo 'nested_result=', $client->getStringValue('flag', 'fallback'), "\n"; +foreach ($ffeRecords as $record) echo json_encode(array($record[0], $record[1], $record[5], $record[6])), "\n"; + +// Suspend inside provider resolution, while its recorder is active. A second +// fiber must not capture its observation in the suspended invocation's scope. +$ffeRecords = array(); +$hook->onAfter = null; +$suspend = \DDTrace\install_hook('DDTrace\\ffe_evaluate', null, function ($data) { + if ($data->args[0] === 'flag') Fiber::suspend(); +}); +$first = new Fiber(function () use ($client) { return $client->getStringValue('flag', 'fallback'); }); +$second = new Fiber(function () use ($client) { return $client->getStringValue('inner', 'fallback'); }); +$first->start(); +$second->start(); +echo 'during_resolution='; var_dump(count($ffeRecords)); +$first->resume(); +\DDTrace\remove_hook($suspend); +echo 'resolution_results=', $first->getReturn(), '/', $second->getReturn(), "\n"; +foreach ($ffeRecords as $record) echo json_encode(array($record[0], $record[1], $record[5], $record[6])), "\n"; + +$ffeRecords = array(); +$hook->onAfter = function ($context) { + if ($context->getFlagKey() === 'flag') { + Fiber::suspend(); + throw new RuntimeException('suspended evaluation failed'); + } +}; +$first = new Fiber(function () use ($client) { return $client->getStringValue('flag', 'fallback'); }); +$second = new Fiber(function () use ($client) { return $client->getStringValue('inner', 'fallback'); }); +$first->start(); +$second->start(); +echo 'while_suspended='; var_dump(count($ffeRecords)); +$first->resume(); +echo 'fiber_results=', $first->getReturn(), '/', $second->getReturn(), "\n"; +foreach ($ffeRecords as $record) echo json_encode(array($record[0], $record[1], $record[5], $record[6])), "\n"; +?> +--EXPECT-- +nested_result=fallback +["inner","blue",null,false] +["inner","blue",null,false] +["flag",null,"GENERAL",true] +during_resolution=int(1) +resolution_results=blue/blue +["inner","blue",null,false] +["flag","blue",null,false] +while_suspended=int(1) +fiber_results=fallback/blue +["inner","blue",null,false] +["flag",null,"GENERAL",true] diff --git a/tests/OpenFeature/flag_evaluation_openfeature_outcome.phpt b/tests/OpenFeature/flag_evaluation_openfeature_outcome.phpt new file mode 100644 index 00000000000..e5c32f1cca7 --- /dev/null +++ b/tests/OpenFeature/flag_evaluation_openfeature_outcome.phpt @@ -0,0 +1,67 @@ +--TEST-- +FFE OpenFeature observations match terminal hook outcomes and preserve evaluation-time metadata +--SKIPIF-- + +--ENV-- +DD_METRICS_OTEL_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 +DD_REMOTE_CONFIG_ENABLED=0 +DD_TRACE_AGENT_URL=http://127.0.0.1:9 +--FILE-- +onBefore = function () { throw new RuntimeException('before failed'); }; + } + if ($case === 'after-error') { + $hook->onAfter = function () use (&$afterStarted) { + $afterStarted = (int) (microtime(true) * 1000); + \DDTrace\Testing\ffe_load_config(ffeOutcomeConfig(false)); + usleep(20000); + throw new RuntimeException('after failed'); + }; + } + if ($case === 'error-hook-error') $hook->onError = function () { throw new RuntimeException('error hook failed'); }; + if ($case === 'finally-error') $hook->onFinally = function () { throw new RuntimeException('finally failed'); }; + $client = ffeOutcomeClient(new \DDTrace\OpenFeature\DataDogProvider(), $hook); + $details = $client->getStringDetails($case === 'provider-error' ? 'missing' : 'flag', 'fallback'); + $record = count($ffeRecords) === 1 ? $ffeRecords[0] : null; + $error = $details->getError() ? $details->getError()->getResolutionErrorCode()->getValue() : null; + echo $case, ': ', json_encode(array('value' => $details->getValue(), 'error' => $error, + 'records' => count($ffeRecords), 'variant' => $record ? $record[1] : null, + 'recordedError' => $record ? $record[5] : null, 'default' => $record ? $record[6] : null, + 'consent' => $record ? $record[7] : null)), "\n"; + if ($case === 'after-error') { + echo 'timestamp_preserved='; var_dump($record !== null && $record[8] <= $afterStarted); + echo 'allocation_cleared='; var_dump($record !== null && $record[2] === null); + } +} +\DDTrace\Testing\ffe_load_config(ffeOutcomeConfig()); +$ffeRecords = array(); +$provider = new \DDTrace\OpenFeature\DataDogProvider(); +$native = new \DDTrace\FeatureFlags\Client(); +echo 'direct_provider=', $provider->resolveStringValue('flag', 'fallback')->getValue(), "\n"; +echo 'native=', $native->getStringValue('flag', 'fallback'), "\n"; +echo 'direct_and_native_records='; var_dump(count($ffeRecords)); +?> +--EXPECT-- +success: {"value":"blue","error":null,"records":1,"variant":"blue","recordedError":null,"default":false,"consent":true} +before-error: {"value":"fallback","error":"GENERAL","records":1,"variant":null,"recordedError":"GENERAL","default":true,"consent":false} +after-error: {"value":"fallback","error":"GENERAL","records":1,"variant":null,"recordedError":"GENERAL","default":true,"consent":true} +timestamp_preserved=bool(true) +allocation_cleared=bool(true) +provider-error: {"value":"fallback","error":"FLAG_NOT_FOUND","records":1,"variant":null,"recordedError":"FLAG_NOT_FOUND","default":true,"consent":true} +error-hook-error: {"value":"fallback","error":"GENERAL","records":1,"variant":null,"recordedError":"GENERAL","default":true,"consent":false} +finally-error: {"value":"blue","error":null,"records":1,"variant":"blue","recordedError":null,"default":false,"consent":true} +direct_provider=blue +native=blue +direct_and_native_records=int(2) diff --git a/tests/OpenFeature/flag_evaluation_openfeature_wire.phpt b/tests/OpenFeature/flag_evaluation_openfeature_wire.phpt new file mode 100644 index 00000000000..042390095d3 --- /dev/null +++ b/tests/OpenFeature/flag_evaluation_openfeature_wire.phpt @@ -0,0 +1,50 @@ +--TEST-- +FFE OpenFeature evaluation counts and deduplicated exposures reach Agent EVP with privacy +--SKIPIF-- + +--ENV-- +DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 +DD_REMOTE_CONFIG_ENABLED=0 +DD_METRICS_OTEL_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_CLI_ENABLED=1 +DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true +DD_SERVICE=php-ffe-wire +DD_ENV=test +DD_VERSION=wire-version +DD_API_KEY=local-test-key-must-not-reach-agent +--FILE-- + +--EXPECT-- +exposure_service=php-ffe-wire +identity=bool(true) +evaluation_application_identity_absent=bool(true) +exposures=int(2) +consented_count=int(2) +consented_subject=string(7) "subject" +consented_context=array(1) { + ["plan"]=> + string(3) "pro" +} +protected_count=int(1) +protected_subject=string(71) "sha256_2234a4963a43ffe952df4e09b1fd0d36f8b49216e5cba4182ae4d01e67cf112a" +protected_context_absent=bool(true) +protected_raw_bytes_absent=bool(true) +evaluation_times_preserved=bool(true) +identity_omissions_preserve_evaluations=bool(true) +first_256_inspected=bool(true) +unicode_and_scalar_boundaries=bool(true) +owned_snapshot=bool(true) +invalid_context_keeps_observation=bool(true) +recovered_connection_preserves_privacy=bool(true) +consented_identity_and_safe_error=bool(true) +terminal_hook_errors=bool(true) +diagnostics_exercised=bool(true) +diagnostics_pii_absent=bool(true) diff --git a/tests/api/Unit/FeatureFlags/ClientTest.php b/tests/api/Unit/FeatureFlags/ClientTest.php index 7ea2458f419..929040e80ae 100644 --- a/tests/api/Unit/FeatureFlags/ClientTest.php +++ b/tests/api/Unit/FeatureFlags/ClientTest.php @@ -200,7 +200,7 @@ public function setSuccess( return $this; } - public function evaluate($flagKey, $expectedType, $defaultValue, $targetingKey = null, array $attributes = array()) + public function evaluate($flagKey, $expectedType, $defaultValue, $targetingKey = null, array $attributes = array(), $targetingKeyInvalid = false) { $this->calls[] = array( 'flagKey' => $flagKey, diff --git a/tests/api/Unit/FeatureFlags/flag_evaluation_final_outcome.phpt b/tests/api/Unit/FeatureFlags/flag_evaluation_final_outcome.phpt new file mode 100644 index 00000000000..b27da2ce172 --- /dev/null +++ b/tests/api/Unit/FeatureFlags/flag_evaluation_final_outcome.phpt @@ -0,0 +1,57 @@ +--TEST-- +FFE counts capture the final mapped outcome once, independently of OTLP metrics +--ENV-- +DD_METRICS_OTEL_ENABLED=false +DD_TRACE_GENERATE_ROOT_SPAN=0 +--FILE-- +args, 0, 8); +}); +$config = array( + 'createdAt' => '2026-05-22T00:00:00Z', + 'environment' => array('name' => 'test'), + 'observeFullEvaluationData' => true, + 'flags' => array('flag' => array( + 'key' => 'flag', 'enabled' => true, 'variationType' => 'STRING', + 'variations' => array('blue' => array('key' => 'blue', 'value' => 'blue')), + 'allocations' => array(array( + 'key' => 'allocation', 'rules' => array(), 'doLog' => false, + 'splits' => array(array('variationKey' => 'blue', 'serialId' => 0, 'shards' => array())), + )), + )), +); +var_dump(\DDTrace\Testing\ffe_load_config(json_encode($config))); +$client = new \DDTrace\FeatureFlags\Client(); +$context = array('targetingKey' => 'subject', 'attributes' => array('plan' => 'pro')); +var_dump($client->getStringValue('flag', 'fallback', $context)); +var_dump($client->getBooleanValue('flag', false, $context)); +var_dump($client->getStringValue('missing', 'fallback', $context)); + +// Simulate a config reload after assignment and a PHP mapping failure. +// The record must keep the assignment's consent and the mapper's final error. +$config['observeFullEvaluationData'] = false; +$reload = \DDTrace\install_hook('DDTrace\\ffe_evaluate', null, function ($hook) use ($config) { + $hook->returned->valueJson = '{invalid-json'; + \DDTrace\Testing\ffe_load_config(json_encode($config)); +}); +var_dump($client->getStringValue('flag', 'fallback', $context)); +\DDTrace\remove_hook($reload); + +// This is the evaluator mode used by OpenFeature: its own metric hook owns OTLP. +$evaluator = \DDTrace\FeatureFlags\Internal\NativeEvaluator::create(false); +var_dump($evaluator->evaluate('flag', \DDTrace\FeatureFlags\EvaluationType::STRING, + 'fallback', 'subject', array('plan' => 'pro'))->getValue()); +echo json_encode($calls, JSON_UNESCAPED_SLASHES), "\n"; +?> +--EXPECT-- +bool(true) +string(4) "blue" +bool(false) +string(8) "fallback" +string(8) "fallback" +string(4) "blue" +[["flag","blue","allocation","subject",{"plan":"pro"},null,false,true],["flag",null,null,"subject",{"plan":"pro"},"TYPE_MISMATCH",true,true],["missing",null,null,"subject",{"plan":"pro"},"FLAG_NOT_FOUND",true,true],["flag",null,null,"subject",{"plan":"pro"},"PARSE_ERROR",true,true],["flag","blue","allocation","subject",{"plan":"pro"},null,false,false]] diff --git a/tests/api/Unit/FeatureFlags/flag_evaluation_targeting_key.phpt b/tests/api/Unit/FeatureFlags/flag_evaluation_targeting_key.phpt new file mode 100644 index 00000000000..c87cf548ec0 --- /dev/null +++ b/tests/api/Unit/FeatureFlags/flag_evaluation_targeting_key.phpt @@ -0,0 +1,50 @@ +--TEST-- +FFE omits coerced targeting identity without changing evaluation or converting twice +--ENV-- +DD_METRICS_OTEL_ENABLED=false +DD_TRACE_GENERATE_ROOT_SPAN=0 +--FILE-- +conversions; + return 'subject'; + } +} +$native = $submitted = array(); +\DDTrace\install_hook('DDTrace\\ffe_evaluate', function ($hook) use (&$native) { + $native[] = $hook->args[2]; +}); +\DDTrace\install_hook('DDTrace\\Internal\\record_ffe_flag_evaluation', function ($hook) use (&$submitted) { + $submitted[] = $hook->args; +}); +$client = new \DDTrace\FeatureFlags\Client(new \DDTrace\Log\NullLogger('debug')); +$subject = new FfeTargetingSubject(); +$cases = array( + array('string', 'subject', 'subject', 'subject', false), + array('integer', 123, '123', null, true), + array('boolean', false, '', null, true), + array('object', $subject, 'subject', null, true), + array('empty', '', '', '', false), + array('null', null, null, null, false), +); +foreach ($cases as $i => $case) { + list($label, $input, $evaluatedKey, $reportedKey, $invalid) = $case; + $value = $client->getStringValue('missing', 'fallback', array('targetingKey' => $input)); + $args = $submitted[$i]; + echo $label, ': '; + var_dump($value === 'fallback' && $native[$i] === $evaluatedKey + && $args[3] === $reportedKey && isset($args[9]) && $args[9] === $invalid); +} +echo 'one conversion: '; var_dump($subject->conversions === 1); +?> +--EXPECT-- +string: bool(true) +integer: bool(true) +boolean: bool(true) +object: bool(true) +empty: bool(true) +null: bool(true) +one conversion: bool(true) diff --git a/tests/api/Unit/FeatureFlags/flag_evaluation_test_session.phpt b/tests/api/Unit/FeatureFlags/flag_evaluation_test_session.phpt new file mode 100644 index 00000000000..224c47b935a --- /dev/null +++ b/tests/api/Unit/FeatureFlags/flag_evaluation_test_session.phpt @@ -0,0 +1,87 @@ +--TEST-- +FFE writers follow a changed test-session token without rerouting earlier evaluations +--SKIPIF-- + +--ENV-- +DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 +DD_REMOTE_CONFIG_ENABLED=0 +DD_METRICS_OTEL_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_CLI_ENABLED=1 +DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true +DD_SERVICE=php-ffe-test-session +--FILE-- + 0.5) break; + $connection = @stream_socket_accept($listener, 0.1); + if (!$connection) continue; + stream_set_timeout($connection, 2); + $request = fgets($connection); + $headers = array(); + while (($line = fgets($connection)) !== false && trim($line) !== '') { + $parts = explode(':', $line, 2); + if (count($parts) === 2) $headers[strtolower(trim($parts[0]))] = trim($parts[1]); + } + $length = (int) ($headers['content-length'] ?? 0); + $body = ''; + while (strlen($body) < $length && !feof($connection)) { + $chunk = fread($connection, $length - strlen($body)); + if ($chunk === false || $chunk === '') break; + $body .= $chunk; + } + if (strpos($request, '/evp_proxy/') !== false) { + $payload = json_decode($body, true); + $token = $headers['x-datadog-test-session-token'] ?? 'missing'; + if (!isset($counts[$token])) $counts[$token] = array('evaluations' => 0, 'exposures' => 0); + if (strpos($request, '/flagevaluation ') !== false) { + foreach ($payload['flagEvaluations'] as $row) { + $counts[$token]['evaluations'] += $row['evaluation_count']; + } + } else { + $counts[$token]['exposures'] += count($payload['exposures']); + } + } + fwrite($connection, "HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}"); + fclose($connection); + } while (microtime(true) < $deadline); + $stdout .= ffeTestProcessRead($pipes[1]); + $stderr .= ffeTestProcessRead($pipes[2]); + if ($exitCode !== 0) throw new RuntimeException('child failed or timed out: ' . $stdout . $stderr); + ksort($counts); + echo json_encode($counts), "\n"; +} finally { + if ($exitCode === null) proc_terminate($process); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + fclose($listener); +} +?> +--EXPECT-- +{"session-a":{"evaluations":1,"exposures":1},"session-b":{"evaluations":1,"exposures":1}} diff --git a/tests/api/Unit/FeatureFlags/flag_evaluation_timestamp.phpt b/tests/api/Unit/FeatureFlags/flag_evaluation_timestamp.phpt new file mode 100644 index 00000000000..1517747e9fb --- /dev/null +++ b/tests/api/Unit/FeatureFlags/flag_evaluation_timestamp.phpt @@ -0,0 +1,73 @@ +--TEST-- +FFE timestamps describe evaluation entry, including defaults and mapping errors +--ENV-- +DD_METRICS_OTEL_ENABLED=false +DD_TRACE_GENERATE_ROOT_SPAN=0 +--FILE-- + '2026-05-22T00:00:00Z', + 'environment' => array('name' => 'test'), + 'observeFullEvaluationData' => true, + 'flags' => array('flag' => array( + 'key' => 'flag', 'enabled' => true, 'variationType' => 'STRING', + 'variations' => array('blue' => array('key' => 'blue', 'value' => 'blue')), + 'allocations' => array(array( + 'key' => 'allocation', 'rules' => array(), 'doLog' => false, + 'splits' => array(array('variationKey' => 'blue', 'shards' => array())), + )), + )), +); +\DDTrace\install_hook('DDTrace\\ffe_evaluate', function () use (&$enteredNativeAt) { + $enteredNativeAt = (int) (microtime(true) * 1000); + // A timestamp taken after native evaluation would be observably too late. + usleep(3000); +}, function ($hook) use (&$reloadDuringMapping, &$config) { + if ($reloadDuringMapping) { + $config['observeFullEvaluationData'] = false; + \DDTrace\Testing\ffe_load_config(json_encode($config)); + $hook->returned->valueJson = '{invalid-json'; + } +}); +\DDTrace\install_hook('DDTrace\\Internal\\record_ffe_flag_evaluation', function ($hook) use (&$submitted) { + $submitted[] = $hook->args; +}); + +$evaluator = \DDTrace\FeatureFlags\Internal\NativeEvaluator::create(false); +$cases = array( + array('no config', 'flag', 'string', 'PROVIDER_NOT_READY', false, 'fallback'), + array('success', 'flag', 'string', null, true, 'blue'), + array('type error', 'flag', 'boolean', 'TYPE_MISMATCH', true, false), + array('missing flag', 'missing', 'string', 'FLAG_NOT_FOUND', true, 'fallback'), + array('mapping error after reload', 'flag', 'string', 'PARSE_ERROR', true, 'fallback'), + array('protected after reload', 'flag', 'string', null, false, 'blue'), + array('null native result', "bad-\xff", 'string', 'PROVIDER_NOT_READY', false, 'fallback'), +); +foreach ($cases as $i => $case) { + list($label, $flag, $type, $error, $consent, $expectedValue) = $case; + if ($i === 1) \DDTrace\Testing\ffe_load_config(json_encode($config)); + $reloadDuringMapping = $i === 4; + $before = (int) (microtime(true) * 1000); + $details = $evaluator->evaluate($flag, $type, $type === 'boolean' ? false : 'fallback'); + $args = $submitted[$i]; + echo $label, ': '; + var_dump(count($submitted) === $i + 1 + && $details->getValue() === $expectedValue + && $args[5] === $error && $args[7] === $consent + && isset($args[8]) && is_int($args[8]) + && $args[8] >= $before && $args[8] <= $enteredNativeAt); +} +?> +--EXPECT-- +no config: bool(true) +success: bool(true) +type error: bool(true) +missing flag: bool(true) +mapping error after reload: bool(true) +protected after reload: bool(true) +null native result: bool(true) diff --git a/tests/api/Unit/FeatureFlags/flag_evaluation_wire.phpt b/tests/api/Unit/FeatureFlags/flag_evaluation_wire.phpt new file mode 100644 index 00000000000..264b0b8682e --- /dev/null +++ b/tests/api/Unit/FeatureFlags/flag_evaluation_wire.phpt @@ -0,0 +1,47 @@ +--TEST-- +FFE native evaluation counts and deduplicated exposures reach Agent EVP with privacy +--SKIPIF-- + +--ENV-- +DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 +DD_REMOTE_CONFIG_ENABLED=0 +DD_METRICS_OTEL_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_CLI_ENABLED=1 +DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true +DD_SERVICE=php-ffe-wire +DD_ENV=test +DD_VERSION=wire-version +DD_API_KEY=local-test-key-must-not-reach-agent +--FILE-- + +--EXPECT-- +exposure_service=php-ffe-wire +identity=bool(true) +evaluation_application_identity_absent=bool(true) +exposures=int(2) +consented_count=int(2) +consented_subject=string(7) "subject" +consented_context=array(1) { + ["plan"]=> + string(3) "pro" +} +protected_count=int(1) +protected_subject=string(71) "sha256_2234a4963a43ffe952df4e09b1fd0d36f8b49216e5cba4182ae4d01e67cf112a" +protected_context_absent=bool(true) +protected_raw_bytes_absent=bool(true) +evaluation_times_preserved=bool(true) +identity_omissions_preserve_evaluations=bool(true) +first_256_inspected=bool(true) +unicode_and_scalar_boundaries=bool(true) +owned_snapshot=bool(true) +invalid_context_keeps_observation=bool(true) +recovered_connection_preserves_privacy=bool(true) +consented_identity_and_safe_error=bool(true) +diagnostics_exercised=bool(true) +diagnostics_pii_absent=bool(true) diff --git a/tests/ext/ffe/system_test_data_evaluate.phpt b/tests/api/Unit/FeatureFlags/system_test_data_evaluate.phpt similarity index 78% rename from tests/ext/ffe/system_test_data_evaluate.phpt rename to tests/api/Unit/FeatureFlags/system_test_data_evaluate.phpt index 8292184d8f1..364bb1b79a2 100644 --- a/tests/ext/ffe/system_test_data_evaluate.phpt +++ b/tests/api/Unit/FeatureFlags/system_test_data_evaluate.phpt @@ -10,14 +10,9 @@ if (getenv('USE_ZEND_ALLOC') === '0' && !getenv('SKIP_ASAN')) { DD_TRACE_GENERATE_ROOT_SPAN=0 --FILE-- +--EXPECT-- +bool(true) +bool(true) +bool(false) diff --git a/tests/ext/ffe/evaluation_metrics_native.phpt b/tests/ext/ffe/evaluation_metrics_native.phpt index c0d88766433..6d5943c254e 100644 --- a/tests/ext/ffe/evaluation_metrics_native.phpt +++ b/tests/ext/ffe/evaluation_metrics_native.phpt @@ -53,5 +53,5 @@ old_metrics_forwarder_exists=false old_exposure_forwarder_exists=false recorded=true load=true -evaluation_without_native_metric={"valueJson":"\"blue\"","variant":"blue","allocationKey":"alloc-string","reason":0,"errorCode":0,"doLog":true,"serialId":7,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} -missing_flag_without_native_metric={"valueJson":"null","variant":null,"allocationKey":null,"reason":5,"errorCode":3,"doLog":false,"serialId":null,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} +evaluation_without_native_metric={"valueJson":"\"blue\"","variant":"blue","allocationKey":"alloc-string","reason":0,"errorCode":0,"doLog":true,"observeFullEvaluationData":false,"serialId":7,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} +missing_flag_without_native_metric={"valueJson":"null","variant":null,"allocationKey":null,"reason":5,"errorCode":3,"doLog":false,"observeFullEvaluationData":false,"serialId":null,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} diff --git a/tests/ext/ffe/flag_evaluation_flush.phpt b/tests/ext/ffe/flag_evaluation_flush.phpt new file mode 100644 index 00000000000..8865a436b7a --- /dev/null +++ b/tests/ext/ffe/flag_evaluation_flush.phpt @@ -0,0 +1,29 @@ +--TEST-- +FFE explicit and configured shutdown flushes wait for pending HTTP sends with either trace sender +--SKIPIF-- + +--ENV-- +DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 +DD_REMOTE_CONFIG_ENABLED=0 +DD_METRICS_OTEL_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_CLI_ENABLED=1 +DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true +DD_TRACE_AGENT_TIMEOUT=5000 +DD_SERVICE=php-ffe-flush +--FILE-- + +--EXPECT-- +false/explicit: bool(true) +false/internal: bool(true) +false/shutdown: bool(true) +true/explicit: bool(true) +true/internal: bool(true) +true/shutdown: bool(true) diff --git a/tests/ext/ffe/flag_evaluation_fork.phpt b/tests/ext/ffe/flag_evaluation_fork.phpt new file mode 100644 index 00000000000..35d53417cd5 --- /dev/null +++ b/tests/ext/ffe/flag_evaluation_fork.phpt @@ -0,0 +1,27 @@ +--TEST-- +FFE observations before and after fork use the existing subprocess-sidecar lifecycle without duplication +--SKIPIF-- + +--ENV-- +DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 +DD_REMOTE_CONFIG_ENABLED=0 +DD_METRICS_OTEL_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_CLI_ENABLED=1 +DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true +DD_TRACE_AGENT_TIMEOUT=5000 +DD_TRACE_SIDECAR_CONNECTION_MODE=subprocess +DD_SERVICE=php-ffe-fork +--FILE-- + +--EXPECT-- +false/fork: bool(true) +true/fork: bool(true) diff --git a/tests/ext/ffe/flag_evaluation_identity.phpt b/tests/ext/ffe/flag_evaluation_identity.phpt new file mode 100644 index 00000000000..121281e6a50 --- /dev/null +++ b/tests/ext/ffe/flag_evaluation_identity.phpt @@ -0,0 +1,74 @@ +--TEST-- +FFE evaluations remain deliverable without application identity pending sidecar attribution +--SKIPIF-- + +--ENV-- +DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 +DD_REMOTE_CONFIG_ENABLED=0 +DD_METRICS_OTEL_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_CLI_ENABLED=1 +DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true +DD_TRACE_AGENT_TIMEOUT=5000 +DD_SERVICE= +DD_ENV= +DD_VERSION= +--FILE-- + +--EXPECT-- +default=bool(true) +configured=bool(true) diff --git a/tests/ext/ffe/flag_evaluation_kill_switch.phpt b/tests/ext/ffe/flag_evaluation_kill_switch.phpt new file mode 100644 index 00000000000..0c5a354f89b --- /dev/null +++ b/tests/ext/ffe/flag_evaluation_kill_switch.phpt @@ -0,0 +1,17 @@ +--TEST-- +FFE count kill switch disables submission without disabling OTLP metrics +--ENV-- +DD_FLAGGING_EVALUATION_COUNTS_ENABLED=false +DD_METRICS_OTEL_ENABLED=true +--FILE-- + 'pro'), null, false, true, 1700000000123 +)); +var_dump(\DDTrace\Internal\record_ffe_evaluation_metric( + 'flag', 'blue', 'SPLIT', null, 'allocation' +)); +?> +--EXPECT-- +bool(false) +bool(true) diff --git a/tests/ext/ffe/flag_evaluation_rejection_fork.phpt b/tests/ext/ffe/flag_evaluation_rejection_fork.phpt new file mode 100644 index 00000000000..03af38aa541 --- /dev/null +++ b/tests/ext/ffe/flag_evaluation_rejection_fork.phpt @@ -0,0 +1,100 @@ +--TEST-- +FFE fork children report only their own rejected observations +--SKIPIF-- + +--ENV-- +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_CLI_ENABLED=1 +DD_INSTRUMENTATION_TELEMETRY_ENABLED=1 +DD_REMOTE_CONFIG_ENABLED=0 +DD_METRICS_OTEL_ENABLED=0 +DD_TRACE_SIDECAR_CONNECTION_MODE=subprocess +DD_TRACE_LOG_LEVEL=off +--INI-- +datadog.trace.agent_url="file://{PWD}/tmp/ffe-rejection-fork/" +--FILE-- +service = $service; + $span->env = 'test'; + \DDTrace\close_span(); + dd_trace_serialize_closed_spans(); +} + +function rejectEvaluation() { + if (\DDTrace\Internal\record_ffe_flag_evaluation( + '', 'blue', null, null, array(), null, false, false, 1700000000123 + )) throw new RuntimeException('invalid observation was accepted'); +} + +$parentService = 'ffe-rejection-parent-' . getmypid(); +setApplication($parentService); +rejectEvaluation(); +rejectEvaluation(); +$pid = pcntl_fork(); +if ($pid === -1) throw new RuntimeException('fork failed'); +if ($pid === 0) { + setApplication('ffe-rejection-child-' . getmypid()); + rejectEvaluation(); + dd_trace_internal_fn('finalize_telemetry'); + exit(0); +} +pcntl_waitpid($pid, $status); +if (!pcntl_wifexited($status) || pcntl_wexitstatus($status) !== 0) { + throw new RuntimeException('child failed'); +} +// The pcntl integration starts a new root span for the fork. Restore the +// parent's named telemetry application before draining its rejection counters. +setApplication($parentService); +dd_trace_internal_fn('finalize_telemetry'); + +// Separate applications expose inherited counters instead of letting a stopped +// telemetry worker silently discard the child's data. Wait for both to finish. +$services = array($parentService => 'parent', 'ffe-rejection-child-' . $pid => 'child'); +$path = __DIR__ . '/tmp/ffe-rejection-fork/telemetry-*.json'; +$deadline = microtime(true) + 30; +do { + usleep(10000); + $counts = array('parent' => 0, 'child' => 0); + $closed = array(); + // Directory capture publishes complete requests atomically and avoids + // inheriting the single-file recorder's locked mutex across a fork. + foreach (glob($path) as $file) { + $envelope = json_decode(file_get_contents($file), true); + $service = $envelope['application']['service_name'] ?? ''; + if (!isset($services[$service])) continue; + $batch = $envelope['request_type'] === 'message-batch' ? $envelope['payload'] : array($envelope); + foreach ($batch as $event) { + if ($event['request_type'] === 'app-closing') $closed[$service] = true; + if ($event['request_type'] !== 'generate-metrics') continue; + foreach ($event['payload']['series'] as $series) { + if ($series['metric'] !== 'flagevaluation.rows.dropped') continue; + if ($series['namespace'] !== 'tracers' || $series['tags'] !== array('reason:serialization_error')) { + throw new RuntimeException('unexpected drop metric dimensions'); + } + foreach ($series['points'] as $point) $counts[$services[$service]] += $point[1]; + } + } + } +} while (count($closed) !== 2 && microtime(true) < $deadline); +echo 'completed='; var_dump(count($closed) === 2); +echo 'drop_counts=', json_encode($counts), "\n"; +?> +--EXPECT-- +completed=bool(true) +drop_counts={"parent":2,"child":1} +--CLEAN-- + +--ENV-- +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_CLI_ENABLED=1 +DD_INSTRUMENTATION_TELEMETRY_ENABLED=1 +DD_REMOTE_CONFIG_ENABLED=0 +DD_METRICS_OTEL_ENABLED=0 +DD_TRACE_LOG_LEVEL=off +--INI-- +datadog.trace.agent_url="file://{PWD}/ffe-rejection-metrics.out" +--FILE-- +service = $service; +$span->env = 'test'; +\DDTrace\close_span(); +dd_trace_serialize_closed_spans(); + +// Invalid required metadata cannot be encoded. Optional identity loss is not +// a dropped evaluation; native telemetry accounts for its omission instead. +var_dump(recordEvaluation("invalid-\xFF")); +var_dump(recordEvaluation("invalid-\xFF")); +var_dump(recordEvaluation('')); +var_dump(recordEvaluation('valid', "invalid-\xFF")); +var_dump(recordEvaluation(str_repeat('x', 10 * 1024 * 1024))); +var_dump(recordEvaluation('degraded', str_repeat('x', 10 * 1024 * 1024))); +if (ini_set('datadog.flagging_evaluation_counts_enabled', '0') === false) { + throw new RuntimeException('could not disable flagevaluation submission'); +} +var_dump(recordEvaluation("disabled-\xFF")); +ini_set('datadog.flagging_evaluation_counts_enabled', '1'); + +// Evaluation must not reconnect. Wait for the existing broken-connection +// helper to take effect, then use ordinary lifecycle traffic to recover. +dd_trace_internal_fn('break_sidecar_connection'); +$deadline = microtime(true) + 5; +do { + usleep(10000); + $accepted = recordEvaluation('after-disconnect'); +} while ($accepted && microtime(true) < $deadline); +var_dump($accepted); +var_dump(recordEvaluation('still-disconnected')); +var_dump(recordEvaluation('still-disconnected')); +dd_trace_internal_fn('stats_sidecar'); +var_dump(recordEvaluation('recovered')); +// Finish accepted EVP writes before CLEAN removes the shared capture file. +dd_trace_synchronous_flush(); + +// Finalize again for a fresh telemetry application. Reusing the already-stopped +// worker could hide duplicate points by discarding them, rather than proving +// that the PHP counters were drained. +dd_trace_internal_fn('finalize_telemetry'); +$secondService = $service . '-second'; +$span = \DDTrace\start_span(); +$span->service = $secondService; +$span->env = 'test'; +\DDTrace\close_span(); +dd_trace_serialize_closed_spans(); +dd_trace_internal_fn('finalize_telemetry'); +$path = __DIR__ . '/ffe-rejection-metrics.out'; +$deadline = microtime(true) + 30; +do { + usleep(10000); + $counts = array(); + $closed = array(); + $raw = is_file($path) ? file_get_contents($path) : ''; + foreach (explode("\n", $raw) as $line) { + $envelope = json_decode($line, true); + $application = $envelope['application']['service_name'] ?? ''; + if ($application !== $service && $application !== $secondService) continue; + $batch = $envelope['request_type'] === 'message-batch' ? $envelope['payload'] : array($envelope); + foreach ($batch as $event) { + if ($event['request_type'] === 'app-closing') $closed[$application] = true; + if ($event['request_type'] !== 'generate-metrics') continue; + foreach ($event['payload']['series'] as $series) { + if ($series['metric'] !== 'flagevaluation.rows.dropped') continue; + $key = $series['namespace'] . ':' . implode(',', $series['tags']); + foreach ($series['points'] as $point) { + $counts[$key] = ($counts[$key] ?? 0) + $point[1]; + } + } + } + } +} while (count($closed) !== 2 && microtime(true) < $deadline); +ksort($counts); +echo 'completed='; var_dump(count($closed) === 2); +echo 'drop_counts=', json_encode($counts), "\n"; +echo 'diagnostics_pii_absent='; +var_dump(strpos($raw, 'rejection-pii-canary@example.test') === false); +?> +--EXPECT-- +bool(false) +bool(false) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(false) +bool(false) +bool(false) +bool(true) +completed=bool(true) +drop_counts={"tracers:reason:payload_limit":1,"tracers:reason:serialization_error":3,"tracers:reason:unavailable":3} +diagnostics_pii_absent=bool(true) +--CLEAN-- + +--ENV-- +DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 +DD_REMOTE_CONFIG_ENABLED=0 +DD_METRICS_OTEL_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_CLI_ENABLED=1 +DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true +DD_TRACE_AGENT_TIMEOUT=5000 +DD_SERVICE=php-ffe-signal-flush +DD_TRACE_FORCE_FLUSH_ON_SIGTERM=1 +DD_TRACE_FORCE_FLUSH_ON_SIGINT=1 +VALGRIND_OPTS=--leak-check=no +--FILE-- + +--EXPECT-- +false/sigterm: bool(true) +false/sigint: bool(true) +true/sigterm: bool(true) +true/sigint: bool(true) diff --git a/tests/ext/ffe/flag_evaluation_threads.phpt b/tests/ext/ffe/flag_evaluation_threads.phpt new file mode 100644 index 00000000000..d33bf70f8fb --- /dev/null +++ b/tests/ext/ffe/flag_evaluation_threads.phpt @@ -0,0 +1,32 @@ +--TEST-- +FFE flush from the main thread includes worker-only evaluations +--SKIPIF-- + +--ENV-- +DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 +DD_REMOTE_CONFIG_ENABLED=0 +DD_METRICS_OTEL_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_CLI_ENABLED=1 +DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true +DD_TRACE_AGENT_TIMEOUT=5000 +DD_SERVICE=php-ffe-threads +--FILE-- + +--EXPECT-- +false/worker: bool(true) +false/explicit: bool(true) +false/internal: bool(true) +false/shutdown: bool(true) +true/worker: bool(true) +true/explicit: bool(true) +true/internal: bool(true) +true/shutdown: bool(true) diff --git a/tests/ext/ffe/native_bridge_evaluate.phpt b/tests/ext/ffe/native_bridge_evaluate.phpt index f9fdb8761be..c5e80dc0e90 100644 --- a/tests/ext/ffe/native_bridge_evaluate.phpt +++ b/tests/ext/ffe/native_bridge_evaluate.phpt @@ -132,14 +132,14 @@ has_config_before=false native_exposure_flush_exists=true internal_exposure_flush_exists=false old_exposure_forwarder_exists=false -provider_not_ready={"valueJson":"null","variant":null,"allocationKey":null,"reason":5,"errorCode":6,"doLog":false,"serialId":null,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} +provider_not_ready={"valueJson":"null","variant":null,"allocationKey":null,"reason":5,"errorCode":6,"doLog":false,"observeFullEvaluationData":false,"serialId":null,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} load=true has_config_after=true -success={"valueJson":"\"blue\"","variant":"blue","allocationKey":"alloc-string","reason":0,"errorCode":0,"doLog":true,"serialId":7,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} +success={"valueJson":"\"blue\"","variant":"blue","allocationKey":"alloc-string","reason":0,"errorCode":0,"doLog":true,"observeFullEvaluationData":false,"serialId":7,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} object_success_value={"enabled":true,"threshold":2} object_success_metadata={"variant":"json-a","allocation_key":"alloc-json","reason":0,"error_code":0,"do_log":true} -numeric_attribute_key={"valueJson":"\"numeric-attribute-name\"","variant":"numeric-key","allocationKey":"alloc-numeric-attribute","reason":2,"errorCode":0,"doLog":true,"serialId":null,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} -empty_targeting_key={"valueJson":"\"empty-targeting-key\"","variant":"empty-target","allocationKey":"alloc-empty-targeting-key","reason":3,"errorCode":0,"doLog":true,"serialId":null,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} -missing={"valueJson":"null","variant":null,"allocationKey":null,"reason":5,"errorCode":3,"doLog":false,"serialId":null,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} -type_mismatch={"valueJson":"null","variant":null,"allocationKey":null,"reason":5,"errorCode":1,"doLog":false,"serialId":null,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} -parse_error={"valueJson":"null","variant":null,"allocationKey":null,"reason":5,"errorCode":2,"doLog":false,"serialId":null,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} +numeric_attribute_key={"valueJson":"\"numeric-attribute-name\"","variant":"numeric-key","allocationKey":"alloc-numeric-attribute","reason":2,"errorCode":0,"doLog":true,"observeFullEvaluationData":false,"serialId":null,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} +empty_targeting_key={"valueJson":"\"empty-targeting-key\"","variant":"empty-target","allocationKey":"alloc-empty-targeting-key","reason":3,"errorCode":0,"doLog":true,"observeFullEvaluationData":false,"serialId":null,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} +missing={"valueJson":"null","variant":null,"allocationKey":null,"reason":5,"errorCode":3,"doLog":false,"observeFullEvaluationData":false,"serialId":null,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} +type_mismatch={"valueJson":"null","variant":null,"allocationKey":null,"reason":5,"errorCode":1,"doLog":false,"observeFullEvaluationData":false,"serialId":null,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} +parse_error={"valueJson":"null","variant":null,"allocationKey":null,"reason":5,"errorCode":2,"doLog":false,"observeFullEvaluationData":false,"serialId":null,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} diff --git a/tests/ext/ffe/remote_config_lifecycle.phpt b/tests/ext/ffe/remote_config_lifecycle.phpt index 1a78212d634..801504bad74 100644 --- a/tests/ext/ffe/remote_config_lifecycle.phpt +++ b/tests/ext/ffe/remote_config_lifecycle.phpt @@ -83,7 +83,7 @@ reset_request_replayer(); before=false loaded=true has_config_after_add=true -success={"valueJson":"\"blue\"","variant":"blue","allocationKey":"alloc-string","reason":0,"errorCode":0,"doLog":true,"serialId":7,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} +success={"valueJson":"\"blue\"","variant":"blue","allocationKey":"alloc-string","reason":0,"errorCode":0,"doLog":true,"observeFullEvaluationData":false,"serialId":7,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} removed=true has_config_after_remove=false version_increased=true diff --git a/tests/ext/ffe/stubs/ffe_process.inc b/tests/ext/ffe/stubs/ffe_process.inc new file mode 100644 index 00000000000..3ded4e0d58f --- /dev/null +++ b/tests/ext/ffe/stubs/ffe_process.inc @@ -0,0 +1,45 @@ + true)); + if (!is_resource($process)) throw new RuntimeException('cannot launch PHP child'); + $pipes[1] = fopen($logs[0], 'r'); + $pipes[2] = fopen($logs[1], 'r'); + return $process; +} + +function ffeTestProcessRead($stream) +{ + // Clear PHP's EOF state before checking whether the child appended output. + if (fseek($stream, ftell($stream)) !== 0) throw new RuntimeException('cannot read child output'); + return stream_get_contents($stream); +} + +function ffeTestStreamContains($stream, $needle) +{ + if (!rewind($stream)) throw new RuntimeException('cannot rewind diagnostic log'); + $overlap = strlen($needle) - 1; + $tail = ''; + while (!feof($stream)) { + $chunk = fread($stream, 8192); + if ($chunk === false) throw new RuntimeException('cannot read diagnostic log'); + $chunk = $tail . $chunk; + if (strpos($chunk, $needle) !== false) return true; + // Retain enough bytes to find a marker split across two reads. + $tail = $overlap > 0 ? substr($chunk, -$overlap) : ''; + } + return false; +} diff --git a/tests/ext/ffe/stubs/flag_evaluation_flush.inc b/tests/ext/ffe/stubs/flag_evaluation_flush.inc new file mode 100644 index 00000000000..35bc70ec219 --- /dev/null +++ b/tests/ext/ffe/stubs/flag_evaluation_flush.inc @@ -0,0 +1,99 @@ + 0.5) break; + $connection = @stream_socket_accept($listener, 0.1); + if (!$connection) continue; + stream_set_timeout($connection, 2); + $request = fgets($connection); + $length = 0; + while (($line = fgets($connection)) !== false && trim($line) !== '') { + if (stripos($line, 'Content-Length:') === 0) $length = (int) substr($line, 15); + } + $body = ''; + while (strlen($body) < $length && !feof($connection)) { + $chunk = fread($connection, $length - strlen($body)); + if ($chunk === false || $chunk === '') break; + $body .= $chunk; + } + if (strpos($request, '/flagevaluation ') !== false) { + $lastCaptureAt = microtime(true); + $payload = json_decode($body, true); + foreach ($payload['flagEvaluations'] as $row) { + $key = $row['flag']['key']; + $counts[$key] = (isset($counts[$key]) ? $counts[$key] : 0) + $row['evaluation_count']; + } + } + if (strpos($request, '/flagevaluation ') !== false && !$released) { + $released = true; + fwrite($pipes[0], "flush\n"); + fflush($pipes[0]); + // Hold the HTTP response while the child tries to flush. + // A missing FFE flush returns/exits during this interval. + $blocked = true; + $holdUntil = microtime(true) + 0.3; + do { + $stdout .= ffeTestProcessRead($pipes[1]); + $status = proc_get_status($process); + if (strpos($stdout, 'returned') !== false || !$status['running']) { + $blocked = false; + if (!$status['running']) { + $exitCode = $status['exitcode']; + $exitedAt = microtime(true); + } + break; + } + usleep(1000); + } while (microtime(true) < $holdUntil); + } + fwrite($connection, "HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}"); + fclose($connection); + } while (microtime(true) < $deadline); + $stdout .= ffeTestProcessRead($pipes[1]); + $stderr .= ffeTestProcessRead($pipes[2]); + if ($exitCode !== 0) throw new RuntimeException('child failed or timed out: ' . $stdout . $stderr); + $expected = $mode === 'fork' ? array('flush-child' => 1, 'flush-flag' => 1) : array('flush-flag' => 1); + ksort($counts); + echo $sidecarTraceSender, '/', $mode, ': '; + $terminates = in_array($mode, array('shutdown', 'sigterm', 'sigint'), true); + var_dump($counts === $expected && $blocked && ($terminates || strpos($stdout, 'returned') !== false)); + } finally { + if ($exitCode === null) proc_terminate($process); + foreach ($pipes as $pipe) fclose($pipe); + proc_close($process); + fclose($listener); + } + } +} +?> diff --git a/tests/ext/ffe/stubs/flag_evaluation_flush_app.php b/tests/ext/ffe/stubs/flag_evaluation_flush_app.php new file mode 100644 index 00000000000..2fd00fb131c --- /dev/null +++ b/tests/ext/ffe/stubs/flag_evaluation_flush_app.php @@ -0,0 +1,43 @@ +run(function ($channel) { + if (!\DDTrace\Internal\record_ffe_flag_evaluation( + 'flush-flag', 'blue', null, 'worker-subject', array(), null, false, false, 1700000000123 + )) throw new RuntimeException('worker submission rejected'); + if ($channel->recv() === 'flush') dd_trace_synchronous_flush(); + return true; +}, array($channel)); + +// The capture server releases this only after it is holding an HTTP send. +if (trim(fgets(STDIN)) !== 'flush') throw new RuntimeException('missing flush barrier'); +$mode = getenv('PHP_FFE_FLUSH_MODE'); +$channel->send($mode === 'worker' ? 'flush' : 'finish'); +if (!$future->value()) throw new RuntimeException('worker did not finish'); +$runtime->close(); +switch ($mode) { + case 'explicit': + dd_trace_synchronous_flush(); + break; + case 'internal': + dd_trace_internal_fn('synchronous_flush'); + break; + case 'shutdown': + exit(0); +} +echo "returned\n"; diff --git a/tests/ext/sidecar_shutdown_flush_error.phpt b/tests/ext/sidecar_shutdown_flush_error.phpt new file mode 100644 index 00000000000..44476302e23 --- /dev/null +++ b/tests/ext/sidecar_shutdown_flush_error.phpt @@ -0,0 +1,27 @@ +--TEST-- +Shutdown flush releases the error returned by a broken sidecar connection +--ENV-- +DD_TRACE_CLI_ENABLED=1 +DD_TRACE_SIDECAR_TRACE_SENDER=1 +DD_TRACE_FORCE_FLUSH_ON_SHUTDOWN=1 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_INSTRUMENTATION_TELEMETRY_ENABLED=0 +DD_REMOTE_CONFIG_ENABLED=0 +DD_METRICS_OTEL_ENABLED=0 +DD_TRACE_LOG_LEVEL=off +--FILE-- + +--EXPECT-- +bool(true) +bool(true) diff --git a/tests/internal-api-stress-test.php b/tests/internal-api-stress-test.php index d696d0226af..6cabfc588b9 100644 --- a/tests/internal-api-stress-test.php +++ b/tests/internal-api-stress-test.php @@ -141,6 +141,25 @@ function call_function(ReflectionFunction $function) global $minFunctionArgs; print date('Y-m-d H:i:s') . " Executing: {$function->name}\n"; + if ($function->name === 'DDTrace\\Internal\\record_ffe_flag_evaluation') { + // Ten parameters make the exhaustive garbage product too large on + // older PHP. Exercise a valid call and each argument's garbage values + // independently, keeping the other arguments valid so parsing reaches it. + $arguments = ['flag', 'variant', 'allocation', 'subject', [], null, false, false, 1700000000123, false]; + $function->invokeArgs($arguments); + foreach ($arguments as $position => $unused) { + foreach (generate_garbage() as $garbage) { + $invocation = $arguments; + $invocation[$position] = $garbage; + try { + $function->invokeArgs($invocation); + } catch (TypeError $e) { + } + } + } + return; + } + $i = PHP_VERSION_ID >= 80100 ? $function->getNumberOfRequiredParameters() : ($minFunctionArgs[$function->name] ?? 0); $invocations = $i == 0 ? [[]] : []; $invocationTypeMap = []; diff --git a/tracer/configuration.h b/tracer/configuration.h index ca83da9d11e..180b1e4553e 100644 --- a/tracer/configuration.h +++ b/tracer/configuration.h @@ -164,6 +164,7 @@ CONFIG(BOOL, DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT, "false") \ CONFIG(BOOL, DD_TRACE_STATS_COMPUTATION_ENABLED, "false") \ CONFIG(BOOL, DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED, "false") \ + CONFIG(BOOL, DD_FLAGGING_EVALUATION_COUNTS_ENABLED, "true") \ DD_INTEGRATIONS #ifndef DDTRACE_CONFIGURATION diff --git a/tracer/ddtrace.c b/tracer/ddtrace.c index 27c95e8649c..9123c9059e4 100644 --- a/tracer/ddtrace.c +++ b/tracer/ddtrace.c @@ -391,6 +391,9 @@ void ddtrace_mshutdown() { ddtrace_integrations_mshutdown(); + ddog_SidecarFlushOptions flush_options = { + .traces_and_stats = true, .telemetry = true, .flag_evaluations = true + }; #ifndef _WIN32 if (!get_global_DD_TRACE_SIDECAR_TRACE_SENDER()) { ddtrace_coms_mshutdown(); @@ -400,10 +403,10 @@ void ddtrace_mshutdown() { * it is safe to free the cached proxy env strings for ASan. */ ddtrace_coms_mshutdown_proxy_env(); } - } else /* ! part of the if outside the ifdef */ + } #endif if (get_global_DD_TRACE_FORCE_FLUSH_ON_SHUTDOWN() && DATADOG_G(sidecar)) { - ddog_sidecar_flush(&DATADOG_G(sidecar), (ddog_SidecarFlushOptions){.traces_and_stats = true, .telemetry = true}); + ddog_MaybeError_drop(ddog_sidecar_flush(&DATADOG_G(sidecar), flush_options)); } ddtrace_telemetry_flush_bgs_metrics_final(); @@ -726,6 +729,8 @@ void ddtrace_internal_handle_postfork(void) { } void ddtrace_internal_handle_fork() { + // The parent owns its already-recorded losses; the child starts fresh. + memset(&DDTRACE_G(ffe_submission_drops), 0, sizeof(DDTRACE_G(ffe_submission_drops))); #ifdef __linux__ ddtrace_otel_detach(); ddtrace_otel_tid_fork_handler(); diff --git a/tracer/ddtrace.stub.php b/tracer/ddtrace.stub.php index adac5b1f10c..0f994d6f579 100644 --- a/tracer/ddtrace.stub.php +++ b/tracer/ddtrace.stub.php @@ -66,6 +66,7 @@ final class FfeResult { public int $reason = 0; public int $errorCode = 0; public bool $doLog = false; + public bool $observeFullEvaluationData = false; public ?int $serialId = null; public array $providerState = []; public ?string $errorMessage = null; @@ -1093,6 +1094,9 @@ function record_ffe_evaluation_metric(string $flagKey, ?string $variant, ?string */ function flush_ffe_evaluation_metrics(): bool {} + /** @internal Submit one final evaluation without waiting or reconnecting. */ + function record_ffe_flag_evaluation(string $flagKey, ?string $variant, ?string $allocationKey, ?string $targetingKey, array $attributes, ?string $errorType, bool $runtimeDefaultUsed, bool $observeFullEvaluationData, int $evaluationTimestampMs, bool $targetingKeyInvalid = false): bool {} + } namespace datadog\appsec\v2 { diff --git a/tracer/ddtrace_arginfo.h b/tracer/ddtrace_arginfo.h index afa0f62d9f7..dc107f67b09 100644 --- a/tracer/ddtrace_arginfo.h +++ b/tracer/ddtrace_arginfo.h @@ -1,5 +1,5 @@ -/* This is a generated file, edit ddtrace.stub.php instead. - * Stub hash: b3087c1f239d5aa8ea38f875b7236f47e56a1ca7 */ +/* This is a generated file, edit the .stub.php file instead. + * Stub hash: 4725b7bd60526a5eeb72ca5e58dbd828c002876a */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_trace_method, 0, 3, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, className, IS_STRING, 0) @@ -184,18 +184,11 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_DDTrace_ffe_evaluate, 0, 4, DDTra ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, recordMetric, _IS_BOOL, 0, "true") ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_ffe_has_config, 0, 0, _IS_BOOL, 0) -ZEND_END_ARG_INFO() +#define arginfo_DDTrace_ffe_has_config arginfo_DDTrace_are_endpoints_collected ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_ffe_config_version, 0, 0, IS_LONG, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_Testing_ffe_load_config, 0, 1, _IS_BOOL, 0) - ZEND_ARG_TYPE_INFO(0, json, IS_STRING, 0) -ZEND_END_ARG_INFO() - -#define arginfo_DDTrace_Testing_flush_ffe_exposures arginfo_DDTrace_are_endpoints_collected - ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_System_container_id, 0, 0, IS_STRING, 1) ZEND_END_ARG_INFO() @@ -229,6 +222,12 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_UserRequest_set_blocking ZEND_ARG_TYPE_INFO(0, blockingFunction, IS_CALLABLE, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_Testing_ffe_load_config, 0, 1, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, json, IS_STRING, 0) +ZEND_END_ARG_INFO() + +#define arginfo_DDTrace_Testing_flush_ffe_exposures arginfo_DDTrace_are_endpoints_collected + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_Testing_trigger_error, 0, 2, IS_VOID, 0) ZEND_ARG_TYPE_INFO(0, message, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, errorType, IS_LONG, 0) @@ -255,7 +254,19 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_Internal_record_ffe_eval ZEND_ARG_TYPE_INFO(0, allocationKey, IS_STRING, 1) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_Internal_flush_ffe_evaluation_metrics, 0, 0, _IS_BOOL, 0) +#define arginfo_DDTrace_Internal_flush_ffe_evaluation_metrics arginfo_DDTrace_are_endpoints_collected + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_Internal_record_ffe_flag_evaluation, 0, 9, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, flagKey, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, variant, IS_STRING, 1) + ZEND_ARG_TYPE_INFO(0, allocationKey, IS_STRING, 1) + ZEND_ARG_TYPE_INFO(0, targetingKey, IS_STRING, 1) + ZEND_ARG_TYPE_INFO(0, attributes, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, errorType, IS_STRING, 1) + ZEND_ARG_TYPE_INFO(0, runtimeDefaultUsed, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, observeFullEvaluationData, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, evaluationTimestampMs, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, targetingKeyInvalid, _IS_BOOL, 0, "false") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_datadog_appsec_v2_track_user_login_success, 0, 1, IS_VOID, 0) @@ -286,8 +297,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_dd_trace_noop, 0, 0, _IS_BOOL, 0 ZEND_ARG_VARIADIC_TYPE_INFO(0, args, IS_MIXED, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_dd_trace_dd_get_memory_limit, 0, 0, IS_LONG, 0) -ZEND_END_ARG_INFO() +#define arginfo_dd_trace_dd_get_memory_limit arginfo_DDTrace_ffe_config_version #define arginfo_dd_trace_check_memory_under_limit arginfo_DDTrace_are_endpoints_collected @@ -311,7 +321,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_dd_trace_buffer_span, 0, 1, _IS_ ZEND_ARG_TYPE_INFO(0, traceArray, IS_ARRAY, 0) ZEND_END_ARG_INFO() -#define arginfo_dd_trace_coms_trigger_writer_flush arginfo_dd_trace_dd_get_memory_limit +#define arginfo_dd_trace_coms_trigger_writer_flush arginfo_DDTrace_ffe_config_version ZEND_BEGIN_ARG_INFO_EX(arginfo_dd_trace_internal_fn, 0, 0, 1) ZEND_ARG_TYPE_INFO(0, functionName, IS_STRING, 0) @@ -322,11 +332,11 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_dd_trace_set_trace_id, 0, 0, _IS ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, traceId, IS_STRING, 1, "null") ZEND_END_ARG_INFO() -#define arginfo_dd_trace_closed_spans_count arginfo_dd_trace_dd_get_memory_limit +#define arginfo_dd_trace_closed_spans_count arginfo_DDTrace_ffe_config_version #define arginfo_dd_trace_tracer_is_limited arginfo_DDTrace_are_endpoints_collected -#define arginfo_dd_trace_compile_time_microseconds arginfo_dd_trace_dd_get_memory_limit +#define arginfo_dd_trace_compile_time_microseconds arginfo_DDTrace_ffe_config_version #define arginfo_dd_trace_serialize_closed_spans arginfo_DDTrace_current_context @@ -371,16 +381,16 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_DDTrace_SpanLink_fromHeader ZEND_ARG_TYPE_MASK(0, headersOrCallback, MAY_BE_ARRAY|MAY_BE_CALLABLE, NULL) ZEND_END_ARG_INFO() -#define arginfo_class_DDTrace_SpanData_getDuration arginfo_dd_trace_dd_get_memory_limit +#define arginfo_class_DDTrace_SpanData_getDuration arginfo_DDTrace_ffe_config_version -#define arginfo_class_DDTrace_SpanData_getStartTime arginfo_dd_trace_dd_get_memory_limit +#define arginfo_class_DDTrace_SpanData_getStartTime arginfo_DDTrace_ffe_config_version ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_DDTrace_SpanData_getLink, 0, 0, DDTrace\\SpanLink, 0) ZEND_END_ARG_INFO() #define arginfo_class_DDTrace_SpanData_hexId arginfo_DDTrace_startup_logs -#define arginfo_class_DDTrace_Integration_init arginfo_dd_trace_dd_get_memory_limit +#define arginfo_class_DDTrace_Integration_init arginfo_DDTrace_ffe_config_version ZEND_FUNCTION(DDTrace_trace_method); ZEND_FUNCTION(DDTrace_trace_function); @@ -428,8 +438,6 @@ ZEND_FUNCTION(DDTrace_flush_endpoints); ZEND_FUNCTION(DDTrace_ffe_evaluate); ZEND_FUNCTION(DDTrace_ffe_has_config); ZEND_FUNCTION(DDTrace_ffe_config_version); -ZEND_FUNCTION(DDTrace_Testing_ffe_load_config); -ZEND_FUNCTION(DDTrace_Testing_flush_ffe_exposures); ZEND_FUNCTION(DDTrace_System_container_id); ZEND_FUNCTION(DDTrace_System_process_tags_base_hash); ZEND_FUNCTION(DDTrace_Config_integration_analytics_enabled); @@ -438,6 +446,8 @@ ZEND_FUNCTION(DDTrace_UserRequest_has_listeners); ZEND_FUNCTION(DDTrace_UserRequest_notify_start); ZEND_FUNCTION(DDTrace_UserRequest_notify_commit); ZEND_FUNCTION(DDTrace_UserRequest_set_blocking_function); +ZEND_FUNCTION(DDTrace_Testing_ffe_load_config); +ZEND_FUNCTION(DDTrace_Testing_flush_ffe_exposures); ZEND_FUNCTION(DDTrace_Testing_trigger_error); ZEND_FUNCTION(DDTrace_Testing_emit_asm_event); ZEND_FUNCTION(DDTrace_Testing_normalize_tag_value); @@ -445,6 +455,7 @@ ZEND_FUNCTION(DDTrace_Internal_add_span_flag); ZEND_FUNCTION(DDTrace_Internal_handle_fork); ZEND_FUNCTION(DDTrace_Internal_record_ffe_evaluation_metric); ZEND_FUNCTION(DDTrace_Internal_flush_ffe_evaluation_metrics); +ZEND_FUNCTION(DDTrace_Internal_record_ffe_flag_evaluation); ZEND_FUNCTION(datadog_appsec_v2_track_user_login_success); ZEND_FUNCTION(datadog_appsec_v2_track_user_login_failure); ZEND_FUNCTION(dd_trace_env_config); @@ -547,6 +558,7 @@ static const zend_function_entry ext_functions[] = { ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Internal", "handle_fork"), zif_DDTrace_Internal_handle_fork, arginfo_DDTrace_Internal_handle_fork, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Internal", "record_ffe_evaluation_metric"), zif_DDTrace_Internal_record_ffe_evaluation_metric, arginfo_DDTrace_Internal_record_ffe_evaluation_metric, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Internal", "flush_ffe_evaluation_metrics"), zif_DDTrace_Internal_flush_ffe_evaluation_metrics, arginfo_DDTrace_Internal_flush_ffe_evaluation_metrics, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Internal", "record_ffe_flag_evaluation"), zif_DDTrace_Internal_record_ffe_flag_evaluation, arginfo_DDTrace_Internal_record_ffe_flag_evaluation, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("datadog\\appsec\\v2", "track_user_login_success"), zif_datadog_appsec_v2_track_user_login_success, arginfo_datadog_appsec_v2_track_user_login_success, 0, NULL, NULL) ZEND_RAW_FENTRY(ZEND_NS_NAME("datadog\\appsec\\v2", "track_user_login_failure"), zif_datadog_appsec_v2_track_user_login_failure, arginfo_datadog_appsec_v2_track_user_login_failure, 0, NULL, NULL) ZEND_FE(dd_trace_env_config, arginfo_dd_trace_env_config) @@ -639,69 +651,75 @@ static zend_class_entry *register_class_DDTrace_FfeResult(void) zval property_valueJson_default_value; ZVAL_NULL(&property_valueJson_default_value); - zend_string *property_valueJson_name = zend_string_init("valueJson", sizeof("valueJson") - 1, true); + zend_string *property_valueJson_name = zend_string_init("valueJson", sizeof("valueJson") - 1, 1); zend_declare_typed_property(class_entry, property_valueJson_name, &property_valueJson_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING|MAY_BE_NULL)); - zend_string_release_ex(property_valueJson_name, true); + zend_string_release(property_valueJson_name); zval property_variant_default_value; ZVAL_NULL(&property_variant_default_value); - zend_string *property_variant_name = zend_string_init("variant", sizeof("variant") - 1, true); + zend_string *property_variant_name = zend_string_init("variant", sizeof("variant") - 1, 1); zend_declare_typed_property(class_entry, property_variant_name, &property_variant_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING|MAY_BE_NULL)); - zend_string_release_ex(property_variant_name, true); + zend_string_release(property_variant_name); zval property_allocationKey_default_value; ZVAL_NULL(&property_allocationKey_default_value); - zend_string *property_allocationKey_name = zend_string_init("allocationKey", sizeof("allocationKey") - 1, true); + zend_string *property_allocationKey_name = zend_string_init("allocationKey", sizeof("allocationKey") - 1, 1); zend_declare_typed_property(class_entry, property_allocationKey_name, &property_allocationKey_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING|MAY_BE_NULL)); - zend_string_release_ex(property_allocationKey_name, true); + zend_string_release(property_allocationKey_name); zval property_reason_default_value; ZVAL_LONG(&property_reason_default_value, 0); - zend_string *property_reason_name = zend_string_init("reason", sizeof("reason") - 1, true); + zend_string *property_reason_name = zend_string_init("reason", sizeof("reason") - 1, 1); zend_declare_typed_property(class_entry, property_reason_name, &property_reason_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); - zend_string_release_ex(property_reason_name, true); + zend_string_release(property_reason_name); zval property_errorCode_default_value; ZVAL_LONG(&property_errorCode_default_value, 0); - zend_string *property_errorCode_name = zend_string_init("errorCode", sizeof("errorCode") - 1, true); + zend_string *property_errorCode_name = zend_string_init("errorCode", sizeof("errorCode") - 1, 1); zend_declare_typed_property(class_entry, property_errorCode_name, &property_errorCode_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); - zend_string_release_ex(property_errorCode_name, true); + zend_string_release(property_errorCode_name); zval property_doLog_default_value; ZVAL_FALSE(&property_doLog_default_value); - zend_string *property_doLog_name = zend_string_init("doLog", sizeof("doLog") - 1, true); + zend_string *property_doLog_name = zend_string_init("doLog", sizeof("doLog") - 1, 1); zend_declare_typed_property(class_entry, property_doLog_name, &property_doLog_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_BOOL)); - zend_string_release_ex(property_doLog_name, true); + zend_string_release(property_doLog_name); + + zval property_observeFullEvaluationData_default_value; + ZVAL_FALSE(&property_observeFullEvaluationData_default_value); + zend_string *property_observeFullEvaluationData_name = zend_string_init("observeFullEvaluationData", sizeof("observeFullEvaluationData") - 1, 1); + zend_declare_typed_property(class_entry, property_observeFullEvaluationData_name, &property_observeFullEvaluationData_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_BOOL)); + zend_string_release(property_observeFullEvaluationData_name); zval property_serialId_default_value; ZVAL_NULL(&property_serialId_default_value); - zend_string *property_serialId_name = zend_string_init("serialId", sizeof("serialId") - 1, true); + zend_string *property_serialId_name = zend_string_init("serialId", sizeof("serialId") - 1, 1); zend_declare_typed_property(class_entry, property_serialId_name, &property_serialId_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG|MAY_BE_NULL)); - zend_string_release_ex(property_serialId_name, true); + zend_string_release(property_serialId_name); zval property_providerState_default_value; ZVAL_EMPTY_ARRAY(&property_providerState_default_value); - zend_string *property_providerState_name = zend_string_init("providerState", sizeof("providerState") - 1, true); + zend_string *property_providerState_name = zend_string_init("providerState", sizeof("providerState") - 1, 1); zend_declare_typed_property(class_entry, property_providerState_name, &property_providerState_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ARRAY)); - zend_string_release_ex(property_providerState_name, true); + zend_string_release(property_providerState_name); zval property_errorMessage_default_value; ZVAL_NULL(&property_errorMessage_default_value); - zend_string *property_errorMessage_name = zend_string_init("errorMessage", sizeof("errorMessage") - 1, true); + zend_string *property_errorMessage_name = zend_string_init("errorMessage", sizeof("errorMessage") - 1, 1); zend_declare_typed_property(class_entry, property_errorMessage_name, &property_errorMessage_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING|MAY_BE_NULL)); - zend_string_release_ex(property_errorMessage_name, true); + zend_string_release(property_errorMessage_name); zval property_hasConfig_default_value; ZVAL_NULL(&property_hasConfig_default_value); - zend_string *property_hasConfig_name = zend_string_init("hasConfig", sizeof("hasConfig") - 1, true); + zend_string *property_hasConfig_name = zend_string_init("hasConfig", sizeof("hasConfig") - 1, 1); zend_declare_typed_property(class_entry, property_hasConfig_name, &property_hasConfig_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_BOOL|MAY_BE_NULL)); - zend_string_release_ex(property_hasConfig_name, true); + zend_string_release(property_hasConfig_name); zval property_configVersion_default_value; ZVAL_NULL(&property_configVersion_default_value); - zend_string *property_configVersion_name = zend_string_init("configVersion", sizeof("configVersion") - 1, true); + zend_string *property_configVersion_name = zend_string_init("configVersion", sizeof("configVersion") - 1, 1); zend_declare_typed_property(class_entry, property_configVersion_name, &property_configVersion_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG|MAY_BE_NULL)); - zend_string_release_ex(property_configVersion_name, true); + zend_string_release(property_configVersion_name); return class_entry; } @@ -720,15 +738,15 @@ static zend_class_entry *register_class_DDTrace_SpanEvent(zend_class_entry *clas zval property_attributes_default_value; ZVAL_UNDEF(&property_attributes_default_value); - zend_string *property_attributes_name = zend_string_init("attributes", sizeof("attributes") - 1, true); + zend_string *property_attributes_name = zend_string_init("attributes", sizeof("attributes") - 1, 1); zend_declare_typed_property(class_entry, property_attributes_name, &property_attributes_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ARRAY)); - zend_string_release_ex(property_attributes_name, true); + zend_string_release(property_attributes_name); zval property_timestamp_default_value; ZVAL_UNDEF(&property_timestamp_default_value); - zend_string *property_timestamp_name = zend_string_init("timestamp", sizeof("timestamp") - 1, true); + zend_string *property_timestamp_name = zend_string_init("timestamp", sizeof("timestamp") - 1, 1); zend_declare_typed_property(class_entry, property_timestamp_name, &property_timestamp_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); - zend_string_release_ex(property_timestamp_name, true); + zend_string_release(property_timestamp_name); return class_entry; } @@ -742,10 +760,10 @@ static zend_class_entry *register_class_DDTrace_ExceptionSpanEvent(zend_class_en zval property_exception_default_value; ZVAL_UNDEF(&property_exception_default_value); - zend_string *property_exception_name = zend_string_init("exception", sizeof("exception") - 1, true); + zend_string *property_exception_name = zend_string_init("exception", sizeof("exception") - 1, 1); zend_string *property_exception_class_Throwable = zend_string_init("Throwable", sizeof("Throwable")-1, 1); zend_declare_typed_property(class_entry, property_exception_name, &property_exception_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_exception_class_Throwable, 0, 0)); - zend_string_release_ex(property_exception_name, true); + zend_string_release(property_exception_name); return class_entry; } @@ -760,33 +778,33 @@ static zend_class_entry *register_class_DDTrace_SpanLink(zend_class_entry *class zval property_traceId_default_value; ZVAL_UNDEF(&property_traceId_default_value); - zend_string *property_traceId_name = zend_string_init("traceId", sizeof("traceId") - 1, true); + zend_string *property_traceId_name = zend_string_init("traceId", sizeof("traceId") - 1, 1); zend_declare_typed_property(class_entry, property_traceId_name, &property_traceId_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); - zend_string_release_ex(property_traceId_name, true); + zend_string_release(property_traceId_name); zval property_spanId_default_value; ZVAL_UNDEF(&property_spanId_default_value); - zend_string *property_spanId_name = zend_string_init("spanId", sizeof("spanId") - 1, true); + zend_string *property_spanId_name = zend_string_init("spanId", sizeof("spanId") - 1, 1); zend_declare_typed_property(class_entry, property_spanId_name, &property_spanId_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); - zend_string_release_ex(property_spanId_name, true); + zend_string_release(property_spanId_name); zval property_traceState_default_value; ZVAL_UNDEF(&property_traceState_default_value); - zend_string *property_traceState_name = zend_string_init("traceState", sizeof("traceState") - 1, true); + zend_string *property_traceState_name = zend_string_init("traceState", sizeof("traceState") - 1, 1); zend_declare_typed_property(class_entry, property_traceState_name, &property_traceState_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); - zend_string_release_ex(property_traceState_name, true); + zend_string_release(property_traceState_name); zval property_attributes_default_value; ZVAL_UNDEF(&property_attributes_default_value); - zend_string *property_attributes_name = zend_string_init("attributes", sizeof("attributes") - 1, true); + zend_string *property_attributes_name = zend_string_init("attributes", sizeof("attributes") - 1, 1); zend_declare_typed_property(class_entry, property_attributes_name, &property_attributes_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ARRAY)); - zend_string_release_ex(property_attributes_name, true); + zend_string_release(property_attributes_name); zval property_droppedAttributesCount_default_value; ZVAL_UNDEF(&property_droppedAttributesCount_default_value); - zend_string *property_droppedAttributesCount_name = zend_string_init("droppedAttributesCount", sizeof("droppedAttributesCount") - 1, true); + zend_string *property_droppedAttributesCount_name = zend_string_init("droppedAttributesCount", sizeof("droppedAttributesCount") - 1, 1); zend_declare_typed_property(class_entry, property_droppedAttributesCount_name, &property_droppedAttributesCount_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); - zend_string_release_ex(property_droppedAttributesCount_name, true); + zend_string_release(property_droppedAttributesCount_name); return class_entry; } @@ -800,15 +818,15 @@ static zend_class_entry *register_class_DDTrace_GitMetadata(void) zval property_commitSha_default_value; ZVAL_EMPTY_STRING(&property_commitSha_default_value); - zend_string *property_commitSha_name = zend_string_init("commitSha", sizeof("commitSha") - 1, true); + zend_string *property_commitSha_name = zend_string_init("commitSha", sizeof("commitSha") - 1, 1); zend_declare_typed_property(class_entry, property_commitSha_name, &property_commitSha_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); - zend_string_release_ex(property_commitSha_name, true); + zend_string_release(property_commitSha_name); zval property_repositoryUrl_default_value; ZVAL_EMPTY_STRING(&property_repositoryUrl_default_value); - zend_string *property_repositoryUrl_name = zend_string_init("repositoryUrl", sizeof("repositoryUrl") - 1, true); + zend_string *property_repositoryUrl_name = zend_string_init("repositoryUrl", sizeof("repositoryUrl") - 1, 1); zend_declare_typed_property(class_entry, property_repositoryUrl_name, &property_repositoryUrl_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); - zend_string_release_ex(property_repositoryUrl_name, true); + zend_string_release(property_repositoryUrl_name); return class_entry; } @@ -830,27 +848,27 @@ static zend_class_entry *register_class_DDTrace_SpanData(void) zval property_service_default_value; ZVAL_EMPTY_STRING(&property_service_default_value); - zend_string *property_service_name = zend_string_init("service", sizeof("service") - 1, true); + zend_string *property_service_name = zend_string_init("service", sizeof("service") - 1, 1); zend_declare_typed_property(class_entry, property_service_name, &property_service_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING|MAY_BE_NULL)); - zend_string_release_ex(property_service_name, true); + zend_string_release(property_service_name); zval property_env_default_value; ZVAL_EMPTY_STRING(&property_env_default_value); - zend_string *property_env_name = zend_string_init("env", sizeof("env") - 1, true); + zend_string *property_env_name = zend_string_init("env", sizeof("env") - 1, 1); zend_declare_typed_property(class_entry, property_env_name, &property_env_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); - zend_string_release_ex(property_env_name, true); + zend_string_release(property_env_name); zval property_version_default_value; ZVAL_EMPTY_STRING(&property_version_default_value); - zend_string *property_version_name = zend_string_init("version", sizeof("version") - 1, true); + zend_string *property_version_name = zend_string_init("version", sizeof("version") - 1, 1); zend_declare_typed_property(class_entry, property_version_name, &property_version_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); - zend_string_release_ex(property_version_name, true); + zend_string_release(property_version_name); zval property_meta_struct_default_value; ZVAL_EMPTY_ARRAY(&property_meta_struct_default_value); - zend_string *property_meta_struct_name = zend_string_init("meta_struct", sizeof("meta_struct") - 1, true); + zend_string *property_meta_struct_name = zend_string_init("meta_struct", sizeof("meta_struct") - 1, 1); zend_declare_typed_property(class_entry, property_meta_struct_name, &property_meta_struct_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ARRAY)); - zend_string_release_ex(property_meta_struct_name, true); + zend_string_release(property_meta_struct_name); zval property_type_default_value; ZVAL_EMPTY_STRING(&property_type_default_value); @@ -858,46 +876,46 @@ static zend_class_entry *register_class_DDTrace_SpanData(void) zval property_meta_default_value; ZVAL_EMPTY_ARRAY(&property_meta_default_value); - zend_string *property_meta_name = zend_string_init("meta", sizeof("meta") - 1, true); + zend_string *property_meta_name = zend_string_init("meta", sizeof("meta") - 1, 1); zend_declare_typed_property(class_entry, property_meta_name, &property_meta_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ARRAY)); - zend_string_release_ex(property_meta_name, true); + zend_string_release(property_meta_name); zval property_metrics_default_value; ZVAL_EMPTY_ARRAY(&property_metrics_default_value); - zend_string *property_metrics_name = zend_string_init("metrics", sizeof("metrics") - 1, true); + zend_string *property_metrics_name = zend_string_init("metrics", sizeof("metrics") - 1, 1); zend_declare_typed_property(class_entry, property_metrics_name, &property_metrics_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ARRAY)); - zend_string_release_ex(property_metrics_name, true); + zend_string_release(property_metrics_name); zval property_exception_default_value; ZVAL_NULL(&property_exception_default_value); - zend_string *property_exception_name = zend_string_init("exception", sizeof("exception") - 1, true); + zend_string *property_exception_name = zend_string_init("exception", sizeof("exception") - 1, 1); zend_string *property_exception_class_Throwable = zend_string_init("Throwable", sizeof("Throwable")-1, 1); zend_declare_typed_property(class_entry, property_exception_name, &property_exception_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_exception_class_Throwable, 0, MAY_BE_NULL)); - zend_string_release_ex(property_exception_name, true); + zend_string_release(property_exception_name); zval property_id_default_value; ZVAL_UNDEF(&property_id_default_value); - zend_string *property_id_name = zend_string_init("id", sizeof("id") - 1, true); + zend_string *property_id_name = zend_string_init("id", sizeof("id") - 1, 1); zend_declare_typed_property(class_entry, property_id_name, &property_id_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); - zend_string_release_ex(property_id_name, true); + zend_string_release(property_id_name); zval property_links_default_value; ZVAL_EMPTY_ARRAY(&property_links_default_value); - zend_string *property_links_name = zend_string_init("links", sizeof("links") - 1, true); + zend_string *property_links_name = zend_string_init("links", sizeof("links") - 1, 1); zend_declare_typed_property(class_entry, property_links_name, &property_links_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ARRAY)); - zend_string_release_ex(property_links_name, true); + zend_string_release(property_links_name); zval property_events_default_value; ZVAL_EMPTY_ARRAY(&property_events_default_value); - zend_string *property_events_name = zend_string_init("events", sizeof("events") - 1, true); + zend_string *property_events_name = zend_string_init("events", sizeof("events") - 1, 1); zend_declare_typed_property(class_entry, property_events_name, &property_events_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ARRAY)); - zend_string_release_ex(property_events_name, true); + zend_string_release(property_events_name); zval property_peerServiceSources_default_value; ZVAL_EMPTY_ARRAY(&property_peerServiceSources_default_value); - zend_string *property_peerServiceSources_name = zend_string_init("peerServiceSources", sizeof("peerServiceSources") - 1, true); + zend_string *property_peerServiceSources_name = zend_string_init("peerServiceSources", sizeof("peerServiceSources") - 1, 1); zend_declare_typed_property(class_entry, property_peerServiceSources_name, &property_peerServiceSources_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ARRAY)); - zend_string_release_ex(property_peerServiceSources_name, true); + zend_string_release(property_peerServiceSources_name); zval property_parent_default_value; ZVAL_UNDEF(&property_parent_default_value); @@ -906,22 +924,22 @@ static zend_class_entry *register_class_DDTrace_SpanData(void) zval property_stack_default_value; ZVAL_UNDEF(&property_stack_default_value); - zend_string *property_stack_name = zend_string_init("stack", sizeof("stack") - 1, true); + zend_string *property_stack_name = zend_string_init("stack", sizeof("stack") - 1, 1); zend_string *property_stack_class_DDTrace_SpanStack = zend_string_init("DDTrace\\SpanStack", sizeof("DDTrace\\SpanStack")-1, 1); zend_declare_typed_property(class_entry, property_stack_name, &property_stack_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_stack_class_DDTrace_SpanStack, 0, 0)); - zend_string_release_ex(property_stack_name, true); + zend_string_release(property_stack_name); zval property_onClose_default_value; ZVAL_EMPTY_ARRAY(&property_onClose_default_value); - zend_string *property_onClose_name = zend_string_init("onClose", sizeof("onClose") - 1, true); + zend_string *property_onClose_name = zend_string_init("onClose", sizeof("onClose") - 1, 1); zend_declare_typed_property(class_entry, property_onClose_name, &property_onClose_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ARRAY)); - zend_string_release_ex(property_onClose_name, true); + zend_string_release(property_onClose_name); zval property_baggage_default_value; ZVAL_EMPTY_ARRAY(&property_baggage_default_value); - zend_string *property_baggage_name = zend_string_init("baggage", sizeof("baggage") - 1, true); + zend_string *property_baggage_name = zend_string_init("baggage", sizeof("baggage") - 1, 1); zend_declare_typed_property(class_entry, property_baggage_name, &property_baggage_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ARRAY)); - zend_string_release_ex(property_baggage_name, true); + zend_string_release(property_baggage_name); return class_entry; } @@ -945,65 +963,65 @@ static zend_class_entry *register_class_DDTrace_RootSpanData(zend_class_entry *c zval property_origin_default_value; ZVAL_UNDEF(&property_origin_default_value); - zend_string *property_origin_name = zend_string_init("origin", sizeof("origin") - 1, true); + zend_string *property_origin_name = zend_string_init("origin", sizeof("origin") - 1, 1); zend_declare_typed_property(class_entry, property_origin_name, &property_origin_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); - zend_string_release_ex(property_origin_name, true); + zend_string_release(property_origin_name); zval property_propagatedTags_default_value; ZVAL_EMPTY_ARRAY(&property_propagatedTags_default_value); - zend_string *property_propagatedTags_name = zend_string_init("propagatedTags", sizeof("propagatedTags") - 1, true); + zend_string *property_propagatedTags_name = zend_string_init("propagatedTags", sizeof("propagatedTags") - 1, 1); zend_declare_typed_property(class_entry, property_propagatedTags_name, &property_propagatedTags_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ARRAY)); - zend_string_release_ex(property_propagatedTags_name, true); + zend_string_release(property_propagatedTags_name); zval property_samplingPriority_default_value; ZVAL_LONG(&property_samplingPriority_default_value, DDTRACE_PRIORITY_SAMPLING_UNKNOWN); - zend_string *property_samplingPriority_name = zend_string_init("samplingPriority", sizeof("samplingPriority") - 1, true); + zend_string *property_samplingPriority_name = zend_string_init("samplingPriority", sizeof("samplingPriority") - 1, 1); zend_declare_typed_property(class_entry, property_samplingPriority_name, &property_samplingPriority_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); - zend_string_release_ex(property_samplingPriority_name, true); + zend_string_release(property_samplingPriority_name); zval property_propagatedSamplingPriority_default_value; ZVAL_UNDEF(&property_propagatedSamplingPriority_default_value); - zend_string *property_propagatedSamplingPriority_name = zend_string_init("propagatedSamplingPriority", sizeof("propagatedSamplingPriority") - 1, true); + zend_string *property_propagatedSamplingPriority_name = zend_string_init("propagatedSamplingPriority", sizeof("propagatedSamplingPriority") - 1, 1); zend_declare_typed_property(class_entry, property_propagatedSamplingPriority_name, &property_propagatedSamplingPriority_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); - zend_string_release_ex(property_propagatedSamplingPriority_name, true); + zend_string_release(property_propagatedSamplingPriority_name); zval property_tracestate_default_value; ZVAL_UNDEF(&property_tracestate_default_value); - zend_string *property_tracestate_name = zend_string_init("tracestate", sizeof("tracestate") - 1, true); + zend_string *property_tracestate_name = zend_string_init("tracestate", sizeof("tracestate") - 1, 1); zend_declare_typed_property(class_entry, property_tracestate_name, &property_tracestate_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); - zend_string_release_ex(property_tracestate_name, true); + zend_string_release(property_tracestate_name); zval property_tracestateTags_default_value; ZVAL_EMPTY_ARRAY(&property_tracestateTags_default_value); - zend_string *property_tracestateTags_name = zend_string_init("tracestateTags", sizeof("tracestateTags") - 1, true); + zend_string *property_tracestateTags_name = zend_string_init("tracestateTags", sizeof("tracestateTags") - 1, 1); zend_declare_typed_property(class_entry, property_tracestateTags_name, &property_tracestateTags_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ARRAY)); - zend_string_release_ex(property_tracestateTags_name, true); + zend_string_release(property_tracestateTags_name); zval property_parentId_default_value; ZVAL_UNDEF(&property_parentId_default_value); - zend_string *property_parentId_name = zend_string_init("parentId", sizeof("parentId") - 1, true); + zend_string *property_parentId_name = zend_string_init("parentId", sizeof("parentId") - 1, 1); zend_declare_typed_property(class_entry, property_parentId_name, &property_parentId_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); - zend_string_release_ex(property_parentId_name, true); + zend_string_release(property_parentId_name); zval property_traceId_default_value; ZVAL_EMPTY_STRING(&property_traceId_default_value); - zend_string *property_traceId_name = zend_string_init("traceId", sizeof("traceId") - 1, true); + zend_string *property_traceId_name = zend_string_init("traceId", sizeof("traceId") - 1, 1); zend_declare_typed_property(class_entry, property_traceId_name, &property_traceId_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); - zend_string_release_ex(property_traceId_name, true); + zend_string_release(property_traceId_name); zval property_gitMetadata_default_value; ZVAL_NULL(&property_gitMetadata_default_value); - zend_string *property_gitMetadata_name = zend_string_init("gitMetadata", sizeof("gitMetadata") - 1, true); + zend_string *property_gitMetadata_name = zend_string_init("gitMetadata", sizeof("gitMetadata") - 1, 1); zend_string *property_gitMetadata_class_DDTrace_GitMetadata = zend_string_init("DDTrace\\GitMetadata", sizeof("DDTrace\\GitMetadata")-1, 1); zend_declare_typed_property(class_entry, property_gitMetadata_name, &property_gitMetadata_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_gitMetadata_class_DDTrace_GitMetadata, 0, MAY_BE_NULL)); - zend_string_release_ex(property_gitMetadata_name, true); + zend_string_release(property_gitMetadata_name); zval property_inferredSpan_default_value; ZVAL_NULL(&property_inferredSpan_default_value); - zend_string *property_inferredSpan_name = zend_string_init("inferredSpan", sizeof("inferredSpan") - 1, true); + zend_string *property_inferredSpan_name = zend_string_init("inferredSpan", sizeof("inferredSpan") - 1, 1); zend_string *property_inferredSpan_class_DDTrace_InferredSpanData = zend_string_init("DDTrace\\InferredSpanData", sizeof("DDTrace\\InferredSpanData")-1, 1); zend_declare_typed_property(class_entry, property_inferredSpan_name, &property_inferredSpan_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_inferredSpan_class_DDTrace_InferredSpanData, 0, MAY_BE_NULL)); - zend_string_release_ex(property_inferredSpan_name, true); + zend_string_release(property_inferredSpan_name); return class_entry; } @@ -1022,16 +1040,16 @@ static zend_class_entry *register_class_DDTrace_SpanStack(void) zval property_active_default_value; ZVAL_NULL(&property_active_default_value); - zend_string *property_active_name = zend_string_init("active", sizeof("active") - 1, true); + zend_string *property_active_name = zend_string_init("active", sizeof("active") - 1, 1); zend_string *property_active_class_DDTrace_SpanData = zend_string_init("DDTrace\\SpanData", sizeof("DDTrace\\SpanData")-1, 1); zend_declare_typed_property(class_entry, property_active_name, &property_active_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_active_class_DDTrace_SpanData, 0, MAY_BE_NULL)); - zend_string_release_ex(property_active_name, true); + zend_string_release(property_active_name); zval property_spanCreationObservers_default_value; ZVAL_EMPTY_ARRAY(&property_spanCreationObservers_default_value); - zend_string *property_spanCreationObservers_name = zend_string_init("spanCreationObservers", sizeof("spanCreationObservers") - 1, true); + zend_string *property_spanCreationObservers_name = zend_string_init("spanCreationObservers", sizeof("spanCreationObservers") - 1, 1); zend_declare_typed_property(class_entry, property_spanCreationObservers_name, &property_spanCreationObservers_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ARRAY)); - zend_string_release_ex(property_spanCreationObservers_name, true); + zend_string_release(property_spanCreationObservers_name); return class_entry; } @@ -1045,21 +1063,21 @@ static zend_class_entry *register_class_DDTrace_Integration(void) zval const_NOT_LOADED_value; ZVAL_LONG(&const_NOT_LOADED_value, DD_TRACE_INTEGRATION_NOT_LOADED); - zend_string *const_NOT_LOADED_name = zend_string_init_interned("NOT_LOADED", sizeof("NOT_LOADED") - 1, true); + zend_string *const_NOT_LOADED_name = zend_string_init_interned("NOT_LOADED", sizeof("NOT_LOADED") - 1, 1); zend_declare_class_constant_ex(class_entry, const_NOT_LOADED_name, &const_NOT_LOADED_value, ZEND_ACC_PUBLIC, NULL); - zend_string_release_ex(const_NOT_LOADED_name, true); + zend_string_release(const_NOT_LOADED_name); zval const_LOADED_value; ZVAL_LONG(&const_LOADED_value, DD_TRACE_INTEGRATION_LOADED); - zend_string *const_LOADED_name = zend_string_init_interned("LOADED", sizeof("LOADED") - 1, true); + zend_string *const_LOADED_name = zend_string_init_interned("LOADED", sizeof("LOADED") - 1, 1); zend_declare_class_constant_ex(class_entry, const_LOADED_name, &const_LOADED_value, ZEND_ACC_PUBLIC, NULL); - zend_string_release_ex(const_LOADED_name, true); + zend_string_release(const_LOADED_name); zval const_NOT_AVAILABLE_value; ZVAL_LONG(&const_NOT_AVAILABLE_value, DD_TRACE_INTEGRATION_NOT_AVAILABLE); - zend_string *const_NOT_AVAILABLE_name = zend_string_init_interned("NOT_AVAILABLE", sizeof("NOT_AVAILABLE") - 1, true); + zend_string *const_NOT_AVAILABLE_name = zend_string_init_interned("NOT_AVAILABLE", sizeof("NOT_AVAILABLE") - 1, 1); zend_declare_class_constant_ex(class_entry, const_NOT_AVAILABLE_name, &const_NOT_AVAILABLE_value, ZEND_ACC_PUBLIC, NULL); - zend_string_release_ex(const_NOT_AVAILABLE_name, true); + zend_string_release(const_NOT_AVAILABLE_name); return class_entry; } diff --git a/tracer/ddtrace_globals.h b/tracer/ddtrace_globals.h index a0bc90fb8c5..43d3e098677 100644 --- a/tracer/ddtrace_globals.h +++ b/tracer/ddtrace_globals.h @@ -121,6 +121,12 @@ typedef struct { void *ffe_metric_buffer; size_t ffe_metric_buffer_len; size_t ffe_metric_buffer_cap; + struct { + uint64_t unavailable; + uint64_t queue_overflow; + uint64_t serialization_error; + uint64_t payload_limit; + } ffe_submission_drops; } ddtrace_globals; diff --git a/tracer/ffe.c b/tracer/ffe.c index 884b49bdf11..a88bfb0d64e 100644 --- a/tracer/ffe.c +++ b/tracer/ffe.c @@ -2,6 +2,7 @@ #include "configuration.h" #include "span.h" +#include "tracer_telemetry.h" #include #include #include @@ -15,6 +16,101 @@ ZEND_EXTERN_MODULE_GLOBALS(datadog); #define DD_FFE_METRIC_BUFFER_LIMIT 1000 #define DD_FFE_EXPOSURE_BUFFER_LIMIT 1000 +#define DD_FFE_CONTEXT_FIELD_LIMIT 256 + +bool ddtrace_ffe_record_flag_evaluation(zend_string *flag_key, zend_string *variant, + zend_string *allocation_key, zend_string *targeting_key, HashTable *attributes, + zend_string *error_type, bool runtime_default_used, bool observe_full_evaluation_data, + int64_t evaluation_timestamp_ms, bool targeting_key_invalid) { + if (!get_DD_FLAGGING_EVALUATION_COUNTS_ENABLED()) { + return false; + } + if (!flag_key || !ZSTR_LEN(flag_key)) { + ddtrace_telemetry_count_ffe_rejection(DDOG_FFE_SUBMISSION_STATUS_INVALID_INPUT); + return false; + } + if (!DATADOG_G(sidecar) || !DATADOG_G(request_initialized)) { + ddtrace_telemetry_count_ffe_rejection(DDOG_FFE_SUBMISSION_STATUS_UNAVAILABLE); + return false; + } + + ddog_FfeScalarAttribute scalars[DD_FFE_CONTEXT_FIELD_LIMIT] = {0}; + zend_string *owned_keys[DD_FFE_CONTEXT_FIELD_LIMIT] = {0}; + size_t count = 0; + ddog_FfeSnapshotState snapshot = {.targeting_key_invalid = targeting_key_invalid}; + // Protected observations never walk or serialize their context. + if (observe_full_evaluation_data) { + snapshot.context_truncated = zend_hash_num_elements(attributes) > DD_FFE_CONTEXT_FIELD_LIMIT; + zend_ulong index; + zend_string *key; + zval *value; + ZEND_HASH_FOREACH_KEY_VAL(attributes, index, key, value) { + if (count == DD_FFE_CONTEXT_FIELD_LIMIT) { + break; + } + if (!key) { + key = owned_keys[count] = zend_long_to_str((zend_long) index); + } + ddog_FfeScalarAttribute *attribute = &scalars[count++]; + attribute->key = dd_zend_string_to_CharSlice(key); + ZVAL_DEREF(value); + switch (Z_TYPE_P(value)) { + case IS_STRING: + attribute->kind = 0; + attribute->string_value = dd_zend_string_to_CharSlice(Z_STR_P(value)); + break; + case IS_TRUE: + case IS_FALSE: + attribute->kind = 1; + attribute->bool_value = Z_TYPE_P(value) == IS_TRUE; + break; + case IS_LONG: + attribute->kind = 2; + attribute->integer_value = Z_LVAL_P(value); + break; + case IS_DOUBLE: + attribute->kind = 3; + attribute->double_value = Z_DVAL_P(value); + break; + default: + // The evaluator accepts scalars only. Retain an omission + // marker if an internal caller supplies anything else. + attribute->kind = 4; + break; + } + } ZEND_HASH_FOREACH_END(); + } + + ddog_FfeFlagEvaluation event = { + .timestamp_ms = evaluation_timestamp_ms, + .first_evaluation_ms = evaluation_timestamp_ms, + .last_evaluation_ms = evaluation_timestamp_ms, + .evaluation_count = 1, + .flag_key = dd_zend_string_to_CharSlice(flag_key), + .variant = dd_zend_string_to_CharSlice(variant), + .allocation_key = dd_zend_string_to_CharSlice(allocation_key), + .targeting_key = dd_zend_string_to_CharSlice(targeting_key), + .error_message = dd_zend_string_to_CharSlice(error_type), + .runtime_default_used = runtime_default_used, + .observe_full_evaluation_data = observe_full_evaluation_data, + }; + // Sidecar application attribution is deferred to FFL-3438/FFL-3439. + // The current FFI still requires a context argument. + ddog_FfeTelemetryContext context = {0}; + ddog_FfeSubmissionStatus status = ddog_sidecar_try_submit_ffe_flag_evaluation( + DATADOG_G(sidecar), &context, &event, + (ddog_Slice_FfeScalarAttribute) {.ptr = scalars, .len = count}, &snapshot); + for (size_t i = 0; i < count; ++i) { + if (owned_keys[i]) { + zend_string_release(owned_keys[i]); + } + } + if (status == DDOG_FFE_SUBMISSION_STATUS_ACCEPTED) { + return true; + } + ddtrace_telemetry_count_ffe_rejection(status); + return false; +} typedef struct { zend_string *flag_key; diff --git a/tracer/ffe.h b/tracer/ffe.h index 4da7f951f28..ab5c8ed3c6f 100644 --- a/tracer/ffe.h +++ b/tracer/ffe.h @@ -9,6 +9,11 @@ bool ddtrace_ffe_record_evaluation_metric(zend_string *flag_key, zend_string *variant, const char *reason, const char *error_type, zend_string *allocation_key); bool ddtrace_ffe_flush_evaluation_metrics(void); +bool ddtrace_ffe_record_flag_evaluation(zend_string *flag_key, zend_string *variant, + zend_string *allocation_key, zend_string *targeting_key, HashTable *attributes, + zend_string *error_type, bool runtime_default_used, bool observe_full_evaluation_data, + int64_t evaluation_timestamp_ms, bool targeting_key_invalid); + void ddtrace_ffe_record_exposure(zend_string *flag_key, zend_string *targeting_key, zend_string *subject_attributes_json, zend_string *allocation_key, zend_string *variant, int32_t serial_id, bool has_serial_id); bool ddtrace_ffe_flush_exposures(void); diff --git a/tracer/functions.c b/tracer/functions.c index 5657740cb11..88cb85ca85b 100644 --- a/tracer/functions.c +++ b/tracer/functions.c @@ -1934,6 +1934,7 @@ PHP_FUNCTION(DDTrace_ffe_evaluate) { ddtrace_ffe_update_long_property(return_value, ZEND_STRL("reason"), ddtrace_ffe_effective_reason(result.reason, result.error_code)); ddtrace_ffe_update_long_property(return_value, ZEND_STRL("errorCode"), result.error_code); ddtrace_ffe_update_bool_property(return_value, ZEND_STRL("doLog"), result.do_log); + ddtrace_ffe_update_bool_property(return_value, ZEND_STRL("observeFullEvaluationData"), result.observe_full_evaluation_data); // serialId is only populated when the native result actually carried one // (has_serial_id). It stays null otherwise so the PHP accumulator can use // the Pattern B "missing variant => runtime default" branch, rather than @@ -2127,6 +2128,7 @@ PHP_FUNCTION(dd_trace_internal_fn) { } } else if (FUNCTION_NAME_MATCHES("synchronous_flush")) { uint32_t timeout = 1000; + ddog_SidecarFlushOptions flush_options = {.traces_and_stats = true, .flag_evaluations = true}; if (params_count == 1) { timeout = Z_LVAL_P(ZVAL_VARARG_PARAM(params, 0)); } @@ -2135,10 +2137,10 @@ PHP_FUNCTION(dd_trace_internal_fn) { if (dd_rinit_once_done) { ddtrace_coms_synchronous_flush(timeout); } - } else + } #endif if (DATADOG_G(sidecar)) { - datadog_ffi_try("Failed synchronously flushing traces", ddog_sidecar_flush(&DATADOG_G(sidecar), (ddog_SidecarFlushOptions){.traces_and_stats = true})); + datadog_ffi_try("Failed synchronously flushing traces and flag evaluations", ddog_sidecar_flush(&DATADOG_G(sidecar), flush_options)); } RETVAL_TRUE; #ifndef _WIN32 @@ -2342,15 +2344,18 @@ PHP_FUNCTION(dd_trace_synchronous_flush) { RETURN_NULL(); } + // FFE's coalescer is shared: evaluations may have come from another PHP + // thread even when this thread has never submitted one. + ddog_SidecarFlushOptions flush_options = {.traces_and_stats = true, .flag_evaluations = true}; #ifndef _WIN32 if (!get_global_DD_TRACE_SIDECAR_TRACE_SENDER()) { if (dd_rinit_once_done) { ddtrace_coms_synchronous_flush(timeout); } - } else + } #endif if (DATADOG_G(sidecar)) { - datadog_ffi_try("Failed synchronously flushing traces", ddog_sidecar_flush(&DATADOG_G(sidecar), (ddog_SidecarFlushOptions){.traces_and_stats = true})); + datadog_ffi_try("Failed synchronously flushing traces and flag evaluations", ddog_sidecar_flush(&DATADOG_G(sidecar), flush_options)); } RETURN_NULL(); } @@ -2933,6 +2938,30 @@ PHP_FUNCTION(DDTrace_Internal_flush_ffe_evaluation_metrics) { RETURN_BOOL(ddtrace_ffe_flush_evaluation_metrics()); } +PHP_FUNCTION(DDTrace_Internal_record_ffe_flag_evaluation) { + zend_string *flag_key, *variant = NULL, *allocation_key = NULL, *targeting_key = NULL, *error_type = NULL; + HashTable *attributes; + zend_bool runtime_default_used, observe_full_evaluation_data; + zend_long evaluation_timestamp_ms; + zend_bool targeting_key_invalid = false; + ZEND_PARSE_PARAMETERS_START(9, 10) + Z_PARAM_STR(flag_key) + Z_PARAM_STR_OR_NULL(variant) + Z_PARAM_STR_OR_NULL(allocation_key) + Z_PARAM_STR_OR_NULL(targeting_key) + Z_PARAM_ARRAY_HT(attributes) + Z_PARAM_STR_OR_NULL(error_type) + Z_PARAM_BOOL(runtime_default_used) + Z_PARAM_BOOL(observe_full_evaluation_data) + Z_PARAM_LONG(evaluation_timestamp_ms) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(targeting_key_invalid) + ZEND_PARSE_PARAMETERS_END(); + RETURN_BOOL(ddtrace_ffe_record_flag_evaluation(flag_key, variant, allocation_key, + targeting_key, attributes, error_type, runtime_default_used, observe_full_evaluation_data, + evaluation_timestamp_ms, targeting_key_invalid)); +} + /* {{{ proto array generate_distributed_tracing_headers() */ PHP_FUNCTION(DDTrace_generate_distributed_tracing_headers) { zend_array *inject = NULL; diff --git a/tracer/tracer_telemetry.c b/tracer/tracer_telemetry.c index d4f9f659e17..774d892bbb7 100644 --- a/tracer/tracer_telemetry.c +++ b/tracer/tracer_telemetry.c @@ -73,6 +73,60 @@ void ddtrace_telemetry_first_init(void) { dd_composer_hook_id = zai_hook_install((zai_str)ZAI_STR_EMPTY, (zai_str)ZAI_STR_EMPTY, dd_check_for_composer_autoloader, NULL, ZAI_HOOK_AUX_UNUSED, 0); } +void ddtrace_telemetry_count_ffe_rejection(ddog_FfeSubmissionStatus status) { + if (!get_global_DD_INSTRUMENTATION_TELEMETRY_ENABLED()) { + return; + } + switch (status) { + case DDOG_FFE_SUBMISSION_STATUS_UNAVAILABLE: + ++DDTRACE_G(ffe_submission_drops.unavailable); + break; + case DDOG_FFE_SUBMISSION_STATUS_WOULD_BLOCK: + // Shared load shedding, pending configuration, or socket pressure. + // Conversion already happened, so this is not pre_queue_overflow. + ++DDTRACE_G(ffe_submission_drops.queue_overflow); + break; + case DDOG_FFE_SUBMISSION_STATUS_INVALID_INPUT: + // Required metadata cannot be encoded; optional field loss is + // reported independently by the native sidecar, not as a row drop. + ++DDTRACE_G(ffe_submission_drops.serialization_error); + break; + case DDOG_FFE_SUBMISSION_STATUS_PAYLOAD_TOO_LARGE: + ++DDTRACE_G(ffe_submission_drops.payload_limit); + break; + default: + return; + } +} + +static void ddtrace_telemetry_flush_ffe_rejections(ddog_SidecarActionsBuffer *buffer) { + struct { + uint64_t *count; + ddog_CharSlice tag; + } reasons[] = { + {&DDTRACE_G(ffe_submission_drops.unavailable), DDOG_CHARSLICE_C("reason:unavailable")}, + {&DDTRACE_G(ffe_submission_drops.queue_overflow), DDOG_CHARSLICE_C("reason:queue_overflow")}, + {&DDTRACE_G(ffe_submission_drops.serialization_error), DDOG_CHARSLICE_C("reason:serialization_error")}, + {&DDTRACE_G(ffe_submission_drops.payload_limit), DDOG_CHARSLICE_C("reason:payload_limit")}, + }; + ddog_CharSlice metric = DDOG_CHARSLICE_C("flagevaluation.rows.dropped"); + bool registered = false; + for (size_t i = 0; i < sizeof(reasons) / sizeof(reasons[0]); ++i) { + uint64_t count = *reasons[i].count; + if (!count) { + continue; + } + if (!registered) { + ddog_sidecar_telemetry_register_metric(&DATADOG_G(sidecar), metric, DDOG_METRIC_TYPE_COUNT, DDOG_METRIC_NAMESPACE_TRACERS); + registered = true; + } + // Ordinary telemetry is best-effort. Do not replay these losses on a + // second finalize, or duplicate the sidecar's later-stage drop counters. + *reasons[i].count = 0; + ddog_sidecar_telemetry_add_span_metric_point_buffer(buffer, metric, (double)count, reasons[i].tag); + } +} + void ddtrace_telemetry_finalize(void) { ddog_SidecarActionsBuffer *buffer = datadog_telemetry_buffer(); // Send information about explicitly disabled integrations @@ -107,11 +161,14 @@ void ddtrace_telemetry_finalize(void) { ddog_sidecar_telemetry_register_metric(&DATADOG_G(sidecar), DDOG_CHARSLICE_C("context_header_style.malformed"), DDOG_METRIC_TYPE_COUNT, DDOG_METRIC_NAMESPACE_TRACERS); ddog_sidecar_telemetry_add_span_metric_point_buffer(buffer, DDOG_CHARSLICE_C("context_header_style.malformed"), DDTRACE_G(baggage_malformed_count), DDOG_CHARSLICE_C("header_style:baggage")); + ddtrace_telemetry_flush_ffe_rejections(buffer); + // Flush any accumulated BGS (background sender) metrics if enough time has passed. ddtrace_telemetry_flush_bgs_metrics_if_due(DATADOG_GLOBALS_PTR()); } void ddtrace_telemetry_rinit(void) { + memset(&DDTRACE_G(ffe_submission_drops), 0, sizeof(DDTRACE_G(ffe_submission_drops))); zend_hash_init(&DDTRACE_G(telemetry_spans_created_per_integration), 8, unused, NULL, 0); DDTRACE_G(baggage_extract_count) = 0; DDTRACE_G(baggage_inject_count) = 0; diff --git a/tracer/tracer_telemetry.h b/tracer/tracer_telemetry.h index 05de395fe32..1e1234ae447 100644 --- a/tracer/tracer_telemetry.h +++ b/tracer/tracer_telemetry.h @@ -12,6 +12,8 @@ void ddtrace_telemetry_notify_integration(const char *name, size_t name_len); void ddtrace_telemetry_notify_integration_version(const char *name, size_t name_len, const char *version, size_t version_len); void ddtrace_telemetry_inc_spans_created(ddtrace_span_data *span); +// Fixed request-local counters only; no logging, allocation, or sidecar calls. +void ddtrace_telemetry_count_ffe_rejection(ddog_FfeSubmissionStatus status); // Called by the background sender thread (coms.c) to accumulate metrics atomically. // Never touches the sidecar; the request thread flushes via ddtrace_telemetry_flush_bgs_metrics_if_due(). void ddtrace_telemetry_send_trace_api_metrics(trace_api_metrics metrics);