sentry-options 1.2.2__tar.gz → 1.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sentry_options-1.2.2 → sentry_options-1.2.4}/Cargo.lock +82 -7
- {sentry_options-1.2.2 → sentry_options-1.2.4}/Cargo.toml +3 -2
- {sentry_options-1.2.2 → sentry_options-1.2.4}/PKG-INFO +1 -1
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/python/uv.lock +1 -1
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/rust/Cargo.toml +1 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/rust/src/features.rs +298 -125
- {sentry_options-1.2.2 → sentry_options-1.2.4}/pyproject.toml +1 -1
- {sentry_options-1.2.2 → sentry_options-1.2.4}/sentry-options-validation/Cargo.toml +1 -3
- {sentry_options-1.2.2 → sentry_options-1.2.4}/sentry-options-validation/src/feature-schema-defs.json +5 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/sentry-options-validation/src/lib.rs +90 -11
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/python/Cargo.toml +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/python/benches/bench_get.py +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/python/benches/bench_has.py +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/python/benches/bench_isset.py +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/python/src/lib.rs +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/python/tests/conftest.py +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/python/tests/features_test.py +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/python/tests/options_override_test.py +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/python/tests/options_test.py +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/python/tests/propagation_test.py +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/python/tests/refresh_test.py +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/rust/benches/features_bench.rs +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/rust/benches/isset_bench.rs +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/rust/benches/options_bench.rs +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/rust/benches/reload_trace.rs +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/rust/src/lib.rs +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/rust/src/testing.rs +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/clients/rust/tests/override_validation.rs +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/python/sentry_options/__init__.py +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/python/sentry_options/_core.pyi +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/python/sentry_options/py.typed +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/python/sentry_options/testing.py +0 -0
- {sentry_options-1.2.2 → sentry_options-1.2.4}/sentry-options-validation/src/namespace-schema.json +0 -0
|
@@ -435,7 +435,7 @@ dependencies = [
|
|
|
435
435
|
|
|
436
436
|
[[package]]
|
|
437
437
|
name = "example"
|
|
438
|
-
version = "1.2.
|
|
438
|
+
version = "1.2.4"
|
|
439
439
|
dependencies = [
|
|
440
440
|
"anyhow",
|
|
441
441
|
"sentry-options",
|
|
@@ -996,6 +996,15 @@ dependencies = [
|
|
|
996
996
|
"windows-sys 0.61.2",
|
|
997
997
|
]
|
|
998
998
|
|
|
999
|
+
[[package]]
|
|
1000
|
+
name = "nu-ansi-term"
|
|
1001
|
+
version = "0.50.3"
|
|
1002
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1003
|
+
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
|
1004
|
+
dependencies = [
|
|
1005
|
+
"windows-sys 0.61.2",
|
|
1006
|
+
]
|
|
1007
|
+
|
|
999
1008
|
[[package]]
|
|
1000
1009
|
name = "num"
|
|
1001
1010
|
version = "0.4.3"
|
|
@@ -1437,7 +1446,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|
|
1437
1446
|
|
|
1438
1447
|
[[package]]
|
|
1439
1448
|
name = "sentry-options"
|
|
1440
|
-
version = "1.2.
|
|
1449
|
+
version = "1.2.4"
|
|
1441
1450
|
dependencies = [
|
|
1442
1451
|
"arc-swap",
|
|
1443
1452
|
"criterion",
|
|
@@ -1447,11 +1456,12 @@ dependencies = [
|
|
|
1447
1456
|
"sha1",
|
|
1448
1457
|
"tempfile",
|
|
1449
1458
|
"thiserror",
|
|
1459
|
+
"tracing",
|
|
1450
1460
|
]
|
|
1451
1461
|
|
|
1452
1462
|
[[package]]
|
|
1453
1463
|
name = "sentry-options-cli"
|
|
1454
|
-
version = "1.2.
|
|
1464
|
+
version = "1.2.4"
|
|
1455
1465
|
dependencies = [
|
|
1456
1466
|
"chrono",
|
|
1457
1467
|
"clap",
|
|
@@ -1461,12 +1471,14 @@ dependencies = [
|
|
|
1461
1471
|
"serde_yaml",
|
|
1462
1472
|
"tempfile",
|
|
1463
1473
|
"thiserror",
|
|
1474
|
+
"tracing",
|
|
1475
|
+
"tracing-subscriber",
|
|
1464
1476
|
"walkdir",
|
|
1465
1477
|
]
|
|
1466
1478
|
|
|
1467
1479
|
[[package]]
|
|
1468
1480
|
name = "sentry-options-python"
|
|
1469
|
-
version = "1.2.
|
|
1481
|
+
version = "1.2.4"
|
|
1470
1482
|
dependencies = [
|
|
1471
1483
|
"pyo3",
|
|
1472
1484
|
"sentry-options",
|
|
@@ -1475,7 +1487,7 @@ dependencies = [
|
|
|
1475
1487
|
|
|
1476
1488
|
[[package]]
|
|
1477
1489
|
name = "sentry-options-validation"
|
|
1478
|
-
version = "1.2.
|
|
1490
|
+
version = "1.2.4"
|
|
1479
1491
|
dependencies = [
|
|
1480
1492
|
"anyhow",
|
|
1481
1493
|
"arc-swap",
|
|
@@ -1485,6 +1497,7 @@ dependencies = [
|
|
|
1485
1497
|
"serde_json",
|
|
1486
1498
|
"tempfile",
|
|
1487
1499
|
"thiserror",
|
|
1500
|
+
"tracing",
|
|
1488
1501
|
]
|
|
1489
1502
|
|
|
1490
1503
|
[[package]]
|
|
@@ -1566,6 +1579,15 @@ dependencies = [
|
|
|
1566
1579
|
"digest",
|
|
1567
1580
|
]
|
|
1568
1581
|
|
|
1582
|
+
[[package]]
|
|
1583
|
+
name = "sharded-slab"
|
|
1584
|
+
version = "0.1.7"
|
|
1585
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1586
|
+
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
|
1587
|
+
dependencies = [
|
|
1588
|
+
"lazy_static",
|
|
1589
|
+
]
|
|
1590
|
+
|
|
1569
1591
|
[[package]]
|
|
1570
1592
|
name = "shlex"
|
|
1571
1593
|
version = "1.3.0"
|
|
@@ -1676,6 +1698,15 @@ dependencies = [
|
|
|
1676
1698
|
"syn",
|
|
1677
1699
|
]
|
|
1678
1700
|
|
|
1701
|
+
[[package]]
|
|
1702
|
+
name = "thread_local"
|
|
1703
|
+
version = "1.1.10"
|
|
1704
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1705
|
+
checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
|
|
1706
|
+
dependencies = [
|
|
1707
|
+
"cfg-if",
|
|
1708
|
+
]
|
|
1709
|
+
|
|
1679
1710
|
[[package]]
|
|
1680
1711
|
name = "tinystr"
|
|
1681
1712
|
version = "0.8.2"
|
|
@@ -1756,14 +1787,26 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
|
|
1756
1787
|
|
|
1757
1788
|
[[package]]
|
|
1758
1789
|
name = "tracing"
|
|
1759
|
-
version = "0.1.
|
|
1790
|
+
version = "0.1.44"
|
|
1760
1791
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1761
|
-
checksum = "
|
|
1792
|
+
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
|
1762
1793
|
dependencies = [
|
|
1763
1794
|
"pin-project-lite",
|
|
1795
|
+
"tracing-attributes",
|
|
1764
1796
|
"tracing-core",
|
|
1765
1797
|
]
|
|
1766
1798
|
|
|
1799
|
+
[[package]]
|
|
1800
|
+
name = "tracing-attributes"
|
|
1801
|
+
version = "0.1.31"
|
|
1802
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1803
|
+
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
|
1804
|
+
dependencies = [
|
|
1805
|
+
"proc-macro2",
|
|
1806
|
+
"quote",
|
|
1807
|
+
"syn",
|
|
1808
|
+
]
|
|
1809
|
+
|
|
1767
1810
|
[[package]]
|
|
1768
1811
|
name = "tracing-core"
|
|
1769
1812
|
version = "0.1.36"
|
|
@@ -1771,6 +1814,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
|
1771
1814
|
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
|
1772
1815
|
dependencies = [
|
|
1773
1816
|
"once_cell",
|
|
1817
|
+
"valuable",
|
|
1818
|
+
]
|
|
1819
|
+
|
|
1820
|
+
[[package]]
|
|
1821
|
+
name = "tracing-log"
|
|
1822
|
+
version = "0.2.0"
|
|
1823
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1824
|
+
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
|
1825
|
+
dependencies = [
|
|
1826
|
+
"log",
|
|
1827
|
+
"once_cell",
|
|
1828
|
+
"tracing-core",
|
|
1829
|
+
]
|
|
1830
|
+
|
|
1831
|
+
[[package]]
|
|
1832
|
+
name = "tracing-subscriber"
|
|
1833
|
+
version = "0.3.23"
|
|
1834
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1835
|
+
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
|
1836
|
+
dependencies = [
|
|
1837
|
+
"nu-ansi-term",
|
|
1838
|
+
"sharded-slab",
|
|
1839
|
+
"smallvec",
|
|
1840
|
+
"thread_local",
|
|
1841
|
+
"tracing-core",
|
|
1842
|
+
"tracing-log",
|
|
1774
1843
|
]
|
|
1775
1844
|
|
|
1776
1845
|
[[package]]
|
|
@@ -1843,6 +1912,12 @@ dependencies = [
|
|
|
1843
1912
|
"vsimd",
|
|
1844
1913
|
]
|
|
1845
1914
|
|
|
1915
|
+
[[package]]
|
|
1916
|
+
name = "valuable"
|
|
1917
|
+
version = "0.1.1"
|
|
1918
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1919
|
+
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
|
1920
|
+
|
|
1846
1921
|
[[package]]
|
|
1847
1922
|
name = "version_check"
|
|
1848
1923
|
version = "0.9.5"
|
|
@@ -3,13 +3,13 @@ resolver = "3"
|
|
|
3
3
|
members = ["sentry-options-validation", "clients/rust", "clients/python"]
|
|
4
4
|
|
|
5
5
|
[workspace.package]
|
|
6
|
-
version = "1.2.
|
|
6
|
+
version = "1.2.4"
|
|
7
7
|
edition = "2024"
|
|
8
8
|
repository = "https://github.com/getsentry/sentry-options"
|
|
9
9
|
license = "Apache-2.0"
|
|
10
10
|
|
|
11
11
|
[workspace.dependencies]
|
|
12
|
-
sentry-options-validation = { path = "sentry-options-validation", version = "1.2.
|
|
12
|
+
sentry-options-validation = { path = "sentry-options-validation", version = "1.2.4" }
|
|
13
13
|
serde = { version = "1.0", features = ["derive"] }
|
|
14
14
|
serde_json = "1.0.145"
|
|
15
15
|
anyhow = "1.0.100"
|
|
@@ -19,3 +19,4 @@ jsonschema = "0.36.0"
|
|
|
19
19
|
sha1 = "0.10"
|
|
20
20
|
arc-swap = "1.9.1"
|
|
21
21
|
chrono = { version = "0.4.45", default-features = false, features = ["std", "clock"] }
|
|
22
|
+
tracing = "0.1.44"
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
use num::bigint::{BigInt, Sign};
|
|
7
7
|
use std::cell::Cell;
|
|
8
8
|
use std::collections::HashMap;
|
|
9
|
-
use std::sync::OnceLock;
|
|
10
9
|
|
|
11
10
|
use serde_json::Value;
|
|
12
11
|
use sha1::{Digest, Sha1};
|
|
@@ -88,7 +87,7 @@ impl FeatureContext {
|
|
|
88
87
|
/// The result is cached and reset when identity fields or context data change.
|
|
89
88
|
/// When no identity fields are set, all context keys are used (non-deterministic
|
|
90
89
|
/// rollout across contexts with different keys).
|
|
91
|
-
/// The id value is mostly used to id % 100
|
|
90
|
+
/// The id value is mostly used to id % 100 <= rollout.
|
|
92
91
|
pub fn id(&self) -> u64 {
|
|
93
92
|
if let Some(id) = self.cached_id.get() {
|
|
94
93
|
return id;
|
|
@@ -229,7 +228,7 @@ impl Segment {
|
|
|
229
228
|
if self.rollout >= 100 {
|
|
230
229
|
return true;
|
|
231
230
|
}
|
|
232
|
-
context.id() % 100
|
|
231
|
+
context.id() % 100 <= self.rollout
|
|
233
232
|
}
|
|
234
233
|
}
|
|
235
234
|
|
|
@@ -256,96 +255,113 @@ impl Condition {
|
|
|
256
255
|
}
|
|
257
256
|
|
|
258
257
|
fn matches(&self, context: &FeatureContext) -> bool {
|
|
259
|
-
let
|
|
260
|
-
|
|
261
|
-
};
|
|
262
|
-
match &self.operator {
|
|
258
|
+
let ctx_val = context.get(&self.property);
|
|
259
|
+
let result = match &self.operator {
|
|
263
260
|
OperatorKind::In => eval_in(ctx_val, &self.value),
|
|
264
|
-
OperatorKind::NotIn =>
|
|
261
|
+
OperatorKind::NotIn => eval_in(ctx_val, &self.value).map(|m| !m),
|
|
265
262
|
OperatorKind::Contains => eval_contains(ctx_val, &self.value),
|
|
266
|
-
OperatorKind::NotContains =>
|
|
263
|
+
OperatorKind::NotContains => eval_contains(ctx_val, &self.value).map(|m| !m),
|
|
267
264
|
OperatorKind::Equals => eval_equals(ctx_val, &self.value),
|
|
268
|
-
OperatorKind::NotEquals =>
|
|
265
|
+
OperatorKind::NotEquals => eval_equals(ctx_val, &self.value).map(|m| !m),
|
|
269
266
|
OperatorKind::Matches => eval_matches(ctx_val, &self.value),
|
|
270
|
-
OperatorKind::NotMatches =>
|
|
271
|
-
}
|
|
267
|
+
OperatorKind::NotMatches => eval_matches(ctx_val, &self.value).map(|m| !m),
|
|
268
|
+
};
|
|
269
|
+
// A comparison that cannot be made is a non-match even when negated:
|
|
270
|
+
// flagpole raises for these, and the exception is caught above the
|
|
271
|
+
// evaluator rather than inverted.
|
|
272
|
+
result.unwrap_or(false)
|
|
272
273
|
}
|
|
273
274
|
}
|
|
274
275
|
|
|
275
|
-
///
|
|
276
|
+
/// For scalar ctx_val, check if it's contained in the condition array.
|
|
277
|
+
/// For array ctx_val, check if there is any intersection with the condition array.
|
|
276
278
|
/// String comparison is case-insensitive.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
279
|
+
///
|
|
280
|
+
/// `None` means the comparison could not be made.
|
|
281
|
+
fn eval_in(ctx_val: Option<&Value>, condition_val: &Value) -> Option<bool> {
|
|
282
|
+
let arr = condition_val.as_array()?;
|
|
283
|
+
match ctx_val {
|
|
284
|
+
None => Some(false),
|
|
285
|
+
Some(Value::Object(_)) => None,
|
|
286
|
+
Some(Value::Array(ctx_arr)) => Some(ctx_arr.iter().any(|v| scalar_in(v, arr))),
|
|
287
|
+
Some(scalar) => Some(scalar_in(scalar, arr)),
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
fn scalar_in(ctx_val: &Value, arr: &[Value]) -> bool {
|
|
281
292
|
match ctx_val {
|
|
282
293
|
Value::String(s) => {
|
|
283
294
|
let s_lower = s.to_lowercase();
|
|
284
295
|
arr.iter()
|
|
285
296
|
.any(|v| v.as_str().is_some_and(|cv| cv.to_lowercase() == s_lower))
|
|
286
297
|
}
|
|
287
|
-
Value::Number(n) =>
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
} else if let Some(f) = n.as_f64() {
|
|
291
|
-
arr.iter().any(|v| v.as_f64().is_some_and(|cv| cv == f))
|
|
292
|
-
} else {
|
|
293
|
-
false
|
|
294
|
-
}
|
|
295
|
-
}
|
|
298
|
+
Value::Number(n) => arr
|
|
299
|
+
.iter()
|
|
300
|
+
.any(|v| v.as_number().is_some_and(|cv| numbers_equal(n, cv))),
|
|
296
301
|
Value::Bool(b) => arr.iter().any(|v| v.as_bool().is_some_and(|cv| cv == *b)),
|
|
297
302
|
_ => false,
|
|
298
303
|
}
|
|
299
304
|
}
|
|
300
305
|
|
|
306
|
+
/// Compare two JSON numbers regardless of their type (int vs float)
|
|
307
|
+
/// 1 == 1, 1.0 == 1, and 1.0 == 1.0
|
|
308
|
+
fn numbers_equal(a: &serde_json::Number, b: &serde_json::Number) -> bool {
|
|
309
|
+
// !f64 as opposed to (i64 || u64)
|
|
310
|
+
if !a.is_f64() && !b.is_f64() {
|
|
311
|
+
return a == b;
|
|
312
|
+
}
|
|
313
|
+
// mismatched types are cast to float
|
|
314
|
+
matches!((a.as_f64(), b.as_f64()), (Some(x), Some(y)) if x == y)
|
|
315
|
+
}
|
|
316
|
+
|
|
301
317
|
/// Check if a context array contains a condition scalar value.
|
|
302
318
|
/// String comparison is case-insensitive.
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
319
|
+
///
|
|
320
|
+
/// A property that is missing or is not a list cannot be searched at all, so
|
|
321
|
+
/// `not_contains` does not grant the feature for it either.
|
|
322
|
+
fn eval_contains(ctx_val: Option<&Value>, condition_val: &Value) -> Option<bool> {
|
|
323
|
+
let ctx_arr = ctx_val?.as_array()?;
|
|
307
324
|
match condition_val {
|
|
308
325
|
Value::String(s) => {
|
|
309
326
|
let s_lower = s.to_lowercase();
|
|
327
|
+
Some(
|
|
328
|
+
ctx_arr
|
|
329
|
+
.iter()
|
|
330
|
+
.any(|v| v.as_str().is_some_and(|cv| cv.to_lowercase() == s_lower)),
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
Value::Number(n) => Some(
|
|
310
334
|
ctx_arr
|
|
311
335
|
.iter()
|
|
312
|
-
.any(|v| v.
|
|
313
|
-
|
|
314
|
-
Value::
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
false
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
Value::Bool(b) => ctx_arr
|
|
324
|
-
.iter()
|
|
325
|
-
.any(|v| v.as_bool().is_some_and(|cv| cv == *b)),
|
|
326
|
-
_ => false,
|
|
336
|
+
.any(|v| v.as_number().is_some_and(|cv| numbers_equal(n, cv))),
|
|
337
|
+
),
|
|
338
|
+
Value::Bool(b) => Some(
|
|
339
|
+
ctx_arr
|
|
340
|
+
.iter()
|
|
341
|
+
.any(|v| v.as_bool().is_some_and(|cv| cv == *b)),
|
|
342
|
+
),
|
|
343
|
+
_ => None,
|
|
327
344
|
}
|
|
328
345
|
}
|
|
329
346
|
|
|
330
347
|
/// Check if a context value equals a condition value.
|
|
348
|
+
/// A missing property equals nothing, and is not treated as unusable.
|
|
349
|
+
fn eval_equals(ctx_val: Option<&Value>, condition_val: &Value) -> Option<bool> {
|
|
350
|
+
Some(match ctx_val {
|
|
351
|
+
Some(ctx_val) => values_equal(ctx_val, condition_val),
|
|
352
|
+
None => false,
|
|
353
|
+
})
|
|
354
|
+
}
|
|
355
|
+
|
|
331
356
|
/// Scalars are compared directly (strings case-insensitively).
|
|
332
357
|
/// Arrays are compared element-wise with matching length.
|
|
333
|
-
fn
|
|
358
|
+
fn values_equal(ctx_val: &Value, condition_val: &Value) -> bool {
|
|
334
359
|
match (ctx_val, condition_val) {
|
|
335
360
|
(Value::String(a), Value::String(b)) => a.to_lowercase() == b.to_lowercase(),
|
|
336
|
-
(Value::Number(a), Value::Number(b)) =>
|
|
337
|
-
// Compare as i64 first, fall back to f64
|
|
338
|
-
if let (Some(ai), Some(bi)) = (a.as_i64(), b.as_i64()) {
|
|
339
|
-
ai == bi
|
|
340
|
-
} else if let (Some(af), Some(bf)) = (a.as_f64(), b.as_f64()) {
|
|
341
|
-
af == bf
|
|
342
|
-
} else {
|
|
343
|
-
false
|
|
344
|
-
}
|
|
345
|
-
}
|
|
361
|
+
(Value::Number(a), Value::Number(b)) => numbers_equal(a, b),
|
|
346
362
|
(Value::Bool(a), Value::Bool(b)) => a == b,
|
|
347
363
|
(Value::Array(a), Value::Array(b)) => {
|
|
348
|
-
a.len() == b.len() && a.iter().zip(b.iter()).all(|(av, bv)|
|
|
364
|
+
a.len() == b.len() && a.iter().zip(b.iter()).all(|(av, bv)| values_equal(av, bv))
|
|
349
365
|
}
|
|
350
366
|
_ => false,
|
|
351
367
|
}
|
|
@@ -399,71 +415,21 @@ fn glob_star_match(pattern: &str, value: &str) -> bool {
|
|
|
399
415
|
/// Check if a string context value matches any pattern in the condition array.
|
|
400
416
|
/// Patterns use star-only glob semantics; comparison is case-insensitive.
|
|
401
417
|
/// Returns false for non-string context values.
|
|
402
|
-
fn eval_matches(ctx_val:
|
|
403
|
-
let
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
enum DebugLogLevel {
|
|
417
|
-
None,
|
|
418
|
-
Parse,
|
|
419
|
-
Match,
|
|
420
|
-
All,
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
static DEBUG_LOG_LEVEL: OnceLock<DebugLogLevel> = OnceLock::new();
|
|
424
|
-
static DEBUG_MATCH_SAMPLE_RATE: OnceLock<u64> = OnceLock::new();
|
|
425
|
-
|
|
426
|
-
fn debug_log_level() -> &'static DebugLogLevel {
|
|
427
|
-
DEBUG_LOG_LEVEL.get_or_init(|| {
|
|
428
|
-
match std::env::var("SENTRY_OPTIONS_FEATURE_DEBUG_LOG")
|
|
429
|
-
.as_deref()
|
|
430
|
-
.unwrap_or("")
|
|
431
|
-
{
|
|
432
|
-
"all" => DebugLogLevel::All,
|
|
433
|
-
"parse" => DebugLogLevel::Parse,
|
|
434
|
-
"match" => DebugLogLevel::Match,
|
|
435
|
-
_ => DebugLogLevel::None,
|
|
436
|
-
}
|
|
437
|
-
})
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
fn debug_match_sample_rate() -> u64 {
|
|
441
|
-
*DEBUG_MATCH_SAMPLE_RATE.get_or_init(|| {
|
|
442
|
-
std::env::var("SENTRY_OPTIONS_FEATURE_DEBUG_LOG_SAMPLE_RATE")
|
|
443
|
-
.ok()
|
|
444
|
-
.and_then(|v| v.parse::<f64>().ok())
|
|
445
|
-
.map(|r| (r.clamp(0.0, 1.0) * 1000.0) as u64)
|
|
446
|
-
.unwrap_or(1000)
|
|
447
|
-
})
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
fn debug_log_parse(msg: &str) {
|
|
451
|
-
match debug_log_level() {
|
|
452
|
-
DebugLogLevel::Parse | DebugLogLevel::All => eprintln!("[sentry-options/parse] {msg}"),
|
|
453
|
-
_ => {}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
fn debug_log_match(feature: &str, result: bool, context_id: u64) {
|
|
458
|
-
match debug_log_level() {
|
|
459
|
-
DebugLogLevel::Match | DebugLogLevel::All
|
|
460
|
-
if context_id % 1000 < debug_match_sample_rate() =>
|
|
461
|
-
{
|
|
462
|
-
eprintln!(
|
|
463
|
-
"[sentry-options/match] feature='{feature}' result={result} context_id={context_id}"
|
|
464
|
-
);
|
|
418
|
+
fn eval_matches(ctx_val: Option<&Value>, condition_val: &Value) -> Option<bool> {
|
|
419
|
+
let arr = condition_val.as_array()?;
|
|
420
|
+
match ctx_val {
|
|
421
|
+
// A container cannot be glob matched against.
|
|
422
|
+
Some(Value::Array(_) | Value::Object(_)) => None,
|
|
423
|
+
None => Some(false),
|
|
424
|
+
Some(scalar) => {
|
|
425
|
+
let Some(s) = scalar.as_str() else {
|
|
426
|
+
return Some(false);
|
|
427
|
+
};
|
|
428
|
+
Some(arr.iter().any(|v| {
|
|
429
|
+
v.as_str()
|
|
430
|
+
.is_some_and(|pattern| glob_star_match(pattern, s))
|
|
431
|
+
}))
|
|
465
432
|
}
|
|
466
|
-
_ => {}
|
|
467
433
|
}
|
|
468
434
|
}
|
|
469
435
|
|
|
@@ -494,24 +460,29 @@ impl FeatureChecker {
|
|
|
494
460
|
let feature_val = match opts.get(&self.namespace, &key) {
|
|
495
461
|
Ok(v) => v,
|
|
496
462
|
Err(e) => {
|
|
497
|
-
|
|
463
|
+
tracing::debug!(key = %key, error = %e, "Failed to get feature");
|
|
498
464
|
return false;
|
|
499
465
|
}
|
|
500
466
|
};
|
|
501
467
|
|
|
502
468
|
let feature = match Feature::from_json(&feature_val) {
|
|
503
469
|
Some(f) => {
|
|
504
|
-
|
|
470
|
+
tracing::debug!(key = %key, "Parsed feature");
|
|
505
471
|
f
|
|
506
472
|
}
|
|
507
473
|
None => {
|
|
508
|
-
|
|
474
|
+
tracing::debug!(key = %key, "Failed to parse feature");
|
|
509
475
|
return false;
|
|
510
476
|
}
|
|
511
477
|
};
|
|
512
478
|
|
|
513
479
|
let result = feature.matches(context);
|
|
514
|
-
|
|
480
|
+
tracing::debug!(
|
|
481
|
+
feature = feature_name,
|
|
482
|
+
result,
|
|
483
|
+
context_id = context.id(),
|
|
484
|
+
"Feature match result"
|
|
485
|
+
);
|
|
515
486
|
result
|
|
516
487
|
}
|
|
517
488
|
}
|
|
@@ -823,6 +794,97 @@ mod tests {
|
|
|
823
794
|
assert!(!check(&opts, "organizations:test-feature", &ctx2));
|
|
824
795
|
}
|
|
825
796
|
|
|
797
|
+
#[test]
|
|
798
|
+
fn test_condition_in_list_property_matches_on_overlap() {
|
|
799
|
+
let cond = r#"{"property": "plan_family", "operator": "in", "value": ["business"]}"#;
|
|
800
|
+
let (opts, _t) = setup_feature_options(&feature_json(true, 100, cond));
|
|
801
|
+
|
|
802
|
+
let mut ctx = FeatureContext::new();
|
|
803
|
+
ctx.insert("plan_family", json!(["enterprise business", "Business"]));
|
|
804
|
+
assert!(check(&opts, "organizations:test-feature", &ctx));
|
|
805
|
+
|
|
806
|
+
let mut ctx2 = FeatureContext::new();
|
|
807
|
+
ctx2.insert("plan_family", json!(["team"]));
|
|
808
|
+
assert!(!check(&opts, "organizations:test-feature", &ctx2));
|
|
809
|
+
|
|
810
|
+
let mut ctx3 = FeatureContext::new();
|
|
811
|
+
ctx3.insert("plan_family", json!([]));
|
|
812
|
+
assert!(!check(&opts, "organizations:test-feature", &ctx3));
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
#[test]
|
|
816
|
+
fn test_condition_not_in_list_property_requires_no_overlap() {
|
|
817
|
+
let cond = r#"{"property": "plan_family", "operator": "not_in", "value": ["business"]}"#;
|
|
818
|
+
let (opts, _t) = setup_feature_options(&feature_json(true, 100, cond));
|
|
819
|
+
|
|
820
|
+
let mut ctx = FeatureContext::new();
|
|
821
|
+
ctx.insert("plan_family", json!(["enterprise business", "business"]));
|
|
822
|
+
assert!(!check(&opts, "organizations:test-feature", &ctx));
|
|
823
|
+
|
|
824
|
+
let mut ctx2 = FeatureContext::new();
|
|
825
|
+
ctx2.insert("plan_family", json!(["team"]));
|
|
826
|
+
assert!(check(&opts, "organizations:test-feature", &ctx2));
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
#[test]
|
|
830
|
+
fn test_condition_in_list_property_no_type_coercion() {
|
|
831
|
+
let cond = r#"{"property": "org_id", "operator": "in", "value": ["123"]}"#;
|
|
832
|
+
let (opts, _t) = setup_feature_options(&feature_json(true, 100, cond));
|
|
833
|
+
|
|
834
|
+
let mut ctx = FeatureContext::new();
|
|
835
|
+
ctx.insert("org_id", json!([123]));
|
|
836
|
+
assert!(!check(&opts, "organizations:test-feature", &ctx));
|
|
837
|
+
|
|
838
|
+
let int_cond = r#"{"property": "org_id", "operator": "in", "value": [123]}"#;
|
|
839
|
+
let (int_opts, _t2) = setup_feature_options(&feature_json(true, 100, int_cond));
|
|
840
|
+
assert!(check(&int_opts, "organizations:test-feature", &ctx));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
#[test]
|
|
844
|
+
fn test_condition_in_matches_across_int_and_float() {
|
|
845
|
+
let cond = r#"{"property": "org_id", "operator": "in", "value": [123.0]}"#;
|
|
846
|
+
let (opts, _t) = setup_feature_options(&feature_json(true, 100, cond));
|
|
847
|
+
|
|
848
|
+
let mut ctx = FeatureContext::new();
|
|
849
|
+
ctx.insert("org_id", json!(123));
|
|
850
|
+
assert!(check(&opts, "organizations:test-feature", &ctx));
|
|
851
|
+
|
|
852
|
+
let mut list_ctx = FeatureContext::new();
|
|
853
|
+
list_ctx.insert("org_id", json!([123]));
|
|
854
|
+
assert!(check(&opts, "organizations:test-feature", &list_ctx));
|
|
855
|
+
|
|
856
|
+
let mut ctx2 = FeatureContext::new();
|
|
857
|
+
ctx2.insert("org_id", json!(124));
|
|
858
|
+
assert!(!check(&opts, "organizations:test-feature", &ctx2));
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
#[test]
|
|
862
|
+
fn test_condition_in_large_ints_compare_exactly() {
|
|
863
|
+
// 2^53 + 1 and 2^53 collide as f64, so ints must not round-trip through it.
|
|
864
|
+
let cond = r#"{"property": "org_id", "operator": "in", "value": [9007199254740993]}"#;
|
|
865
|
+
let (opts, _t) = setup_feature_options(&feature_json(true, 100, cond));
|
|
866
|
+
|
|
867
|
+
let mut ctx = FeatureContext::new();
|
|
868
|
+
ctx.insert("org_id", json!(9007199254740992i64));
|
|
869
|
+
assert!(!check(&opts, "organizations:test-feature", &ctx));
|
|
870
|
+
|
|
871
|
+
let mut ctx2 = FeatureContext::new();
|
|
872
|
+
ctx2.insert("org_id", json!(9007199254740993i64));
|
|
873
|
+
assert!(check(&opts, "organizations:test-feature", &ctx2));
|
|
874
|
+
|
|
875
|
+
// Above i64::MAX, where as_i64() gives up but the values are still exact.
|
|
876
|
+
let big = r#"{"property": "org_id", "operator": "in", "value": [18446744073709551615]}"#;
|
|
877
|
+
let (big_opts, _t2) = setup_feature_options(&feature_json(true, 100, big));
|
|
878
|
+
|
|
879
|
+
let mut ctx3 = FeatureContext::new();
|
|
880
|
+
ctx3.insert("org_id", json!(18446744073709551614u64));
|
|
881
|
+
assert!(!check(&big_opts, "organizations:test-feature", &ctx3));
|
|
882
|
+
|
|
883
|
+
let mut ctx4 = FeatureContext::new();
|
|
884
|
+
ctx4.insert("org_id", json!(18446744073709551615u64));
|
|
885
|
+
assert!(check(&big_opts, "organizations:test-feature", &ctx4));
|
|
886
|
+
}
|
|
887
|
+
|
|
826
888
|
#[test]
|
|
827
889
|
fn test_condition_contains() {
|
|
828
890
|
let cond = r#"{"property": "tags", "operator": "contains", "value": "beta"}"#;
|
|
@@ -837,6 +899,30 @@ mod tests {
|
|
|
837
899
|
assert!(!check(&opts, "organizations:test-feature", &ctx2));
|
|
838
900
|
}
|
|
839
901
|
|
|
902
|
+
#[test]
|
|
903
|
+
fn test_condition_contains_matches_across_int_and_float() {
|
|
904
|
+
let int_cond = r#"{"property": "ids", "operator": "contains", "value": 123}"#;
|
|
905
|
+
let (int_opts, _t) = setup_feature_options(&feature_json(true, 100, int_cond));
|
|
906
|
+
|
|
907
|
+
let float_cond = r#"{"property": "ids", "operator": "contains", "value": 123.0}"#;
|
|
908
|
+
let (float_opts, _t2) = setup_feature_options(&feature_json(true, 100, float_cond));
|
|
909
|
+
|
|
910
|
+
let mut int_ctx = FeatureContext::new();
|
|
911
|
+
int_ctx.insert("ids", json!([123]));
|
|
912
|
+
let mut float_ctx = FeatureContext::new();
|
|
913
|
+
float_ctx.insert("ids", json!([123.0]));
|
|
914
|
+
|
|
915
|
+
// Matches regardless of which side holds the float.
|
|
916
|
+
assert!(check(&int_opts, "organizations:test-feature", &int_ctx));
|
|
917
|
+
assert!(check(&int_opts, "organizations:test-feature", &float_ctx));
|
|
918
|
+
assert!(check(&float_opts, "organizations:test-feature", &int_ctx));
|
|
919
|
+
assert!(check(&float_opts, "organizations:test-feature", &float_ctx));
|
|
920
|
+
|
|
921
|
+
let mut other = FeatureContext::new();
|
|
922
|
+
other.insert("ids", json!([124]));
|
|
923
|
+
assert!(!check(&int_opts, "organizations:test-feature", &other));
|
|
924
|
+
}
|
|
925
|
+
|
|
840
926
|
#[test]
|
|
841
927
|
fn test_condition_equals() {
|
|
842
928
|
let cond = r#"{"property": "plan", "operator": "equals", "value": "enterprise"}"#;
|
|
@@ -851,6 +937,21 @@ mod tests {
|
|
|
851
937
|
assert!(!check(&opts, "organizations:test-feature", &ctx2));
|
|
852
938
|
}
|
|
853
939
|
|
|
940
|
+
#[test]
|
|
941
|
+
fn test_condition_equals_large_ints_compare_exactly() {
|
|
942
|
+
// Above i64::MAX these used to collapse into f64 and compare equal.
|
|
943
|
+
let cond = r#"{"property": "org_id", "operator": "equals", "value": 18446744073709551615}"#;
|
|
944
|
+
let (opts, _t) = setup_feature_options(&feature_json(true, 100, cond));
|
|
945
|
+
|
|
946
|
+
let mut ctx = FeatureContext::new();
|
|
947
|
+
ctx.insert("org_id", json!(18446744073709551614u64));
|
|
948
|
+
assert!(!check(&opts, "organizations:test-feature", &ctx));
|
|
949
|
+
|
|
950
|
+
let mut ctx2 = FeatureContext::new();
|
|
951
|
+
ctx2.insert("org_id", json!(18446744073709551615u64));
|
|
952
|
+
assert!(check(&opts, "organizations:test-feature", &ctx2));
|
|
953
|
+
}
|
|
954
|
+
|
|
854
955
|
#[test]
|
|
855
956
|
fn test_condition_equals_bool() {
|
|
856
957
|
let cond = r#"{"property": "is_free", "operator": "equals", "value": true}"#;
|
|
@@ -1404,4 +1505,76 @@ mod tests {
|
|
|
1404
1505
|
ctx.insert("org_id", json!(123));
|
|
1405
1506
|
assert!(check(&opts, "organizations:test-feature", &ctx));
|
|
1406
1507
|
}
|
|
1508
|
+
|
|
1509
|
+
#[test]
|
|
1510
|
+
fn test_id_string_is_stable() {
|
|
1511
|
+
for (value, expected) in [
|
|
1512
|
+
(json!("sentry"), "sentry"),
|
|
1513
|
+
(json!(123), "123"),
|
|
1514
|
+
(json!(-7), "-7"),
|
|
1515
|
+
(json!(true), "True"),
|
|
1516
|
+
(json!(false), "False"),
|
|
1517
|
+
(json!(null), "None"),
|
|
1518
|
+
(json!(1.0), "1.0"),
|
|
1519
|
+
(json!(1.5), "1.5"),
|
|
1520
|
+
(json!(["a", "b"]), "[a, b]"),
|
|
1521
|
+
(json!([1, 2]), "[1, 2]"),
|
|
1522
|
+
(json!([]), "[]"),
|
|
1523
|
+
] {
|
|
1524
|
+
assert_eq!(value_to_id_string(&value), expected, "for {value}");
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
fn condition_matches(operator: &str, value: &str, ctx: &FeatureContext) -> bool {
|
|
1529
|
+
let json = format!(r#"{{"property": "prop", "operator": "{operator}", "value": {value}}}"#);
|
|
1530
|
+
Condition::from_json(&serde_json::from_str(&json).unwrap())
|
|
1531
|
+
.expect("condition should parse")
|
|
1532
|
+
.matches(ctx)
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
#[test]
|
|
1536
|
+
fn test_missing_property_matches_only_negated_conditions() {
|
|
1537
|
+
let ctx = FeatureContext::new();
|
|
1538
|
+
for (operator, value, expected) in [
|
|
1539
|
+
("in", "[1]", false),
|
|
1540
|
+
("not_in", "[1]", true),
|
|
1541
|
+
("equals", "1", false),
|
|
1542
|
+
("not_equals", "1", true),
|
|
1543
|
+
("matches", r#"["a"]"#, false),
|
|
1544
|
+
("not_matches", r#"["a"]"#, true),
|
|
1545
|
+
// `contains` has no list to search, so neither form matches.
|
|
1546
|
+
("contains", r#""a""#, false),
|
|
1547
|
+
("not_contains", r#""a""#, false),
|
|
1548
|
+
] {
|
|
1549
|
+
assert_eq!(
|
|
1550
|
+
condition_matches(operator, value, &ctx),
|
|
1551
|
+
expected,
|
|
1552
|
+
"{operator}"
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
#[test]
|
|
1558
|
+
fn test_unusable_property_type_matches_neither_form() {
|
|
1559
|
+
let mut map = FeatureContext::new();
|
|
1560
|
+
map.insert("prop", json!({"k": "v"}));
|
|
1561
|
+
let mut scalar = FeatureContext::new();
|
|
1562
|
+
scalar.insert("prop", json!("a"));
|
|
1563
|
+
let mut list = FeatureContext::new();
|
|
1564
|
+
list.insert("prop", json!(["b"]));
|
|
1565
|
+
|
|
1566
|
+
for (operator, value, ctx) in [
|
|
1567
|
+
("in", "[1]", &map),
|
|
1568
|
+
("not_in", "[1]", &map),
|
|
1569
|
+
("contains", r#""a""#, &scalar),
|
|
1570
|
+
("not_contains", r#""a""#, &scalar),
|
|
1571
|
+
("matches", r#"["b"]"#, &list),
|
|
1572
|
+
("not_matches", r#"["b"]"#, &list),
|
|
1573
|
+
] {
|
|
1574
|
+
assert!(!condition_matches(operator, value, ctx), "{operator}");
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// A usable list that lacks the value still matches when negated.
|
|
1578
|
+
assert!(condition_matches("not_contains", r#""a""#, &list));
|
|
1579
|
+
}
|
|
1407
1580
|
}
|
{sentry_options-1.2.2 → sentry_options-1.2.4}/sentry-options-validation/src/feature-schema-defs.json
RENAMED
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
"description": {
|
|
29
29
|
"type": "string",
|
|
30
30
|
"description": "Human-readable description of the feature."
|
|
31
|
+
},
|
|
32
|
+
"experiment_mode": {
|
|
33
|
+
"type": ["string", "null"],
|
|
34
|
+
"enum": ["simple", null],
|
|
35
|
+
"description": "When set, the flag is treated as an experiment with exposure tracking. Does not affect evaluation."
|
|
31
36
|
}
|
|
32
37
|
},
|
|
33
38
|
"required": ["owner", "segments", "created_at"],
|
|
@@ -611,10 +611,11 @@ impl SchemaRegistry {
|
|
|
611
611
|
}
|
|
612
612
|
|
|
613
613
|
for key in &unknown_keys {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
614
|
+
tracing::warn!(
|
|
615
|
+
key = %key,
|
|
616
|
+
namespace = %namespace,
|
|
617
|
+
"Ignoring unknown option. This is expected during deployments when \
|
|
618
|
+
values are updated before schemas.",
|
|
618
619
|
);
|
|
619
620
|
}
|
|
620
621
|
|
|
@@ -693,7 +694,7 @@ impl ValuesStoreBuilder {
|
|
|
693
694
|
pub fn build(self) -> ValidationResult<ValuesStore> {
|
|
694
695
|
let values_dir = &self.values_dir;
|
|
695
696
|
if !should_suppress_missing_dir_errors() && fs::metadata(values_dir).is_err() {
|
|
696
|
-
|
|
697
|
+
tracing::warn!(path = %values_dir.display(), "Values directory does not exist");
|
|
697
698
|
}
|
|
698
699
|
|
|
699
700
|
let baseline = Instant::now();
|
|
@@ -826,10 +827,10 @@ impl ValuesStore {
|
|
|
826
827
|
}
|
|
827
828
|
|
|
828
829
|
fn log_refresh_error(&self, error: &ValidationError) {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
830
|
+
tracing::error!(
|
|
831
|
+
path = %self.values_dir.display(),
|
|
832
|
+
error = %error,
|
|
833
|
+
"Failed to reload values",
|
|
833
834
|
);
|
|
834
835
|
}
|
|
835
836
|
|
|
@@ -845,6 +846,7 @@ impl ValuesStore {
|
|
|
845
846
|
return Ok(false);
|
|
846
847
|
}
|
|
847
848
|
|
|
849
|
+
let reload_start = Instant::now();
|
|
848
850
|
let result = self.registry.load_values_json(&self.values_dir);
|
|
849
851
|
|
|
850
852
|
// Publish the new snapshot before bumping the timestamp. The CAS below
|
|
@@ -856,6 +858,16 @@ impl ValuesStore {
|
|
|
856
858
|
// architectures.
|
|
857
859
|
let new_generated_at = match result {
|
|
858
860
|
Ok((new_values, generated_at)) => {
|
|
861
|
+
let elapsed_ms = reload_start.elapsed().as_millis();
|
|
862
|
+
for namespace in new_values.keys() {
|
|
863
|
+
tracing::info!(
|
|
864
|
+
path = %self.values_dir.display(),
|
|
865
|
+
namespace = %namespace,
|
|
866
|
+
elapsed_ms,
|
|
867
|
+
"Reloaded values",
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
|
|
859
871
|
self.values.store(Arc::new(new_values));
|
|
860
872
|
self.last_mtimes.store(Arc::new(current_mtimes));
|
|
861
873
|
Ok(generated_at)
|
|
@@ -894,10 +906,18 @@ impl ValuesStore {
|
|
|
894
906
|
callback(ns, delay)
|
|
895
907
|
}))
|
|
896
908
|
{
|
|
897
|
-
|
|
909
|
+
tracing::error!(
|
|
910
|
+
namespace = %ns,
|
|
911
|
+
error = ?e,
|
|
912
|
+
"Propagation callback panicked",
|
|
913
|
+
);
|
|
898
914
|
}
|
|
899
915
|
}
|
|
900
|
-
None =>
|
|
916
|
+
None => tracing::warn!(
|
|
917
|
+
namespace = %ns,
|
|
918
|
+
generated_at = %ts,
|
|
919
|
+
"Bad generated_at timestamp",
|
|
920
|
+
),
|
|
901
921
|
}
|
|
902
922
|
}
|
|
903
923
|
}
|
|
@@ -2277,6 +2297,65 @@ Error: \"version\" is a required property"
|
|
|
2277
2297
|
schema.validate_option("feature.organizations:fury-mode", &json!({"segments": []}));
|
|
2278
2298
|
assert!(matches!(result, Err(ValidationError::ValueError { .. })));
|
|
2279
2299
|
}
|
|
2300
|
+
|
|
2301
|
+
#[test]
|
|
2302
|
+
fn test_validate_values_feature_with_experiment_mode() {
|
|
2303
|
+
let temp_dir = TempDir::new().unwrap();
|
|
2304
|
+
create_test_schema(&temp_dir, "test", FEATURE_SCHEMA);
|
|
2305
|
+
let registry = SchemaRegistry::from_directory(temp_dir.path()).unwrap();
|
|
2306
|
+
|
|
2307
|
+
let result = registry.validate_values(
|
|
2308
|
+
"test",
|
|
2309
|
+
&json!({
|
|
2310
|
+
"feature.organizations:fury-mode": {
|
|
2311
|
+
"owner": {"team": "hybrid-cloud"},
|
|
2312
|
+
"segments": [],
|
|
2313
|
+
"created_at": "2024-01-01",
|
|
2314
|
+
"experiment_mode": "simple"
|
|
2315
|
+
}
|
|
2316
|
+
}),
|
|
2317
|
+
);
|
|
2318
|
+
assert!(result.is_ok());
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
#[test]
|
|
2322
|
+
fn test_validate_values_feature_with_invalid_experiment_mode_fails() {
|
|
2323
|
+
let temp_dir = TempDir::new().unwrap();
|
|
2324
|
+
create_test_schema(&temp_dir, "test", FEATURE_SCHEMA);
|
|
2325
|
+
let registry = SchemaRegistry::from_directory(temp_dir.path()).unwrap();
|
|
2326
|
+
|
|
2327
|
+
let result = registry.validate_values(
|
|
2328
|
+
"test",
|
|
2329
|
+
&json!({
|
|
2330
|
+
"feature.organizations:fury-mode": {
|
|
2331
|
+
"owner": {"team": "hybrid-cloud"},
|
|
2332
|
+
"segments": [],
|
|
2333
|
+
"created_at": "2024-01-01",
|
|
2334
|
+
"experiment_mode": "not-a-real-mode"
|
|
2335
|
+
}
|
|
2336
|
+
}),
|
|
2337
|
+
);
|
|
2338
|
+
assert!(matches!(result, Err(ValidationError::ValueError { .. })));
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
#[test]
|
|
2342
|
+
fn test_validate_values_feature_without_experiment_mode_still_valid() {
|
|
2343
|
+
let temp_dir = TempDir::new().unwrap();
|
|
2344
|
+
create_test_schema(&temp_dir, "test", FEATURE_SCHEMA);
|
|
2345
|
+
let registry = SchemaRegistry::from_directory(temp_dir.path()).unwrap();
|
|
2346
|
+
|
|
2347
|
+
let result = registry.validate_values(
|
|
2348
|
+
"test",
|
|
2349
|
+
&json!({
|
|
2350
|
+
"feature.organizations:fury-mode": {
|
|
2351
|
+
"owner": {"team": "hybrid-cloud"},
|
|
2352
|
+
"segments": [],
|
|
2353
|
+
"created_at": "2024-01-01"
|
|
2354
|
+
}
|
|
2355
|
+
}),
|
|
2356
|
+
);
|
|
2357
|
+
assert!(result.is_ok());
|
|
2358
|
+
}
|
|
2280
2359
|
}
|
|
2281
2360
|
|
|
2282
2361
|
mod store_tests {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sentry_options-1.2.2 → sentry_options-1.2.4}/sentry-options-validation/src/namespace-schema.json
RENAMED
|
File without changes
|