Source: Tracker/builder.js

/**
 * @class
 * @classdesc Hits builder.
 * @name BuildManager
 * @param parent {object} Instance of the Tag used
 * @public
 */
var BuildManager = function (parent) {
    'use strict';
    var self = this;
    var MAXSIZE = 1500;
    var _secureProtocol = 'https:';

    /**
     * Create an element of the "preQuery" object.
     * @memberof BuildManager#
     * @function
     * @param param {string} name of a querystring parameter
     * @param value {string} value of a querystring parameter
     * @param truncate {boolean} true if the parameter can truncate
     * @returns {object}
     * @private
     */
    var _makeSubQuery = function(param, value, truncate) {
        var p = '&' + param + '=';
        return {
            param: p,
            paramSize: p.length,
            str: value,
            strSize: value.length,
            truncate: truncate
        };
    };

    /**
     * Create one hit with the first elements of the given dictionary and deletes them from it.
     * @memberof BuildManager#
     * @function
     * @param subQueries {object} dictionary generated with the _preQuery method
     * @param hitMaxSize {number}
     * @returns {Array} :
     *  - index 0 : query string to put on a hit
     * <br />- index 1 : dictionary remains unused, null if empty
     * @private
     */
    var _makeOneHit = function(subQueries, hitMaxSize) {
        var hit = '';
        var usedSpace = 0;
        var hitDone = true;

        for (var key in subQueries) {
            if (subQueries.hasOwnProperty(key)) {
                var subQuery = subQueries[key];
                if (subQuery) {
                    hitDone = false;
                    var freeSpace = hitMaxSize - usedSpace;
                    if (subQuery.strSize + subQuery.paramSize < freeSpace) {
                        // le nouveau paramètre rentre dans le hit
                        hit += subQuery.param + subQuery.str;
                        usedSpace += subQuery.strSize + subQuery.paramSize;
                        subQueries[key] = undefined;
                        hitDone = true;
                    } else if (subQuery.truncate) {
                        // le nouveau paramètre ne rentre pas dans le hit mais il est tronquable
                        hit += subQuery.param + subQuery.str.substr(0, freeSpace);
                        subQueries[key].str = subQuery.str.substr(freeSpace, subQuery.strSize - 1);
                        subQueries[key].strSize = subQueries[key].str.length;
                        break;
                    } else {
                        // le nouveau paramètre ne rentre pas dans le hit ET il n'est pas tronquable, on force son découpage et on log une erreur en debug mode
                        if(subQuery.strSize + subQuery.paramSize > hitMaxSize){
                            parent.emit('Tracker:Hit:Build:Error',{
                                lvl:'ERROR',
                                msg:'Too long parameter "' + subQuery.param + '"',
                                details:{value: subQuery.str}
                            });
                            // make the key + value under the max length allowed
                            subQueries[key].str = subQueries[key].str.substr(0,hitMaxSize - subQuery.paramSize - 1);
                            subQueries[key].strSize = subQueries[key].str.length;
                        }
                        break;
                    }
                } else {
                    hitDone = true;
                }
            }
        }

        return [hit, hitDone?null:subQueries];
    };

    /**
     * List of querystring parameters which can be truncated
     * @type {String[]}
     * @private
     */

    var multiHitable = ['ati','atc','pdtl','stc','dz'];

    /**
     * First step in the formating of the querystring parameters: make a dictionary paramName => object (see _makeSubQuery).
     * <br />The resulting dictionary contains all parameters in string format, in the right order, ready to be truncate if they can, etc.
     * @memberof BuildManager#
     * @function
     * @param buffer {object} Buffer part to process
     * @param maxsize {number} Hit max size
     * @returns {Array} :
     *  - index 0 : preProcessed parameters
     * <br />- index 1 : sum of the parameter size
     * <br />- index 2 : true if a parameter was too long and can't be truncated
     * <br />- index 3 : if sizeError name of the offending parameter
     * @private
     */
    var _preQuery = function(buffer, maxsize) {
        var subQueries = {};
        var totalSize = 0;
        var sizeError = false;
        var keySizeError = undefined;
        var lastKey, lastSubQuery;

        for (var key in buffer) {
            if (buffer.hasOwnProperty(key)) {
                var subQueryValue = buffer[key]['value'];
                //si le paramètre est une fonction on l'exécute pour obtenir une valeur
                // qui passera pas les traitements suivants (transformation en string, encodage, etc.)
                if (typeof subQueryValue === 'function') {
                    subQueryValue = subQueryValue();
                }
                if (subQueryValue instanceof Array) {
                    //si la valeur est un tableau on le transforme en chaînes séparés par un séparateur donné ou par ','
                    subQueryValue = subQueryValue.join(buffer[key]['options']['separator'] || ',');
                } else if (typeof subQueryValue === 'object') {
                    //si c'est un object on le sérialise
                    subQueryValue = window['ATInternet']['Utils']['jsonSerialize'](subQueryValue);
                }else if (typeof subQueryValue === 'undefined') {
                    subQueryValue = 'undefined';
                } else {
                    subQueryValue = subQueryValue.toString();
                }
                if (buffer[key]['options']['encode']) {
                    subQueryValue = encodeURIComponent(subQueryValue);
                }

                var subQuery = _makeSubQuery(key, subQueryValue, window['ATInternet']['Utils']['arrayIndexOf'](multiHitable, key) > -1);
                totalSize += subQuery.paramSize + subQuery.strSize;


                if (!buffer[key]['options']['last']) {
                    subQueries[key] = subQuery;

                    if (subQueries[key].paramSize + subQueries[key].strSize > maxsize && !subQueries[key].truncate) {
                        //le paramètre est trop long et non troncable
                        sizeError = true;
                        keySizeError = key;
                        break;
                    }
                } else {
                    //la paramètre avait l'option 'last', on le garde pour le placer à la fin du dictionnaire
                    if (subQueryValue.length > maxsize - 10) {
                        subQueryValue = subQueryValue.substr(0, maxsize - 10);
                    }
                    lastKey = key;
                    lastSubQuery = subQuery;
                }
            }
        }

        if (lastKey) {
            subQueries[lastKey] = lastSubQuery;
        }

        return [subQueries, totalSize, sizeError, keySizeError];
    };

    /**
     * Build at least one queryString (except for "?s=xxxx" parameter) using {@link ATInternet.Tracker.BufferManager parameters in the BufferManager}.
     * <br />Indeed if the total size of the params involves a too long hit it's truncated in several hits (multihits).
     * @memberof BuildManager#
     * @function
     * @param customParams {object} Custom parameters forced for queryString
     * @param filters {Array} List of buffer filters
     * @param callback {function} Callback which will use the queryString
     * @returns {string|Array} String if the total size was short enough, other wise it will be an array.
     * <br />Warning: it will be an array with one element if one of the parameter was too long and couldn't be truncated.
     * @private
     */
    var _buildParams = function (customParams, filters, callback) {
        var createQueriesString = function (buffer) {
            if (buffer === {}) {
                return "";
            }
            var queries = [];
            var resPreQuery = _preQuery(buffer, MAXSIZE);
            var queryStrParams = resPreQuery[0];
            var errorSize = resPreQuery[2];

            if (errorSize) {
                //Il y a une erreur sur le param resPreQuery[3]
                //on génère donc un tableau à un seul élément pour signifier une erreur
                //ce tableau contiendra un hit contenant le paramètre qui a causé l'erreur tronqué et
                //le parametre 'mherr'
                var paramError = resPreQuery[3];
                var queryError = queryStrParams[paramError];

                queryError['str'] = queryError['str'].substr(0, MAXSIZE - 50);
                queryError['strSize'] = MAXSIZE - 50;

                var newPreQueries = {};
                newPreQueries['mherr'] = _makeSubQuery('mherr', '1', false);
                newPreQueries[paramError] = queryError;

                queries.push(_makeOneHit(newPreQueries, MAXSIZE)[0]);
            } else {
                var resHit = _makeOneHit(queryStrParams, MAXSIZE);
                if (resHit[1] === null) {
                    //on n'est pas dans le cas d'un multihit donc on retourne une chaîne
                    queries = resHit[0];
                } else {
                    //on est dans le cas d'un multihit
                    queries.push(resHit[0]);
                    while (resHit[1] !== null) {
                        resHit = _makeOneHit(queryStrParams, MAXSIZE);
                        queries.push(resHit[0]);
                    }
                }
            }

            return queries;
        };

        var QueriesString = '';
        if (!parent['buffer']['presentInFilters'](filters, 'hitType')) {
            filters = parent['buffer']['addInFilters'](filters, 'hitType', ['page']);
        }
        filters = parent['buffer']['addInFilters'](filters, 'hitType', ['all']);

        var params, k;
        if(customParams) {
            //recupérer les filtres et y ajouter celui des permanents
            filters = parent['buffer']['addInFilters'](filters, 'permanent', true);
            params = parent['buffer']['get'](filters, true);
            for (k in customParams) {
                if (customParams.hasOwnProperty(k)) {
                    params[k] = {
                        'value': customParams[k],
                        'options': {}
                    };
                }
            }
            QueriesString = createQueriesString(params);
        } else {
            params = parent['buffer']['get'](filters, true);
            QueriesString = createQueriesString(params);
            //on sort du buffer tous les paramètres qui ne sont pas permanents
            for (k in params) {
                if (params.hasOwnProperty(k)) {
                    if (!params[k]['options']['permanent']) {
                        parent['buffer']['del'](k);
                    }
                }
            }
        }

        callback && callback(QueriesString);
    };

    /**
     * Build the base URL with level 1 and call the callback given with it
     * @memberof BuildManager#
     * @function
     * @param callback {function} Callback which will use the base URL
     * @private
     */
    var _buildConfig = function (callback) {
        var configSecure = parent.getConfig('secure');
        var protocolSecure = (document.location.protocol === _secureProtocol);
        var isSecure = configSecure || protocolSecure;
        var log = isSecure ? parent.getConfig('logSSL') : parent.getConfig('log');
        var baseURL = parent.getConfig('baseURL');
        var domain = parent.getConfig('domain');
        var pixelPath = parent.getConfig('pixelPath');
        var site = parent.getConfig('site');
        if ((baseURL || (log && domain && pixelPath)) && site) {
            domain = '.' + domain;
            site = '?s=' + site;
            var protocol = configSecure ? 'https://' : '//';
            callback && callback(null, (baseURL ? baseURL : protocol + log + domain + pixelPath) + site);
        }
        else {
            callback && callback({message: 'Config error'});
        }
    };

    /**
     * Build a list of hits (base URL + queryString)
     * @memberof BuildManager#
     * @function
     * @param customParams {object} Custom parameters forced
     * @param filters {Array} List of buffer filters
     * @param callback {function} callback which will use the hit list
     * @private
     */
    var _build = function (customParams, filters, callback) {
        _buildConfig(function(err,baseHit){
            if(!err) {
                _buildParams(customParams, filters, function(queriesString){
                    var hits = [];
                    var uuid = ATInternet.Utils.uuid();
                    var guid = uuid.num(13);

                    if (!(queriesString instanceof Array)) {
                        hits.push(baseHit + queriesString);
                    } else if (queriesString.length === 1) {
                        hits.push(baseHit + '&mh=1-2-' + guid + queriesString[0]);
                    } else {
                        for (var i = 1; i <= queriesString.length; i++) {
                            hits.push(baseHit + '&mh=' + i + '-' + queriesString.length + '-' + guid + queriesString[i-1]);
                        }
                    }
                    callback && callback(null, hits);
                });
            } else { //error
                callback && callback(err);
            }
        });
    };

    /**
     * Send at least one hit, more if it's too long (multihits).
     * @name send
     * @memberof BuildManager#
     * @function
     * @param customParams {object} Object which contains some hit parameters that you would like to send specifically (they are given priority over the current buffer)
     * @param filters {Array} List of buffer filters
     * @public
     */
    self['send'] = function (customParams, filters) {
        _build(customParams, filters, function (err, hits) {
            if (!err) {
                for (var i = 0; i < hits.length; i++) {
                    self.sendUrl(hits[i]);
                }
            }
            else {
                parent.emit('Tracker:Hit:Build:Error', {lvl:'ERROR', msg: err.message, details:{hits:hits}});
            }
        });
    };

    /**
     * Send single hit from complete url.
     * <br />An event will be sent {@link ATInternet.Tracker.TriggersManager} :
     * <br />- "Tracker:Hit:Sent:Ok" with hit data if succeed
     * <br />- "Tracker:Hit:Sent:Error" with error data otherwise
     * @name sendUrl
     * @memberof BuildManager#
     * @function
     * @param hit {string} Url to send
     * @public
     */
    self['sendUrl'] = function (hit) {
        var makeTriggerCallback = function (trackerTrigger, hit) {
            return (function () {
                return function (evt) {
                    parent.emit(trackerTrigger, {lvl:(trackerTrigger.indexOf('Error')===-1)?'INFO':'ERROR', details:{hit:hit,event:evt}});
                };
            })();
        };
        var img = new Image();
        img.onload = makeTriggerCallback('Tracker:Hit:Sent:Ok', hit);
        img.onerror = makeTriggerCallback('Tracker:Hit:Sent:Error', hit);
        img.src = hit;
    };

    // For unit tests on private elements !!!
    /* @if test */
    self['buildParams'] = _buildParams;
    self['buildConfig'] = _buildConfig;
    self['build'] = _build;
    self['makeSubQuery'] = _makeSubQuery;
    self['preQuery'] = _preQuery;
    self['multiHitable'] = multiHitable;
    self['makeOneHit'] = _makeOneHit;
    self['secureProtocol'] = _secureProtocol;
    /* @endif */

};