Skip to content

Commit 1bb574a

Browse files
mnoman09mikeproeng37
authored andcommitted
fix(attributes): filters out attributes with null values (#214)
1 parent 1abfbb7 commit 1bb574a

File tree

2 files changed

+208
-8
lines changed

2 files changed

+208
-8
lines changed

core-api/src/main/java/com/optimizely/ab/event/internal/EventFactory.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,18 @@ private List<Attribute> buildAttributeList(ProjectConfig projectConfig, Map<Stri
168168
List<Attribute> attributesList = new ArrayList<Attribute>();
169169

170170
for (Map.Entry<String, ?> entry : attributes.entrySet()) {
171+
// Filter down to the types of values we're allowed to track.
172+
// Don't allow Longs, BigIntegers, or BigDecimals - they /can/ theoretically be serialized as JSON numbers
173+
// but may take on values that can't be faithfully parsed by the backend.
174+
// https://developers.optimizely.com/x/events/api/#Attribute
175+
if (entry.getValue() == null ||
176+
!((entry.getValue() instanceof String) ||
177+
(entry.getValue() instanceof Integer) ||
178+
(entry.getValue() instanceof Double) ||
179+
(entry.getValue() instanceof Boolean))) {
180+
continue;
181+
}
182+
171183
String attributeId = projectConfig.getAttributeId(projectConfig, entry.getKey());
172184
if(attributeId == null) {
173185
continue;

core-api/src/test/java/com/optimizely/ab/event/internal/EventFactoryTest.java

Lines changed: 196 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,15 @@
4040

4141
import javax.annotation.Nullable;
4242
import java.io.IOException;
43-
import java.util.ArrayList;
44-
import java.util.Arrays;
45-
import java.util.Collection;
46-
import java.util.Collections;
47-
import java.util.HashMap;
48-
import java.util.List;
49-
import java.util.Map;
43+
import java.math.BigDecimal;
44+
import java.math.BigInteger;
45+
import java.util.*;
5046

5147
import static com.optimizely.ab.config.ProjectConfigTestUtils.validProjectConfigV2;
5248
import static com.optimizely.ab.config.ProjectConfigTestUtils.validProjectConfigV4;
5349
import static com.optimizely.ab.config.ValidProjectConfigV4.*;
5450
import static junit.framework.Assert.assertNotNull;
51+
import static junit.framework.Assert.assertNotSame;
5552
import static org.hamcrest.CoreMatchers.is;
5653
import static org.hamcrest.Matchers.closeTo;
5754
import static org.hamcrest.Matchers.containsInAnyOrder;
@@ -60,6 +57,7 @@
6057
import static org.junit.Assert.assertNull;
6158
import static org.junit.Assert.assertThat;
6259
import static org.junit.Assert.assertTrue;
60+
import static org.junit.Assume.assumeTrue;
6361
import static org.mockito.Mockito.mock;
6462
import static org.mockito.Mockito.never;
6563
import static org.mockito.Mockito.spy;
@@ -249,6 +247,196 @@ public void createImpressionEventIgnoresUnknownAttributes() throws Exception {
249247
}
250248
}
251249

250+
/**
251+
* Verify that passing through an list value attribute causes that attribute to be ignored, rather than
252+
* causing an exception to be thrown and passing only the valid attributes.
253+
*/
254+
@Test
255+
public void createConversionEventIgnoresInvalidAndAcceptsValidAttributes() {
256+
assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString()));
257+
258+
Bucketer mockBucketAlgorithm = mock(Bucketer.class);
259+
EventType eventType = validProjectConfig.getEventTypes().get(0);
260+
261+
List<Experiment> allExperiments = validProjectConfig.getExperiments();
262+
263+
// Bucket to the first variation for all experiments. However, only a subset of the experiments will actually
264+
// call the bucket function.
265+
for (Experiment experiment : allExperiments) {
266+
when(mockBucketAlgorithm.bucket(experiment, userId))
267+
.thenReturn(experiment.getVariations().get(0));
268+
}
269+
Attribute attribute1 = validProjectConfig.getAttributes().get(0);
270+
Attribute attribute2 = validProjectConfig.getAttributes().get(1);
271+
Attribute doubleAttribute = validProjectConfig.getAttributes().get(5);
272+
Attribute integerAttribute = validProjectConfig.getAttributes().get(4);
273+
Attribute boolAttribute = validProjectConfig.getAttributes().get(3);
274+
275+
BigInteger bigInteger = new BigInteger("12323");
276+
BigDecimal bigDecimal = new BigDecimal("123");
277+
double validDoubleAttribute = 13.1;
278+
int validIntegerAttribute = 12;
279+
boolean validBoolAttribute = true;
280+
281+
Map<String, Object> eventTagMap = new HashMap<>();
282+
eventTagMap.put("boolean_param", false);
283+
eventTagMap.put("string_param", "123");
284+
285+
HashMap<String, Object> attributes = new HashMap<>();
286+
attributes.put(attribute1.getKey(), bigInteger);
287+
attributes.put(attribute2.getKey(), bigDecimal);
288+
attributes.put(doubleAttribute.getKey(), validDoubleAttribute);
289+
attributes.put(integerAttribute.getKey(), validIntegerAttribute);
290+
attributes.put(boolAttribute.getKey(), validBoolAttribute);
291+
292+
DecisionService decisionService = new DecisionService(
293+
mockBucketAlgorithm,
294+
mock(ErrorHandler.class),
295+
validProjectConfig,
296+
mock(UserProfileService.class)
297+
);
298+
Map<Experiment, Variation> experimentVariationMap = createExperimentVariationMap(
299+
validProjectConfig,
300+
decisionService,
301+
eventType.getKey(),
302+
userId,
303+
attributes);
304+
LogEvent conversionEvent = factory.createConversionEvent(
305+
validProjectConfig,
306+
experimentVariationMap,
307+
userId,
308+
eventType.getId(),
309+
eventType.getKey(),
310+
attributes,
311+
eventTagMap);
312+
313+
EventBatch impression = gson.fromJson(conversionEvent.getBody(), EventBatch.class);
314+
315+
//Check valid attributes are getting passed.
316+
assertEquals(impression.getVisitors().get(0).getAttributes().get(0).getKey(), boolAttribute.getKey());
317+
assertEquals(impression.getVisitors().get(0).getAttributes().get(0).getValue(), validBoolAttribute);
318+
319+
assertEquals(impression.getVisitors().get(0).getAttributes().get(1).getKey(), doubleAttribute.getKey());
320+
assertEquals(impression.getVisitors().get(0).getAttributes().get(1).getValue(), validDoubleAttribute);
321+
322+
assertEquals(impression.getVisitors().get(0).getAttributes().get(2).getKey(), integerAttribute.getKey());
323+
assertEquals((int) ((double) impression.getVisitors().get(0).getAttributes().get(2).getValue()), validIntegerAttribute);
324+
325+
// verify that no Feature is created for attribute.getKey() -> invalidAttribute
326+
for (com.optimizely.ab.event.internal.payload.Attribute feature : impression.getVisitors().get(0).getAttributes()) {
327+
assertNotSame(feature.getKey(), attribute1.getKey());
328+
assertNotSame(feature.getValue(), bigInteger);
329+
assertNotSame(feature.getKey(), attribute2.getKey());
330+
assertNotSame(feature.getValue(), bigDecimal);
331+
}
332+
}
333+
334+
/**
335+
* Verify that passing through an list value attribute causes that attribute to be ignored, rather than
336+
* causing an exception to be thrown.
337+
*/
338+
@Test
339+
public void createImpressionEventIgnoresInvalidAttributes() {
340+
assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString()));
341+
// use the "valid" project config and its associated experiment, variation, and attributes
342+
ProjectConfig projectConfig = validProjectConfig;
343+
Experiment activatedExperiment = projectConfig.getExperiments().get(0);
344+
Variation bucketedVariation = activatedExperiment.getVariations().get(0);
345+
Attribute attribute1 = validProjectConfig.getAttributes().get(0);
346+
Attribute attribute2 = validProjectConfig.getAttributes().get(1);
347+
BigInteger bigInteger = new BigInteger("12323");
348+
BigDecimal bigDecimal = new BigDecimal("123");
349+
350+
HashMap<String, Object> attributes = new HashMap<>();
351+
attributes.put(attribute1.getKey(), bigInteger);
352+
attributes.put(attribute2.getKey(), bigDecimal);
353+
354+
LogEvent impressionEvent =
355+
factory.createImpressionEvent(projectConfig, activatedExperiment, bucketedVariation, "userId",
356+
attributes);
357+
358+
EventBatch impression = gson.fromJson(impressionEvent.getBody(), EventBatch.class);
359+
360+
// verify that no Feature is created for attribute.getKey() -> invalidAttribute
361+
for (com.optimizely.ab.event.internal.payload.Attribute feature : impression.getVisitors().get(0).getAttributes()) {
362+
assertNotSame(feature.getKey(), attribute1.getKey());
363+
assertNotSame(feature.getValue(), bigInteger);
364+
assertNotSame(feature.getKey(), attribute2.getKey());
365+
assertNotSame(feature.getValue(), bigDecimal);
366+
}
367+
}
368+
369+
/**
370+
* Verify that Integer, Decimal, Bool and String variables are allowed to pass.
371+
*/
372+
@Test
373+
public void createImpressionEventWithIntegerDecimalBoolAndStringAttributes() {
374+
assumeTrue(datafileVersion >= Integer.parseInt(ProjectConfig.Version.V4.toString()));
375+
// use the "valid" project config and its associated experiment, variation, and attributes
376+
ProjectConfig projectConfig = validProjectConfig;
377+
Experiment activatedExperiment = projectConfig.getExperiments().get(0);
378+
Variation bucketedVariation = activatedExperiment.getVariations().get(0);
379+
Attribute doubleAttribute = validProjectConfig.getAttributes().get(5);
380+
Attribute integerAttribute = validProjectConfig.getAttributes().get(4);
381+
Attribute boolAttribute = validProjectConfig.getAttributes().get(3);
382+
Attribute stringAttribute = validProjectConfig.getAttributes().get(0);
383+
double validDoubleAttribute = 13.1;
384+
int validIntegerAttribute = 12;
385+
boolean validBoolAttribute = true;
386+
String validStringAttribute = "grayfindor";
387+
388+
HashMap<String, Object> attributes = new HashMap<>();
389+
attributes.put(doubleAttribute.getKey(), validDoubleAttribute);
390+
attributes.put(integerAttribute.getKey(), validIntegerAttribute);
391+
attributes.put(boolAttribute.getKey(), validBoolAttribute);
392+
attributes.put(stringAttribute.getKey(), validStringAttribute);
393+
394+
LogEvent impressionEvent =
395+
factory.createImpressionEvent(projectConfig, activatedExperiment, bucketedVariation, "userId",
396+
attributes);
397+
398+
EventBatch impression = gson.fromJson(impressionEvent.getBody(), EventBatch.class);
399+
400+
401+
assertEquals(impression.getVisitors().get(0).getAttributes().get(0).getKey(), boolAttribute.getKey());
402+
assertEquals(impression.getVisitors().get(0).getAttributes().get(0).getValue(), validBoolAttribute);
403+
404+
assertEquals(impression.getVisitors().get(0).getAttributes().get(1).getKey(), doubleAttribute.getKey());
405+
assertEquals(impression.getVisitors().get(0).getAttributes().get(1).getValue(), validDoubleAttribute);
406+
407+
assertEquals(impression.getVisitors().get(0).getAttributes().get(2).getKey(), integerAttribute.getKey());
408+
assertEquals((int) ((double) impression.getVisitors().get(0).getAttributes().get(2).getValue()), validIntegerAttribute);
409+
410+
assertEquals(impression.getVisitors().get(0).getAttributes().get(3).getKey(), stringAttribute.getKey());
411+
assertEquals(impression.getVisitors().get(0).getAttributes().get(3).getValue(), validStringAttribute);
412+
413+
}
414+
415+
/**
416+
* Verify that passing through an null value attribute causes that attribute to be ignored, rather than
417+
* causing an exception to be thrown.
418+
*/
419+
@Test
420+
public void createImpressionEventIgnoresNullAttributes() {
421+
// use the "valid" project config and its associated experiment, variation, and attributes
422+
ProjectConfig projectConfig = validProjectConfig;
423+
Experiment activatedExperiment = projectConfig.getExperiments().get(0);
424+
Variation bucketedVariation = activatedExperiment.getVariations().get(0);
425+
Attribute attribute = validProjectConfig.getAttributes().get(0);
426+
427+
LogEvent impressionEvent =
428+
factory.createImpressionEvent(projectConfig, activatedExperiment, bucketedVariation, "userId",
429+
Collections.singletonMap(attribute.getKey(), null));
430+
431+
EventBatch impression = gson.fromJson(impressionEvent.getBody(), EventBatch.class);
432+
433+
// verify that no Feature is created for attribute.getKey() -> null
434+
for (com.optimizely.ab.event.internal.payload.Attribute feature : impression.getVisitors().get(0).getAttributes()) {
435+
assertNotSame(feature.getKey(), attribute.getKey());
436+
assertNotSame(feature.getValue(), null);
437+
}
438+
}
439+
252440
/**
253441
* Verify that supplying {@link EventFactory} with a custom client engine and client version results in impression
254442
* events being sent with the overriden values.
@@ -946,7 +1134,7 @@ public static Map<Experiment, Variation> createExperimentVariationMap(ProjectCon
9461134
DecisionService decisionService,
9471135
String eventName,
9481136
String userId,
949-
@Nullable Map<String, String> attributes) {
1137+
@Nullable Map<String, ?> attributes) {
9501138

9511139
List<Experiment> eventExperiments = projectConfig.getExperimentsForEventKey(eventName);
9521140
Map<Experiment, Variation> experimentVariationMap = new HashMap<Experiment, Variation>(eventExperiments.size());

0 commit comments

Comments
 (0)