countly-sdk-web-cip
Version:
Countly Web SDK
1,188 lines (1,117 loc) • 150 kB
JavaScript
/************
* 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