fix(openfeature): accept empty string as valid targeting key#8384
fix(openfeature): accept empty string as valid targeting key#8384leoromanovsky wants to merge 6 commits intomasterfrom
Conversation
- Change GetShard() from IsNullOrEmpty to null-only check so empty string hashes normally - Change ResolveAttribute() from IsNullOrEmpty to null-only check for id fallback - Make EvaluationContext constructor and TargetingKey property nullable (string?)
…evaluator - Add 4 new tests: null+static OK, null+sharded error, null+rule OK, empty+sharded OK - Add CreateStaticFlag helper for flags without shards - Update existing test to use null instead of empty string for missing targeting key
…eason fix not yet on main
BenchmarksBenchmark execution time: 2026-03-30 17:02:40 Comparing candidate commit 926cada in PR branch Found 7 performance improvements and 6 performance regressions! Performance is the same for 259 metrics, 16 unstable metrics.
|
…rcing to empty string
dd-oleksii
left a comment
There was a problem hiding this comment.
oh, I though I caught this during initial review of .NET implementation but it looks like I missed defaulting of targeting key 🙃
Per review feedback from dd-oleksii: when targeting key is null,
ResolveAttribute("id") should return null like any other missing
attribute, not throw MissingTargetingKeyException. The shard path
in GetShard() still correctly throws when targeting key is needed.
Execution-Time Benchmarks Report ⏱️Execution-time results for samples comparing This PR (8384) and master.
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Metric | Master (Mean ± 95% CI) | Current (Mean ± 95% CI) | Change | Status |
|---|---|---|---|---|
| .NET Framework 4.8 - Baseline | ||||
| duration | 193.20 ± (193.07 - 194.03) ms | 212.48 ± (211.91 - 212.87) ms | +10.0% | ❌⬆️ |
| .NET Framework 4.8 - Bailout | ||||
| duration | 197.04 ± (197.25 - 198.32) ms | 217.18 ± (216.52 - 217.45) ms | +10.2% | ❌⬆️ |
| .NET Framework 4.8 - CallTarget+Inlining+NGEN | ||||
| duration | 1165.14 ± (1172.42 - 1183.27) ms | 1243.38 ± (1242.29 - 1254.85) ms | +6.7% | ❌⬆️ |
Full Metrics Comparison
FakeDbCommand
| Metric | Master (Mean ± 95% CI) | Current (Mean ± 95% CI) | Change | Status |
|---|---|---|---|---|
| .NET Framework 4.8 - Baseline | ||||
| duration | 71.43 ± (71.50 - 71.78) ms | 71.54 ± (71.51 - 71.88) ms | +0.2% | ✅⬆️ |
| .NET Framework 4.8 - Bailout | ||||
| duration | 76.02 ± (75.97 - 76.33) ms | 75.80 ± (75.86 - 76.24) ms | -0.3% | ✅ |
| .NET Framework 4.8 - CallTarget+Inlining+NGEN | ||||
| duration | 1071.89 ± (1072.00 - 1078.73) ms | 1076.71 ± (1078.47 - 1086.85) ms | +0.4% | ✅⬆️ |
| .NET Core 3.1 - Baseline | ||||
| process.internal_duration_ms | 22.35 ± (22.30 - 22.40) ms | 22.14 ± (22.10 - 22.17) ms | -1.0% | ✅ |
| process.time_to_main_ms | 83.67 ± (83.50 - 83.84) ms | 83.31 ± (83.10 - 83.52) ms | -0.4% | ✅ |
| runtime.dotnet.exceptions.count | 0 ± (0 - 0) | 0 ± (0 - 0) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 10.90 ± (10.90 - 10.91) MB | 10.90 ± (10.90 - 10.91) MB | +0.0% | ✅⬆️ |
| runtime.dotnet.threads.count | 12 ± (12 - 12) | 12 ± (12 - 12) | +0.0% | ✅ |
| .NET Core 3.1 - Bailout | ||||
| process.internal_duration_ms | 22.22 ± (22.18 - 22.25) ms | 22.17 ± (22.14 - 22.21) ms | -0.2% | ✅ |
| process.time_to_main_ms | 85.11 ± (84.95 - 85.28) ms | 85.40 ± (85.20 - 85.60) ms | +0.3% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 0 ± (0 - 0) | 0 ± (0 - 0) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 10.95 ± (10.94 - 10.95) MB | 10.92 ± (10.92 - 10.93) MB | -0.2% | ✅ |
| runtime.dotnet.threads.count | 13 ± (13 - 13) | 13 ± (13 - 13) | +0.0% | ✅ |
| .NET Core 3.1 - CallTarget+Inlining+NGEN | ||||
| process.internal_duration_ms | 223.16 ± (221.99 - 224.32) ms | 224.59 ± (223.36 - 225.82) ms | +0.6% | ✅⬆️ |
| process.time_to_main_ms | 532.05 ± (530.99 - 533.11) ms | 535.43 ± (534.20 - 536.66) ms | +0.6% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 0 ± (0 - 0) | 0 ± (0 - 0) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 48.24 ± (48.20 - 48.27) MB | 48.21 ± (48.17 - 48.25) MB | -0.1% | ✅ |
| runtime.dotnet.threads.count | 28 ± (28 - 28) | 28 ± (28 - 28) | -0.0% | ✅ |
| .NET 6 - Baseline | ||||
| process.internal_duration_ms | 20.94 ± (20.91 - 20.97) ms | 20.91 ± (20.87 - 20.94) ms | -0.1% | ✅ |
| process.time_to_main_ms | 71.92 ± (71.77 - 72.08) ms | 72.11 ± (71.95 - 72.27) ms | +0.3% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 0 ± (0 - 0) | 0 ± (0 - 0) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 10.62 ± (10.62 - 10.62) MB | 10.61 ± (10.61 - 10.62) MB | -0.1% | ✅ |
| runtime.dotnet.threads.count | 10 ± (10 - 10) | 10 ± (10 - 10) | +0.0% | ✅ |
| .NET 6 - Bailout | ||||
| process.internal_duration_ms | 20.92 ± (20.89 - 20.96) ms | 20.94 ± (20.91 - 20.96) ms | +0.1% | ✅⬆️ |
| process.time_to_main_ms | 73.13 ± (72.97 - 73.29) ms | 73.45 ± (73.27 - 73.62) ms | +0.4% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 0 ± (0 - 0) | 0 ± (0 - 0) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 10.66 ± (10.66 - 10.67) MB | 10.73 ± (10.72 - 10.73) MB | +0.6% | ✅⬆️ |
| runtime.dotnet.threads.count | 11 ± (11 - 11) | 11 ± (11 - 11) | +0.0% | ✅ |
| .NET 6 - CallTarget+Inlining+NGEN | ||||
| process.internal_duration_ms | 387.20 ± (384.70 - 389.69) ms | 389.35 ± (386.99 - 391.71) ms | +0.6% | ✅⬆️ |
| process.time_to_main_ms | 530.58 ± (529.61 - 531.55) ms | 530.54 ± (529.53 - 531.54) ms | -0.0% | ✅ |
| runtime.dotnet.exceptions.count | 0 ± (0 - 0) | 0 ± (0 - 0) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 50.23 ± (50.21 - 50.26) MB | 50.33 ± (50.31 - 50.36) MB | +0.2% | ✅⬆️ |
| runtime.dotnet.threads.count | 28 ± (28 - 28) | 28 ± (28 - 28) | -0.3% | ✅ |
| .NET 8 - Baseline | ||||
| process.internal_duration_ms | 19.16 ± (19.12 - 19.19) ms | 19.20 ± (19.16 - 19.23) ms | +0.2% | ✅⬆️ |
| process.time_to_main_ms | 71.28 ± (71.13 - 71.43) ms | 71.21 ± (71.06 - 71.35) ms | -0.1% | ✅ |
| runtime.dotnet.exceptions.count | 0 ± (0 - 0) | 0 ± (0 - 0) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 7.69 ± (7.68 - 7.69) MB | 7.67 ± (7.67 - 7.68) MB | -0.2% | ✅ |
| runtime.dotnet.threads.count | 10 ± (10 - 10) | 10 ± (10 - 10) | +0.0% | ✅ |
| .NET 8 - Bailout | ||||
| process.internal_duration_ms | 19.25 ± (19.21 - 19.28) ms | 19.29 ± (19.25 - 19.32) ms | +0.2% | ✅⬆️ |
| process.time_to_main_ms | 72.88 ± (72.74 - 73.02) ms | 72.86 ± (72.69 - 73.03) ms | -0.0% | ✅ |
| runtime.dotnet.exceptions.count | 0 ± (0 - 0) | 0 ± (0 - 0) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 7.75 ± (7.74 - 7.75) MB | 7.71 ± (7.71 - 7.72) MB | -0.4% | ✅ |
| runtime.dotnet.threads.count | 11 ± (11 - 11) | 11 ± (11 - 11) | +0.0% | ✅ |
| .NET 8 - CallTarget+Inlining+NGEN | ||||
| process.internal_duration_ms | 308.00 ± (305.65 - 310.34) ms | 305.43 ± (303.23 - 307.63) ms | -0.8% | ✅ |
| process.time_to_main_ms | 490.98 ± (490.24 - 491.72) ms | 491.49 ± (490.72 - 492.26) ms | +0.1% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 0 ± (0 - 0) | 0 ± (0 - 0) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 37.26 ± (37.24 - 37.28) MB | 37.24 ± (37.23 - 37.26) MB | -0.0% | ✅ |
| runtime.dotnet.threads.count | 27 ± (27 - 27) | 27 ± (27 - 27) | -0.1% | ✅ |
HttpMessageHandler
| Metric | Master (Mean ± 95% CI) | Current (Mean ± 95% CI) | Change | Status |
|---|---|---|---|---|
| .NET Framework 4.8 - Baseline | ||||
| duration | 193.20 ± (193.07 - 194.03) ms | 212.48 ± (211.91 - 212.87) ms | +10.0% | ❌⬆️ |
| .NET Framework 4.8 - Bailout | ||||
| duration | 197.04 ± (197.25 - 198.32) ms | 217.18 ± (216.52 - 217.45) ms | +10.2% | ❌⬆️ |
| .NET Framework 4.8 - CallTarget+Inlining+NGEN | ||||
| duration | 1165.14 ± (1172.42 - 1183.27) ms | 1243.38 ± (1242.29 - 1254.85) ms | +6.7% | ❌⬆️ |
| .NET Core 3.1 - Baseline | ||||
| process.internal_duration_ms | 187.87 ± (187.50 - 188.24) ms | 206.40 ± (205.89 - 206.91) ms | +9.9% | ✅⬆️ |
| process.time_to_main_ms | 81.24 ± (80.95 - 81.54) ms | 90.44 ± (90.16 - 90.71) ms | +11.3% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 3 ± (3 - 3) | 3 ± (3 - 3) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 16.11 ± (16.07 - 16.14) MB | 15.92 ± (15.90 - 15.94) MB | -1.2% | ✅ |
| runtime.dotnet.threads.count | 20 ± (19 - 20) | 20 ± (20 - 20) | +2.1% | ✅⬆️ |
| .NET Core 3.1 - Bailout | ||||
| process.internal_duration_ms | 186.53 ± (186.24 - 186.82) ms | 206.66 ± (206.17 - 207.14) ms | +10.8% | ✅⬆️ |
| process.time_to_main_ms | 81.95 ± (81.79 - 82.12) ms | 91.50 ± (91.20 - 91.79) ms | +11.6% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 3 ± (3 - 3) | 3 ± (3 - 3) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 16.20 ± (16.17 - 16.24) MB | 16.03 ± (16.01 - 16.05) MB | -1.1% | ✅ |
| runtime.dotnet.threads.count | 21 ± (21 - 21) | 21 ± (21 - 21) | +1.3% | ✅⬆️ |
| .NET Core 3.1 - CallTarget+Inlining+NGEN | ||||
| process.internal_duration_ms | 395.41 ± (394.15 - 396.68) ms | 419.85 ± (418.46 - 421.23) ms | +6.2% | ✅⬆️ |
| process.time_to_main_ms | 525.23 ± (524.34 - 526.13) ms | 568.52 ± (567.11 - 569.92) ms | +8.2% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 3 ± (3 - 3) | 3 ± (3 - 3) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 59.05 ± (58.94 - 59.17) MB | 59.12 ± (59.07 - 59.17) MB | +0.1% | ✅⬆️ |
| runtime.dotnet.threads.count | 30 ± (30 - 30) | 30 ± (30 - 30) | +0.2% | ✅⬆️ |
| .NET 6 - Baseline | ||||
| process.internal_duration_ms | 192.21 ± (191.88 - 192.55) ms | 213.48 ± (212.98 - 213.98) ms | +11.1% | ✅⬆️ |
| process.time_to_main_ms | 70.09 ± (69.91 - 70.26) ms | 78.91 ± (78.66 - 79.17) ms | +12.6% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 4 ± (4 - 4) | 4 ± (4 - 4) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 16.17 ± (16.03 - 16.31) MB | 16.18 ± (16.16 - 16.20) MB | +0.1% | ✅⬆️ |
| runtime.dotnet.threads.count | 19 ± (19 - 19) | 20 ± (19 - 20) | +3.4% | ✅⬆️ |
| .NET 6 - Bailout | ||||
| process.internal_duration_ms | 192.26 ± (191.93 - 192.59) ms | 213.23 ± (212.80 - 213.66) ms | +10.9% | ✅⬆️ |
| process.time_to_main_ms | 71.05 ± (70.94 - 71.15) ms | 80.49 ± (80.23 - 80.74) ms | +13.3% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 4 ± (4 - 4) | 4 ± (4 - 4) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 16.22 ± (16.09 - 16.35) MB | 16.25 ± (16.23 - 16.28) MB | +0.2% | ✅⬆️ |
| runtime.dotnet.threads.count | 19 ± (19 - 20) | 20 ± (20 - 21) | +6.2% | ✅⬆️ |
| .NET 6 - CallTarget+Inlining+NGEN | ||||
| process.internal_duration_ms | 600.34 ± (597.60 - 603.08) ms | 600.28 ± (597.43 - 603.13) ms | -0.0% | ✅ |
| process.time_to_main_ms | 523.80 ± (522.95 - 524.65) ms | 572.51 ± (571.29 - 573.73) ms | +9.3% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 4 ± (4 - 4) | 4 ± (4 - 4) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 62.08 ± (62.00 - 62.17) MB | 61.57 ± (61.47 - 61.67) MB | -0.8% | ✅ |
| runtime.dotnet.threads.count | 30 ± (30 - 30) | 31 ± (31 - 31) | +1.4% | ✅⬆️ |
| .NET 8 - Baseline | ||||
| process.internal_duration_ms | 190.16 ± (189.86 - 190.46) ms | 211.70 ± (211.24 - 212.16) ms | +11.3% | ✅⬆️ |
| process.time_to_main_ms | 69.51 ± (69.33 - 69.68) ms | 78.20 ± (77.94 - 78.46) ms | +12.5% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 4 ± (4 - 4) | 4 ± (4 - 4) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 11.84 ± (11.81 - 11.88) MB | 11.60 ± (11.58 - 11.62) MB | -2.0% | ✅ |
| runtime.dotnet.threads.count | 18 ± (18 - 18) | 19 ± (19 - 19) | +4.2% | ✅⬆️ |
| .NET 8 - Bailout | ||||
| process.internal_duration_ms | 190.07 ± (189.67 - 190.47) ms | 211.36 ± (210.93 - 211.79) ms | +11.2% | ✅⬆️ |
| process.time_to_main_ms | 70.62 ± (70.48 - 70.76) ms | 79.61 ± (79.37 - 79.85) ms | +12.7% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 4 ± (4 - 4) | 4 ± (4 - 4) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 11.88 ± (11.85 - 11.90) MB | 11.70 ± (11.69 - 11.72) MB | -1.5% | ✅ |
| runtime.dotnet.threads.count | 19 ± (19 - 19) | 20 ± (20 - 20) | +4.7% | ✅⬆️ |
| .NET 8 - CallTarget+Inlining+NGEN | ||||
| process.internal_duration_ms | 519.62 ± (517.02 - 522.21) ms | 569.42 ± (561.41 - 577.43) ms | +9.6% | ✅⬆️ |
| process.time_to_main_ms | 480.70 ± (480.02 - 481.38) ms | 530.04 ± (528.93 - 531.15) ms | +10.3% | ✅⬆️ |
| runtime.dotnet.exceptions.count | 4 ± (4 - 4) | 4 ± (4 - 4) | +0.0% | ✅ |
| runtime.dotnet.mem.committed | 50.96 ± (50.93 - 50.99) MB | 51.25 ± (51.16 - 51.34) MB | +0.6% | ✅⬆️ |
| runtime.dotnet.threads.count | 30 ± (30 - 30) | 30 ± (30 - 30) | -0.1% | ✅ |
Comparison explanation
Execution-time benchmarks measure the whole time it takes to execute a program, and are intended to measure the one-off costs. Cases where the execution time results for the PR are worse than latest master results are highlighted in **red**. The following thresholds were used for comparing the execution times:
- Welch test with statistical test for significance of 5%
- Only results indicating a difference greater than 5% and 5 ms are considered.
Note that these results are based on a single point-in-time result for each branch. For full results, see the dashboard.
Graphs show the p99 interval based on the mean and StdDev of the test run, as well as the mean value of the run (shown as a diamond below the graph).
Duration charts
FakeDbCommand (.NET Framework 4.8)
gantt
title Execution time (ms) FakeDbCommand (.NET Framework 4.8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8384) - mean (72ms) : 69, 74
master - mean (72ms) : 70, 74
section Bailout
This PR (8384) - mean (76ms) : 74, 78
master - mean (76ms) : 74, 78
section CallTarget+Inlining+NGEN
This PR (8384) - mean (1,083ms) : 1022, 1143
master - mean (1,075ms) : 1027, 1124
FakeDbCommand (.NET Core 3.1)
gantt
title Execution time (ms) FakeDbCommand (.NET Core 3.1)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8384) - mean (112ms) : 107, 117
master - mean (113ms) : 110, 116
section Bailout
This PR (8384) - mean (114ms) : 111, 117
master - mean (114ms) : 112, 116
section CallTarget+Inlining+NGEN
This PR (8384) - mean (797ms) : 779, 815
master - mean (792ms) : 771, 814
FakeDbCommand (.NET 6)
gantt
title Execution time (ms) FakeDbCommand (.NET 6)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8384) - mean (99ms) : 96, 103
master - mean (99ms) : 95, 103
section Bailout
This PR (8384) - mean (100ms) : 97, 104
master - mean (100ms) : 98, 103
section CallTarget+Inlining+NGEN
This PR (8384) - mean (947ms) : 908, 986
master - mean (946ms) : 910, 982
FakeDbCommand (.NET 8)
gantt
title Execution time (ms) FakeDbCommand (.NET 8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8384) - mean (98ms) : 95, 101
master - mean (98ms) : 95, 101
section Bailout
This PR (8384) - mean (100ms) : 96, 104
master - mean (100ms) : 98, 102
section CallTarget+Inlining+NGEN
This PR (8384) - mean (828ms) : 796, 861
master - mean (829ms) : 793, 865
HttpMessageHandler (.NET Framework 4.8)
gantt
title Execution time (ms) HttpMessageHandler (.NET Framework 4.8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8384) - mean (212ms) : 207, 218
master - mean (194ms) : 189, 198
section Bailout
This PR (8384) - mean (217ms) : crit, 212, 221
master - mean (198ms) : 192, 203
section CallTarget+Inlining+NGEN
This PR (8384) - mean (1,249ms) : crit, 1181, 1316
master - mean (1,178ms) : 1095, 1261
HttpMessageHandler (.NET Core 3.1)
gantt
title Execution time (ms) HttpMessageHandler (.NET Core 3.1)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8384) - mean (306ms) : 298, 315
master - mean (277ms) : 272, 283
section Bailout
This PR (8384) - mean (308ms) : crit, 298, 317
master - mean (277ms) : 273, 280
section CallTarget+Inlining+NGEN
This PR (8384) - mean (1,030ms) : crit, 998, 1061
master - mean (953ms) : 929, 977
HttpMessageHandler (.NET 6)
gantt
title Execution time (ms) HttpMessageHandler (.NET 6)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8384) - mean (302ms) : 292, 311
master - mean (271ms) : 266, 275
section Bailout
This PR (8384) - mean (302ms) : crit, 296, 308
master - mean (271ms) : 268, 275
section CallTarget+Inlining+NGEN
This PR (8384) - mean (1,205ms) : 1171, 1238
master - mean (1,155ms) : 1111, 1199
HttpMessageHandler (.NET 8)
gantt
title Execution time (ms) HttpMessageHandler (.NET 8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8384) - mean (301ms) : 291, 310
master - mean (270ms) : 265, 274
section Bailout
This PR (8384) - mean (302ms) : crit, 293, 311
master - mean (270ms) : 265, 275
section CallTarget+Inlining+NGEN
This PR (8384) - mean (1,135ms) : crit, 1001, 1270
master - mean (1,034ms) : 988, 1080
Motivation
The OpenFeature spec (Requirement 3.1.1) defines targeting key as optional. The correct behavior (matching libdatadog's reference implementation) is:
""empty string"", returns valueid="", returns valuenull/missingTARGETING_KEY_MISSINGerror, returns defaultidfallback), returns value or falls throughThe .NET evaluator uses
StringUtil.IsNullOrEmpty()inGetShard()andResolveAttribute(), which rejects bothnulland""— making empty string behave like null. This is wrong:""is a valid targeting key that should be hashed normally.Changes
FeatureFlagsEvaluator.cs: ChangedStringUtil.IsNullOrEmpty(targetingKey)totargetingKey is nullinGetShard()(line ~384) andResolveAttribute()(line ~425)EvaluationContext.cs: MadeTargetingKeyproperty nullable (string?) to properly represent missing vs emptyEvaluateWithMissingTargetingKeyReturnsTargetingKeyMissingtest to usenullinstead ofstring.EmptyCreateStaticFlaghelper for flags with no shardsDecisions
StringUtil.IsNullOrEmptyitself — it's used in 32+ files. Only changed the call sites in the evaluator.""now hashes normally via$"{salt}-{targetingKey}"producing a deterministic shard assignmentnulltargeting key still returnsTARGETING_KEY_MISSINGwhen shards are present (correct behavior per libdatadog reference)nulltargeting key with static flags (no shards) evaluates successfully