UNPKG

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
(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); // }; }());