From aed4a425bdb7388977e3acb56b965aa84d54ecf3 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 2 Dec 2025 14:49:42 +0300 Subject: [PATCH 01/10] feat: user profile interface --- lib/countly.js | 160 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 141 insertions(+), 19 deletions(-) diff --git a/lib/countly.js b/lib/countly.js index 0963e70..d291927 100644 --- a/lib/countly.js +++ b/lib/countly.js @@ -869,29 +869,151 @@ Countly.Bulk = Bulk; * - pull, to remove value from array property * - addToSet, creates an array property, if property does not exist, and adds unique value to array, only if it does not yet exist in array ************************* */ - var customData = {}; - var change_custom_property = function(key, value, mod) { - key = cc.truncateSingleValue(key, Countly.maxKeyLength, "change_custom_property", Countly.debug); - value = cc.truncateSingleValue(value, Countly.maxValueSize, "change_custom_property", Countly.debug); - if (key === '__proto__' || key === 'constructor' || key === 'prototype') { - cc.log(cc.logLevelEnums.ERROR, "change_custom_property, Provided key is not allowed."); - return; - } + var userProperties = { custom: {} }; - if (Countly.check_consent("users")) { - if (!customData[key]) { - customData[key] = {}; - } - if (mod === "$push" || mod === "$pull" || mod === "$addToSet") { - if (!customData[key][mod]) { - customData[key][mod] = []; + /** + * Control user profile properties. Don't forget to call save after + * @namespace Countly.userProfile + * @name Countly.userProfile + */ + Countly.userProfile = { + /** + * Sets user's custom property value + * @param {Object} user - Countly {@link UserDetails} object + * @param {string=} user.name - user's full name + * @param {string=} user.username - user's username or nickname + * @param {string=} user.email - user's email address + * @param {string=} user.organization - user's organization or company + * @param {string=} user.phone - user's phone number + * @param {string=} user.picture - url to user's picture + * @param {string=} user.picturePath - local path to user's picture, if 'user.picture' is set this will be ignored + * @param {string=} user.gender - M value for male and F value for female + * @param {number=} user.byear - user's birth year used to calculate current age + * @param {Object=} user.custom - object with custom key value properties you want to save with user + */ + set_properties(user) { + cc.log(cc.logLevelEnums.INFO, "set_properties, Adding user details: ", user); + if (Countly.check_consent("users")) { + var props = ["name", "username", "email", "organization", "phone", "picture", "picturePath", "gender", "byear", "custom"]; + var extractedUserParams = cc.getProperties(user, props); + for (var p in extractedUserParams) { + if (p === "custom") { + extractedUserParams.custom = cc.truncateObject(extractedUserParams.custom, Countly.maxKeyLength, Countly.maxValueSize, Countly.maxSegmentationValues, "set_properties"); + for (var c in extractedUserParams.custom) { + userProperties.custom[c] = extractedUserParams.custom[c]; + } + } + else if (p !== "picture" && p === "picturePath") { + userProperties[p] = extractedUserParams[p]; + } + else { + userProperties[p] = cc.truncateSingleValue(extractedUserParams[p], p !== "picture" ? Countly.maxValueSize : 4096, "set_properties", Countly.debug); + } } - customData[key][mod].push(value); } - else { - customData[key][mod] = value; + }, + /** + * Sets user's custom property value + * @param {string} key - name of the property to attach to user + * @param {string|number} value - value to store under provided property + * */ + set_property(key, value) { + Countly.userProfile.set_properties({ custom: { [key]: value } }); + }, + /** + * Sets user's custom property value only if it was not set before + * @param {string} key - name of the property to attach to user + * @param {string|number} value - value to store under provided property + * */ + set_once(key, value) { + change_custom_property(key, value, "$setOnce"); + }, + /** + * Unset's/delete's user's custom property + * @param {string} key - name of the property to delete + * */ + unset(key) { + delete userProperties.custom[key]; + }, + /** + * Increment value under the key of this user's custom properties by one + * @param {string} key - name of the property to attach to user + * */ + increment(key) { + change_custom_property(key, 1, "$inc"); + }, + /** + * Increment value under the key of this user's custom properties by provided value + * @param {string} key - name of the property to attach to user + * @param {number} value - value by which to increment server value + * */ + increment_by(key, value) { + change_custom_property(key, value, "$inc"); + }, + /** + * Multiply value under the key of this user's custom properties by provided value + * @param {string} key - name of the property to attach to user + * @param {number} value - value by which to multiply server value + * */ + multiply(key, value) { + change_custom_property(key, value, "$mul"); + }, + /** + * Save maximal value under the key of this user's custom properties + * @param {string} key - name of the property to attach to user + * @param {number} value - value which to compare to server's value and store maximal value of both provided + * */ + max(key, value) { + change_custom_property(key, value, "$max"); + }, + /** + * Save minimal value under the key of this user's custom properties + * @param {string} key - name of the property to attach to user + * @param {number} value - value which to compare to server's value and store minimal value of both provided + * */ + min(key, value) { + change_custom_property(key, value, "$min"); + }, + /** + * Add value to array under the key of this user's custom properties. If property is not an array, it will be converted to array + * @param {string} key - name of the property to attach to user + * @param {string|number} value - value which to add to array + * */ + push(key, value) { + change_custom_property(key, value, "$push"); + }, + /** + * Add value to array under the key of this user's custom properties, storing only unique values. If property is not an array, it will be converted to array + * @param {string} key - name of the property to attach to user + * @param {string|number} value - value which to add to array + * */ + push_unique(key, value) { + change_custom_property(key, value, "$addToSet"); + }, + /** + * Remove value from array under the key of this user's custom properties + * @param {string} key - name of the property + * @param {string|number} value - value which to remove from array + * */ + pull(key, value) { + change_custom_property(key, value, "$pull"); + }, + /** + * Save changes made to user's custom properties object and send them to server + * */ + save() { + if (Countly.check_consent("users")) { + if (Object.keys(userProperties).length < 2 && Object.keys(userProperties.custom).length === 0) { + cc.log(cc.logLevelEnums.INFO, "save, No user properties to save."); + return; + } + if (userProperties.custom && Object.keys(userProperties.custom).length === 0) { + delete userProperties.custom; + } + toRequestQueue({ user_details: JSON.stringify(userProperties) }); } - } + userProperties = { custom: {} }; + }, }; /** From 89d006af0a3ccb47ad465a087c97ea799bef85dd Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 2 Dec 2025 14:50:17 +0300 Subject: [PATCH 02/10] feat: caching of customs with predefineds --- lib/countly.js | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/lib/countly.js b/lib/countly.js index d291927..87f282a 100644 --- a/lib/countly.js +++ b/lib/countly.js @@ -836,24 +836,31 @@ Countly.Bulk = Bulk; * @param {Object=} user.custom - object with custom key value properties you want to save with user * */ Countly.user_details = function(user) { - cc.log(cc.logLevelEnums.INFO, "user_details, Adding user details: ", user); + Countly.userProfile.set_properties(user); + Countly.userProfile.save(); + }; + + var change_custom_property = function(key, value, mod) { + key = cc.truncateSingleValue(key, Countly.maxKeyLength, "change_custom_property", Countly.debug); + value = cc.truncateSingleValue(value, Countly.maxValueSize, "change_custom_property", Countly.debug); + if (key === '__proto__' || key === 'constructor' || key === 'prototype') { + cc.log(cc.logLevelEnums.ERROR, "change_custom_property, Provided key is not allowed."); + return; + } + if (Countly.check_consent("users")) { - var props = ["name", "username", "email", "organization", "phone", "picture", "gender", "byear", "custom"]; - user.name = cc.truncateSingleValue(user.name, Countly.maxValueSize, "user_details", Countly.debug); - user.username = cc.truncateSingleValue(user.username, Countly.maxValueSize, "user_details", Countly.debug); - user.email = cc.truncateSingleValue(user.email, Countly.maxValueSize, "user_details", Countly.debug); - user.organization = cc.truncateSingleValue(user.organization, Countly.maxValueSize, "user_details", Countly.debug); - user.phone = cc.truncateSingleValue(user.phone, Countly.maxValueSize, "user_details", Countly.debug); - user.picture = cc.truncateSingleValue(user.picture, 4096, "user_details", Countly.debug); - user.gender = cc.truncateSingleValue(user.gender, Countly.maxValueSize, "user_details", Countly.debug); - user.byear = cc.truncateSingleValue(user.byear, Countly.maxValueSize, "user_details", Countly.debug); - user.custom = cc.truncateObject(user.custom, Countly.maxKeyLength, Countly.maxValueSize, Countly.maxSegmentationValues, "user_details"); - var request = { user_details: JSON.stringify(cc.getProperties(user, props)) }; - if (!user.picture && user.picturePath) { - cc.log(cc.logLevelEnums.INFO, "user_details, Picture is not set but picturePath is set, will try to upload picture from path."); - request.picturePath = user.picturePath; - } - toRequestQueue(request); + if (!userProperties.custom[key]) { + userProperties.custom[key] = {}; + } + if (mod === "$push" || mod === "$pull" || mod === "$addToSet") { + if (!userProperties.custom[key][mod]) { + userProperties.custom[key][mod] = []; + } + userProperties.custom[key][mod].push(value); + } + else { + userProperties.custom[key][mod] = value; + } } }; From 43bfa85a871fce822563da52d2c562be9c161558 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 2 Dec 2025 14:51:01 +0300 Subject: [PATCH 03/10] feat: use new interface in userData --- lib/countly.js | 43 +++++++++++-------------------------------- 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/lib/countly.js b/lib/countly.js index 87f282a..7852850 100644 --- a/lib/countly.js +++ b/lib/countly.js @@ -1054,25 +1054,21 @@ Countly.Bulk = Bulk; * @param {string|number} value - value to store under provided property * */ set_once(key, value) { - key = cc.truncateSingleValue(key, Countly.maxKeyLength, "set_once", Countly.debug); - value = cc.truncateSingleValue(value, Countly.maxValueSize, "set_once", Countly.debug); - change_custom_property(key, value, "$setOnce"); + Countly.userProfile.set_once(key, value); }, /** * Unset's/delete's user's custom property * @param {string} key - name of the property to delete * */ unset(key) { - key = cc.truncateSingleValue(key, Countly.maxKeyLength, "unset", Countly.debug); - customData[key] = ""; + Countly.userProfile.unset(key); }, /** * Increment value under the key of this user's custom properties by one * @param {string} key - name of the property to attach to user * */ increment(key) { - key = cc.truncateSingleValue(key, Countly.maxKeyLength, "increment", Countly.debug); - change_custom_property(key, 1, "$inc"); + Countly.userProfile.increment(key); }, /** * Increment value under the key of this user's custom properties by provided value @@ -1080,9 +1076,7 @@ Countly.Bulk = Bulk; * @param {number} value - value by which to increment server value * */ increment_by(key, value) { - key = cc.truncateSingleValue(key, Countly.maxKeyLength, "increment_by", Countly.debug); - value = cc.truncateSingleValue(value, Countly.maxValueSize, "increment_by", Countly.debug); - change_custom_property(key, value, "$inc"); + Countly.userProfile.increment_by(key, value); }, /** * Multiply value under the key of this user's custom properties by provided value @@ -1090,9 +1084,7 @@ Countly.Bulk = Bulk; * @param {number} value - value by which to multiply server value * */ multiply(key, value) { - key = cc.truncateSingleValue(key, Countly.maxKeyLength, "multiply", Countly.debug); - value = cc.truncateSingleValue(value, Countly.maxValueSize, "multiply", Countly.debug); - change_custom_property(key, value, "$mul"); + Countly.userProfile.multiply(key, value); }, /** * Save maximal value under the key of this user's custom properties @@ -1100,9 +1092,7 @@ Countly.Bulk = Bulk; * @param {number} value - value which to compare to server's value and store maximal value of both provided * */ max(key, value) { - key = cc.truncateSingleValue(key, Countly.maxKeyLength, "max", Countly.debug); - value = cc.truncateSingleValue(value, Countly.maxValueSize, "max", Countly.debug); - change_custom_property(key, value, "$max"); + Countly.userProfile.max(key, value); }, /** * Save minimal value under the key of this user's custom properties @@ -1110,9 +1100,7 @@ Countly.Bulk = Bulk; * @param {number} value - value which to compare to server's value and store minimal value of both provided * */ min(key, value) { - key = cc.truncateSingleValue(key, Countly.maxKeyLength, "min", Countly.debug); - value = cc.truncateSingleValue(value, Countly.maxValueSize, "min", Countly.debug); - change_custom_property(key, value, "$min"); + Countly.userProfile.min(key, value); }, /** * Add value to array under the key of this user's custom properties. If property is not an array, it will be converted to array @@ -1120,9 +1108,7 @@ Countly.Bulk = Bulk; * @param {string|number} value - value which to add to array * */ push(key, value) { - key = cc.truncateSingleValue(key, Countly.maxKeyLength, "push", Countly.debug); - value = cc.truncateSingleValue(value, Countly.maxValueSize, "push", Countly.debug); - change_custom_property(key, value, "$push"); + Countly.userProfile.push(key, value); }, /** * Add value to array under the key of this user's custom properties, storing only unique values. If property is not an array, it will be converted to array @@ -1130,9 +1116,7 @@ Countly.Bulk = Bulk; * @param {string|number} value - value which to add to array * */ push_unique(key, value) { - key = cc.truncateSingleValue(key, Countly.maxKeyLength, "push_unique", Countly.debug); - value = cc.truncateSingleValue(value, Countly.maxValueSize, "push_unique", Countly.debug); - change_custom_property(key, value, "$addToSet"); + Countly.userProfile.push_unique(key, value); }, /** * Remove value from array under the key of this user's custom properties @@ -1140,18 +1124,13 @@ Countly.Bulk = Bulk; * @param {string|number} value - value which to remove from array * */ pull(key, value) { - key = cc.truncateSingleValue(key, Countly.maxKeyLength, "pull", Countly.debug); - value = cc.truncateSingleValue(value, Countly.maxValueSize, "pull", Countly.debug); - change_custom_property(key, value, "$pull"); + Countly.userProfile.pull(key, value); }, /** * Save changes made to user's custom properties object and send them to server * */ save() { - if (Countly.check_consent("users")) { - toRequestQueue({ user_details: JSON.stringify({ custom: customData }) }); - } - customData = {}; + Countly.userProfile.save(); }, }; From 88bc74da85ddddf12edba927b037741ed84cdbb7 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 2 Dec 2025 14:51:42 +0300 Subject: [PATCH 04/10] feat: deprecate others --- lib/countly.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/countly.js b/lib/countly.js index 7852850..f26509b 100644 --- a/lib/countly.js +++ b/lib/countly.js @@ -834,6 +834,7 @@ Countly.Bulk = Bulk; * @param {string=} user.gender - M value for male and F value for female * @param {number=} user.byear - user's birth year used to calculate current age * @param {Object=} user.custom - object with custom key value properties you want to save with user + * @deprecated use {@link Countly.userProfile.set_properties} instead then call {@link Countly.userProfile.save} * */ Countly.user_details = function(user) { Countly.userProfile.set_properties(user); @@ -1027,6 +1028,7 @@ Countly.Bulk = Bulk; * Control user related custom properties. Don't forget to call save after finishing manipulation of custom data * @namespace Countly.userData * @name Countly.userData + * @deprecated use {@link Countly.userProfile} instead * @example * //set custom key value property * Countly.userData.set("twitter", "ar2rsawseen"); @@ -1042,16 +1044,16 @@ Countly.Bulk = Bulk; * Sets user's custom property value * @param {string} key - name of the property to attach to user * @param {string|number} value - value to store under provided property + * @deprecated use {@link Countly.userProfile.set_property} instead * */ set(key, value) { - key = cc.truncateSingleValue(key, Countly.maxKeyLength, "set", Countly.debug); - value = cc.truncateSingleValue(value, Countly.maxValueSize, "set", Countly.debug); - customData[key] = value; + Countly.userProfile.set_property(key, value); }, /** * Sets user's custom property value only if it was not set before * @param {string} key - name of the property to attach to user * @param {string|number} value - value to store under provided property + * @deprecated use {@link Countly.userProfile.set_once} instead * */ set_once(key, value) { Countly.userProfile.set_once(key, value); @@ -1059,6 +1061,7 @@ Countly.Bulk = Bulk; /** * Unset's/delete's user's custom property * @param {string} key - name of the property to delete + * @deprecated use {@link Countly.userProfile.unset} instead * */ unset(key) { Countly.userProfile.unset(key); @@ -1066,6 +1069,7 @@ Countly.Bulk = Bulk; /** * Increment value under the key of this user's custom properties by one * @param {string} key - name of the property to attach to user + * @deprecated use {@link Countly.userProfile.increment} instead * */ increment(key) { Countly.userProfile.increment(key); @@ -1074,6 +1078,7 @@ Countly.Bulk = Bulk; * Increment value under the key of this user's custom properties by provided value * @param {string} key - name of the property to attach to user * @param {number} value - value by which to increment server value + * @deprecated use {@link Countly.userProfile.increment_by} instead * */ increment_by(key, value) { Countly.userProfile.increment_by(key, value); @@ -1082,6 +1087,7 @@ Countly.Bulk = Bulk; * Multiply value under the key of this user's custom properties by provided value * @param {string} key - name of the property to attach to user * @param {number} value - value by which to multiply server value + * @deprecated use {@link Countly.userProfile.multiply} instead * */ multiply(key, value) { Countly.userProfile.multiply(key, value); @@ -1090,6 +1096,7 @@ Countly.Bulk = Bulk; * Save maximal value under the key of this user's custom properties * @param {string} key - name of the property to attach to user * @param {number} value - value which to compare to server's value and store maximal value of both provided + * @deprecated use {@link Countly.userProfile.max} instead * */ max(key, value) { Countly.userProfile.max(key, value); @@ -1098,6 +1105,7 @@ Countly.Bulk = Bulk; * Save minimal value under the key of this user's custom properties * @param {string} key - name of the property to attach to user * @param {number} value - value which to compare to server's value and store minimal value of both provided + * @deprecated use {@link Countly.userProfile.min} instead * */ min(key, value) { Countly.userProfile.min(key, value); @@ -1106,6 +1114,7 @@ Countly.Bulk = Bulk; * Add value to array under the key of this user's custom properties. If property is not an array, it will be converted to array * @param {string} key - name of the property to attach to user * @param {string|number} value - value which to add to array + * @deprecated use {@link Countly.userProfile.push} instead * */ push(key, value) { Countly.userProfile.push(key, value); @@ -1114,6 +1123,7 @@ Countly.Bulk = Bulk; * Add value to array under the key of this user's custom properties, storing only unique values. If property is not an array, it will be converted to array * @param {string} key - name of the property to attach to user * @param {string|number} value - value which to add to array + * @deprecated use {@link Countly.userProfile.push_unique} instead * */ push_unique(key, value) { Countly.userProfile.push_unique(key, value); @@ -1122,12 +1132,14 @@ Countly.Bulk = Bulk; * Remove value from array under the key of this user's custom properties * @param {string} key - name of the property * @param {string|number} value - value which to remove from array + * @deprecated use {@link Countly.userProfile.pull} instead * */ pull(key, value) { Countly.userProfile.pull(key, value); }, /** * Save changes made to user's custom properties object and send them to server + * @deprecated use {@link Countly.userProfile.save} instead * */ save() { Countly.userProfile.save(); From 1ee347c1d902e3ace2e3d8fb858777b62d31b168 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 2 Dec 2025 14:52:08 +0300 Subject: [PATCH 05/10] feat: legacy and new usage tests --- test/tests_user_details.js | 40 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/test/tests_user_details.js b/test/tests_user_details.js index 11b57b2..323bc00 100644 --- a/test/tests_user_details.js +++ b/test/tests_user_details.js @@ -8,14 +8,50 @@ describe("User details tests", () => { beforeEach(async() => { await hp.clearStorage(); }); - it("Record and validate all user details", (done) => { + it("Record and validate all user details - Legacy", (done) => { Countly.init({ app_key: "YOUR_APP_KEY", url: "https://try.count.ly", }); var userDetailObj = testUtils.getUserDetailsObj(); Countly.user_details(userDetailObj); - // read event queue + // read request queue + setTimeout(() => { + var req = hp.readRequestQueue()[0]; + const actualUserDetails = req.user_details; + const isValid = hp.validateUserDetails(actualUserDetails, userDetailObj); + assert.equal(true, isValid); + done(); + }, hp.sWait); + }); + + it("Record and validate all user details", (done) => { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + }); + var userDetailObj = testUtils.getUserDetailsObj(); + Countly.userProfile.set_properties(userDetailObj); + Countly.userProfile.save(); + // read request queue + setTimeout(() => { + var req = hp.readRequestQueue()[0]; + const actualUserDetails = req.user_details; + const isValid = hp.validateUserDetails(actualUserDetails, userDetailObj); + assert.equal(true, isValid); + done(); + }, hp.sWait); + }); + + it("Record and validate all user details", (done) => { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + }); + var userDetailObj = testUtils.getUserDetailsObj(); + Countly.userProfile.set_properties(userDetailObj); + Countly.userProfile.save(); + // read request queue setTimeout(() => { var req = hp.readRequestQueue()[0]; const actualUserDetails = req.user_details; From bcce9c5b9439db130d3a2ef458fd0a5821f7f6de Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 2 Dec 2025 15:37:08 +0300 Subject: [PATCH 06/10] feat: basic tests for the functions --- test/tests_user_details.js | 165 +++++++++++++++++++++++++++++++++++-- 1 file changed, 158 insertions(+), 7 deletions(-) diff --git a/test/tests_user_details.js b/test/tests_user_details.js index 323bc00..c929395 100644 --- a/test/tests_user_details.js +++ b/test/tests_user_details.js @@ -43,20 +43,171 @@ describe("User details tests", () => { }, hp.sWait); }); - it("Record and validate all user details", (done) => { + it("set_property", (done) => { Countly.init({ app_key: "YOUR_APP_KEY", url: "https://try.count.ly", }); - var userDetailObj = testUtils.getUserDetailsObj(); - Countly.userProfile.set_properties(userDetailObj); + Countly.userProfile.set_property("name", "John Doe"); Countly.userProfile.save(); - // read request queue setTimeout(() => { var req = hp.readRequestQueue()[0]; - const actualUserDetails = req.user_details; - const isValid = hp.validateUserDetails(actualUserDetails, userDetailObj); - assert.equal(true, isValid); + const actualUserDetails = JSON.parse(req.user_details); + assert.equal(actualUserDetails.custom.name, "John Doe"); + done(); + }, hp.sWait); + }); + + it("set_once", (done) => { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + }); + Countly.userProfile.set_once("name", "John Doe"); + Countly.userProfile.save(); + setTimeout(() => { + var req = hp.readRequestQueue()[0]; + const actualUserDetails = JSON.parse(req.user_details); + assert.equal(actualUserDetails.custom.name.$setOnce, "John Doe"); + done(); + }, hp.sWait); + }); + + it("increment", (done) => { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + }); + Countly.userProfile.increment("visits"); + Countly.userProfile.save(); + setTimeout(() => { + var req = hp.readRequestQueue()[0]; + const actualUserDetails = JSON.parse(req.user_details); + assert.equal(actualUserDetails.custom.visits.$inc, 1); + done(); + }, hp.sWait); + }); + + it("increment_by", (done) => { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + }); + Countly.userProfile.increment_by("visits", 5); + Countly.userProfile.save(); + setTimeout(() => { + var req = hp.readRequestQueue()[0]; + const actualUserDetails = JSON.parse(req.user_details); + assert.equal(actualUserDetails.custom.visits.$inc, 5); + done(); + }, hp.sWait); + }); + + it("multiply", (done) => { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + }); + Countly.userProfile.multiply("visits", 2); + Countly.userProfile.save(); + setTimeout(() => { + var req = hp.readRequestQueue()[0]; + const actualUserDetails = JSON.parse(req.user_details); + assert.equal(actualUserDetails.custom.visits.$mul, 2); + done(); + }, hp.sWait); + }); + + it("max", (done) => { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + }); + Countly.userProfile.max("score", 100); + Countly.userProfile.save(); + setTimeout(() => { + var req = hp.readRequestQueue()[0]; + const actualUserDetails = JSON.parse(req.user_details); + assert.equal(actualUserDetails.custom.score.$max, 100); + done(); + }, hp.sWait); + }); + + it("min", (done) => { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + }); + Countly.userProfile.min("score", 10); + Countly.userProfile.save(); + setTimeout(() => { + var req = hp.readRequestQueue()[0]; + const actualUserDetails = JSON.parse(req.user_details); + assert.equal(actualUserDetails.custom.score.$min, 10); + done(); + }, hp.sWait); + }); + + it("push", (done) => { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + }); + Countly.userProfile.push("tags", "new"); + Countly.userProfile.save(); + setTimeout(() => { + var req = hp.readRequestQueue()[0]; + const actualUserDetails = JSON.parse(req.user_details); + assert.equal(actualUserDetails.custom.tags.$push, "new"); + done(); + }, hp.sWait); + }); + + it("push_unique", (done) => { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + }); + Countly.userProfile.push_unique("tags", "unique"); + Countly.userProfile.save(); + setTimeout(() => { + var req = hp.readRequestQueue()[0]; + const actualUserDetails = JSON.parse(req.user_details); + assert.equal(actualUserDetails.custom.tags.$addToSet, "unique"); + done(); + }, hp.sWait); + }); + + it("pull", (done) => { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + }); + Countly.userProfile.pull("tags", "old"); + Countly.userProfile.save(); + setTimeout(() => { + var req = hp.readRequestQueue()[0]; + const actualUserDetails = JSON.parse(req.user_details); + assert.equal(actualUserDetails.custom.tags.$pull, "old"); + done(); + }, hp.sWait); + }); + + it("unset", (done) => { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + }); + Countly.userProfile.set_property("keep", "value"); + Countly.userProfile.set_property("remove", "value"); + Countly.userProfile.unset("remove"); + Countly.userProfile.save(); + + setTimeout(() => { + var req = hp.readRequestQueue()[0]; + const actualUserDetails = JSON.parse(req.user_details); + assert.equal(actualUserDetails.custom.keep, "value"); + assert.equal(actualUserDetails.custom.remove, undefined); done(); }, hp.sWait); }); From 6791ff9733458ae35eb4f3d0abd7197c1306d5f0 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 2 Dec 2025 16:11:44 +0300 Subject: [PATCH 07/10] feat: new tests --- test/helpers/helper_functions.js | 7 ++++- test/tests_user_details.js | 45 ++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/test/helpers/helper_functions.js b/test/helpers/helper_functions.js index 996f971..aa9f12a 100644 --- a/test/helpers/helper_functions.js +++ b/test/helpers/helper_functions.js @@ -64,7 +64,12 @@ function readRequestQueue(customPath = false, isBulk = false, isMemory = false) a = CountlyStorage.storeGet("cly_queue"); } else { - a = JSON.parse(fs.readFileSync(destination, "utf-8")).cly_queue; + try { + a = JSON.parse(fs.readFileSync(destination, "utf-8")).cly_queue; + } + catch (e) { + a = []; + } } return a; } diff --git a/test/tests_user_details.js b/test/tests_user_details.js index c929395..e1a260f 100644 --- a/test/tests_user_details.js +++ b/test/tests_user_details.js @@ -43,6 +43,37 @@ describe("User details tests", () => { }, hp.sWait); }); + it("set_properties - No custom", (done) => { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + }); + Countly.userProfile.set_properties({ + name: "Alexandrina Jovovich", + username: "alex_jov", + email: "alex@example.com", + organization: "TechNova", + phone: "+987654321", + picture: "https://example.com/images/profile_alex.jpg", + picturePath: "/test/file/path.jpg", + }); + Countly.userProfile.save(); + + setTimeout(() => { + var req = hp.readRequestQueue()[0]; + const actualUserDetails = JSON.parse(req.user_details); + assert.equal(actualUserDetails.name, "Alexandrina Jovovich"); + assert.equal(actualUserDetails.username, "alex_jov"); + assert.equal(actualUserDetails.email, "alex@example.com"); + assert.equal(actualUserDetails.organization, "TechNova"); + assert.equal(actualUserDetails.phone, "+987654321"); + assert.equal(actualUserDetails.picture, "https://example.com/images/profile_alex.jpg"); + assert.equal(actualUserDetails.picturePath, "/test/file/path.jpg"); + assert.equal(actualUserDetails.custom, undefined); + done(); + }, hp.sWait); + }); + it("set_property", (done) => { Countly.init({ app_key: "YOUR_APP_KEY", @@ -211,4 +242,18 @@ describe("User details tests", () => { done(); }, hp.sWait); }); + + it("save - Nothing to save", (done) => { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + }); + Countly.userProfile.save(); + + setTimeout(() => { + var req = hp.readRequestQueue(); + assert.deepEqual(req.length, 0); + done(); + }, hp.sWait); + }); }); From 38324caca1436e732eae74dc2e1a5714a3757d8c Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Tue, 2 Dec 2025 16:18:14 +0300 Subject: [PATCH 08/10] feat: add changelog --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7221eb9..19065f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## XX.XX.XX +- Added `Countly.userProfile` with following calls and functionality + - `increment` for incrementing custom property value by 1 + - `increment_by` for incrementing custom property value by provided value. + - `max` for saving maximal value between existing and provided. + - `min` for saving minimal value between existing and provided. + - `multiply` for multiplying custom property value by provided value. + - `pull` for removing value from array. + - `push` for inserting value to array which can have duplicates. + - `push_unique` for inserting value to array of unique values. + - `save` for sending provided values to server. + - `set_once` for setting value if it does not exist. + - `set_properties` for setting either custom user properties or predefined user properties. + - `set_property` for setting a single user property. It can be either a custom one or one of the predefined ones. + +- Deprecated `Countly.userData` added `Countly.userProfile` as replacement, mentioned above. +- Deprecated `Countly.user_details`, please use `Countly.userProfile.set_properties` as replacement. + ## 24.10.3 - Added support for uploading user images by providing path to the local image using `picturePath` parameter in `user_details` method (non-bulk) - Reduced SDK log verbosity From 5553a4d33a807ad7134dc5d83f4af3a81a546382 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 3 Dec 2025 10:03:39 +0300 Subject: [PATCH 09/10] feat: tweak tests --- test/tests_user_details.js | 225 +++++++++++++++++++------------------ 1 file changed, 116 insertions(+), 109 deletions(-) diff --git a/test/tests_user_details.js b/test/tests_user_details.js index e1a260f..662eb14 100644 --- a/test/tests_user_details.js +++ b/test/tests_user_details.js @@ -43,202 +43,209 @@ describe("User details tests", () => { }, hp.sWait); }); - it("set_properties - No custom", (done) => { + it("Multiple save operations", (done) => { Countly.init({ app_key: "YOUR_APP_KEY", url: "https://try.count.ly", }); - Countly.userProfile.set_properties({ - name: "Alexandrina Jovovich", - username: "alex_jov", - email: "alex@example.com", - organization: "TechNova", - phone: "+987654321", - picture: "https://example.com/images/profile_alex.jpg", - picturePath: "/test/file/path.jpg", - }); - Countly.userProfile.save(); - - setTimeout(() => { - var req = hp.readRequestQueue()[0]; - const actualUserDetails = JSON.parse(req.user_details); - assert.equal(actualUserDetails.name, "Alexandrina Jovovich"); - assert.equal(actualUserDetails.username, "alex_jov"); - assert.equal(actualUserDetails.email, "alex@example.com"); - assert.equal(actualUserDetails.organization, "TechNova"); - assert.equal(actualUserDetails.phone, "+987654321"); - assert.equal(actualUserDetails.picture, "https://example.com/images/profile_alex.jpg"); - assert.equal(actualUserDetails.picturePath, "/test/file/path.jpg"); - assert.equal(actualUserDetails.custom, undefined); - done(); - }, hp.sWait); - }); - it("set_property", (done) => { - Countly.init({ - app_key: "YOUR_APP_KEY", - url: "https://try.count.ly", - }); + // First save Countly.userProfile.set_property("name", "John Doe"); Countly.userProfile.save(); - setTimeout(() => { - var req = hp.readRequestQueue()[0]; - const actualUserDetails = JSON.parse(req.user_details); - assert.equal(actualUserDetails.custom.name, "John Doe"); - done(); - }, hp.sWait); - }); - it("set_once", (done) => { - Countly.init({ - app_key: "YOUR_APP_KEY", - url: "https://try.count.ly", - }); - Countly.userProfile.set_once("name", "John Doe"); + // Second save + Countly.userProfile.set_property("email", "john@example.com"); Countly.userProfile.save(); - setTimeout(() => { - var req = hp.readRequestQueue()[0]; - const actualUserDetails = JSON.parse(req.user_details); - assert.equal(actualUserDetails.custom.name.$setOnce, "John Doe"); - done(); - }, hp.sWait); - }); - it("increment", (done) => { - Countly.init({ - app_key: "YOUR_APP_KEY", - url: "https://try.count.ly", - }); - Countly.userProfile.increment("visits"); - Countly.userProfile.save(); setTimeout(() => { - var req = hp.readRequestQueue()[0]; - const actualUserDetails = JSON.parse(req.user_details); - assert.equal(actualUserDetails.custom.visits.$inc, 1); - done(); - }, hp.sWait); - }); + var queue = hp.readRequestQueue(); + assert.equal(queue.length, 2); - it("increment_by", (done) => { - Countly.init({ - app_key: "YOUR_APP_KEY", - url: "https://try.count.ly", - }); - Countly.userProfile.increment_by("visits", 5); - Countly.userProfile.save(); - setTimeout(() => { - var req = hp.readRequestQueue()[0]; - const actualUserDetails = JSON.parse(req.user_details); - assert.equal(actualUserDetails.custom.visits.$inc, 5); + const firstSave = JSON.parse(queue[0].user_details); + assert.equal(firstSave.custom.name, "John Doe"); + assert.equal(firstSave.custom.email, undefined); + + const secondSave = JSON.parse(queue[1].user_details); + assert.equal(secondSave.custom.name, undefined); + assert.equal(secondSave.custom.email, "john@example.com"); done(); }, hp.sWait); }); - it("multiply", (done) => { + it("Array operations", (done) => { Countly.init({ app_key: "YOUR_APP_KEY", url: "https://try.count.ly", }); - Countly.userProfile.multiply("visits", 2); + + Countly.userProfile.push("interests", "coding"); + Countly.userProfile.push("interests", "music"); + Countly.userProfile.push_unique("skills", "javascript"); + Countly.userProfile.push_unique("skills", "nodejs"); + Countly.userProfile.pull("old_tags", "deprecated"); Countly.userProfile.save(); + setTimeout(() => { var req = hp.readRequestQueue()[0]; const actualUserDetails = JSON.parse(req.user_details); - assert.equal(actualUserDetails.custom.visits.$mul, 2); + // Multiple operations on same key create an array + assert.deepEqual(actualUserDetails.custom.interests.$push, ["coding", "music"]); + assert.deepEqual(actualUserDetails.custom.skills.$addToSet, ["javascript", "nodejs"]); + assert.equal(actualUserDetails.custom.old_tags.$pull, "deprecated"); done(); }, hp.sWait); }); - it("max", (done) => { + it("Numeric operations", (done) => { Countly.init({ app_key: "YOUR_APP_KEY", url: "https://try.count.ly", }); - Countly.userProfile.max("score", 100); + + Countly.userProfile.increment("counter"); + Countly.userProfile.increment("counter"); + Countly.userProfile.increment_by("score", 50); + Countly.userProfile.multiply("multiplier", 2); + Countly.userProfile.max("high_score", 1000); + Countly.userProfile.min("low_score", 10); Countly.userProfile.save(); + setTimeout(() => { var req = hp.readRequestQueue()[0]; const actualUserDetails = JSON.parse(req.user_details); - assert.equal(actualUserDetails.custom.score.$max, 100); + // Last operation wins for same key + assert.equal(actualUserDetails.custom.counter.$inc, 1); + assert.equal(actualUserDetails.custom.score.$inc, 50); + assert.equal(actualUserDetails.custom.multiplier.$mul, 2); + assert.equal(actualUserDetails.custom.high_score.$max, 1000); + assert.equal(actualUserDetails.custom.low_score.$min, 10); done(); }, hp.sWait); }); - it("min", (done) => { + it("unset", (done) => { Countly.init({ app_key: "YOUR_APP_KEY", url: "https://try.count.ly", }); - Countly.userProfile.min("score", 10); + + Countly.userProfile.set_property("temp1", "value1"); + Countly.userProfile.set_property("temp2", "value2"); + Countly.userProfile.set_property("keep", "important"); + Countly.userProfile.unset("temp1"); + Countly.userProfile.unset("temp2"); Countly.userProfile.save(); + setTimeout(() => { var req = hp.readRequestQueue()[0]; const actualUserDetails = JSON.parse(req.user_details); - assert.equal(actualUserDetails.custom.score.$min, 10); + assert.equal(actualUserDetails.custom.keep, "important"); + assert.equal(actualUserDetails.custom.temp1, undefined); + assert.equal(actualUserDetails.custom.temp2, undefined); done(); }, hp.sWait); }); - it("push", (done) => { + it("set_once", (done) => { Countly.init({ app_key: "YOUR_APP_KEY", url: "https://try.count.ly", }); - Countly.userProfile.push("tags", "new"); + + Countly.userProfile.set_once("first_login", "2024-01-01"); + Countly.userProfile.set_once("registration_source", "mobile_app"); + Countly.userProfile.set_property("last_login", "2024-12-03"); Countly.userProfile.save(); + setTimeout(() => { var req = hp.readRequestQueue()[0]; const actualUserDetails = JSON.parse(req.user_details); - assert.equal(actualUserDetails.custom.tags.$push, "new"); + assert.equal(actualUserDetails.custom.first_login.$setOnce, "2024-01-01"); + assert.equal(actualUserDetails.custom.registration_source.$setOnce, "mobile_app"); + assert.equal(actualUserDetails.custom.last_login, "2024-12-03"); done(); }, hp.sWait); }); - it("push_unique", (done) => { + it("Full user profile with all property types", (done) => { Countly.init({ app_key: "YOUR_APP_KEY", url: "https://try.count.ly", }); - Countly.userProfile.push_unique("tags", "unique"); - Countly.userProfile.save(); - setTimeout(() => { - var req = hp.readRequestQueue()[0]; - const actualUserDetails = JSON.parse(req.user_details); - assert.equal(actualUserDetails.custom.tags.$addToSet, "unique"); - done(); - }, hp.sWait); - }); - it("pull", (done) => { - Countly.init({ - app_key: "YOUR_APP_KEY", - url: "https://try.count.ly", + Countly.userProfile.set_properties({ + name: "Alex Johnson", + username: "alex_j", + email: "alex@example.com", + organization: "TechCorp", + phone: "+1234567890", + picture: "https://example.com/alex.jpg", + gender: "M", + byear: 1990, + custom: { + subscription: "premium", + account_type: "business", + }, }); - Countly.userProfile.pull("tags", "old"); + + Countly.userProfile.increment("total_purchases"); + Countly.userProfile.increment_by("lifetime_value", 500); + Countly.userProfile.push_unique("purchased_products", "product_123"); + Countly.userProfile.max("max_order_value", 250); Countly.userProfile.save(); + setTimeout(() => { var req = hp.readRequestQueue()[0]; const actualUserDetails = JSON.parse(req.user_details); - assert.equal(actualUserDetails.custom.tags.$pull, "old"); + + // Standard properties + assert.equal(actualUserDetails.name, "Alex Johnson"); + assert.equal(actualUserDetails.username, "alex_j"); + assert.equal(actualUserDetails.email, "alex@example.com"); + assert.equal(actualUserDetails.organization, "TechCorp"); + assert.equal(actualUserDetails.phone, "+1234567890"); + assert.equal(actualUserDetails.picture, "https://example.com/alex.jpg"); + assert.equal(actualUserDetails.gender, "M"); + assert.equal(actualUserDetails.byear, 1990); + + // Custom properties + assert.equal(actualUserDetails.custom.subscription, "premium"); + assert.equal(actualUserDetails.custom.account_type, "business"); + assert.equal(actualUserDetails.custom.total_purchases.$inc, 1); + assert.equal(actualUserDetails.custom.lifetime_value.$inc, 500); + assert.equal(actualUserDetails.custom.purchased_products.$addToSet, "product_123"); + assert.equal(actualUserDetails.custom.max_order_value.$max, 250); done(); }, hp.sWait); }); - it("unset", (done) => { + it("set_properties - No custom", (done) => { Countly.init({ app_key: "YOUR_APP_KEY", url: "https://try.count.ly", }); - Countly.userProfile.set_property("keep", "value"); - Countly.userProfile.set_property("remove", "value"); - Countly.userProfile.unset("remove"); + Countly.userProfile.set_properties({ + name: "Alexandrina Jovovich", + username: "alex_jov", + email: "alex@example.com", + organization: "TechNova", + phone: "+987654321", + picture: "https://example.com/images/profile_alex.jpg", + picturePath: "/test/file/path.jpg", + }); Countly.userProfile.save(); setTimeout(() => { var req = hp.readRequestQueue()[0]; const actualUserDetails = JSON.parse(req.user_details); - assert.equal(actualUserDetails.custom.keep, "value"); - assert.equal(actualUserDetails.custom.remove, undefined); + assert.equal(actualUserDetails.name, "Alexandrina Jovovich"); + assert.equal(actualUserDetails.username, "alex_jov"); + assert.equal(actualUserDetails.email, "alex@example.com"); + assert.equal(actualUserDetails.organization, "TechNova"); + assert.equal(actualUserDetails.phone, "+987654321"); + assert.equal(actualUserDetails.picture, "https://example.com/images/profile_alex.jpg"); + assert.equal(actualUserDetails.picturePath, "/test/file/path.jpg"); + assert.equal(actualUserDetails.custom, undefined); done(); }, hp.sWait); }); From f97d9d79759fc299c5f002d244c1fa95ccbbed46 Mon Sep 17 00:00:00 2001 From: Arif Burak Demiray Date: Wed, 3 Dec 2025 10:40:02 +0300 Subject: [PATCH 10/10] fix: unset behavior --- lib/countly.js | 2 +- test/tests_user_details.js | 49 +++++++++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/lib/countly.js b/lib/countly.js index f26509b..6630705 100644 --- a/lib/countly.js +++ b/lib/countly.js @@ -941,7 +941,7 @@ Countly.Bulk = Bulk; * @param {string} key - name of the property to delete * */ unset(key) { - delete userProperties.custom[key]; + Countly.userProfile.set_properties({ custom: { [key]: "" } }); }, /** * Increment value under the key of this user's custom properties by one diff --git a/test/tests_user_details.js b/test/tests_user_details.js index 662eb14..b6481fe 100644 --- a/test/tests_user_details.js +++ b/test/tests_user_details.js @@ -140,8 +140,8 @@ describe("User details tests", () => { var req = hp.readRequestQueue()[0]; const actualUserDetails = JSON.parse(req.user_details); assert.equal(actualUserDetails.custom.keep, "important"); - assert.equal(actualUserDetails.custom.temp1, undefined); - assert.equal(actualUserDetails.custom.temp2, undefined); + assert.equal(actualUserDetails.custom.temp1, ""); + assert.equal(actualUserDetails.custom.temp2, ""); done(); }, hp.sWait); }); @@ -263,4 +263,47 @@ describe("User details tests", () => { done(); }, hp.sWait); }); -}); + + it("Legacy calls", (done) => { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + }); + + Countly.user_details({ name: "Legacy User", email: "legacy@example.com", custom: { legacy_key: "legacy_value" } }); + + Countly.userData.set("name", "Should Not Override"); + Countly.userData.unset("legacy_key"); + Countly.userData.increment("some_counter"); + Countly.userData.increment_by("another_counter", 5); + Countly.userData.push("some_array", "value"); + Countly.userData.push_unique("unique_array", "unique_value"); + Countly.userData.pull("some_array", "value"); + Countly.userData.set_once("set_array", "set_value"); + Countly.userData.max("max_value", 100); + Countly.userData.min("min_value", 10); + Countly.userData.multiply("multiplier", 3); + Countly.userData.save(); + setTimeout(() => { + var queue = hp.readRequestQueue(); + const actualUserDetails = JSON.parse(queue[0].user_details); + assert.equal(actualUserDetails.name, "Legacy User"); + assert.equal(actualUserDetails.email, "legacy@example.com"); + assert.equal(actualUserDetails.custom.legacy_key, "legacy_value"); + + const secondUserDetails = JSON.parse(queue[1].user_details); + assert.equal(secondUserDetails.custom.name, "Should Not Override"); + assert.equal(secondUserDetails.custom.legacy_key, ""); + assert.equal(secondUserDetails.custom.some_counter.$inc, 1); + assert.equal(secondUserDetails.custom.another_counter.$inc, 5); + assert.equal(secondUserDetails.custom.some_array.$push, "value"); + assert.equal(secondUserDetails.custom.unique_array.$addToSet, "unique_value"); + assert.equal(secondUserDetails.custom.some_array.$pull, "value"); + assert.equal(secondUserDetails.custom.set_array.$setOnce, "set_value"); + assert.equal(secondUserDetails.custom.max_value.$max, 100); + assert.equal(secondUserDetails.custom.min_value.$min, 10); + assert.equal(secondUserDetails.custom.multiplier.$mul, 3); + done(); + }, hp.sWait); + }); +}); \ No newline at end of file