Skip to content

Commit d9a6e9a

Browse files
[FSSDK-9079] fix: send odp event empty values (#346)
* add empty check to SendOdpEvent * eval idempotence_id per event * convert fs_user_id identifier variants * add unit tests * Optimize fs_user_id updates * use constant for error msg * make SendOdpEvent type and data optional --------- Co-authored-by: Mike Chu <[email protected]>
1 parent 028d820 commit d9a6e9a

File tree

8 files changed

+252
-18
lines changed

8 files changed

+252
-18
lines changed

OptimizelySDK.Tests/OdpTests/OdpEventManagerTests.cs

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* Copyright 2022-2023, Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -308,6 +308,118 @@ public void ShouldDiscardEventsWithInvalidIdentifiersDictionary()
308308
Times.Exactly(2));
309309
}
310310

311+
[Test]
312+
public void ShouldConvertFsUserIdIdentifier()
313+
{
314+
var event1 = new OdpEvent("t3", "a3",
315+
new Dictionary<string, string>
316+
{
317+
{
318+
"fs-user-id", "123"
319+
}
320+
},
321+
new Dictionary<string, object>()
322+
);
323+
324+
var event2 = new OdpEvent("t3", "a3",
325+
new Dictionary<string, string>
326+
{
327+
{
328+
"FS-user-ID", "123"
329+
}
330+
},
331+
new Dictionary<string, object>()
332+
);
333+
334+
var event3 = new OdpEvent("t3", "a3",
335+
new Dictionary<string, string>
336+
{
337+
{
338+
"FS_USER_ID", "123"
339+
},
340+
{
341+
"fs.user.id", "456"
342+
}
343+
},
344+
new Dictionary<string, object>()
345+
);
346+
347+
var event4 = new OdpEvent("t3", "a3",
348+
new Dictionary<string, string>
349+
{
350+
{
351+
"fs_user_id", "123"
352+
},
353+
{
354+
"fsuserid", "456"
355+
}
356+
},
357+
new Dictionary<string, object>()
358+
);
359+
360+
var expectedIdentifiers = new List<Dictionary<string, string>> {
361+
new Dictionary<string, string> {
362+
{
363+
"fs_user_id", "123"
364+
}
365+
},
366+
new Dictionary<string, string> {
367+
{
368+
"fs_user_id", "123"
369+
}
370+
},
371+
new Dictionary<string, string> {
372+
{
373+
"fs_user_id", "123"
374+
},
375+
{
376+
"fs.user.id", "456"
377+
}
378+
},
379+
new Dictionary<string, string>
380+
{
381+
{
382+
"fs_user_id", "123"
383+
},
384+
{
385+
"fsuserid", "456"
386+
}
387+
},
388+
};
389+
390+
var cde = new CountdownEvent(4);
391+
var eventsCollector = new List<List<OdpEvent>>();
392+
_mockApiManager.Setup(api => api.SendEvents(
393+
It.IsAny<string>(),
394+
It.IsAny<string>(),
395+
Capture.In(eventsCollector))
396+
).Callback(() => cde.Signal());
397+
398+
var eventManager = new OdpEventManager.Builder().
399+
WithOdpEventApiManager(_mockApiManager.Object).
400+
WithLogger(_mockLogger.Object).
401+
WithEventQueue(new BlockingCollection<object>(10)). // max capacity of 10
402+
WithFlushInterval(TimeSpan.FromMilliseconds(0)).
403+
Build();
404+
eventManager.UpdateSettings(_odpConfig);
405+
406+
eventManager.SendEvent(event1);
407+
eventManager.SendEvent(event2);
408+
eventManager.SendEvent(event3);
409+
eventManager.SendEvent(event4);
410+
cde.Wait(MAX_COUNT_DOWN_EVENT_WAIT_MS);
411+
412+
413+
foreach (int i in Enumerable.Range(0, 4))
414+
{
415+
CollectionAssert.AreEquivalent(
416+
expectedIdentifiers[i],
417+
eventsCollector[i][0].Identifiers
418+
);
419+
420+
}
421+
}
422+
311423
[Test]
312424
public void ShouldAddAdditionalInformationToEachEvent()
313425
{

OptimizelySDK.Tests/OptimizelyTest.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public class OptimizelyTest
5555
private Mock<EventProcessor> EventProcessorMock;
5656
private NotificationCenter NotificationCenter;
5757
private Mock<TestNotificationCallbacks> NotificationCallbackMock;
58+
private Mock<IOdpManager> OdpManagerMock;
5859
private Variation VariationWithKeyControl;
5960
private Variation VariationWithKeyVariation;
6061
private Variation GroupVariation;
@@ -109,6 +110,8 @@ public void Initialize()
109110
CallBase = true,
110111
};
111112

