UNPKG

countly-sdk-nodejs

Version:
1,031 lines (986 loc) 79.2 kB
/** ********** * Countly NodeJS SDK * https://github.com/Countly/countly-sdk-nodejs *********** */ /** * Countly object to manage the internal queue and send requests to Countly server. More information on {@link http://resources.count.ly/docs/countly-sdk-for-nodejs} * @name Countly * @global * @namespace Countly * @example <caption>SDK integration</caption> * var Countly = require("countly-sdk-nodejs"); * * Countly.init({ * app_key: "{YOUR-APP-KEY}", * url: "https://API_HOST/", * debug: true * }); * * Countly.begin_session(); */ var os = require("os"); var http = require("http"); var https = require("https"); var cluster = require("cluster"); var cc = require("./countly-common"); var Bulk = require("./countly-bulk"); var CountlyStorage = require("./countly-storage"); var Countly = {}; Countly.StorageTypes = cc.storageTypeEnums; Countly.DeviceIdType = cc.deviceIdTypeEnums; Countly.Bulk = Bulk; (function() { var SDK_VERSION = "24.10.2"; var SDK_NAME = "javascript_native_nodejs"; var inited = false; var sessionStarted = false; var platform; var apiPath = "/i"; var readPath = "/o/sdk"; var beatInterval = 500; var queueSize = 1000; var requestQueue = []; var eventQueue = []; var remoteConfigs = {}; var crashLogs = []; var timedEvents = {}; var crashSegments = null; var autoExtend = true; var lastBeat; var storedDuration = 0; var lastView = null; var lastViewTime = 0; var lastMsTs = 0; var lastViewStoredDuration = 0; var failTimeout = 0; var failTimeoutAmount = 60; var sessionUpdate = 60; var maxEventBatch = 100; var readyToProcess = true; var trackTime = true; var metrics = {}; var lastParams = {}; var startTime; var maxKeyLength = 128; var maxValueSize = 256; var maxSegmentationValues = 100; var maxBreadcrumbCount = 100; var maxStackTraceLinesPerThread = 30; var maxStackTraceLineLength = 200; var deviceIdType = null; var heartBeatTimer = null; /** * Array with list of available features that you can require consent for */ Countly.features = ["sessions", "events", "views", "crashes", "attribution", "users", "location", "star-rating", "apm", "feedback", "remote-config"]; // create object to store consents var consents = {}; for (var feat = 0; feat < Countly.features.length; feat++) { consents[Countly.features[feat]] = {}; } /** * Initialize Countly object * @param {Object} conf - Countly initialization {@link Init} object with configuration options * @param {string} conf.app_key - app key for your app created in Countly * @param {string} conf.device_id - to identify a visitor, will be auto generated if not provided * @param {string} conf.url - your Countly server url, you can use your own server URL or IP here * @param {string} [conf.app_version=0.0] - the version of your app or website * @param {string=} conf.country_code - country code for your visitor * @param {string=} conf.city - name of the city of your visitor * @param {string=} conf.ip_address - ip address of your visitor * @param {boolean} [conf.debug=false] - output debug info into console * @param {number} [conf.interval=500] - set an interval how often to check if there is any data to report and report it in miliseconds * @param {number} [conf.queue_size=1000] - maximum amount of queued requests to store * @param {number} [conf.fail_timeout=60] - set time in seconds to wait after failed connection to server in seconds * @param {number} [conf.session_update=60] - how often in seconds should session be extended * @param {number} [conf.max_events=100] - maximum amount of events to send in one batch * @param {boolean} [conf.force_post=false] - force using post method for all requests * @param {boolean} [conf.clear_stored_device_id=false] - set it to true if you want to erase the stored device ID * @param {boolean} [conf.test_mode=false] - set it to true if you want to initiate test_mode * @param {string} [conf.storage_path] - where SDK would store data, including id, queues, etc * @param {boolean} [conf.require_consent=false] - pass true if you are implementing GDPR compatible consent management. It would prevent running any functionality without proper consent * @param {boolean|function} [conf.remote_config=false] - Enable automatic remote config fetching, provide callback function to be notified when fetching done * @param {function} [conf.http_options=] - function to get http options by reference and overwrite them, before running each request * @deprecated {number} [conf.max_logs=100] - maximum amount of breadcrumbs to store for crash logs * @param {number} [conf.max_key_length=128] - maximum size of all string keys * @param {number} [conf.max_value_size=256] - maximum size of all values in our key-value pairs (Except "picture" field, that has a limit of 4096 chars) * @param {number} [conf.max_segmentation_values=30] - max amount of custom (dev provided) segmentation in one event * @param {number} [conf.max_breadcrumb_count=100] - maximum amount of breadcrumbs that can be recorded before the oldest one is deleted * @param {number} [conf.max_stack_trace_lines_per_thread=30] - maximum amount of stack trace lines would be recorded per thread * @param {number} [conf.max_stack_trace_line_length=200] - maximum amount of characters are allowed per stack trace line. This limits also the crash message length * @param {Object} conf.metrics - provide {@link Metrics} for this user/device, or else will try to collect what's possible * @param {string} conf.metrics._os - name of platform/operating system * @param {string} conf.metrics._os_version - version of platform/operating system * @param {string} conf.metrics._device - device name * @param {string} conf.metrics._resolution - screen resolution of the device * @param {string} conf.metrics._carrier - carrier or operator used for connection * @param {string} conf.metrics._density - screen density of the device * @param {string} conf.metrics._locale - locale or language of the device in ISO format * @param {string} conf.metrics._store - source from where the user/device/installation came from * @param {StorageTypes} conf.storage_type - to determine which storage type is going to be applied * @param {Object} conf.custom_storage_method - user given storage methods * @example * Countly.init({ * app_key: "{YOUR-APP-KEY}", * url: "https://API_HOST/", * debug: true, * app_version: "1.0", * metrics:{ * _os: "Ubuntu", * _os_version: "16.04", * _device: "aws-server" * } * }); */ Countly.init = function(conf) { if (!inited) { startTime = cc.getTimestamp(); inited = true; conf = conf || {}; timedEvents = {}; beatInterval = conf.interval || Countly.interval || beatInterval; queueSize = conf.queue_size || Countly.queue_size || queueSize; failTimeoutAmount = conf.fail_timeout || Countly.fail_timeout || failTimeoutAmount; sessionUpdate = conf.session_update || Countly.session_update || sessionUpdate; maxEventBatch = conf.max_events || Countly.max_events || maxEventBatch; metrics = conf.metrics || Countly.metrics || {}; conf.debug = conf.debug || Countly.debug || false; conf.clear_stored_device_id = conf.clear_stored_device_id || false; Countly.test_mode = conf.test_mode || false; Countly.app_key = conf.app_key || Countly.app_key || null; Countly.url = cc.stripTrailingSlash(conf.url || Countly.url || ""); Countly.app_version = conf.app_version || Countly.app_version || "0.0"; Countly.country_code = conf.country_code || Countly.country_code || null; Countly.city = conf.city || Countly.city || null; Countly.ip_address = conf.ip_address || Countly.ip_address || null; Countly.force_post = conf.force_post || Countly.force_post || false; Countly.require_consent = conf.require_consent || Countly.require_consent || false; Countly.remote_config = conf.remote_config || Countly.remote_config || false; Countly.http_options = conf.http_options || Countly.http_options || null; Countly.maxKeyLength = conf.max_key_length || Countly.max_key_length || maxKeyLength; Countly.maxValueSize = conf.max_value_size || Countly.max_value_size || maxValueSize; Countly.maxSegmentationValues = conf.max_segmentation_values || Countly.max_segmentation_values || maxSegmentationValues; Countly.maxBreadcrumbCount = conf.max_breadcrumb_count || Countly.max_breadcrumb_count || conf.max_logs || Countly.max_logs || maxBreadcrumbCount; Countly.maxStackTraceLinesPerThread = conf.max_stack_trace_lines_per_thread || Countly.max_stack_trace_lines_per_thread || maxStackTraceLinesPerThread; Countly.maxStackTraceLineLength = conf.max_stack_trace_line_length || Countly.max_stack_trace_line_length || maxStackTraceLineLength; conf.storage_path = conf.storage_path || Countly.storage_path; conf.storage_type = conf.storage_type || Countly.storage_type; // Common module debug value is set to init time debug value cc.debug = conf.debug; CountlyStorage.initStorage(conf.storage_path, conf.storage_type, false, conf.custom_storage_method); // clear stored device ID if flag is set if (conf.clear_stored_device_id) { cc.log(cc.logLevelEnums.WARNING, "init, clear_stored_device_id is true, erasing the stored ID."); CountlyStorage.storeSet("cly_id", null); CountlyStorage.storeSet("cly_id_type", null); } if (Countly.url === "") { cc.log(cc.logLevelEnums.ERROR, "init, Server URL not provided."); } else { cc.log(cc.logLevelEnums.INFO, "init, Countly initialized."); cc.log(cc.logLevelEnums.DEBUG, `init, Starting timestamp: [${startTime}].`); cc.log(cc.logLevelEnums.DEBUG, `init, Beat interval: [${beatInterval}].`); cc.log(cc.logLevelEnums.DEBUG, `init, Queue size: [${queueSize}].`); cc.log(cc.logLevelEnums.DEBUG, `init, Fail timeout amount: [${failTimeoutAmount}].`); cc.log(cc.logLevelEnums.DEBUG, `init, Session update interval: [${sessionUpdate}].`); cc.log(cc.logLevelEnums.DEBUG, `init, Max event batch size: [${maxEventBatch}].`); if (metrics) { cc.log(cc.logLevelEnums.DEBUG, `init, Metrics: `, metrics); } if (Countly.test_mode) { cc.log(cc.logLevelEnums.DEBUG, `init, Test mode is on.`); } cc.log(cc.logLevelEnums.DEBUG, `init, app_key: [${Countly.app_key}].`); cc.log(cc.logLevelEnums.DEBUG, `init, url: [${Countly.url}].`); cc.log(cc.logLevelEnums.DEBUG, `init, app_version: [${Countly.app_version}].`); if (Countly.country_code) { cc.log(cc.logLevelEnums.DEBUG, `init, Country code: [${Countly.country_code}].`); } if (Countly.city) { cc.log(cc.logLevelEnums.DEBUG, `init, City: [${Countly.city}].`); } if (Countly.ip_address) { cc.log(cc.logLevelEnums.DEBUG, `init, IP address: [${Countly.ip_address}].`); } cc.log(cc.logLevelEnums.DEBUG, `init, Force POST requests: [${Countly.force_post}].`); cc.log(cc.logLevelEnums.DEBUG, `init, Storage path: [${CountlyStorage.getStoragePath()}].`); cc.log(cc.logLevelEnums.DEBUG, `init, Require consent: [${Countly.require_consent}].`); if (Countly.remote_config) { cc.log(cc.logLevelEnums.DEBUG, `init, Automatic Remote Configuration is on.`); } if (Countly.http_options) { cc.log(cc.logLevelEnums.DEBUG, `init, Custom headers: [${Countly.http_options}].`); } cc.log(cc.logLevelEnums.DEBUG, `init, General value limits are,\n for key length: [${Countly.maxKeyLength}],\n for value size: [${Countly.maxValueSize}],\n for segmentation: [${Countly.maxSegmentationValues}].`); cc.log(cc.logLevelEnums.DEBUG, `init, Crash related value limits are,\n for breadcrumb count: [${Countly.maxBreadcrumbCount}],\n for trace lines per thread: [${Countly.maxStackTraceLinesPerThread}],\n for trace line length: [${Countly.maxStackTraceLineLength}].`); if (cluster.isMaster) { // fetch stored ID and ID type var storedId = CountlyStorage.storeGet("cly_id", null); var storedIdType = CountlyStorage.storeGet("cly_id_type", null); // if there was a stored ID if (storedId !== null) { Countly.device_id = storedId; // deviceIdType = storedIdType || cc.deviceIdTypeEnums.DEVELOPER_SUPPLIED; if (storedIdType === null) { // even though the device ID set, not type was set, setting it here if (cc.isUUID(storedId)) { // assuming it is a UUID so also assuming it is SDK generated storedIdType = cc.deviceIdTypeEnums.SDK_GENERATED; } else { // assuming it was set by the developer storedIdType = cc.deviceIdTypeEnums.DEVELOPER_SUPPLIED; } } deviceIdType = storedIdType; } // if the user provided device ID during the init and no ID was stored else if (conf.device_id || Countly.device_id) { Countly.device_id = conf.device_id || Countly.device_id; deviceIdType = cc.deviceIdTypeEnums.DEVELOPER_SUPPLIED; } // if no device ID provided during init nor it was stored previously else { Countly.device_id = cc.generateUUID(); deviceIdType = cc.deviceIdTypeEnums.SDK_GENERATED; } // save the ID and ID type CountlyStorage.storeSet("cly_id", Countly.device_id); CountlyStorage.storeSet("cly_id_type", deviceIdType); // create queues requestQueue = CountlyStorage.storeGet("cly_queue", []); eventQueue = CountlyStorage.storeGet("cly_event", []); remoteConfigs = CountlyStorage.storeGet("cly_remote_configs", {}); heartBeat(); // listen to current workers if (cluster.workers) { for (var id in cluster.workers) { cluster.workers[id].on("message", handleWorkerMessage); } } // handle future workers cluster.on("fork", (worker) => { worker.on("message", handleWorkerMessage); }); if (Countly.remote_config) { Countly.fetch_remote_config(Countly.remote_config); } } } } }; /** * WARNING!!! * Should be used only for testing purposes!!! * * Resets Countly to its initial state (used mainly to wipe the queues in memory). * Calling this will result in a loss of data * @param {boolean} preventRequestProcessing - if true request queues wont be processed, for testing purposes */ Countly.halt = function(preventRequestProcessing) { cc.log(cc.logLevelEnums.WARNING, "halt, Resetting Countly."); inited = false; sessionStarted = false; beatInterval = 500; queueSize = 1000; requestQueue = []; eventQueue = []; remoteConfigs = {}; crashLogs = []; timedEvents = {}; crashSegments = null; autoExtend = true; storedDuration = 0; lastView = null; lastViewTime = 0; lastMsTs = 0; lastViewStoredDuration = 0; failTimeout = 0; failTimeoutAmount = 60; sessionUpdate = 60; maxEventBatch = 100; readyToProcess = !preventRequestProcessing; trackTime = true; metrics = {}; lastParams = {}; maxKeyLength = 128; maxValueSize = 256; maxSegmentationValues = 100; maxBreadcrumbCount = 100; maxStackTraceLinesPerThread = 30; maxStackTraceLineLength = 200; deviceIdType = null; if (heartBeatTimer) { clearInterval(heartBeatTimer); heartBeatTimer = null; } // cc DEBUG cc.debug = false; cc.debugBulk = false; cc.debugBulkUser = false; // CONSENTS consents = {}; for (var a = 0; a < Countly.features.length; a++) { consents[Countly.features[a]] = {}; } // device_id Countly.device_id = undefined; Countly.device_id_type = undefined; Countly.remote_config = undefined; Countly.require_consent = false; Countly.debug = undefined; Countly.app_key = undefined; Countly.url = undefined; Countly.app_version = undefined; Countly.country_code = undefined; Countly.city = undefined; Countly.ip_address = undefined; Countly.force_post = undefined; Countly.require_consent = undefined; Countly.http_options = undefined; CountlyStorage.resetStorage(); }; /** * Modify feature groups for consent management. Allows you to group multiple features under one feature group * @param {object} features - object to define feature name as key and core features as value * @example <caption>Adding all features under one group</caption> * Countly.group_features({all:["sessions","events","views","crashes","attribution","users"]}); * //After this call Countly.add_consent("all") to allow all features @example <caption>Grouping features</caption> * Countly.group_features({ * activity:["sessions","events","views"], * info:["attribution","users"] * }); * //After this call Countly.add_consent("activity") to allow "sessions","events","views" * //or call Countly.add_consent("info") to allow "attribution","users" * //or call Countly.add_consent("crashes") to allow some separate feature */ Countly.group_features = function(features) { if (features) { for (var i in features) { if (!consents[i]) { cc.log(cc.logLevelEnums.INFO, "group_features, Trying to group the features."); if (typeof features[i] === "string") { consents[i] = { features: [features[i]] }; } else if (features[i] && features[i].constructor === Array && features[i].length) { consents[i] = { features: features[i] }; } else { cc.log(cc.logLevelEnums.WARNING, `group_features, Incorrect feature list for: [${i}] value: [${features[i]}].`); } } else { cc.log(cc.logLevelEnums.WARNING, `group_features, Feature name: [${i}] is already reserved.`); } } } else { cc.log(cc.logLevelEnums.ERROR, `group_features, Incorrect features value provided: [${features}].`); } }; /** * Check if consent is given for specific feature (either core feature of from custom feature group) * @param {string} feature - name of the feature, possible values, "sessions","events","views","crashes","attribution","users" or customly provided through {@link Countly.group_features} * @returns {bool} true if consent is given, false if not */ Countly.check_consent = function(feature) { if (!Countly.require_consent) { // we don't need to have specific consents cc.log(cc.logLevelEnums.INFO, `check_consent, Require consent is off. Giving consent for: [${feature}] feature.`); return true; } if (consents[feature] && consents[feature].optin) { cc.log(cc.logLevelEnums.INFO, `check_consent, Giving consent for: [${feature}] feature.`); return true; } if (consents[feature] && !consents[feature].optin) { cc.log(cc.logLevelEnums.ERROR, `check_consent, User is not optin. Consent refused for: [${feature}] feature.`); return false; } cc.log(cc.logLevelEnums.ERROR, `check_consent, No feature available for: [${feature}].`); return false; }; /** * Check if any consent is given, for some cases, when crucial parts are like device_id are needed for any request * @returns {bool} true if any consent is given, false if not */ Countly.check_any_consent = function() { if (!Countly.require_consent) { cc.log(cc.logLevelEnums.INFO, "check_any_consent, require_consent is off, no consent is necessary."); // we don't need to have consents return true; } for (var i in consents) { if (consents[i] && consents[i].optin) { cc.log(cc.logLevelEnums.INFO, `check_any_consent, found consent for: [${consents[i]}].`); return true; } } cc.log(cc.logLevelEnums.WARNING, "check_any_consent, no consent is given yet."); return false; }; /** * Enable/disable logging, to be used after init * @param {boolean} enableLogging - if true logging is enabled */ Countly.setLoggingEnabled = function(enableLogging) { cc.debug = enableLogging; }; /** * Add consent for specific feature, meaning, user allowed to track that data (either core feature of from custom feature group) * @param {string|array} feature - name of the feature, possible values, "sessions","events","views","crashes","attribution","users" or customly provided through {@link Countly.group_features} */ Countly.add_consent = function(feature) { cc.log(cc.logLevelEnums.INFO, `add_consent, Adding consent for: [${feature}].`); if (feature.constructor === Array) { for (var i = 0; i < feature.length; i++) { Countly.add_consent(feature[i]); } } else if (consents[feature]) { if (consents[feature].features) { consents[feature].optin = true; // this is added group, let's iterate through sub features Countly.add_consent(consents[feature].features); } else { // this is core feature if (consents[feature].optin !== true) { consents[feature].optin = true; updateConsent(); setTimeout(() => { if (feature === "sessions" && lastParams.begin_session) { Countly.begin_session.apply(Countly, lastParams.begin_session); lastParams.begin_session = null; } else if (feature === "views" && lastParams.track_pageview) { lastView = null; Countly.track_pageview.apply(Countly, lastParams.track_pageview); lastParams.track_pageview = null; } if (lastParams.change_id) { Countly.change_id.apply(Countly, lastParams.change_id); lastParams.change_id = null; } }, 1); } } } else { cc.log(cc.logLevelEnums.WARNING, `add_consent, No feature available for: [${feature}].`); } }; /** * Remove consent for specific feature, meaning, user opted out to track that data (either core feature of from custom feature group) * @param {string|array} feature - name of the feature, possible values, "sessions","events","views","crashes","attribution","users" or custom provided through {@link Countly.group_features} */ Countly.remove_consent = function(feature) { cc.log(cc.logLevelEnums.INFO, `remove_consent, Removing consent for: [${feature}].`); Countly.remove_consent_internal(feature, true); }; /** * Remove consent internally for specific feature,so that a request wont be sent for the operation * @param {string|array} feature - name of the feature, possible values, "sessions","events","views","crashes","attribution","users" or custom provided through {@link CountlyBulkUser.group_features} * @param {Boolean} enforceConsentUpdate - regulates if a request will be sent to the server or not. If true, removing consents will send a request to the server and if false, consents will be removed without a request */ Countly.remove_consent_internal = function(feature, enforceConsentUpdate) { // if true updateConsent will execute when possible enforceConsentUpdate = enforceConsentUpdate || false; if (feature.constructor === Array) { for (var i = 0; i < feature.length; i++) { Countly.remove_consent_internal(feature[i], enforceConsentUpdate); } } else if (consents[feature]) { if (consents[feature].features) { // this is added group, let's iterate through sub features Countly.remove_consent_internal(consents[feature].features, enforceConsentUpdate); } else { consents[feature].optin = false; // this is core feature if (enforceConsentUpdate && consents[feature].optin !== false) { updateConsent(); } } } else { cc.log(cc.logLevelEnums.WARNING, `remove_consent, No feature available for: [${feature}].`); } }; var consentTimer; var updateConsent = function() { if (consentTimer) { // delay syncing consents clearTimeout(consentTimer); consentTimer = null; } consentTimer = setTimeout(() => { var consentMessage = {}; for (var i = 0; i < Countly.features.length; i++) { if (consents[Countly.features[i]].optin === true) { consentMessage[Countly.features[i]] = true; } else { consentMessage[Countly.features[i]] = false; } } toRequestQueue({ consent: JSON.stringify(consentMessage) }); cc.log(cc.logLevelEnums.DEBUG, "updateConsent, Consent update request has been sent to the queue."); }, 1000); }; /** * Start session * @param {boolean} noHeartBeat - true if you don't want to use internal heartbeat to manage session */ Countly.begin_session = function(noHeartBeat) { if (Countly.check_consent("sessions")) { if (!sessionStarted) { cc.log(cc.logLevelEnums.INFO, "begin_session, Session started."); lastBeat = cc.getTimestamp(); sessionStarted = true; autoExtend = !(noHeartBeat); var req = {}; req.begin_session = 1; req.metrics = JSON.stringify(getMetrics()); toRequestQueue(req); } } else { lastParams.begin_session = arguments; } }; /** * Report session duration * @param {int} sec - amount of seconds to report for current session */ Countly.session_duration = function(sec) { if (Countly.check_consent("sessions")) { if (sessionStarted) { cc.log(cc.logLevelEnums.INFO, `session_duration, Session duration: [${sec}] seconds.`); toRequestQueue({ session_duration: sec }); } } }; /** * End current session * @param {int} sec - amount of seconds to report for current session, before ending it */ Countly.end_session = function(sec) { if (Countly.check_consent("sessions")) { if (sessionStarted) { sec = sec || cc.getTimestamp() - lastBeat; cc.log(cc.logLevelEnums.INFO, `end_session, Ending session. Duration: [${sec}] seconds.`); reportViewDuration(); sessionStarted = false; toRequestQueue({ end_session: 1, session_duration: sec }); } } }; /** * Check and return the current device id type * @returns {number} a number that indicates the device id type */ Countly.get_device_id_type = function() { cc.log(cc.logLevelEnums.INFO, `check_device_id_type, Retrieving the current device id type: [${deviceIdType}].`); return deviceIdType; }; /** * Gets the current device id * @returns {string} device id */ Countly.get_device_id = function() { cc.log(cc.logLevelEnums.INFO, `get_device_id, Retrieving the device id: [${Countly.device_id}].`); return Countly.device_id; }; /** * Changes the current device ID according to the device ID type (the preffered method) * @param {string} newId - new user/device ID to use. Must be a non-empty string value. Invalid values (like null, empty string or undefined) will be rejected * */ Countly.set_id = function(newId) { cc.log(cc.logLevelEnums.INFO, `set_id, Changing the device ID to: [${newId}]`); if (newId === null || newId === undefined || newId === "" || typeof newId !== "string") { cc.log(cc.logLevelEnums.WARNING, "set_id, The provided id is not a valid ID"); return; } if (Countly.get_device_id_type() === cc.deviceIdTypeEnums.DEVELOPER_SUPPLIED) { // change ID without merge as current ID is Dev supplied, so not first login Countly.change_id(newId, false); } else { // change ID with merge as current ID is not Dev supplied*/ Countly.change_id(newId, true); } }; /** * Change current user/device id * @param {string} newId - new user/device ID to use * @param {boolean=} merge - move data from old ID to new ID on server * */ Countly.change_id = function(newId, merge) { newId = cc.truncateSingleValue(newId, Countly.maxValueSize, "change_id", Countly.debug); cc.log(cc.logLevelEnums.INFO, `change_id, Changing ID. Current ID: [${Countly.device_id}], new ID: [${newId}], merge: [${merge}].`); if (cluster.isMaster) { if (Countly.device_id !== newId) { if (!merge) { // empty event queue if (eventQueue.length > 0) { toRequestQueue({ events: JSON.stringify(eventQueue) }); eventQueue = []; CountlyStorage.storeSet("cly_event", eventQueue); } // end current session Countly.end_session(); // clear timed events timedEvents = {}; // clear all consents Countly.remove_consent_internal(Countly.features, false); } var oldId = Countly.device_id; Countly.device_id = newId; deviceIdType = cc.deviceIdTypeEnums.DEVELOPER_SUPPLIED; CountlyStorage.storeSet("cly_id", Countly.device_id); CountlyStorage.storeSet("cly_id_type", deviceIdType); if (merge) { if (Countly.check_any_consent()) { toRequestQueue({ old_device_id: oldId }); } else { lastParams.change_id = arguments; } } else { // start new session for new id Countly.begin_session(!autoExtend); } if (Countly.remote_config) { remoteConfigs = {}; if (cluster.isMaster) { CountlyStorage.storeSet("cly_remote_configs", remoteConfigs); } Countly.fetch_remote_config(Countly.remote_config); } } } else { process.send({ cly: { change_id: newId, merge } }); } }; /** * Report custom event * @param {Event} event - Countly {@link Event} object * @param {string} event.key - name or id of the event * @param {number} [event.count=1] - how many times did event occur * @param {number=} event.sum - sum to report with event (if any) * @param {number=} event.dur - duration to report with event (if any) * @param {Object=} event.segmentation - object with segments key /values * */ Countly.add_event = function(event) { cc.log(`Trying to add the event: [${event.key}].`); // initially no consent is given var respectiveConsent = false; // to match the internal events and their respective required consents. Sets respectiveConsent to true if the consent is given switch (event.key) { case cc.internalEventKeyEnums.NPS: respectiveConsent = Countly.check_consent('feedback'); break; case cc.internalEventKeyEnums.SURVEY: respectiveConsent = Countly.check_consent('feedback'); break; case cc.internalEventKeyEnums.STAR_RATING: respectiveConsent = Countly.check_consent('star-rating'); break; case cc.internalEventKeyEnums.VIEW: respectiveConsent = Countly.check_consent('views'); break; case cc.internalEventKeyEnums.ORIENTATION: respectiveConsent = Countly.check_consent('users'); break; case cc.internalEventKeyEnums.PUSH_ACTION: respectiveConsent = Countly.check_consent('push'); break; case cc.internalEventKeyEnums.ACTION: respectiveConsent = Countly.check_consent('clicks') || Countly.check_consent('scroll'); break; default: respectiveConsent = Countly.check_consent('events'); } // if consent is given adds event to the queue if (respectiveConsent) { add_cly_events(event); } }; /** * Add events to event queue * @memberof Countly._internals * @param {Event} event - countly event */ function add_cly_events(event) { if (!event.key) { cc.log(cc.logLevelEnums.ERROR, "add_cly_events, Event must have 'key' property."); return; } if (cluster.isMaster) { if (!event.count) { event.count = 1; } event.key = cc.truncateSingleValue(event.key, Countly.maxKeyLength, "add_cly_event", Countly.debug); event.segmentation = cc.truncateObject(event.segmentation, Countly.maxKeyLength, Countly.maxValueSize, Countly.maxSegmentationValues, "add_cly_event", Countly.debug); var props = ["key", "count", "sum", "dur", "segmentation"]; var e = cc.getProperties(event, props); e.timestamp = getMsTimestamp(); var date = new Date(); e.hour = date.getHours(); e.dow = date.getDay(); cc.log(cc.logLevelEnums.DEBUG, "add_cly_events, Adding event: ", event); eventQueue.push(e); CountlyStorage.storeSet("cly_event", eventQueue); } else { process.send({ cly: { event } }); } } /** * Start timed event, which will fill in duration property upon ending automatically * @param {string} key - event name that will be used as key property * */ Countly.start_event = function(key) { key = cc.truncateSingleValue(key, Countly.maxKeyLength, "start_event", Countly.debug); if (timedEvents[key]) { cc.log(cc.logLevelEnums.ERROR, `start_event, Timed event with key: [${key}] already exists.`); return; } cc.log(cc.logLevelEnums.INFO, `start_event, Timer for timed event with key: [${key}] starting.`); timedEvents[key] = cc.getTimestamp(); }; /** * End timed event * @param {string|Object} event - event key if string or Countly event same as passed to {@link Countly.add_event} * */ Countly.end_event = function(event) { if (typeof event === "string") { event = cc.truncateSingleValue(event, Countly.maxKeyLength, "end_event", Countly.debug); event = { key: event }; } if (!event.key) { cc.log(cc.logLevelEnums.ERROR, "end_event, Event must have 'key' property."); return; } if (!timedEvents[event.key]) { cc.log(cc.logLevelEnums.ERROR, `end_event, Timed event with key: [${event.key}] does not exist.`); return; } event.key = cc.truncateSingleValue(event.key, Countly.maxKeyLength, "end_event"); cc.log(cc.logLevelEnums.INFO, `end_event, Timer for timed event with key: [${event.key}] is stopping.`); event.dur = cc.getTimestamp() - timedEvents[event.key]; Countly.add_event(event); delete timedEvents[event.key]; }; /** * Report user data * @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.gender - M value for male and F value for femail * @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 * */ Countly.user_details = function(user) { cc.log(cc.logLevelEnums.INFO, "user_details, Adding user details: ", user); 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"); toRequestQueue({ user_details: JSON.stringify(cc.getProperties(user, props)) }); } }; /** ************************ * Modifying custom property values of user details * Possible modification commands * - inc, to increment existing value by provided value * - mul, to multiply existing value by provided value * - max, to select maximum value between existing and provided value * - min, to select minimum value between existing and provided value * - setOnce, to set value only if it was not set before * - push, creates an array property, if property does not exist, and adds value to array * - 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; } if (Countly.check_consent("users")) { if (!customData[key]) { customData[key] = {}; } if (mod === "$push" || mod === "$pull" || mod === "$addToSet") { if (!customData[key][mod]) { customData[key][mod] = []; } customData[key][mod].push(value); } else { customData[key][mod] = value; } } }; /** * Control user related custom properties. Don't forget to call save after finishing manipulation of custom data * @namespace Countly.userData * @name Countly.userData * @example * //set custom key value property * Countly.userData.set("twitter", "ar2rsawseen"); * //create or increase specific number property * Countly.userData.increment("login_count"); * //add new value to array property if it is not already there * Countly.userData.push_unique("selected_category", "IT"); * //send all custom property modified data to server * Countly.userData.save(); */ Countly.userData = { /** * 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(key, value) { key = cc.truncateSingleValue(key, Countly.maxKeyLength, "set", Countly.debug); value = cc.truncateSingleValue(value, Countly.maxValueSize, "set", Countly.debug); customData[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) { 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"); }, /** * 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] = ""; }, /** * 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"); }, /** * 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) { 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"); }, /** * 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) { key = cc.truncateSingleValue(key, Countly.maxKeyLength, "multiply", Countly.debug); value = cc.truncateSingleValue(value, Countly.maxValueSize, "multiply", Countly.debug); 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) { key = cc.truncateSingleValue(key, Countly.maxKeyLength, "max", Countly.debug); value = cc.truncateSingleValue(value, Countly.maxValueSize, "max", Countly.debug); 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) { key = cc.truncateSingleValue(key, Countly.maxKeyLength, "min", Countly.debug); value = cc.truncateSingleValue(value, Countly.maxValueSize, "min", Countly.debug); 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) { key = cc.truncateSingleValue(key, Countly.maxKeyLength, "push", Countly.debug); value = cc.truncateSingleValue(value, Countly.maxValueSize, "push", Countly.debug); 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) { 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"); }, /** * 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) { key = cc.truncateSingleValue(key, Countly.maxKeyLength, "pull", Countly.debug); value = cc.truncateSingleValue(value, Countly.maxValueSize, "pull", Countly.debug); 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")) { toRequestQueue({ user_details: JSON.stringify({ custom: customData }) }); } customData = {}; }, }; /** * Report user conversion to the server (when user signup or made a purchase, or whatever your conversion is) * @param {string} campaign_id - id of campaign, the last part of the countly campaign link * @param {string=} campaign_user_id - id of user's clicked on campaign link, if you have one * */ Countly.report_conversion = function(campaign_id, campaign_user_id) { if (Countly.check_consent("attribution")) { if (campaign_id && campaign_user_id) { campaign_id = cc.truncateSingleValue(campaign_id, Countly.maxValueSize, "report_conversion", Countly.debug); campaign_user_id = cc.truncateSingleValue(campaign_user_id, Countly.maxValueSize, "report_conversion", Countly.debug); toRequestQueue({ campaign_id: campaign_id, cam