Skip to content

Commit efe28c4

Browse files
authored
Merge pull request #355 from Countly/extended_array_support
feat: add array support to the user properties
2 parents 1267820 + 6ac71b5 commit efe28c4

File tree

5 files changed

+234
-20
lines changed

5 files changed

+234
-20
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## XX.XX.XX
2-
* Added mixed type of immutable lists, arrays to client given segmentations
2+
* ! Minor breaking change ! Unsupported types for user properties will now be omitted, they won't be converted to strings.
3+
4+
* Added support for mixed type of immutable lists, arrays to client given segmentations and user properties.
5+
* Added array, list and JSONArray support to the user properties.
6+
37
* Mitigated issues where:
48
* session was ending regardless of manual control after without merge, not anymore.
59
* session was not starting even if consent is not required and automatic sessions are enabled after without merge, not anymore.

sdk/src/androidTest/java/ly/count/android/sdk/DeviceIdTests.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ public void sessionDurationScenario_1() throws InterruptedException {
303303
assertEquals(5, TestUtils.getCurrentRQ().length);
304304

305305
TestUtils.validateRequest("ff_merge", TestUtils.map("old_device_id", "1234"), 1);
306-
TestUtils.validateRequest("ff_merge", TestUtils.map("user_details", "{\"custom\":{\"prop2\":\"123\",\"prop1\":\"string\",\"prop3\":\"false\"}}"), 2);
306+
TestUtils.validateRequest("ff_merge", TestUtils.map("user_details", "{\"custom\":{\"prop2\":123,\"prop1\":\"string\",\"prop3\":false}}"), 2);
307307
ModuleSessionsTests.validateSessionEndRequest(3, 3, "ff_merge");
308308

309309
Thread.sleep(1000);
@@ -334,9 +334,9 @@ public void sessionDurationScenario_1() throws InterruptedException {
334334

335335
assertEquals(9, TestUtils.getCurrentRQ().length);
336336

337-
TestUtils.validateRequest("ff", TestUtils.map("user_details", "{\"custom\":{\"prop4\":\"[sd]\"}}"), 5);
338-
TestUtils.validateRequest("ff", TestUtils.map("user_details", "{\"custom\":{\"prop6\":\"{key=123}\",\"prop5\":\"{key=value}\",\"prop7\":\"{key=false}\"}}"), 6);
339-
TestUtils.validateRequest("ff", TestUtils.map("user_details", "{\"custom\":{\"prop2\":\"456\",\"prop1\":\"string_a\",\"prop3\":\"true\"}}"), 7);
337+
TestUtils.validateRequest("ff", TestUtils.map("user_details", "{\"custom\":{\"prop4\":[\"sd\"]}}"), 5);
338+
TestUtils.validateRequest("ff", TestUtils.map("user_details", "{\"custom\":{}}"), 6);
339+
TestUtils.validateRequest("ff", TestUtils.map("user_details", "{\"custom\":{\"prop2\":456,\"prop1\":\"string_a\",\"prop3\":true}}"), 7);
340340

341341
TestUtils.validateRequest("ff_merge", TestUtils.map("old_device_id", "ff"), 8);
342342
}

sdk/src/androidTest/java/ly/count/android/sdk/ModuleUserProfileTests.java

Lines changed: 198 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@
22

33
import androidx.test.core.app.ApplicationProvider;
44
import androidx.test.ext.junit.runners.AndroidJUnit4;
5+
import java.util.Arrays;
56
import java.util.HashMap;
7+
import java.util.List;
68
import java.util.Map;
79
import java.util.Random;
810
import java.util.concurrent.ConcurrentHashMap;
11+
import org.json.JSONArray;
912
import org.json.JSONException;
1013
import org.json.JSONObject;
1114
import org.junit.After;
1215
import org.junit.Assert;
1316
import org.junit.Before;
1417
import org.junit.Test;
1518
import org.junit.runner.RunWith;
19+
import org.mockito.Mockito;
20+
import org.mockito.internal.util.collections.Sets;
1621

1722
@RunWith(AndroidJUnit4.class)
1823
public class ModuleUserProfileTests {
@@ -514,7 +519,7 @@ public void internalLimit_setProperties() throws JSONException {
514519
ModuleUserProfile.NAME_KEY, "name",
515520
ModuleUserProfile.PICTURE_KEY, "picture"
516521
), TestUtils.map(
517-
"cu", "23", // because in user profiles, all values are stored as strings
522+
"cu", 23, // because in user profiles, all values are stored as strings
518523
"ha", "black")
519524
);
520525
}
@@ -567,12 +572,11 @@ public void internalLimit_setProperties_maxValueSize() throws JSONException {
567572
ModuleUserProfile.PICTURE_KEY, "picture"
568573
), TestUtils.map(
569574
"custom1", "va", // because in user profiles, all values are stored as strings
570-
"custom2", "23",
575+
"custom2", 23,
571576
"hair", "bl",
572-
"custom3", "1234",
573-
"custom4", "1234.5",
574-
"custom5", "true",
575-
"custom6", obj.toString()) // toString() is called on non-String values
577+
"custom3", 1234,
578+
"custom4", 1234.5,
579+
"custom5", true)
576580
);
577581
}
578582