113+
OdpManagerMock = new Mock<IOdpManager>();
114+
112115
DecisionServiceMock = new Mock<DecisionService>(new Bucketer(LoggerMock.Object),
113116
ErrorHandlerMock.Object,
114117
null, LoggerMock.Object);
@@ -6166,5 +6169,62 @@ public static void SetCulture(string culture)
61666169
}
61676170

61686171
#endregion Test Culture
6172+
6173+
#region Test SendOdpEvent
6174+
6175+
[Test]
6176+
public void TestSendOdpEventNullAction()
6177+
{
6178+
var optly = new Optimizely(TestData.OdpIntegrationDatafile, logger: LoggerMock.Object, odpManager: OdpManagerMock.Object);
6179+
optly.SendOdpEvent(action: null, identifiers: new Dictionary<string, string>(), type: "type");
6180+
LoggerMock.Verify(l => l.Log(LogLevel.ERROR, Constants.ODP_INVALID_ACTION_MESSAGE),
6181+
Times.Exactly(1));
6182+
6183+
optly.Dispose();
6184+
}
6185+
6186+
[Test]
6187+
public void TestSendOdpEventEmptyStringAction()
6188+
{
6189+
var optly = new Optimizely(TestData.OdpIntegrationDatafile, logger: LoggerMock.Object, odpManager: OdpManagerMock.Object);
6190+
optly.SendOdpEvent(action: "", identifiers: new Dictionary<string, string>(), type: "type");
6191+
LoggerMock.Verify(l => l.Log(LogLevel.ERROR, Constants.ODP_INVALID_ACTION_MESSAGE),
6192+
Times.Exactly(1));
6193+
6194+
optly.Dispose();
6195+
}
6196+
[Test]
6197+
public void TestSendOdpEventNullType()
6198+
{
6199+
var identifiers = new Dictionary<string, string>();
6200+
var optly = new Optimizely(TestData.OdpIntegrationDatafile, logger: LoggerMock.Object, odpManager: OdpManagerMock.Object);
6201+
6202+
optly.SendOdpEvent(action: "action", identifiers: identifiers, type: null);
6203+
6204+
LoggerMock.Verify(l => l.Log(LogLevel.ERROR, It.IsAny<string>()),
6205+
Times.Never);
6206+
OdpManagerMock.Verify(e => e.SendEvent("fullstack", "action", identifiers, null),
6207+
Times.Once);
6208+
6209+
optly.Dispose();
6210+
}
6211+
6212+
[Test]
6213+
public void TestSendOdpEventEmptyStringType()
6214+
{
6215+
var identifiers = new Dictionary<string, string>();
6216+
var optly = new Optimizely(TestData.OdpIntegrationDatafile, logger: LoggerMock.Object, odpManager: OdpManagerMock.Object);
6217+
6218+
optly.SendOdpEvent(action: "action", identifiers: identifiers, type: "");
6219+
6220+
LoggerMock.Verify(l => l.Log(LogLevel.ERROR, It.IsAny<string>()),
6221+
Times.Never);
6222+
OdpManagerMock.Verify(e => e.SendEvent("fullstack", "action", identifiers, null),
6223+
Times.Once);
6224+
6225+
optly.Dispose();
6226+
}
6227+
6228+
#endregion Test SendOdpEvent
61696229
}
61706230
}

