<commit_before>/************
* Countly Web SDK
* https://github.com/Countly/countly-sdk-web
************/
(function (Countly) {
	'use strict';

	var inited = false,
		sessionStarted = false,
		apiPath = "/i",
		beatInterval = 500,
		requestQueue = [],
		eventQueue = [],
        crashLogs = [],
        timedEvents = {},
        crashSegments = null,
		autoExtend = true,
		lastBeat,
        storedDuration = 0,
        lastView = null,
        lastViewTime = 0,
        lastViewStoredDuration = 0,
        failTimeout = 0,
        failTimeoutAmount = 60,
        readyToProcess = true,
        platform,
        urlParseRE = /^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/,
        searchBotRE = /(nuhk|Googlebot|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)/,
        isBot = false,
        trackTime = true,
        startTime;
	
	Countly.init = function(ob){
		if(!inited){
            startTime = getTimestamp();
			inited = true;
			requestQueue = store("cly_queue") || [];
            timedEvents = store("cly_timed") || {};
			ob = ob || {};
            beatInterval = ob.interval || Countly.interval || beatInterval;
            failTimeoutAmount = ob.fail_timeout || Countly.fail_timeout || failTimeoutAmount;
			Countly.debug = ob.debug || Countly.debug || false;
			Countly.app_key = ob.app_key || Countly.app_key || null;
			Countly.device_id = ob.device_id || Countly.device_id || getId();
			Countly.url = stripTrailingSlash(ob.url || Countly.url || "https://cloud.count.ly");
			Countly.app_version = ob.app_version || Countly.app_version || "0.0";
			Countly.country_code = ob.country_code || Countly.country_code || null;
			Countly.city = ob.city || Countly.city || null;
			Countly.ip_address = ob.ip_address || Countly.ip_address || null;
			Countly.ignore_bots = ob.ignore_bots || Countly.ignore_bots || true;
			Countly.q = Countly.q || [];
            log("Countly initialized");
            if(searchBotRE.test(navigator.userAgent)){
                isBot = true;
            }
			if(Countly.q.constructor !== Array)
				Countly.q = [];
			heartBeat();
			store("cly_id", Countly.device_id);
            if (location.search) {
                var parts = location.search.substring(1).split('&');

                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]);
                }
            }
		}
	};
	
	Countly.begin_session = function(noHeartBeat){
		if(!sessionStarted){
			log("Session started");
			lastBeat = getTimestamp();
			sessionStarted = true;
			autoExtend = (noHeartBeat) ? false : true;
			var req = {};
			req.begin_session = 1;
			req.metrics = JSON.stringify(getMetrics());
			toRequestQueue(req);
		}
	};
	
	Countly.session_duration = function(sec){
		if(sessionStarted){
			log("Session extended", sec);
			toRequestQueue({session_duration:sec});
		}
	};
	
	Countly.end_session = function(sec){
		if(sessionStarted){
            sec = sec || getTimestamp()-lastBeat;
			log("Ending session");
            reportViewDuration();
			sessionStarted = false;
			toRequestQueue({end_session:1, session_duration:sec});
            heartBeat();
		}
	};
	
	Countly.change_id = function(newId, merge){
		var oldId = Countly.device_id;
		Countly.device_id = newId;
		store("cly_id", Countly.device_id);
		log("Changing id");
        if(merge)
            toRequestQueue({old_device_id:oldId});
	};
	
	Countly.add_event = function(event){
		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 = getTimestamp();
        var date = new Date();
        e.hour = date.getHours();
        e.dow = date.getDay();
		eventQueue.push(e);
		log("Adding event: ", event);
	};
    
    Countly.start_event = function(key){
        if(timedEvents[key]){
            log("Timed event with key " + key + " already started");
            return;
        }
        timedEvents[key] = getTimestamp();
        store("cly_timed", timedEvents);
    };
    
    Countly.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 " + key + " was not started");
            return;
        }
        event.dur = getTimestamp() - timedEvents[event.key];
        Countly.add_event(event);
        delete timedEvents[event.key];
        store("cly_timed", timedEvents);
    };
	
	Countly.user_details = function(user){
		log("Adding userdetails: ", user);
		var props = ["name", "username", "email", "organization", "phone", "picture", "gender", "byear", "custom"];
		toRequestQueue({user_details: JSON.stringify(getProperties(user, props))});
	};
    
    Countly.report_conversion = function(campaign_id, campaign_user_id){
        campaign_id = campaign_id || store("cly_cmp_id");
        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});
        else
            log("No campaign data found");
    };
    
    /**************************
    * 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(!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;
    };
    Countly.userData = {
        set: function(key, value){
            customData[key] = value;
        },
        set_once: function(key, value){
            change_custom_property(key, 1, "$setOnce");
        },
        increment: function(key){
            change_custom_property(key, 1, "$inc");
        },
        increment_by: function(key, value){
            change_custom_property(key, value, "$inc");
        },
        multiply: function(key, value){
            change_custom_property(key, value, "$mul");
        },
        max: function(key, value){
            change_custom_property(key, value, "$max");
        },
        min: function(key, value){
            change_custom_property(key, value, "$min");
        },
        push: function(key, value){
            change_custom_property(key, value, "$push");
        },
        push_unique: function(key, value){
            change_custom_property(key, value, "$addToSet");
        },
        pull: function(key, value){
            change_custom_property(key, value, "$pull");
        },
        save: function(){
            toRequestQueue({user_details: JSON.stringify({custom:customData})});
            customData = {};
        }
    };
    
    Countly.track_errors = function(segments){
        crashSegments = segments;
        window.onerror = function(msg, url, line, col, err) {
            if(typeof err !== "undefined")
                recordError(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 = [];
                    var f = arguments.callee.caller; // jshint ignore:line
                    while (f) {
                        stack.push(f.name);
                        f = f.caller;
                    }
                    error += stack.join("\n");
                }catch(ex){}
                recordError(error, false);
            }
        };
    };
    
    Countly.log_error = function(err, segments){
        recordError(err, true, segments);
    };
    
    Countly.add_log = function(record){
        crashLogs.push(record);
    };
    
    /**
	*  PULIC HELP METHODS FOR COMMON ACTIONS
	**/
    
    Countly.stop_time = function(){
        trackTime = false;
        storedDuration = getTimestamp() - lastBeat;
        lastViewStoredDuration = getTimestamp() - lastViewTime;
    };
    
    Countly.start_time = function(){
        trackTime = true;
        lastBeat = getTimestamp() - storedDuration;
        lastViewTime = getTimestamp() - lastViewStoredDuration;
        lastViewStoredDuration = 0;
    };
    
    Countly.track_sessions = function(){
        //start session
        Countly.begin_session();
        Countly.start_time();
        
        //end session on unload
        add_event(window, "beforeunload", function(){
            Countly.end_session();
        });
        add_event(window, "unload", function(){
            Countly.end_session();
        });
        
        //manage sessions on window visibility events
        var hidden = "hidden";
        
        function onchange(){
            if (document[hidden]) {
                Countly.stop_time();
            } else {
                Countly.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(){
                Countly.start_time();
            });
            add_event(window, "focusout", function(){
                Countly.stop_time();
            });
        }
        // All others:
        else{
            //old way
            add_event(window, "focus", function(){
                Countly.start_time();
            });
            add_event(window, "blur", function(){
                Countly.stop_time();
            });
            
            //newer mobile compatible way
            add_event(window, "pageshow", function(){
                Countly.start_time();
            });
            add_event(window, "pagehide", function(){
                Countly.stop_time();
            });
        }
    };
    
    Countly.track_pageview = function(page){
        reportViewDuration();
        page = page || window.location.pathname;
        lastView = page;
        lastViewTime = getTimestamp();
        if(!platform)
            getMetrics();
        var segments = {
            "name": page,
            "visit":1,
            "segment":platform,
            "domain":window.location.hostname
        };
        
        if(typeof document.referrer !== "undefined" && document.referrer.length){
            var matches = urlParseRE.exec(document.referrer);
            //do not report referrers of current website
            if(matches && matches[11] && matches[11] != window.location.hostname)
                segments.start = 1;
        }
        
        //track pageview
        Countly.add_event({
            "key": "[CLY]_view",
            "segmentation": segments
        });
    };
    
    Countly.track_clicks = function(){
        var shouldProcess = true;
        function processClick(event){
            if(shouldProcess){
                shouldProcess = false;
                
                //cross browser click coordinates
                get_page_coord(event);
                if(typeof event.pageX !== "undefined" && typeof event.pageY !== "undefined"){
                    var height = getDocHeight();
                    var width = getDocWidth();
                    
                    //record click event
                    Countly.add_event({
                        "key": "[CLY]_action",
                        "segmentation": {
                            "type": "click",
                            "x":event.pageX,
                            "y":event.pageY,
                            "width":width,
                            "height":height,
                            "domain":window.location.hostname
                        }
                    });
                }
                setTimeout(function(){shouldProcess = true;}, 1000);
            }
        }
        //add any events you want like pageView
        add_event(document, "mousedown", processClick);
        add_event(document, "mouseup", processClick);
        add_event(document, "click", processClick);
    };
    
    Countly.track_links = function(parent){
        parent = parent || document;
        function trackClicks(){
            function processClick(event){

                //get element which was clicked
                var elem = get_event_target(event);
                
                //cross browser click coordinates
                get_page_coord(event);
                
                //record click event
                Countly.add_event({
                    "key": "linkClick",
                    "segmentation": {
                        "href": elem.href,
                        "text": elem.innerText,
                        "id": elem.id,
                        "x":event.pageX,
                        "y":event.pageY
                    }
                });
                //anticipate page unload
                if(typeof elem.href !== "undefined" && elem.target !== '_blank' && !(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)){
                    var link = elem.href.replace(window.location.href.split("#")[0], "");
                    if(link.indexOf("#") !== 0){
                        //most probably user will leave the page
                        
                        //end Countly session
                        Countly.end_session();
                        
                        //prevent user from leaving
                        prevent_default(event);
                        
                        //allow user to leave after some time
                        setTimeout(function() {
                            window.location.href = elem.href;
                        }, 1000);
                    }
                }
            }
            //add any events you want like pageView
            if(typeof parent.getElementsByTagName !== "undefined"){
                var links = parent.getElementsByTagName("a");
                for(var i = 0; i < links.length; i++){
                    add_event(links[i], "click", processClick);
                }
            }
            else{
                log("Can't track clicks");
            }
        }
        if (document.readyState === "complete") {
            trackClicks();
        }
        else{
            add_event(window, "load", trackClicks);
        }
    };
    
    Countly.track_forms = function(parent){
        parent = parent || document;
        function getInputName(input){
            return input.name || input.id || input.type || input.nodeName;
        }
        function processForm(form){
            // use to avoid duplicate submits
            var submitted = false;
            add_event(form, "submit", function(event){
                if(!submitted){
                    submitted = true;
                    
                    var segmentation = {
                        "id": form.id,
                        "name": form.name,
                        "action": form.action,
                        "method": form.method
                    };
                    
                    //get input values
                    var input;
                    if(typeof form.elements !== "undefined")
                        for(var i = 0; i < form.elements.length; i++){
                            input = form.elements[i];
                            if(input.nodeName.toLowerCase() == "select"){
                                if(typeof input.multiple !== "undefined"){<commit_msg>Add JSDoc compatible comments<commit_after>/************
* Countly Web SDK
* https://github.com/Countly/countly-sdk-web
************/

/**
 * Countly object to manage the internal queue and send requests to Countly server
 * @name Countly
 * @global
 * @namespace Countly
 */
(function (Countly) {
	'use strict';

	var inited = false,
		sessionStarted = false,
		apiPath = "/i",
		beatInterval = 500,
		requestQueue = [],
		eventQueue = [],
        crashLogs = [],
        timedEvents = {},
        crashSegments = null,
		autoExtend = true,
		lastBeat,
        storedDuration = 0,
        lastView = null,
        lastViewTime = 0,
        lastViewStoredDuration = 0,
        failTimeout = 0,
        failTimeoutAmount = 60,
        readyToProcess = true,
        platform,
        urlParseRE = /^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/,
        searchBotRE = /(nuhk|Googlebot|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)/,
        isBot = false,
        trackTime = true,
        startTime;

/**
* Countly initialization object
* @typedef {Object} Init
* @property {string} app_key - app key for your app created in Countly
* @property {string} device_id - to identify a visitor, will be auto generated if not provided
* @property {string} [url=https://cloud.count.ly] - your Countly server url, you can use your own server URL or IP here
* @property {string} [app_version=0.0] - the version of your app or website
* @property {string=} country_code - country code for your visitor
* @property {string=} city - name of the city of your visitor
* @property {string=} ip_address - ip address of your visitor
* @property {boolean} [debug=false] - output debug info into console
* @property {boolean} [ignore_bots=true] - option to ignore traffic from bots
* @property {number} [interval=500] - set an interval how often to check if there is any data to report and report it in miliseconds
* @property {number} [fail_timeout=60] - set time in seconds to wait after failed connection to server in seconds
*/

/**
 * Initialize Countly object
 * @param {Init} conf - Countly initialization {@link Init} object with configuration options
 */
	Countly.init = function(ob){
		if(!inited){
            startTime = getTimestamp();
			inited = true;
			requestQueue = store("cly_queue") || [];
            timedEvents = store("cly_timed") || {};
			ob = ob || {};
            beatInterval = ob.interval || Countly.interval || beatInterval;
            failTimeoutAmount = ob.fail_timeout || Countly.fail_timeout || failTimeoutAmount;
			Countly.debug = ob.debug || Countly.debug || false;
			Countly.app_key = ob.app_key || Countly.app_key || null;
			Countly.device_id = ob.device_id || Countly.device_id || getId();
			Countly.url = stripTrailingSlash(ob.url || Countly.url || "https://cloud.count.ly");
			Countly.app_version = ob.app_version || Countly.app_version || "0.0";
			Countly.country_code = ob.country_code || Countly.country_code || null;
			Countly.city = ob.city || Countly.city || null;
			Countly.ip_address = ob.ip_address || Countly.ip_address || null;
			Countly.ignore_bots = ob.ignore_bots || Countly.ignore_bots || true;
			Countly.q = Countly.q || [];
            log("Countly initialized");
            if(searchBotRE.test(navigator.userAgent)){
                isBot = true;
            }
			if(Countly.q.constructor !== Array)
				Countly.q = [];
			heartBeat();
			store("cly_id", Countly.device_id);
            if (location.search) {
                var parts = location.search.substring(1).split('&');

                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]);
                }
            }
		}
	};
    
    /**
    * Start session
    * @param {boolean} noHeartBeat - true if you don't want to use internal heartbeat to manage session
    */
	
	Countly.begin_session = function(noHeartBeat){
		if(!sessionStarted){
			log("Session started");
			lastBeat = getTimestamp();
			sessionStarted = true;
			autoExtend = (noHeartBeat) ? false : true;
			var req = {};
			req.begin_session = 1;
			req.metrics = JSON.stringify(getMetrics());
			toRequestQueue(req);
		}
	};
	
    /**
    * Report session duration
    * @param {int} sec - amount of seconds to report for current session
    */
	Countly.session_duration = function(sec){
		if(sessionStarted){
			log("Session extended", sec);
			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(sessionStarted){
            sec = sec || getTimestamp()-lastBeat;
			log("Ending session");
            reportViewDuration();
			sessionStarted = false;
			toRequestQueue({end_session:1, session_duration:sec});
            heartBeat();
		}
	};
	
    /**
    * 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){
		var oldId = Countly.device_id;
		Countly.device_id = newId;
		store("cly_id", Countly.device_id);
		log("Changing id");
        if(merge)
            toRequestQueue({old_device_id:oldId});
	};
    
    /**
    * Countly custom event object
    * @typedef {Object} Event
    * @property {string} key - name or id of the event
    * @property {number} [count=1] - how many times did event occur
    * @property {number=} sum - sum to report with event (if any)
    * @property {number=} dur - duration to report with event (if any)
    * @property {Object=} segmentation - object with segments key /values
    */
	
    /**
    * Report custom event
    * @param {Event} event - Countly {@link Event} object
    **/
	Countly.add_event = function(event){
		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 = getTimestamp();
        var date = new Date();
        e.hour = date.getHours();
        e.dow = date.getDay();
		eventQueue.push(e);
		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
    **/
    Countly.start_event = function(key){
        if(timedEvents[key]){
            log("Timed event with key " + key + " already started");
            return;
        }
        timedEvents[key] = getTimestamp();
        store("cly_timed", timedEvents);
    };
    
    /**
    * End timed event
    * @param {string|Event} event - event key if string or Countly {@link Event} object
    **/
    Countly.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 " + key + " was not started");
            return;
        }
        event.dur = getTimestamp() - timedEvents[event.key];
        Countly.add_event(event);
        delete timedEvents[event.key];
        store("cly_timed", timedEvents);
    };
	
    /**
    * Countly user information object
    * @typedef {Object} UserDetails
    * @property {string=} name - user's full name
    * @property {string=} username - user's username or nickname
    * @property {string=} email - user's email address
    * @property {string=} organization - user's organization or company
    * @property {string=} phone - user's phone number
    * @property {string=} picture - url to user's picture
    * @property {string=} gender - M value for male and F value for femail
    * @property {number=} byear - user's birth year used to calculate current age
    * @property {Object=} custom - object with custom key value properties you want to save with user
    */
	
    /**
    * Report custom event
    * @param {UserDetails} user - Countly {@link UserDetails} object
    **/
	Countly.user_details = function(user){
		log("Adding userdetails: ", user);
		var props = ["name", "username", "email", "organization", "phone", "picture", "gender", "byear", "custom"];
		toRequestQueue({user_details: JSON.stringify(getProperties(user, props))});
	};
    
    /**
    * 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, 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
    **/
    Countly.report_conversion = function(campaign_id, campaign_user_id){
        campaign_id = campaign_id || store("cly_cmp_id");
        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});
        else
            log("No campaign data found");
    };
    
    /**************************
    * 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(!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;
    };
    
    /**
    * @namespace Countly.userData
    */
    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: function(key, value){
            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: function(key, value){
            change_custom_property(key, 1, "$setOnce");
        },
        /**
        * 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: function(key){
            change_custom_property(key, 1, "$inc");
        },
        /**
        * Increment value under the key of this user's custom properties by provided value
        * @param {string} key - name of the property to attach to user
        * @param {number} value - value by which to increment server value
        **/
        increment_by: function(key, value){
            change_custom_property(key, value, "$inc");
        },
        /**
        * Multiply value under the key of this user's custom properties by provided value
        * @param {string} key - name of the property to attach to user
        * @param {number} value - value by which to multiply server value
        **/
        multiply: function(key, value){
            change_custom_property(key, value, "$mul");
        },
        /**
        * Save maximal value under the key of this user's custom properties
        * @param {string} key - name of the property to attach to user
        * @param {number} value - value which to compare to server's value and store maximal value of both provided
        **/
        max: function(key, value){
            change_custom_property(key, value, "$max");
        },
        /**
        * Save minimal value under the key of this user's custom properties
        * @param {string} key - name of the property to attach to user
        * @param {number} value - value which to compare to server's value and store minimal value of both provided
        **/
        min: 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
        * @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
        * @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
        * @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
        **/
        save: function(){
            toRequestQueue({user_details: JSON.stringify({custom:customData})});
            customData = {};
        }
    };
    
    /**
    * 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.
    **/
    Countly.track_errors = function(segments){
        crashSegments = segments;
        window.onerror = function(msg, url, line, col, err) {
            if(typeof err !== "undefined")
                recordError(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 = [];
                    var f = arguments.callee.caller; // jshint ignore:line
                    while (f) {
                        stack.push(f.name);
                        f = f.caller;
                    }
                    error += stack.join("\n");
                }catch(ex){}
                recordError(error, false);
            }
        };
    };
    
    /**
    * Log an exception that you catched 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.
    **/
    Countly.log_error = function(err, segments){
        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
    **/
    Countly.add_log = function(record){
        crashLogs.push(record);
    };
    
    /**
    * Stop tracking duration time for this user
    **/
    Countly.stop_time = function(){
        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
    **/
    Countly.start_time = function(){
        trackTime = true;
        lastBeat = getTimestamp() - storedDuration;
        lastViewTime = getTimestamp() - lastViewStoredDuration;
        lastViewStoredDuration = 0;
    };
    
    /**
    * Track user sessions automatically, including  time user spent on your website
    **/
    Countly.track_sessions = function(){
        //start session
        Countly.begin_session();
        Countly.start_time();
        
        //end session on unload
        add_event(window, "beforeunload", function(){
            Countly.end_session();
        });
        add_event(window, "unload", function(){
            Countly.end_session();
        });
        
        //manage sessions on window visibility events
        var hidden = "hidden";
        
        function onchange(){
            if (document[hidden]) {
                Countly.stop_time();
            } else {
                Countly.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(){
                Countly.start_time();
            });
            add_event(window, "focusout", function(){
                Countly.stop_time();
            });
        }
        // All others:
        else{
            //old way
            add_event(window, "focus", function(){
                Countly.start_time();
            });
            add_event(window, "blur", function(){
                Countly.stop_time();
            });
            
            //newer mobile compatible way
            add_event(window, "pageshow", function(){
                Countly.start_time();
            });
            add_event(window, "pagehide", function(){
                Countly.stop_time();
            });
        }
    };
    
    /**
    * Track page views user visits
    * @param {string=} page - optional name of the page, by default uses current url path
    **/
    Countly.track_pageview = function(page){
        reportViewDuration();
        page = page || window.location.pathname;
        lastView = page;
        lastViewTime = getTimestamp();
        if(!platform)
            getMetrics();
        var segments = {
            "name": page,
            "visit":1,
            "segment":platform,
            "domain":window.location.hostname
        };
        
        if(typeof document.referrer !== "undefined" && document.referrer.length){
            var matches = urlParseRE.exec(document.referrer);
            //do not report referrers of current website
            if(matches && matches[11] && matches[11] != window.location.hostname)
                segments.start = 1;
        }
        
        //track pageview
        Countly.add_event({
            "key": "[CLY]_view",
            "segmentation": segments
        });
    };
    
    Countly.track_clicks = function(){
        var shouldProcess = true;
        function processClick(event){
            if(shouldProcess){
                shouldProcess = false;
                
                //cross browser click coordinates
                get_page_coord(event);
                if(typeof event.pageX !== "undefined" && typeof event.pageY !== "undefined"){
                    var height = getDocHeight();
                    var width = getDocWidth();
                    
                    //record click event
                    Countly.add_event({
                        "key": "[CLY]_action",
                        "segmentation": {
                            "type": "click",
                            "x":event.pageX,
                            "y":event.pageY,
                            "width":width,
                            "height":height,
                            "domain":window.location.hostname
                        }
                    });
                }
                setTimeout(function(){shouldProcess = true;}, 1000);
            }
        }
        //add any events you want like pageView
        add_event(document, "mousedown", processClick);
        add_event(document, "mouseup", processClick);
        add_event(document, "click", processClick);
    };
    
    Countly.track_links = function(parent){
        parent = parent || document;
        function trackClicks(){
            function processClick(event){

                //get element which was clicked
                var elem = get_event_target(event);
                
                //cross browser click coordinates
                get_page_coord(event);
                
                //record click event
                Countly.add_event({
                    "key": "linkClick",
                    "segmentation": {
                        "href": elem.href,
                        "text": elem.innerText,
                        "id": elem.id,
                        "x":event.pageX,
                        "y":event.pageY
                    }
                });
                //anticipate page unload
                if(typeof elem.href !== "undefined" && elem.target !== '_blank' && !(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)){
                    var link = elem.href.replace(window.location.href.split("#")[0], "");
                    if(link.indexOf("#") !== 0){
                        //most probably user will leave the page
                        
                        //end Countly session
                        Countly.end_session();
                        
                        //prevent user from leaving
                        prevent_default(event);
                        
                        //allow user to leave after some time
                        setTimeout(function() {
                            window.location.href = elem.href;
                        }, 1000);
                    }
                }
            }
            //add any events you want like pageView
            if(typeof parent.getElementsByTagName !== "undefined"){
                var links = parent.getElementsByTagName("a");
                for(var i = 0; i < links.length; i++){
                    add_event(links[i], "click", processClick);
                }
            }
            else{
                log("Can't track clicks");
            }
        }
        if (document.readyState === "complete") {
            trackClicks();
        }
        else{
            add_event(window, "load", trackClicks);
        }
    };
    
    /**
    * Generate custom event for all forms that were submitted on this page
    * @param {Object} parent - DOM object which children to track, by default it is document body
    **/
    Countly.track_forms = function(parent){
        parent = parent || document;
        function getInputName(input){
            return input.name || input.id || input.type || input.nodeName;
        }
        function processForm(form){
            // use to avoid duplicate submits
            var submitted = false;
            add_event(form, "submit", function(event){
                if(!submitted){
                    submitted = true;
                    
                    var segmentation = {
                        "id": form.id,
                        "name": form.name,
                        "action": form.action,
                        "method": form.method
                    };
                    
                    //get input values
                    var input;
                    if(typeof form.elements !== "undefined")
                        for(var i = 0; i < form.elements.length; i++){
                            input = form.elements[i];
                            if(input.nodeName.toLowerCase() == "select"){
                                if(typeof input.multiple !== "undefined"){
{'fraction_non_alphanumeric': 0.12488944324753373, 'fraction_numerical': 0.002267830819820841, 'mean_word_length': 1.79832466048991, 'pattern_counts': {'":': 54, '<': 9, '<?xml version=': 0, '>': 3, 'https://': 5, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2431266', 'n_tokens_mistral': 12356, 'n_tokens_neox': 11693, 'n_words': 3075}