Skip to content

Commit 17c7181

Browse files
mfahadahmedmikeproeng37
authored andcommitted
fix(api): Only track attributes with valid attribute types. (#103)
1 parent ed28c56 commit 17c7181

File tree

7 files changed

+187
-43
lines changed

7 files changed

+187
-43
lines changed

OptimizelySDK.Tests/EventTests/EventBuilderTest.cs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,115 @@ public void TestCreateImpressionEventWithTypedAttributes()
328328
Assert.IsTrue(TestData.CompareObjects(expectedLogEvent, logEvent));
329329
}
330330

331+
[Test]
332+
public void TestCreateImpressionEventRemovesInvalidAttributesFromPayload()
333+
{
334+
var guid = Guid.NewGuid();
335+
var timeStamp = TestData.SecondsSince1970();
336+
337+
var payloadParams = new Dictionary<string, object>
338+
{
339+
{ "visitors", new object[]
340+
{
341+
new Dictionary<string, object>()
342+
{
343+
{ "snapshots", new object[]
344+
{
345+
new Dictionary<string, object>
346+
{
347+
{ "decisions", new object[]
348+
{
349+
new Dictionary<string, object>
350+
{
351+
{"campaign_id", "7719770039" },
352+
{"experiment_id", "7716830082" },
353+
{"variation_id", "7722370027" }
354+
}
355+
}
356+
},
357+
{ "events", new object[]
358+
{
359+
new Dictionary<string, object>
360+
{
361+
{"entity_id", "7719770039" },
362+
{"timestamp", timeStamp },
363+
{"uuid", guid },
364+
{"key", "campaign_activated" }
365+
}
366+
}
367+
}
368+
}
369+
}
370+
},
371+
{"attributes", new object[]
372+
{
373+
new Dictionary<string, object>
374+
{
375+
{"entity_id", "7723280020" },
376+
{"key", "device_type" },
377+
{"type", "custom" },
378+
{"value", "iPhone"}
379+
},
380+
new Dictionary<string, object>
381+
{
382+
{"entity_id", "323434545" },
383+
{"key", "boolean_key" },
384+
{"type", "custom" },
385+
{"value", true}
386+
},
387+
new Dictionary<string, object>
388+
{
389+
{"entity_id", "808797686" },
390+
{"key", "double_key" },
391+
{"type", "custom" },
392+
{"value", 3.14}
393+
},
394+
new Dictionary<string, object>
395+
{
396+
{"entity_id", ControlAttributes.BOT_FILTERING_ATTRIBUTE},
397+
{"key", ControlAttributes.BOT_FILTERING_ATTRIBUTE},
398+
{"type", "custom" },
399+
{"value", true }
400+
}
401+
}
402+
},
403+
{ "visitor_id", TestUserId }
404+
}
405+
}
406+
},
407+
{"project_id", "7720880029" },
408+
{"account_id", "1592310167" },
409+
{"client_name", "csharp-sdk" },
410+
{"client_version", Optimizely.SDK_VERSION },
411+
{"revision", "15" },
412+
{"anonymize_ip", false}
413+
};
414+
415+
var expectedLogEvent = new LogEvent("https://logx.optimizely.com/v1/events",
416+
payloadParams,
417+
"POST",
418+
new Dictionary<string, string>
419+
{
420+
{ "Content-Type", "application/json" }
421+
});
422+
423+
var userAttributes = new UserAttributes
424+
{
425+
{"device_type", "iPhone" },
426+
{"boolean_key", true },
427+
{"double_key", 3.14 },
428+
{ "", "Android" },
429+
{ "null", null },
430+
{ "objects", new object() },
431+
{ "arrays", new string[] { "a", "b", "c" } },
432+
};
433+
434+
var logEvent = EventBuilder.CreateImpressionEvent(Config, Config.GetExperimentFromKey("test_experiment"), "7722370027", TestUserId, userAttributes);
435+
TestData.ChangeGUIDAndTimeStamp(logEvent.Params, timeStamp, guid);
436+
437+
Assert.IsTrue(TestData.CompareObjects(expectedLogEvent, logEvent));
438+
}
439+
331440
[Test]
332441
public void TestCreateConversionEventNoAttributesNoValue()
333442
{

OptimizelySDK.Tests/OptimizelyTest.cs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -401,9 +401,12 @@ public void TestActivateWithAttributes()
401401
EventBuilderMock.Verify(b => b.CreateImpressionEvent(It.IsAny<ProjectConfig>(), Config.GetExperimentFromKey("test_experiment"),
402402
"7722370027", "test_user", OptimizelyHelper.UserAttributes), Times.Once);
403403

404+
404405
LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Exactly(6));
406+
LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"test_user\" is not in the forced variation map."), Times.Once);
405407
LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), Times.Once);
406408
LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [test_user] is in variation [control] of experiment [test_experiment]."), Times.Once);
409+
LoggerMock.Verify(l => l.Log(LogLevel.INFO, "This decision will not be saved since the UserProfileService is null."));
407410
LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user test_user in experiment test_experiment."), Times.Once);
408411
LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, @"Dispatching impression event to URL logx.optimizely.com/decision with params {""param1"":""val1""}."), Times.Once);
409412