OptimizelySDK/IOptimizely.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ Variation GetVariation(string experimentKey, string userId,
8383
bool SetForcedVariation(string experimentKey, string userId, string variationKey);
8484

8585
/// <summary>
86-
/// Gets the forced variation key for the given user and experiment.
86+
/// Gets the forced variation key for the given user and experiment.
8787
/// </summary>
8888
/// <param name="experimentKey">The experiment key</param>
8989
/// <param name="userId">The user ID</param>
@@ -162,7 +162,7 @@ string GetFeatureVariableString(string featureKey, string variableKey, string us
162162
#endregion
163163

164164
#if USE_ODP
165-
void SendOdpEvent(string type, string action, Dictionary<string, string> identifiers,
165+
void SendOdpEvent(string action, Dictionary<string, string> identifiers, string type,
166166
Dictionary<string, object> data
167167
);
168168
#endif

OptimizelySDK/Odp/Constants.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ public static class Constants
6767
/// </summary>
6868
public const string ODP_INVALID_DATA_MESSAGE = "ODP event send failed.";
6969

70+
/// <summary>
71+
/// Default message to log when an ODP Event contains invalid action
72+
/// </summary>
73+
public const string ODP_INVALID_ACTION_MESSAGE = "ODP action is not valid (cannot be empty).";
74+
7075
/// <summary>
7176
/// Default message to log when sending ODP event fails
7277
/// </summary>
@@ -116,5 +121,15 @@ public static class Constants
116121
/// Type of ODP key used for fetching segments & sending events
117122
/// </summary>
118123
public const string FS_USER_ID = "fs_user_id";
124+
125+
/// <summary>
126+
/// Alternate form of ODP key that is auto-converted to FS_USER_ID
127+
/// </summary>
128+
public const string FS_USER_ID_ALIAS = "fs-user-id";
129+
130+
/// <summary>
131+
/// Unique identifier used for ODP events
132+
/// </summary>
133+
public const string IDEMPOTENCE_ID = "idempotence_id";
119134
}
120135
}

OptimizelySDK/Odp/Entity/OdpEvent.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public class OdpEvent
3636
/// <summary>
3737
/// Dictionary for identifiers. The caller must provide at least one key-value pair.
3838
/// </summary>
39-
public Dictionary<string, string> Identifiers { get; }
39+
public Dictionary<string, string> Identifiers { get; set; }
4040

4141
/// <summary>
4242
/// Event data in a key-value pair format

OptimizelySDK/Odp/OdpEventManager.cs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ public class OdpEventManager : IOdpEventManager, IDisposable
4040
private ILogger _logger;
4141
private IErrorHandler _errorHandler;
4242
private BlockingCollection<object> _eventQueue;
43+
private static readonly string[] fsUserIdMatches = {
44+
Constants.FS_USER_ID,
45+
Constants.FS_USER_ID_ALIAS,
46+
};
4347

4448
/// <summary>
4549
/// Object to ensure mutually exclusive locking for thread safety
@@ -302,6 +306,8 @@ public void SendEvent(OdpEvent odpEvent)
302306
return;
303307
}
304308

