Skip to content

Commit 1f405cd

Browse files
[FSSDK-11177] Decision Service CMAB + Optimizely Client + Impression event adjustment (#394)
1 parent 375e6a2 commit 1f405cd

31 files changed

+2916
-244
lines changed

OptimizelySDK.Net35/OptimizelySDK.Net35.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@
230230
<Compile Include="..\OptimizelySDK\Bucketing\UserProfileUtil.cs">
231231
<Link>Bucketing\UserProfileUtil</Link>
232232
</Compile>
233+
<Compile Include="..\OptimizelySDK\Bucketing\VariationDecisionResult.cs">
234+
<Link>Bucketing\VariationDecisionResult.cs</Link>
235+
</Compile>
233236
<Compile Include="..\OptimizelySDK\Entity\FeatureVariableUsage.cs">
234237
<Link>Entity\FeatureVariableUsage</Link>
235238
</Compile>

OptimizelySDK.Net40/OptimizelySDK.Net40.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@
229229
<Compile Include="..\OptimizelySDK\Bucketing\UserProfileUtil.cs">
230230
<Link>Bucketing\UserProfileUtil</Link>
231231
</Compile>
232+
<Compile Include="..\OptimizelySDK\Bucketing\VariationDecisionResult.cs">
233+
<Link>Bucketing\VariationDecisionResult.cs</Link>
234+
</Compile>
232235
<Compile Include="..\OptimizelySDK\Entity\FeatureVariableUsage.cs">
233236
<Link>Entity\FeatureVariableUsage</Link>
234237
</Compile>

OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
<Compile Include="..\OptimizelySDK\Bucketing\UserProfileTracker.cs" />
8080
<Compile Include="..\OptimizelySDK\Bucketing\UserProfileService.cs" />
8181
<Compile Include="..\OptimizelySDK\Bucketing\UserProfileUtil.cs" />
82+
<Compile Include="..\OptimizelySDK\Bucketing\VariationDecisionResult.cs" />
8283
<Compile Include="..\OptimizelySDK\ProjectConfig.cs" />
8384
<Compile Include="..\OptimizelySDK\Config\DatafileProjectConfig.cs" />
8485
<Compile Include="..\OptimizelySDK\Entity\Integration.cs" />

OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@
142142
<Compile Include="..\OptimizelySDK\Bucketing\UserProfileUtil.cs">
143143
<Link>Bucketing\UserProfileUtil.cs</Link>
144144
</Compile>
145+
<Compile Include="..\OptimizelySDK\Bucketing\VariationDecisionResult.cs">
146+
<Link>Bucketing\VariationDecisionResult.cs</Link>
147+
</Compile>
145148
<Compile Include="..\OptimizelySDK\Config\DatafileProjectConfig.cs">
146149
<Link>Config\DatafileProjectConfig.cs</Link>
147150
</Compile>
@@ -196,6 +199,9 @@
196199
<Compile Include="..\OptimizelySDK\Cmab\CmabRetryConfig.cs">
197200
<Link>Cmab\CmabRetryConfig.cs</Link>
198201
</Compile>
202+
<Compile Include="..\OptimizelySDK\Cmab\CmabConfig.cs">
203+
<Link>Cmab\CmabConfig.cs</Link>
204+
</Compile>
199205
<Compile Include="..\OptimizelySDK\Cmab\CmabModels.cs">
200206
<Link>Cmab\CmabModels.cs</Link>
201207
</Compile>
@@ -369,6 +375,9 @@
369375
<Compile Include="..\OptimizelySDK\Utils\Validator.cs">
370376
<Link>Utils\Validator.cs</Link>
371377
</Compile>
378+
<Compile Include="..\OptimizelySDK\Utils\ICacheWithRemove.cs">
379+
<Link>Utils\ICacheWithRemove.cs</Link>
380+
</Compile>
372381
<Compile Include="..\OptimizelySDK\Event\BatchEventProcessor.cs">
373382
<Link>Event\BatchEventProcessor.cs</Link>
374383
</Compile>
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
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+
* http://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 Moq;
19+
using Newtonsoft.Json;
20+
using NUnit.Framework;
21+
using OptimizelySDK.Bucketing;
22+
using OptimizelySDK.Config;
23+
using OptimizelySDK.Entity;
24+
using OptimizelySDK.ErrorHandler;
25+
using OptimizelySDK.Logger;
26+
27+
namespace OptimizelySDK.Tests
28+
{
29+
[TestFixture]
30+
public class BucketerBucketToEntityIdTest
31+
{
32+
[SetUp]
33+
public void SetUp()
34+
{
35+
_loggerMock = new Mock<ILogger>();
36+
}
37+
38+
private const string ExperimentId = "bucket_entity_exp";
39+
private const string ExperimentKey = "bucket_entity_experiment";
40+
private const string GroupId = "group_1";
41+
42+
private Mock<ILogger> _loggerMock;
43+
44+
[Test]
45+
public void BucketToEntityIdAllowsBucketingWhenNoGroup()
46+
{
47+
var config = CreateConfig(new ConfigSetup { IncludeGroup = false });
48+
var experiment = config.GetExperimentFromKey(ExperimentKey);
49+
var bucketer = new Bucketer(_loggerMock.Object);
50+
51+
var fullAllocation = CreateTrafficAllocations(new TrafficAllocation
52+
{
53+
EntityId = "entity_123",
54+
EndOfRange = 10000,
55+
});
56+
var fullResult = bucketer.BucketToEntityId(config, experiment, "bucketing_id", "user",
57+
fullAllocation);
58+
Assert.IsNotNull(fullResult.ResultObject);
59+
Assert.AreEqual("entity_123", fullResult.ResultObject);
60+
61+
var zeroAllocation = CreateTrafficAllocations(new TrafficAllocation
62+
{
63+
EntityId = "entity_123",
64+
EndOfRange = 0,
65+
});
66+
var zeroResult = bucketer.BucketToEntityId(config, experiment, "bucketing_id", "user",
67+
zeroAllocation);
68+
Assert.IsNull(zeroResult.ResultObject);
69+
}
70+
71+
[Test]
72+
public void BucketToEntityIdReturnsEntityIdWhenGroupAllowsUser()
73+
{
74+
var config = CreateConfig(new ConfigSetup
75+
{
76+
IncludeGroup = true,
77+
GroupPolicy = "random",
78+
GroupEndOfRange = 10000,
79+
});
80+
81+
var experiment = config.GetExperimentFromKey(ExperimentKey);
82+
var bucketer = new Bucketer(_loggerMock.Object);
83+
84+
var testCases = new[]
85+
{
86+
new { BucketingId = "ppid1", EntityId = "entity1" },
87+
new { BucketingId = "ppid2", EntityId = "entity2" },
88+
new { BucketingId = "ppid3", EntityId = "entity3" },
89+
new
90+
{
91+
BucketingId =
92+
"a very very very very very very very very very very very very very very very long ppd string",
93+
EntityId = "entity4",
94+
},
95+
};
96+
97+
foreach (var testCase in testCases)
98+
{
99+
var allocation = CreateTrafficAllocations(new TrafficAllocation
100+
{
101+
EntityId = testCase.EntityId,
102+
EndOfRange = 10000,
103+
});
104+
var result = bucketer.BucketToEntityId(config, experiment, testCase.BucketingId,
105+
testCase.BucketingId, allocation);
106+
Assert.AreEqual(testCase.EntityId, result.ResultObject,
107+
$"Failed for {testCase.BucketingId}");
108+
}
109+
}
110+
111+
[Test]
112+
public void BucketToEntityIdReturnsNullWhenGroupRejectsUser()
113+
{
114+
var config = CreateConfig(new ConfigSetup
115+
{
116+
IncludeGroup = true,
117+
GroupPolicy = "random",
118+
GroupEndOfRange = 0,
119+
});
120+
121+
var experiment = config.GetExperimentFromKey(ExperimentKey);
122+
var bucketer = new Bucketer(_loggerMock.Object);
123+
124+
var allocation = CreateTrafficAllocations(new TrafficAllocation
125+
{
126+
EntityId = "entity1",
127+
EndOfRange = 10000,
128+
});
129+
var testCases = new[]
130+
{
131+
"ppid1",
132+
"ppid2",
133+
"ppid3",
134+
"a very very very very very very very very very very very very very very very long ppd string",
135+
};
136+
137+
foreach (var bucketingId in testCases)
138+
{
139+
var result = bucketer.BucketToEntityId(config, experiment, bucketingId, bucketingId,
140+
allocation);
141+
Assert.IsNull(result.ResultObject, $"Expected null for {bucketingId}");
142+
}
143+
}
144+
145+
[Test]
146+
public void BucketToEntityIdAllowsBucketingWhenGroupOverlapping()
147+
{
148+
var config = CreateConfig(new ConfigSetup
149+
{
150+
IncludeGroup = true,
151+
GroupPolicy = "overlapping",
152+
GroupEndOfRange = 10000,
153+
});
154+
155+
var experiment = config.GetExperimentFromKey(ExperimentKey);
156+
var bucketer = new Bucketer(_loggerMock.Object);
157+
158+
var allocation = CreateTrafficAllocations(new TrafficAllocation
159+
{
160+
EntityId = "entity_overlapping",
161+
EndOfRange = 10000,
162+
});
163+
var result =
164+
bucketer.BucketToEntityId(config, experiment, "bucketing_id", "user", allocation);
165+
Assert.AreEqual("entity_overlapping", result.ResultObject);
166+
}
167+
168+
private static IList<TrafficAllocation> CreateTrafficAllocations(
169+
params TrafficAllocation[] allocations
170+
)
171+
{
172+
return new List<TrafficAllocation>(allocations);
173+
}
174+
175+
private ProjectConfig CreateConfig(ConfigSetup setup)
176+
{
177+
if (setup == null)
178+
{
179+
setup = new ConfigSetup();
180+
}
181+
182+
var datafile = BuildDatafile(setup);
183+
return DatafileProjectConfig.Create(datafile, _loggerMock.Object,
184+
new NoOpErrorHandler());
185+
}
186+
187+
private static string BuildDatafile(ConfigSetup setup)
188+
{
189+
var variations = new object[]
190+
{
191+
new Dictionary<string, object>
192+
{
193+
{ "id", "var_1" },
194+
{ "key", "variation_1" },
195+
{ "variables", new object[0] },
196+
},
197+
};
198+
199+
var experiment = new Dictionary<string, object>
200+
{
201+
{ "status", "Running" },
202+
{ "key", ExperimentKey },
203+
{ "layerId", "layer_1" },
204+
{ "id", ExperimentId },
205+
{ "audienceIds", new string[0] },
206+
{ "audienceConditions", "[]" },
207+
{ "forcedVariations", new Dictionary<string, string>() },
208+
{ "variations", variations },
209+
{
210+
"trafficAllocation", new object[]
211+
{
212+
new Dictionary<string, object>
213+
{
214+
{ "entityId", "var_1" },
215+
{ "endOfRange", 10000 },
216+
},
217+
}
218+
},
219+
};
220+
221+
object[] groups;
222+
if (setup.IncludeGroup)
223+
{
224+
var groupExperiment = new Dictionary<string, object>(experiment);
225+
groupExperiment["trafficAllocation"] = new object[0];
226+
227+
groups = new object[]
228+
{
229+
new Dictionary<string, object>
230+
{
231+
{ "id", GroupId },
232+
{ "policy", setup.GroupPolicy },
233+
{
234+
"trafficAllocation", new object[]
235+
{
236+
new Dictionary<string, object>
237+
{
238+
{ "entityId", ExperimentId },
239+
{ "endOfRange", setup.GroupEndOfRange },
240+
},
241+
}
242+
},
243+
{ "experiments", new object[] { groupExperiment } },
244+
},
245+
};
246+
}
247+
else
248+
{
249+
groups = new object[0];
250+
}
251+
252+
var datafile = new Dictionary<string, object>
253+
{
254+
{ "version", "4" },
255+
{ "projectId", "project_1" },
256+
{ "accountId", "account_1" },
257+
{ "revision", "1" },
258+
{ "environmentKey", string.Empty },
259+
{ "sdkKey", string.Empty },
260+
{ "sendFlagDecisions", false },
261+
{ "anonymizeIP", false },
262+
{ "botFiltering", false },
263+
{ "attributes", new object[0] },
264+
{ "audiences", new object[0] },
265+
{ "typedAudiences", new object[0] },
266+
{ "events", new object[0] },
267+
{ "featureFlags", new object[0] },
268+
{ "rollouts", new object[0] },
269+
{ "integrations", new object[0] },
270+
{ "holdouts", new object[0] },
271+
{ "groups", groups },
272+
{ "experiments", new object[] { experiment } },
273+
{ "segments", new object[0] },
274+
};
275+
276+
return JsonConvert.SerializeObject(datafile);
277+
}
278+
279+
private class ConfigSetup
280+
{
281+
public bool IncludeGroup { get; set; }
282+
public string GroupPolicy { get; set; }
283+
public int GroupEndOfRange { get; set; }
284+
}
285+
}
286+
}

0 commit comments

Comments
 (0)