shadowaide-js
Version:
Automatically include the Net Promoter Score survey in your web or hybrid app with ShadowAide
1,204 lines (1,069 loc) • 48 kB
JavaScript
(function(){
var cookies;
function readCookie(name, c, C, i) {
if(cookies){ return cookies[name]; }
c = document.cookie.split('; ');
cookies = {};
for(i=c.length-1; i>=0; i--){
C = c[i].split('=');
cookies[C[0]] = C[1];
}
return cookies[name];
}
function writeCookie(name, v, opts) {
document.cookie = name + '=' + v + ';' + opts;
if (!cookies) cookies = {};
cookies[name] = v;
}
window.readCookie = readCookie;
window.writeCookie = writeCookie;
})();
(function() {
if (!String.prototype.includes) {
String.prototype.includes = function(search, start) {
'use strict';
if (typeof start !== 'number') {
start = 0;
}
if (start + search.length > this.length) {
return false;
} else {
return this.indexOf(search, start) !== -1;
}
};
}
})();
/**
* TODO/Fix:
* -- identify cases for login/logout, ensure use details properly attached and logout ends BE session
* -- pagecontent calls sent to BE multiple times
* -- stop sending resource_load events to BE
* -- handle case where log/etc events are sent to BE prior to page load event (page load event maybe should be init'd on first event against location????)
*/
(function() {
'use strict';
/*
* Target queue for processing events async from page once the SA library has been loaded. This definition
* also gets into the main embed code for the script that clients add to their page when loading the script
* async, so we expect the client has created this, but set it up if it hasn't been
*/
window.sa = window.sa || function() {
var sa = {};
sa.config = {
debug: false,
dev: false,
ping_interval: 60,
batch: true,
report_logs: true,
reporting_host: 'https://events.shadowaide.com',
reporting_port: '443',
scroll_tracking: false,
mouse_tracking: false
};
( sa.q = sa.q || [] ).push(arguments);
return sa;
}();
/*
* identify({id}, {traits}, {options}, {callback});
* alias({id}, {secondId}, {options}, {callback});
* send({event}, {properties}, {options}, {callback});
*
* survey
* a/b testing
* debug mode
* *.on() (For client code connecting into events associated with this library)
*
* ? Disable tracking of specific events/data (logs, user ip address,
* ? Ad blocker impacts to script
* ? Enable/disable event call batching
* ? Mobile network rate limiting/delayed submission
* ? Keepalive ping timeperiod
*
* ? How to handle config setting changes related to stuff like log intercepting
*/
function getCurrentScript() {
var currentscript = document.currentScript;
if (!currentscript) {
var scripts = document.getElementsByTagName("script");
for (var scriptId = 0, scriptCount = scripts.length; scriptId < scriptCount; scriptId++) {
if (scripts[scriptId].innerHTML.slice(0, 10) == '//shadowaide') {
currentscript = scripts[scriptId];
break;
}
}
}
return currentscript;
}
var scrollTimeout, resizeTimeout, mouseTimeout;
function debounce(func, timeout, wait, immediate) {
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
return timeout;
};
};
function censor(censor) {
var i = 0;
return function(key, value) {
if(i !== 0 && typeof(censor) === 'object' && typeof(value) == 'object' && censor == value)
return '[Circular]';
if(i >= 29) // seems to be a hard-coded maximum of 30 serialized objects?
return '[Unknown]';
++i; // so we know we aren't using the original object anymore
return value;
}
}
function trim(x) {
return x.replace(/^\s+|\s+$/gm,'');
}
function addEvent(element, evnt, funct) {
if (!element) return;
if (element.attachEvent)
return element.attachEvent('on'+evnt, funct);
else
return element.addEventListener(evnt, funct, false);
}
var rnotwhite = ( /\S+/g );
function removeClass(element, value) {
var current = element.className,
classes = value.match( rnotwhite ) || [],
cur, clazz, finalValue;
cur = element.nodeType === 1 && ( ' ' + trim(current) + ' ' ).replace( value, ' ' );
if ( cur ) {
var j = 0;
while ( ( clazz = classes[ j++ ] ) ) {
// Remove *all* instances
while ( cur.indexOf( ' ' + clazz + ' ' ) > -1 ) {
cur = cur.replace( ' ' + clazz + ' ', ' ' );
}
}
// Only assign if different to avoid unneeded rendering.
finalValue = cur;
if ( current !== finalValue ) {
element.setAttribute( 'class', finalValue );
}
}
}
function addClass(element, value) {
element.className = element.className + ' ' + value;
}
function mergeProps(o1, o2) {
for (var p in o2) {
try {
if ( o2[p].constructor == Object ) {
o1[p] = mergeProps(o1[p], o2[p]);
} else {
o1[p] = o2[p];
}
} catch(e) {
// Property in destination object not set; create it and set its value.
o1[p] = o2[p];
}
}
return o1;
}
var applicationUUID = null,
reportingHost = 'https://events.shadowaide.com',
reportingPort = 443,
dashboardHost = 'https://dashboard.shadowaide.com',
dashboardPort = 443,
currentscript = getCurrentScript();
if (currentscript) {
applicationUUID = currentscript.getAttribute("data-apikey");
if (!applicationUUID) applicationUUID = currentscript.getAttribute("apikey");
if (currentscript.hasAttribute('data-reportingsvr')) reportingHost = currentscript.getAttribute("data-reportingsvr");
else if (currentscript.hasAttribute('reportingsvr')) reportingHost = currentscript.getAttribute("reportingsvr");
if (currentscript.hasAttribute('data-reportingport')) reportingPort = currentscript.getAttribute("data-reportingport");
else if (currentscript.hasAttribute('reportingport')) reportingPort = currentscript.getAttribute("reportingport");
if (currentscript.hasAttribute('data-dashboardsvr')) dashboardHost = currentscript.getAttribute("data-dashboardsvr");
else if (currentscript.hasAttribute('dashboardsvr')) dashboardHost = currentscript.getAttribute("dashboardsvr");
if (currentscript.hasAttribute('data-dashboardport')) dashboardPort = currentscript.getAttribute("data-dashboardport");
else if (currentscript.hasAttribute('dashboardport')) dashboardPort = currentscript.getAttribute("dashboardport");
}
var init = {
/**
* Perform all bootstrap initialization
*/
bootstrap: function() {
init.takeOverConsole();
init.registerListeners();
init.loadStyling();
service.loadQueue();
setInterval(function() {
// service.addNewResourcesToQueue();
service.processQueue();
}, 3000);
},
takeOverConsole: function() {
var console = window.console;
if (!window.sa.config.report_logs || !console) return;
function intercept(method) {
var original = console[method],
reportMethod = method;
console[method] = function() {
if (window.sa.config.report_logs) {
var eventType = service.eventType.CONSOLEERR;
if (reportMethod == 'warn') eventType = service.eventType.CONSOLEWARN;
else if (reportMethod == 'log') eventType = service.eventType.CONSOLELOG;
var details = service.addDefaultData({
type: eventType,
data: arguments
});
service.addToQueue(details);
}
if (original.apply) {
// Do this for normal browsers
original.apply(console, arguments);
} else {
// Do this for IE
var message = Array.prototype.slice.apply(arguments).join(' ');
original(message);
}
};
}
var methods = ['log', 'warn', 'error'];
for (var i = 0; i < methods.length; i++)
intercept(methods[i]);
},
loadStyling: function() {
var cssId = 'sa-css'; // you could encode the css path itself to generate id..
if (!document.getElementById(cssId))
{
var head = document.getElementsByTagName('head')[0];
var link = document.createElement('link');
link.id = cssId;
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = '//events.shadowaide.com/js-client/css/sa_client.css';
link.media = 'all';
head.appendChild(link);
}
},
/**
* Register required listeners for watching for various browser events that we want tracked. To be called during initialization
*/
registerListeners: function() {
var nativeonerror = window.onerror;
window.onerror = function(msg, url, line, col, error) {
handleError(msg, url, line, col, error);
if (nativeonerror) nativeonerror();
};
document.addEventListener('click', function(event) {
var targetText = event.currentTarget.activeElement.textContent;
service.addEventToQueue(service.eventType.CLICK, {
timestamp: new Date().valueOf(),
location: document.URL,
pageX: event.pageX,
pageY: event.pageY,
targetPath: browser.getPathTo(event.target),
sourcePath: browser.getPathTo(event.srcElement),
targetText: targetText
});
});
window.addEventListener('abort', function(event) {
service.addEventToQueue('abort', {
timestamp: new Date().valueOf()
});
});
window.addEventListener('error', function(event) {
service.addToQueue({
type: service.eventType.CONSOLEERR,
timestamp: new Date().valueOf(),
data: event
});
});
window.addEventListener('hashchange', window.shadowaide.pageload);
if ('onpageshow' in window) {
window.addEventListener('pageshow', window.shadowaide.pageload);
window.addEventListener('pagehide', function(event) {});
} else {
window.addEventListener('load', window.shadowaide.pageload);
window.addEventListener('beforeunload', function(event) {
service.addEventToQueue('unload', {
timestamp: new Date().valueOf()
});
// service.addNewResourcesToQueue();
service.processQueue();
});
}
window.addEventListener('resize', function(event) {
resizeTimeout = debounce(function() {
service.addEventToQueue('resize', {
timestamp: new Date().valueOf(),
height: window.innerHeight,
width: window.innerWidth
});
}, resizeTimeout, 300, false)();
});
window.addEventListener('scroll', function(event) {
if (window.sa.config.scroll_tracking) {
scrollTimeout = debounce(function () {
service.addEventToQueue(service.eventType.SCROLL, {
timestamp: new Date().valueOf(),
scrollX: window.scrollX,
scrollY: window.scrollY
});
}, scrollTimeout, 300, false)();
}
});
window.addEventListener('mousemove', function(event) {
if (window.sa.config.mouse_tracking) {
var target = event.toElement || event.relatedTarget || event.target;
mouseTimeout = debounce(function () {
service.addEventToQueue(service.eventType.MOUSEMOVE, {
timestamp: new Date().valueOf(),
toElement: browser.getPathTo(target),
offsetX: event.offsetX,
offsetY: event.offsetY,
offsetXPct: event.offsetX / target.offsetWidth,
offsetYPct: event.offsetY / target.offsetHeight
});
}, mouseTimeout, 300, false)();
}
// How to get x and y offset within the element (should make as a percentage position to allow for resized elements
});
window.addEventListener('unload', function(event) {
// service.addNewResourcesToQueue();
service.processQueue();
});
window.addEventListener('submit', function(event) {
service.addEventToQueue('submit', service.addDefaultData({}));
});
// if (document.readyState === 'complete') {
if (/loaded|complete/.test(document.readyState)) {
// Handle case where page has loaded prior to this script executing (preventing a page load event from happening)
window.shadowaide.pageload();
}
}
};
var service = {
eventType: Object.freeze({
PAGELOAD: 'page_load',
RESOURCELOAD: 'resource_load',
IDENTIFY: 'identify',
CLICK: 'click',
SCROLL: 'scroll',
LOG: 'log',
CONSOLELOG: 'console_log',
CONSOLEWARN: 'console_warn',
CONSOLEERR: 'console_error',
INTERACTION: 'interaction',
MOUSEMOVE: 'mousemove'
}),
/**
* Collection of all events loaded that need to be sent to the backend. This is synced with LocalStorage and/or cookies
* (as needed) to keep track of events after a page transition
*
* @type {Array}
*/
eventqueue: [],
isProcessing: false,
lastLoadedResource: -1,
addDefaultData: function(info) {
info.timestamp = new Date().valueOf();
if (!info.data) info.data = {};
info.data.title = document.title;
info.data.location = document.URL;
info.data.browser = browser.getBrowserDetails();
var timing = browser.getObjectProfile(window.performance && window.performance.timing ? window.performance.timing : {});
info.data.timing = {
dns: timing.dns,
dom: timing.dom,
duration: timing.duration,
entryType: timing.entryType,
load: timing.load,
loadtime: timing.loadtime,
name: timing.name,
ssl: timing.ssl,
tcp: timing.tcp,
ttfb: timing.ttfb
}
info.data.referrer = document.referrer;
return info;
},
/**
* Post individual event to service for processing
*
* @param type
* @param data
*/
sendEvent: function(type, data) {
var session = getUserSession(),
data = {
apikey: applicationUUID,
type: type,
session: session,
data: data
},
value = JSON.stringify(data);
var http = new XMLHttpRequest();
http.open('POST', reportingHost + ':' + reportingPort + '/api/v1/interaction/');
http.setRequestHeader('Content-Type', 'application/json');
http.send(value);
},
/**
* Send the supplied batch to the service for processing
*
* @param batchData
*/
sendBatch: function(batchData) {
if (batchData.length < 1) return;
var session = getUserSession(),
data = {
apikey: applicationUUID,
session: session,
events: batchData
},
value = JSON.stringify(data);
var http = new XMLHttpRequest();
http.open('POST', reportingHost + ':' + reportingPort + '/api/v1/event/batch');
http.setRequestHeader('Content-Type', 'application/json');
http.send(value);
},
/**
* Add a new event to the processing queue to be sent to the service. Maintains sync between events in the queue and
* in persistent storage
*
* @param data
*/
addToQueue: function(data) {
service.eventqueue.push(data);
return service.persistQueue();
},
/**
* Save the current queue stream to persistent storage
*
* @param queue
*/
persistQueue: function(queue) {
var persist = queue ? queue : service.eventqueue;
browser.storeData('shadow.queue', JSON.stringify(persist, censor(persist)));
return queue ? queue : service.eventqueue;
},
/**
* Load queue data from persisten storage
*/
loadQueue: function() {
var cached = browser.getData('shadow.queue');
if (cached) service.eventqueue = JSON.parse(cached);
else if (!service.eventqueue) service.eventqueue = [];
return service.eventqueue;
},
batchSize: 10,
/**
* Process the current queue records
*/
processQueue: function() {
if (!applicationUUID || service.isProcessing) return; // skip if we're already processing against the queue when the next loop hits
service.isProcessing = true;
while (service.eventqueue.length) {
try {
service.sendBatch(service.eventqueue.slice(0, service.batchSize));
service.eventqueue.splice(0, service.batchSize);
service.persistQueue(); // Update to remove any successfully processed messages
} catch (ex) {
service.isProcessing = false;
break;
}
}
service.isProcessing = false;
},
addNewResourcesToQueue: function() {
var resources = browser.getResourceProfiles(); // Load any http requests made since last process loop and queue for sending
var compareHost = reportingHost.replace(/^https?\:/i, '');
for (var resourceId=0, resourceCount=resources.length; resourceId < resourceCount; resourceId++) {
var compareUrl = resources[resourceId].name.replace(/^https?\:/i, '');
if (compareUrl != compareHost + ((reportingPort == '80' || reportingPort == '443') ? '' : ':' + reportingPort) + '/api/v1/event/batch')
service.addToQueue({
type: service.eventType.RESOURCELOAD,
timestamp: new Date().getTime(),
data: browser.getObjectProfile(resources[resourceId])
});
}
},
addEventToQueue: function(eventType, event) {
var details = service.addDefaultData({
type: eventType,
event: eventType,
data: event
});
service.addToQueue(details);
}
};
var browser = {
getObjectProfile: function(profData) {
return {
loadtime: profData.requestStart && profData.navigationStart ? profData.loadEventStart - profData.navigationStart :undefined,
dns: profData.domainLookupEnd && profData.domainLookupStart ? profData.domainLookupEnd - profData.domainLookupStart :undefined,
tcp: profData.connectEnd && profData.connectStart ? profData.connectEnd - profData.connectStart :undefined,
ssl: profData.connectEnd && profData.secureConnectionStart ? profData.connectEnd - profData.secureConnectionStart :undefined,
ttfb: profData.responseStart && profData.navigationStart ? profData.responseStart - profData.navigationStart :undefined,
dom: profData.domComplete && profData.domLoading ? profData.domComplete - profData.domLoading :undefined,
load: profData.loadEventEnd && profData.loadEventStart ? profData.loadEventEnd - profData.loadEventStart :undefined,
name: profData.name ? profData.name : window.location.href,
duration: profData.duration ? profData.duration : undefined,
initiatorType: profData.initiatorType ? profData.initiatorType : undefined,
entryType: profData.entryType ? profData.entryType :'page'
};
},
getBrowserDetails: function() {
var result = {
agent: navigator.userAgent.toLowerCase(),
codename: navigator.appCodeName,
name: navigator.appName,
version: navigator.appVersion,
majorver: parseInt(navigator.appVersion),
minorver: parseFloat(navigator.appVersion),
platform: navigator.platform,
viewport: Math.max(document.documentElement.clientWidth, window.innerWidth || 0) + 'x' +
Math.max(document.documentElement.clientHeight,window.innerHeight || 0),
resolution: window.screen.availHeight + 'x' + window.screen.availWidth,
pixelratio: window.devicePixelRatio,
language: navigator.languages || navigator.language || navigator.userLanguage,
plugins: []
};
var plugins = browser.getPlugins();
for (var i=0; i<plugins.length; i++) {
result.plugins.push({
name: plugins[i].name,
mime: plugins[i].mime
});
}
return result;
},
getPlugins: function() {
var result = [];
for (var pluginId=0, pluginCount=navigator.plugins.length; pluginId < pluginCount; pluginId++) {
result.push(browser.getPluginDetails(navigator.plugins[pluginId]));
}
return result;
},
getPluginDetails: function(pluginConfig) {
var mimeType = null,
result = {
name: pluginConfig.name
};
for (var mimeId=0, mimeCount=pluginConfig.length; mimeId < mimeCount; mimeId++) {
if (mimeType) mimeType += ','
mimeType += pluginConfig[mimeId].type;
}
result.mime = mimeType;
return result;
},
lastLoadedResource: -1,
getResourceProfiles: function(lastResourceSeen) {
if (!lastResourceSeen) lastResourceSeen = browser.lastLoadedResource;
var resources = (window.performance && window.performance.getEntriesByType) ? window.performance.getEntriesByType("resource") :
(window.performance && window.performance.getEntries) ? window.performance.getEntries({ entryType: "resource" }) : [],
result = [];
for (var resourceId=lastResourceSeen+1, resourceCount=resources.length; resourceId < resourceCount; resourceId++) {
result.push(resources[resourceId]);
browser.lastLoadedResource = resourceId;
}
return result;
},
storeData: function(key, value) {
if (localStorage) localStorage.setItem(key, value)
else { //TODO
}
},
getData: function(key) {
var data = null;
if (localStorage)
data = localStorage.getItem(key);
else { }
return data;
},
getPathTo: function(element) {
if (!element) return;
if (element.id!=='')
return 'id("'+element.id+'")';
if (element===document.body)
return element.tagName;
var ix = 0;
var siblings = element.parentNode ? element.parentNode.childNodes : [];
for (var i= 0; i<siblings.length; i++) {
var sibling= siblings[i];
if (sibling===element)
return browser.getPathTo(element.parentNode)+'/'+element.tagName+'['+(ix+1)+']';
if (sibling.nodeType===1 && sibling.tagName===element.tagName)
ix++;
}
}
};
var util = {
/**
* Helper function for generating a uuid
*
* @returns {string}
*/
guid: function() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
};
/**
* Called when the window object raises an error, formats the data into an appropriate tracker and adds it to the
* event queue
*
* @param msg
* @param url
* @param line
* @param col
* @param error
*/
function handleError(msg, url, line, col, error) {
service.addToQueue({
msg: msg,
url: url,
line: line,
col: col,
error: error
});
}
/**
* Preprocess a stack record for formatting. TODO: handle processing of any available js map file for included stack elements
*
* @param stack
* @returns {Array}
*/
function processStack(stack) {
return stack
.replace(/^[^\(]+?[\n$]/gm, '')
.replace(/^s+at\s+/gm, '')
.replace(/^Object.<anonymous>\s*\(/gm, '{anonymous}()@')
.split('\n')
}
function getUserSession() {
var session = browser.getData('session');
if (!session) {
session = readCookie('sa_session'); // Work around local storage not working across sub-domains
if (!session) {
var rootDomain = location.hostname.match(/^([^\/?#]+)(?:[\/?#]|$)/i)[1].split('.').slice(-2).join('.');
session = util.guid();
writeCookie('sa_session', session, 'domain=' + rootDomain + ';path=/');
}
browser.storeData('session', session);
}
return session;
}
/**
* Does stuff like checking if there's extra sampling needed (like saving the page content), survey flow status/config,
* A/B testing, etc
*/
var saSettings = null;
function getClientSettings(doneFn) {
var session = getUserSession(),
data = {
apikey: applicationUUID,
session: session
},
value = JSON.stringify(data);
var http = new XMLHttpRequest();
http.onreadystatechange = function() {
if (http.readyState == XMLHttpRequest.DONE) {
if (http.status == 200 || http.status == 304) {
var settings = JSON.parse(http.responseText);
if (settings) {
var sasdec = null,
sasres = null,
jsDayInterval = 86400000,
currentDate = new Date(),
declineDate = currentDate - settings.declineResurveyInterval * jsDayInterval,
resurveyDate = currentDate - settings.resurveyInterval * jsDayInterval;
settings.surveyAllowed = true;
if (localStorage) {
sasdec = readCookie('sasdec');
sasres = readCookie('sasres');
}
if (sasdec && sasdec > declineDate ||
sasres && sasres > resurveyDate) {
settings.surveyAllowed = false;
}
if (settings.remaining <= 0) {
settings.surveyAllowed = false;
}
saSettings = settings;
}
}
if (doneFn) doneFn(saSettings);
}
};
http.open('GET', dashboardHost + ':' + dashboardPort + '/api/v1/config');
http.setRequestHeader('Content-Type', 'application/json');
http.setRequestHeader('x-api-key', applicationUUID);
http.send(value);
};
getClientSettings();
function copyPage() {
return {
type: 'pagecontents',
apikey: applicationUUID,
data: {
location: document.URL,
doctype: document.doctype,
document: document.documentElement.outerHTML
}
};
};
function sendPage(pageData) {
var value = JSON.stringify(pageData);
var http = new XMLHttpRequest();
http.open('POST', reportingHost + ':' + reportingPort + '/api/v1/event');
http.setRequestHeader('Content-Type', 'application/json');
http.send(value);
};
function inSample(isAnon, anonSampleRate, idSampleRate) {
var rand = Math.random();
if (!isAnon) {
if (rand < (+idSampleRate / 100)) return true;
} else {
if (rand < (+anonSampleRate / 100)) return true;
}
return false;
}
function pageQualifiesForSurvey(page, allowedList) {
for (var i = 0; i < allowedList.length; i++) {
if (!page.search(allowedList[i])) continue;
return true;
}
return false;
}
function showNPSSurvey() {
if (!saSettings) {
getClientSettings(function(settings) {
if (userQualifiesForSurvey(settings)) {
setTimeout(function() {
renderSurvey();
}, settings.timeDelay * 1000);
}
});
} else {
if (userQualifiesForSurvey(saSettings)) {
setTimeout(function() {
renderSurvey();
}, saSettings.timeDelay * 1000);
}
}
}
function userQualifiesForSurvey(settings) {
if (window.sa.config.dev) return true;
if (!settings || !settings.surveyAllowed) return false;
var isAnon = !window.shadowaide.identify || !window.shadowaide.identify.email;
if (!inSample(isAnon, settings.anonymousPct, settings.registeredPct)) return false;
if (!pageQualifiesForSurvey(window.location.pathname, settings.showOn)) return false;
return true;
}
function renderSurvey() {
if (localStorage) writeCookie('sasres', new Date());
var existingSurvey = document.getElementById('sa-survey-modal');
if (existingSurvey) return; //TODO: Reset survey to default;
var config = {
lang: {
en: {
nps_question: 'How likely are you to recommend {{product}} to a friend or co-worker?',
product: 'this product or service',
website: "this website",
score_unlikely: 'Not al all likely',
score_likely: 'Extremely likely',
dismiss: 'Dismiss',
explain_score: 'Help us by explaining your score',
submit: 'Send',
thanks: 'Thank you for your response, and your feedback!'
},
es: { // Spain spanish
nps_question: "¿Qué probabilidad hay de que usted recomiende este {{product}} a un amigo o compañero de trabajo?",
product: "producto o servicio",
website: "sitio web",
score_unlikely: "Absolutamente improbable",
score_likely: "Extremadamente probable",
dismiss: "Descartar",
explain_score: "Ayúdenos explicando su puntuación",
submit: "ENVIAR",
thanks: '¡Gracias por su respuesta y por sus comentarios!'
},
ru: { // Russian
nps_question: "Насколько вероятно, что вы порекомендуете этот {{product}} другу или коллеге?",
product: "продукт или сервис",
website: "веб-сайт",
score_unlikely: "Маловероятно",
score_likely: "Очень вероятно",
dismiss: "Отклонить",
explain_score: "Помогите нам, объяснив ваш рейтинг",
submit: "ОТПРАВИТЬ",
thanks: 'Спасибо за Ваш ответ и отзыв!!'
},
pt: { // Portuguese
nps_question: "Qual a probabilidade de você recomendar este {{product}} a um amigo ou colega de trabalho?",
product: "produto ou serviço",
website: "website",
score_unlikely: "Nem um pouco provável",
score_likely: "Extremamente provável",
dismiss: "Dispensar",
explain_score: "Ajude-nos explicando sua classificação",
submit: "ENVIAR",
thanks: 'Obrigado por sua resposta e por seus comentários!'
},
ja: { // Japanese
nps_question: "この{{product}}を友人または同僚に薦める可能性はどれくらいありますか?",
product: "商品またはサービス",
website: "ウェブサイト",
score_unlikely: "可能性が全くない",
score_likely: "可能性が非常に高い",
dismiss: "無視",
explain_score: "そのスコアをつけた理由を説明してください",
submit: "送信",
thanks: 'ご意見とご感想をありがとうございました!'
},
it: { // Italian
nps_question: "Quanto è probabile che raccomanderà questo {{product}} ad un amico o a un collega?",
product: "prodotto o servizio",
website: "sito internet",
score_unlikely: "Per niente probabile",
score_likely: "Estremamente probabile",
dismiss: "Declinare",
explain_score: "Ci aiuti spiegando la Sua valutazione",
submit: "INVIA",
thanks: 'Grazie per la tua risposta e per la tua valutazione!'
},
hi: { // Hindi
nps_question: "आपके द्वारा इस {{product}} की सिफारिश किसी मित्र या सहकर्मी को करने की कितनी संभावना है?",
product: "उत्पाद या सेवा",
website: "वेबसाइट",
score_unlikely: "कोई संभावना नहीं",
score_likely: "अत्यंत संभावना",
dismiss: "हटाएं",
explain_score: "अपना स्कोर समझाकर हमारी सहायता करें",
submit: "भेजें",
thanks: 'आपकी प्रतिक्रिया, और आपके फीडबैक के लिए धन्यवाद!'
},
de: { // German
nps_question: "Wie wahrscheinlich ist es, dass Sie {{product}} einem Freund oder Arbeitskollegen weiterempfehlen?",
product: "dieses Produkt/diesen Service",
website: "diese Website",
score_unlikely: "Sehr unwahrscheinlich",
score_likely: "Sehr wahrscheinlich",
dismiss: "Ignorieren",
explain_score: "Helfen Sie uns, indem Sie Ihre Bewertung erläutern",
submit: "ABSCHICKEN",
thanks: 'Danke für Ihre Antworten und Ihr Feedback!'
},
fr: { // French
nps_question: "Comment situez-vous la probabilité que vous recommandiez ce {{product}} à un ami ou un collègue ?",
product: "produit ou service",
website: "site Web",
score_unlikely: "Pas du tout probable",
score_likely: "Extrêmement probable",
dismiss: "Rejeter",
explain_score: "Aidez-nous en expliquant votre score",
submit: "ENVOYER",
thanks: 'Merci pour votre réponse et vos commentaires !'
},
zh: { // Chinese (simplified, Mandarin... technically zh_HANS and zh_CN)
nps_question: "您向朋友或同事推荐此{{product}}的可能性有多大?",
product: "产品或服务",
website: "网站",
score_unlikely: "完全不可能",
score_likely: "极有可能",
dismiss: "忽略",
explain_score: "解释您的分数来帮助我们",
submit: "发送",
thanks: '感谢您的回应,以及您的反馈'
}
}
};
config = mergeProps(config, window.sa.config);
var lang = navigator.languages && navigator.languages[0] ||
navigator.language ||
navigator.userLanguage;
if (!lang) lang='en';
if (config.lang[lang] === undefined && lang.includes('-'))
lang = lang.substring(0, lang.indexOf('-'));
if (config.lang[lang] == undefined) lang = 'en';
var strings = config.lang[lang],
question = strings.nps_question;
question = question.replace('{{product}}', strings.product);
var surveyHtml = '<div id="sa-survey-modal" class="light sa-survey-modal sa-bottom">\
<form id="sa-survey-form" method="post">\
<div id="sa-survey-close"><span id="sa-survey-x">X</span><span id="sa-survey-dismiss">' + strings.dismiss + '</span></div>\
<div class="thanks sa-survey-collapsed" id="sa-survey-thanks">\
<p>' + strings.thanks + '</p>\
</div>\
<div class="sa-survey-question" id="sa-survey-question">\
<p id="sa-language-direction" dir="ltr">' + question + '</p>\
</div>\
<div class="sa-nps-score" id="sa-survey-fullscore">\
<ul id="sa-nps-score">\
<li>0</li>\
<li>1</li>\
<li>2</li>\
<li>3</li>\
<li>4</li>\
<li>5</li>\
<li>6</li>\
<li>7</li>\
<li>8</li>\
<li>9</li>\
<li>10</li>\
</ul>\
<label id="sa-survey-not-likely-label">' + strings.score_unlikely + '</label>\
<label id="sa-survey-likely-label">' + strings.score_likely + '</label>\
</div>\
<div class="sa-nps-score sa-survey-collapsed" id="sa-survey-feedback">\
<textarea name="feedback" rows="2" placeholder="' + strings.explain_score + '" id="sa-survey-text"></textarea>\
<input type="submit" value="' + strings.submit + '" id="sa-survey-submit">\
</div>\
</form>\
<p id="sa-powered-by">Powered By <a href="https://www.shadowaide.com" target="_blank">ShadowAide</a></p>\
</div>';
var div = document.createElement('div');
div.innerHTML = surveyHtml;
document.body.appendChild(div);
var selectedScore = null;
addEvent(document.getElementById('sa-nps-score'), 'click', function(e) {
selectedScore = +e.target.outerText;
var children=e.target.parentElement.children,
count=children.length;
for (var score=0; score < count; score++) {
removeClass(children[score], 'sa-score-selected');
}
addClass(e.target, 'sa-score-selected');
removeClass(document.getElementById('sa-survey-feedback'), 'sa-survey-collapsed');
});
document.getElementById('sa-survey-form').onsubmit = function(e) {
e.stopPropagation(); event.cancelBubble = true; e.preventDefault();
var details = service.addDefaultData({
type: 'survey',
data: {
score: selectedScore,
feedback: document.getElementById('sa-survey-text').value
}
});
service.addToQueue(details);
removeClass(document.getElementById('sa-survey-thanks'), 'sa-survey-collapsed');
addClass(document.getElementById('sa-survey-fullscore'), 'sa-survey-collapsed');
addClass(document.getElementById('sa-survey-question'), 'sa-survey-collapsed');
addClass(document.getElementById('sa-survey-feedback'), 'sa-survey-collapsed');
addClass(document.getElementById('sa-survey-submit'), 'sa-survey-collapsed');
};
addEvent(document.getElementById('sa-survey-close'), 'click', function(e) {
if (localStorage) writeCookie('sasdec', new Date());
div.remove();
});
}
window.shadowaide = {
init: function(key) {
applicationUUID = key;
},
config: function(data) {
// if (!data) return shadowconfig.config;
},
identify: function(data) {
if (!data) return window.shadowaide.identity;
if (!data.email) {
data = {
id: null,
name: null,
email: null,
age: null,
plan: null,
customer_since: null,
active_experiments: null
};
}
window.shadowaide.identity = data;
service.addEventToQueue(service.eventType.IDENTIFY, data);
/*
* data: [userid], [traits], [segments], [callback]
* data example: {
* id: 'id_i18kjdsn',
* name: 'Test User',
* email: 'test@sg.com' // Required
* age: 23,
* plan: 'premium',
* customer_since: '2011/06/01',
* active_experiments: ['plans_b', 'landing_c']
* }
* }
*
* [userid] is generated automatically and tied to a user until they clear their cache,
* provide a app supplied value for the user ID to track more broadly
*/
},
debug: function(mode) {
if (!mode) return window.sa.config.debug;
else window.sa.config.debug = mode;
},
log: function(level, msg, data) {
var details = service.addDefaultData({
type: service.eventType.LOG,
msg: msg,
data: data
});
service.addToQueue(details);
},
track: function(type, event, data) {},
event: function(type, data) {
data.timestamp = new Date().valueOf();
service.addEventToQueue(type, data);
},
pageload: function(event) {
var details = service.addDefaultData({
type: service.eventType.PAGELOAD
});
service.addToQueue(details);
// sendPage(copyPage());
service.processQueue();
},
error: function(error) {
},
try: function(wrappedFunction) {
try {
wrappedFunction();
} catch(error) {
this.error(error);
}
},
showSurvey: showNPSSurvey
};
/*
* Collection items for errors
* --> Stack trace (at failure point)
* --> sequence of steps user processed from start of page until error occurred (including page initialization data)
* --> Any console output
* --> User click events
* --> Triggered mouseover/etc events
* --> Ajax requests
*
* Collection items for triggered application log
*
* collect usage analytics
*
* Page collection details
* --> page load time (w/ analytics on event/subtimes
* --> Time on page
* --> Timestamp
* --> URL
* --> User Local Timestamp (or timezone)
* --> JS libraries found on page (inc. version number)
*/
init.bootstrap();
/*
* Replace queue with function to live operate of supplied commands
*/
// window.sa = function() {
// ( window.sa.q = window.sa.q || [] ).push(arguments);
// };
}());