/**
* @class
* @classdesc Hits builder.
* @name BuildManager
* @param tag {object} Instance of the Tag used
* @public
*/
var BuildManager = function (tag) {
'use strict';
var self = this;
var MINSIZE = 2000;
var MAXSIZE = 0;
var AVAILABLESIZE = 0;
/**
* List of querystring parameters which can be truncated
* For others parameters (ati, atc, pdtl, etc.), see truncate option
* @type {string[]}
* @private
*/
var _multiHitable = ['dz'];
/**
* Request method
* @type {string}
* @private
*/
var _requestMethod = '';
/**
* 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
* @param multihit {boolean} true if the parameter must be added in all multi-hits
* @param separator {string} separator to consider for multi-hits
* @param encode {boolean} true if if the parameter value is encoded
* @param last {boolean} true if if the parameter must be placed at the end
* @returns {object}
* @private
*/
var _makeSubQueryObject = function (param, value, truncate, multihit, separator, encode, last) {
var p = '&' + param + '=';
return {
param: p,
paramSize: p.length,
str: value,
strSize: value.length,
truncate: truncate,
multihit: multihit,
separator: separator || '',
encode: encode,
last: last
};
};
/**
* 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 maxSizeHit {number} size available for inserting hit parameters
* @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, maxSizeHit) {
var hit = '';
var freeSpace = 0;
var usedSpace = 0;
var index = 0;
var indexSeparator = -1;
var subQueryObject = null;
var remainingParameters = null;
for (var key in subQueries) {
if (subQueries.hasOwnProperty(key)) {
subQueryObject = subQueries[key];
if (subQueryObject) {
freeSpace = maxSizeHit - usedSpace;
if (subQueryObject.last && remainingParameters !== null) {
remainingParameters[key] = subQueryObject;
} else if (subQueryObject.strSize + subQueryObject.paramSize <= freeSpace) {
// The new parameter fits into the hit
hit += subQueryObject.param + subQueryObject.str;
usedSpace += subQueryObject.paramSize + subQueryObject.strSize;
} else {
remainingParameters = remainingParameters || {};
remainingParameters[key] = subQueryObject;
if (subQueryObject.truncate) {
// The new parameter does not fit into the hit but it may be truncated
index = freeSpace - subQueryObject.paramSize;
if (subQueryObject.separator) {
var query = subQueryObject.str.substring(0, freeSpace);
if (subQueryObject.encode) {
indexSeparator = query.lastIndexOf(encodeURIComponent(subQueryObject.separator));
} else {
indexSeparator = query.lastIndexOf(subQueryObject.separator);
}
if (indexSeparator > 0) {
index = indexSeparator;
}
}
hit += subQueryObject.param + subQueryObject.str.substring(0, index);
usedSpace += subQueryObject.paramSize + subQueryObject.str.substring(0, index).length;
remainingParameters[key].str = subQueryObject.str.substring(index, subQueryObject.strSize);
remainingParameters[key].strSize = remainingParameters[key].str.length;
}
}
}
}
}
return [hit, remainingParameters];
};
/**
* First step in the formatting of the querystring parameters: make a dictionary paramName => object (see _makeSubQueryObject).
* <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
* @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) {
var subQueries = {};
var sizeError = false;
var keySizeError = undefined;
var bufferValue, bufferOptions;
var encode, truncate, multihit, last;
var subQueryObject, lastKey, lastSubQueryObject;
var baseMultihit = '';
for (var key in buffer) {
if (buffer.hasOwnProperty(key)) {
encode = false;
truncate = false;
multihit = false;
last = false;
bufferValue = buffer[key]._value;
bufferOptions = buffer[key]._options || {};
if (typeof bufferOptions.encode === 'boolean') {
encode = bufferOptions.encode;
}
// If the parameter is a function, it is executed to obtain a value
// which will not pass the following processing (transformation into a string, encoding, etc.)
if (typeof bufferValue === 'function') {
bufferValue = bufferValue();
}
if (bufferValue instanceof Array) {
// If the value is an array, it is transformed into strings separated by a given separator or by ","
bufferValue = bufferValue.join(bufferOptions.separator || ',');
} else if (typeof bufferValue === 'object') {
// If it's an object, we serialize it
bufferValue = ATInternet.Utils.jsonSerialize(bufferValue);
} else if (typeof bufferValue === 'undefined') {
bufferValue = 'undefined';
} else {
bufferValue = bufferValue.toString();
}
if (encode) {
bufferValue = encodeURIComponent(bufferValue);
}
if (ATInternet.Utils.arrayIndexOf(_multiHitable, key) > -1) {
truncate = true;
} else if (typeof bufferOptions.truncate === 'boolean') {
truncate = bufferOptions.truncate;
}
if (typeof bufferOptions.multihit === 'boolean') {
multihit = bufferOptions.multihit;
}
if (typeof bufferOptions.last === 'boolean') {
last = bufferOptions.last;
}
subQueryObject = _makeSubQueryObject(key, bufferValue, truncate, multihit, bufferOptions.separator, encode, last);
if (multihit) {
// We remove the size of multihit parameters
AVAILABLESIZE -= (subQueryObject.paramSize + subQueryObject.strSize);
baseMultihit += subQueryObject.param + subQueryObject.str;
} else {
if (!last) {
subQueries[key] = subQueryObject;
if (((subQueries[key].paramSize + subQueries[key].strSize) > AVAILABLESIZE) && !subQueries[key].truncate) {
// The parameter is too long and cannot be truncated
tag.emit('Tracker:Hit:Build:Error', {
lvl: 'ERROR',
msg: 'Too long parameter: "' + subQueries[key].param + '"',
details: {value: subQueries[key].str}
});
sizeError = true;
keySizeError = key;
break;
}
} else {
// The parameter has the option'last', we keep it to place it at the end of the dictionary
if (subQueryObject.paramSize + subQueryObject.strSize > AVAILABLESIZE) {
subQueryObject.str = subQueryObject.str.substring(0, AVAILABLESIZE - subQueryObject.paramSize);
subQueryObject.strSize = subQueryObject.str.length;
}
lastKey = key;
lastSubQueryObject = subQueryObject;
}
}
}
}
if (lastKey) {
subQueries[lastKey] = lastSubQueryObject;
}
return [subQueries, sizeError, keySizeError, baseMultihit];
};
/**
* 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 baseMultihit = '';
var createQueriesString = function (buffer) {
if (buffer === {}) {
return [];
}
var queries = [];
var resPreQuery = _preQuery(buffer);
var queryStrParams = resPreQuery[0];
var sizeError = resPreQuery[1];
baseMultihit = resPreQuery[3];
var resHit;
if (sizeError) {
// There is an error on the resPreQuery parameter[2]
// We generate an array with only one element to indicate an error
// This array will contain a hit with the parameter that caused the "truncated error" and
// the parameter "mherr"
var paramError = resPreQuery[2];
var queryError = queryStrParams[paramError];
queryError['str'] = queryError['str'].substring(0, AVAILABLESIZE - queryError['paramSize']);
queryError['strSize'] = queryError['str'].length;
var newPreQueries = {};
newPreQueries['mherr'] = _makeSubQueryObject('mherr', '1', false, false, '', false, false);
newPreQueries[paramError] = queryError;
queryStrParams = newPreQueries;
}
resHit = _makeOneHit(queryStrParams, AVAILABLESIZE);
if (resHit[1] === null) {
// We are not in the case of a multihit so we return a string
queries = resHit[0];
} else {
// We are in the case of a multihit so we return an array
queries.push(resHit[0]);
while (resHit[1] !== null) {
resHit = _makeOneHit(resHit[1], AVAILABLESIZE);
queries.push(resHit[0]);
}
}
return queries;
};
var QueriesString = '';
if (!tag.buffer.presentInFilters(filters, 'hitType')) {
filters = tag.buffer.addInFilters(filters, 'hitType', ['page']);
}
filters = tag.buffer.addInFilters(filters, 'hitType', ['all']);
var params, key, bufferParam;
if (ATInternet.Utils.isObject(customParams)) {
// Retrieve the filters and add the filter for permanent
filters = tag.buffer.addInFilters(filters, 'permanent', true);
params = tag.buffer.get(filters, true);
var paramValue, paramOptions;
for (key in customParams) {
if (customParams.hasOwnProperty(key)) {
paramOptions = {};
if (customParams[key] && typeof customParams[key] === 'object' && customParams[key].hasOwnProperty('_value')) {
paramValue = customParams[key]._value;
if (customParams[key].hasOwnProperty('_options')) {
paramOptions = customParams[key]._options;
}
} else {
paramValue = customParams[key];
}
bufferParam = ATInternet.Utils.privacy.testBufferParam(key, paramValue);
if (bufferParam.toSetInBuffer) {
params[key] = {
'_value': bufferParam.value,
'_options': paramOptions
};
}
}
}
QueriesString = createQueriesString(params);
} else {
params = tag.buffer.get(filters, true);
QueriesString = createQueriesString(params);
// We take out of the buffer all the parameters that are not permanent
for (key in params) {
if (params.hasOwnProperty(key)) {
if (!params[key]._options || !params[key]._options.permanent) {
tag.buffer.del(key);
}
}
}
}
callback && callback(QueriesString, baseMultihit);
};
/**
* Get collect domain depending on configuration
* @name getCollectDomain
* @memberof BuildManager#
* @function
* @return {string}
* @public
*/
self.getCollectDomain = function () {
var collectDomain = '';
var log = tag.getConfig('logSSL') || tag.getConfig('log');
var domain = tag.getConfig('domain');
if (log && domain) {
collectDomain = log + '.' + domain;
} else {
collectDomain = tag.getConfig('collectDomainSSL') || tag.getConfig('collectDomain');
}
return collectDomain;
};
/**
* 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 finalURL = '';
var baseURL = tag.getConfig('baseURL');
if (baseURL) {
finalURL = baseURL;
} else {
// Collection domain
var collectDomain = self.getCollectDomain();
// Pixel path
var pixelPath = tag.getConfig('pixelPath');
pixelPath = pixelPath || '/';
if (pixelPath.charAt(0) !== '/') {
pixelPath = '/' + pixelPath;
}
// Final URL
if (collectDomain) {
var configHttp = tag.getConfig('forceHttp');
var protocol = configHttp ? 'http://' : 'https://';
finalURL = protocol + collectDomain + pixelPath;
}
}
// Callback
var site = tag.getConfig('site');
if (finalURL && site) {
callback && callback(null, finalURL + '?s=' + 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) {
// We remove the size of the hit base and reserve a space
// for the multihit management parameter just in case
AVAILABLESIZE = MAXSIZE - (baseHit.length + '&mh=xxxx-xxxx-xxxxxxxxxxxxx'.length);
_buildParams(customParams, filters, function (queriesString, baseMultihit) {
var hits = [];
var uuid = ATInternet.Utils.uuid();
var guid = uuid.num(13);
if (!(queriesString instanceof Array)) {
hits.push(baseHit + baseMultihit + queriesString);
} else {
for (var i = 1; i <= queriesString.length; i++) {
hits.push(baseHit + baseMultihit + '&mh=' + i + '-' + queriesString.length + '-' + guid + queriesString[i - 1]);
}
}
callback && callback(null, hits);
});
} else {
callback && callback(err);
}
});
};
/**
* Emit trigger and execute callback
* @memberof BuildManager#
* @function
* @param trackerTrigger {string} Trigger to emit
* @param hit {string} Hit to send
* @param method {string} Sending method (GET|POST)
* @param callback {function} Callback to execute
* @param level {string} Trigger level
* @param isMultiHit {boolean} Is the hit to be sent is part of a multihit
* @param elementType {string} Element type (mailto, form, redirection)
* @private
*/
var _makeTriggerCallback = function (trackerTrigger, hit, method, callback, level, isMultiHit, elementType) {
return (function () {
return function (evt) {
tag.emit(trackerTrigger, {
lvl: level,
details: {
hit: hit,
method: method,
event: evt,
isMultiHit: isMultiHit,
elementType: elementType
}
});
callback && callback();
};
})();
};
/**
* 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
* @param callback {function} Callback to execute
* @param requestMethod {string} Overloading the global method of sending hits (GET|POST)
* @param elementType {string} Element type (mailto, form, redirection)
* @public
*/
self.send = function (customParams, filters, callback, requestMethod, elementType) {
_build(customParams, filters, function (err, hits) {
if (!err) {
for (var i = 0; i < hits.length; i++) {
self.sendUrl(hits[i], callback, requestMethod, elementType);
}
} else {
tag.emit('Tracker:Hit:Build:Error', {
lvl: 'ERROR',
msg: err.message,
details: {}
});
callback && callback();
}
});
};
/**
* Send GET request with image.
* @name _sendImage
* @memberof BuildManager#
* @function
* @param hit {string} Url to send
* @param callback {function} Callback to execute
* @param isMultiHit {boolean} Is the hit to be sent is part of a multihit
* @param elementType {string} Element type (mailto, form, redirection)
* @private
*/
var _sendImage = function (hit, callback, isMultiHit, elementType) {
var img = new Image();
img.onload = _makeTriggerCallback('Tracker:Hit:Sent:Ok', hit, 'GET', callback, 'INFO', isMultiHit, elementType);
img.onerror = _makeTriggerCallback('Tracker:Hit:Sent:Error', hit, 'GET', callback, 'ERROR', isMultiHit, elementType);
img.src = hit;
};
/**
* Send POST request with beacon.
* @name _sendBeacon
* @memberof BuildManager#
* @function
* @param hit {string} Url to send
* @param callback {function} Callback to execute
* @param isMultiHit {boolean} Is the hit to be sent is part of a multihit
* @private
*/
var _sendBeacon = function (hit, callback, isMultiHit) {
var trackertrigger = 'Tracker:Hit:Sent:Error';
var method = 'POST';
var level = 'ERROR';
if (window.navigator.sendBeacon(hit, null)) {
trackertrigger = 'Tracker:Hit:Sent:Ok';
level = 'INFO';
}
_makeTriggerCallback(trackertrigger, hit, method, callback, level, isMultiHit, '')();
};
/**
* Init.
* @memberof BuildManager#
* @function
* @private
*/
var _init = function () {
MAXSIZE = Math.max((tag.getConfig('maxHitSize') || 0), MINSIZE);
AVAILABLESIZE = Math.max((tag.getConfig('maxHitSize') || 0), MINSIZE);
_requestMethod = tag.getConfig('requestMethod');
};
// Initialise global values.
_init();
/**
* 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
* @param callback {function} Callback to execute
* @param requestMethod {string} Overloading the global method of sending hits (GET|POST)
* @param elementType {string} Element type (mailto, form, redirection)
* @public
*/
self.sendUrl = function (hit, callback, requestMethod, elementType) {
var isMultiHit = (hit.indexOf('&mh=') > -1);
var configRequestMethod = requestMethod || _requestMethod;
if (ATInternet.Utils.isOptedOut() && !tag.getConfig('sendHitWhenOptOut')) {
_makeTriggerCallback('Tracker:Hit:Sent:NoTrack', hit, configRequestMethod, callback, 'INFO', isMultiHit, elementType)();
} else {
if (configRequestMethod === 'POST' && ATInternet.Utils.isBeaconMethodAvailable()) {
_sendBeacon(hit, callback, isMultiHit);
} else {
_sendImage(hit, callback, isMultiHit, elementType);
}
}
};
// For unit tests on private elements !!!
/* @if test */
self._makeSubQueryObject = _makeSubQueryObject;
self._makeOneHit = _makeOneHit;
self._preQuery = _preQuery;
self._buildParams = _buildParams;
self._buildConfig = _buildConfig;
self._build = _build;
self._makeTriggerCallback = _makeTriggerCallback;
self._sendImage = _sendImage;
self._sendBeacon = _sendBeacon;
self.setAvailableSize = function (size) {
AVAILABLESIZE = size;
};
self._multiHitable = _multiHitable;
self._requestMethod = _requestMethod;
/* @endif */
};