Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,45 @@ For more information about configuring and instrumenting **ddtrace**, view the [

The **ddtrace** package provides an [OpenTracing-compatible tracer](https://docs.datadoghq.com/tracing/custom_instrumentation/php/?tab=tracingfunctioncalls#opentracing).

### OpenFeature evaluation telemetry opt-out

The Datadog OpenFeature provider can suppress evaluation telemetry for individual subjects. This option is disabled by default.
Enable the provider option, then set `dd_evaluation_telemetry_opt_out` to boolean `true` in the evaluation context:

```php
use DDTrace\OpenFeature\DataDogProvider;
use OpenFeature\implementation\flags\Attributes;
use OpenFeature\implementation\flags\EvaluationContext;
use OpenFeature\OpenFeatureAPI;

$api = OpenFeatureAPI::getInstance();
$api->setProvider(new DataDogProvider(enableEvaluationTelemetryOptOutParameter: true));
$context = new EvaluationContext('subject-123', new Attributes([
'dd_evaluation_telemetry_opt_out' => true,
]));
$value = $api->getClient()->getBooleanValue('checkout-redesign', false, $context);
```

Only boolean `true` suppresses new evaluation metrics and automatic flag span annotations. Strings such as `"true"` do not suppress telemetry.
The provider reads the final merged context after OpenFeature `before` hooks.
Telemetry suppression preserves flag evaluation, experiment exposures, custom hooks, unrelated telemetry, and previously queued data.
This option does not disable experiment participation or exposure logging. The native `DDTrace\FeatureFlags\Client` API remains unchanged.

The attribute remains available to targeting rules and normal exposure subject attributes, regardless of activation.
It follows the same scalar attribute normalization and exposure handling as other context attributes.
A targeting rule can change the assignment when this attribute changes. Telemetry suppression does not change that targeting behavior.

To enable support through the environment, set this variable before PHP starts:

```sh
export DD_FEATURE_FLAGS_EVALUATION_TELEMETRY_PER_SUBJECT_OPT_OUT_ENABLED=true
```

Then construct `new DataDogProvider()` without the option. The provider captures tracer configuration when it is constructed.
An explicit `true` or `false` constructor option overrides the environment. Omitted or `null` options use tracer configuration.
Unset or invalid environment values default to disabled. The equivalent PHP INI option is `datadog.feature_flags_evaluation_telemetry_per_subject_opt_out_enabled`.
Environment activation only enables support for the subject attribute. Subjects without typed boolean `true` continue to record evaluation telemetry.

## Contributing

Before contributing to this open source project, read our [CONTRIBUTING.md](CONTRIBUTING.md).
Expand Down
7 changes: 7 additions & 0 deletions metadata/supported-configurations.json
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,13 @@
"default": "true"
}
],
"DD_FEATURE_FLAGS_EVALUATION_TELEMETRY_PER_SUBJECT_OPT_OUT_ENABLED": [
{
"implementation": "A",
"type": "boolean",
"default": "false"
}
],
"DD_GIT_COMMIT_SHA": [
{
"implementation": "B",
Expand Down
31 changes: 25 additions & 6 deletions src/DDTrace/OpenFeature/DataDogProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use DDTrace\FeatureFlags\EvaluationReason;
use DDTrace\FeatureFlags\EvaluationType;
use DDTrace\FeatureFlags\Internal\Evaluator;
use DDTrace\FeatureFlags\Internal\EvaluationTelemetry;
use DDTrace\FeatureFlags\Internal\Metric\EvaluationMetric;
use DDTrace\FeatureFlags\Internal\Metric\EvaluationMetricRecorder;
use DDTrace\FeatureFlags\Internal\NativeEvaluator;
Expand All @@ -34,12 +35,23 @@ final class DataDogProvider extends AbstractProvider
private LoggerInterface $datadogLogger;
private bool $warnedAboutRuntimeNotReady = false;
private EvaluationMetricRecorder $metricRecorder;
private bool $enableEvaluationTelemetryOptOutParameter;

public function __construct(?LoggerInterface $logger = null)
{
/**
* When enabled, boolean true in dd_evaluation_telemetry_opt_out suppresses
* evaluation metrics and automatic span annotations. Exposures are unchanged.
* Null uses tracer configuration at construction. Explicit true or false overrides it.
*/
public function __construct(
?LoggerInterface $logger = null,
?bool $enableEvaluationTelemetryOptOutParameter = null
) {
$enableEvaluationTelemetryOptOutParameter = $enableEvaluationTelemetryOptOutParameter
?? (\dd_trace_env_config('DD_FEATURE_FLAGS_EVALUATION_TELEMETRY_PER_SUBJECT_OPT_OUT_ENABLED') === true);
// 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->evaluator = NativeEvaluator::create(false, $enableEvaluationTelemetryOptOutParameter);
$this->enableEvaluationTelemetryOptOutParameter = $enableEvaluationTelemetryOptOutParameter;
$this->datadogLogger = new NonThrowingLogger($logger ?: GlobalLogger::get());
$this->metricRecorder = EvaluationMetricRecorder::createDefault();
}
Expand All @@ -50,9 +62,10 @@ public function __construct(?LoggerInterface $logger = null)
public static function createWithDependencies(
?Evaluator $evaluator = null,
?LoggerInterface $logger = null,
$metricRecorder = null
$metricRecorder = null,
?bool $enableEvaluationTelemetryOptOutParameter = null
): self {
$provider = new self($logger);
$provider = new self($logger, $enableEvaluationTelemetryOptOutParameter);
if ($evaluator !== null) {
$provider->evaluator = $evaluator;
}
Expand Down Expand Up @@ -113,12 +126,18 @@ private function resolve(
?EvaluationContext $context
): ResolutionDetailsInterface {
$normalizedContext = $this->normalizeContext($context);
$recordTelemetry = EvaluationTelemetry::shouldRecord(
$this->enableEvaluationTelemetryOptOutParameter,
$normalizedContext['attributes'] ?? []
);
$details = $this->evaluate($flagKey, $expectedType, $defaultValue, $normalizedContext);
$this->warnIfRuntimeNotReady($details);
// The PHP OpenFeature SDK does not pass ResolutionDetails to finally
// hooks, so PHP records metrics here after native evaluation has the
// final provider result.
$this->recordEvaluationMetric($flagKey, $details);
if ($recordTelemetry) {
$this->recordEvaluationMetric($flagKey, $details);
}
// APM span enrichment is recorded inside NativeEvaluator::evaluate()
// (the shared choke point), so there is nothing to do here.

Expand Down
16 changes: 16 additions & 0 deletions src/api/FeatureFlags/Internal/EvaluationTelemetry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace DDTrace\FeatureFlags\Internal;

/** @internal Admission policy for new evaluation telemetry only. */
final class EvaluationTelemetry
{
const OPT_OUT_ATTRIBUTE = 'dd_evaluation_telemetry_opt_out';

public static function shouldRecord($enableOptOutParameter, array $attributes)
{
return !$enableOptOutParameter
|| !isset($attributes[self::OPT_OUT_ATTRIBUTE])
|| $attributes[self::OPT_OUT_ATTRIBUTE] !== true;
}
}
22 changes: 16 additions & 6 deletions src/api/FeatureFlags/Internal/NativeEvaluator.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,32 @@ final class NativeEvaluator implements Evaluator

private $mapper;
private $recordMetrics;
private $enableEvaluationTelemetryOptOutParameter;

private function __construct($mapper = null, bool $recordMetrics = true)
{
private function __construct(
$mapper = null,
bool $recordMetrics = true,
bool $enableEvaluationTelemetryOptOutParameter = false
) {
if ($mapper !== null && !$mapper instanceof ResultMapper) {
throw new \InvalidArgumentException('Expected a ResultMapper instance');
}

$this->mapper = $mapper ?: new ResultMapper();
$this->recordMetrics = $recordMetrics;
$this->enableEvaluationTelemetryOptOutParameter = $enableEvaluationTelemetryOptOutParameter;
}

public static function isAvailable()
{
return function_exists('DDTrace\\ffe_evaluate');
}

public static function create($recordMetrics = true)
public static function create($recordMetrics = true, $enableEvaluationTelemetryOptOutParameter = false)
{
return self::isAvailable() ? new self(null, $recordMetrics) : new UnavailableEvaluator();
return self::isAvailable()
? new self(null, $recordMetrics, $enableEvaluationTelemetryOptOutParameter)
: new UnavailableEvaluator();
}

public function evaluate(
Expand All @@ -39,12 +46,13 @@ public function evaluate(
$targetingKey = null,
array $attributes = array()
) {
$recordTelemetry = EvaluationTelemetry::shouldRecord($this->enableEvaluationTelemetryOptOutParameter, $attributes);
$rawResult = \DDTrace\ffe_evaluate(
$flagKey,
$this->typeId($expectedType),
$targetingKey,
$this->normalizeAttributes($attributes),
$this->recordMetrics
$this->recordMetrics && $recordTelemetry
);

if (is_array($rawResult) || is_object($rawResult)) {
Expand All @@ -59,7 +67,9 @@ public function evaluate(
// is returned otherwise), so enrichment is recorded here once rather than
// duplicated in each caller. No-op when the gate is off or there is no
// active root span; never throws into evaluation.
SpanEnrichmentRegistry::record($flagKey, $details, $targetingKey);
if ($recordTelemetry) {
SpanEnrichmentRegistry::record($flagKey, $details, $targetingKey);
}

return $details;
}
Expand Down
Loading
Loading