@@ -423,17 +426,14 @@ public void TestActivateWithNullAttributes()
423426
var variation = (Variation)optly.Invoke("Activate", "test_experiment", "test_user", OptimizelyHelper.NullUserAttributes);
424427

425428
EventBuilderMock.Verify(b => b.CreateImpressionEvent(It.IsAny<ProjectConfig>(), Config.GetExperimentFromKey("test_experiment"),
426-
"7722370027", "test_user", OptimizelyHelper.UserAttributes), Times.Once);
429+
"7722370027", "test_user", OptimizelyHelper.NullUserAttributes), Times.Once);
427430

428-
LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Exactly(9));
431+
LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Exactly(6));
429432

430433
//"User "test_user" is not in the forced variation map."
431434
LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"test_user\" is not in the forced variation map."), Times.Once);
432435
LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."), Times.Once);
433436
LoggerMock.Verify(l => l.Log(LogLevel.INFO, "User [test_user] is in variation [control] of experiment [test_experiment]."), Times.Once);
434-
LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "[UserAttributes] Null value for key null_value removed and will not be sent to results."), Times.Once);
435-
LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "[UserAttributes] Null value for key wont_be_sent removed and will not be sent to results."), Times.Once);
436-
LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "[UserAttributes] Null value for key bad_food removed and will not be sent to results."), Times.Once);
437437
LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Activating user test_user in experiment test_experiment."), Times.Once);
438438
LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, @"Dispatching impression event to URL logx.optimizely.com/decision with params {""param1"":""val1""}."), Times.Once);
439439

@@ -683,7 +683,7 @@ public void TestTrackWithNullAttributesWithNullEventValue()
683683
{ "wont_send_null", null}
684684
});
685685

686-
LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Exactly(21));
686+
LoggerMock.Verify(l => l.Log(It.IsAny<LogLevel>(), It.IsAny<string>()), Times.Exactly(18));
687687

688688
LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"test_user\" is not in the forced variation map."), Times.Exactly(3));
689689
LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Assigned bucket [3037] to user [test_user] with bucketing ID [test_user]."));
@@ -698,9 +698,6 @@ public void TestTrackWithNullAttributesWithNullEventValue()
698698
LoggerMock.Verify(l => l.Log(LogLevel.INFO, "This decision will not be saved since the UserProfileService is null."));
699699
LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Experiment \"paused_experiment\" is not running."));
700700
LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Not tracking user \"test_user\" for experiment \"paused_experiment\""));
701-
LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "[UserAttributes] Null value for key null_value removed and will not be sent to results."));
702-
LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "[UserAttributes] Null value for key wont_be_sent removed and will not be sent to results."));
703-
LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "[UserAttributes] Null value for key bad_food removed and will not be sent to results."));
704701
LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "[EventTags] Null value for key wont_send_null removed and will not be sent to results."));
705702
LoggerMock.Verify(l => l.Log(LogLevel.INFO, "Tracking event purchase for user test_user."));
706703
LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "Dispatching conversion event to URL logx.optimizely.com/track with params {\"param1\":\"val1\"}."));

