Last active
April 4, 2023 18:41
-
-
Save Kobe/7ce463c23917a075cb9e89a939c2d6b8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var module = module || {}; | |
window.module.config = window.module.config || {}; | |
window.module.config.tracking = window.module.config.tracking || {}; | |
function tracker(window, module, trackingOptions, $) {// NOSONAR | |
'use strict'; | |
var LOG_EVENT_CONSOLE = 'console'; | |
var LOG_EVENT_ERROR = 'error'; | |
var LOG_EVENT_EVENT_TRACKING = 'event-tracking'; | |
var LOG_EVENT_PAGE = 'page'; | |
var WEBTREKK_TIMEOUT = 5000; // ms | |
var WEBTREKK_RETRY_DELAY = 100; // ms | |
var INTERNAL_LOGGING_URL = '/logClientInfo'; | |
var LOCAL_LOGGER_NAME = 'ErrorTracking'; | |
var LOGGING_LEVEL = 'WARN'; | |
var defaultOptions = { | |
console: { | |
enabled: false, // {boolean} console output tracking, not debug | |
temporarilyDisabled: false // {boolean} for internal use only, to prevent console tracking of debug output | |
}, | |
error: { | |
enabled: false, // {boolean} | |
debug: true, // {boolean} debug output of errors | |
filter: { | |
// {function} returns true if there are blacklisted values in the message | |
isBlacklisted: function(message) { return false; }, | |
// {function} only capture errors from specific urls (e.g. no third party), use Regex to specify matched urls | |
isValidFile: function(file) { return true; }, | |
// {function} prevents tracking errors of specific user agents | |
isValidBrowser: function() { return true; }, | |
// {function} if true, filter errors in row 0, means CORS is disabled for that script, so we get no information | |
isValidRow: function(row) { | |
/* row === 0 means violation of cross domain policy | |
* see: - http://blog.errorception.com/2012/12/catching-cross-domain-js-errors.html | |
* - http://stackoverflow.com/questions/1653308/access-control-allow-origin-multiple-origin-domains | |
* - https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes | |
*/ | |
return row > 0; | |
} | |
}, | |
preventDefaultErrorHandler: false, // {boolean}, see: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers.onerror | |
ratio: 100, // {number} percentage of users, where tracking is enabled, does not effect debug | |
stackTrace: { | |
trimLength: 5, // {number} trim stack trace to number of elements | |
trimString: false // {boolean|string|Array} to remove unuseful debug info and reduce data | |
} | |
}, | |
event: { | |
enabled: false, // {boolean} | |
debug: true // {boolean} | |
}, | |
timing: {}, | |
send: { | |
webtrekk: function(params) { | |
if (isWebtrekkLoaded()) { | |
sendToWebtrekk(); | |
} else { | |
var retryCount = 0, | |
webtrekkLoadedVerifier = setInterval(function () { | |
if (isWebtrekkLoaded()) { | |
sendToWebtrekk(); | |
} else { | |
retryCount++; | |
} | |
if (retryCount >= WEBTREKK_TIMEOUT / WEBTREKK_RETRY_DELAY || isWebtrekkLoaded()) { | |
clearInterval(webtrekkLoadedVerifier); | |
} | |
}, WEBTREKK_RETRY_DELAY); | |
} | |
function isWebtrekkLoaded() { | |
return typeof wt === 'object' && typeof wt.sendinfo === 'function'; | |
} | |
// collect and translate data for Webtrekk and send data to Webtrekk | |
function sendToWebtrekk() {// NOSONAR | |
var webtrekkEventParameter; | |
switch (params.type) { | |
case LOG_EVENT_CONSOLE: | |
case LOG_EVENT_ERROR: | |
if (params.name && params.value) { | |
webtrekkEventParameter = { | |
linkId: params.name, | |
customClickParameter: {3: params.value} | |
}; | |
} else { | |
webtrekkEventParameter = { | |
linkId: 'error.unknown', | |
customClickParameter: {3: 'error.unknown | missing tracking parameter'} | |
}; | |
} | |
break; | |
case LOG_EVENT_EVENT_TRACKING: | |
if (params.name) { | |
if (typeof params.value === 'undefined') { | |
webtrekkEventParameter = {linkId: params.name}; | |
} | |
if (typeof params.value === 'object') { | |
var customClickParameter = {}; | |
// fix object to support $.each, see: http://api.jquery.com/jquery.each/#entry-longdesc | |
if (params.value.length) {// NOSONAR | |
params.value.length_ = params.value.length; | |
delete params.value.length; | |
} | |
/* | |
map event param object to customClickParameter | |
valid Webtrekk Custom Event Parameter (add more values when necessary): | |
- Position = 1 | |
- Element = 2 | |
'invalid/used otherwise' Webtrekk Custom Event Parameter: | |
- JS Tracking = 3 | |
- Scrollposition = 540 | |
examples: | |
- $.publish('track.event', 'click'); | |
- $.publish('track.event', ['click']); | |
- $.publish('track.event', ['leadout', { element: 'shop' }]); | |
- $.publish('track.event', ['foo', { element: 'shop', position: '10' }]); | |
- $.publish('track.event', ['foo', { element: 'shop', position: 'name' }]); | |
*/ | |
$.each(params.value, function (key, value) { | |
switch (key.toLowerCase()) {// NOSONAR | |
case 'element': | |
customClickParameter[2] = value; | |
break; | |
case 'position': | |
customClickParameter[1] = value; | |
break; | |
default: | |
break; | |
} | |
}); | |
//console.log({ linkId: params.name, customLinkParameter: customClickParameter }); | |
webtrekkEventParameter = { | |
linkId: params.name, | |
customClickParameter: customClickParameter | |
}; | |
} | |
} else { | |
webtrekkEventParameter = { | |
linkId: 'error.unknown', | |
customClickParameter: {3: 'error.unknown | missing tracking parameter'} | |
}; | |
} | |
break; | |
default: | |
break; | |
} | |
if (webtrekkEventParameter) { | |
wt.sendinfo(webtrekkEventParameter); | |
} | |
} | |
}, | |
internal: function(params) { | |
var wanted_log_sources = [LOG_EVENT_ERROR]; | |
if ($.inArray(params.type, wanted_log_sources) === -1 || | |
typeof params.value === 'undefined' || | |
!params.value.length) { | |
return; | |
} | |
$.ajax({ | |
method: 'POST', | |
url: INTERNAL_LOGGING_URL, | |
data: JSON.stringify({'message': params.value, 'logger': LOCAL_LOGGER_NAME, 'level': LOGGING_LEVEL}), | |
contentType: 'application/json' | |
}); | |
} | |
} | |
}, | |
options = {}; | |
/** | |
* set initial options | |
*/ | |
function initOptions() { | |
$.extend(true, options, defaultOptions, trackingOptions); | |
// enable/disable tracking, related to ratio option | |
options.error.enabled = options.error.enabled && (Math.floor(Math.random() * 100 / options.error.ratio) === 0); | |
// enable/disable console output tracking, it's not for debug output | |
options.console.enabled = 'console' in window && options.console.enabled; | |
} | |
/** | |
* sends tracking information | |
* | |
* we use an optional function for sending to be more flexible with the tracking | |
* | |
* @todo - add error event array to collect all errors and send them later, could prevent sending to many events via aggregation of errors | |
* - maybe move the ready() delay to the optional function, to be more flexible with other trackings | |
* @param params | |
*/ | |
function sendTracking(params){ | |
$.each(options.send, function (id, trackingFunction) { | |
trackingFunction(params); | |
}); | |
} | |
/** | |
* console output of tracking informations | |
* | |
* WARNING: When using the firebug console, this function will throw an exception 'dbg is not defined'. It's a | |
* bug with firebug itself. There is no problem when using the built-in developer tools of Firefox. | |
* | |
* @param message | |
* @param type | |
*/ | |
function debugTracking(message, type){ | |
options.console.temporarilyDisabled = true; // prevent console tracking of debug output | |
if ('console' in window) { | |
if (!console.debug) { | |
console.debug = console.log; | |
} | |
if (!console.error) { | |
console.error = console.log; | |
} | |
switch (type) { | |
case 'debug': | |
console.debug(message); | |
break; | |
case 'error': | |
console.error(message); | |
break; | |
case 'info': | |
console.error(message); | |
break; | |
default: | |
console.log(message); | |
} | |
} | |
options.console.temporarilyDisabled = false; | |
} | |
/** | |
* cleanup & normalize stack traces for tracking | |
* | |
* @todo - allow to remove multiple strings via array | |
* | |
* @param [Object} errorObject | |
* @param {String} removeString | |
* @returns {String|null} | |
*/ | |
function normalizeStackTrace(errorObject, removeString) { | |
var stackTraceString, | |
trimLength = options.error.stackTrace.trimLength || 100, | |
trimString = options.error.stackTrace.trimString; | |
// return if there is no stacktrace | |
if (typeof errorObject === 'undefined' || errorObject === null || typeof errorObject.stack !== 'string') { | |
return null; | |
} else { | |
stackTraceString = errorObject.stack; | |
} | |
// remove a given string | |
if (removeString) { | |
stackTraceString = stackTraceString.replace(new RegExp(removeString,'g'), ''); // see: http://stackoverflow.com/a/494046/2590616 | |
} | |
// remove error message from stack trace (e.g. in Chrome) | |
if (stackTraceString.indexOf(errorObject.message) > -1) { | |
stackTraceString = stackTraceString.substr(stackTraceString.indexOf(errorObject.message)+errorObject.message.length); | |
} | |
// cleanup string | |
stackTraceString = stackTraceString | |
// trim newline & whitespace, see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Polyfill | |
.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '') | |
.replace(/\/</, '') // remove useless '/<' | |
//.replace(/\/:/, ':') // remove slash from '/:' | |
.replace(/at | at |\)/gm, '').replace(/ \(/gm, '@').replace(/@/g, ' @ ') // create similar output for chrome & firefox | |
.split('\n') // split at newline | |
.splice(0, trimLength) // trim length of stack trace elements | |
.join(' > ') // join with ' > ' as separator | |
.replace(/ +/g, ' ') // remove multiple whitepaces, see: http://stackoverflow.com/a/1981366/2590616 | |
.replace(/^ @ /, '') // remove trailing ' @ ' | |
.replace(/ > @ /g, ' > '); // remove useless ' > @ ' combination | |
// trim stacktrace to remove unuseful debug info and reduce data | |
$.each($.isArray(trimString) ? trimString : [trimString], function(index, trimString) {// NOSONAR | |
var trimPosition = (typeof trimString === 'string' && stackTraceString.indexOf(trimString) > 0) ? | |
stackTraceString.indexOf(trimString) : stackTraceString.length; | |
stackTraceString = stackTraceString.substr(0, trimPosition); | |
}); | |
return stackTraceString; | |
} | |
/** | |
* try to normalize the output of the error object across different browsers | |
* | |
* @param {Error} errorObject | |
*/ | |
function normalizeErrorObject(errorObject) { | |
if (typeof errorObject === 'object' && errorObject !== null && typeof errorObject.stack === 'string') { | |
errorObject.stack = normalizeStackTrace(errorObject); | |
// check if filename, line number or column number are empty, then extract it from stacktrace | |
if (!errorObject.fileName || !errorObject.lineNumber || !errorObject.columnNumber) { | |
var errorArray = errorObject.stack.split(' > ')[0].split(':'); // get first stack trace entry and split it into details | |
if (errorArray.length >= 3) { | |
errorObject.columnNumber = errorArray.pop(); // last item is column | |
errorObject.lineNumber = errorArray.pop(); // (second) last item is line number | |
errorObject.fileName = errorArray.join(':').split(' ').pop(); // rest should be filename | |
} | |
} | |
} | |
return errorObject; | |
} | |
/** | |
* Parses error messages of custom errors | |
* | |
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error | |
* | |
* @param message | |
* @returns {string|Error|Array} | |
*/ | |
function parseCustomErrorMessage(message) { | |
var errorMessage = '', | |
errorObject = {}; | |
if (typeof message === 'string') { | |
errorMessage = message; | |
} | |
if (typeof message === 'object') { | |
if (message instanceof Error) { | |
errorObject = message; | |
// allows to use an array as error message in new Error | |
// new Error() joins an array with ',', so we split and rejoin it here with ' | ' | |
// but we don't do it if there is ', ' in the message | |
if (errorObject.message.indexOf(',') !== errorObject.message.indexOf(', ')) { | |
errorObject.message = errorObject.message.split(',').join(' | '); | |
} | |
errorObject = normalizeErrorObject(errorObject); | |
// to reduce string length and redundancy | |
// we use stack trace only and no errorObject.fileName, errorObject.lineNumber or errorObject.columnNumber | |
if (errorObject.stack) { | |
errorMessage = [errorObject.message, errorObject.stack].join(' | '); | |
} else { | |
errorMessage = errorObject.message; | |
} | |
} | |
if ($.isArray(message)) { | |
errorMessage = message.join(' | '); | |
} | |
} | |
return errorMessage; | |
} | |
/** | |
* track errors | |
* | |
* - tracking of errors is done decupled via publish/subscribe | |
* - filtering of errors should be done in the publish/capturing part, formatting of the errors on the subscribe/tracking part | |
* - the data from '_event_' should be ignored, it's a leftover of the tiny pub/sub handling | |
* | |
* @param name | |
* @returns {Function} | |
*/ | |
function trackError(name) { | |
/** | |
* handle errors happening while ajax calls | |
* | |
* @param _event_ | |
* @param {jQXHR} jqxhr | |
* @param {object} settings | |
* @param {Error} thrownError | |
*/ | |
function trackAjaxError(_event_, jqxhr, settings, thrownError){ | |
var errorName = 'error.' + name, | |
errorMessage = [errorName, thrownError + ' (' + jqxhr.status + ')', settings.type, settings.url].join(' | '); | |
if (options.error.debug) { | |
debugTracking(errorMessage); | |
} | |
if (options.error.enabled) { | |
sendTracking({type: LOG_EVENT_ERROR, name: errorName, value: errorMessage}); | |
} | |
} | |
/** | |
* handles several custom error types: | |
* error.debug: handle errors fired on purpose for debug and qa reasons | |
* error.form: form errors | |
* error.ui: errors shown to the user (e.g. 'We are unable to...') | |
* | |
* @example $.publish('track.error.debug', new Error(['foo', 'bar'])); // allows stack trace output | |
* @example $.publish('track.error.form', 'foo bar')); | |
* @example $.publish('track.error.ui', ['foo', 'bar']); | |
* | |
* @param _event_ | |
* @param {string|Error|Array} message | |
*/ | |
function trackCustomError(_event_, message) { | |
var errorName = 'error.' + name, | |
errorMessage; | |
// check if we have multiple messages instead of one | |
if (arguments.length > 2) {// NOSONAR | |
message = Array.prototype.slice.call(arguments); // overwrite message with an array of messages | |
message.shift(); // remove _event_ object | |
} | |
message = parseCustomErrorMessage(message); | |
errorMessage = [errorName, message].join(' | '); | |
if (options.error.debug) { | |
debugTracking(errorMessage, name === 'debug' ? 'debug' : ''); | |
} | |
if (options.error.enabled) { | |
sendTracking({type: LOG_EVENT_ERROR, name: errorName, value: errorMessage}); | |
} | |
} | |
/** | |
* handle generic onerror errors not captured otherwise | |
* | |
* @param _event_ | |
* @param {string} message | |
* @param {string} file | |
* @param {number} row | |
* @param {number} column | |
* @param {Error} errorObject | |
*/ | |
function trackOnError(_event_, message, file, row, column, errorObject){ | |
var errorName = 'error.' + name, | |
fileOrigin = file ? (file.substr(0, file.indexOf(file.split('/')[2]) + file.split('/')[2].length)) : '', | |
errorMessage = [errorName, message]; | |
// add stack trace to error message | |
if (typeof errorObject !== 'undefined' && errorObject !== null && typeof errorObject.stack === 'string') { | |
errorMessage.push(normalizeStackTrace(errorObject, fileOrigin)); | |
} | |
errorMessage = errorMessage.join(' | '); | |
if (!options.error.filter.isBlacklisted(errorMessage)) { | |
if (options.error.debug) { | |
debugTracking(errorMessage, 'error'); | |
} | |
if (options.error.enabled) { | |
sendTracking({type: LOG_EVENT_ERROR, name: errorName, value: errorMessage}); | |
} | |
} | |
} | |
switch(name) { | |
case 'ajax': | |
return trackAjaxError; | |
case 'debug': | |
return trackCustomError; | |
case 'form': | |
return trackCustomError; | |
case 'onerror': | |
return trackOnError; | |
case 'ui': | |
return trackCustomError; | |
default: | |
// unknown errors are impossible here | |
} | |
} | |
/** | |
* captures all jQuery AJAX Errors | |
*/ | |
function initTrackAjaxError() { | |
$(document).ajaxError(function(event, jqxhr, settings, thrownError) { | |
// jqxhr.status === 0 means the request has been canceled by the browser (e.g because of page reload), we don't care about that | |
var requestHasBeenCanceled = jqxhr.status === 0; | |
if (!requestHasBeenCanceled) { | |
$.publish('track.error.ajax', [jqxhr, settings, thrownError]); | |
} | |
}); | |
} | |
/** | |
* captures all generic script errors via windows.onerror | |
* | |
* @see http://stackoverflow.com/questions/951791/javascript-global-error-handling | |
* https://bugsnag.com/blog/script-error | |
* | |
* @todo - filtering of useless errors (analyze captured errors before adding more filters) | |
*/ | |
function initTrackOnError() { | |
window.onerror = function onError(message, file, row, column, errorObject){ | |
if (options.error.filter.isValidRow(row) && options.error.filter.isValidFile(file)) { | |
$.publish('track.error.onerror', [message, file, row, column, errorObject]); | |
} | |
return options.error.preventDefaultErrorHandler; | |
}; | |
} | |
/** | |
* handle error subscription and initialize error tracking | |
*/ | |
function initTrackError() { | |
if (options.error.filter.isValidBrowser()) { | |
$.each(['ajax', 'debug', 'form', 'onerror', 'ui'], function(index, name) { | |
$.subscribe('track.error.'+name, trackError(name)); | |
}); | |
initTrackAjaxError(); | |
initTrackOnError(); | |
} | |
} | |
/** | |
* capture console output | |
* | |
* @todo - make the output more useful, because it will be tracked too much at the moment, even useless browser error output errors via console.error | |
* - implement ratio | |
* | |
* @see http://stackoverflow.com/a/23679915/2590616 | |
* https://msdn.microsoft.com/en-us/library/windows/apps/hh696634.aspx | |
* http://getfirebug.com/logging | |
*/ | |
function trackConsole() { | |
$.each(['assert', 'cd', 'clear', 'count', 'countReset', 'debug', 'dir', 'dirxml', 'error', | |
'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', | |
'markTimeline', 'profile', 'profileEnd', 'select', 'table', 'time', 'timeEnd', | |
'timeStamp', 'timeline', 'timelineEnd', 'trace', 'warn' | |
], function(index, value) { | |
window.console['_'+value] = window.console[value]; // make a reference to prevent recursion! | |
window.console[value] = function() { | |
var consoleName = 'console.' + value, | |
consoleMessage = [consoleName].concat(Array.prototype.slice.call(arguments)).join(' | ');// NOSONAR | |
// do not track console output, e.g. for internal debug output | |
if (!options.console.temporarilyDisabled) { | |
sendTracking({type: LOG_EVENT_CONSOLE, name: consoleName, value: consoleMessage}); | |
} | |
// call a reference of the function to prevent recursion | |
window.console['_'+value].apply(console, Array.prototype.slice.call(arguments)); | |
return arguments; | |
}; | |
}); | |
} | |
/** | |
* | |
*/ | |
function initTrackConsole() { | |
if (options.console.enabled) { | |
trackConsole(); | |
} | |
} | |
/** | |
* track event | |
* | |
* @todo - check current webtrekk events compatibility | |
* - use convertString() from _ipc-webtrekk.js (maybe in options.send()) | |
* | |
* @param {string} name | |
* @returns {Function} | |
*/ | |
function trackEvent(name){ | |
return function(_event_, eventName, eventValue){ | |
if (options.event.debug) { | |
var eventMessage = [name, eventName]; | |
if (eventValue) { | |
eventMessage.push(JSON.stringify(eventValue)); | |
} | |
eventMessage = eventMessage.join(' | '); | |
debugTracking(eventMessage, 'info'); | |
} | |
if (options.event.enabled) { | |
if (name === 'event') { | |
sendTracking({type: LOG_EVENT_EVENT_TRACKING, name: eventName, value: eventValue}); | |
} | |
if (name === 'page') { | |
sendTracking({type: LOG_EVENT_PAGE, name: eventName}); | |
} | |
} | |
}; | |
} | |
/** | |
* | |
*/ | |
function initTrackEvent() { | |
$.each(['event', 'page', 'session'], function(index, name) { | |
$.subscribe('track.'+name, trackEvent(name)); | |
}); | |
$.subscribe('track.event', trackEvent('event')); | |
} | |
/** | |
* track timing (RUM) | |
* | |
* @todo replace rum tracking script | |
* @returns {Function} | |
*/ | |
function trackTiming(name){ | |
return function(_event_, data){ | |
if (options.debug) console.info(name, JSON.stringify(data)); | |
}; | |
} | |
/** | |
* | |
*/ | |
function initTrackTiming() { | |
$.subscribe('track.timing', trackTiming('track.timing')); | |
} | |
/** | |
* | |
*/ | |
function init() { | |
initOptions(); | |
initTrackError(); | |
initTrackConsole(); | |
initTrackEvent(); | |
initTrackTiming(); | |
} | |
init(); | |
} | |
tracker(window, window.module, window.module.config.tracking, jQuery); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment