Skip to content

Commit 6f7d2c8

Browse files
[FSSDK-11544] holdout parsing
1 parent 73d261a commit 6f7d2c8

File tree

8 files changed

+260
-0
lines changed

8 files changed

+260
-0
lines changed

OptimizelySDK.Net35/OptimizelySDK.Net35.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,9 @@
224224
<Compile Include="..\OptimizelySDK\Utils\ExperimentUtils.cs">
225225
<Link>Bucketing\ExperimentUtils</Link>
226226
</Compile>
227+
<Compile Include="..\OptimizelySDK\Utils\HoldoutConfig.cs">
228+
<Link>Utils\HoldoutConfig.cs</Link>
229+
</Compile>
227230
<Compile Include="..\OptimizelySDK\Bucketing\UserProfileUtil.cs">
228231
<Link>Bucketing\UserProfileUtil</Link>
229232
</Compile>

OptimizelySDK.Net40/OptimizelySDK.Net40.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@
223223
<Compile Include="..\OptimizelySDK\Utils\ExperimentUtils.cs">
224224
<Link>Bucketing\ExperimentUtils</Link>
225225
</Compile>
226+
<Compile Include="..\OptimizelySDK\Utils\HoldoutConfig.cs">
227+
<Link>Utils\HoldoutConfig.cs</Link>
228+
</Compile>
226229
<Compile Include="..\OptimizelySDK\Bucketing\UserProfileUtil.cs">
227230
<Link>Bucketing\UserProfileUtil</Link>
228231
</Compile>

OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
<Compile Include="..\OptimizelySDK\Utils\ControlAttributes.cs" />
6868
<Compile Include="..\OptimizelySDK\Utils\ExceptionExtensions.cs" />
6969
<Compile Include="..\OptimizelySDK\Utils\ExperimentUtils.cs" />
70+
<Compile Include="..\OptimizelySDK\Utils\HoldoutConfig.cs" />
7071
<Compile Include="..\OptimizelySDK\Utils\ConditionParser.cs" />
7172
<Compile Include="..\OptimizelySDK\Utils\AttributeMatchTypes.cs" />
7273
<Compile Include="..\OptimizelySDK\Utils\DecisionInfoTypes.cs" />

OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,9 @@
340340
<Compile Include="..\OptimizelySDK\Utils\ExperimentUtils.cs">
341341
<Link>Utils\ExperimentUtils.cs</Link>
342342
</Compile>
343+
<Compile Include="..\OptimizelySDK\Utils\HoldoutConfig.cs">
344+
<Link>Utils\HoldoutConfig.cs</Link>
345+
</Compile>
343346
<Compile Include="..\OptimizelySDK\Utils\Schema.cs">
344347
<Link>Utils\Schema.cs</Link>
345348
</Compile>

OptimizelySDK/Config/DatafileProjectConfig.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,13 @@ private Dictionary<string, Dictionary<string, Variation>> _VariationIdMap
216216

217217
public Dictionary<string, Rollout> RolloutIdMap => _RolloutIdMap;
218218

219+
/// <summary>
220+
/// Associative array of Holdout ID to Holdout(s) in the datafile
221+
/// </summary>
222+
private Dictionary<string, Holdout> _HoldoutIdMap;
223+
224+
public Dictionary<string, Holdout> HoldoutIdMap => _HoldoutIdMap;
225+
219226
/// <summary>
220227
/// Associative array of experiment IDs that exist in any feature
221228
/// for checking that experiment is a feature experiment.
@@ -232,6 +239,11 @@ private Dictionary<string, Dictionary<string, Variation>> _VariationIdMap
232239
public Dictionary<string, Dictionary<string, Variation>> FlagVariationMap =>
233240
_FlagVariationMap;
234241

242+
/// <summary>
243+
/// Holdout configuration manager for flag-to-holdout relationships.
244+
/// </summary>
245+
private HoldoutConfig _holdoutConfig;
246+
235247
//========================= Interfaces ===========================
236248

237249
/// <summary>
@@ -286,6 +298,11 @@ private Dictionary<string, Dictionary<string, Variation>> _VariationIdMap
286298
/// </summary>
287299
public Rollout[] Rollouts { get; set; }
288300

301+
/// <summary>
302+
/// Associative list of Holdouts.
303+
/// </summary>
304+
public Holdout[] Holdouts { get; set; }
305+
289306
/// <summary>
290307
/// Associative list of Integrations.
291308
/// </summary>
@@ -309,6 +326,7 @@ private void Initialize()
309326
TypedAudiences = TypedAudiences ?? new Audience[0];
310327
FeatureFlags = FeatureFlags ?? new FeatureFlag[0];
311328
Rollouts = Rollouts ?? new Rollout[0];
329+
Holdouts = Holdouts ?? new Holdout[0];
312330
Integrations = Integrations ?? new Integration[0];
313331
_ExperimentKeyMap = new Dictionary<string, Experiment>();
314332

@@ -327,6 +345,8 @@ private void Initialize()
327345
f => f.Key, true);
328346
_RolloutIdMap = ConfigParser<Rollout>.GenerateMap(Rollouts,
329347
r => r.Id.ToString(), true);
348+
_HoldoutIdMap = ConfigParser<Holdout>.GenerateMap(Holdouts,
349+
h => h.Id, true);
330350

331351
// Overwrite similar items in audience id map with typed audience id map.
332352
var typedAudienceIdMap = ConfigParser<Audience>.GenerateMap(TypedAudiences,
@@ -450,6 +470,9 @@ private void Initialize()
450470
}
451471

452472
_FlagVariationMap = flagToVariationsMap;
473+
474+
// Initialize HoldoutConfig for managing flag-to-holdout relationships
475+
_holdoutConfig = new HoldoutConfig(Holdouts ?? new Holdout[0]);
453476
}
454477

455478
/// <summary>
@@ -773,6 +796,34 @@ public Rollout GetRolloutFromId(string rolloutId)
773796
return new Rollout();
774797
}
775798

799+
/// <summary>
800+
/// Get the holdout from the ID
801+
/// </summary>
802+
/// <param name="holdoutId">ID for holdout</param>
803+
/// <returns>Holdout Entity corresponding to the holdout ID or a dummy entity if ID is invalid</returns>
804+
public Holdout GetHoldout(string holdoutId)
805+
{
806+
#if NET35 || NET40
807+
if (string.IsNullOrEmpty(holdoutId) || string.IsNullOrEmpty(holdoutId.Trim()))
808+
#else
809+
if (string.IsNullOrWhiteSpace(holdoutId))
810+
#endif
811+
{
812+
return new Holdout();
813+
}
814+
815+
if (_HoldoutIdMap.ContainsKey(holdoutId))
816+
{
817+
return _HoldoutIdMap[holdoutId];
818+
}
819+
820+
var message = $@"Holdout ID ""{holdoutId}"" is not in datafile.";
821+
Logger.Log(LogLevel.ERROR, message);
822+
ErrorHandler.HandleError(
823+
new InvalidExperimentException("Provided holdout is not in datafile."));
824+
return new Holdout();
825+
}
826+
776827
/// <summary>
777828
/// Get attribute ID for the provided attribute key
778829
/// </summary>
@@ -832,5 +883,16 @@ public string ToDatafile()
832883
{
833884
return _datafile;
834885
}
886+
887+
/// <summary>
888+
/// Get holdout instances associated with the given feature flag key.
889+
/// </summary>
890+
/// <param name="flagKey">Feature flag key</param>
891+
/// <returns>Array of holdouts associated with the flag, empty array if none</returns>
892+
public Holdout[] GetHoldoutsForFlag(string flagKey)
893+
{
894+
var holdouts = _holdoutConfig?.GetHoldoutsForFlag(flagKey);
895+
return holdouts?.ToArray() ?? new Holdout[0];
896+
}
835897
}
836898
}

OptimizelySDK/OptimizelySDK.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@
174174
<Compile Include="Utils\EventTagUtils.cs"/>
175175
<Compile Include="Bucketing\UserProfileUtil.cs"/>
176176
<Compile Include="Utils\ExperimentUtils.cs"/>
177+
<Compile Include="Utils\HoldoutConfig.cs"/>
177178
<Compile Include="Utils\ControlAttributes.cs"/>
178179
<Compile Include="Utils\ExceptionExtensions.cs"/>
179180
<Compile Include="Utils\DateTimeUtils.cs"/>

