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
17 changes: 13 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,8 @@ walkdir = { version = "2.4", default-features = false }
wasm-bindgen-futures = { version = "0.4", default-features = false }
wasm-bindgen-test = { version = "=0.3.50", default-features = false }
web-time = { version = "1", default-features = false }
webpki-roots = { version = "1.0.10", default-features = false }
webpki-root-certs = { version = "1.0.10", default-features = false }
windows = { version = "0.59.0", default-features = false }
windows-sys = { version = "0.52", default-features = false }
winver = { version = "1.0.0", default-features = false }
Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,49 @@ 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).

### Per-subject experimentation opt-out

Enable immediate exclusion through SDK configuration:

```sh
export DD_FEATURE_FLAGS_EXPERIMENTATION_OPT_OUT_MODE=immediate
```

Pass the customer's stored opt-out time in each subject's 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());
$attributes = [];
if ($athlete->experimentationOptOutAt !== null) {
$attributes['dd_experimentation_opt_out_at'] =
$athlete->experimentationOptOutAt->format(DATE_RFC3339);
}
$enabled = $api->getClient()->getBooleanValue(
'new-route-planner',
false,
new EvaluationContext($athlete->id, new Attributes($attributes))
);
```

The default mode is `disabled`. An explicit constructor setting overrides the environment:
`new DataDogProvider(null, ExperimentationOptOutMode::DISABLED)`, using
`DDTrace\FeatureFlags\ExperimentationOptOutMode`.
The native `DDTrace\FeatureFlags\Client` accepts the same second constructor argument.
Configuration is resolved once when the client or provider is constructed.

In immediate mode, a valid RFC3339 timestamp skips allocations that record experiment exposures.
Evaluation continues through ordinary feature allocations. If none match, the SDK returns the application default.
A future timestamp also takes effect immediately. It does not schedule exclusion.
Omit the attribute to retain normal participation. Null, non-string, empty, and malformed values return
the application default with `INVALID_CONTEXT`.
Unsupported configuration values fail construction. Evaluation telemetry remains independent.

## Contributing

Before contributing to this open source project, read our [CONTRIBUTING.md](CONTRIBUTING.md).
Expand Down
3 changes: 2 additions & 1 deletion components-rs/datadog.h
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ struct ddog_FfeResult ddog_ffe_evaluate(ddog_CharSlice flag_key,
int32_t expected_type,
ddog_CharSlice targeting_key,
const struct ddog_FfeAttribute *attributes,
uintptr_t attributes_count);
uintptr_t attributes_count,
bool experimentation_opt_out_immediate);

bool ddog_shall_log(enum ddog_Log category);

Expand Down
81 changes: 73 additions & 8 deletions components-rs/ffe.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::bytes::MaybeOwnedZendString;
use datadog_ffe::rules_based::{
self as ffe, AssignmentReason, AssignmentValue, Attribute, Configuration, EvaluationContext,
EvaluationError, ExpectedFlagType, Str, UniversalFlagConfig,
EvaluationError, EvaluationOptions, ExpectedFlagType, Str, UniversalFlagConfig,
};
use libdd_common_ffi::slice::{AsBytes, CharSlice};
use std::cell::RefCell;
Expand Down Expand Up @@ -77,6 +77,7 @@ const ERROR_NONE: i32 = 0;
const ERROR_TYPE_MISMATCH: i32 = 1;
const ERROR_CONFIG_PARSE: i32 = 2;
const ERROR_FLAG_UNRECOGNIZED: i32 = 3;
const ERROR_INVALID_CONTEXT: i32 = 5;
const ERROR_CONFIG_MISSING: i32 = 6;
const ERROR_GENERAL: i32 = 7;

Expand Down Expand Up @@ -125,6 +126,7 @@ pub extern "C" fn ddog_ffe_evaluate(
targeting_key: CharSlice<'_>,
attributes: *const FfeAttribute<'_>,
attributes_count: usize,
experimentation_opt_out_immediate: bool,
) -> FfeResult {
if flag_key.as_raw_parts().0.is_null() {
return invalid_result();
Expand Down Expand Up @@ -158,13 +160,21 @@ pub extern "C" fn ddog_ffe_evaluate(

FFE_STATE.with(|state| {
let state = state.borrow();
let assignment = ffe::get_assignment(
state.config.as_ref(),
flag_key,
&context,
expected_type,
ffe::now(),
);
let assignment = state
.config
.as_ref()
.ok_or(EvaluationError::ConfigurationMissing)
.and_then(|config| {
config.eval_flag_with_options(
flag_key,
&context,
expected_type,
ffe::now(),
EvaluationOptions {
experimentation_opt_out_immediate,
},
)
});

result_from_assignment(assignment)
})
Expand Down Expand Up @@ -237,6 +247,7 @@ fn result_from_assignment(assignment: Result<ffe::Assignment, EvaluationError>)
Err(error) => {
let (error_code, reason) = match &error {
EvaluationError::TypeMismatch { .. } => (ERROR_TYPE_MISMATCH, REASON_ERROR),
EvaluationError::InvalidContext => (ERROR_INVALID_CONTEXT, REASON_ERROR),
EvaluationError::ConfigurationParseError => (ERROR_CONFIG_PARSE, REASON_ERROR),
EvaluationError::FlagConfigurationInvalid => (ERROR_CONFIG_PARSE, REASON_ERROR),
EvaluationError::ConfigurationMissing => (ERROR_CONFIG_MISSING, REASON_ERROR),
Expand Down Expand Up @@ -420,6 +431,7 @@ mod tests {
CharSlice::from(""),
std::ptr::null(),
0,
false,
);

assert!(result.valid);
Expand All @@ -433,6 +445,59 @@ mod tests {
clear_config();
}

#[test]
fn immediate_opt_out_skips_logging_and_rejects_invalid_context() {
setup_zend_string_functions();
clear_config();
assert!(ddog_ffe_load_config(CharSlice::from(
EMPTY_TARGETING_KEY_CONFIG
)));
let attr = FfeAttribute {
key: CharSlice::from("dd_experimentation_opt_out_at"),
value_type: ATTR_TYPE_STRING,
string_value: CharSlice::from("2099-01-01T00:00:00Z"),
number_value: 0.0,
bool_value: false,
};
let result = ddog_ffe_evaluate(
CharSlice::from("empty.targeting.shard.flag"),
TYPE_STRING,
CharSlice::from(""),
&attr,
1,
true,
);
assert!(result.valid);
assert_eq!(result.reason, REASON_DEFAULT);
assert_eq!(result.error_code, ERROR_NONE);
assert!(!result.do_log);
let invalid = FfeAttribute {
value_type: ATTR_TYPE_BOOL,
..attr
};
let result = ddog_ffe_evaluate(
CharSlice::from("empty.targeting.shard.flag"),
TYPE_STRING,
CharSlice::from(""),
&invalid,
1,
true,
);
assert_eq!(result.error_code, ERROR_INVALID_CONTEXT);
assert!(!result.do_log);
let result = ddog_ffe_evaluate(
CharSlice::from("empty.targeting.shard.flag"),
TYPE_STRING,
CharSlice::from(""),
&invalid,
1,
false,
);
assert_eq!(result.error_code, ERROR_NONE);
assert!(result.do_log);
clear_config();
}

#[test]
fn configuration_state_is_thread_local() {
clear_config();
Expand Down
2 changes: 1 addition & 1 deletion libdatadog
Submodule libdatadog updated 51 files
+16 −11 .github/CODEOWNERS
+57 −9 .github/actions/setup-sccache/action.yml
+1 −0 .github/labeler.yml
+1 −3 .github/workflows/cargo-deny-bans.yml
+71 −4 .github/workflows/coverage.yml
+12 −20 .github/workflows/lint.yml
+6 −16 .github/workflows/pr-metadata-docs-and-deps.yml
+9 −8 .github/workflows/pr-title-semver-check.yml
+4 −0 .github/workflows/release-proposal-dispatch.yml
+15 −3 .github/workflows/test.yml
+5 −9 .github/workflows/verify-proto-files.yml
+18 −8 Cargo.lock
+3 −0 Cargo.toml
+7 −6 benchmark/select_bench_packages.sh
+5 −1 datadog-sidecar/src/service/exception_hash_rate_limiter.rs
+6 −1 libdd-common/Cargo.toml
+0 −3 libdd-crashtracker-ffi/Cargo.toml
+3 −3 libdd-crashtracker-ffi/src/collector_windows/api.rs
+39 −5 libdd-crashtracker/src/collector_windows/api.rs
+72 −1 libdd-ffe-ffi/src/assignment.rs
+1 −1 libdd-ffe-test-suite/ffe-system-test-data
+82 −4 libdd-ffe-test-suite/tests/canonical_fixtures.rs
+6 −0 libdd-ffe/src/rules_based/error.rs
+206 −6 libdd-ffe/src/rules_based/eval/eval_assignment.rs
+1 −1 libdd-ffe/src/rules_based/eval/mod.rs
+1 −1 libdd-ffe/src/rules_based/mod.rs
+55 −14 libdd-ipc/src/rate_limiter.rs
+3 −0 libdd-tinybytes/Cargo.toml
+7 −0 libdd-tinybytes/src/bytes_string.rs
+16 −0 libdd-trace-model/Cargo.toml
+20 −0 libdd-trace-model/README.md
+363 −0 libdd-trace-model/src/lib.rs
+6 −1 libdd-trace-utils/Cargo.toml
+25 −9 libdd-trace-utils/benches/vec_map_bench.rs
+5 −5 libdd-trace-utils/src/agentless_encoder/mod.rs
+4 −4 libdd-trace-utils/src/agentless_encoder/tests_v1.rs
+7 −7 libdd-trace-utils/src/json_log_encoder/span_v1.rs
+1 −0 libdd-trace-utils/src/lib.rs
+3 −3 libdd-trace-utils/src/msgpack_decoder/v1/mod.rs
+4 −4 libdd-trace-utils/src/msgpack_decoder/v1/span.rs
+13 −13 libdd-trace-utils/src/msgpack_encoder/v04/span_v1.rs
+4 −4 libdd-trace-utils/src/msgpack_encoder/v1/mod.rs
+2 −2 libdd-trace-utils/src/msgpack_encoder/v1/span_v1.rs
+4 −4 libdd-trace-utils/src/otlp_encoder/mapper_v1.rs
+24 −6 libdd-trace-utils/src/span/mod.rs
+1 −31 libdd-trace-utils/src/span/v1/mod.rs
+263 −27 libdd-trace-utils/src/span/vec_map.rs
+367 −0 libdd-trace-utils/src/stack_alloc.rs
+1 −1 libdd-trace-utils/src/tracer_payload.rs
+7 −7 libdd-trace-utils/tests/test_send_data.rs
+98 −0 scripts/split_lcov.sh
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_EXPERIMENTATION_OPT_OUT_MODE": [
{
"implementation": "A",
"type": "string",
"default": "disabled"
}
],
"DD_GIT_COMMIT_SHA": [
{
"implementation": "B",
Expand Down
9 changes: 7 additions & 2 deletions src/DDTrace/OpenFeature/DataDogProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ final class DataDogProvider extends AbstractProvider
private bool $warnedAboutRuntimeNotReady = false;
private EvaluationMetricRecorder $metricRecorder;

public function __construct(?LoggerInterface $logger = null)
public function __construct(?LoggerInterface $logger = null, ?string $experimentationOptOutMode = 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->evaluator = NativeEvaluator::create(false, $experimentationOptOutMode);
$this->datadogLogger = new NonThrowingLogger($logger ?: GlobalLogger::get());
$this->metricRecorder = EvaluationMetricRecorder::createDefault();
}
Expand Down Expand Up @@ -204,6 +204,10 @@ private function normalizeContext(?EvaluationContext $context): array

$attributes = [];
foreach ($context->getAttributes()->toArray() as $key => $value) {
if ($key === 'dd_experimentation_opt_out_at') {
$attributes[$key] = $value;
continue;
}
if (is_bool($value) || is_int($value) || is_float($value) || is_string($value)) {
$attributes[(string) $key] = $value;
}
Expand Down Expand Up @@ -254,6 +258,7 @@ private function mapErrorCode(string $errorCode): ErrorCode
EvaluationErrorCode::FLAG_NOT_FOUND => ErrorCode::FLAG_NOT_FOUND(),
EvaluationErrorCode::PARSE_ERROR => ErrorCode::PARSE_ERROR(),
EvaluationErrorCode::TYPE_MISMATCH => ErrorCode::TYPE_MISMATCH(),
EvaluationErrorCode::INVALID_CONTEXT => ErrorCode::INVALID_CONTEXT(),
default => ErrorCode::GENERAL(),
};
}
Expand Down
8 changes: 6 additions & 2 deletions src/api/FeatureFlags/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ final class Client
private $logger;
private $warnedAboutRuntimeNotReady = false;

public function __construct($logger = null)
public function __construct($logger = null, $experimentationOptOutMode = null)
{
if ($logger !== null && !$logger instanceof LoggerInterface) {
throw new \InvalidArgumentException('Expected a LoggerInterface instance');
}

$this->evaluator = NativeEvaluator::create();
$this->evaluator = NativeEvaluator::create(true, $experimentationOptOutMode);
$this->logger = new NonThrowingLogger($logger ?: GlobalLogger::get());
}

Expand Down Expand Up @@ -132,6 +132,10 @@ private function normalizeContext(array $context)
$attributes = array();
if (isset($context['attributes']) && is_array($context['attributes'])) {
foreach ($context['attributes'] as $key => $value) {
if ($key === 'dd_experimentation_opt_out_at') {
$attributes[$key] = $value;
continue;
}
if (is_bool($value) || is_int($value) || is_float($value) || is_string($value)) {
$attributes[(string) $key] = $value;
}
Expand Down
2 changes: 2 additions & 0 deletions src/api/FeatureFlags/EvaluationErrorCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ final class EvaluationErrorCode
const TYPE_MISMATCH = 'TYPE_MISMATCH';
const GENERAL = 'GENERAL';
const PROVIDER_NOT_READY = 'PROVIDER_NOT_READY';
const INVALID_CONTEXT = 'INVALID_CONTEXT';

private static $valid = array(
self::FLAG_NOT_FOUND => true,
self::PARSE_ERROR => true,
self::TYPE_MISMATCH => true,
self::GENERAL => true,
self::PROVIDER_NOT_READY => true,
self::INVALID_CONTEXT => true,
);

private function __construct()
Expand Down
Loading
Loading