diff --git a/Cargo.lock b/Cargo.lock index c5d34a05d3e..cad47d404a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,6 +143,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" +[[package]] +name = "ascii" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97be891acc47ca214468e09425d02cef3af2c94d0d82081cd02061f996802f14" + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -209,6 +215,15 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.4.0", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -267,8 +282,8 @@ dependencies = [ "itoa", "matchit", "memchr", - "mime", - "percent-encoding", + "mime 0.3.17", + "percent-encoding 2.3.1", "pin-project-lite", "serde_core", "sync_wrapper", @@ -288,7 +303,7 @@ dependencies = [ "http", "http-body", "http-body-util", - "mime", + "mime 0.3.17", "pin-project-lite", "sync_wrapper", "tower-layer", @@ -310,6 +325,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" +dependencies = [ + "byteorder", + "safemem", +] + [[package]] name = "base64" version = "0.21.7" @@ -343,7 +368,7 @@ dependencies = [ "itertools 0.12.1", "lazy_static", "lazycell", - "log", + "log 0.4.25", "prettyplease", "proc-macro2", "quote", @@ -401,6 +426,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "bolero" version = "0.13.0" @@ -495,6 +529,16 @@ dependencies = [ "cc", ] +[[package]] +name = "buf_redux" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" +dependencies = [ + "memchr", + "safemem", +] + [[package]] name = "build_common" version = "0.0.1" @@ -581,7 +625,7 @@ dependencies = [ "clap", "heck 0.4.1", "indexmap 2.12.1", - "log", + "log 0.4.25", "proc-macro2", "quote", "serde", @@ -600,7 +644,7 @@ dependencies = [ "clap", "heck 0.5.0", "indexmap 2.12.1", - "log", + "log 0.4.25", "proc-macro2", "quote", "serde", @@ -672,6 +716,12 @@ dependencies = [ "windows-link 0.1.1", ] +[[package]] +name = "chunked_transfer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498d20a7aaf62625b9bf26e637cf7736417cde1d0c99f1d04d1170229a85cf87" + [[package]] name = "ciborium" version = "0.2.2" @@ -750,6 +800,15 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "cmake" version = "0.1.57" @@ -1149,6 +1208,27 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "datadog-ffe" +version = "1.0.0" +dependencies = [ + "chrono", + "criterion", + "derive_more", + "env_logger 0.10.2", + "faststr", + "log 0.4.25", + "md5", + "pyo3", + "regex", + "serde", + "serde-bool", + "serde_json", + "serde_with", + "thiserror 2.0.12", + "url 2.5.4", +] + [[package]] name = "datadog-ipc" version = "0.1.0" @@ -1164,7 +1244,7 @@ dependencies = [ "libdd-common 1.1.0", "libdd-tinybytes", "memfd", - "nix", + "nix 0.29.0", "page_size", "pin-project", "pretty_assertions", @@ -1197,10 +1277,10 @@ dependencies = [ "anyhow", "constcat", "http-body-util", - "hyper", + "hyper 1.6.0", "libdd-common 1.1.0", "libdd-data-pipeline", - "percent-encoding", + "percent-encoding 2.3.1", "regex", "regex-automata 0.4.9", "serde", @@ -1219,8 +1299,8 @@ dependencies = [ "datadog-live-debugger", "libdd-common 1.1.0", "libdd-common-ffi", - "log", - "percent-encoding", + "log 0.4.25", + "percent-encoding 2.3.1", "serde_json", "tokio", "tokio-util", @@ -1251,7 +1331,7 @@ dependencies = [ "libdd-common 1.1.0 (git+https://github.com/DataDog/libdatadog?tag=v26.0.0)", "libdd-library-config-ffi", "libdd-profiling", - "log", + "log 0.4.25", "perfcnt", "rand 0.8.5", "rand_distr", @@ -1269,22 +1349,24 @@ version = "0.0.1" dependencies = [ "anyhow", "base64 0.22.1", + "datadog-ffe", "datadog-live-debugger", "datadog-remote-config", "futures", "futures-util", "http", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-util", "libdd-common 1.1.0", "libdd-trace-protobuf", "manual_future", + "regex", "serde", "serde_json", "serde_with", "sha2", - "time", + "time 0.3.37", "tokio", "tokio-util", "tracing", @@ -1310,7 +1392,7 @@ dependencies = [ "http", "http-body-util", "httpmock", - "hyper", + "hyper 1.6.0", "libc 0.2.177", "libdd-common 1.1.0", "libdd-common-ffi", @@ -1324,7 +1406,7 @@ dependencies = [ "manual_future", "memory-stats", "microseh", - "nix", + "nix 0.29.0", "prctl", "priority-queue", "rand 0.8.5", @@ -1388,6 +1470,7 @@ dependencies = [ "bincode", "cbindgen 0.27.0", "const-str", + "datadog-ffe", "datadog-ipc", "datadog-live-debugger", "datadog-live-debugger-ffi", @@ -1408,7 +1491,7 @@ dependencies = [ "libdd-telemetry-ffi", "libdd-tinybytes", "libdd-trace-utils", - "log", + "log 0.4.25", "paste", "regex", "regex-automata 0.4.9", @@ -1455,6 +1538,27 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.96", +] + [[package]] name = "diff" version = "0.1.13" @@ -1471,6 +1575,16 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.8.0", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1541,7 +1655,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ - "log", + "log 0.4.25", ] [[package]] @@ -1552,7 +1666,7 @@ checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" dependencies = [ "humantime", "is-terminal", - "log", + "log 0.4.25", "regex", "termcolor", ] @@ -1564,7 +1678,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" dependencies = [ "env_filter", - "log", + "log 0.4.25", ] [[package]] @@ -1573,6 +1687,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.10" @@ -1616,6 +1741,17 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "faststr" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca7d44d22004409a61c393afb3369c8f7bb74abcae49fe249ee01dcc3002113" +dependencies = [ + "bytes", + "serde", + "simdutf8", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1671,7 +1807,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ - "percent-encoding", + "percent-encoding 2.3.1", ] [[package]] @@ -1887,11 +2023,17 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daa0a64d21a7eb230583b4c5f4e23b7e4e57974f96620f42a7e75e08ae66d745" dependencies = [ - "log", + "log 0.4.25", "plain", "scroll", ] +[[package]] +name = "groupable" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32619942b8be646939eaf3db0602b39f5229b74575b67efc897811ded1db4e57" + [[package]] name = "h2" version = "0.4.8" @@ -1988,7 +2130,7 @@ dependencies = [ "headers-core", "http", "httpdate", - "mime", + "mime 0.3.17", "sha1", ] @@ -2103,7 +2245,7 @@ dependencies = [ "headers", "http", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-util", "path-tree", "regex", @@ -2116,7 +2258,7 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tracing", - "url", + "url 2.5.4", ] [[package]] @@ -2125,6 +2267,25 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "0.10.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a0652d9a2609a968c14be1a9ea00bf4b1d64e2e1f53a1b51b6fff3a6e829273" +dependencies = [ + "base64 0.9.3", + "httparse", + "language-tags", + "log 0.3.9", + "mime 0.2.6", + "num_cpus", + "time 0.1.45", + "traitobject", + "typeable", + "unicase 1.4.2", + "url 1.7.2", +] + [[package]] name = "hyper" version = "1.6.0" @@ -2154,7 +2315,7 @@ checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", "http", - "hyper", + "hyper 1.6.0", "hyper-util", "rustls", "rustls-native-certs", @@ -2171,7 +2332,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" dependencies = [ - "hyper", + "hyper 1.6.0", "hyper-util", "pin-project-lite", "tokio", @@ -2191,10 +2352,10 @@ dependencies = [ "futures-util", "http", "http-body", - "hyper", + "hyper 1.6.0", "ipnet", "libc 0.2.177", - "percent-encoding", + "percent-encoding 2.3.1", "pin-project-lite", "socket2 0.5.10", "tokio", @@ -2349,6 +2510,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "1.0.3" @@ -2376,7 +2548,7 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ - "autocfg", + "autocfg 1.4.0", "hashbrown 0.12.3", "serde", ] @@ -2393,6 +2565,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + [[package]] name = "integer-encoding" version = "3.0.4" @@ -2426,6 +2607,22 @@ dependencies = [ "serde", ] +[[package]] +name = "iron" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6d308ca2d884650a8bf9ed2ff4cb13fbb2207b71f64cda11dc9b892067295e8" +dependencies = [ + "hyper 0.10.16", + "log 0.3.9", + "mime_guess 1.8.8", + "modifier", + "num_cpus", + "plugin", + "typemap", + "url 1.7.2", +] + [[package]] name = "is-terminal" version = "0.4.13" @@ -2486,7 +2683,7 @@ dependencies = [ "cfg-if", "combine", "jni-sys", - "log", + "log 0.4.25", "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", @@ -2527,6 +2724,12 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "language-tags" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" + [[package]] name = "lazy_static" version = "1.4.0" @@ -2575,15 +2778,20 @@ dependencies = [ "http", "http-body", "http-body-util", - "hyper", + "httparse", + "hyper 1.6.0", "hyper-rustls", "hyper-util", "indexmap 2.12.1", "libc 0.2.177", "maplit", - "nix", + "mime 0.3.17", + "multipart", + "nix 0.29.0", "pin-project", + "rand 0.8.5", "regex", + "reqwest", "rustls", "rustls-native-certs", "serde", @@ -2611,11 +2819,11 @@ dependencies = [ "http", "http-body", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-rustls", "hyper-util", "libc 0.2.177", - "nix", + "nix 0.29.0", "pin-project", "regex", "rustls", @@ -2640,7 +2848,7 @@ dependencies = [ "chrono", "crossbeam-queue", "function_name", - "hyper", + "hyper 1.6.0", "libdd-common 1.1.0", "serde", ] @@ -2662,7 +2870,7 @@ dependencies = [ "libc 0.2.177", "libdd-common 1.1.0", "libdd-telemetry", - "nix", + "nix 0.29.0", "num-derive", "num-traits", "os_info", @@ -2713,7 +2921,7 @@ dependencies = [ "http", "http-body-util", "httpmock", - "hyper", + "hyper 1.6.0", "hyper-util", "libdd-common 1.1.0", "libdd-ddsketch", @@ -2818,7 +3026,7 @@ dependencies = [ "libdd-alloc", "libdd-common 1.1.0 (git+https://github.com/DataDog/libdatadog?tag=v26.0.0)", "libdd-profiling-protobuf", - "mime", + "mime 0.3.17", "parking_lot", "prost", "rand 0.8.5", @@ -2851,7 +3059,7 @@ dependencies = [ "hashbrown 0.15.2", "http", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-util", "libc 0.2.177", "libdd-common 1.1.0", @@ -2947,7 +3155,7 @@ dependencies = [ "http", "http-body-util", "httpmock", - "hyper", + "hyper 1.6.0", "indexmap 2.12.1", "libdd-common 1.1.0", "libdd-tinybytes", @@ -3017,15 +3225,28 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ - "autocfg", + "autocfg 1.4.0", "scopeguard", ] +[[package]] +name = "log" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" +dependencies = [ + "log 0.4.25", +] + [[package]] name = "log" version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +dependencies = [ + "serde", + "value-bag", +] [[package]] name = "lru-slab" @@ -3057,12 +3278,24 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + [[package]] name = "matchit" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.7.4" @@ -3093,7 +3326,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ - "autocfg", + "autocfg 1.4.0", ] [[package]] @@ -3117,20 +3350,41 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "mime" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba626b8a6de5da682e1caa06bdb42a335aee5a84db8e5046a3e8ab17ba0a3ae0" +dependencies = [ + "log 0.3.9", +] + [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "1.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216929a5ee4dd316b1702eedf5e74548c123d370f47841ceaac38ca154690ca3" +dependencies = [ + "mime 0.2.6", + "phf 0.7.24", + "phf_codegen 0.7.24", + "unicase 1.4.2", +] + [[package]] name = "mime_guess" version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ - "mime", - "unicase", + "mime 0.3.17", + "unicase 2.8.1", ] [[package]] @@ -3170,6 +3424,12 @@ dependencies = [ "tempdir", ] +[[package]] +name = "modifier" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f5c9112cb662acd3b204077e0de5bc66305fa8df65c8019d5adb10e9ab6e58" + [[package]] name = "msvc-demangler" version = "0.10.1" @@ -3185,6 +3445,59 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "multipart" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182" +dependencies = [ + "buf_redux", + "httparse", + "hyper 0.10.16", + "iron", + "log 0.4.25", + "mime 0.3.17", + "mime_guess 2.0.5", + "nickel", + "quick-error", + "rand 0.8.5", + "safemem", + "tempfile", + "tiny_http", + "twoway", +] + +[[package]] +name = "mustache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51956ef1c5d20a1384524d91e616fb44dfc7d8f249bf696d49c97dd3289ecab5" +dependencies = [ + "log 0.3.9", + "serde", +] + +[[package]] +name = "nickel" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5061a832728db2dacb61cefe0ce303b58f85764ec680e71d9138229640a46d9" +dependencies = [ + "groupable", + "hyper 0.10.16", + "lazy_static", + "log 0.3.9", + "modifier", + "mustache", + "plugin", + "regex", + "serde", + "serde_json", + "time 0.1.45", + "typemap", + "url 1.7.2", +] + [[package]] name = "nix" version = "0.29.0" @@ -3198,6 +3511,18 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.8.0", + "cfg-if", + "cfg_aliases", + "libc 0.2.177", +] + [[package]] name = "nom" version = "4.2.3" @@ -3270,7 +3595,7 @@ version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "autocfg", + "autocfg 1.4.0", "libm", ] @@ -3285,74 +3610,233 @@ dependencies = [ ] [[package]] -name = "object" -version = "0.31.1" +name = "objc2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" dependencies = [ - "flate2", - "memchr", - "ruzstd", + "objc2-encode", ] [[package]] -name = "object" -version = "0.36.7" +name = "objc2-cloud-kit" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "memchr", + "bitflags 2.8.0", + "objc2", + "objc2-foundation", ] [[package]] -name = "once_cell" -version = "1.20.2" +name = "objc2-core-data" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] [[package]] -name = "oorandom" -version = "11.1.4" +name = "objc2-core-foundation" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.8.0", + "dispatch2", + "objc2", +] [[package]] -name = "openssl-probe" -version = "0.1.5" +name = "objc2-core-graphics" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.8.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] [[package]] -name = "opentelemetry" -version = "0.17.0" +name = "objc2-core-image" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6105e89802af13fdf48c49d7646d3b533a70e536d818aae7e78ba0433d01acb8" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" dependencies = [ - "async-trait", - "crossbeam-channel", - "futures-channel", - "futures-executor", - "futures-util", - "js-sys", - "lazy_static", - "percent-encoding", - "pin-project", - "rand 0.8.5", - "thiserror 1.0.69", - "tokio", - "tokio-stream", + "objc2", + "objc2-foundation", ] [[package]] -name = "opentelemetry-jaeger" -version = "0.16.0" +name = "objc2-core-location" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c0b12cd9e3f9b35b52f6e0dac66866c519b26f424f4bbf96e3fe8bfbdc5229" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" dependencies = [ - "async-trait", - "lazy_static", - "opentelemetry", - "opentelemetry-semantic-conventions", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.8.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.8.0", + "block2", + "libc 0.2.177", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.8.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.8.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.8.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "flate2", + "memchr", + "ruzstd", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "oorandom" +version = "11.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "opentelemetry" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6105e89802af13fdf48c49d7646d3b533a70e536d818aae7e78ba0433d01acb8" +dependencies = [ + "async-trait", + "crossbeam-channel", + "futures-channel", + "futures-executor", + "futures-util", + "js-sys", + "lazy_static", + "percent-encoding 2.3.1", + "pin-project", + "rand 0.8.5", + "thiserror 1.0.69", + "tokio", + "tokio-stream", +] + +[[package]] +name = "opentelemetry-jaeger" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c0b12cd9e3f9b35b52f6e0dac66866c519b26f424f4bbf96e3fe8bfbdc5229" +dependencies = [ + "async-trait", + "lazy_static", + "opentelemetry", + "opentelemetry-semantic-conventions", "thiserror 1.0.69", "thrift", "tokio", @@ -3378,13 +3862,18 @@ dependencies = [ [[package]] name = "os_info" -version = "3.9.2" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e6520c8cc998c5741ee68ec1dc369fc47e5f0ea5320018ecf2a1ccd6328f48b" +checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" dependencies = [ - "log", + "android_system_properties", + "log 0.4.25", + "nix 0.30.1", + "objc2", + "objc2-foundation", + "objc2-ui-kit", "serde", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3447,6 +3936,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -3477,13 +3972,32 @@ dependencies = [ "indexmap 2.12.1", ] +[[package]] +name = "phf" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" +dependencies = [ + "phf_shared 0.7.24", +] + [[package]] name = "phf" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ac8b67553a7ca9457ce0e526948cad581819238f4a9d1ea74545851fa24f37" dependencies = [ - "phf_shared", + "phf_shared 0.9.0", +] + +[[package]] +name = "phf_codegen" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" +dependencies = [ + "phf_generator 0.7.24", + "phf_shared 0.7.24", ] [[package]] @@ -3492,8 +4006,18 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "963adb11cf22ee65dfd401cf75577c1aa0eca58c0b97f9337d2da61d3e640503" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.9.1", + "phf_shared 0.9.0", +] + +[[package]] +name = "phf_generator" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" +dependencies = [ + "phf_shared 0.7.24", + "rand 0.6.5", ] [[package]] @@ -3502,17 +4026,27 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d43f3220d96e0080cc9ea234978ccd80d904eafb17be31bb0f76daaea6493082" dependencies = [ - "phf_shared", + "phf_shared 0.9.0", "rand 0.8.5", ] +[[package]] +name = "phf_shared" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" +dependencies = [ + "siphasher 0.2.3", + "unicase 1.4.2", +] + [[package]] name = "phf_shared" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a68318426de33640f02be62b4ae8eb1261be2efbc337b60c54d845bf4484e0d9" dependencies = [ - "siphasher", + "siphasher 0.3.11", ] [[package]] @@ -3596,6 +4130,15 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "plugin" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a6a0dc3910bc8db877ffed8e457763b317cf880df4ae19109b9f77d277cf6e0" +dependencies = [ + "typemap", +] + [[package]] name = "portable-atomic" version = "1.10.0" @@ -3627,7 +4170,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "059a34f111a9dee2ce1ac2826a68b24601c4298cfeb1a587c3cb493d5ab46f52" dependencies = [ "libc 0.2.177", - "nix", + "nix 0.29.0", ] [[package]] @@ -3662,7 +4205,7 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "714c75db297bc88a63783ffc6ab9f830698a6705aa0201416931759ef4c8183d" dependencies = [ - "autocfg", + "autocfg 1.4.0", "equivalent", "indexmap 2.12.1", ] @@ -3720,7 +4263,7 @@ dependencies = [ "num-traits", "rand 0.8.5", "rand_chacha 0.3.1", - "rand_xorshift", + "rand_xorshift 0.3.0", "regex-syntax 0.8.5", "unarray", ] @@ -3743,7 +4286,7 @@ checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" dependencies = [ "heck 0.5.0", "itertools 0.12.1", - "log", + "log 0.4.25", "multimap", "petgraph", "prettyplease", @@ -3833,6 +4376,73 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dc55d7dec32ecaf61e0bd90b3d2392d721a28b95cfd23c3e176eccefbeab2f2" +[[package]] +name = "pyo3" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d" +dependencies = [ + "indoc", + "libc 0.2.177", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6" +dependencies = [ + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089" +dependencies = [ + "libc 0.2.177", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quinn" version = "0.11.9" @@ -3917,6 +4527,25 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.8", + "libc 0.2.177", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift 0.1.1", + "winapi 0.3.9", +] + [[package]] name = "rand" version = "0.8.5" @@ -3939,6 +4568,16 @@ dependencies = [ "zerocopy 0.8.24", ] +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.3.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -4002,6 +4641,68 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc 0.2.177", + "rand_core 0.4.2", + "winapi 0.3.9", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc 0.2.177", + "rand_core 0.4.2", + "rdrand", + "winapi 0.3.9", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "rand_xorshift" version = "0.3.0" @@ -4153,13 +4854,13 @@ dependencies = [ "http", "http-body", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-rustls", "hyper-util", "js-sys", - "log", - "mime_guess", - "percent-encoding", + "log 0.4.25", + "mime_guess 2.0.5", + "percent-encoding 2.3.1", "pin-project-lite", "quinn", "rustls", @@ -4171,7 +4872,7 @@ dependencies = [ "tower", "tower-http", "tower-service", - "url", + "url 2.5.4", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -4250,6 +4951,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.43" @@ -4322,7 +5032,7 @@ dependencies = [ "core-foundation", "core-foundation-sys", "jni", - "log", + "log 0.4.25", "once_cell", "rustls", "rustls-native-certs", @@ -4375,6 +5085,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + [[package]] name = "same-file" version = "1.0.6" @@ -4501,6 +5217,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-bool" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fdd050c9c2ed5ae1fb29e71be0a6efdd9df43c7cb13ea5826528cfe10c51db0" +dependencies = [ + "serde", +] + [[package]] name = "serde_bytes" version = "0.11.15" @@ -4541,6 +5266,15 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "serde_fmt" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e497af288b3b95d067a23a4f749f2861121ffcb2f6d8379310dcda040c345ed" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_json" version = "1.0.137" @@ -4587,7 +5321,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_with_macros", - "time", + "time 0.3.37", ] [[package]] @@ -4701,6 +5435,12 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "siphasher" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" + [[package]] name = "siphasher" version = "0.3.11" @@ -4713,7 +5453,7 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ - "autocfg", + "autocfg 1.4.0", ] [[package]] @@ -4753,7 +5493,7 @@ dependencies = [ "kernel32-sys", "libc 0.2.177", "memfd", - "nix", + "nix 0.29.0", "rlimit", "tempfile", "winapi 0.2.8", @@ -4790,6 +5530,84 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "sval" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1aaf178a50bbdd86043fce9bf0a5867007d9b382db89d1c96ccae4601ff1ff9" + +[[package]] +name = "sval_buffer" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89273e48f03807ebf51c4d81c52f28d35ffa18a593edf97e041b52de143df89" +dependencies = [ + "sval", + "sval_ref", +] + +[[package]] +name = "sval_dynamic" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0430f4e18e7eba21a49d10d25a8dec3ce0e044af40b162347e99a8e3c3ced864" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_fmt" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835f51b9d7331b9d7fc48fc716c02306fa88c4a076b1573531910c91a525882d" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_json" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13cbfe3ef406ee2366e7e8ab3678426362085fa9eaedf28cb878a967159dced3" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_nested" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b20358af4af787c34321a86618c3cae12eabdd0e9df22cd9dd2c6834214c518" +dependencies = [ + "sval", + "sval_buffer", + "sval_ref", +] + +[[package]] +name = "sval_ref" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5e500f8eb2efa84f75e7090f7fc43f621b9f8b6cde571c635b3855f97b332a" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_serde" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2032ae39b11dcc6c18d5fbc50a661ea191cac96484c59ccf49b002261ca2c1" +dependencies = [ + "serde_core", + "sval", + "sval_nested", +] + [[package]] name = "symbolic-common" version = "12.13.3" @@ -4875,6 +5693,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "target-lexicon" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1dd07eb858a2067e2f3c7155d54e929265c264e6f37efe3ee7a8d1b5a1dd0ba" + [[package]] name = "target-triple" version = "0.1.4" @@ -5047,11 +5871,22 @@ checksum = "b82ca8f46f95b3ce96081fe3dd89160fdea970c254bb72925255d1b62aae692e" dependencies = [ "byteorder", "integer-encoding", - "log", + "log 0.4.25", "ordered-float", "threadpool", ] +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc 0.2.177", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi 0.3.9", +] + [[package]] name = "time" version = "0.3.37" @@ -5083,6 +5918,19 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny_http" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e22cb179b63e5fc2d0b5be237dc107da072e2407809ac70a8ce85b93fe8f562" +dependencies = [ + "ascii", + "chrono", + "chunked_transfer", + "log 0.4.25", + "url 1.7.2", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -5260,10 +6108,10 @@ dependencies = [ "http", "http-body", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-timeout", "hyper-util", - "percent-encoding", + "percent-encoding 2.3.1", "pin-project", "socket2 0.6.2", "sync_wrapper", @@ -5341,7 +6189,7 @@ version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ - "log", + "log 0.4.25", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -5355,7 +6203,7 @@ checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" dependencies = [ "crossbeam-channel", "thiserror 1.0.69", - "time", + "time 0.3.37", "tracing-subscriber", ] @@ -5386,7 +6234,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ - "log", + "log 0.4.25", "once_cell", "tracing-core", ] @@ -5435,6 +6283,12 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "traitobject" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04a79e25382e2e852e8da874249358d382ebaf259d0d34e75d8db16a7efabbc7" + [[package]] name = "try-lock" version = "0.2.5" @@ -5456,6 +6310,15 @@ dependencies = [ "toml", ] +[[package]] +name = "twoway" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" +dependencies = [ + "memchr", +] + [[package]] name = "twox-hash" version = "1.6.3" @@ -5466,6 +6329,27 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "typeable" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typemap" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653be63c80a3296da5551e1bfd2cca35227e13cdd08c6668903ae2f4f77aa1f6" +dependencies = [ + "unsafe-any", +] + [[package]] name = "typenum" version = "1.17.0" @@ -5478,18 +6362,42 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "unicase" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" +dependencies = [ + "version_check 0.1.5", +] + [[package]] name = "unicase" version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-width" version = "0.2.1" @@ -5502,6 +6410,21 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] +name = "unsafe-any" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30360d7979f5e9c6e6cea48af192ea8fab4afb3cf72597154b8f08935bc9c7f" +dependencies = [ + "traitobject", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -5514,6 +6437,17 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "url" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" +dependencies = [ + "idna 0.1.5", + "matches", + "percent-encoding 1.0.1", +] + [[package]] name = "url" version = "2.5.4" @@ -5521,8 +6455,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna", - "percent-encoding", + "idna 1.0.3", + "percent-encoding 2.3.1", ] [[package]] @@ -5565,6 +6499,42 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16530907bfe2999a1773ca5900a65101e092c70f642f25cc23ca0c43573262c5" +dependencies = [ + "erased-serde", + "serde_core", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00ae130edd690eaa877e4f40605d534790d1cf1d651e7685bd6a144521b251f" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] + [[package]] name = "value-trait" version = "0.10.1" @@ -5608,6 +6578,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -5642,7 +6618,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", - "log", + "log 0.4.25", "proc-macro2", "quote", "syn 2.0.96", @@ -6268,8 +7244,8 @@ dependencies = [ "bit_field", "bitflags 1.3.2", "csv", - "phf", - "phf_codegen", + "phf 0.9.0", + "phf_codegen 0.9.0", "raw-cpuid", "serde_json", ] diff --git a/Makefile b/Makefile index 922218229ee..d4b81f88289 100644 --- a/Makefile +++ b/Makefile @@ -48,7 +48,7 @@ RUN_TESTS_CMD := DD_SERVICE= DD_ENV= REPORT_EXIT_STATUS=1 TEST_PHP_SRCDIR=$(PROJ C_FILES = $(shell find components components-rs ext src/dogstatsd zend_abstract_interface -name '*.c' -o -name '*.h' | awk '{ printf "$(BUILD_DIR)/%s\n", $$1 }' ) TEST_FILES = $(shell find tests/ext -name '*.php*' -o -name '*.inc' -o -name '*.json' -o -name '*.yaml' -o -name 'CONFLICTS' | awk '{ printf "$(BUILD_DIR)/%s\n", $$1 }' ) -RUST_FILES = $(BUILD_DIR)/Cargo.toml $(BUILD_DIR)/Cargo.lock $(shell find components-rs -name '*.c' -o -name '*.rs' -o -name 'Cargo.toml' | awk '{ printf "$(BUILD_DIR)/%s\n", $$1 }' ) $(shell find libdatadog/{build-common,datadog-ipc,datadog-ipc-macros,datadog-live-debugger,datadog-live-debugger-ffi,datadog-remote-config,datadog-sidecar,datadog-sidecar-ffi,datadog-sidecar-macros,libdd-alloc,libdd-common,libdd-common-ffi,libdd-crashtracker,libdd-crashtracker-ffi,libdd-data-pipeline,libdd-ddsketch,libdd-dogstatsd-client,libdd-library-config,libdd-library-config-ffi,libdd-log,libdd-telemetry,libdd-telemetry-ffi,libdd-tinybytes,libdd-trace-*,spawn_worker,tools/{cc_utils,sidecar_mockgen},libdd-trace-*,Cargo.toml} \( -type l -o -type f \) \( -path "*/src*" -o -path "*/examples*" -o -path "*Cargo.toml" -o -path "*/build.rs" -o -path "*/tests/dataservice.rs" -o -path "*/tests/service_functional.rs" \) -not -path "*/datadog-ipc/build.rs" -not -path "*/datadog-sidecar-ffi/build.rs") +RUST_FILES = $(BUILD_DIR)/Cargo.toml $(BUILD_DIR)/Cargo.lock $(shell find components-rs -name '*.c' -o -name '*.rs' -o -name 'Cargo.toml' | awk '{ printf "$(BUILD_DIR)/%s\n", $$1 }' ) $(shell find libdatadog/{build-common,datadog-ffe,datadog-ipc,datadog-ipc-macros,datadog-live-debugger,datadog-live-debugger-ffi,datadog-remote-config,datadog-sidecar,datadog-sidecar-ffi,datadog-sidecar-macros,libdd-alloc,libdd-common,libdd-common-ffi,libdd-crashtracker,libdd-crashtracker-ffi,libdd-data-pipeline,libdd-ddsketch,libdd-dogstatsd-client,libdd-library-config,libdd-library-config-ffi,libdd-log,libdd-telemetry,libdd-telemetry-ffi,libdd-tinybytes,libdd-trace-*,spawn_worker,tools/{cc_utils,sidecar_mockgen},libdd-trace-*,Cargo.toml} \( -type l -o -type f \) \( -path "*/src*" -o -path "*/examples*" -o -path "*Cargo.toml" -o -path "*/build.rs" -o -path "*/tests/dataservice.rs" -o -path "*/tests/service_functional.rs" \) -not -path "*/datadog-ipc/build.rs" -not -path "*/datadog-sidecar-ffi/build.rs") ALL_OBJECT_FILES = $(C_FILES) $(RUST_FILES) $(BUILD_DIR)/Makefile TEST_OPCACHE_FILES = $(shell find tests/opcache -name '*.php*' -o -name '.gitkeep' | awk '{ printf "$(BUILD_DIR)/%s\n", $$1 }' ) TEST_STUB_FILES = $(shell find tests/ext -type d -name 'stubs' -exec find '{}' -type f \; | awk '{ printf "$(BUILD_DIR)/%s\n", $$1 }' ) diff --git a/components-rs/Cargo.toml b/components-rs/Cargo.toml index 90b6851e1e4..49c6808c4d1 100644 --- a/components-rs/Cargo.toml +++ b/components-rs/Cargo.toml @@ -15,7 +15,7 @@ libdd-telemetry-ffi = { path = "../libdatadog/libdd-telemetry-ffi", default-feat datadog-live-debugger = { path = "../libdatadog/datadog-live-debugger" } datadog-live-debugger-ffi = { path = "../libdatadog/datadog-live-debugger-ffi", default-features = false } datadog-ipc = { path = "../libdatadog/datadog-ipc" } -datadog-remote-config = { path = "../libdatadog/datadog-remote-config" } +datadog-remote-config = { path = "../libdatadog/datadog-remote-config", features = ["ffe"] } datadog-sidecar = { path = "../libdatadog/datadog-sidecar" } datadog-sidecar-ffi = { path = "../libdatadog/datadog-sidecar-ffi" } libdd-tinybytes = { path = "../libdatadog/libdd-tinybytes" } @@ -23,6 +23,7 @@ libdd-trace-utils = { path = "../libdatadog/libdd-trace-utils" } libdd-crashtracker-ffi = { path = "../libdatadog/libdd-crashtracker-ffi", default-features = false, features = ["collector"] } libdd-library-config-ffi = { path = "../libdatadog/libdd-library-config-ffi", default-features = false } spawn_worker = { path = "../libdatadog/spawn_worker" } +datadog-ffe = { path = "../libdatadog/datadog-ffe" } anyhow = { version = "1.0" } const-str = "0.5.6" itertools = "0.11.0" diff --git a/components-rs/ddtrace.h b/components-rs/ddtrace.h index c0bab2a9eab..afaf8a5d85c 100644 --- a/components-rs/ddtrace.h +++ b/components-rs/ddtrace.h @@ -61,7 +61,8 @@ uint32_t ddog_get_logs_count(ddog_CharSlice level); void ddog_init_remote_config(bool live_debugging_enabled, bool appsec_activation, - bool appsec_config); + bool appsec_config, + bool ffe_enabled); struct ddog_RemoteConfigState *ddog_init_remote_config_state(const struct ddog_Endpoint *endpoint); @@ -69,6 +70,37 @@ const char *ddog_remote_config_get_path(const struct ddog_RemoteConfigState *rem bool ddog_process_remote_configs(struct ddog_RemoteConfigState *remote_config); +bool ddog_ffe_load_config(const char *json); + +bool ddog_ffe_has_config(void); + +bool ddog_ffe_config_changed(void); + +struct FfeResult; + +struct FfeAttribute { + const char *key; + int32_t value_type; /* 0=string, 1=number, 2=bool */ + const char *string_value; + double number_value; + bool bool_value; +}; + +struct FfeResult *ddog_ffe_evaluate( + const char *flag_key, + int32_t expected_type, + const char *targeting_key, + const struct FfeAttribute *attributes, + size_t attributes_count); + +const char *ddog_ffe_result_value(const struct FfeResult *r); +const char *ddog_ffe_result_variant(const struct FfeResult *r); +const char *ddog_ffe_result_allocation_key(const struct FfeResult *r); +int32_t ddog_ffe_result_reason(const struct FfeResult *r); +int32_t ddog_ffe_result_error_code(const struct FfeResult *r); +bool ddog_ffe_result_do_log(const struct FfeResult *r); +void ddog_ffe_free_result(struct FfeResult *r); + bool ddog_type_can_be_instrumented(const struct ddog_RemoteConfigState *remote_config, ddog_CharSlice typename_); diff --git a/components-rs/ffe.rs b/components-rs/ffe.rs new file mode 100644 index 00000000000..2c2089e14cd --- /dev/null +++ b/components-rs/ffe.rs @@ -0,0 +1,330 @@ +use datadog_ffe::rules_based::{ + self as ffe, AssignmentReason, AssignmentValue, Attribute, Configuration, EvaluationContext, + EvaluationError, ExpectedFlagType, Str, UniversalFlagConfig, +}; +use std::collections::HashMap; +use std::ffi::{c_char, CStr, CString}; +use std::sync::{Arc, Mutex}; + +/// Holds both the FFE configuration and a "changed" flag atomically behind a +/// single Mutex. This avoids the race where another thread could observe +/// `config` updated but `changed` still false (or vice-versa). +/// +/// A `RwLock` would be more appropriate here (many readers via `ddog_ffe_evaluate`, +/// rare writer via `store_config`), but PHP is single-threaded per process so +/// contention is not a practical concern. Keeping a Mutex for simplicity. +struct FfeState { + config: Option, + changed: bool, +} + +lazy_static::lazy_static! { + static ref FFE_STATE: Mutex = Mutex::new(FfeState { + config: None, + changed: false, + }); +} + +/// Called by remote_config when a new FFE configuration arrives via RC. +pub fn store_config(config: Configuration) { + if let Ok(mut state) = FFE_STATE.lock() { + state.config = Some(config); + state.changed = true; + } +} + +/// Called by remote_config when an FFE configuration is removed. +pub fn clear_config() { + if let Ok(mut state) = FFE_STATE.lock() { + state.config = None; + state.changed = true; + } +} + +/// Load a UFC JSON config string directly into the FFE engine. +/// Used by tests to load config without Remote Config. +#[no_mangle] +pub extern "C" fn ddog_ffe_load_config(json: *const c_char) -> bool { + if json.is_null() { + return false; + } + let json_str = match unsafe { CStr::from_ptr(json) }.to_str() { + Ok(s) => s, + Err(_) => return false, + }; + match UniversalFlagConfig::from_json(json_str.as_bytes().to_vec()) { + Ok(ufc) => { + store_config(Configuration::from_server_response(ufc)); + true + } + Err(_) => false, + } +} + +/// Check if FFE configuration is loaded. +#[no_mangle] +pub extern "C" fn ddog_ffe_has_config() -> bool { + FFE_STATE.lock().map(|s| s.config.is_some()).unwrap_or(false) +} + +/// Check if FFE config has changed since last check. +/// Resets the changed flag after reading. +#[no_mangle] +pub extern "C" fn ddog_ffe_config_changed() -> bool { + if let Ok(mut state) = FFE_STATE.lock() { + let was_changed = state.changed; + state.changed = false; + was_changed + } else { + false + } +} + +// Reason codes returned to PHP via ddog_ffe_result_reason(). +// Must match Provider::$REASON_MAP in src/DDTrace/FeatureFlags/Provider.php. +const REASON_STATIC: i32 = 0; +const REASON_DEFAULT: i32 = 1; +const REASON_TARGETING_MATCH: i32 = 2; +const REASON_SPLIT: i32 = 3; +const REASON_DISABLED: i32 = 4; +const REASON_ERROR: i32 = 5; + +// Error codes returned to PHP via ddog_ffe_result_error_code(). +// 0 means no error. +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_CONFIG_MISSING: i32 = 6; +const ERROR_GENERAL: i32 = 7; + +// Attribute value types passed from C (matches FfeAttribute.value_type). +const ATTR_TYPE_STRING: i32 = 0; +const ATTR_TYPE_NUMBER: i32 = 1; +const ATTR_TYPE_BOOL: i32 = 2; + +// Expected flag type IDs passed from C (matches Provider::$TYPE_MAP). +const TYPE_STRING: i32 = 0; +const TYPE_INTEGER: i32 = 1; +const TYPE_FLOAT: i32 = 2; +const TYPE_BOOLEAN: i32 = 3; +const TYPE_OBJECT: i32 = 4; + +/// Opaque handle for FFE evaluation results returned to C/PHP. +pub struct FfeResult { + pub value_json: CString, + pub variant: Option, + pub allocation_key: Option, + pub reason: i32, + pub error_code: i32, + pub do_log: bool, +} + +/// A single attribute passed from C/PHP for building an EvaluationContext. +#[repr(C)] +pub struct FfeAttribute { + pub key: *const c_char, + /// 0 = string, 1 = number, 2 = bool + pub value_type: i32, + pub string_value: *const c_char, + pub number_value: f64, + pub bool_value: bool, +} + +/// Evaluate a feature flag using the stored Configuration. +/// +/// Accepts structured attributes from C instead of a JSON blob. +/// `targeting_key` may be null (no targeting key). +/// `attributes` / `attributes_count` describe an array of `FfeAttribute`. +/// Returns null if no config is loaded. +#[no_mangle] +pub extern "C" fn ddog_ffe_evaluate( + flag_key: *const c_char, + expected_type: i32, + targeting_key: *const c_char, + attributes: *const FfeAttribute, + attributes_count: usize, +) -> *mut FfeResult { + if flag_key.is_null() { + return std::ptr::null_mut(); + } + let flag_key = match unsafe { CStr::from_ptr(flag_key) }.to_str() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + + let expected_type = match expected_type { + TYPE_STRING => ExpectedFlagType::String, + TYPE_INTEGER => ExpectedFlagType::Integer, + TYPE_FLOAT => ExpectedFlagType::Float, + TYPE_BOOLEAN => ExpectedFlagType::Boolean, + TYPE_OBJECT => ExpectedFlagType::Object, + _ => return std::ptr::null_mut(), + }; + + // Build targeting key + let tk = if targeting_key.is_null() { + None + } else { + match unsafe { CStr::from_ptr(targeting_key) }.to_str() { + Ok(s) if !s.is_empty() => Some(Str::from(s)), + _ => None, + } + }; + + // Build attributes map from the C array + let mut attrs = HashMap::new(); + if !attributes.is_null() && attributes_count > 0 { + let slice = unsafe { std::slice::from_raw_parts(attributes, attributes_count) }; + for attr in slice { + if attr.key.is_null() { + continue; + } + let key = match unsafe { CStr::from_ptr(attr.key) }.to_str() { + Ok(s) => s, + Err(_) => continue, + }; + let value = match attr.value_type { + ATTR_TYPE_STRING => { + if attr.string_value.is_null() { + continue; + } + match unsafe { CStr::from_ptr(attr.string_value) }.to_str() { + Ok(s) => Attribute::from(s), + Err(_) => continue, + } + } + ATTR_TYPE_NUMBER => Attribute::from(attr.number_value), + ATTR_TYPE_BOOL => Attribute::from(attr.bool_value), + _ => continue, + }; + attrs.insert(Str::from(key), value); + } + } + + let context = EvaluationContext::new(tk, Arc::new(attrs)); + + let state = match FFE_STATE.lock() { + Ok(s) => s, + Err(_) => return std::ptr::null_mut(), + }; + + let assignment = ffe::get_assignment( + state.config.as_ref(), + flag_key, + &context, + expected_type, + ffe::now(), + ); + + let result = match assignment { + Ok(a) => FfeResult { + value_json: CString::new(assignment_value_to_json(&a.value)).unwrap_or_default(), + variant: Some(CString::new(a.variation_key.as_str()).unwrap_or_default()), + allocation_key: Some(CString::new(a.allocation_key.as_str()).unwrap_or_default()), + reason: match a.reason { + AssignmentReason::Static => REASON_STATIC, + AssignmentReason::TargetingMatch => REASON_TARGETING_MATCH, + AssignmentReason::Split => REASON_SPLIT, + }, + error_code: ERROR_NONE, + do_log: a.do_log, + }, + Err(err) => { + let (error_code, reason) = match &err { + EvaluationError::TypeMismatch { .. } => (ERROR_TYPE_MISMATCH, REASON_ERROR), + EvaluationError::ConfigurationParseError => (ERROR_CONFIG_PARSE, REASON_ERROR), + EvaluationError::ConfigurationMissing => (ERROR_CONFIG_MISSING, REASON_ERROR), + EvaluationError::FlagUnrecognizedOrDisabled => (ERROR_FLAG_UNRECOGNIZED, REASON_DEFAULT), + EvaluationError::FlagDisabled => (ERROR_NONE, REASON_DISABLED), + EvaluationError::DefaultAllocationNull => (ERROR_NONE, REASON_DEFAULT), + _ => (ERROR_GENERAL, REASON_ERROR), + }; + FfeResult { + value_json: CString::new("null").unwrap_or_default(), + variant: None, + allocation_key: None, + reason, + error_code, + do_log: false, + } + } + }; + + Box::into_raw(Box::new(result)) +} + +#[no_mangle] +pub extern "C" fn ddog_ffe_result_value(r: *const FfeResult) -> *const c_char { + if r.is_null() { + return std::ptr::null(); + } + unsafe { &*r }.value_json.as_ptr() +} + +#[no_mangle] +pub extern "C" fn ddog_ffe_result_variant(r: *const FfeResult) -> *const c_char { + if r.is_null() { + return std::ptr::null(); + } + unsafe { &*r } + .variant + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(std::ptr::null()) +} + +#[no_mangle] +pub extern "C" fn ddog_ffe_result_allocation_key(r: *const FfeResult) -> *const c_char { + if r.is_null() { + return std::ptr::null(); + } + unsafe { &*r } + .allocation_key + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(std::ptr::null()) +} + +#[no_mangle] +pub extern "C" fn ddog_ffe_result_reason(r: *const FfeResult) -> i32 { + if r.is_null() { + return -1; + } + unsafe { &*r }.reason +} + +#[no_mangle] +pub extern "C" fn ddog_ffe_result_error_code(r: *const FfeResult) -> i32 { + if r.is_null() { + return -1; + } + unsafe { &*r }.error_code +} + +#[no_mangle] +pub extern "C" fn ddog_ffe_result_do_log(r: *const FfeResult) -> bool { + if r.is_null() { + return false; + } + unsafe { &*r }.do_log +} + +#[no_mangle] +pub unsafe extern "C" fn ddog_ffe_free_result(r: *mut FfeResult) { + if !r.is_null() { + drop(Box::from_raw(r)); + } +} + +fn assignment_value_to_json(value: &AssignmentValue) -> String { + match value { + AssignmentValue::String(s) => serde_json::to_string(s.as_str()).unwrap_or_default(), + AssignmentValue::Integer(i) => i.to_string(), + AssignmentValue::Float(f) => serde_json::Number::from_f64(*f) + .map(|n| n.to_string()) + .unwrap_or_else(|| f.to_string()), + AssignmentValue::Boolean(b) => b.to_string(), + AssignmentValue::Json { raw, .. } => raw.get().to_string(), + } +} diff --git a/components-rs/lib.rs b/components-rs/lib.rs index 07ff0cb0223..8ae73f7c583 100644 --- a/components-rs/lib.rs +++ b/components-rs/lib.rs @@ -5,6 +5,7 @@ pub mod log; pub mod remote_config; +pub mod ffe; pub mod sidecar; pub mod telemetry; pub mod bytes; diff --git a/components-rs/remote_config.rs b/components-rs/remote_config.rs index 75e97b019d2..1193d127ced 100644 --- a/components-rs/remote_config.rs +++ b/components-rs/remote_config.rs @@ -31,6 +31,7 @@ use std::ptr::NonNull; use std::sync::Arc; use tracing::debug; use crate::bytes::{ZendString, OwnedZendString, dangling_zend_string}; +use datadog_ffe::rules_based::Configuration; pub const DYANMIC_CONFIG_UPDATE_UNMODIFIED: *mut ZendString = 1isize as *mut ZendString; @@ -101,6 +102,7 @@ pub unsafe extern "C" fn ddog_init_remote_config( live_debugging_enabled: bool, appsec_activation: bool, appsec_config: bool, + ffe_enabled: bool, ) { DDTRACE_REMOTE_CONFIG_PRODUCTS.push(RemoteConfigProduct::ApmTracing); DDTRACE_REMOTE_CONFIG_CAPABILITIES.push(RemoteConfigCapabilities::ApmTracingCustomTags); @@ -117,6 +119,11 @@ pub unsafe extern "C" fn ddog_init_remote_config( DDTRACE_REMOTE_CONFIG_CAPABILITIES.push(RemoteConfigCapabilities::AsmActivation); } + if ffe_enabled { + DDTRACE_REMOTE_CONFIG_PRODUCTS.push(RemoteConfigProduct::FfeFlags); + DDTRACE_REMOTE_CONFIG_CAPABILITIES.push(RemoteConfigCapabilities::FfeFlagConfigurationRules); + } + if live_debugging_enabled { DDTRACE_REMOTE_CONFIG_PRODUCTS.push(RemoteConfigProduct::LiveDebugger) } @@ -348,6 +355,11 @@ pub extern "C" fn ddog_process_remote_configs(remote_config: &mut RemoteConfigSt remote_config.dynamic_config.active_config_path = Some(value.config_id); } } + RemoteConfigData::FfeFlags(ufc) => { + debug!("Received FFE flags configuration"); + let config = Configuration::from_server_response(ufc); + crate::ffe::store_config(config); + } RemoteConfigData::Ignored(_) => (), RemoteConfigData::TracerFlareConfig(_) => {} RemoteConfigData::TracerFlareTask(_) => {} @@ -364,6 +376,10 @@ pub extern "C" fn ddog_process_remote_configs(remote_config: &mut RemoteConfigSt remove_old_configs(remote_config); } } + RemoteConfigProduct::FfeFlags => { + debug!("FFE flags configuration removed"); + crate::ffe::clear_config(); + } _ => (), }, } diff --git a/ext/configuration.h b/ext/configuration.h index 8984674f682..c2e6a83c1a9 100644 --- a/ext/configuration.h +++ b/ext/configuration.h @@ -264,6 +264,7 @@ enum ddtrace_sampling_rules_format { CONFIG(BOOL, DD_TRACE_RESOURCE_RENAMING_ENABLED, "false") \ CONFIG(BOOL, DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT, "false") \ CONFIG(BOOL, DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED, "false") \ + CONFIG(BOOL, DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED, "false") \ DD_INTEGRATIONS #ifndef _WIN32 diff --git a/ext/ddtrace.c b/ext/ddtrace.c index 98962d314b8..a6c4f3a3c26 100644 --- a/ext/ddtrace.c +++ b/ext/ddtrace.c @@ -128,6 +128,7 @@ bool ddtrace_has_excluded_module; static zend_module_entry *ddtrace_module; + #if PHP_VERSION_ID >= 80000 && PHP_VERSION_ID < 80200 static bool dd_has_other_observers; static int dd_observer_extension_backup = -1; @@ -3007,6 +3008,95 @@ PHP_FUNCTION(dd_trace_internal_fn) { ddtrace_metric_add_point(Z_STR_P(metric_name), zval_get_double(metric_value), Z_STR_P(tags)); RETVAL_TRUE; } + } else if (FUNCTION_NAME_MATCHES("ffe_has_config")) { + RETVAL_BOOL(ddog_ffe_has_config()); + } else if (FUNCTION_NAME_MATCHES("ffe_config_changed")) { + RETVAL_BOOL(ddog_ffe_config_changed()); + } else if (params_count == 1 && FUNCTION_NAME_MATCHES("ffe_load_config")) { + zval *json_zv = ZVAL_VARARG_PARAM(params, 0); + if (Z_TYPE_P(json_zv) == IS_STRING) { + RETVAL_BOOL(ddog_ffe_load_config(Z_STRVAL_P(json_zv))); + } + } else if (FUNCTION_NAME_MATCHES("ffe_evaluate") && params_count >= 4) { + /* ffe_evaluate(flag_key, type_id, targeting_key, attributes) */ + zval *flag_key_zv = ZVAL_VARARG_PARAM(params, 0); + zval *type_zv = ZVAL_VARARG_PARAM(params, 1); + zval *targeting_key_zv = ZVAL_VARARG_PARAM(params, 2); + zval *attrs_zv = ZVAL_VARARG_PARAM(params, 3); + if (Z_TYPE_P(flag_key_zv) == IS_STRING) { + int32_t type_id = (int32_t)zval_get_long(type_zv); + const char *targeting_key = NULL; + if (Z_TYPE_P(targeting_key_zv) == IS_STRING && Z_STRLEN_P(targeting_key_zv) > 0) { + targeting_key = Z_STRVAL_P(targeting_key_zv); + } + struct FfeAttribute *c_attrs = NULL; + size_t attrs_count = 0; + if (Z_TYPE_P(attrs_zv) == IS_ARRAY) { + HashTable *ht = Z_ARRVAL_P(attrs_zv); + attrs_count = zend_hash_num_elements(ht); + if (attrs_count > 0) { + c_attrs = ecalloc(attrs_count, sizeof(struct FfeAttribute)); + size_t idx = 0; + zend_string *key; + zval *val; + ZEND_HASH_FOREACH_STR_KEY_VAL(ht, key, val) { + if (!key || idx >= attrs_count) { continue; } + c_attrs[idx].key = ZSTR_VAL(key); + switch (Z_TYPE_P(val)) { + case IS_STRING: + c_attrs[idx].value_type = 0; + c_attrs[idx].string_value = Z_STRVAL_P(val); + break; + case IS_LONG: + c_attrs[idx].value_type = 1; + c_attrs[idx].number_value = (double)Z_LVAL_P(val); + break; + case IS_DOUBLE: + c_attrs[idx].value_type = 1; + c_attrs[idx].number_value = Z_DVAL_P(val); + break; + case IS_TRUE: + c_attrs[idx].value_type = 2; + c_attrs[idx].bool_value = true; + break; + case IS_FALSE: + c_attrs[idx].value_type = 2; + c_attrs[idx].bool_value = false; + break; + default: + continue; /* skip unsupported types; targets ZEND_HASH_FOREACH loop */ + } + idx++; + } ZEND_HASH_FOREACH_END(); + attrs_count = idx; + } + } + struct FfeResult *result = ddog_ffe_evaluate( + Z_STRVAL_P(flag_key_zv), type_id, targeting_key, c_attrs, attrs_count); + if (c_attrs) { + efree(c_attrs); + } + if (result) { + array_init(return_value); + const char *val = ddog_ffe_result_value(result); + const char *var = ddog_ffe_result_variant(result); + const char *ak = ddog_ffe_result_allocation_key(result); + if (val) { add_assoc_string(return_value, "value_json", (char *)val); } + else { add_assoc_null(return_value, "value_json"); } + if (var) { add_assoc_string(return_value, "variant", (char *)var); } + else { add_assoc_null(return_value, "variant"); } + if (ak) { add_assoc_string(return_value, "allocation_key", (char *)ak); } + else { add_assoc_null(return_value, "allocation_key"); } + add_assoc_long(return_value, "reason", ddog_ffe_result_reason(result)); + add_assoc_long(return_value, "error_code", ddog_ffe_result_error_code(result)); + add_assoc_bool(return_value, "do_log", ddog_ffe_result_do_log(result)); + ddog_ffe_free_result(result); + } else { + RETVAL_NULL(); + } + } else { + RETVAL_NULL(); + } } else if (FUNCTION_NAME_MATCHES("dump_sidecar")) { if (!ddtrace_sidecar) { RETURN_FALSE; diff --git a/ext/sidecar.c b/ext/sidecar.c index 9732879acc5..572ff6283a4 100644 --- a/ext/sidecar.c +++ b/ext/sidecar.c @@ -220,7 +220,7 @@ void ddtrace_sidecar_setup(bool appsec_activation, bool appsec_config) { ddtrace_set_non_resettable_sidecar_globals(); ddtrace_set_resettable_sidecar_globals(); - ddog_init_remote_config(get_global_DD_INSTRUMENTATION_TELEMETRY_ENABLED(), appsec_activation, appsec_config); + ddog_init_remote_config(get_global_DD_INSTRUMENTATION_TELEMETRY_ENABLED(), appsec_activation, appsec_config, get_global_DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED()); ddtrace_sidecar = dd_sidecar_connection_factory(); if (!ddtrace_sidecar) { // Something went wrong diff --git a/libdatadog b/libdatadog index f483c69f73a..ed316b6382d 160000 --- a/libdatadog +++ b/libdatadog @@ -1 +1 @@ -Subproject commit f483c69f73acb72e688623cc634cf9f204d7cc14 +Subproject commit ed316b6382d545b4b416f47096f615ed1b413be5 diff --git a/src/DDTrace/FeatureFlags/ExposureCache.php b/src/DDTrace/FeatureFlags/ExposureCache.php new file mode 100644 index 00000000000..ce16715b0fc --- /dev/null +++ b/src/DDTrace/FeatureFlags/ExposureCache.php @@ -0,0 +1,117 @@ +cache = new LRUCache($capacity); + } + + /** + * Add an exposure event to the cache. + * + * @param string $flagKey + * @param string $subjectId + * @param string $variantKey + * @param string $allocationKey + * @return bool true if the event is new or value changed, false if exact duplicate + */ + public function add($flagKey, $subjectId, $variantKey, $allocationKey) + { + $key = self::makeKey($flagKey, $subjectId); + $newValue = self::makeValue($variantKey, $allocationKey); + + // Always put (updates LRU position even for duplicates) + $oldValue = $this->cache->put($key, $newValue); + + return $oldValue === null || $oldValue !== $newValue; + } + + /** + * Get the cached value for a (flag, subject) pair. + * + * @param string $flagKey + * @param string $subjectId + * @return array|null [variantKey, allocationKey] or null if not found + */ + public function get($flagKey, $subjectId) + { + $key = self::makeKey($flagKey, $subjectId); + $value = $this->cache->get($key); + if ($value === null) { + return null; + } + return self::parseValue($value); + } + + /** + * Return the number of entries in the cache. + * + * @return int + */ + public function size() + { + return $this->cache->size(); + } + + /** + * Clear all entries. + */ + public function clear() + { + $this->cache->clear(); + } + + /** + * Build a composite key that avoids collision. + * Uses length-prefixing: "::" + */ + private static function makeKey($flagKey, $subjectId) + { + $f = $flagKey !== null ? $flagKey : ''; + $s = $subjectId !== null ? $subjectId : ''; + return strlen($f) . ':' . $f . ':' . $s; + } + + /** + * Build a composite value string. + */ + private static function makeValue($variantKey, $allocationKey) + { + $v = $variantKey !== null ? $variantKey : ''; + $a = $allocationKey !== null ? $allocationKey : ''; + return strlen($v) . ':' . $v . ':' . $a; + } + + /** + * Parse a composite value string back into [variantKey, allocationKey]. + */ + private static function parseValue($value) + { + $colonPos = strpos($value, ':'); + if ($colonPos === false) { + return [$value, '']; + } + $len = (int) substr($value, 0, $colonPos); + $variant = substr($value, $colonPos + 1, $len); + $allocation = substr($value, $colonPos + 1 + $len + 1); + return [$variant, $allocation]; + } +} diff --git a/src/DDTrace/FeatureFlags/ExposureWriter.php b/src/DDTrace/FeatureFlags/ExposureWriter.php new file mode 100644 index 00000000000..d6c63e76747 --- /dev/null +++ b/src/DDTrace/FeatureFlags/ExposureWriter.php @@ -0,0 +1,190 @@ +agentUrl = $this->resolveAgentUrl(); + } + + /** + * Add an exposure event to the buffer. + * + * @param array $event Exposure event data + */ + public function enqueue(array $event) + { + if (count($this->buffer) >= self::MAX_BUFFER_SIZE) { + $this->droppedEvents++; + return; + } + $this->buffer[] = $event; + } + + /** + * Send all buffered exposure events as a batch to the EVP proxy. + */ + public function flush() + { + if (empty($this->buffer)) { + return; + } + + $events = $this->buffer; + $dropped = $this->droppedEvents; + $this->buffer = []; + $this->droppedEvents = 0; + + if ($dropped > 0 && function_exists('dd_trace_env_config') && \dd_trace_env_config('DD_TRACE_DEBUG')) { + error_log("ddtrace/ffe: dropped $dropped exposure event(s) due to full buffer"); + } + + $payload = [ + 'context' => [ + 'service' => $this->getConfigValue('DD_SERVICE', ''), + 'env' => $this->getConfigValue('DD_ENV', ''), + 'version' => $this->getConfigValue('DD_VERSION', ''), + ], + 'exposures' => $events, + ]; + + $url = rtrim($this->agentUrl, '/') . '/evp_proxy/v2/api/v2/exposures'; + $body = json_encode($payload); + + if (!function_exists('curl_init')) { + return; + } + + $ch = curl_init($url); + if ($ch === false) { + return; + } + + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $body, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'X-Datadog-EVP-Subdomain: event-platform-intake', + ], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT_MS => 500, + CURLOPT_CONNECTTIMEOUT_MS => 100, + ]); + + $response = curl_exec($ch); + if ($response === false && function_exists('dd_trace_env_config') && \dd_trace_env_config('DD_TRACE_DEBUG')) { + error_log('ddtrace/ffe: failed to send exposures: ' . curl_error($ch)); + } + curl_close($ch); + } + + /** + * Return the number of events currently in the buffer. + * + * @return int + */ + public function getBufferCount() + { + return count($this->buffer); + } + + /** + * Build a complete exposure event array. + * + * @param string $flagKey + * @param string $variantKey + * @param string $allocationKey + * @param string|null $targetingKey + * @param array $attributes + * @return array + */ + public static function buildEvent( + $flagKey, + $variantKey, + $allocationKey, + $targetingKey = null, + array $attributes = [] + ) { + return [ + 'timestamp' => (int)(microtime(true) * 1000), + 'allocation' => ['key' => $allocationKey], + 'flag' => ['key' => $flagKey], + 'variant' => ['key' => $variantKey], + 'subject' => [ + 'id' => $targetingKey ?? '', + 'attributes' => $attributes, + ], + ]; + } + + /** + * Resolve the agent URL from environment configuration. + * + * Checks DD_TRACE_AGENT_URL first. If not set, constructs from + * DD_AGENT_HOST (default: localhost) and DD_TRACE_AGENT_PORT (default: 8126). + * + * @return string + */ + private function resolveAgentUrl() + { + $agentUrl = $this->getConfigValue('DD_TRACE_AGENT_URL', ''); + if ($agentUrl !== '') { + return rtrim($agentUrl, '/'); + } + + $host = $this->getConfigValue('DD_AGENT_HOST', 'localhost'); + if ($host === '') { + $host = 'localhost'; + } + + $port = $this->getConfigValue('DD_TRACE_AGENT_PORT', '8126'); + if ($port === '') { + $port = '8126'; + } + + return 'http://' . $host . ':' . $port; + } + + /** + * Read a configuration value using dd_trace_env_config() if available, + * otherwise fall back to getenv(). + * + * @param string $name + * @param string $default + * @return string + */ + private function getConfigValue($name, $default = '') + { + if (function_exists('dd_trace_env_config')) { + $value = \dd_trace_env_config($name); + if ($value !== '' && $value !== false && $value !== null) { + return (string)$value; + } + return $default; + } + + $value = getenv($name); + if ($value !== false && $value !== '') { + return $value; + } + + return $default; + } +} diff --git a/src/DDTrace/FeatureFlags/LRUCache.php b/src/DDTrace/FeatureFlags/LRUCache.php new file mode 100644 index 00000000000..d04c3cd5957 --- /dev/null +++ b/src/DDTrace/FeatureFlags/LRUCache.php @@ -0,0 +1,124 @@ + */ + private $cache = []; + + /** + * @param int $maxSize Maximum number of entries in the cache + */ + public function __construct($maxSize = 65536) + { + $this->maxSize = $maxSize; + } + + /** + * Get a value from the cache by key. + * + * Accessing an entry promotes it to the most recently used position. + * + * @param string $key + * @return mixed|null The cached value, or null if not found + */ + public function get($key) + { + if (!array_key_exists($key, $this->cache)) { + return null; + } + + // Move to end (most recently used) by removing and re-adding + $value = $this->cache[$key]; + unset($this->cache[$key]); + $this->cache[$key] = $value; + + return $value; + } + + /** + * Set a value in the cache. + * + * If the key already exists, the value is updated and the entry is promoted + * to the most recently used position. If the cache is at capacity, the least + * recently used entry is evicted. + * + * @param string $key + * @param mixed $value + */ + public function set($key, $value) + { + // If key already exists, remove it first so it moves to the end + if (array_key_exists($key, $this->cache)) { + unset($this->cache[$key]); + } + + $this->cache[$key] = $value; + + // Evict least recently used entry if over capacity + if (count($this->cache) > $this->maxSize) { + reset($this->cache); + $evictKey = key($this->cache); + unset($this->cache[$evictKey]); + } + } + + /** + * Put a value in the cache and return the previous value. + * + * Like set(), but returns the old value (or null if the key was not present). + * Always updates the LRU position, even when the value is unchanged. + * + * @param string $key + * @param mixed $value + * @return mixed|null The previous value, or null if the key was new + */ + public function put($key, $value) + { + $oldValue = null; + if (array_key_exists($key, $this->cache)) { + $oldValue = $this->cache[$key]; + unset($this->cache[$key]); + } + + $this->cache[$key] = $value; + + // Evict least recently used entry if over capacity + if (count($this->cache) > $this->maxSize) { + reset($this->cache); + $evictKey = key($this->cache); + unset($this->cache[$evictKey]); + } + + return $oldValue; + } + + /** + * Return the number of entries in the cache. + * + * @return int + */ + public function size() + { + return count($this->cache); + } + + /** + * Clear all entries from the cache. + */ + public function clear() + { + $this->cache = []; + } +} diff --git a/src/DDTrace/FeatureFlags/Provider.php b/src/DDTrace/FeatureFlags/Provider.php new file mode 100644 index 00000000000..8ade021f6a0 --- /dev/null +++ b/src/DDTrace/FeatureFlags/Provider.php @@ -0,0 +1,288 @@ + 'STATIC', + 1 => 'DEFAULT', + 2 => 'TARGETING_MATCH', + 3 => 'SPLIT', + 4 => 'DISABLED', + 5 => 'ERROR', + ]; + + private static $TYPE_MAP = [ + 'STRING' => 0, + 'INTEGER' => 1, + 'NUMERIC' => 2, + 'BOOLEAN' => 3, + 'JSON' => 4, + ]; + + /** @var ExposureWriter */ + private $writer; + + /** @var ExposureCache */ + private $exposureCache; + + /** @var bool */ + private $enabled; + + /** @var bool */ + private $configLoaded = false; + + /** @var bool */ + private $shutdownRegistered = false; + + /** @var Provider|null */ + private static $instance = null; + + public function __construct() + { + $this->writer = new ExposureWriter(); + $this->exposureCache = new ExposureCache(65536); + $this->enabled = $this->isFeatureFlagEnabled(); + } + + /** + * Get the singleton instance. + */ + public static function getInstance() + { + if (self::$instance === null) { + self::$instance = new self(); + } + return self::$instance; + } + + /** + * Reset the singleton (useful for testing). + */ + public static function reset() + { + self::$instance = null; + } + + /** + * Initialize the provider: load config from RC into the native engine. + */ + public function start() + { + if (!$this->enabled) { + return false; + } + $this->checkNativeConfig(); + $this->registerShutdown(); + return true; + } + + /** + * Register a shutdown function to auto-flush exposure events at request end. + */ + private function registerShutdown() + { + if ($this->shutdownRegistered) { + return; + } + $writer = $this->writer; + register_shutdown_function(function () use ($writer) { + $writer->flush(); + }); + $this->shutdownRegistered = true; + } + + /** + * Check if the native FFE configuration has been loaded or changed via Remote Config. + */ + private function checkNativeConfig() + { + // Check if config changed since last check (covers both new config and removal) + if (\dd_trace_internal_fn('ffe_config_changed')) { + $hasConfig = \dd_trace_internal_fn('ffe_has_config'); + if ($hasConfig && !$this->configLoaded) { + $this->configLoaded = true; + } elseif (!$hasConfig && $this->configLoaded) { + // Config was removed via RC + $this->configLoaded = false; + $this->exposureCache->clear(); + } + } elseif (!$this->configLoaded && \dd_trace_internal_fn('ffe_has_config')) { + // First check — config was already loaded before provider started + $this->configLoaded = true; + } + } + + /** + * Evaluate a feature flag using the native datadog-ffe engine. + * + * @param string $flagKey The flag key to evaluate + * @param string $variationType The expected variation type (STRING, BOOLEAN, INTEGER, NUMERIC, JSON) + * @param mixed $defaultValue The default value to return if evaluation fails + * @param string|null $targetingKey The targeting key (user/subject ID) + * @param array $attributes Additional context attributes + * @return array ['value' => mixed, 'reason' => string, 'variant' => string|null, 'allocation_key' => string|null] + */ + public function evaluate($flagKey, $variationType, $defaultValue, $targetingKey, $attributes = []) + { + if (!$this->enabled) { + return ['value' => $defaultValue, 'reason' => 'DISABLED', 'variant' => null, 'allocation_key' => null]; + } + + // Ensure native config is loaded + $this->checkNativeConfig(); + + if (!$this->configLoaded) { + return ['value' => $defaultValue, 'reason' => 'DEFAULT', 'variant' => null, 'allocation_key' => null]; + } + + $typeId = isset(self::$TYPE_MAP[$variationType]) ? self::$TYPE_MAP[$variationType] : 0; + + // Call the native evaluation engine with structured attributes + $result = \dd_trace_internal_fn('ffe_evaluate', $flagKey, $typeId, + $targetingKey, is_array($attributes) ? $attributes : []); + + if ($result === null) { + return ['value' => $defaultValue, 'reason' => 'DEFAULT', 'variant' => null, 'allocation_key' => null]; + } + + $errorCode = isset($result['error_code']) ? (int)$result['error_code'] : 0; + $reason = isset($result['reason']) ? (int)$result['reason'] : 1; + $reasonStr = isset(self::$REASON_MAP[$reason]) ? self::$REASON_MAP[$reason] : 'DEFAULT'; + + // Error or no variant → return default + if ($errorCode !== 0 || $result['variant'] === null) { + return ['value' => $defaultValue, 'reason' => $reasonStr, 'variant' => null, 'allocation_key' => null]; + } + + // Parse the value from JSON + $value = $this->parseNativeValue($result['value_json'], $variationType, $defaultValue); + + // Report exposure event (deduplicated via ExposureCache) + $doLog = !empty($result['do_log']); + if ($doLog && $result['variant'] !== null && $result['allocation_key'] !== null) { + $this->reportExposure( + $flagKey, + $result['variant'], + $result['allocation_key'], + $targetingKey, + $attributes + ); + } + + return [ + 'value' => $value, + 'reason' => $reasonStr, + 'variant' => $result['variant'], + 'allocation_key' => $result['allocation_key'], + ]; + } + + /** + * Parse a native value JSON string into the correct PHP type. + */ + private function parseNativeValue($valueJson, $variationType, $defaultValue) + { + if ($valueJson === null || $valueJson === 'null') { + return $defaultValue; + } + + switch ($variationType) { + case 'BOOLEAN': + return $valueJson === 'true'; + case 'INTEGER': + return (int)$valueJson; + case 'NUMERIC': + return (float)$valueJson; + case 'JSON': + $decoded = json_decode($valueJson, true); + if (json_last_error() === JSON_ERROR_NONE) { + return $decoded; + } + return $defaultValue; + case 'STRING': + default: + // String values come as JSON-encoded strings (with quotes) + $decoded = json_decode($valueJson); + return is_string($decoded) ? $decoded : $valueJson; + } + } + + /** + * Report a feature flag exposure event, deduplicated via ExposureCache. + */ + private function reportExposure($flagKey, $variantKey, $allocationKey, $targetingKey, $attributes) + { + if (!$variantKey || !$allocationKey) { + return; + } + + $subjectId = $targetingKey !== null ? $targetingKey : ''; + + // add() returns true for new events or when value changed, false for exact duplicates + if (!$this->exposureCache->add($flagKey, $subjectId, $variantKey, $allocationKey)) { + return; + } + + $event = ExposureWriter::buildEvent( + $flagKey, + $variantKey, + $allocationKey, + $subjectId, + is_array($attributes) ? $attributes : [] + ); + + $this->writer->enqueue($event); + } + + /** + * Flush pending exposure events. + */ + public function flush() + { + $this->writer->flush(); + } + + /** + * Clear the exposure cache. + */ + public function clearExposureCache() + { + $this->exposureCache->clear(); + } + + /** + * Check if the feature flag provider is enabled via env var. + */ + private function isFeatureFlagEnabled() + { + // Check env var first (most reliable across all SAPIs) + $envVal = getenv('DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED'); + if ($envVal !== false) { + return strtolower($envVal) === 'true' || $envVal === '1'; + } + + // Fall back to INI config + if (function_exists('dd_trace_env_config')) { + return (bool)\dd_trace_env_config('DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED'); + } + + return false; + } + + /** + * Check if config has been loaded into the native engine. + */ + public function isReady() + { + return $this->configLoaded; + } +} diff --git a/src/DDTrace/OpenFeature/DataDogProvider.php b/src/DDTrace/OpenFeature/DataDogProvider.php new file mode 100644 index 00000000000..a61037f41c3 --- /dev/null +++ b/src/DDTrace/OpenFeature/DataDogProvider.php @@ -0,0 +1,130 @@ +getBooleanValue('my-flag', false, $context); + * + * Requires: composer require open-feature/sdk + */ +class DataDogProvider extends AbstractProvider +{ + /** @var Provider */ + private $ffeProvider; + + public function __construct() + { + $this->ffeProvider = Provider::getInstance(); + $this->ffeProvider->start(); + } + + public function getMetadata(): Metadata + { + return new class implements Metadata { + public function getName(): string + { + return 'Datadog'; + } + }; + } + + public function resolveBooleanValue( + string $flagKey, + bool $defaultValue, + ?EvaluationContext $context = null + ): ResolutionDetailsInterface { + return $this->resolve($flagKey, 'BOOLEAN', $defaultValue, $context); + } + + public function resolveStringValue( + string $flagKey, + string $defaultValue, + ?EvaluationContext $context = null + ): ResolutionDetailsInterface { + return $this->resolve($flagKey, 'STRING', $defaultValue, $context); + } + + public function resolveIntegerValue( + string $flagKey, + int $defaultValue, + ?EvaluationContext $context = null + ): ResolutionDetailsInterface { + return $this->resolve($flagKey, 'INTEGER', $defaultValue, $context); + } + + public function resolveFloatValue( + string $flagKey, + float $defaultValue, + ?EvaluationContext $context = null + ): ResolutionDetailsInterface { + return $this->resolve($flagKey, 'NUMERIC', $defaultValue, $context); + } + + public function resolveObjectValue( + string $flagKey, + array $defaultValue, + ?EvaluationContext $context = null + ): ResolutionDetailsInterface { + return $this->resolve($flagKey, 'JSON', $defaultValue, $context); + } + + /** + * @param mixed $defaultValue + */ + private function resolve( + string $flagKey, + string $variationType, + $defaultValue, + ?EvaluationContext $context = null + ): ResolutionDetailsInterface { + $targetingKey = ''; + $attributes = []; + + if ($context !== null) { + $targetingKey = $context->getTargetingKey() ?? ''; + $attrs = $context->getAttributes(); + if ($attrs !== null) { + $attributes = $attrs->toArray(); + } + } + + $result = $this->ffeProvider->evaluate( + $flagKey, + $variationType, + $defaultValue, + $targetingKey, + $attributes + ); + + $builder = new ResolutionDetailsBuilder(); + $builder->withValue($result['value']); + + if (isset($result['reason'])) { + $builder->withReason($result['reason']); + } + + if (isset($result['variant']) && $result['variant'] !== null) { + $builder->withVariant($result['variant']); + } + + return $builder->build(); + } +} diff --git a/src/bridge/_files_tracer.php b/src/bridge/_files_tracer.php index 41383405c12..41216c65c67 100644 --- a/src/bridge/_files_tracer.php +++ b/src/bridge/_files_tracer.php @@ -40,4 +40,8 @@ __DIR__ . '/../DDTrace/Propagators/TextMap.php', __DIR__ . '/../DDTrace/ScopeManager.php', __DIR__ . '/../DDTrace/Tracer.php', + __DIR__ . '/../DDTrace/FeatureFlags/LRUCache.php', + __DIR__ . '/../DDTrace/FeatureFlags/ExposureCache.php', + __DIR__ . '/../DDTrace/FeatureFlags/ExposureWriter.php', + __DIR__ . '/../DDTrace/FeatureFlags/Provider.php', ]; diff --git a/tests/FeatureFlags/EvaluationTest.php b/tests/FeatureFlags/EvaluationTest.php new file mode 100644 index 00000000000..a1e382df17a --- /dev/null +++ b/tests/FeatureFlags/EvaluationTest.php @@ -0,0 +1,182 @@ + 0, + 'INTEGER' => 1, + 'NUMERIC' => 2, + 'BOOLEAN' => 3, + 'JSON' => 4, + ]; + return isset($map[$variationType]) ? $map[$variationType] : -1; + } + + /** + * Build the attributes array for ffe_evaluate from the test case attributes. + * Only scalar types (string, number, bool) are supported by the FFI bridge. + */ + private static function buildAttributes(array $attrs) + { + $result = []; + foreach ($attrs as $key => $value) { + if (is_string($value) || is_numeric($value) || is_bool($value)) { + $result[$key] = $value; + } + } + return $result; + } + + /** + * Parse the value_json string returned by ffe_evaluate based on the variation type. + */ + private static function parseValueJson($valueJson, $variationType) + { + switch ($variationType) { + case 'STRING': + return json_decode($valueJson, true); + case 'INTEGER': + return (int) $valueJson; + case 'NUMERIC': + return (float) $valueJson; + case 'BOOLEAN': + return $valueJson === 'true'; + case 'JSON': + return json_decode($valueJson, true); + default: + return json_decode($valueJson, true); + } + } + + /** + * Data provider that scans all evaluation case fixture files and flattens + * every scenario into a [fileName, caseIndex, caseData] tuple. + */ + public function provideEvaluationCases() + { + $casesDir = __DIR__ . '/fixtures/evaluation-cases'; + $files = glob($casesDir . '/*.json'); + $dataset = []; + + foreach ($files as $filePath) { + $fileName = basename($filePath, '.json'); + $cases = json_decode(file_get_contents($filePath), true); + + foreach ($cases as $index => $case) { + $label = sprintf('%s#%d (%s)', $fileName, $index, $case['flag']); + $dataset[$label] = [$fileName, $index, $case]; + } + } + + return $dataset; + } + + /** + * @dataProvider provideEvaluationCases + */ + public function testEvaluation($fileName, $caseIndex, $case) + { + if (!self::$configLoaded) { + $this->markTestSkipped('UFC config was not loaded'); + } + + $flagKey = $case['flag']; + $variationType = $case['variationType']; + $typeId = self::variationTypeToId($variationType); + $targetingKey = isset($case['targetingKey']) ? $case['targetingKey'] : ''; + $attributes = isset($case['attributes']) ? self::buildAttributes($case['attributes']) : []; + $defaultValue = isset($case['defaultValue']) ? $case['defaultValue'] : null; + $expectedValue = $case['result']['value']; + + // Skip test cases that reference flags not present in the UFC config + // AND expect a non-default result (these require a different config). + if (!in_array($flagKey, self::$configFlagKeys) && $expectedValue !== $defaultValue) { + $this->markTestSkipped( + sprintf('Flag "%s" not in UFC config and expected non-default value', $flagKey) + ); + } + + $result = \dd_trace_internal_fn('ffe_evaluate', $flagKey, $typeId, $targetingKey, $attributes); + + $this->assertNotNull( + $result, + sprintf('ffe_evaluate returned null for %s#%d', $fileName, $caseIndex) + ); + + $this->assertArrayHasKey('value_json', $result); + + $errorCode = isset($result['error_code']) ? (int) $result['error_code'] : 0; + + // When the evaluator returns an error, the Provider layer would return + // the defaultValue. If the expected result equals the defaultValue, + // verify the evaluator correctly returned an error (no match). + if ($errorCode !== 0 && $expectedValue === $defaultValue) { + // Evaluator correctly could not resolve — Provider returns default. + $this->assertTrue(true); + return; + } + + // error_code=0 with reason=1 means DefaultAllocationNull (no matching + // allocation). Same Provider-level default behavior applies. + $reason = isset($result['reason']) ? (int) $result['reason'] : -1; + if ($errorCode === 0 && $reason === 1 && $expectedValue === $defaultValue) { + $this->assertTrue(true); + return; + } + + $actualValue = self::parseValueJson($result['value_json'], $variationType); + + if ($variationType === 'NUMERIC') { + $this->assertEquals( + $expectedValue, + $actualValue, + sprintf('Value mismatch for %s#%d (flag=%s)', $fileName, $caseIndex, $flagKey), + 1e-10 + ); + } else { + $this->assertSame( + $expectedValue, + $actualValue, + sprintf('Value mismatch for %s#%d (flag=%s): expected %s, got %s', + $fileName, $caseIndex, $flagKey, + json_encode($expectedValue), json_encode($actualValue)) + ); + } + } +} diff --git a/tests/FeatureFlags/ExposureCacheTest.php b/tests/FeatureFlags/ExposureCacheTest.php new file mode 100644 index 00000000000..c3051c11670 --- /dev/null +++ b/tests/FeatureFlags/ExposureCacheTest.php @@ -0,0 +1,183 @@ +add('flag', 'subject', 'variant', 'allocation'); + + $this->assertTrue($added); + $this->assertSame(1, $cache->size()); + } + + public function testAddingDuplicateEventsReturnsFalse() + { + $cache = new ExposureCache(5); + $cache->add('flag', 'subject', 'variant', 'allocation'); + $duplicateAdded = $cache->add('flag', 'subject', 'variant', 'allocation'); + + $this->assertFalse($duplicateAdded); + $this->assertSame(1, $cache->size()); + } + + public function testAddingEventsWithSameKeyButDifferentDetailsUpdatesCache() + { + $cache = new ExposureCache(5); + $added1 = $cache->add('flag', 'subject', 'variant1', 'allocation1'); + $added2 = $cache->add('flag', 'subject', 'variant2', 'allocation2'); + + $retrieved = $cache->get('flag', 'subject'); + + $this->assertTrue($added1); + $this->assertTrue($added2); + $this->assertSame(1, $cache->size()); + $this->assertSame('variant2', $retrieved[0]); + $this->assertSame('allocation2', $retrieved[1]); + } + + public function testLRUEvictionWhenCapacityExceeded() + { + $cache = new ExposureCache(2); + $cache->add('flag1', 'subject1', 'variant1', 'allocation1'); + $cache->add('flag2', 'subject2', 'variant2', 'allocation2'); + $cache->add('flag3', 'subject3', 'variant3', 'allocation3'); + + $this->assertSame(2, $cache->size()); + $this->assertNull($cache->get('flag1', 'subject1'), 'event1 should be evicted'); + + $retrieved3 = $cache->get('flag3', 'subject3'); + $this->assertNotNull($retrieved3); + $this->assertSame('variant3', $retrieved3[0]); + $this->assertSame('allocation3', $retrieved3[1]); + } + + public function testSingleCapacityCache() + { + $cache = new ExposureCache(1); + $cache->add('flag1', 'subject1', 'variant1', 'allocation1'); + $cache->add('flag2', 'subject2', 'variant2', 'allocation2'); + + $this->assertSame(1, $cache->size()); + } + + public function testZeroCapacityCache() + { + $cache = new ExposureCache(0); + $added = $cache->add('flag', 'subject', 'variant', 'allocation'); + + $this->assertTrue($added); + $this->assertSame(0, $cache->size()); + } + + public function testEmptyCacheSize() + { + $cache = new ExposureCache(5); + $this->assertSame(0, $cache->size()); + } + + public function testMultipleAdditionsWithSameFlagDifferentSubjects() + { + $cache = new ExposureCache(10); + $results = []; + for ($i = 0; $i < 5; $i++) { + $results[] = $cache->add('flag', "subject{$i}", 'variant', 'allocation'); + } + + $this->assertSame([true, true, true, true, true], $results); + $this->assertSame(5, $cache->size()); + } + + public function testMultipleAdditionsWithSameSubjectDifferentFlags() + { + $cache = new ExposureCache(10); + $results = []; + for ($i = 0; $i < 5; $i++) { + $results[] = $cache->add("flag{$i}", 'subject', 'variant', 'allocation'); + } + + $this->assertSame([true, true, true, true, true], $results); + $this->assertSame(5, $cache->size()); + } + + public function testKeyEqualityWithNullValues() + { + $cache = new ExposureCache(5); + $cache->add('', '', 'variant', 'allocation'); + $duplicateAdded = $cache->add('', '', 'variant', 'allocation'); + + $this->assertFalse($duplicateAdded); + $this->assertSame(1, $cache->size()); + } + + public function testUpdatingExistingKeyMaintainsLRUPosition() + { + $cache = new ExposureCache(3); + $cache->add('flag1', 'subject1', 'variant1', 'allocation1'); + $cache->add('flag2', 'subject2', 'variant2', 'allocation2'); + $cache->add('flag3', 'subject3', 'variant3', 'allocation3'); + // Update event1 with new details — moves it to most recent + $cache->add('flag1', 'subject1', 'variant2', 'allocation2'); + // Should evict event2, not event1 + $cache->add('flag4', 'subject4', 'variant4', 'allocation4'); + + $this->assertSame(3, $cache->size()); + + $retrieved1 = $cache->get('flag1', 'subject1'); + $this->assertNotNull($retrieved1, 'event1 should be present (was updated)'); + $this->assertSame('variant2', $retrieved1[0]); + $this->assertSame('allocation2', $retrieved1[1]); + + $this->assertNull($cache->get('flag2', 'subject2'), 'event2 should be evicted'); + + $retrieved4 = $cache->get('flag4', 'subject4'); + $this->assertNotNull($retrieved4); + $this->assertSame('variant4', $retrieved4[0]); + } + + public function testDuplicateExposureKeepsSubjectHotInLRUOrder() + { + $cache = new ExposureCache(3); + // Fill cache + $added1 = $cache->add('flag1', 'subject1', 'variant1', 'allocation1'); + $added2 = $cache->add('flag2', 'subject2', 'variant2', 'allocation2'); + $added3 = $cache->add('flag3', 'subject3', 'variant3', 'allocation3'); + + // Duplicate exposure for subject1: should NOT change size, but SHOULD bump recency + $duplicateAdded = $cache->add('flag1', 'subject1', 'variant1', 'allocation1'); + + // Now push over capacity: the LRU entry (event2) should be evicted, not event1 + $added4 = $cache->add('flag4', 'subject4', 'variant4', 'allocation4'); + + $this->assertTrue($added1); + $this->assertTrue($added2); + $this->assertTrue($added3); + $this->assertFalse($duplicateAdded, 'exact duplicate should return false'); + $this->assertTrue($added4); + + $this->assertSame(3, $cache->size()); + + // Hot subject1 should still be present (duplicate bumped its recency) + $retrieved1 = $cache->get('flag1', 'subject1'); + $this->assertNotNull($retrieved1, 'hot subject1 should still be present'); + $this->assertSame('variant1', $retrieved1[0]); + $this->assertSame('allocation1', $retrieved1[1]); + + // subject2 should be evicted (it was LRU) + $this->assertNull($cache->get('flag2', 'subject2'), 'subject2 should be evicted'); + + // Newest subject4 should be present + $this->assertNotNull($cache->get('flag4', 'subject4')); + } +} diff --git a/tests/FeatureFlags/LRUCacheTest.php b/tests/FeatureFlags/LRUCacheTest.php new file mode 100644 index 00000000000..3f724b382a7 --- /dev/null +++ b/tests/FeatureFlags/LRUCacheTest.php @@ -0,0 +1,169 @@ +assertNull($cache->get('nonexistent')); + } + + public function testSetAndGet() + { + $cache = new LRUCache(10); + $cache->set('key1', 'value1'); + $this->assertSame('value1', $cache->get('key1')); + } + + public function testEviction() + { + $cache = new LRUCache(3); + $cache->set('a', 1); + $cache->set('b', 2); + $cache->set('c', 3); + + // Cache is full; inserting a 4th should evict 'a' (least recently used) + $cache->set('d', 4); + + $this->assertNull($cache->get('a'), 'Oldest entry should be evicted'); + $this->assertSame(2, $cache->get('b')); + $this->assertSame(3, $cache->get('c')); + $this->assertSame(4, $cache->get('d')); + } + + public function testAccessPromotesEntry() + { + $cache = new LRUCache(3); + $cache->set('a', 1); + $cache->set('b', 2); + $cache->set('c', 3); + + // Access 'a' to promote it — now 'b' is the least recently used + $cache->get('a'); + + $cache->set('d', 4); + + $this->assertNull($cache->get('b'), "'b' should be evicted as LRU"); + $this->assertSame(1, $cache->get('a'), "'a' should survive after promotion"); + $this->assertSame(3, $cache->get('c')); + $this->assertSame(4, $cache->get('d')); + } + + public function testUpdateExistingKey() + { + $cache = new LRUCache(3); + $cache->set('a', 1); + $cache->set('b', 2); + $cache->set('c', 3); + + // Update 'a' — this should promote it to most recently used + $cache->set('a', 100); + + $this->assertSame(100, $cache->get('a'), 'Value should be updated'); + + // Now 'b' is LRU. Adding a new entry should evict 'b'. + $cache->set('d', 4); + $this->assertNull($cache->get('b'), "'b' should be evicted"); + $this->assertSame(100, $cache->get('a')); + } + + public function testClear() + { + $cache = new LRUCache(10); + $cache->set('a', 1); + $cache->set('b', 2); + $cache->clear(); + + $this->assertNull($cache->get('a')); + $this->assertNull($cache->get('b')); + } + + public function testEvictionOrder() + { + $cache = new LRUCache(4); + + // Insert a, b, c, d in order — LRU order: a, b, c, d + $cache->set('a', 1); + $cache->set('b', 2); + $cache->set('c', 3); + $cache->set('d', 4); + + // Access 'b' and 'a' — LRU order is now: c, d, b, a + $cache->get('b'); + $cache->get('a'); + + // Insert 'e' — should evict 'c' (the LRU) — order: d, b, a, e + $cache->set('e', 5); + $this->assertNull($cache->get('c'), "'c' should be evicted first"); + + // Insert 'f' — should evict 'd' (now the LRU) — order: b, a, e, f + $cache->set('f', 6); + $this->assertNull($cache->get('d'), "'d' should be evicted"); + + // b, a, e, f should still be present + $this->assertSame(2, $cache->get('b')); + $this->assertSame(1, $cache->get('a')); + $this->assertSame(5, $cache->get('e')); + $this->assertSame(6, $cache->get('f')); + } + + public function testSizeOneCache() + { + $cache = new LRUCache(1); + $cache->set('a', 1); + $this->assertSame(1, $cache->get('a')); + + $cache->set('b', 2); + $this->assertNull($cache->get('a'), 'Old entry should be evicted in size-1 cache'); + $this->assertSame(2, $cache->get('b')); + } + + public function testPutReturnsOldValue() + { + $cache = new LRUCache(10); + $old1 = $cache->put('a', 1); + $old2 = $cache->put('a', 2); + + $this->assertNull($old1, 'First put should return null'); + $this->assertSame(1, $old2, 'Second put should return old value'); + $this->assertSame(2, $cache->get('a')); + } + + public function testPutPromotesLRU() + { + $cache = new LRUCache(3); + $cache->put('a', 1); + $cache->put('b', 2); + $cache->put('c', 3); + + // put 'a' again (same value) — should promote to most recent + $cache->put('a', 1); + + // Adding 'd' should evict 'b' (LRU), not 'a' + $cache->put('d', 4); + $this->assertNull($cache->get('b'), "'b' should be evicted"); + $this->assertSame(1, $cache->get('a'), "'a' should survive after put promotion"); + } + + public function testSize() + { + $cache = new LRUCache(10); + $this->assertSame(0, $cache->size()); + + $cache->set('a', 1); + $this->assertSame(1, $cache->size()); + + $cache->set('b', 2); + $this->assertSame(2, $cache->size()); + + $cache->clear(); + $this->assertSame(0, $cache->size()); + } +} diff --git a/tests/FeatureFlags/bootstrap.php b/tests/FeatureFlags/bootstrap.php new file mode 100644 index 00000000000..264528135cb --- /dev/null +++ b/tests/FeatureFlags/bootstrap.php @@ -0,0 +1,25 @@ += 8) { + require dirname(__DIR__) . '/Common/MultiPHPUnitVersionAdapter_typed.php'; +} else { + require dirname(__DIR__) . '/Common/MultiPHPUnitVersionAdapter_untyped.php'; +} + +// Stub dd_trace_internal_fn if the extension is not loaded +if (!function_exists('dd_trace_internal_fn')) { + function dd_trace_internal_fn() { return false; } +} diff --git a/tests/FeatureFlags/fixtures/config/ufc-config.json b/tests/FeatureFlags/fixtures/config/ufc-config.json new file mode 100644 index 00000000000..91038d0fdbb --- /dev/null +++ b/tests/FeatureFlags/fixtures/config/ufc-config.json @@ -0,0 +1,3353 @@ +{ + "createdAt": "2024-04-17T19:40:53.716Z", + "format": "SERVER", + "environment": { + "name": "Test" + }, + "flags": { + "empty_flag": { + "key": "empty_flag", + "enabled": true, + "variationType": "STRING", + "variations": {}, + "allocations": [] + }, + "disabled_flag": { + "key": "disabled_flag", + "enabled": false, + "variationType": "INTEGER", + "variations": {}, + "allocations": [] + }, + "no_allocations_flag": { + "key": "no_allocations_flag", + "enabled": true, + "variationType": "JSON", + "variations": { + "control": { + "key": "control", + "value": { + "variant": "control" + } + }, + "treatment": { + "key": "treatment", + "value": { + "variant": "treatment" + } + } + }, + "allocations": [] + }, + "numeric_flag": { + "key": "numeric_flag", + "enabled": true, + "variationType": "NUMERIC", + "variations": { + "e": { + "key": "e", + "value": 2.7182818 + }, + "pi": { + "key": "pi", + "value": 3.1415926 + } + }, + "allocations": [ + { + "key": "rollout", + "splits": [ + { + "variationKey": "pi", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "regex-flag": { + "key": "regex-flag", + "enabled": true, + "variationType": "STRING", + "variations": { + "partial-example": { + "key": "partial-example", + "value": "partial-example" + }, + "test": { + "key": "test", + "value": "test" + } + }, + "allocations": [ + { + "key": "partial-example", + "rules": [ + { + "conditions": [ + { + "attribute": "email", + "operator": "MATCHES", + "value": "@example\\.com" + } + ] + } + ], + "splits": [ + { + "variationKey": "partial-example", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "test", + "rules": [ + { + "conditions": [ + { + "attribute": "email", + "operator": "MATCHES", + "value": ".*@test\\.com" + } + ] + } + ], + "splits": [ + { + "variationKey": "test", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "numeric-one-of": { + "key": "numeric-one-of", + "enabled": true, + "variationType": "INTEGER", + "variations": { + "1": { + "key": "1", + "value": 1 + }, + "2": { + "key": "2", + "value": 2 + }, + "3": { + "key": "3", + "value": 3 + } + }, + "allocations": [ + { + "key": "1-for-1", + "rules": [ + { + "conditions": [ + { + "attribute": "number", + "operator": "ONE_OF", + "value": [ + "1" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "1", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "2-for-123456789", + "rules": [ + { + "conditions": [ + { + "attribute": "number", + "operator": "ONE_OF", + "value": [ + "123456789" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "2", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "3-for-not-2", + "rules": [ + { + "conditions": [ + { + "attribute": "number", + "operator": "NOT_ONE_OF", + "value": [ + "2" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "3", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "boolean-one-of-matches": { + "key": "boolean-one-of-matches", + "enabled": true, + "variationType": "INTEGER", + "variations": { + "1": { + "key": "1", + "value": 1 + }, + "2": { + "key": "2", + "value": 2 + }, + "3": { + "key": "3", + "value": 3 + }, + "4": { + "key": "4", + "value": 4 + }, + "5": { + "key": "5", + "value": 5 + } + }, + "allocations": [ + { + "key": "1-for-one-of", + "rules": [ + { + "conditions": [ + { + "attribute": "one_of_flag", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "1", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "2-for-matches", + "rules": [ + { + "conditions": [ + { + "attribute": "matches_flag", + "operator": "MATCHES", + "value": "true" + } + ] + } + ], + "splits": [ + { + "variationKey": "2", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "3-for-not-one-of", + "rules": [ + { + "conditions": [ + { + "attribute": "not_one_of_flag", + "operator": "NOT_ONE_OF", + "value": [ + "false" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "3", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "4-for-not-matches", + "rules": [ + { + "conditions": [ + { + "attribute": "not_matches_flag", + "operator": "NOT_MATCHES", + "value": "false" + } + ] + } + ], + "splits": [ + { + "variationKey": "4", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "5-for-matches-null", + "rules": [ + { + "conditions": [ + { + "attribute": "null_flag", + "operator": "ONE_OF", + "value": [ + "null" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "5", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "empty_string_flag": { + "key": "empty_string_flag", + "enabled": true, + "comment": "Testing the empty string as a variation value", + "variationType": "STRING", + "variations": { + "empty_string": { + "key": "empty_string", + "value": "" + }, + "non_empty": { + "key": "non_empty", + "value": "non_empty" + } + }, + "allocations": [ + { + "key": "allocation-empty", + "rules": [ + { + "conditions": [ + { + "attribute": "country", + "operator": "MATCHES", + "value": "US" + } + ] + } + ], + "splits": [ + { + "variationKey": "empty_string", + "shards": [ + { + "salt": "allocation-empty-shards", + "totalShards": 10000, + "ranges": [ + { + "start": 0, + "end": 10000 + } + ] + } + ] + } + ], + "doLog": true + }, + { + "key": "allocation-test", + "rules": [], + "splits": [ + { + "variationKey": "non_empty", + "shards": [ + { + "salt": "allocation-empty-shards", + "totalShards": 10000, + "ranges": [ + { + "start": 0, + "end": 10000 + } + ] + } + ] + } + ], + "doLog": true + } + ] + }, + "kill-switch": { + "key": "kill-switch", + "enabled": true, + "variationType": "BOOLEAN", + "variations": { + "on": { + "key": "on", + "value": true + }, + "off": { + "key": "off", + "value": false + } + }, + "allocations": [ + { + "key": "on-for-NA", + "rules": [ + { + "conditions": [ + { + "attribute": "country", + "operator": "ONE_OF", + "value": [ + "US", + "Canada", + "Mexico" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "on", + "shards": [ + { + "salt": "some-salt", + "totalShards": 10000, + "ranges": [ + { + "start": 0, + "end": 10000 + } + ] + } + ] + } + ], + "doLog": true + }, + { + "key": "on-for-age-50+", + "rules": [ + { + "conditions": [ + { + "attribute": "age", + "operator": "GTE", + "value": 50 + } + ] + } + ], + "splits": [ + { + "variationKey": "on", + "shards": [ + { + "salt": "some-salt", + "totalShards": 10000, + "ranges": [ + { + "start": 0, + "end": 10000 + } + ] + } + ] + } + ], + "doLog": true + }, + { + "key": "off-for-all", + "rules": [], + "splits": [ + { + "variationKey": "off", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "comparator-operator-test": { + "key": "comparator-operator-test", + "enabled": true, + "variationType": "STRING", + "variations": { + "small": { + "key": "small", + "value": "small" + }, + "medium": { + "key": "medium", + "value": "medium" + }, + "large": { + "key": "large", + "value": "large" + } + }, + "allocations": [ + { + "key": "small-size", + "rules": [ + { + "conditions": [ + { + "attribute": "size", + "operator": "LT", + "value": 10 + } + ] + } + ], + "splits": [ + { + "variationKey": "small", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "medum-size", + "rules": [ + { + "conditions": [ + { + "attribute": "size", + "operator": "GTE", + "value": 10 + }, + { + "attribute": "size", + "operator": "LTE", + "value": 20 + } + ] + } + ], + "splits": [ + { + "variationKey": "medium", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "large-size", + "rules": [ + { + "conditions": [ + { + "attribute": "size", + "operator": "GT", + "value": 25 + } + ] + } + ], + "splits": [ + { + "variationKey": "large", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "start-and-end-date-test": { + "key": "start-and-end-date-test", + "enabled": true, + "variationType": "STRING", + "variations": { + "old": { + "key": "old", + "value": "old" + }, + "current": { + "key": "current", + "value": "current" + }, + "new": { + "key": "new", + "value": "new" + } + }, + "allocations": [ + { + "key": "old-versions", + "splits": [ + { + "variationKey": "old", + "shards": [] + } + ], + "endAt": "2002-10-31T09:00:00.594Z", + "doLog": true + }, + { + "key": "future-versions", + "splits": [ + { + "variationKey": "new", + "shards": [] + } + ], + "startAt": "2052-10-31T09:00:00.594Z", + "doLog": true + }, + { + "key": "current-versions", + "splits": [ + { + "variationKey": "current", + "shards": [] + } + ], + "startAt": "2022-10-31T09:00:00.594Z", + "endAt": "2050-10-31T09:00:00.594Z", + "doLog": true + } + ] + }, + "null-operator-test": { + "key": "null-operator-test", + "enabled": true, + "variationType": "STRING", + "variations": { + "old": { + "key": "old", + "value": "old" + }, + "new": { + "key": "new", + "value": "new" + } + }, + "allocations": [ + { + "key": "null-operator", + "rules": [ + { + "conditions": [ + { + "attribute": "size", + "operator": "IS_NULL", + "value": true + } + ] + }, + { + "conditions": [ + { + "attribute": "size", + "operator": "LT", + "value": 10 + } + ] + } + ], + "splits": [ + { + "variationKey": "old", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "not-null-operator", + "rules": [ + { + "conditions": [ + { + "attribute": "size", + "operator": "IS_NULL", + "value": false + } + ] + } + ], + "splits": [ + { + "variationKey": "new", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "new-user-onboarding": { + "key": "new-user-onboarding", + "enabled": true, + "variationType": "STRING", + "variations": { + "control": { + "key": "control", + "value": "control" + }, + "red": { + "key": "red", + "value": "red" + }, + "blue": { + "key": "blue", + "value": "blue" + }, + "green": { + "key": "green", + "value": "green" + }, + "yellow": { + "key": "yellow", + "value": "yellow" + }, + "purple": { + "key": "purple", + "value": "purple" + } + }, + "allocations": [ + { + "key": "id rule", + "rules": [ + { + "conditions": [ + { + "attribute": "id", + "operator": "MATCHES", + "value": "zach" + } + ] + } + ], + "splits": [ + { + "variationKey": "purple", + "shards": [] + } + ], + "doLog": false + }, + { + "key": "internal users", + "rules": [ + { + "conditions": [ + { + "attribute": "email", + "operator": "MATCHES", + "value": "@mycompany.com" + } + ] + } + ], + "splits": [ + { + "variationKey": "green", + "shards": [] + } + ], + "doLog": false + }, + { + "key": "experiment", + "rules": [ + { + "conditions": [ + { + "attribute": "country", + "operator": "NOT_ONE_OF", + "value": [ + "US", + "Canada", + "Mexico" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "control", + "shards": [ + { + "salt": "traffic-new-user-onboarding-experiment", + "totalShards": 10000, + "ranges": [ + { + "start": 0, + "end": 6000 + } + ] + }, + { + "salt": "split-new-user-onboarding-experiment", + "totalShards": 10000, + "ranges": [ + { + "start": 0, + "end": 5000 + } + ] + } + ] + }, + { + "variationKey": "red", + "shards": [ + { + "salt": "traffic-new-user-onboarding-experiment", + "totalShards": 10000, + "ranges": [ + { + "start": 0, + "end": 6000 + } + ] + }, + { + "salt": "split-new-user-onboarding-experiment", + "totalShards": 10000, + "ranges": [ + { + "start": 5000, + "end": 8000 + } + ] + } + ] + }, + { + "variationKey": "yellow", + "shards": [ + { + "salt": "traffic-new-user-onboarding-experiment", + "totalShards": 10000, + "ranges": [ + { + "start": 0, + "end": 6000 + } + ] + }, + { + "salt": "split-new-user-onboarding-experiment", + "totalShards": 10000, + "ranges": [ + { + "start": 8000, + "end": 10000 + } + ] + } + ] + } + ], + "doLog": true + }, + { + "key": "rollout", + "rules": [ + { + "conditions": [ + { + "attribute": "country", + "operator": "ONE_OF", + "value": [ + "US", + "Canada", + "Mexico" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "blue", + "shards": [ + { + "salt": "split-new-user-onboarding-rollout", + "totalShards": 10000, + "ranges": [ + { + "start": 0, + "end": 8000 + } + ] + } + ], + "extraLogging": { + "allocationvalue_type": "rollout", + "owner": "hippo" + } + } + ], + "doLog": true + } + ] + }, + "integer-flag": { + "key": "integer-flag", + "enabled": true, + "variationType": "INTEGER", + "variations": { + "one": { + "key": "one", + "value": 1 + }, + "two": { + "key": "two", + "value": 2 + }, + "three": { + "key": "three", + "value": 3 + } + }, + "allocations": [ + { + "key": "targeted allocation", + "rules": [ + { + "conditions": [ + { + "attribute": "country", + "operator": "ONE_OF", + "value": [ + "US", + "Canada", + "Mexico" + ] + } + ] + }, + { + "conditions": [ + { + "attribute": "email", + "operator": "MATCHES", + "value": ".*@example.com" + } + ] + } + ], + "splits": [ + { + "variationKey": "three", + "shards": [ + { + "salt": "full-range-salt", + "totalShards": 10000, + "ranges": [ + { + "start": 0, + "end": 10000 + } + ] + } + ] + } + ], + "doLog": true + }, + { + "key": "50/50 split", + "rules": [], + "splits": [ + { + "variationKey": "one", + "shards": [ + { + "salt": "split-numeric-flag-some-allocation", + "totalShards": 10000, + "ranges": [ + { + "start": 0, + "end": 5000 + } + ] + } + ] + }, + { + "variationKey": "two", + "shards": [ + { + "salt": "split-numeric-flag-some-allocation", + "totalShards": 10000, + "ranges": [ + { + "start": 5000, + "end": 10000 + } + ] + } + ] + } + ], + "doLog": true + } + ] + }, + "json-config-flag": { + "key": "json-config-flag", + "enabled": true, + "variationType": "JSON", + "variations": { + "one": { + "key": "one", + "value": { + "integer": 1, + "string": "one", + "float": 1.0 + } + }, + "two": { + "key": "two", + "value": { + "integer": 2, + "string": "two", + "float": 2.0 + } + }, + "empty": { + "key": "empty", + "value": {} + } + }, + "allocations": [ + { + "key": "Optionally Force Empty", + "rules": [ + { + "conditions": [ + { + "attribute": "Force Empty", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "empty", + "shards": [ + { + "salt": "full-range-salt", + "totalShards": 10000, + "ranges": [ + { + "start": 0, + "end": 10000 + } + ] + } + ] + } + ], + "doLog": true + }, + { + "key": "50/50 split", + "rules": [], + "splits": [ + { + "variationKey": "one", + "shards": [ + { + "salt": "traffic-json-flag", + "totalShards": 10000, + "ranges": [ + { + "start": 0, + "end": 10000 + } + ] + }, + { + "salt": "split-json-flag", + "totalShards": 10000, + "ranges": [ + { + "start": 0, + "end": 5000 + } + ] + } + ] + }, + { + "variationKey": "two", + "shards": [ + { + "salt": "traffic-json-flag", + "totalShards": 10000, + "ranges": [ + { + "start": 0, + "end": 10000 + } + ] + }, + { + "salt": "split-json-flag", + "totalShards": 10000, + "ranges": [ + { + "start": 5000, + "end": 10000 + } + ] + } + ] + } + ], + "doLog": true + } + ] + }, + "special-characters": { + "key": "special-characters", + "enabled": true, + "variationType": "JSON", + "variations": { + "de": { + "key": "de", + "value": { + "a": "k\u00fcmmert", + "b": "sch\u00f6n" + } + }, + "ua": { + "key": "ua", + "value": { + "a": "\u043f\u0456\u043a\u043b\u0443\u0432\u0430\u0442\u0438\u0441\u044f", + "b": "\u043b\u044e\u0431\u043e\u0432" + } + }, + "zh": { + "key": "zh", + "value": { + "a": "\u7167\u987e", + "b": "\u6f02\u4eae" + } + }, + "emoji": { + "key": "emoji", + "value": { + "a": "\ud83e\udd17", + "b": "\ud83c\udf38" + } + } + }, + "allocations": [ + { + "key": "allocation-test", + "splits": [ + { + "variationKey": "de", + "shards": [ + { + "salt": "split-json-flag", + "totalShards": 10000, + "ranges": [ + { + "start": 0, + "end": 2500 + } + ] + } + ] + }, + { + "variationKey": "ua", + "shards": [ + { + "salt": "split-json-flag", + "totalShards": 10000, + "ranges": [ + { + "start": 2500, + "end": 5000 + } + ] + } + ] + }, + { + "variationKey": "zh", + "shards": [ + { + "salt": "split-json-flag", + "totalShards": 10000, + "ranges": [ + { + "start": 5000, + "end": 7500 + } + ] + } + ] + }, + { + "variationKey": "emoji", + "shards": [ + { + "salt": "split-json-flag", + "totalShards": 10000, + "ranges": [ + { + "start": 7500, + "end": 10000 + } + ] + } + ] + } + ], + "doLog": true + }, + { + "key": "allocation-default", + "splits": [ + { + "variationKey": "de", + "shards": [] + } + ], + "doLog": false + } + ] + }, + "string_flag_with_special_characters": { + "key": "string_flag_with_special_characters", + "enabled": true, + "comment": "Testing the string with special characters and spaces", + "variationType": "STRING", + "variations": { + "string_with_spaces": { + "key": "string_with_spaces", + "value": " a b c d e f " + }, + "string_with_only_one_space": { + "key": "string_with_only_one_space", + "value": " " + }, + "string_with_only_multiple_spaces": { + "key": "string_with_only_multiple_spaces", + "value": " " + }, + "string_with_dots": { + "key": "string_with_dots", + "value": ".a.b.c.d.e.f." + }, + "string_with_only_one_dot": { + "key": "string_with_only_one_dot", + "value": "." + }, + "string_with_only_multiple_dots": { + "key": "string_with_only_multiple_dots", + "value": "......." + }, + "string_with_comas": { + "key": "string_with_comas", + "value": ",a,b,c,d,e,f," + }, + "string_with_only_one_coma": { + "key": "string_with_only_one_coma", + "value": "," + }, + "string_with_only_multiple_comas": { + "key": "string_with_only_multiple_comas", + "value": ",,,,,,," + }, + "string_with_colons": { + "key": "string_with_colons", + "value": ":a:b:c:d:e:f:" + }, + "string_with_only_one_colon": { + "key": "string_with_only_one_colon", + "value": ":" + }, + "string_with_only_multiple_colons": { + "key": "string_with_only_multiple_colons", + "value": ":::::::" + }, + "string_with_semicolons": { + "key": "string_with_semicolons", + "value": ";a;b;c;d;e;f;" + }, + "string_with_only_one_semicolon": { + "key": "string_with_only_one_semicolon", + "value": ";" + }, + "string_with_only_multiple_semicolons": { + "key": "string_with_only_multiple_semicolons", + "value": ";;;;;;;" + }, + "string_with_slashes": { + "key": "string_with_slashes", + "value": "/a/b/c/d/e/f/" + }, + "string_with_only_one_slash": { + "key": "string_with_only_one_slash", + "value": "/" + }, + "string_with_only_multiple_slashes": { + "key": "string_with_only_multiple_slashes", + "value": "///////" + }, + "string_with_dashes": { + "key": "string_with_dashes", + "value": "-a-b-c-d-e-f-" + }, + "string_with_only_one_dash": { + "key": "string_with_only_one_dash", + "value": "-" + }, + "string_with_only_multiple_dashes": { + "key": "string_with_only_multiple_dashes", + "value": "-------" + }, + "string_with_underscores": { + "key": "string_with_underscores", + "value": "_a_b_c_d_e_f_" + }, + "string_with_only_one_underscore": { + "key": "string_with_only_one_underscore", + "value": "_" + }, + "string_with_only_multiple_underscores": { + "key": "string_with_only_multiple_underscores", + "value": "_______" + }, + "string_with_plus_signs": { + "key": "string_with_plus_signs", + "value": "+a+b+c+d+e+f+" + }, + "string_with_only_one_plus_sign": { + "key": "string_with_only_one_plus_sign", + "value": "+" + }, + "string_with_only_multiple_plus_signs": { + "key": "string_with_only_multiple_plus_signs", + "value": "+++++++" + }, + "string_with_equal_signs": { + "key": "string_with_equal_signs", + "value": "=a=b=c=d=e=f=" + }, + "string_with_only_one_equal_sign": { + "key": "string_with_only_one_equal_sign", + "value": "=" + }, + "string_with_only_multiple_equal_signs": { + "key": "string_with_only_multiple_equal_signs", + "value": "=======" + }, + "string_with_dollar_signs": { + "key": "string_with_dollar_signs", + "value": "$a$b$c$d$e$f$" + }, + "string_with_only_one_dollar_sign": { + "key": "string_with_only_one_dollar_sign", + "value": "$" + }, + "string_with_only_multiple_dollar_signs": { + "key": "string_with_only_multiple_dollar_signs", + "value": "$$$$$$$" + }, + "string_with_at_signs": { + "key": "string_with_at_signs", + "value": "@a@b@c@d@e@f@" + }, + "string_with_only_one_at_sign": { + "key": "string_with_only_one_at_sign", + "value": "@" + }, + "string_with_only_multiple_at_signs": { + "key": "string_with_only_multiple_at_signs", + "value": "@@@@@@@" + }, + "string_with_amp_signs": { + "key": "string_with_amp_signs", + "value": "&a&b&c&d&e&f&" + }, + "string_with_only_one_amp_sign": { + "key": "string_with_only_one_amp_sign", + "value": "&" + }, + "string_with_only_multiple_amp_signs": { + "key": "string_with_only_multiple_amp_signs", + "value": "&&&&&&&" + }, + "string_with_hash_signs": { + "key": "string_with_hash_signs", + "value": "#a#b#c#d#e#f#" + }, + "string_with_only_one_hash_sign": { + "key": "string_with_only_one_hash_sign", + "value": "#" + }, + "string_with_only_multiple_hash_signs": { + "key": "string_with_only_multiple_hash_signs", + "value": "#######" + }, + "string_with_percentage_signs": { + "key": "string_with_percentage_signs", + "value": "%a%b%c%d%e%f%" + }, + "string_with_only_one_percentage_sign": { + "key": "string_with_only_one_percentage_sign", + "value": "%" + }, + "string_with_only_multiple_percentage_signs": { + "key": "string_with_only_multiple_percentage_signs", + "value": "%%%%%%%" + }, + "string_with_tilde_signs": { + "key": "string_with_tilde_signs", + "value": "~a~b~c~d~e~f~" + }, + "string_with_only_one_tilde_sign": { + "key": "string_with_only_one_tilde_sign", + "value": "~" + }, + "string_with_only_multiple_tilde_signs": { + "key": "string_with_only_multiple_tilde_signs", + "value": "~~~~~~~" + }, + "string_with_asterix_signs": { + "key": "string_with_asterix_signs", + "value": "*a*b*c*d*e*f*" + }, + "string_with_only_one_asterix_sign": { + "key": "string_with_only_one_asterix_sign", + "value": "*" + }, + "string_with_only_multiple_asterix_signs": { + "key": "string_with_only_multiple_asterix_signs", + "value": "*******" + }, + "string_with_single_quotes": { + "key": "string_with_single_quotes", + "value": "'a'b'c'd'e'f'" + }, + "string_with_only_one_single_quote": { + "key": "string_with_only_one_single_quote", + "value": "'" + }, + "string_with_only_multiple_single_quotes": { + "key": "string_with_only_multiple_single_quotes", + "value": "'''''''" + }, + "string_with_question_marks": { + "key": "string_with_question_marks", + "value": "?a?b?c?d?e?f?" + }, + "string_with_only_one_question_mark": { + "key": "string_with_only_one_question_mark", + "value": "?" + }, + "string_with_only_multiple_question_marks": { + "key": "string_with_only_multiple_question_marks", + "value": "???????" + }, + "string_with_exclamation_marks": { + "key": "string_with_exclamation_marks", + "value": "!a!b!c!d!e!f!" + }, + "string_with_only_one_exclamation_mark": { + "key": "string_with_only_one_exclamation_mark", + "value": "!" + }, + "string_with_only_multiple_exclamation_marks": { + "key": "string_with_only_multiple_exclamation_marks", + "value": "!!!!!!!" + }, + "string_with_opening_parentheses": { + "key": "string_with_opening_parentheses", + "value": "(a(b(c(d(e(f(" + }, + "string_with_only_one_opening_parenthese": { + "key": "string_with_only_one_opening_parenthese", + "value": "(" + }, + "string_with_only_multiple_opening_parentheses": { + "key": "string_with_only_multiple_opening_parentheses", + "value": "(((((((" + }, + "string_with_closing_parentheses": { + "key": "string_with_closing_parentheses", + "value": ")a)b)c)d)e)f)" + }, + "string_with_only_one_closing_parenthese": { + "key": "string_with_only_one_closing_parenthese", + "value": ")" + }, + "string_with_only_multiple_closing_parentheses": { + "key": "string_with_only_multiple_closing_parentheses", + "value": ")))))))" + } + }, + "allocations": [ + { + "key": "allocation-test-string_with_spaces", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_spaces", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_spaces", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_space", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_space", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_space", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_spaces", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_spaces", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_spaces", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_dots", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_dots", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_dots", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_dot", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_dot", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_dot", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_dots", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_dots", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_dots", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_comas", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_comas", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_comas", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_coma", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_coma", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_coma", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_comas", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_comas", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_comas", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_colons", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_colons", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_colons", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_colon", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_colon", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_colon", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_colons", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_colons", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_colons", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_semicolons", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_semicolons", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_semicolons", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_semicolon", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_semicolon", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_semicolon", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_semicolons", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_semicolons", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_semicolons", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_slashes", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_slashes", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_slashes", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_slash", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_slash", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_slash", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_slashes", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_slashes", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_slashes", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_dashes", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_dashes", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_dashes", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_dash", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_dash", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_dash", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_dashes", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_dashes", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_dashes", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_underscores", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_underscores", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_underscores", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_underscore", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_underscore", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_underscore", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_underscores", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_underscores", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_underscores", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_plus_signs", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_plus_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_plus_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_plus_sign", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_plus_sign", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_plus_sign", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_plus_signs", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_plus_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_plus_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_equal_signs", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_equal_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_equal_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_equal_sign", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_equal_sign", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_equal_sign", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_equal_signs", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_equal_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_equal_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_dollar_signs", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_dollar_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_dollar_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_dollar_sign", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_dollar_sign", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_dollar_sign", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_dollar_signs", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_dollar_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_dollar_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_at_signs", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_at_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_at_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_at_sign", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_at_sign", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_at_sign", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_at_signs", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_at_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_at_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_amp_signs", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_amp_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_amp_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_amp_sign", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_amp_sign", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_amp_sign", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_amp_signs", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_amp_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_amp_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_hash_signs", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_hash_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_hash_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_hash_sign", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_hash_sign", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_hash_sign", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_hash_signs", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_hash_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_hash_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_percentage_signs", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_percentage_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_percentage_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_percentage_sign", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_percentage_sign", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_percentage_sign", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_percentage_signs", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_percentage_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_percentage_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_tilde_signs", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_tilde_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_tilde_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_tilde_sign", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_tilde_sign", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_tilde_sign", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_tilde_signs", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_tilde_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_tilde_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_asterix_signs", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_asterix_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_asterix_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_asterix_sign", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_asterix_sign", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_asterix_sign", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_asterix_signs", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_asterix_signs", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_asterix_signs", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_single_quotes", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_single_quotes", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_single_quotes", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_single_quote", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_single_quote", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_single_quote", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_single_quotes", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_single_quotes", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_single_quotes", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_question_marks", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_question_marks", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_question_marks", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_question_mark", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_question_mark", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_question_mark", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_question_marks", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_question_marks", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_question_marks", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_exclamation_marks", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_exclamation_marks", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_exclamation_marks", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_exclamation_mark", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_exclamation_mark", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_exclamation_mark", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_exclamation_marks", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_exclamation_marks", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_exclamation_marks", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_opening_parentheses", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_opening_parentheses", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_opening_parentheses", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_opening_parenthese", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_opening_parenthese", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_opening_parenthese", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_opening_parentheses", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_opening_parentheses", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_opening_parentheses", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_closing_parentheses", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_closing_parentheses", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_closing_parentheses", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_one_closing_parenthese", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_one_closing_parenthese", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_one_closing_parenthese", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "allocation-test-string_with_only_multiple_closing_parentheses", + "rules": [ + { + "conditions": [ + { + "attribute": "string_with_only_multiple_closing_parentheses", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "string_with_only_multiple_closing_parentheses", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "boolean-false-assignment": { + "key": "boolean-false-assignment", + "enabled": true, + "variationType": "BOOLEAN", + "variations": { + "false-variation": { + "key": "false-variation", + "value": false + }, + "true-variation": { + "key": "true-variation", + "value": true + } + }, + "allocations": [ + { + "key": "disable-feature", + "rules": [ + { + "conditions": [ + { + "attribute": "should_disable_feature", + "operator": "ONE_OF", + "value": [ + "true" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "false-variation", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "enable-feature", + "rules": [ + { + "conditions": [ + { + "attribute": "should_disable_feature", + "operator": "ONE_OF", + "value": [ + "false" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "true-variation", + "shards": [] + } + ], + "doLog": true + } + ], + "totalShards": 10000 + }, + "empty-string-variation": { + "key": "empty-string-variation", + "enabled": true, + "variationType": "STRING", + "variations": { + "empty-content": { + "key": "empty-content", + "value": "" + }, + "detailed-content": { + "key": "detailed-content", + "value": "detailed_content" + } + }, + "allocations": [ + { + "key": "minimal-content", + "rules": [ + { + "conditions": [ + { + "attribute": "content_type", + "operator": "ONE_OF", + "value": [ + "minimal" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "empty-content", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "full-content", + "rules": [ + { + "conditions": [ + { + "attribute": "content_type", + "operator": "ONE_OF", + "value": [ + "full" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "detailed-content", + "shards": [] + } + ], + "doLog": true + } + ], + "totalShards": 10000 + }, + "falsy-value-assignments": { + "key": "falsy-value-assignments", + "enabled": true, + "variationType": "INTEGER", + "variations": { + "zero-limit": { + "key": "zero-limit", + "value": 0 + }, + "premium-limit": { + "key": "premium-limit", + "value": 100 + } + }, + "allocations": [ + { + "key": "free-tier-limit", + "rules": [ + { + "conditions": [ + { + "attribute": "plan_tier", + "operator": "ONE_OF", + "value": [ + "free" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "zero-limit", + "shards": [] + } + ], + "doLog": true + }, + { + "key": "premium-tier-limit", + "rules": [ + { + "conditions": [ + { + "attribute": "plan_tier", + "operator": "ONE_OF", + "value": [ + "premium" + ] + } + ] + } + ], + "splits": [ + { + "variationKey": "premium-limit", + "shards": [] + } + ], + "doLog": true + } + ], + "totalShards": 10000 + }, + "empty-targeting-key-flag": { + "key": "empty-targeting-key-flag", + "enabled": true, + "variationType": "STRING", + "variations": { + "on": { + "key": "on", + "value": "on-value" + }, + "off": { + "key": "off", + "value": "off-value" + } + }, + "allocations": [ + { + "key": "default-allocation", + "rules": [], + "splits": [ + { + "variationKey": "on", + "shards": [] + } + ], + "doLog": true + } + ] + }, + "microsecond-date-test": { + "key": "microsecond-date-test", + "enabled": true, + "variationType": "STRING", + "variations": { + "expired": { + "key": "expired", + "value": "expired" + }, + "active": { + "key": "active", + "value": "active" + }, + "future": { + "key": "future", + "value": "future" + } + }, + "allocations": [ + { + "key": "expired-allocation", + "splits": [ + { + "variationKey": "expired", + "shards": [] + } + ], + "endAt": "2002-10-31T09:00:00.594321Z", + "doLog": true + }, + { + "key": "future-allocation", + "splits": [ + { + "variationKey": "future", + "shards": [] + } + ], + "startAt": "2052-10-31T09:00:00.123456Z", + "doLog": true + }, + { + "key": "active-allocation", + "splits": [ + { + "variationKey": "active", + "shards": [] + } + ], + "startAt": "2022-10-31T09:00:00.235982Z", + "endAt": "2050-10-31T09:00:00.987654Z", + "doLog": true + } + ] + } + } +} diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-case-boolean-false-assignment.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-boolean-false-assignment.json new file mode 100644 index 00000000000..cdd2b0a513d --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-boolean-false-assignment.json @@ -0,0 +1,38 @@ +[ + { + "flag": "boolean-false-assignment", + "variationType": "BOOLEAN", + "defaultValue": true, + "targetingKey": "alice", + "attributes": { + "should_disable_feature": true + }, + "result": { + "value": false + } + }, + { + "flag": "boolean-false-assignment", + "variationType": "BOOLEAN", + "defaultValue": true, + "targetingKey": "bob", + "attributes": { + "should_disable_feature": false + }, + "result": { + "value": true + } + }, + { + "flag": "boolean-false-assignment", + "variationType": "BOOLEAN", + "defaultValue": true, + "targetingKey": "charlie", + "attributes": { + "unknown_attribute": "value" + }, + "result": { + "value": true + } + } +] \ No newline at end of file diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-case-boolean-one-of-matches.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-boolean-one-of-matches.json new file mode 100644 index 00000000000..f616614d936 --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-boolean-one-of-matches.json @@ -0,0 +1,192 @@ +[ + { + "flag": "boolean-one-of-matches", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "alice", + "attributes": { + "one_of_flag": true + }, + "result": { + "value": 1 + } + }, + { + "flag": "boolean-one-of-matches", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "bob", + "attributes": { + "one_of_flag": false + }, + "result": { + "value": 0 + } + }, + { + "flag": "boolean-one-of-matches", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "charlie", + "attributes": { + "one_of_flag": "True" + }, + "result": { + "value": 0 + } + }, + { + "flag": "boolean-one-of-matches", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "derek", + "attributes": { + "matches_flag": true + }, + "result": { + "value": 2 + } + }, + { + "flag": "boolean-one-of-matches", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "erica", + "attributes": { + "matches_flag": false + }, + "result": { + "value": 0 + } + }, + { + "flag": "boolean-one-of-matches", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "frank", + "attributes": { + "not_matches_flag": false + }, + "result": { + "value": 0 + } + }, + { + "flag": "boolean-one-of-matches", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "george", + "attributes": { + "not_matches_flag": true + }, + "result": { + "value": 4 + } + }, + { + "flag": "boolean-one-of-matches", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "haley", + "attributes": { + "not_matches_flag": "False" + }, + "result": { + "value": 4 + } + }, + { + "flag": "boolean-one-of-matches", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "ivy", + "attributes": { + "not_one_of_flag": true + }, + "result": { + "value": 3 + } + }, + { + "flag": "boolean-one-of-matches", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "julia", + "attributes": { + "not_one_of_flag": false + }, + "result": { + "value": 0 + } + }, + { + "flag": "boolean-one-of-matches", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "kim", + "attributes": { + "not_one_of_flag": "False" + }, + "result": { + "value": 3 + } + }, + { + "flag": "boolean-one-of-matches", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "lucas", + "attributes": { + "not_one_of_flag": "true" + }, + "result": { + "value": 3 + } + }, + { + "flag": "boolean-one-of-matches", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "mike", + "attributes": { + "not_one_of_flag": "false" + }, + "result": { + "value": 0 + } + }, + { + "flag": "boolean-one-of-matches", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "nicole", + "attributes": { + "null_flag": "null" + }, + "result": { + "value": 5 + } + }, + { + "flag": "boolean-one-of-matches", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "owen", + "attributes": { + "null_flag": null + }, + "result": { + "value": 0 + } + }, + { + "flag": "boolean-one-of-matches", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "pete", + "attributes": {}, + "result": { + "value": 0 + } + } +] diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-case-comparator-operator-flag.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-comparator-operator-flag.json new file mode 100644 index 00000000000..2d94f30eb30 --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-comparator-operator-flag.json @@ -0,0 +1,64 @@ +[ + { + "flag": "comparator-operator-test", + "variationType": "STRING", + "defaultValue": "unknown", + "targetingKey": "alice", + "attributes": { + "size": 5, + "country": "US" + }, + "result": { + "value": "small" + } + }, + { + "flag": "comparator-operator-test", + "variationType": "STRING", + "defaultValue": "unknown", + "targetingKey": "bob", + "attributes": { + "size": 10, + "country": "Canada" + }, + "result": { + "value": "medium" + } + }, + { + "flag": "comparator-operator-test", + "variationType": "STRING", + "defaultValue": "unknown", + "targetingKey": "charlie", + "attributes": { + "size": 25 + }, + "result": { + "value": "unknown" + } + }, + { + "flag": "comparator-operator-test", + "variationType": "STRING", + "defaultValue": "unknown", + "targetingKey": "david", + "attributes": { + "size": 26 + }, + "result": { + "value": "large" + } + }, + { + "flag": "comparator-operator-test", + "variationType": "STRING", + "defaultValue": "unknown", + "targetingKey": "elize", + "attributes": { + "country": "UK" + }, + "result": { + "value": "unknown" + } + } +] diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-case-disabled-flag.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-disabled-flag.json new file mode 100644 index 00000000000..0da79189ade --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-disabled-flag.json @@ -0,0 +1,40 @@ +[ + { + "flag": "disabled_flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "alice", + "attributes": { + "email": "alice@mycompany.com", + "country": "US" + }, + "result": { + "value": 0 + } + }, + { + "flag": "disabled_flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "bob", + "attributes": { + "email": "bob@example.com", + "country": "Canada" + }, + "result": { + "value": 0 + } + }, + { + "flag": "disabled_flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "charlie", + "attributes": { + "age": 50 + }, + "result": { + "value": 0 + } + } +] diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-case-empty-flag.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-empty-flag.json new file mode 100644 index 00000000000..52100b1fe47 --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-empty-flag.json @@ -0,0 +1,40 @@ +[ + { + "flag": "empty_flag", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "alice", + "attributes": { + "email": "alice@mycompany.com", + "country": "US" + }, + "result": { + "value": "default_value" + } + }, + { + "flag": "empty_flag", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "bob", + "attributes": { + "email": "bob@example.com", + "country": "Canada" + }, + "result": { + "value": "default_value" + } + }, + { + "flag": "empty_flag", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "charlie", + "attributes": { + "age": 50 + }, + "result": { + "value": "default_value" + } + } +] diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-case-empty-string-variation.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-empty-string-variation.json new file mode 100644 index 00000000000..1af431f3db4 --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-empty-string-variation.json @@ -0,0 +1,38 @@ +[ + { + "flag": "empty-string-variation", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "empty_user", + "attributes": { + "content_type": "minimal" + }, + "result": { + "value": "" + } + }, + { + "flag": "empty-string-variation", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "full_user", + "attributes": { + "content_type": "full" + }, + "result": { + "value": "detailed_content" + } + }, + { + "flag": "empty-string-variation", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "default_user", + "attributes": { + "content_type": "unknown" + }, + "result": { + "value": "default_value" + } + } +] \ No newline at end of file diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-case-falsy-value-assignments.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-falsy-value-assignments.json new file mode 100644 index 00000000000..4ad259620d8 --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-falsy-value-assignments.json @@ -0,0 +1,38 @@ +[ + { + "flag": "falsy-value-assignments", + "variationType": "INTEGER", + "defaultValue": 999, + "targetingKey": "zero_user", + "attributes": { + "plan_tier": "free" + }, + "result": { + "value": 0 + } + }, + { + "flag": "falsy-value-assignments", + "variationType": "INTEGER", + "defaultValue": 999, + "targetingKey": "premium_user", + "attributes": { + "plan_tier": "premium" + }, + "result": { + "value": 100 + } + }, + { + "flag": "falsy-value-assignments", + "variationType": "INTEGER", + "defaultValue": 999, + "targetingKey": "unknown_user", + "attributes": { + "plan_tier": "trial" + }, + "result": { + "value": 999 + } + } +] \ No newline at end of file diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-case-flag-with-empty-string.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-flag-with-empty-string.json new file mode 100644 index 00000000000..0bf562efa35 --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-flag-with-empty-string.json @@ -0,0 +1,24 @@ +[ + { + "flag": "empty_string_flag", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "alice", + "attributes": { + "country": "US" + }, + "result": { + "value": "" + } + }, + { + "flag": "empty_string_flag", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "bob", + "attributes": {}, + "result": { + "value": "non_empty" + } + } +] diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-case-integer-flag.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-integer-flag.json new file mode 100644 index 00000000000..5e930885b44 --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-integer-flag.json @@ -0,0 +1,244 @@ +[ + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "alice", + "attributes": { + "email": "alice@mycompany.com", + "country": "US" + }, + "result": { + "value": 3 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "bob", + "attributes": { + "email": "bob@example.com", + "country": "Canada" + }, + "result": { + "value": 3 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "charlie", + "attributes": { + "age": 50 + }, + "result": { + "value": 2 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "debra", + "attributes": { + "email": "test@test.com", + "country": "Mexico", + "age": 25 + }, + "result": { + "value": 3 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "1", + "attributes": {}, + "result": { + "value": 2 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "2", + "attributes": {}, + "result": { + "value": 2 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "3", + "attributes": {}, + "result": { + "value": 2 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "4", + "attributes": {}, + "result": { + "value": 2 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "5", + "attributes": {}, + "result": { + "value": 1 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "6", + "attributes": {}, + "result": { + "value": 2 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "7", + "attributes": {}, + "result": { + "value": 1 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "8", + "attributes": {}, + "result": { + "value": 1 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "9", + "attributes": {}, + "result": { + "value": 2 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "10", + "attributes": {}, + "result": { + "value": 2 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "11", + "attributes": {}, + "result": { + "value": 1 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "12", + "attributes": {}, + "result": { + "value": 1 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "13", + "attributes": {}, + "result": { + "value": 2 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "14", + "attributes": {}, + "result": { + "value": 1 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "15", + "attributes": {}, + "result": { + "value": 2 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "16", + "attributes": {}, + "result": { + "value": 2 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "17", + "attributes": {}, + "result": { + "value": 1 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "18", + "attributes": {}, + "result": { + "value": 1 + } + }, + { + "flag": "integer-flag", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "19", + "attributes": {}, + "result": { + "value": 1 + } + } +] diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-case-kill-switch-flag.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-kill-switch-flag.json new file mode 100644 index 00000000000..8f34a1bc3af --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-kill-switch-flag.json @@ -0,0 +1,290 @@ +[ + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "alice", + "attributes": { + "email": "alice@mycompany.com", + "country": "US" + }, + "result": { + "value": true + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "bob", + "attributes": { + "email": "bob@example.com", + "country": "Canada" + }, + "result": { + "value": true + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "barbara", + "attributes": { + "email": "barbara@example.com", + "country": "canada" + }, + "result": { + "value": false + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "charlie", + "attributes": { + "age": 40 + }, + "result": { + "value": false + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "debra", + "attributes": { + "email": "test@test.com", + "country": "Mexico", + "age": 25 + }, + "result": { + "value": true + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "1", + "attributes": {}, + "result": { + "value": false + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "2", + "attributes": { + "country": "Mexico" + }, + "result": { + "value": true + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "3", + "attributes": { + "country": "UK", + "age": 50 + }, + "result": { + "value": true + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "4", + "attributes": { + "country": "Germany" + }, + "result": { + "value": false + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "5", + "attributes": { + "country": "Germany" + }, + "result": { + "value": false + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "6", + "attributes": { + "country": "Germany" + }, + "result": { + "value": false + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "7", + "attributes": { + "country": "US", + "age": 12 + }, + "result": { + "value": true + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "8", + "attributes": { + "country": "Italy", + "age": 60 + }, + "result": { + "value": true + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "9", + "attributes": { + "email": "email@email.com" + }, + "result": { + "value": false + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "10", + "attributes": {}, + "result": { + "value": false + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "11", + "attributes": {}, + "result": { + "value": false + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "12", + "attributes": { + "country": "US" + }, + "result": { + "value": true + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "13", + "attributes": { + "country": "Canada" + }, + "result": { + "value": true + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "14", + "attributes": {}, + "result": { + "value": false + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "15", + "attributes": { + "country": "Denmark" + }, + "result": { + "value": false + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "16", + "attributes": { + "country": "Norway" + }, + "result": { + "value": false + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "17", + "attributes": { + "country": "UK" + }, + "result": { + "value": false + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "18", + "attributes": { + "country": "UK" + }, + "result": { + "value": false + } + }, + { + "flag": "kill-switch", + "variationType": "BOOLEAN", + "defaultValue": false, + "targetingKey": "19", + "attributes": { + "country": "UK" + }, + "result": { + "value": false + } + } +] diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-case-microsecond-date-flag.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-microsecond-date-flag.json new file mode 100644 index 00000000000..b944dbf6d80 --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-microsecond-date-flag.json @@ -0,0 +1,36 @@ +[ + { + "flag": "microsecond-date-test", + "variationType": "STRING", + "defaultValue": "unknown", + "targetingKey": "alice", + "attributes": {}, + "result": { + "value": "active" + } + }, + { + "flag": "microsecond-date-test", + "variationType": "STRING", + "defaultValue": "unknown", + "targetingKey": "bob", + "attributes": { + "country": "US" + }, + "result": { + "value": "active" + } + }, + { + "flag": "microsecond-date-test", + "variationType": "STRING", + "defaultValue": "unknown", + "targetingKey": "charlie", + "attributes": { + "version": "1.0.0" + }, + "result": { + "value": "active" + } + } +] diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-case-new-user-onboarding-flag.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-new-user-onboarding-flag.json new file mode 100644 index 00000000000..8ed3e2a024f --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-new-user-onboarding-flag.json @@ -0,0 +1,318 @@ +[ + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "alice", + "attributes": { + "email": "alice@mycompany.com", + "country": "US" + }, + "result": { + "value": "green" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "bob", + "attributes": { + "email": "bob@example.com", + "country": "Canada" + }, + "result": { + "value": "default" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "charlie", + "attributes": { + "age": 50 + }, + "result": { + "value": "default" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "debra", + "attributes": { + "email": "test@test.com", + "country": "Mexico", + "age": 25 + }, + "result": { + "value": "blue" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "zach", + "attributes": { + "email": "test@test.com", + "country": "Mexico", + "age": 25 + }, + "result": { + "value": "purple" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "zach", + "attributes": { + "id": "override-id", + "email": "test@test.com", + "country": "Mexico", + "age": 25 + }, + "result": { + "value": "blue" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "Zach", + "attributes": { + "email": "test@test.com", + "country": "Mexico", + "age": 25 + }, + "result": { + "value": "default" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "1", + "attributes": {}, + "result": { + "value": "default" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "2", + "attributes": { + "country": "Mexico" + }, + "result": { + "value": "blue" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "3", + "attributes": { + "country": "UK", + "age": 33 + }, + "result": { + "value": "control" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "4", + "attributes": { + "country": "Germany" + }, + "result": { + "value": "red" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "5", + "attributes": { + "country": "Germany" + }, + "result": { + "value": "yellow" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "6", + "attributes": { + "country": "Germany" + }, + "result": { + "value": "yellow" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "7", + "attributes": { + "country": "US" + }, + "result": { + "value": "blue" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "8", + "attributes": { + "country": "Italy" + }, + "result": { + "value": "red" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "9", + "attributes": { + "email": "email@email.com" + }, + "result": { + "value": "default" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "10", + "attributes": {}, + "result": { + "value": "default" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "11", + "attributes": {}, + "result": { + "value": "default" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "12", + "attributes": { + "country": "US" + }, + "result": { + "value": "blue" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "13", + "attributes": { + "country": "Canada" + }, + "result": { + "value": "blue" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "14", + "attributes": {}, + "result": { + "value": "default" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "15", + "attributes": { + "country": "Denmark" + }, + "result": { + "value": "yellow" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "16", + "attributes": { + "country": "Norway" + }, + "result": { + "value": "control" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "17", + "attributes": { + "country": "UK" + }, + "result": { + "value": "control" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "18", + "attributes": { + "country": "UK" + }, + "result": { + "value": "default" + } + }, + { + "flag": "new-user-onboarding", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "19", + "attributes": { + "country": "UK" + }, + "result": { + "value": "red" + } + } +] diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-case-no-allocations-flag.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-no-allocations-flag.json new file mode 100644 index 00000000000..132c39db32a --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-no-allocations-flag.json @@ -0,0 +1,52 @@ +[ + { + "flag": "no_allocations_flag", + "variationType": "JSON", + "defaultValue": { + "hello": "world" + }, + "targetingKey": "alice", + "attributes": { + "email": "alice@mycompany.com", + "country": "US" + }, + "result": { + "value": { + "hello": "world" + } + } + }, + { + "flag": "no_allocations_flag", + "variationType": "JSON", + "defaultValue": { + "hello": "world" + }, + "targetingKey": "bob", + "attributes": { + "email": "bob@example.com", + "country": "Canada" + }, + "result": { + "value": { + "hello": "world" + } + } + }, + { + "flag": "no_allocations_flag", + "variationType": "JSON", + "defaultValue": { + "hello": "world" + }, + "targetingKey": "charlie", + "attributes": { + "age": 50 + }, + "result": { + "value": { + "hello": "world" + } + } + } +] diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-case-null-operator-flag.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-null-operator-flag.json new file mode 100644 index 00000000000..09e5d78dacd --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-null-operator-flag.json @@ -0,0 +1,64 @@ +[ + { + "flag": "null-operator-test", + "variationType": "STRING", + "defaultValue": "default-null", + "targetingKey": "alice", + "attributes": { + "size": 5, + "country": "US" + }, + "result": { + "value": "old" + } + }, + { + "flag": "null-operator-test", + "variationType": "STRING", + "defaultValue": "default-null", + "targetingKey": "bob", + "attributes": { + "size": 10, + "country": "Canada" + }, + "result": { + "value": "new" + } + }, + { + "flag": "null-operator-test", + "variationType": "STRING", + "defaultValue": "default-null", + "targetingKey": "charlie", + "attributes": { + "size": null + }, + "result": { + "value": "old" + } + }, + { + "flag": "null-operator-test", + "variationType": "STRING", + "defaultValue": "default-null", + "targetingKey": "david", + "attributes": { + "size": 26 + }, + "result": { + "value": "new" + } + }, + { + "flag": "null-operator-test", + "variationType": "STRING", + "defaultValue": "default-null", + "targetingKey": "elize", + "attributes": { + "country": "UK" + }, + "result": { + "value": "old" + } + } +] diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-case-numeric-flag.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-numeric-flag.json new file mode 100644 index 00000000000..757f0f70e55 --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-numeric-flag.json @@ -0,0 +1,40 @@ +[ + { + "flag": "numeric_flag", + "variationType": "NUMERIC", + "defaultValue": 0.0, + "targetingKey": "alice", + "attributes": { + "email": "alice@mycompany.com", + "country": "US" + }, + "result": { + "value": 3.1415926 + } + }, + { + "flag": "numeric_flag", + "variationType": "NUMERIC", + "defaultValue": 0.0, + "targetingKey": "bob", + "attributes": { + "email": "bob@example.com", + "country": "Canada" + }, + "result": { + "value": 3.1415926 + } + }, + { + "flag": "numeric_flag", + "variationType": "NUMERIC", + "defaultValue": 0.0, + "targetingKey": "charlie", + "attributes": { + "age": 50 + }, + "result": { + "value": 3.1415926 + } + } +] diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-case-numeric-one-of.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-numeric-one-of.json new file mode 100644 index 00000000000..9eaccbc477c --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-numeric-one-of.json @@ -0,0 +1,86 @@ +[ + { + "flag": "numeric-one-of", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "alice", + "attributes": { + "number": 1 + }, + "result": { + "value": 1 + } + }, + { + "flag": "numeric-one-of", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "bob", + "attributes": { + "number": 2 + }, + "result": { + "value": 0 + } + }, + { + "flag": "numeric-one-of", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "charlie", + "attributes": { + "number": 3 + }, + "result": { + "value": 3 + } + }, + { + "flag": "numeric-one-of", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "derek", + "attributes": { + "number": 4 + }, + "result": { + "value": 3 + } + }, + { + "flag": "numeric-one-of", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "erica", + "attributes": { + "number": "1" + }, + "result": { + "value": 1 + } + }, + { + "flag": "numeric-one-of", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "frank", + "attributes": { + "number": 1 + }, + "result": { + "value": 1 + } + }, + { + "flag": "numeric-one-of", + "variationType": "INTEGER", + "defaultValue": 0, + "targetingKey": "george", + "attributes": { + "number": 123456789 + }, + "result": { + "value": 2 + } + } +] diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-case-of-7-empty-targeting-key.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-of-7-empty-targeting-key.json new file mode 100644 index 00000000000..03f03ab1ecb --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-of-7-empty-targeting-key.json @@ -0,0 +1,12 @@ +[ + { + "flag": "empty-targeting-key-flag", + "variationType": "STRING", + "defaultValue": "default", + "targetingKey": "", + "attributes": {}, + "result": { + "value": "on-value" + } + } +] diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-case-regex-flag.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-regex-flag.json new file mode 100644 index 00000000000..94aa87f23a9 --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-regex-flag.json @@ -0,0 +1,53 @@ +[ + { + "flag": "regex-flag", + "variationType": "STRING", + "defaultValue": "none", + "targetingKey": "alice", + "attributes": { + "version": "1.15.0", + "email": "alice@example.com" + }, + "result": { + "value": "partial-example" + } + }, + { + "flag": "regex-flag", + "variationType": "STRING", + "defaultValue": "none", + "targetingKey": "bob", + "attributes": { + "version": "0.20.1", + "email": "bob@test.com" + }, + "result": { + "value": "test" + } + }, + { + "flag": "regex-flag", + "variationType": "STRING", + "defaultValue": "none", + "targetingKey": "charlie", + "attributes": { + "version": "2.1.13" + }, + "result": { + "value": "none" + } + }, + { + "flag": "regex-flag", + "variationType": "STRING", + "defaultValue": "none", + "targetingKey": "derek", + "attributes": { + "version": "2.1.13", + "email": "derek@gmail.com" + }, + "result": { + "value": "none" + } + } +] diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-case-start-and-end-date-flag.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-start-and-end-date-flag.json new file mode 100644 index 00000000000..7a48ec35886 --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-case-start-and-end-date-flag.json @@ -0,0 +1,40 @@ +[ + { + "flag": "start-and-end-date-test", + "variationType": "STRING", + "defaultValue": "unknown", + "targetingKey": "alice", + "attributes": { + "version": "1.15.0", + "country": "US" + }, + "result": { + "value": "current" + } + }, + { + "flag": "start-and-end-date-test", + "variationType": "STRING", + "defaultValue": "unknown", + "targetingKey": "bob", + "attributes": { + "version": "0.20.1", + "country": "Canada" + }, + "result": { + "value": "current" + } + }, + { + "flag": "start-and-end-date-test", + "variationType": "STRING", + "defaultValue": "unknown", + "targetingKey": "charlie", + "attributes": { + "version": "2.1.13" + }, + "result": { + "value": "current" + } + } +] diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-flag-that-does-not-exist.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-flag-that-does-not-exist.json new file mode 100644 index 00000000000..7499bba1c50 --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-flag-that-does-not-exist.json @@ -0,0 +1,40 @@ +[ + { + "flag": "flag-that-does-not-exist", + "variationType": "NUMERIC", + "defaultValue": 0.0, + "targetingKey": "alice", + "attributes": { + "email": "alice@mycompany.com", + "country": "US" + }, + "result": { + "value": 0.0 + } + }, + { + "flag": "flag-that-does-not-exist", + "variationType": "NUMERIC", + "defaultValue": 0.0, + "targetingKey": "bob", + "attributes": { + "email": "bob@example.com", + "country": "Canada" + }, + "result": { + "value": 0.0 + } + }, + { + "flag": "flag-that-does-not-exist", + "variationType": "NUMERIC", + "defaultValue": 0.0, + "targetingKey": "charlie", + "attributes": { + "age": 50 + }, + "result": { + "value": 0.0 + } + } +] diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-json-config-flag.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-json-config-flag.json new file mode 100644 index 00000000000..ecc799546b0 --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-json-config-flag.json @@ -0,0 +1,72 @@ +[ + { + "flag": "json-config-flag", + "variationType": "JSON", + "defaultValue": { + "foo": "bar" + }, + "targetingKey": "alice", + "attributes": { + "email": "alice@mycompany.com", + "country": "US" + }, + "result": { + "value": { + "integer": 1, + "string": "one", + "float": 1.0 + } + } + }, + { + "flag": "json-config-flag", + "variationType": "JSON", + "defaultValue": { + "foo": "bar" + }, + "targetingKey": "bob", + "attributes": { + "email": "bob@example.com", + "country": "Canada" + }, + "result": { + "value": { + "integer": 2, + "string": "two", + "float": 2.0 + } + } + }, + { + "flag": "json-config-flag", + "variationType": "JSON", + "defaultValue": { + "foo": "bar" + }, + "targetingKey": "charlie", + "attributes": { + "age": 50 + }, + "result": { + "value": { + "integer": 2, + "string": "two", + "float": 2.0 + } + } + }, + { + "flag": "json-config-flag", + "variationType": "JSON", + "defaultValue": { + "foo": "bar" + }, + "targetingKey": "diana", + "attributes": { + "Force Empty": true + }, + "result": { + "value": {} + } + } +] diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-no-allocations-flag.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-no-allocations-flag.json new file mode 100644 index 00000000000..45867e5897c --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-no-allocations-flag.json @@ -0,0 +1,52 @@ +[ + { + "flag": "no_allocations_flag", + "variationType": "JSON", + "defaultValue": { + "message": "Hello, world!" + }, + "targetingKey": "alice", + "attributes": { + "email": "alice@mycompany.com", + "country": "US" + }, + "result": { + "value": { + "message": "Hello, world!" + } + } + }, + { + "flag": "no_allocations_flag", + "variationType": "JSON", + "defaultValue": { + "message": "Hello, world!" + }, + "targetingKey": "bob", + "attributes": { + "email": "bob@example.com", + "country": "Canada" + }, + "result": { + "value": { + "message": "Hello, world!" + } + } + }, + { + "flag": "no_allocations_flag", + "variationType": "JSON", + "defaultValue": { + "message": "Hello, world!" + }, + "targetingKey": "charlie", + "attributes": { + "age": 50 + }, + "result": { + "value": { + "message": "Hello, world!" + } + } + } +] diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-special-characters.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-special-characters.json new file mode 100644 index 00000000000..120647ec3b5 --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-special-characters.json @@ -0,0 +1,54 @@ +[ + { + "flag": "special-characters", + "variationType": "JSON", + "defaultValue": {}, + "targetingKey": "ash", + "attributes": {}, + "result": { + "value": { + "a": "kümmert", + "b": "schön" + } + } + }, + { + "flag": "special-characters", + "variationType": "JSON", + "defaultValue": {}, + "targetingKey": "ben", + "attributes": {}, + "result": { + "value": { + "a": "піклуватися", + "b": "любов" + } + } + }, + { + "flag": "special-characters", + "variationType": "JSON", + "defaultValue": {}, + "targetingKey": "cameron", + "attributes": {}, + "result": { + "value": { + "a": "照顾", + "b": "漂亮" + } + } + }, + { + "flag": "special-characters", + "variationType": "JSON", + "defaultValue": {}, + "targetingKey": "darryl", + "attributes": {}, + "result": { + "value": { + "a": "🤗", + "b": "🌸" + } + } + } +] diff --git a/tests/FeatureFlags/fixtures/evaluation-cases/test-string-with-special-characters.json b/tests/FeatureFlags/fixtures/evaluation-cases/test-string-with-special-characters.json new file mode 100644 index 00000000000..e56322d44f0 --- /dev/null +++ b/tests/FeatureFlags/fixtures/evaluation-cases/test-string-with-special-characters.json @@ -0,0 +1,794 @@ +[ + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_spaces", + "attributes": { + "string_with_spaces": true + }, + "result": { + "value": " a b c d e f " + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_space", + "attributes": { + "string_with_only_one_space": true + }, + "result": { + "value": " " + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_spaces", + "attributes": { + "string_with_only_multiple_spaces": true + }, + "result": { + "value": " " + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_dots", + "attributes": { + "string_with_dots": true + }, + "result": { + "value": ".a.b.c.d.e.f." + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_dot", + "attributes": { + "string_with_only_one_dot": true + }, + "result": { + "value": "." + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_dots", + "attributes": { + "string_with_only_multiple_dots": true + }, + "result": { + "value": "......." + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_comas", + "attributes": { + "string_with_comas": true + }, + "result": { + "value": ",a,b,c,d,e,f," + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_coma", + "attributes": { + "string_with_only_one_coma": true + }, + "result": { + "value": "," + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_comas", + "attributes": { + "string_with_only_multiple_comas": true + }, + "result": { + "value": ",,,,,,," + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_colons", + "attributes": { + "string_with_colons": true + }, + "result": { + "value": ":a:b:c:d:e:f:" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_colon", + "attributes": { + "string_with_only_one_colon": true + }, + "result": { + "value": ":" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_colons", + "attributes": { + "string_with_only_multiple_colons": true + }, + "result": { + "value": ":::::::" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_semicolons", + "attributes": { + "string_with_semicolons": true + }, + "result": { + "value": ";a;b;c;d;e;f;" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_semicolon", + "attributes": { + "string_with_only_one_semicolon": true + }, + "result": { + "value": ";" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_semicolons", + "attributes": { + "string_with_only_multiple_semicolons": true + }, + "result": { + "value": ";;;;;;;" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_slashes", + "attributes": { + "string_with_slashes": true + }, + "result": { + "value": "/a/b/c/d/e/f/" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_slash", + "attributes": { + "string_with_only_one_slash": true + }, + "result": { + "value": "/" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_slashes", + "attributes": { + "string_with_only_multiple_slashes": true + }, + "result": { + "value": "///////" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_dashes", + "attributes": { + "string_with_dashes": true + }, + "result": { + "value": "-a-b-c-d-e-f-" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_dash", + "attributes": { + "string_with_only_one_dash": true + }, + "result": { + "value": "-" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_dashes", + "attributes": { + "string_with_only_multiple_dashes": true + }, + "result": { + "value": "-------" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_underscores", + "attributes": { + "string_with_underscores": true + }, + "result": { + "value": "_a_b_c_d_e_f_" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_underscore", + "attributes": { + "string_with_only_one_underscore": true + }, + "result": { + "value": "_" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_underscores", + "attributes": { + "string_with_only_multiple_underscores": true + }, + "result": { + "value": "_______" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_plus_signs", + "attributes": { + "string_with_plus_signs": true + }, + "result": { + "value": "+a+b+c+d+e+f+" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_plus_sign", + "attributes": { + "string_with_only_one_plus_sign": true + }, + "result": { + "value": "+" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_plus_signs", + "attributes": { + "string_with_only_multiple_plus_signs": true + }, + "result": { + "value": "+++++++" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_equal_signs", + "attributes": { + "string_with_equal_signs": true + }, + "result": { + "value": "=a=b=c=d=e=f=" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_equal_sign", + "attributes": { + "string_with_only_one_equal_sign": true + }, + "result": { + "value": "=" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_equal_signs", + "attributes": { + "string_with_only_multiple_equal_signs": true + }, + "result": { + "value": "=======" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_dollar_signs", + "attributes": { + "string_with_dollar_signs": true + }, + "result": { + "value": "$a$b$c$d$e$f$" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_dollar_sign", + "attributes": { + "string_with_only_one_dollar_sign": true + }, + "result": { + "value": "$" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_dollar_signs", + "attributes": { + "string_with_only_multiple_dollar_signs": true + }, + "result": { + "value": "$$$$$$$" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_at_signs", + "attributes": { + "string_with_at_signs": true + }, + "result": { + "value": "@a@b@c@d@e@f@" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_at_sign", + "attributes": { + "string_with_only_one_at_sign": true + }, + "result": { + "value": "@" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_at_signs", + "attributes": { + "string_with_only_multiple_at_signs": true + }, + "result": { + "value": "@@@@@@@" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_amp_signs", + "attributes": { + "string_with_amp_signs": true + }, + "result": { + "value": "&a&b&c&d&e&f&" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_amp_sign", + "attributes": { + "string_with_only_one_amp_sign": true + }, + "result": { + "value": "&" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_amp_signs", + "attributes": { + "string_with_only_multiple_amp_signs": true + }, + "result": { + "value": "&&&&&&&" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_hash_signs", + "attributes": { + "string_with_hash_signs": true + }, + "result": { + "value": "#a#b#c#d#e#f#" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_hash_sign", + "attributes": { + "string_with_only_one_hash_sign": true + }, + "result": { + "value": "#" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_hash_signs", + "attributes": { + "string_with_only_multiple_hash_signs": true + }, + "result": { + "value": "#######" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_percentage_signs", + "attributes": { + "string_with_percentage_signs": true + }, + "result": { + "value": "%a%b%c%d%e%f%" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_percentage_sign", + "attributes": { + "string_with_only_one_percentage_sign": true + }, + "result": { + "value": "%" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_percentage_signs", + "attributes": { + "string_with_only_multiple_percentage_signs": true + }, + "result": { + "value": "%%%%%%%" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_tilde_signs", + "attributes": { + "string_with_tilde_signs": true + }, + "result": { + "value": "~a~b~c~d~e~f~" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_tilde_sign", + "attributes": { + "string_with_only_one_tilde_sign": true + }, + "result": { + "value": "~" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_tilde_signs", + "attributes": { + "string_with_only_multiple_tilde_signs": true + }, + "result": { + "value": "~~~~~~~" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_asterix_signs", + "attributes": { + "string_with_asterix_signs": true + }, + "result": { + "value": "*a*b*c*d*e*f*" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_asterix_sign", + "attributes": { + "string_with_only_one_asterix_sign": true + }, + "result": { + "value": "*" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_asterix_signs", + "attributes": { + "string_with_only_multiple_asterix_signs": true + }, + "result": { + "value": "*******" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_single_quotes", + "attributes": { + "string_with_single_quotes": true + }, + "result": { + "value": "'a'b'c'd'e'f'" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_single_quote", + "attributes": { + "string_with_only_one_single_quote": true + }, + "result": { + "value": "'" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_single_quotes", + "attributes": { + "string_with_only_multiple_single_quotes": true + }, + "result": { + "value": "'''''''" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_question_marks", + "attributes": { + "string_with_question_marks": true + }, + "result": { + "value": "?a?b?c?d?e?f?" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_question_mark", + "attributes": { + "string_with_only_one_question_mark": true + }, + "result": { + "value": "?" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_question_marks", + "attributes": { + "string_with_only_multiple_question_marks": true + }, + "result": { + "value": "???????" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_exclamation_marks", + "attributes": { + "string_with_exclamation_marks": true + }, + "result": { + "value": "!a!b!c!d!e!f!" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_exclamation_mark", + "attributes": { + "string_with_only_one_exclamation_mark": true + }, + "result": { + "value": "!" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_exclamation_marks", + "attributes": { + "string_with_only_multiple_exclamation_marks": true + }, + "result": { + "value": "!!!!!!!" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_opening_parentheses", + "attributes": { + "string_with_opening_parentheses": true + }, + "result": { + "value": "(a(b(c(d(e(f(" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_opening_parenthese", + "attributes": { + "string_with_only_one_opening_parenthese": true + }, + "result": { + "value": "(" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_opening_parentheses", + "attributes": { + "string_with_only_multiple_opening_parentheses": true + }, + "result": { + "value": "(((((((" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_closing_parentheses", + "attributes": { + "string_with_closing_parentheses": true + }, + "result": { + "value": ")a)b)c)d)e)f)" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_one_closing_parenthese", + "attributes": { + "string_with_only_one_closing_parenthese": true + }, + "result": { + "value": ")" + } + }, + { + "flag": "string_flag_with_special_characters", + "variationType": "STRING", + "defaultValue": "default_value", + "targetingKey": "string_with_only_multiple_closing_parentheses", + "attributes": { + "string_with_only_multiple_closing_parentheses": true + }, + "result": { + "value": ")))))))" + } + } +] diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 986e2fb0e9b..bd1225ea971 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -136,6 +136,9 @@ ./Unit/ + + ./FeatureFlags/ +