UNPKG

countly-sdk-web-cip

Version:
1,188 lines (1,117 loc) 150 kB
/************ * Countly Web SDK * https://github.com/Countly/countly-sdk-web ************/ /** * 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-web} * @name Countly * @global * @namespace Countly * @example <caption>SDK integration</caption> * <script type="text/javascript"> * * //some default pre init * var Countly = Countly || {}; * Countly.q = Countly.q || []; * * //provide your app key that you retrieved from Countly dashboard * Countly.app_key = "YOUR_APP_KEY"; * * //provide your server IP or name. Use try.count.ly for EE trial server. * //if you use your own server, make sure you have https enabled if you use * //https below. * Countly.url = "https://yourdomain.com"; * * //start pushing function calls to queue * //track sessions automatically * Countly.q.push(["track_sessions"]); * * //track sessions automatically * Countly.q.push(["track_pageview"]); * * //load countly script asynchronously * (function() { * var cly = document.createElement("script"); cly.type = "text/javascript"; * cly.async = true; * //enter url of script here * cly.src = "https://cdn.jsdelivr.net/countly-sdk-web/latest/countly.min.js"; * cly.onload = function(){Countly.init()}; * var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(cly, s); * })(); * </script> */ (function(root, factory) { if (typeof define === "function" && define.amd) { define([], function() { return factory(root.Countly); }); } else if (typeof module === "object" && module.exports) { module.exports = factory(root.Countly); } else { root.Countly = factory(root.Countly); } }(typeof window !== "undefined" ? window : this, function(Countly) { "use strict"; // Make sure the code is being run in a browser // eslint-disable-next-line no-undef if (typeof (window) === 'undefined') { return; } /** @lends Countly */ Countly = Countly || {}; /** * Array with list of available features that you can require consent for * @example * Countly.features = ["sessions", "events", "views", "scrolls", "clicks", "forms", "crashes", "attribution", "users", "star-rating", "location", "apm", "feedback", "remote-config"]; */ Countly.features = ["sessions", "events", "views", "scrolls", "clicks", "forms", "crashes", "attribution", "users", "star-rating", "location", "apm", "feedback", "remote-config"]; /** * Object with which UTM tags to check and record * @example * Countly.utm = {"source": true, "medium": true, "campaign": true, "term": true, "content": true}; */ Countly.utm = {"source": true, "medium": true, "campaign": true, "term": true, "content": true}; /** * Async api queue, push commands here to be executed when script is loaded or after * @example <caption>Add command as array</caption> * Countly.q.push(['add_event',{ * key:"asyncButtonClick", * segmentation: { * "id": ob.id * } * }]); */ Countly.q = Countly.q || []; /** * Array of functions that are waiting to be notified that script has loaded and instantiated * @example * Countly.onload.push(function(){ * console.log("script loaded"); * }); */ Countly.onload = Countly.onload || []; var SDK_VERSION = "20.11.3"; var SDK_NAME = "javascript_native_web"; var urlParseRE = /^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/; var searchBotRE = /(CountlySiteBot|nuhk|Googlebot|GoogleSecurityScanner|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver|bingbot|Google Web Preview|Mediapartners-Google|AdsBot-Google|Baiduspider|Ezooms|YahooSeeker|AltaVista|AVSearch|Mercator|Scooter|InfoSeek|Ultraseek|Lycos|Wget|YandexBot|Yandex|YaDirectFetcher|SiteBot|Exabot|AhrefsBot|MJ12bot|TurnitinBot|magpie-crawler|Nutch Crawler|CMS Crawler|rogerbot|Domnutch|ssearch_bot|XoviBot|netseer|digincore|fr-crawler|wesee|AliasIO|contxbot|PingdomBot|BingPreview|HeadlessChrome)/; /** * @this Countly * @param {Object} ob - Configuration object */ var CountlyClass = function(ob) { var self = this, global = !Countly.i, sessionStarted = false, apiPath = "/i", readPath = "/o/sdk", beatInterval = getConfig("interval", ob, 500), queueSize = getConfig("queue_size", ob, 1000), requestQueue = [], eventQueue = [], remoteConfigs = {}, crashLogs = [], timedEvents = {}, ignoreReferrers = getConfig("ignore_referrers", ob, []), crashSegments = null, autoExtend = true, lastBeat, storedDuration = 0, lastView = null, lastViewTime = 0, lastViewStoredDuration = 0, failTimeout = 0, failTimeoutAmount = getConfig("fail_timeout", ob, 60), inactivityTime = getConfig("inactivity_time", ob, 20), inactivityCounter = 0, sessionUpdate = getConfig("session_update", ob, 60), maxEventBatch = getConfig("max_events", ob, 10), maxCrashLogs = getConfig("max_logs", ob, 100), useSessionCookie = getConfig("use_session_cookie", ob, true), sessionCookieTimeout = getConfig("session_cookie_timeout", ob, 30), readyToProcess = true, hasPulse = false, offlineMode = getConfig("offline_mode", ob, false), syncConsents = {}, lastParams = {}, trackTime = true, startTime = getTimestamp(), lsSupport = true, firstView = null; try { localStorage.setItem("cly_testLocal", true); //clean up test localStorage.removeItem('cly_testLocal'); } catch (e) { lsSupport = false; } //create object to store consents var consents = {}; for (var it = 0; it < Countly.features.length; it++) { consents[Countly.features[it]] = {}; } this.initialize = function() { this.serialize = ob.serialize || Countly.serialize; this.deserialize = ob.deserialize || Countly.deserialize; this.getViewName = ob.getViewName || Countly.getViewName; this.getViewUrl = ob.getViewUrl || Countly.getViewUrl; this.namespace = getConfig("namespace", ob, ""); this.app_key = getConfig("app_key", ob, null); this.onload = getConfig("onload", ob, []); this.utm = getConfig("utm", ob, {"source": true, "medium": true, "campaign": true, "term": true, "content": true}); this.ignore_prefetch = getConfig("ignore_prefetch", ob, true); this.debug = getConfig("debug", ob, false); this.test_mode = getConfig("test_mode", ob, false); this.metrics = getConfig("metrics", ob, {}); this.headers = getConfig("headers", ob, {}); this.url = stripTrailingSlash(getConfig("url", ob, "")); this.app_version = getConfig("app_version", ob, "0.0"); this.country_code = getConfig("country_code", ob, null); this.city = getConfig("city", ob, null); this.ip_address = getConfig("ip_address", ob, null); this.ignore_bots = getConfig("ignore_bots", ob, true); this.force_post = getConfig("force_post", ob, false); this.remote_config = getConfig("remote_config", ob, false); this.ignore_visitor = getConfig("ignore_visitor", ob, false); this.require_consent = getConfig("require_consent", ob, false); this.track_domains = getConfig("track_domains", ob, true); this.storage = getConfig("storage", ob, "default"); if (this.storage === "cookie") { lsSupport = false; } if (!Array.isArray(ignoreReferrers)) { ignoreReferrers = []; } if (this.url === "") { log("Please provide server URL"); this.ignore_visitor = true; } if (store("cly_ignore")) { //opted out user this.ignore_visitor = true; } migrate(); if (!offlineMode) { this.device_id = getConfig("device_id", ob, getId()); } else if (!this.device_id) { this.device_id = "[CLY]_temp_id"; } requestQueue = store("cly_queue") || []; eventQueue = store("cly_event") || []; remoteConfigs = store("cly_remote_configs") || {}; checkIgnore(); if (window.name && window.name.indexOf("cly:") === 0) { try { this.passed_data = JSON.parse(window.name.replace("cly:", "")); } catch (ex) { log("Could not parse name", window.name); } } else if (location.hash && location.hash.indexOf("#cly:") === 0) { try { this.passed_data = JSON.parse(location.hash.replace("#cly:", "")); } catch (ex) { log("Could not parse hash", location.hash); } } if ((this.passed_data && this.passed_data.app_key && this.passed_data.app_key === this.app_key) || (this.passed_data && !this.passed_data.app_key && global)) { if (this.passed_data.token && this.passed_data.purpose) { if (this.passed_data.token !== store("cly_old_token")) { setToken(this.passed_data.token); store("cly_old_token", this.passed_data.token); } this.passed_data.url = this.passed_data.url || this.url; if (this.passed_data.purpose === "heatmap") { this.ignore_visitor = true; showLoader(); loadJS(this.passed_data.url + "/views/heatmap.js", hideLoader); } } } if (!this.ignore_visitor) { log("Countly initialized"); if (location.search) { var parts = location.search.substring(1).split("&"); var utms = {}; var hasUTM = false; for (var i = 0; i < parts.length; i++) { var nv = parts[i].split("="); if (nv[0] === "cly_id") { store("cly_cmp_id", nv[1]); } else if (nv[0] === "cly_uid") { store("cly_cmp_uid", nv[1]); } else if (nv[0] === "cly_device_id") { this.device_id = nv[1]; } else if ((nv[0] + "").indexOf("utm_") === 0 && this.utm[nv[0].replace("utm_", "")]) { utms[nv[0].replace("utm_", "")] = nv[1]; hasUTM = true; } } if (hasUTM) { for (var tag in this.utm) { if (utms[tag]) { this.userData.set("utm_" + tag, utms[tag]); } else { this.userData.unset("utm_" + tag); } } this.userData.save(); } } if (!offlineMode) { if (this.device_id !== store("cly_id")) { store("cly_id", this.device_id); } } notifyLoaders(); setTimeout(function() { heartBeat(); if (self.remote_config) { self.fetch_remote_config(self.remote_config); } }, 1); } document.documentElement.setAttribute('data-countly-useragent', navigator.userAgent); }; /** * 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","scrolls","clicks","forms","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"], * interaction:["scrolls","clicks","forms"] * }); * //After this call Countly.add_consent("activity") to allow "sessions","events","views" * //or call Countly.add_consent("interaction") to allow "scrolls","clicks","forms" * //or call Countly.add_consent("crashes") to allow some separate feature */ this.group_features = function(features) { if (features) { for (var i in features) { if (!consents[i]) { if (typeof features[i] === "string") { consents[i] = { features: [features[i]] }; } else if (features[i] && Array.isArray(features[i]) && features[i].length) { consents[i] = { features: features[i] }; } else { log("Incorrect feature list for " + i + " value: " + features[i]); } } else { log("Feature name " + i + " is already reserved"); } } } else { log("Incorrect features: " + 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","scrolls","clicks","forms","crashes","attribution","users" or custom provided through {@link Countly.group_features} * @returns {Boolean} true if consent was given for the feature or false if it was not */ this.check_consent = function(feature) { if (!this.require_consent) { //we don't need to have specific consents return true; } if (consents[feature]) { return (consents[feature] && consents[feature].optin) ? true : false; } else { log("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 {Boolean} true is has any consent given, false if no consents given */ this.check_any_consent = function() { if (!this.require_consent) { //we don't need to have consents return true; } for (var i in consents) { if (consents[i] && consents[i].optin) { return true; } } return false; }; /** * 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","scrolls","clicks","forms","crashes","attribution","users", etc or custom provided through {@link Countly.group_features} */ this.add_consent = function(feature) { log("Adding consent for " + feature); if (Array.isArray(feature)) { for (var i = 0; i < feature.length; i++) { this.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 this.add_consent(consents[feature].features); } else { //this is core feature if (consents[feature].optin !== true) { syncConsents[feature] = true; consents[feature].optin = true; updateConsent(); setTimeout(function() { if (feature === "sessions" && lastParams.begin_session) { self.begin_session.apply(self, lastParams.begin_session); lastParams.begin_session = null; } else if (feature === "views" && lastParams.track_pageview) { lastView = null; self.track_pageview.apply(self, lastParams.track_pageview); lastParams.track_pageview = null; } if (lastParams.change_id) { self.change_id.apply(self, lastParams.change_id); lastParams.change_id = null; } }, 1); } } } else { log("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","scrolls","clicks","forms","crashes","attribution","users", etc or custom provided through {@link Countly.group_features} */ this.remove_consent = function(feature) { log("Removing consent for " + feature); if (Array.isArray(feature)) { for (var i = 0; i < feature.length; i++) { this.remove_consent(feature[i]); } } else if (consents[feature]) { if (consents[feature].features) { //this is added group, let's iterate through sub features this.remove_consent(consents[feature].features); } else { //this is core feature if (consents[feature].optin !== false) { syncConsents[feature] = false; updateConsent(); } } consents[feature].optin = false; } else { log("No feature available for " + feature); } }; var consentTimer; var updateConsent = function() { if (consentTimer) { //delay syncing consents clearTimeout(consentTimer); consentTimer = null; } consentTimer = setTimeout(function() { if (hasAnyProperties(syncConsents)) { //we have consents to sync, create request toRequestQueue({ consent: JSON.stringify(syncConsents) }); //clear consents that needs syncing syncConsents = {}; } }, 1000); }; this.enable_offline_mode = function() { offlineMode = true; this.device_id = "[CLY]_temp_id"; }; this.disable_offline_mode = function(device_id) { offlineMode = false; if (device_id && this.device_id !== device_id) { this.device_id = device_id; store("cly_id", this.device_id); log("Changing id"); } else { this.device_id = getId(); if (this.device_id !== store("cly_id")) { store("cly_id", this.device_id); } } var needResync = false; if (requestQueue.length > 0) { for (var i = 0; i < requestQueue.length; i++) { if (requestQueue[i].device_id === "[CLY]_temp_id") { requestQueue[i].device_id = this.device_id; needResync = true; } } } if (needResync) { store("cly_queue", requestQueue, true); } }; /** * Start session * @param {boolean} noHeartBeat - true if you don't want to use internal heartbeat to manage session * @param {bool} force - force begin session request even if session cookie is enabled */ this.begin_session = function(noHeartBeat, force) { if (this.check_consent("sessions")) { if (!sessionStarted) { //report orientation this.report_orientation(); add_event(window, "resize", function() { self.report_orientation(); }); lastBeat = getTimestamp(); sessionStarted = true; autoExtend = (noHeartBeat) ? false : true; var expire = store("cly_session"); if (force || !useSessionCookie || !expire || parseInt(expire) <= getTimestamp()) { log("Session started"); if (firstView === null) { firstView = true; } var req = {}; req.begin_session = 1; req.metrics = JSON.stringify(getMetrics()); toRequestQueue(req); } store("cly_session", getTimestamp() + (sessionCookieTimeout * 60)); } } else { lastParams.begin_session = arguments; } }; /** * Report session duration * @param {int} sec - amount of seconds to report for current session */ this.session_duration = function(sec) { if (this.check_consent("sessions")) { if (sessionStarted) { log("Session extended", sec); toRequestQueue({ session_duration: sec }); extendSession(); } } }; /** * End current session * @param {int} sec - amount of seconds to report for current session, before ending it * @param {bool} force - force end session request even if session cookie is enabled */ this.end_session = function(sec, force) { if (this.check_consent("sessions")) { if (sessionStarted) { sec = sec || getTimestamp() - lastBeat; reportViewDuration(); if (!useSessionCookie || force) { log("Ending session"); toRequestQueue({ end_session: 1, session_duration: sec }); } else { this.session_duration(sec); } sessionStarted = false; } } }; /** * 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 **/ this.change_id = function(newId, merge) { if (this.device_id !== newId) { if (!merge) { //empty event queue if (eventQueue.length > 0) { toRequestQueue({ events: JSON.stringify(eventQueue) }); eventQueue = []; store("cly_event", eventQueue); } //end current session this.end_session(null, true); //clear timed events timedEvents = {}; } var oldId = this.device_id; this.device_id = newId; store("cly_id", this.device_id); log("Changing id"); if (merge) { if (this.check_any_consent()) { toRequestQueue({ old_device_id: oldId }); } else { lastParams.change_id = arguments; } } else { //start new session for new id this.begin_session(!autoExtend, true); } if (this.remote_config) { remoteConfigs = {}; store("cly_remote_configs", remoteConfigs); this.fetch_remote_config(this.remote_config); } } }; /** * Report custom event * @param {Object} 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 **/ this.add_event = function(event) { if (this.check_consent("events")) { add_cly_events(event); } }; /** * Add events to event queue * @memberof Countly._internals * @param {Event} event - countly event */ function add_cly_events(event) { //ignore bots if (self.ignore_visitor) { return; } if (!event.key) { log("Event must have key property"); return; } if (!event.count) { event.count = 1; } var props = ["key", "count", "sum", "dur", "segmentation"]; var e = getProperties(event, props); e.timestamp = getMsTimestamp(); var date = new Date(); e.hour = date.getHours(); e.dow = date.getDay(); eventQueue.push(e); store("cly_event", eventQueue); log("Adding event: ", 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 **/ this.start_event = function(key) { if (timedEvents[key]) { log("Timed event with key " + key + " already started"); return; } timedEvents[key] = getTimestamp(); }; /** * End timed event * @param {string|object} event - event key if string or Countly event same as passed to {@link Countly.add_event} **/ this.end_event = function(event) { if (typeof event === "string") { event = { key: event }; } if (!event.key) { log("Event must have key property"); return; } if (!timedEvents[event.key]) { log("Timed event with key " + event.key + " was not started"); return; } event.dur = getTimestamp() - timedEvents[event.key]; this.add_event(event); delete timedEvents[event.key]; }; /** * Report device orientation * @param {string=} orientation - orientation as landscape or portrait **/ this.report_orientation = function(orientation) { if (this.check_consent("users")) { add_cly_events({ "key": "[CLY]_orientation", "segmentation": { "mode": orientation || getOrientation() } }); } }; /** * Report user conversion to the server (when user signup or made a purchase, or whatever your conversion is), if there is no campaign data, user will be reported as organic * @param {string=} campaign_id - id of campaign, or will use the one that is stored after campaign link click * @param {string=} campaign_user_id - id of user's click on campaign, or will use the one that is stored after campaign link click **/ this.report_conversion = function(campaign_id, campaign_user_id) { if (this.check_consent("attribution")) { campaign_id = campaign_id || store("cly_cmp_id") || "cly_organic"; campaign_user_id = campaign_user_id || store("cly_cmp_uid"); if (campaign_id && campaign_user_id) { toRequestQueue({ campaign_id: campaign_id, campaign_user: campaign_user_id }); } else if (campaign_id) { toRequestQueue({ campaign_id: campaign_id }); } } }; /** * Provide information about user * @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 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 **/ this.user_details = function(user) { if (this.check_consent("users")) { log("Adding user details: ", user); var props = ["name", "username", "email", "organization", "phone", "picture", "gender", "byear", "custom"]; toRequestQueue({ user_details: JSON.stringify(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) { if (self.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(); */ this.userData = { /** * Sets user's custom property value * @memberof Countly.userData * @param {string} key - name of the property to attach to user * @param {string|number} value - value to store under provided property **/ set: function(key, value) { customData[key] = value; }, /** * Unset/deletes user's custom property * @memberof Countly.userData * @param {string} key - name of the property to delete **/ unset: function(key) { customData[key] = ""; }, /** * Sets user's custom property value only if it was not set before * @memberof Countly.userData * @param {string} key - name of the property to attach to user * @param {string|number} value - value to store under provided property **/ set_once: function(key, value) { change_custom_property(key, value, "$setOnce"); }, /** * Increment value under the key of this user's custom properties by one * @memberof Countly.userData * @param {string} key - name of the property to attach to user **/ increment: function(key) { change_custom_property(key, 1, "$inc"); }, /** * Increment value under the key of this user's custom properties by provided value * @memberof Countly.userData * @param {string} key - name of the property to attach to user * @param {number} value - value by which to increment server value **/ increment_by: function(key, value) { change_custom_property(key, value, "$inc"); }, /** * Multiply value under the key of this user's custom properties by provided value * @memberof Countly.userData * @param {string} key - name of the property to attach to user * @param {number} value - value by which to multiply server value **/ multiply: function(key, value) { change_custom_property(key, value, "$mul"); }, /** * Save maximal value under the key of this user's custom properties * @memberof Countly.userData * @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: function(key, value) { change_custom_property(key, value, "$max"); }, /** * Save minimal value under the key of this user's custom properties * @memberof Countly.userData * @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: function(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 * @memberof Countly.userData * @param {string} key - name of the property to attach to user * @param {string|number} value - value which to add to array **/ push: function(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 * @memberof Countly.userData * @param {string} key - name of the property to attach to user * @param {string|number} value - value which to add to array **/ push_unique: function(key, value) { change_custom_property(key, value, "$addToSet"); }, /** * Remove value from array under the key of this user's custom properties * @memberof Countly.userData * @param {string} key - name of the property * @param {string|number} value - value which to remove from array **/ pull: function(key, value) { change_custom_property(key, value, "$pull"); }, /** * Save changes made to user's custom properties object and send them to server * @memberof Countly.userData **/ save: function() { if (self.check_consent("users")) { toRequestQueue({ user_details: JSON.stringify({ custom: customData }) }); } customData = {}; } }; /** * Report performance trace * @param {Object} trace - apm trace object * @param {string} trace.type - device or network * @param {string} trace.name - url or view of the trace * @param {number} trace.stz - start timestamp * @param {number} trace.etz - end timestamp * @param {Object} trace.app_metrics - key/value metrics like duration, to report with trace where value is number * @param {Object=} trace.apm_attr - object profiling attributes (not yet supported) */ this.report_trace = function(trace) { if (this.check_consent("apm")) { var props = ["type", "name", "stz", "etz", "apm_metrics", "apm_attr"]; for (var i = 0; i < props.length; i++) { if (props[i] !== "apm_attr" && typeof trace[props[i]] === "undefined") { log("APM trace must have a", props[i]); return; } } var e = getProperties(trace, props); e.timestamp = trace.stz; var date = new Date(); e.hour = date.getHours(); e.dow = date.getDay(); toRequestQueue({ apm: JSON.stringify(e) }); log("Adding APM trace: ", e); } }; /** * Automatically track javascript errors that happen on the website and report them to the server * @param {string=} segments - additional key value pairs you want to provide with error report, like versions of libraries used, etc. **/ this.track_errors = function(segments) { Countly.i[this.app_key].tracking_crashes = true; if (!window.cly_crashes) { window.cly_crashes = true; crashSegments = segments; //override global uncaught error handler window.onerror = function(msg, url, line, col, err) { if (typeof err !== "undefined") { dispatchErrors(err, false); } else { col = col || (window.event && window.event.errorCharacter); var error = ""; if (typeof msg !== "undefined") { error += msg + "\n"; } if (typeof url !== "undefined") { error += "at " + url; } if (typeof line !== "undefined") { error += ":" + line; } if (typeof col !== "undefined") { error += ":" + col; } error += "\n"; try { var stack = []; // eslint-disable-next-line no-caller var f = arguments.callee.caller; while (f) { stack.push(f.name); f = f.caller; } error += stack.join("\n"); } catch (ex) { //silent error } dispatchErrors(error, false); } }; window.addEventListener('unhandledrejection', function(event) { dispatchErrors(new Error('Unhandled rejection (reason: ' + (event.reason && event.reason.stack ? event.reason.stack : event.reason) + ').'), true); }); } }; /** * Log an exception that you caught through try and catch block and handled yourself and just want to report it to server * @param {Object} err - error exception object provided in catch block * @param {string=} segments - additional key value pairs you want to provide with error report, like versions of libraries used, etc. **/ this.log_error = function(err, segments) { this.recordError(err, true, segments); }; /** * Add new line in the log of breadcrumbs of what user did, will be included together with error report * @param {string} record - any text describing what user did **/ this.add_log = function(record) { if (this.check_consent("crashes")) { if (crashLogs.length > maxCrashLogs) { crashLogs.shift(); } crashLogs.push(record); } }; /** * Fetch remote config * @param {array=} keys - Array of keys to fetch, if not provided will fetch all keys * @param {array=} omit_keys - Array of keys to omit, if provided will fetch all keys except provided ones * @param {function=} callback - Callback to notify with first param error and second param remote config object **/ this.fetch_remote_config = function(keys, omit_keys, callback) { if (this.check_consent("remote-config")) { var request = { method: "fetch_remote_config" }; if (this.check_consent("sessions")) { request.metrics = JSON.stringify(getMetrics()); } if (keys) { if (!callback && typeof keys === "function") { callback = keys; keys = null; } else if (Array.isArray(keys) && keys.length) { request.keys = JSON.stringify(keys); } } if (omit_keys) { if (!callback && typeof omit_keys === "function") { callback = omit_keys; omit_keys = null; } else if (Array.isArray(omit_keys) && omit_keys.length) { request.omit_keys = JSON.stringify(omit_keys); } } prepareRequest(request); sendXmlHttpRequest(this.url + readPath, request, function(err, params, responseText) { try { var configs = JSON.parse(responseText); if (request.keys || request.omit_keys) { //we merge config for (var i in configs) { remoteConfigs[i] = configs[i]; } } else { //we replace config remoteConfigs = configs; } store("cly_remote_configs", remoteConfigs); } catch (ex) { //silent catch } if (typeof callback === "function") { callback(err, remoteConfigs); } }); } else { log("Remote config requires explicit consent"); if (typeof callback === "function") { callback(new Error("Remote config requires explicit consent"), remoteConfigs); } } }; /** * Get Remote config object or specific value for provided key * @param {string=} key - if provided, will return value for key, or return whole object * @returns {object} remote configs **/ this.get_remote_config = function(key) { if (typeof key !== "undefined") { return remoteConfigs[key]; } return remoteConfigs; }; /** * Stop tracking duration time for this user **/ this.stop_time = function() { if (trackTime) { trackTime = false; storedDuration = getTimestamp() - lastBeat; lastViewStoredDuration = getTimestamp() - lastViewTime; } }; /** * Start tracking duration time for this user, by default it is automatically tracked if you are using internal session handling **/ this.start_time = function() { if (!trackTime) { trackTime = true; lastBeat = getTimestamp() - storedDuration; lastViewTime = getTimestamp() - lastViewStoredDuration; lastViewStoredDuration = 0; extendSession(); } }; /** * Track user sessions automatically, including time user spent on your website **/ this.track_sessions = function() { //start session this.begin_session(); this.start_time(); //end session on unload add_event(window, "beforeunload", function() { self.end_session(); }); //manage sessions on window visibility events var hidden = "hidden"; /** * Handle visibility change events */ function onchange() { if (document[hidden]) { self.stop_time(); } else { self.start_time(); } } //Page Visibility API if (hidden in document) { document.addEventListener("visibilitychange", onchange); } else if ((hidden = "mozHidden") in document) { document.addEventListener("mozvisibilitychange", onchange); } else if ((hidden = "webkitHidden") in document) { document.addEventListener("webkitvisibilitychange", onchange); } else if ((hidden = "msHidden") in document) { document.addEventListener("msvisibilitychange", onchange); } // IE 9 and lower: else if ("onfocusin" in document) { add_event(window, "focusin", function() { self.start_time(); }); add_event(window, "focusout", function() { self.stop_time(); }); } // All others: else { //old way add_event(window, "focus", function() { self.start_time(); }); add_event(window, "blur", function() { self.stop_time(); }); //newer mobile compatible way add_event(window, "pageshow", function() { self.start_time(); }); add_event(window, "pagehide", function() { self.s