309+
odpEvent.Identifiers = ConvertCriticalIdentifiers(odpEvent.Identifiers);
310+
305311
odpEvent.Data = AugmentCommonData(odpEvent.Data);
306312
if (!_eventQueue.TryAdd(odpEvent))
307313
{
@@ -429,7 +435,40 @@ private Dictionary<string, dynamic> AugmentCommonData(
429435
Dictionary<string, dynamic> sourceData
430436
)
431437
{
432-
return sourceData.MergeInPlace<string, object>(_commonData);
438+
sourceData.MergeInPlace<string, object>(_commonData);
439+
if (!sourceData.ContainsKey(Constants.IDEMPOTENCE_ID))
440+
{
441+
sourceData.Add(Constants.IDEMPOTENCE_ID, Guid.NewGuid());
442+
}
443+
return sourceData;
444+
}
445+
446+
/// <summary>
447+
/// Convert critical identifiers to the correct format for ODP
448+
/// </summary>
449+
/// <param name="identifiers">Collection of identifiers to validate</param>
450+
/// <returns>New version of identifiers with corrected values</returns>
451+
private static Dictionary<string, string> ConvertCriticalIdentifiers(
452+
Dictionary<string, string> identifiers
453+
)
454+
{
455+
if (identifiers.ContainsKey(Constants.FS_USER_ID))
456+
{
457+
return identifiers;
458+
}
459+
460+
var identList = new List<KeyValuePair<string, string>>(identifiers);
461+
foreach (var kvp in identList)
462+
{
463+
if (fsUserIdMatches.Contains(kvp.Key, StringComparer.OrdinalIgnoreCase))
464+
{
465+
identifiers.Remove(kvp.Key);
466+
identifiers[Constants.FS_USER_ID] = kvp.Value;
467+
break;
468+
}
469+
}
470+
471+
return identifiers;
433472
}
434473

435474
/// <summary>
@@ -562,7 +601,6 @@ public OdpEventManager Build()
562601

563602
manager._commonData = new Dictionary<string, object>
564603
{
565-
{ "idempotence_id", Guid.NewGuid() },
566604
{ "data_source_type", "sdk" },
567605
{ "data_source", Optimizely.SDK_TYPE },
568606
{ "data_source_version", Optimizely.SDK_VERSION },

OptimizelySDK/Optimizely.cs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,14 +1354,25 @@ internal void IdentifyUser(string userId)
13541354
/// <summary>
13551355
/// Add event to queue for sending to ODP
13561356
/// </summary>
1357-
/// <param name="type">Type of event (typically `fullstack` from server-side SDK events)</param>
13581357
/// <param name="action">Subcategory of the event type</param>
13591358
/// <param name="identifiers">Dictionary for identifiers. The caller must provide at least one key-value pair.</param>
1360-
/// <param name="data">Event data in a key-value pair format</param>
1361-
public void SendOdpEvent(string type, string action, Dictionary<string, string> identifiers,
1362-
Dictionary<string, object> data
1359+
/// <param name="type">Type of event (defaults to `fullstack`)</param>
1360+
/// <param name="data">Optional event data in a key-value pair format</param>
1361+
public void SendOdpEvent(string action, Dictionary<string, string> identifiers, string type = Constants.ODP_EVENT_TYPE,
1362+
Dictionary<string, object> data = null
13631363
)
13641364
{
1365+
if (String.IsNullOrEmpty(action))
1366+
{
1367+
Logger.Log(LogLevel.ERROR, Constants.ODP_INVALID_ACTION_MESSAGE);
1368+
return;
1369+
}
1370+
1371+
if (String.IsNullOrEmpty(type))
1372+
{
1373+
type = Constants.ODP_EVENT_TYPE;
1374+
}
1375+
13651376
OdpManager?.SendEvent(type, action, identifiers, data);
13661377
}
13671378
#endif

OptimizelySDK/Utils/CollectionExtensions.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
/*
2-
* Copyright 2022, Optimizely
1+
/*
2+
* Copyright 2022-2023, Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -29,10 +29,9 @@ public static class CollectionExtensions
2929
/// <param name="right">Dictionary to merge into another</param>
3030
/// <typeparam name="TKey">Key type</typeparam>
3131
/// <typeparam name="TValue">Value type</typeparam>
32-
/// <returns>Merged Dictionary</returns>
3332
/// <exception cref="ArgumentNullException">Thrown if target Dictionary is null</exception>
34-
public static Dictionary<TKey, TValue> MergeInPlace<TKey, TValue>(
35-
this Dictionary<TKey, TValue> left, Dictionary<TKey, TValue> right
33+
public static void MergeInPlace<TKey, TValue>(this Dictionary<TKey, TValue> left,
34+
Dictionary<TKey, TValue> right
3635
)
3736
{
3837
if (left == null)
@@ -43,16 +42,15 @@ this Dictionary<TKey, TValue> left, Dictionary<TKey, TValue> right
4342

4443
if (right == null)
4544
{
46-
return left;
45+
return;
4746
}
4847

48+
// Only update the left dictionary when the key doesn't exist
4949
foreach (var kvp in right.Where(
5050
kvp => !left.ContainsKey(kvp.Key)))
5151
{
5252
left.Add(kvp.Key, kvp.Value);
5353
}
54-
55-
return left;
5654
}
5755
}
5856
}

0 commit comments

Comments
 (0)