OptimizelySDK/ProjectConfig.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ public interface ProjectConfig
128128
/// </summary>
129129
Dictionary<string, Rollout> RolloutIdMap { get; }
130130

131+
/// <summary>
132+
/// Associative array of Holdout ID to Holdout(s) in the datafile
133+
/// </summary>
134+
Dictionary<string, Holdout> HoldoutIdMap { get; }
135+
131136
/// <summary>
132137
/// Associative dictionary of Flag to Variation key and Variation in the datafile
133138
/// </summary>
@@ -175,6 +180,11 @@ public interface ProjectConfig
175180
/// </summary>
176181
Rollout[] Rollouts { get; set; }
177182

183+
/// <summary>
184+
/// Associative list of Holdouts.
185+
/// </summary>
186+
Holdout[] Holdouts { get; set; }
187+
178188
/// <summary>
179189
/// Associative list of Integrations.
180190
/// </summary>
@@ -308,6 +318,20 @@ public interface ProjectConfig
308318
/// <returns>List| Feature flag ids list, null otherwise</returns>
309319
List<string> GetExperimentFeatureList(string experimentId);
310320

321+
/// <summary>
322+
/// Get the holdout from the ID
323+
/// </summary>
324+
/// <param name="holdoutId">ID for holdout</param>
325+
/// <returns>Holdout Entity corresponding to the holdout ID or a dummy entity if ID is invalid</returns>
326+
Holdout GetHoldout(string holdoutId);
327+
328+
/// <summary>
329+
/// Get holdout instances associated with the given feature flag key.
330+
/// </summary>
331+
/// <param name="flagKey">Feature flag key</param>
332+
/// <returns>Array of holdouts associated with the flag, empty array if none</returns>
333+
Holdout[] GetHoldoutsForFlag(string flagKey);
334+
311335
/// <summary>
312336
/// Returns the datafile corresponding to ProjectConfig
313337
/// </summary>