@@ -613,7 +617,7 @@ public void internalLimit_setProperties_maxSegmentationValues() throws JSONExcep
613617
Countly.sharedInstance().userProfile().setProperties(TestUtils.map("a", "b", "c", "d", "f", 5, "level", 45, "age", 101));
614618
Countly.sharedInstance().userProfile().save();
615619

616-
validateUserProfileRequest(TestUtils.map(), TestUtils.map("f", "5", "age", "101"));
620+
validateUserProfileRequest(TestUtils.map(), TestUtils.map("f", 5, "age", 101));
617621
}
618622

619623
/**
@@ -694,6 +698,193 @@ public void setUserProperties_null() throws JSONException {
694698
validateUserProfileRequest(new HashMap<>(), new HashMap<>());
695699
}
696700

701+
/**
702+
* "setProperties" with Array properties
703+
* Validate that all primitive types arrays are successfully recorded
704+
* And validate that Object arrays are not recorded
705+
* But Generic type of Object array which its values are only primitive types are recorded
706+
*
707+
* @throws JSONException if the JSON is not valid
708+
*/
709+
@Test
710+
public void setProperties_validateSupportedArrays() throws JSONException {
711+
int[] arr = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
712+
boolean[] arrB = { true, false, true, false, true, false, true, false, true, false };
713+
String[] arrS = { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" };
714+
long[] arrL = { Long.MAX_VALUE, Long.MIN_VALUE };
715+
double[] arrD = { Double.MAX_VALUE, Double.MIN_VALUE };
716+
Long[] arrLO = { Long.MAX_VALUE, Long.MIN_VALUE };
717+
Double[] arrDO = { Double.MAX_VALUE, Double.MIN_VALUE };
718+
Boolean[] arrBO = { Boolean.TRUE, Boolean.FALSE };
719+
Integer[] arrIO = { Integer.MAX_VALUE, Integer.MIN_VALUE };
720+
Object[] arrObj = { "1", 1, 1.1d, true, 1.1f, Long.MAX_VALUE };
721+
Object[] arrObjStr = { "1", "1", "1.1d", "true", "1.1f", "Long.MAX_VALUE" };
722+
723+
CountlyConfig countlyConfig = TestUtils.createBaseConfig();
724+
Countly countly = new Countly().init(countlyConfig);
725+
726+
Map<String, Object> userProperties = TestUtils.map(
727+
"arr", arr,
728+
"arrB", arrB,
729+
"arrS", arrS,
730+
"arrL", arrL,
731+
"arrD", arrD,
732+
"arrLO", arrLO,
733+
"arrDO", arrDO,
734+
"arrBO", arrBO,
735+
"arrIO", arrIO,
736+
"arrObj", arrObj,
737+
"arrObjStr", arrObjStr
738+
);
739+
740+
countly.userProfile().setProperties(userProperties);
741+
countly.userProfile().save();
742+
743+
Map<String, Object> expectedCustomProperties = TestUtils.map(
744+
"arr", new JSONArray(arr),
745+
"arrB", new JSONArray(arrB),
746+
"arrS", new JSONArray(arrS),
747+
"arrL", new JSONArray(arrL),
748+
"arrD", new JSONArray(arrD),
749+
"arrLO", new JSONArray(arrLO),
750+
"arrDO", new JSONArray(arrDO),
751+
"arrBO", new JSONArray(arrBO),
752+
"arrIO", new JSONArray(arrIO)
753+
);
754+
755+
validateUserProfileRequest(new HashMap<>(), expectedCustomProperties);
756+
}
757+
758+
/**
759+
* "setProperties" with List properties
760+
* Validate that all primitive types Lists are successfully recorded
761+
* And validate that List of Objects is not recorded
762+
* But Generic type of Object list which its values are only primitive types are recorded
763+
*
764+
* @throws JSONException if the JSON is not valid
765+
*/
766+
@Test
767+
public void setProperties_validateSupportedLists() throws JSONException {
768+
List<Integer> arr = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
769+
List<Boolean> arrB = Arrays.asList(true, false, true, false, true, false, true, false, true, false);
770+
List<String> arrS = Arrays.asList("1", "2", "3", "4", "5", "6", "7", "8", "9", "10");
771+
List<Long> arrLO = Arrays.asList(Long.MAX_VALUE, Long.MIN_VALUE);
772+
List<Double> arrDO = Arrays.asList(Double.MAX_VALUE, Double.MIN_VALUE);
773+
List<Boolean> arrBO = Arrays.asList(Boolean.TRUE, Boolean.FALSE);
774+
List<Integer> arrIO = Arrays.asList(Integer.MAX_VALUE, Integer.MIN_VALUE);
775+
List<Object> arrObj = Arrays.asList("1", 1, 1.1d, true, Long.MAX_VALUE);
776+
List<Object> arrObjStr = Arrays.asList("1", "1", "1.1d", "true", "Long.MAX_VALUE");
777+
778+
CountlyConfig countlyConfig = TestUtils.createBaseConfig();
779+
Countly countly = new Countly().init(countlyConfig);
780+
781+
Map<String, Object> userProperties = TestUtils.map(
782+
"arr", arr,
783+
"arrB", arrB,
784+
"arrS", arrS,
785+
"arrLO", arrLO,
786+
"arrDO", arrDO,
787+
"arrBO", arrBO,
788+
"arrIO", arrIO,
789+
"arrObj", arrObj,
790+
"arrObjStr", arrObjStr
791+
);
792+
793+
countly.userProfile().setProperties(userProperties);
794+
countly.userProfile().save();
795+
796+
Map<String, Object> expectedCustomProperties = TestUtils.map(
797+
"arr", new JSONArray(arr),
798+
"arrB", new JSONArray(arrB),
799+
"arrS", new JSONArray(arrS),
800+
"arrLO", new JSONArray(arrLO),
801+
"arrDO", new JSONArray(arrDO),
802+
"arrBO", new JSONArray(arrBO),
803+
"arrIO", new JSONArray(arrIO),
804+
"arrObjStr", new JSONArray(arrObjStr),
805+
"arrObj", new JSONArray(arrObj)
806+
);
807+
808+
validateUserProfileRequest(new HashMap<>(), expectedCustomProperties);
809+
}
810+
811+
/**
812+
* "setProperties" with JSONArray properties
813+
* Validate that all primitive types JSONArrays are successfully recorded
814+
* And validate and JSONArray of Objects is not recorded
815+
*
816+
* @throws JSONException if the JSON is not valid
817+
*/
818+
@Test
819+
public void setProperties_validateSupportedJSONArrays() throws JSONException {
820+
JSONArray arr = new JSONArray(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
821+
JSONArray arrB = new JSONArray(Arrays.asList(true, false, true, false, true, false, true, false, true, false));
822+
JSONArray arrS = new JSONArray(Arrays.asList("1", "2", "3", "4", "5", "6", "7", "8", "9", "10"));
823+
JSONArray arrL = new JSONArray(Arrays.asList(Long.MAX_VALUE, Long.MIN_VALUE));
824+
JSONArray arrD = new JSONArray(Arrays.asList(Double.MAX_VALUE, Double.MIN_VALUE));
825+
JSONArray arrBO = new JSONArray(Arrays.asList(Boolean.TRUE, Boolean.FALSE));
826+
JSONArray arrIO = new JSONArray(Arrays.asList(Integer.MAX_VALUE, Integer.MIN_VALUE));
827+
JSONArray arrObj = new JSONArray(Arrays.asList("1", 1, 1.1d, true, Long.MAX_VALUE));
828+
829+
CountlyConfig countlyConfig = TestUtils.createBaseConfig();
830+
Countly countly = new Countly().init(countlyConfig);
831+
832+
Map<String, Object> userProperties = TestUtils.map(
833+
"arr", arr,
834+
"arrB", arrB,
835+
"arrS", arrS,
836+
"arrL", arrL,
837+
"arrD", arrD,
838+
"arrBO", arrBO,
839+
"arrIO", arrIO,
840+
"arrObj", arrObj
841+
);
842+
843+
// Record event with the created segmentation
844+
countly.userProfile().setProperties(userProperties);
845+
countly.userProfile().save();
846+
847+
// Prepare expected segmentation with JSONArrays
848+
Map<String, Object> expectedCustomProperties = TestUtils.map(
849+
"arr", arr,
850+
"arrB", arrB,
851+
"arrS", arrS,
852+
"arrL", arrL,
853+
"arrD", arrD,
854+
"arrBO", arrBO,
855+
"arrIO", arrIO,
856+
"arrObj", arrObj
857+
);
858+
859+
validateUserProfileRequest(new HashMap<>(), expectedCustomProperties);
860+
}
861+
862+
/**
863+
* "setProperties" with invalid data types
864+
* Validate that unsupported data types are not recorded
865+
*
866+
* @throws JSONException if the JSON is not valid
867+
*/
868+
@Test
869+
public void setProperties_unsupportedDataTypes() throws JSONException {
870+
CountlyConfig countlyConfig = TestUtils.createBaseConfig();
871+
countlyConfig.setEventQueueSizeToSend(1);
872+
Countly countly = new Countly().init(countlyConfig);
873+
874+
Map<String, Object> userProperties = TestUtils.map(
875+
"a", TestUtils.map(),
876+
"b", TestUtils.json(),
877+
"c", new Object(),
878+
"d", Sets.newSet(),
879+
"e", Mockito.mock(ModuleLog.class)
880+
);
881+
882+
countly.userProfile().setProperties(userProperties);
883+
countly.userProfile().save();
884+
885+
validateUserProfileRequest(new HashMap<>(), new HashMap<>());
886+
}
887+
697888
protected static void validateUserProfileRequest(String deviceId, int idx, int size, Map<String, Object> predefined, Map<String, Object> custom) throws JSONException {
698889
Map<String, String>[] RQ = TestUtils.getCurrentRQ();
699890
Assert.assertEquals(size, RQ.length);

sdk/src/main/java/ly/count/android/sdk/ModuleUserProfile.java

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public class ModuleUserProfile extends ModuleBase {
3232
String picture;
3333
static String picturePath;//protected only for testing
3434
String gender;
35-
Map<String, String> custom;
35+
Map<String, Object> custom;
3636
Map<String, JSONObject> customMods;
3737
int byear = 0;
3838

@@ -219,12 +219,15 @@ void modifyCustomData(String key, Object value, String mod) {
219219
return;
220220
}
221221

222-
Object truncatedValue;
222+
Object valueAdded;
223223
String truncatedKey = UtilsInternalLimits.truncateKeyLength(key, _cly.config_.sdkInternalLimits.maxKeyLength, _cly.L, "[ModuleUserProfile] modifyCustomData");
224224
if (value instanceof String) {
225-
truncatedValue = UtilsInternalLimits.truncateValueSize((String) value, _cly.config_.sdkInternalLimits.maxValueSize, _cly.L, "[ModuleUserProfile] modifyCustomData");
225+
valueAdded = UtilsInternalLimits.truncateValueSize((String) value, _cly.config_.sdkInternalLimits.maxValueSize, _cly.L, "[ModuleUserProfile] modifyCustomData");
226+
} else if (UtilsInternalLimits.isSupportedDataType(value)) {
227+
valueAdded = value;
226228
} else {
227-
truncatedValue = value;
229+
L.w("[ModuleUserProfile] modifyCustomData, provided an unsupported type for key: [" + key + "], value: [" + value + "], type: [" + value.getClass().getSimpleName() + "], mod: [" + mod + "], omitting call");
230+
return;
228231
}
229232

230233
if (customMods == null) {
@@ -234,14 +237,14 @@ void modifyCustomData(String key, Object value, String mod) {
234237
JSONObject ob;
235238
if (!mod.equals("$pull") && !mod.equals("$push") && !mod.equals("$addToSet")) {
236239
ob = new JSONObject();
237-
ob.put(mod, truncatedValue);
240+
ob.put(mod, valueAdded);
238241
} else {
239242
if (customMods.containsKey(truncatedKey)) {
240243
ob = customMods.get(truncatedKey);
241244
} else {
242245
ob = new JSONObject();
243246
}
244-
ob.accumulate(mod, truncatedValue);
247+
ob.accumulate(mod, valueAdded);
245248
}
246249
customMods.put(truncatedKey, ob);
247250
isSynced = false;
@@ -264,7 +267,7 @@ void setPropertiesInternal(@NonNull Map<String, Object> data) {
264267

265268
//todo recheck if in the future these can be <String, Object>
266269
Map<String, String> dataNamedFields = new HashMap<>();
267-
Map<String, String> dataCustomFields = new HashMap<>();
270+
Map<String, Object> dataCustomFields = new HashMap<>();
268271

269272
for (Map.Entry<String, Object> entry : data.entrySet()) {
270273
String key = entry.getKey();
@@ -297,7 +300,11 @@ void setPropertiesInternal(@NonNull Map<String, Object> data) {
297300

298301
if (!isNamed) {
299302
String truncatedKey = UtilsInternalLimits.truncateKeyLength(key, _cly.config_.sdkInternalLimits.maxKeyLength, _cly.L, "[ModuleUserProfile] setPropertiesInternal");
300-
dataCustomFields.put(truncatedKey, value.toString());
303+
if (UtilsInternalLimits.isSupportedDataType(value)) {
304+
dataCustomFields.put(truncatedKey, value);
305+
} else {
306+
L.w("[ModuleUserProfile] setPropertiesInternal, provided an unsupported type for key: [" + key + "], value: [" + value + "], type: [" + value.getClass().getSimpleName() + "], omitting call");
307+
}
301308
}
302309
}
303310

sdk/src/main/java/ly/count/android/sdk/UtilsInternalLimits.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,18 @@ private static boolean isSupportedDataTypeBasic(@Nullable Object value) {
351351
return value instanceof String || value instanceof Integer || value instanceof Double || value instanceof Boolean || value instanceof Float || value instanceof Long;
352352
}
353353

354+
/**
355+
* This function currently validates below segmentations:
356+
* - Event segmentations (custom ones not internal keys)
357+
* - Crash segmentations
358+
* - View segmentations
359+
* - User profile custom properties
360+
* - User profile custom properties modifiers
361+
* - Feedback widgets' results
362+
*
363+
* @param value to check
364+
* @return true if the value is a supported data type
365+
*/
354366
static boolean isSupportedDataType(@Nullable Object value) {
355367
if (isSupportedDataTypeBasic(value)) {
356368
return true;

0 commit comments

Comments
 (0)