OptimizelySDK.Tests/UtilsTests/ValidatorTest.cs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017, Optimizely
2+
* Copyright 2017-2018, 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.
@@ -126,6 +126,7 @@ public void TestAreEventTagsValidValidEventTags()
126126
}));
127127
}
128128

129+
[Test]
129130
public void TestAreEventTagsValidInvalidTags()
130131
{
131132
// Some of the tests cases are not applicable because C# is strongly typed.
@@ -136,5 +137,46 @@ public void TestAreEventTagsValidInvalidTags()
136137
}));
137138
}
138139

140+
[Test]
141+
public void TestIsUserAttributeValidWithValidValues()
142+
{
143+
var userAttributes = new UserAttributes
144+
{
145+
{ "device_type", "Android" },
146+
{ "is_firefox", true },
147+
{ "num_users", 15 },
148+
{ "pi_value", 3.14 }
149+
};
150+
151+
foreach (var attribute in userAttributes)
152+
Assert.True(Validator.IsUserAttributeValid(attribute));
153+
}
154+
155+
[Test]
156+
public void TestIsUserAttributeValidWithInvalidValues()
157+
{
158+
var invalidUserAttributes = new UserAttributes
159+
{
160+
{ "objects", new object() },
161+
{ "arrays", new string[] { "a", "b", "c" } }
162+
};
163+
164+
foreach (var attribute in invalidUserAttributes)
165+
Assert.False(Validator.IsUserAttributeValid(attribute));
166+
}
167+
168+
[Test]
169+
public void TestIsUserAttributeValidWithEmptyKeyOrValue()
170+
{
171+
var validUserAttributes = new UserAttributes
172+
{
173+
{ "", "Android" },
174+
{ "integer", 0 },
175+
{ "string", string.Empty }
176+
};
177+
178+
foreach (var attribute in validUserAttributes)
179+
Assert.True(Validator.IsUserAttributeValid(attribute));
180+
}
139181
}
140182
}

OptimizelySDK/Entity/UserAttributes.cs

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,6 @@ namespace OptimizelySDK.Entity
2020
{
2121
public class UserAttributes : Dictionary<string, object>
2222
{
23-
public UserAttributes FilterNullValues(ILogger logger)
24-
{
25-
UserAttributes answer = new UserAttributes();
26-
foreach (KeyValuePair<string, object> pair in this) {
27-
if (pair.Value != null) {
28-
answer[pair.Key] = pair.Value;
29-
} else {
30-
logger.Log(LogLevel.ERROR, string.Format("[UserAttributes] Null value for key {0} removed and will not be sent to results.", pair.Key));
31-
}
32-
}
33-
return answer;
34-
}
23+
3524
}
3625
}

OptimizelySDK/Event/Builder/EventBuilder.cs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ private Dictionary<string, object> GetCommonParams(ProjectConfig config, string
8585
{ "snapshots", new object[0]},
8686
{ "visitor_id", userId },
8787
{ "attributes", new object[0] }
88-
};
88+
};
8989