OptimizelySDK/Utils/HoldoutConfig.cs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* Copyright 2025, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System.Collections.Generic;
18+
using System.Linq;
19+
using OptimizelySDK.Entity;
20+
21+
namespace OptimizelySDK.Utils
22+
{
23+
/// <summary>
24+
/// Configuration manager for holdouts, providing flag-to-holdout relationship mapping and optimization logic.
25+
/// </summary>
26+
public class HoldoutConfig
27+
{
28+
private readonly List<Holdout> _allHoldouts;
29+
private readonly List<Holdout> _globalHoldouts;
30+
private readonly Dictionary<string, Holdout> _holdoutIdMap;
31+
private readonly Dictionary<string, List<Holdout>> _includedHoldouts;
32+
private readonly Dictionary<string, List<Holdout>> _excludedHoldouts;
33+
private readonly Dictionary<string, List<Holdout>> _flagHoldoutCache;
34+
35+
/// <summary>
36+
/// Initializes a new instance of the HoldoutConfig class.
37+
/// </summary>
38+
/// <param name="allHoldouts">Array of all holdouts from the datafile</param>
39+
public HoldoutConfig(Holdout[] allHoldouts = null)
40+
{
41+
_allHoldouts = allHoldouts?.ToList() ?? new List<Holdout>();
42+
_globalHoldouts = new List<Holdout>();
43+
_holdoutIdMap = new Dictionary<string, Holdout>();
44+
_includedHoldouts = new Dictionary<string, List<Holdout>>();
45+
_excludedHoldouts = new Dictionary<string, List<Holdout>>();
46+
_flagHoldoutCache = new Dictionary<string, List<Holdout>>();
47+
48+
UpdateHoldoutMapping();
49+
}
50+
51+
/// <summary>
52+
/// Updates internal mappings of holdouts including the id map, global list, and per-flag inclusion/exclusion maps.
53+
/// </summary>
54+
private void UpdateHoldoutMapping()
55+
{
56+
// Clear existing mappings
57+
_holdoutIdMap.Clear();
58+
_globalHoldouts.Clear();
59+
_includedHoldouts.Clear();
60+
_excludedHoldouts.Clear();
61+
_flagHoldoutCache.Clear();
62+
63+
foreach (var holdout in _allHoldouts)
64+
{
65+
// Build ID mapping
66+
_holdoutIdMap[holdout.Id] = holdout;
67+
68+
var hasIncludedFlags = holdout.IncludedFlags != null && holdout.IncludedFlags.Length > 0;
69+
var hasExcludedFlags = holdout.ExcludedFlags != null && holdout.ExcludedFlags.Length > 0;
70+
71+
if (!hasIncludedFlags && !hasExcludedFlags)
72+
{
73+
// Global holdout (no included or excluded flags)
74+
_globalHoldouts.Add(holdout);
75+
}
76+
else if (hasIncludedFlags)
77+
{
78+
// Holdout with specific included flags
79+
foreach (var flagId in holdout.IncludedFlags)
80+
{
81+
if (!_includedHoldouts.ContainsKey(flagId))
82+
_includedHoldouts[flagId] = new List<Holdout>();
83+
84+
_includedHoldouts[flagId].Add(holdout);
85+
}
86+
}
87+
else if (hasExcludedFlags)
88+
{
89+
// Global holdout with excluded flags
90+
_globalHoldouts.Add(holdout);
91+
92+
foreach (var flagId in holdout.ExcludedFlags)
93+
{
94+
if (!_excludedHoldouts.ContainsKey(flagId))
95+
_excludedHoldouts[flagId] = new List<Holdout>();
96+
97+
_excludedHoldouts[flagId].Add(holdout);
98+
}
99+
}
100+
}
101+
}
102+
103+
/// <summary>
104+
/// Returns the applicable holdouts for the given flag ID by combining global holdouts (excluding any specified) and included holdouts, in that order.
105+
/// Caches the result for future calls.
106+
/// </summary>
107+
/// <param name="flagId">The flag identifier</param>
108+
/// <returns>A list of Holdout objects relevant to the given flag</returns>
109+
public List<Holdout> GetHoldoutsForFlag(string flagId)
110+
{
111+
if (_allHoldouts.Count == 0)
112+
return new List<Holdout>();
113+
114+
// Check cache first
115+
if (_flagHoldoutCache.ContainsKey(flagId))
116+
return _flagHoldoutCache[flagId];
117+
118+
var activeHoldouts = new List<Holdout>();
119+
120+
// Start with global holdouts, excluding any that are specifically excluded for this flag
121+
var excludedForFlag = _excludedHoldouts.ContainsKey(flagId) ? _excludedHoldouts[flagId] : new List<Holdout>();
122+
123+
foreach (var globalHoldout in _globalHoldouts)
124+
{
125+
if (!excludedForFlag.Contains(globalHoldout))
126+
{
127+
activeHoldouts.Add(globalHoldout);
128+
}
129+
}
130+
131+
// Add included holdouts for this flag
132+
if (_includedHoldouts.ContainsKey(flagId))
133+
{
134+
activeHoldouts.AddRange(_includedHoldouts[flagId]);
135+
}
136+
137+
// Cache the result
138+
_flagHoldoutCache[flagId] = activeHoldouts;
139+
140+
return activeHoldouts;
141+
}
142+
143+
/// <summary>
144+
/// Get a Holdout object for an ID.
145+
/// </summary>
146+
/// <param name="holdoutId">The holdout identifier</param>
147+
/// <returns>The Holdout object if found, null otherwise</returns>
148+
public Holdout GetHoldout(string holdoutId)
149+
{
150+
return _holdoutIdMap.ContainsKey(holdoutId) ? _holdoutIdMap[holdoutId] : null;
151+
}
152+
153+
/// <summary>
154+
/// Gets the total number of holdouts.
155+
/// </summary>
156+
public int HoldoutCount => _allHoldouts.Count;
157+
158+
/// <summary>
159+
/// Gets the number of global holdouts.
160+
/// </summary>
161+
public int GlobalHoldoutCount => _globalHoldouts.Count;
162+
}
163+
}

0 commit comments

Comments
 (0)