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 diff --git a/lib/countly.js b/lib/countly.js index 0963e70..6630705 100644 --- a/lib/countly.js +++ b/lib/countly.js @@ -834,26 +834,34 @@ 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) { - 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; + } } }; @@ -869,35 +877,158 @@ 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) { + Countly.userProfile.set_properties({ 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: {} }; + }, }; /** * 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"); @@ -913,116 +1044,105 @@ 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) { - 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 + * @deprecated use {@link Countly.userProfile.unset} instead * */ 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 + * @deprecated use {@link Countly.userProfile.increment} instead * */ 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 * @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) { - 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 * @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) { - 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 * @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) { - 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 * @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) { - 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 * @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) { - 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 * @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) { - 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 * @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) { - 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 + * @deprecated use {@link Countly.userProfile.save} instead * */ save() { - if (Countly.check_consent("users")) { - toRequestQueue({ user_details: JSON.stringify({ custom: customData }) }); - } - customData = {}; + Countly.userProfile.save(); }, }; 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 11b57b2..b6481fe 100644 --- a/test/tests_user_details.js +++ b/test/tests_user_details.js @@ -8,14 +8,32 @@ 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; @@ -24,4 +42,268 @@ describe("User details tests", () => { done(); }, hp.sWait); }); -}); + + it("Multiple save operations", (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(); + + // Second save + Countly.userProfile.set_property("email", "john@example.com"); + Countly.userProfile.save(); + + setTimeout(() => { + var queue = hp.readRequestQueue(); + assert.equal(queue.length, 2); + + 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("Array operations", (done) => { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + }); + + 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); + // 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("Numeric operations", (done) => { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + }); + + 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); + // 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("unset", (done) => { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + }); + + 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.keep, "important"); + assert.equal(actualUserDetails.custom.temp1, ""); + assert.equal(actualUserDetails.custom.temp2, ""); + done(); + }, hp.sWait); + }); + + it("set_once", (done) => { + Countly.init({ + app_key: "YOUR_APP_KEY", + url: "https://try.count.ly", + }); + + 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.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("Full user profile with all property types", (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.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); + + // 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("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("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); + }); + + 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