9090
comonParams[Params.VISITORS] = new object[] { visitor };
9191
comonParams[Params.PROJECT_ID] = config.ProjectId;
@@ -97,20 +97,23 @@ private Dictionary<string, object> GetCommonParams(ProjectConfig config, string
9797

9898
var userFeatures = new List<Dictionary<string, object>>();
9999

100-
foreach (var userAttribute in userAttributes.Where(a => !string.IsNullOrEmpty(a.Key))) {
101-
var attributeId = config.GetAttributeId(userAttribute.Key);
100+
//Omit attribute values that are not supported by the log endpoint.
101+
foreach (var validUserAttribute in userAttributes.Where(attribute => Validator.IsUserAttributeValid(attribute)))
102+
{
103+
var attributeId = config.GetAttributeId(validUserAttribute.Key);
102104
if (!string.IsNullOrEmpty(attributeId)) {
103105
userFeatures.Add(new Dictionary<string, object>
104106
{
105-
{ "entity_id", attributeId },
106-
{ "key", userAttribute.Key },
107-
{ "type", CUSTOM_ATTRIBUTE_FEATURE_TYPE },
108-
{ "value", userAttribute.Value}
109-
});
107+
{ "entity_id", attributeId },
108+
{ "key", validUserAttribute.Key },
109+
{ "type", CUSTOM_ATTRIBUTE_FEATURE_TYPE },
110+
{ "value", validUserAttribute.Value}
111+
});
110112
}
111113
}
112114

113-
if (config.BotFiltering.HasValue) {
115+
if (config.BotFiltering.HasValue)
116+
{
114117
userFeatures.Add(new Dictionary<string, object>
115118
{
116119
{ "entity_id", ControlAttributes.BOT_FILTERING_ATTRIBUTE },
@@ -121,6 +124,7 @@ private Dictionary<string, object> GetCommonParams(ProjectConfig config, string
121124
}
122125

123126
visitor["attributes"] = userFeatures;
127+
124128
return comonParams;
125129
}
126130

OptimizelySDK/Optimizely.cs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,6 @@ public Variation Activate(string experimentKey, string userId, UserAttributes us
195195
return null;
196196
}
197197

198-
if (userAttributes != null) {
199-
userAttributes = userAttributes.FilterNullValues(Logger);
200-
}
201-
202198
SendImpressionEvent(experiment, variation, userId, userAttributes);
203199

204200
return variation;
@@ -270,11 +266,6 @@ public void Track(string eventKey, string userId, UserAttributes userAttributes
270266
if (validExperimentIdToVariationMap.Count > 0)
271267
{
272268

273-
if (userAttributes != null)
274-
{
275-
userAttributes = userAttributes.FilterNullValues(Logger);
276-
}
277-
278269
if (eventTags != null)
279270
{
280271
eventTags = eventTags.FilterNullValues(Logger);

OptimizelySDK/Utils/Validator.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017, Optimizely
2+
* Copyright 2017-2018, 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.
@@ -14,6 +14,7 @@
1414
* limitations under the License.
1515
*/
1616
using OptimizelySDK.Entity;
17+
using System;
1718
using System.Collections.Generic;
1819
using System.Linq;
1920
using Attribute = OptimizelySDK.Entity.Attribute;
@@ -22,7 +23,6 @@ namespace OptimizelySDK.Utils
2223
{
2324
public static class Validator
2425
{
25-
2626
/// <summary>
2727
/// Validate the ProjectConfig JSON
2828
/// </summary>
@@ -87,7 +87,7 @@ public static bool AreEventTagsValid(Dictionary<string, object> eventTags) {
8787
public static bool IsFeatureFlagValid(ProjectConfig projectConfig, FeatureFlag featureFlag)
8888
{
8989
var experimentIds = featureFlag.ExperimentIds;
90-
90+
9191
if (experimentIds == null || experimentIds.Count <= 1)
9292
return true;
9393

@@ -102,6 +102,18 @@ public static bool IsFeatureFlagValid(ProjectConfig projectConfig, FeatureFlag f
102102

103103
return true;
104104
}
105+
106+
/// <summary>
107+
/// Determine if given user attribute is valid.
108+
/// </summary>
109+
/// <param name="attribute">Attribute key and value pair</param>
110+
/// <returns>true if attribute key is not null and value is one of the supported type, false otherwise</returns>
111+
public static bool IsUserAttributeValid(KeyValuePair<string, object> attribute)
112+
{
113+
return (attribute.Key != null) &&
114+
(attribute.Value is int || attribute.Value is string || attribute.Value is double
115+
|| attribute.Value is bool || attribute.Value is float || attribute.Value is long);
116+
}
105117
}
106118
}
107119

0 commit comments

Comments
 (0)