Comparing version 0.0.9 to 0.1.0
@@ -101,97 +101,103 @@ (function webpackUniversalModuleDefinition(root, factory) { | ||
class Config { | ||
// Set up a default config | ||
constructor() { | ||
// Type: string, REQUIRED | ||
// Endpoint Url | ||
this.uploadEndpoint = "http://localhost:9000"; | ||
// Type: string | ||
// Website ID | ||
this.websiteId = "unknown"; | ||
// Default config | ||
let config = { | ||
// Type: string, REQUIRED | ||
// Endpoint Url | ||
uploadEndpoint: "http://localhost:9000", | ||
// Endpoint type, "absolute" or "relative" | ||
this.endpointType = "absolute"; | ||
// Type: string | ||
// Website ID | ||
websiteId: "unknown", | ||
// Upload mode, "mixed", "periodic" or "event-triggered" | ||
this.uploadMode = "mixed"; | ||
// Endpoint type, "absolute" or "relative" | ||
endpointType: "absolute", | ||
// Type: number | ||
// If `uploadMode` is "mixed", "periodic", data will be uploaded every `uploadPeriod` ms. | ||
// If no data are collected in a period, no data will be uploaded | ||
this.uploadPeriod = 5000; | ||
// upload protocol, "https" or "http" | ||
// If you declare it in `uploadEndpoint`, this property will be ignored. | ||
uploadProtocol: "https", | ||
// Type: number | ||
// If `uploadMode` == "event-triggered" | ||
// The website interaction data will be uploaded when every `frequency` events are captured. | ||
this.frequency = 50; | ||
// Upload mode, "periodic" or "event-triggered" | ||
uploadMode: "periodic", | ||
// Type: bool | ||
// Use GET method to upload data? (stringified data will be embedded in URI) | ||
this.enableGet = false; | ||
// Type: number | ||
// If `uploadMode` == "periodic", data will be uploaded every `uploadPeriod` ms. | ||
// If no data are collected in a period, no data will be uploaded | ||
uploadPeriod: 5000, | ||
// Type: number | ||
// Time interval for resending the failed trace data | ||
this.resendInterval = 3000; | ||
// Type: number | ||
// If `uploadMode` == "event-triggered" | ||
// The website interaction data will be uploaded when every `frequency` events are captured. | ||
frequency: 50, | ||
// Type: HTML DOM Element | ||
// Capture the events occur in `this.scope` | ||
this.scope = window.document; | ||
// Type: bool | ||
// Use GET method to upload data? (stringified data will be embedded in URI) | ||
enableGet: false, | ||
// These parameters are required for runing a Mouselog agent | ||
this._requiredParams = [ | ||
"uploadEndpoint", | ||
] | ||
// Type: number | ||
// Time interval for resending the failed trace data | ||
resendInterval: 3000, | ||
} | ||
// These parameters will be ignored when updating config | ||
this._ignoredParams = [ | ||
"scope", | ||
] | ||
} | ||
// ---------------------------- | ||
build(config, isUpdating = false) { | ||
try { | ||
this._requiredParams.forEach(key => { | ||
if (!config.hasOwnProperty(key)) { | ||
throw new Error(`Param ${key} is required but not declared.`); | ||
} | ||
}); | ||
// Overwrite the default config | ||
Object.keys(config).forEach( key => { | ||
// Overwriting Class private members / function method is not allowed | ||
if (this[key] && !key.startsWith("_") && typeof(this[key]) != "function") { | ||
// Do not update some `ignored` parameter | ||
if (!(isUpdating && key in this._ignoredParams)) { | ||
this[key] = config[key] | ||
} | ||
} | ||
}) | ||
this.absoluteUrl = this._formatUrl(); | ||
} catch(err) { | ||
console.log(err); | ||
return false; | ||
} | ||
return true; | ||
} | ||
let requiredParams = [ | ||
"uploadEndpoint", | ||
]; | ||
update(config) { | ||
return this.build(config, true); | ||
} | ||
// Returns a boolean indicating if config is built successfully | ||
let buildConfig = (params) => { | ||
try { | ||
requiredParams.forEach(key => { | ||
if (!(params.hasOwnProperty(key))) { | ||
throw new Error(`Param ${key} is required but not declared.`); | ||
_formatUrl() { | ||
let url = this.uploadEndpoint; | ||
if (this.endpointType == "relative") { | ||
if (url.startsWith("./")) { | ||
url = url.slice(1); | ||
} else if (url[0] !== "/") { | ||
url = "/" + url; | ||
} | ||
}); | ||
config = Object.assign(config, params); | ||
config.absoluteUrl = formatUrl(); | ||
} catch(err) { | ||
console.log(err); | ||
return false; | ||
} | ||
return true; | ||
} | ||
let updateConfig = (params) => { | ||
// Generate new config | ||
return buildConfig(params); | ||
} | ||
let formatUrl = () => { | ||
let url = config.uploadEndpoint | ||
if (config.endpointType == "relative") { | ||
// Format the head -> "/*" | ||
if (url.startsWith("./")) { | ||
url = url.slice(1); | ||
} else if (url[0] !== "/") { | ||
url = `/${url}`; | ||
// Format the tail | ||
if (url[url.length-1] === "/") { | ||
url = url.slice(0, url.length-1); | ||
} | ||
// Construct absolute URL | ||
url = `${window.location.origin}${url}` | ||
} else if (this.endpointType !== "absolute") { | ||
throw new Error('`endpointType` can only be "absolute" or "relative"'); | ||
} | ||
// Format the tail | ||
if (url[url.length-1] === "/") { | ||
url = url.slice(0, url.length-1); | ||
} | ||
// Construct absolute URL | ||
url = `${config.uploadProtocol}://${window.location.hostname}${url}`; | ||
} else if (config.endpointType == "absolute") { | ||
if (!(url.startsWith("http://") || url.startsWith("https://"))) { | ||
url = `${config.uploadProtocol}://${url}`; | ||
} | ||
} else { | ||
throw new Error('`endpointType` can only be "absolute" or "relative"'); | ||
return url; | ||
} | ||
return url; | ||
} | ||
module.exports = { config, buildConfig, updateConfig } | ||
module.exports = { Config }; | ||
@@ -202,27 +208,6 @@ /***/ }), | ||
let mouselog = __webpack_require__(2); | ||
const uuidv4 = __webpack_require__(2); | ||
const Uploader = __webpack_require__(5); | ||
let { Config } = __webpack_require__(0); | ||
function run(config) { | ||
mouselog.run(config); | ||
} | ||
function stop() { | ||
mouselog.stop(); | ||
} | ||
module.exports = { run, stop }; | ||
/***/ }), | ||
/* 2 */ | ||
/***/ (function(module, __webpack_exports__, __webpack_require__) { | ||
"use strict"; | ||
__webpack_require__.r(__webpack_exports__); | ||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "run", function() { return run; }); | ||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "stop", function() { return stop; }); | ||
const uuidv4 = __webpack_require__(3); | ||
const Uploader = __webpack_require__(6); | ||
let { config, buildConfig } = __webpack_require__(0); | ||
let targetEvents = [ | ||
@@ -240,139 +225,171 @@ "mousemove", | ||
]; | ||
let pageLoadTime = new Date(); | ||
let uploader; | ||
let impressionId; | ||
let eventsList; | ||
let pageLoadTime; | ||
let uploadIdx; | ||
let uploadInterval; | ||
function getRelativeTimestampInSeconds() { | ||
let diff = new Date() - pageLoadTime; | ||
return Math.trunc(diff) / 1000; | ||
} | ||
class Mouselog{ | ||
constructor() { | ||
this.config = new Config(); | ||
this.impressionId = uuidv4(); | ||
this.mouselogLoadTime = new Date(); | ||
this.uploader = new Uploader(); | ||
this.eventsList = []; | ||
this.eventsCount = 0; | ||
this.uploadInterval; // For "periodic" upload mode | ||
this.uploadTimeout; // For "mixed" upload mode | ||
} | ||
function getButton(btn) { | ||
if (btn === '2') { | ||
return 'Right'; | ||
} else { | ||
return "" | ||
_clearBuffer() { | ||
this.eventsList = []; | ||
} | ||
} | ||
function newTrace() { | ||
let trace = { | ||
id: '0', | ||
idx: uploadIdx, | ||
url: window.location.hostname ? window.location.hostname : "localhost", | ||
path: window.location.pathname, | ||
width: document.body.scrollWidth, | ||
height: document.body.scrollHeight, | ||
pageLoadTime: pageLoadTime, | ||
label: -1, | ||
guess: -1, | ||
events: [] | ||
} | ||
uploadIdx += 1; | ||
return trace; | ||
} | ||
_newTrace() { | ||
let trace = { | ||
id: '0', | ||
idx: this.uploadIdx, | ||
url: window.location.hostname ? window.location.hostname : "localhost", | ||
path: window.location.pathname, | ||
width: document.body.scrollWidth, | ||
height: document.body.scrollHeight, | ||
pageLoadTime: pageLoadTime, | ||
events: [] | ||
} | ||
this.uploadIdx += 1; | ||
return trace; | ||
} | ||
function uploadTrace() { | ||
let trace = newTrace(); | ||
trace.events = eventsList; | ||
eventsList = []; | ||
return uploader.upload(trace); // This is a promise | ||
} | ||
_mouseHandler(evt) { | ||
// PC's Chrome on Mobile mode can still receive "contextmenu" event with zero X, Y, so we ignore these events. | ||
if (evt.type === 'contextmenu' && evt.pageX === 0 && evt.pageY === 0) { | ||
return; | ||
} | ||
let x = evt.pageX; | ||
let y = evt.pageY; | ||
if (x === undefined) { | ||
x = evt.changedTouches[0].pageX; | ||
y = evt.changedTouches[0].pageY; | ||
} | ||
let tmpEvt = { | ||
id: this.eventsCount, | ||
timestamp: getRelativeTimestampInSeconds(), | ||
type: evt.type, | ||
x: x, | ||
y: y, | ||
button: getButton(evt.button) | ||
} | ||
function mouseHandler(evt) { | ||
// PC's Chrome on Mobile mode can still receive "contextmenu" event with zero X, Y, so we ignore these events. | ||
if (evt.type === 'contextmenu' && evt.pageX === 0 && evt.pageY === 0) { | ||
return; | ||
if (evt.type == "wheel") { | ||
tmpEvt.deltaX = evt.deltaX; | ||
tmpEvt.deltaY = evt.deltaY; | ||
} | ||
this.eventsList.push(tmpEvt); | ||
this.eventsCount += 1; | ||
if ( this.config.uploadMode == "event-triggered" && this.eventsList.length % this.config.frequency == 0 ) { | ||
this._uploadTrace(); | ||
} | ||
if ( this.config.uploadMode == "mixed" && this.eventsList.length % this.config.frequency == 0) { | ||
this._periodUploadTimeout(); | ||
this._uploadTrace(); | ||
} | ||
} | ||
let x = evt.pageX; | ||
let y = evt.pageY; | ||
if (x === undefined) { | ||
x = evt.changedTouches[0].pageX; | ||
y = evt.changedTouches[0].pageY; | ||
_fetchConfigFromServer() { | ||
// Upload an empty trace to fetch config from server | ||
let trace = this._newTrace(); | ||
return this.uploader.upload(trace); // This is a promise | ||
} | ||
let tmpEvt = { | ||
id: eventsList.length, | ||
timestamp: getRelativeTimestampInSeconds(), | ||
type: evt.type, | ||
x: x, | ||
y: y, | ||
button: getButton(evt.button) | ||
_uploadTrace() { | ||
let trace = this._newTrace(); | ||
trace.events = this.eventsList; | ||
this.eventsList = []; | ||
return this.uploader.upload(trace); // This is a promise | ||
} | ||
if (evt.type == "wheel") { | ||
tmpEvt.deltaX = evt.deltaX; | ||
tmpEvt.deltaY = evt.deltaY; | ||
_periodUploadTimeout() { | ||
clearTimeout(this.uploadTimeout); | ||
this.uploadTimeout = setTimeout(() => { | ||
if (this.eventsList.length > 0) { | ||
this._uploadTrace(); | ||
} | ||
}) | ||
} | ||
eventsList.push(tmpEvt); | ||
if ( config.uploadMode == "event-triggered" && eventsList.length % config.frequency == 0 ) { | ||
uploadTrace(); | ||
_periodUploadInterval() { | ||
clearInterval(this.uploadInterval); | ||
this.uploadInterval = setInterval(() => { | ||
if (this.eventsList.length > 0) { | ||
this._uploadTrace(); | ||
} | ||
}, this.config.uploadPeriod); | ||
} | ||
} | ||
function clearBuffer() { | ||
eventsList = []; | ||
} | ||
_runCollector() { | ||
targetEvents.forEach( s => { | ||
this.config.scope.addEventListener(s, (evt) => this._mouseHandler(evt)); | ||
}); | ||
// Initialize the mouselog | ||
function init(params) { | ||
return new Promise((resolve) => { | ||
clearBuffer(); | ||
pageLoadTime = new Date(); | ||
uploadIdx = 0; | ||
uploader = new Uploader(); | ||
impressionId = uuidv4(); | ||
uploader.setImpressionId(impressionId); | ||
if (buildConfig(params)) { | ||
// Upload an empty data to fetch config from backend | ||
uploadTrace().then( result => { | ||
if (result.status === 0) { // Success | ||
// clean up the buffer before unloading the window | ||
onbeforeunload = (evt) => { | ||
if (eventsList.length != 0) { | ||
uploadTrace(); | ||
} | ||
} | ||
resolve({status: 0}); | ||
} else { // Fail | ||
console.log(result.msg); | ||
resolve({status: -1, msg: `Fail to initialize config.`}); | ||
} | ||
}); | ||
} else { | ||
resolve({status: -1, msg: `Fail to initialize config.`}); | ||
if (this.config.uploadMode === "periodic") { | ||
this._periodUploadInterval(); | ||
} | ||
}) | ||
} | ||
function runCollector() { | ||
targetEvents.forEach( s => { | ||
window.document.addEventListener(s, (evt) => mouseHandler(evt)); | ||
}); | ||
if (this.config.uploadMode === "mixed") { | ||
this._periodUploadTimeout(); | ||
} | ||
} | ||
if (config.uploadMode === "periodic") { | ||
uploadInterval = setInterval(() => { | ||
if (eventsList.length != 0) { | ||
uploadTrace(); | ||
_stopCollector() { | ||
targetEvents.forEach( s => { | ||
this.config.scope.removeEventListener(s, (evt) => this._mouseHandler(evt)); | ||
}); | ||
clearInterval(this.uploadInterval); | ||
clearTimeout(this.uploadTimeout); | ||
} | ||
_resetCollector() { | ||
this._stopCollector(); | ||
this._runCollector(); | ||
} | ||
_init(config) { | ||
this.impressionId = uuidv4(); | ||
this._clearBuffer(); | ||
this.uploadIdx = 0; | ||
this.uploader = new Uploader(this.impressionId, this.config); | ||
this.uploader.setImpressionId(this.impressionId); | ||
if (this.config.build(config)) { | ||
// Async: Upload an empty data to fetch config from server | ||
this._fetchConfigFromServer().then( result => { | ||
if (result.status == 1) { | ||
if (this.config.update(result.config)) { | ||
this._resetCollector(); | ||
this.uploader.setConfig(this.config); | ||
console.log("Config updated.") | ||
} else { | ||
throw new Error(`Unable to update config with server config.`); | ||
} | ||
} else { | ||
throw new Error(`Fail to get config from server.`); | ||
} | ||
}).catch(err => { | ||
console.log(err); | ||
}); | ||
window.onbeforeunload = (evt) => { | ||
if (this.eventsList.length != 0) { | ||
this._uploadTrace(); | ||
} | ||
} | ||
}, config.uploadPeriod); | ||
return {status: 0}; | ||
} else { | ||
return {status: -1, msg: `Invalid configuration.`}; | ||
} | ||
} | ||
} | ||
function stopCollector() { | ||
targetEvents.forEach( s => { | ||
window.document.removeEventListener(s, (evt) => mouseHandler(evt)); | ||
}); | ||
clearInterval(uploadInterval); | ||
} | ||
function run(params) { | ||
init(params).then( res => { | ||
if (res.status === 0) { | ||
runCollector(); | ||
uploader.start(impressionId); | ||
run(config) { | ||
let res = this._init(config); | ||
if (res.status == 0) { | ||
this._runCollector(); | ||
this.uploader.start(this.impressionId); | ||
console.log("Mouselog agent is activated!"); | ||
@@ -383,20 +400,33 @@ } else { | ||
} | ||
}) | ||
} | ||
stop() { | ||
this.uploader.stop(); | ||
this._stopCollector(); | ||
this._clearBuffer(); | ||
console.log(`Mouselog agent ${this.impressionId} is stopped!`); | ||
} | ||
} | ||
function stop() { | ||
uploader.stop(); | ||
stopCollector(); | ||
clearBuffer(); | ||
console.log("Mouselog agent is stopped!"); | ||
function getRelativeTimestampInSeconds() { | ||
let diff = new Date() - pageLoadTime; | ||
return Math.trunc(diff) / 1000; | ||
} | ||
function getButton(btn) { | ||
if (btn === '2') { | ||
return 'Right'; | ||
} else { | ||
return "" | ||
} | ||
} | ||
module.exports = { Mouselog }; | ||
/***/ }), | ||
/* 3 */ | ||
/* 2 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
var rng = __webpack_require__(4); | ||
var bytesToUuid = __webpack_require__(5); | ||
var rng = __webpack_require__(3); | ||
var bytesToUuid = __webpack_require__(4); | ||
@@ -432,3 +462,3 @@ function v4(options, buf, offset) { | ||
/***/ }), | ||
/* 4 */ | ||
/* 3 */ | ||
/***/ (function(module, exports) { | ||
@@ -473,3 +503,3 @@ | ||
/***/ }), | ||
/* 5 */ | ||
/* 4 */ | ||
/***/ (function(module, exports) { | ||
@@ -506,3 +536,3 @@ | ||
/***/ }), | ||
/* 6 */ | ||
/* 5 */ | ||
/***/ (function(module, exports, __webpack_require__) { | ||
@@ -519,11 +549,12 @@ | ||
class Uploader { | ||
constructor() { | ||
constructor(impressionId, config) { | ||
this.impressionId = impressionId; | ||
this.config = config; | ||
this.resendQueue = []; | ||
} | ||
start(impressionId) { | ||
this.impressionId = impressionId; | ||
start() { | ||
this.resendInterval = setInterval(()=>{ | ||
this._resendFailedData.call(this); | ||
}, config.resendInterval); | ||
}, this.config.resendInterval); | ||
} | ||
@@ -537,3 +568,3 @@ | ||
upload(data) { | ||
// resolve(true/false): uploaded success/fail. | ||
// resolve({status:-1/0/1, ...}): uploading success/fail. | ||
// reject(ErrorMessage): Errors occur when updating the config. | ||
@@ -549,5 +580,7 @@ return new Promise( (resolve, reject) => { | ||
if (resObj.msg == "config") { | ||
if (!updateConfig(resObj.data)) { | ||
resolve({status: -1, msg: `Data is uploaded, but errors occur when updating config.`}); | ||
}; | ||
resolve({ | ||
status: 1, | ||
msg: `Get config from server`, | ||
config: resObj.data | ||
}); | ||
} | ||
@@ -561,3 +594,6 @@ resolve({status: 0}); | ||
this._appendFailedData(data); | ||
resolve({status: -1, msg: `Fail to upload a bunch of data: ${err.message}`}); | ||
resolve({ | ||
status: -1, | ||
msg: `Fail to upload a bunch of data, ${err.message}` | ||
}); | ||
}) | ||
@@ -571,4 +607,11 @@ }); | ||
setConfig(config) { | ||
this.stop(); | ||
this.config = config; | ||
this.start(); | ||
} | ||
_resendFailedData() { | ||
let i = 0; | ||
let obj = this.resendQueue[i]; | ||
while (i < this.resendQueue.length) { | ||
@@ -594,4 +637,4 @@ if (obj.status == StatusEnum.SUCCESS) { | ||
_upload(encodedData) { | ||
if (config.enableGet) { | ||
return fetch(`${config.absoluteUrl}/api/upload-trace?websiteId=${config.websiteId}&impressionId=${this.impressionId}&data=${encodedData}`, { | ||
if (this.config.enableGet) { | ||
return fetch(`${this.config.absoluteUrl}/api/upload-trace?websiteId=${this.config.websiteId}&impressionId=${this.impressionId}&data=${encodedData}`, { | ||
method: "GET", | ||
@@ -601,3 +644,3 @@ credentials: "include" | ||
} else { | ||
return fetch(`${config.absoluteUrl}/api/upload-trace?websiteId=${config.websiteId}&impressionId=${this.impressionId}`, { | ||
return fetch(`${this.config.absoluteUrl}/api/upload-trace?websiteId=${this.config.websiteId}&impressionId=${this.impressionId}`, { | ||
method: "POST", | ||
@@ -604,0 +647,0 @@ credentials: "include", |
@@ -1,1 +0,1 @@ | ||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.mouselog=t():e.mouselog=t()}(window,(function(){return function(e){var t={};function o(n){if(t[n])return t[n].exports;var r=t[n]={i:n,l:!1,exports:{}};return e[n].call(r.exports,r,r.exports,o),r.l=!0,r.exports}return o.m=e,o.c=t,o.d=function(e,t,n){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(e,t){if(1&t&&(e=o(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(o.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)o.d(n,r,function(t){return e[t]}.bind(null,r));return n},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="",o(o.s=1)}([function(e,t){let o={uploadEndpoint:"http://localhost:9000",websiteId:"unknown",endpointType:"absolute",uploadProtocol:"https",uploadMode:"periodic",uploadPeriod:5e3,frequency:50,enableGet:!1,resendInterval:3e3},n=["uploadEndpoint"],r=e=>{try{n.forEach(t=>{if(!e.hasOwnProperty(t))throw new Error(`Param ${t} is required but not declared.`)}),o=Object.assign(o,e),o.absoluteUrl=s()}catch(e){return console.log(e),!1}return!0},s=()=>{let e=o.uploadEndpoint;if("relative"==o.endpointType)e.startsWith("./")?e=e.slice(1):"/"!==e[0]&&(e=`/${e}`),"/"===e[e.length-1]&&(e=e.slice(0,e.length-1)),e=`${o.uploadProtocol}://${window.location.hostname}${e}`;else{if("absolute"!=o.endpointType)throw new Error('`endpointType` can only be "absolute" or "relative"');e.startsWith("http://")||e.startsWith("https://")||(e=`${o.uploadProtocol}://${e}`)}return e};e.exports={config:o,buildConfig:r,updateConfig:e=>r(e)}},function(e,t,o){let n=o(2);e.exports={run:function(e){n.run(e)},stop:function(){n.stop()}}},function(e,t,o){"use strict";o.r(t),o.d(t,"run",(function(){return y})),o.d(t,"stop",(function(){return w}));const n=o(3),r=o(6);let s,i,a,u,l,d,{config:c,buildConfig:p}=o(0),f=["mousemove","mousedown","mouseup","mouseclick","dblclick","contextmenu","wheel","torchstart","touchmove","touchend"];function h(){let e=new Date-u;return Math.trunc(e)/1e3}function g(){let e=function(){let e={id:"0",idx:l,url:window.location.hostname?window.location.hostname:"localhost",path:window.location.pathname,width:document.body.scrollWidth,height:document.body.scrollHeight,pageLoadTime:u,label:-1,guess:-1,events:[]};return l+=1,e}();return e.events=a,a=[],s.upload(e)}function m(e){if("contextmenu"===e.type&&0===e.pageX&&0===e.pageY)return;let t=e.pageX,o=e.pageY;void 0===t&&(t=e.changedTouches[0].pageX,o=e.changedTouches[0].pageY);let n={id:a.length,timestamp:h(),type:e.type,x:t,y:o,button:(r=e.button,"2"===r?"Right":"")};var r;"wheel"==e.type&&(n.deltaX=e.deltaX,n.deltaY=e.deltaY),a.push(n),"event-triggered"==c.uploadMode&&a.length%c.frequency==0&&g()}function b(){a=[]}function y(e){(function(e){return new Promise(t=>{b(),u=new Date,l=0,s=new r,i=n(),s.setImpressionId(i),p(e)?g().then(e=>{0===e.status?(onbeforeunload=e=>{0!=a.length&&g()},t({status:0})):(console.log(e.msg),t({status:-1,msg:"Fail to initialize config."}))}):t({status:-1,msg:"Fail to initialize config."})})})(e).then(e=>{0===e.status?(f.forEach(e=>{window.document.addEventListener(e,e=>m(e))}),"periodic"===c.uploadMode&&(d=setInterval(()=>{0!=a.length&&g()},c.uploadPeriod)),s.start(i),console.log("Mouselog agent is activated!")):(console.log(e.msg),console.log("Fail to initialize Mouselog agent."))})}function w(){s.stop(),f.forEach(e=>{window.document.removeEventListener(e,e=>m(e))}),clearInterval(d),b(),console.log("Mouselog agent is stopped!")}},function(e,t,o){var n=o(4),r=o(5);e.exports=function(e,t,o){var s=t&&o||0;"string"==typeof e&&(t="binary"===e?new Array(16):null,e=null);var i=(e=e||{}).random||(e.rng||n)();if(i[6]=15&i[6]|64,i[8]=63&i[8]|128,t)for(var a=0;a<16;++a)t[s+a]=i[a];return t||r(i)}},function(e,t){var o="undefined"!=typeof crypto&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto)||"undefined"!=typeof msCrypto&&"function"==typeof window.msCrypto.getRandomValues&&msCrypto.getRandomValues.bind(msCrypto);if(o){var n=new Uint8Array(16);e.exports=function(){return o(n),n}}else{var r=new Array(16);e.exports=function(){for(var e,t=0;t<16;t++)0==(3&t)&&(e=4294967296*Math.random()),r[t]=e>>>((3&t)<<3)&255;return r}}},function(e,t){for(var o=[],n=0;n<256;++n)o[n]=(n+256).toString(16).substr(1);e.exports=function(e,t){var n=t||0,r=o;return[r[e[n++]],r[e[n++]],r[e[n++]],r[e[n++]],"-",r[e[n++]],r[e[n++]],"-",r[e[n++]],r[e[n++]],"-",r[e[n++]],r[e[n++]],"-",r[e[n++]],r[e[n++]],r[e[n++]],r[e[n++]],r[e[n++]],r[e[n++]]].join("")}},function(e,t,o){let{config:n,updateConfig:r}=o(0),s=0,i=1,a=2;e.exports=class{constructor(){this.resendQueue=[]}start(e){this.impressionId=e,this.resendInterval=setInterval(()=>{this._resendFailedData.call(this)},n.resendInterval)}stop(){clearInterval(this.resendInterval)}upload(e){return new Promise((t,o)=>{let n=JSON.stringify(e);this._upload(n).then(e=>{if(200!=e.status)throw new Error("Response status code is not 200.");e.json().then(e=>{if("ok"!==e.status)throw new Error("Response object status is not ok.");"config"==e.msg&&(r(e.data)||t({status:-1,msg:"Data is uploaded, but errors occur when updating config."})),t({status:0})})}).catch(o=>{this._appendFailedData(e),t({status:-1,msg:`Fail to upload a bunch of data: ${o.message}`})})})}setImpressionId(e){this.impressionId=e}_resendFailedData(){let e=0;for(;e<this.resendQueue.length;)obj.status==a?this.resendQueue.splice(e,1):(e+=1,obj.status==s&&(obj.status=i,this.upload(obj.data).then(e=>{obj.status=e?a:s})))}_upload(e){return n.enableGet?fetch(`${n.absoluteUrl}/api/upload-trace?websiteId=${n.websiteId}&impressionId=${this.impressionId}&data=${e}`,{method:"GET",credentials:"include"}):fetch(`${n.absoluteUrl}/api/upload-trace?websiteId=${n.websiteId}&impressionId=${this.impressionId}`,{method:"POST",credentials:"include",body:e})}_appendFailedData(e){this.resendQueue.push({status:s,data:e})}}}])})); | ||
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.mouselog=e():t.mouselog=e()}(window,(function(){return function(t){var e={};function o(i){if(e[i])return e[i].exports;var s=e[i]={i:i,l:!1,exports:{}};return t[i].call(s.exports,s,s.exports,o),s.l=!0,s.exports}return o.m=t,o.c=e,o.d=function(t,e,i){o.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:i})},o.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},o.t=function(t,e){if(1&e&&(t=o(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var i=Object.create(null);if(o.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var s in t)o.d(i,s,function(e){return t[e]}.bind(null,s));return i},o.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return o.d(e,"a",e),e},o.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},o.p="",o(o.s=1)}([function(t,e){t.exports={Config:class{constructor(){this.uploadEndpoint="http://localhost:9000",this.websiteId="unknown",this.endpointType="absolute",this.uploadMode="mixed",this.uploadPeriod=5e3,this.frequency=50,this.enableGet=!1,this.resendInterval=3e3,this.scope=window.document,this._requiredParams=["uploadEndpoint"],this._ignoredParams=["scope"]}build(t,e=!1){try{this._requiredParams.forEach(e=>{if(!t.hasOwnProperty(e))throw new Error(`Param ${e} is required but not declared.`)}),Object.keys(t).forEach(o=>{this[o]&&!o.startsWith("_")&&"function"!=typeof this[o]&&(e&&o in this._ignoredParams||(this[o]=t[o]))}),this.absoluteUrl=this._formatUrl()}catch(t){return console.log(t),!1}return!0}update(t){return this.build(t,!0)}_formatUrl(){let t=this.uploadEndpoint;if("relative"==this.endpointType)t.startsWith("./")?t=t.slice(1):"/"!==t[0]&&(t="/"+t),"/"===t[t.length-1]&&(t=t.slice(0,t.length-1)),t=`${window.location.origin}${t}`;else if("absolute"!==this.endpointType)throw new Error('`endpointType` can only be "absolute" or "relative"');return t}}}},function(t,e,o){const i=o(2),s=o(5);let{Config:n}=o(0),r=["mousemove","mousedown","mouseup","mouseclick","dblclick","contextmenu","wheel","torchstart","touchmove","touchend"],a=new Date;function l(){let t=new Date-a;return Math.trunc(t)/1e3}function u(t){return"2"===t?"Right":""}t.exports={Mouselog:class{constructor(){this.config=new n,this.impressionId=i(),this.mouselogLoadTime=new Date,this.uploader=new s,this.eventsList=[],this.eventsCount=0,this.uploadInterval,this.uploadTimeout}_clearBuffer(){this.eventsList=[]}_newTrace(){let t={id:"0",idx:this.uploadIdx,url:window.location.hostname?window.location.hostname:"localhost",path:window.location.pathname,width:document.body.scrollWidth,height:document.body.scrollHeight,pageLoadTime:a,events:[]};return this.uploadIdx+=1,t}_mouseHandler(t){if("contextmenu"===t.type&&0===t.pageX&&0===t.pageY)return;let e=t.pageX,o=t.pageY;void 0===e&&(e=t.changedTouches[0].pageX,o=t.changedTouches[0].pageY);let i={id:this.eventsCount,timestamp:l(),type:t.type,x:e,y:o,button:u(t.button)};"wheel"==t.type&&(i.deltaX=t.deltaX,i.deltaY=t.deltaY),this.eventsList.push(i),this.eventsCount+=1,"event-triggered"==this.config.uploadMode&&this.eventsList.length%this.config.frequency==0&&this._uploadTrace(),"mixed"==this.config.uploadMode&&this.eventsList.length%this.config.frequency==0&&(this._periodUploadTimeout(),this._uploadTrace())}_fetchConfigFromServer(){let t=this._newTrace();return this.uploader.upload(t)}_uploadTrace(){let t=this._newTrace();return t.events=this.eventsList,this.eventsList=[],this.uploader.upload(t)}_periodUploadTimeout(){clearTimeout(this.uploadTimeout),this.uploadTimeout=setTimeout(()=>{this.eventsList.length>0&&this._uploadTrace()})}_periodUploadInterval(){clearInterval(this.uploadInterval),this.uploadInterval=setInterval(()=>{this.eventsList.length>0&&this._uploadTrace()},this.config.uploadPeriod)}_runCollector(){r.forEach(t=>{this.config.scope.addEventListener(t,t=>this._mouseHandler(t))}),"periodic"===this.config.uploadMode&&this._periodUploadInterval(),"mixed"===this.config.uploadMode&&this._periodUploadTimeout()}_stopCollector(){r.forEach(t=>{this.config.scope.removeEventListener(t,t=>this._mouseHandler(t))}),clearInterval(this.uploadInterval),clearTimeout(this.uploadTimeout)}_resetCollector(){this._stopCollector(),this._runCollector()}_init(t){return this.impressionId=i(),this._clearBuffer(),this.uploadIdx=0,this.uploader=new s(this.impressionId,this.config),this.uploader.setImpressionId(this.impressionId),this.config.build(t)?(this._fetchConfigFromServer().then(t=>{if(1!=t.status)throw new Error("Fail to get config from server.");if(!this.config.update(t.config))throw new Error("Unable to update config with server config.");this._resetCollector(),this.uploader.setConfig(this.config),console.log("Config updated.")}).catch(t=>{console.log(t)}),window.onbeforeunload=t=>{0!=this.eventsList.length&&this._uploadTrace()},{status:0}):{status:-1,msg:"Invalid configuration."}}run(t){let e=this._init(t);0==e.status?(this._runCollector(),this.uploader.start(this.impressionId),console.log("Mouselog agent is activated!")):(console.log(e.msg),console.log("Fail to initialize Mouselog agent."))}stop(){this.uploader.stop(),this._stopCollector(),this._clearBuffer(),console.log(`Mouselog agent ${this.impressionId} is stopped!`)}}}},function(t,e,o){var i=o(3),s=o(4);t.exports=function(t,e,o){var n=e&&o||0;"string"==typeof t&&(e="binary"===t?new Array(16):null,t=null);var r=(t=t||{}).random||(t.rng||i)();if(r[6]=15&r[6]|64,r[8]=63&r[8]|128,e)for(var a=0;a<16;++a)e[n+a]=r[a];return e||s(r)}},function(t,e){var o="undefined"!=typeof crypto&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto)||"undefined"!=typeof msCrypto&&"function"==typeof window.msCrypto.getRandomValues&&msCrypto.getRandomValues.bind(msCrypto);if(o){var i=new Uint8Array(16);t.exports=function(){return o(i),i}}else{var s=new Array(16);t.exports=function(){for(var t,e=0;e<16;e++)0==(3&e)&&(t=4294967296*Math.random()),s[e]=t>>>((3&e)<<3)&255;return s}}},function(t,e){for(var o=[],i=0;i<256;++i)o[i]=(i+256).toString(16).substr(1);t.exports=function(t,e){var i=e||0,s=o;return[s[t[i++]],s[t[i++]],s[t[i++]],s[t[i++]],"-",s[t[i++]],s[t[i++]],"-",s[t[i++]],s[t[i++]],"-",s[t[i++]],s[t[i++]],"-",s[t[i++]],s[t[i++]],s[t[i++]],s[t[i++]],s[t[i++]],s[t[i++]]].join("")}},function(t,e,o){let{config:i,updateConfig:s}=o(0),n=0,r=1,a=2;t.exports=class{constructor(t,e){this.impressionId=t,this.config=e,this.resendQueue=[]}start(){this.resendInterval=setInterval(()=>{this._resendFailedData.call(this)},this.config.resendInterval)}stop(){clearInterval(this.resendInterval)}upload(t){return new Promise((e,o)=>{let i=JSON.stringify(t);this._upload(i).then(t=>{if(200!=t.status)throw new Error("Response status code is not 200.");t.json().then(t=>{if("ok"!==t.status)throw new Error("Response object status is not ok.");"config"==t.msg&&e({status:1,msg:"Get config from server",config:t.data}),e({status:0})})}).catch(o=>{this._appendFailedData(t),e({status:-1,msg:`Fail to upload a bunch of data, ${o.message}`})})})}setImpressionId(t){this.impressionId=t}setConfig(t){this.stop(),this.config=t,this.start()}_resendFailedData(){let t=0,e=this.resendQueue[t];for(;t<this.resendQueue.length;)e.status==a?this.resendQueue.splice(t,1):(t+=1,e.status==n&&(e.status=r,this.upload(e.data).then(t=>{e.status=t?a:n})))}_upload(t){return this.config.enableGet?fetch(`${this.config.absoluteUrl}/api/upload-trace?websiteId=${this.config.websiteId}&impressionId=${this.impressionId}&data=${t}`,{method:"GET",credentials:"include"}):fetch(`${this.config.absoluteUrl}/api/upload-trace?websiteId=${this.config.websiteId}&impressionId=${this.impressionId}`,{method:"POST",credentials:"include",body:t})}_appendFailedData(t){this.resendQueue.push({status:n,data:t})}}}])})); |
160
config.js
@@ -0,95 +1,101 @@ | ||
class Config { | ||
// Set up a default config | ||
constructor() { | ||
// Type: string, REQUIRED | ||
// Endpoint Url | ||
this.uploadEndpoint = "http://localhost:9000"; | ||
// Type: string | ||
// Website ID | ||
this.websiteId = "unknown"; | ||
// Default config | ||
let config = { | ||
// Type: string, REQUIRED | ||
// Endpoint Url | ||
uploadEndpoint: "http://localhost:9000", | ||
// Endpoint type, "absolute" or "relative" | ||
this.endpointType = "absolute"; | ||
// Type: string | ||
// Website ID | ||
websiteId: "unknown", | ||
// Upload mode, "mixed", "periodic" or "event-triggered" | ||
this.uploadMode = "mixed"; | ||
// Endpoint type, "absolute" or "relative" | ||
endpointType: "absolute", | ||
// Type: number | ||
// If `uploadMode` is "mixed", "periodic", data will be uploaded every `uploadPeriod` ms. | ||
// If no data are collected in a period, no data will be uploaded | ||
this.uploadPeriod = 5000; | ||
// upload protocol, "https" or "http" | ||
// If you declare it in `uploadEndpoint`, this property will be ignored. | ||
uploadProtocol: "https", | ||
// Type: number | ||
// If `uploadMode` == "event-triggered" | ||
// The website interaction data will be uploaded when every `frequency` events are captured. | ||
this.frequency = 50; | ||
// Upload mode, "periodic" or "event-triggered" | ||
uploadMode: "periodic", | ||
// Type: bool | ||
// Use GET method to upload data? (stringified data will be embedded in URI) | ||
this.enableGet = false; | ||
// Type: number | ||
// If `uploadMode` == "periodic", data will be uploaded every `uploadPeriod` ms. | ||
// If no data are collected in a period, no data will be uploaded | ||
uploadPeriod: 5000, | ||
// Type: number | ||
// Time interval for resending the failed trace data | ||
this.resendInterval = 3000; | ||
// Type: number | ||
// If `uploadMode` == "event-triggered" | ||
// The website interaction data will be uploaded when every `frequency` events are captured. | ||
frequency: 50, | ||
// Type: HTML DOM Element | ||
// Capture the events occur in `this.scope` | ||
this.scope = window.document; | ||
// Type: bool | ||
// Use GET method to upload data? (stringified data will be embedded in URI) | ||
enableGet: false, | ||
// These parameters are required for runing a Mouselog agent | ||
this._requiredParams = [ | ||
"uploadEndpoint", | ||
] | ||
// Type: number | ||
// Time interval for resending the failed trace data | ||
resendInterval: 3000, | ||
} | ||
// These parameters will be ignored when updating config | ||
this._ignoredParams = [ | ||
"scope", | ||
] | ||
} | ||
// ---------------------------- | ||
build(config, isUpdating = false) { | ||
try { | ||
this._requiredParams.forEach(key => { | ||
if (!config.hasOwnProperty(key)) { | ||
throw new Error(`Param ${key} is required but not declared.`); | ||
} | ||
}); | ||
// Overwrite the default config | ||
Object.keys(config).forEach( key => { | ||
// Overwriting Class private members / function method is not allowed | ||
if (this[key] && !key.startsWith("_") && typeof(this[key]) != "function") { | ||
// Do not update some `ignored` parameter | ||
if (!(isUpdating && key in this._ignoredParams)) { | ||
this[key] = config[key] | ||
} | ||
} | ||
}) | ||
this.absoluteUrl = this._formatUrl(); | ||
} catch(err) { | ||
console.log(err); | ||
return false; | ||
} | ||
return true; | ||
} | ||
let requiredParams = [ | ||
"uploadEndpoint", | ||
]; | ||
update(config) { | ||
return this.build(config, true); | ||
} | ||
// Returns a boolean indicating if config is built successfully | ||
let buildConfig = (params) => { | ||
try { | ||
requiredParams.forEach(key => { | ||
if (!(params.hasOwnProperty(key))) { | ||
throw new Error(`Param ${key} is required but not declared.`); | ||
_formatUrl() { | ||
let url = this.uploadEndpoint; | ||
if (this.endpointType == "relative") { | ||
if (url.startsWith("./")) { | ||
url = url.slice(1); | ||
} else if (url[0] !== "/") { | ||
url = "/" + url; | ||
} | ||
}); | ||
config = Object.assign(config, params); | ||
config.absoluteUrl = formatUrl(); | ||
} catch(err) { | ||
console.log(err); | ||
return false; | ||
} | ||
return true; | ||
} | ||
let updateConfig = (params) => { | ||
// Generate new config | ||
return buildConfig(params); | ||
} | ||
let formatUrl = () => { | ||
let url = config.uploadEndpoint | ||
if (config.endpointType == "relative") { | ||
// Format the head -> "/*" | ||
if (url.startsWith("./")) { | ||
url = url.slice(1); | ||
} else if (url[0] !== "/") { | ||
url = `/${url}`; | ||
// Format the tail | ||
if (url[url.length-1] === "/") { | ||
url = url.slice(0, url.length-1); | ||
} | ||
// Construct absolute URL | ||
url = `${window.location.origin}${url}` | ||
} else if (this.endpointType !== "absolute") { | ||
throw new Error('`endpointType` can only be "absolute" or "relative"'); | ||
} | ||
// Format the tail | ||
if (url[url.length-1] === "/") { | ||
url = url.slice(0, url.length-1); | ||
} | ||
// Construct absolute URL | ||
url = `${config.uploadProtocol}://${window.location.hostname}${url}`; | ||
} else if (config.endpointType == "absolute") { | ||
if (!(url.startsWith("http://") || url.startsWith("https://"))) { | ||
url = `${config.uploadProtocol}://${url}`; | ||
} | ||
} else { | ||
throw new Error('`endpointType` can only be "absolute" or "relative"'); | ||
return url; | ||
} | ||
return url; | ||
} | ||
module.exports = { config, buildConfig, updateConfig } | ||
module.exports = { Config }; |
293
index.js
const uuidv4 = require('uuid/v4'); | ||
const Uploader = require('./uploader'); | ||
let { config, buildConfig } = require('./config'); | ||
let { Config } = require('./config'); | ||
let targetEvents = [ | ||
@@ -18,139 +17,171 @@ "mousemove", | ||
]; | ||
let pageLoadTime = new Date(); | ||
let uploader; | ||
let impressionId; | ||
let eventsList; | ||
let pageLoadTime; | ||
let uploadIdx; | ||
let uploadInterval; | ||
function getRelativeTimestampInSeconds() { | ||
let diff = new Date() - pageLoadTime; | ||
return Math.trunc(diff) / 1000; | ||
} | ||
class Mouselog{ | ||
constructor() { | ||
this.config = new Config(); | ||
this.impressionId = uuidv4(); | ||
this.mouselogLoadTime = new Date(); | ||
this.uploader = new Uploader(); | ||
this.eventsList = []; | ||
this.eventsCount = 0; | ||
this.uploadInterval; // For "periodic" upload mode | ||
this.uploadTimeout; // For "mixed" upload mode | ||
} | ||
function getButton(btn) { | ||
if (btn === '2') { | ||
return 'Right'; | ||
} else { | ||
return "" | ||
_clearBuffer() { | ||
this.eventsList = []; | ||
} | ||
} | ||
function newTrace() { | ||
let trace = { | ||
id: '0', | ||
idx: uploadIdx, | ||
url: window.location.hostname ? window.location.hostname : "localhost", | ||
path: window.location.pathname, | ||
width: document.body.scrollWidth, | ||
height: document.body.scrollHeight, | ||
pageLoadTime: pageLoadTime, | ||
label: -1, | ||
guess: -1, | ||
events: [] | ||
} | ||
uploadIdx += 1; | ||
return trace; | ||
} | ||
_newTrace() { | ||
let trace = { | ||
id: '0', | ||
idx: this.uploadIdx, | ||
url: window.location.hostname ? window.location.hostname : "localhost", | ||
path: window.location.pathname, | ||
width: document.body.scrollWidth, | ||
height: document.body.scrollHeight, | ||
pageLoadTime: pageLoadTime, | ||
events: [] | ||
} | ||
this.uploadIdx += 1; | ||
return trace; | ||
} | ||
function uploadTrace() { | ||
let trace = newTrace(); | ||
trace.events = eventsList; | ||
eventsList = []; | ||
return uploader.upload(trace); // This is a promise | ||
} | ||
_mouseHandler(evt) { | ||
// PC's Chrome on Mobile mode can still receive "contextmenu" event with zero X, Y, so we ignore these events. | ||
if (evt.type === 'contextmenu' && evt.pageX === 0 && evt.pageY === 0) { | ||
return; | ||
} | ||
let x = evt.pageX; | ||
let y = evt.pageY; | ||
if (x === undefined) { | ||
x = evt.changedTouches[0].pageX; | ||
y = evt.changedTouches[0].pageY; | ||
} | ||
let tmpEvt = { | ||
id: this.eventsCount, | ||
timestamp: getRelativeTimestampInSeconds(), | ||
type: evt.type, | ||
x: x, | ||
y: y, | ||
button: getButton(evt.button) | ||
} | ||
function mouseHandler(evt) { | ||
// PC's Chrome on Mobile mode can still receive "contextmenu" event with zero X, Y, so we ignore these events. | ||
if (evt.type === 'contextmenu' && evt.pageX === 0 && evt.pageY === 0) { | ||
return; | ||
if (evt.type == "wheel") { | ||
tmpEvt.deltaX = evt.deltaX; | ||
tmpEvt.deltaY = evt.deltaY; | ||
} | ||
this.eventsList.push(tmpEvt); | ||
this.eventsCount += 1; | ||
if ( this.config.uploadMode == "event-triggered" && this.eventsList.length % this.config.frequency == 0 ) { | ||
this._uploadTrace(); | ||
} | ||
if ( this.config.uploadMode == "mixed" && this.eventsList.length % this.config.frequency == 0) { | ||
this._periodUploadTimeout(); | ||
this._uploadTrace(); | ||
} | ||
} | ||
let x = evt.pageX; | ||
let y = evt.pageY; | ||
if (x === undefined) { | ||
x = evt.changedTouches[0].pageX; | ||
y = evt.changedTouches[0].pageY; | ||
_fetchConfigFromServer() { | ||
// Upload an empty trace to fetch config from server | ||
let trace = this._newTrace(); | ||
return this.uploader.upload(trace); // This is a promise | ||
} | ||
let tmpEvt = { | ||
id: eventsList.length, | ||
timestamp: getRelativeTimestampInSeconds(), | ||
type: evt.type, | ||
x: x, | ||
y: y, | ||
button: getButton(evt.button) | ||
_uploadTrace() { | ||
let trace = this._newTrace(); | ||
trace.events = this.eventsList; | ||
this.eventsList = []; | ||
return this.uploader.upload(trace); // This is a promise | ||
} | ||
if (evt.type == "wheel") { | ||
tmpEvt.deltaX = evt.deltaX; | ||
tmpEvt.deltaY = evt.deltaY; | ||
_periodUploadTimeout() { | ||
clearTimeout(this.uploadTimeout); | ||
this.uploadTimeout = setTimeout(() => { | ||
if (this.eventsList.length > 0) { | ||
this._uploadTrace(); | ||
} | ||
}) | ||
} | ||
eventsList.push(tmpEvt); | ||
if ( config.uploadMode == "event-triggered" && eventsList.length % config.frequency == 0 ) { | ||
uploadTrace(); | ||
_periodUploadInterval() { | ||
clearInterval(this.uploadInterval); | ||
this.uploadInterval = setInterval(() => { | ||
if (this.eventsList.length > 0) { | ||
this._uploadTrace(); | ||
} | ||
}, this.config.uploadPeriod); | ||
} | ||
} | ||
function clearBuffer() { | ||
eventsList = []; | ||
} | ||
_runCollector() { | ||
targetEvents.forEach( s => { | ||
this.config.scope.addEventListener(s, (evt) => this._mouseHandler(evt)); | ||
}); | ||
// Initialize the mouselog | ||
function init(params) { | ||
return new Promise((resolve) => { | ||
clearBuffer(); | ||
pageLoadTime = new Date(); | ||
uploadIdx = 0; | ||
uploader = new Uploader(); | ||
impressionId = uuidv4(); | ||
uploader.setImpressionId(impressionId); | ||
if (buildConfig(params)) { | ||
// Upload an empty data to fetch config from backend | ||
uploadTrace().then( result => { | ||
if (result.status === 0) { // Success | ||
// clean up the buffer before unloading the window | ||
onbeforeunload = (evt) => { | ||
if (eventsList.length != 0) { | ||
uploadTrace(); | ||
} | ||
} | ||
resolve({status: 0}); | ||
} else { // Fail | ||
console.log(result.msg); | ||
resolve({status: -1, msg: `Fail to initialize config.`}); | ||
} | ||
}); | ||
} else { | ||
resolve({status: -1, msg: `Fail to initialize config.`}); | ||
if (this.config.uploadMode === "periodic") { | ||
this._periodUploadInterval(); | ||
} | ||
}) | ||
} | ||
function runCollector() { | ||
targetEvents.forEach( s => { | ||
window.document.addEventListener(s, (evt) => mouseHandler(evt)); | ||
}); | ||
if (this.config.uploadMode === "mixed") { | ||
this._periodUploadTimeout(); | ||
} | ||
} | ||
if (config.uploadMode === "periodic") { | ||
uploadInterval = setInterval(() => { | ||
if (eventsList.length != 0) { | ||
uploadTrace(); | ||
_stopCollector() { | ||
targetEvents.forEach( s => { | ||
this.config.scope.removeEventListener(s, (evt) => this._mouseHandler(evt)); | ||
}); | ||
clearInterval(this.uploadInterval); | ||
clearTimeout(this.uploadTimeout); | ||
} | ||
_resetCollector() { | ||
this._stopCollector(); | ||
this._runCollector(); | ||
} | ||
_init(config) { | ||
this.impressionId = uuidv4(); | ||
this._clearBuffer(); | ||
this.uploadIdx = 0; | ||
this.uploader = new Uploader(this.impressionId, this.config); | ||
this.uploader.setImpressionId(this.impressionId); | ||
if (this.config.build(config)) { | ||
// Async: Upload an empty data to fetch config from server | ||
this._fetchConfigFromServer().then( result => { | ||
if (result.status == 1) { | ||
if (this.config.update(result.config)) { | ||
this._resetCollector(); | ||
this.uploader.setConfig(this.config); | ||
console.log("Config updated.") | ||
} else { | ||
throw new Error(`Unable to update config with server config.`); | ||
} | ||
} else { | ||
throw new Error(`Fail to get config from server.`); | ||
} | ||
}).catch(err => { | ||
console.log(err); | ||
}); | ||
window.onbeforeunload = (evt) => { | ||
if (this.eventsList.length != 0) { | ||
this._uploadTrace(); | ||
} | ||
} | ||
}, config.uploadPeriod); | ||
return {status: 0}; | ||
} else { | ||
return {status: -1, msg: `Invalid configuration.`}; | ||
} | ||
} | ||
} | ||
function stopCollector() { | ||
targetEvents.forEach( s => { | ||
window.document.removeEventListener(s, (evt) => mouseHandler(evt)); | ||
}); | ||
clearInterval(uploadInterval); | ||
} | ||
export function run(params) { | ||
init(params).then( res => { | ||
if (res.status === 0) { | ||
runCollector(); | ||
uploader.start(impressionId); | ||
run(config) { | ||
let res = this._init(config); | ||
if (res.status == 0) { | ||
this._runCollector(); | ||
this.uploader.start(this.impressionId); | ||
console.log("Mouselog agent is activated!"); | ||
@@ -161,11 +192,25 @@ } else { | ||
} | ||
}) | ||
} | ||
stop() { | ||
this.uploader.stop(); | ||
this._stopCollector(); | ||
this._clearBuffer(); | ||
console.log(`Mouselog agent ${this.impressionId} is stopped!`); | ||
} | ||
} | ||
export function stop() { | ||
uploader.stop(); | ||
stopCollector(); | ||
clearBuffer(); | ||
console.log("Mouselog agent is stopped!"); | ||
function getRelativeTimestampInSeconds() { | ||
let diff = new Date() - pageLoadTime; | ||
return Math.trunc(diff) / 1000; | ||
} | ||
function getButton(btn) { | ||
if (btn === '2') { | ||
return 'Right'; | ||
} else { | ||
return "" | ||
} | ||
} | ||
module.exports = { Mouselog }; |
{ | ||
"name": "mouselog", | ||
"version": "0.0.9", | ||
"version": "0.1.0", | ||
"description": "The mouse tracking agent for Mouselog.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -15,3 +15,4 @@ [](https://www.npmjs.com/package/mouselog) | ||
<script> | ||
mouselog.run({ | ||
var agent = new mouselog.Mouselog(); | ||
agent.run({ | ||
uploadEndpoint: "Your_Server_Url", | ||
@@ -29,3 +30,4 @@ websiteId: "Your_Website_Id", | ||
script.onload = () => { | ||
mouselog.run({ | ||
var agent = new mouselog.Mouselog(); | ||
agent.run({ | ||
uploadEndpoint: "Your_Server_Url", | ||
@@ -57,4 +59,5 @@ websiteId: "Your_Website_Id", | ||
```Javascript | ||
const mouselog = require('mouselog'); | ||
mouselog.run({ | ||
const { Mouselog } = require('mouselog'); | ||
let agent = new Mouselog(); | ||
agent.run({ | ||
uploadEndpoint: "Your_Server_Url", | ||
@@ -65,3 +68,3 @@ websiteId: "Your_Website_Id", | ||
``` | ||
You can also deactivate Mouselog by calling `mouselog.stop()`. | ||
You can also deactivate Mouselog by calling `agent.stop()`. | ||
@@ -85,9 +88,8 @@ | ||
// upload protocol, "https" or "http" | ||
// If you declare it in `uploadEndpoint`, this property will be ignored. | ||
uploadProtocol: "https", | ||
// Upload mode, "mixed", "periodic" or "event-triggered" | ||
// "periodic": upload data in every period. | ||
// "event-triggered": upload data when a number of interaction data is captured | ||
// "mixed": the mixture of the previous two upload mode | ||
uploadMode: "mixed", | ||
// Upload mode, "periodic" or "event-triggered" | ||
uploadMode: "periodic", | ||
// Type: number | ||
@@ -105,7 +107,7 @@ // If `uploadMode` == "periodic", data will be uploaded every `uploadPeriod` ms. | ||
// Use GET method to upload data? (stringified data will be embedded in URI) | ||
enableGet: false, | ||
// Type: number | ||
// Time interval for resending the failed trace data | ||
resendInterval: 3000, | ||
enableGet: false, | ||
// Type: HTML DOM Element | ||
// Agent only listens and captures events in `config.scope` | ||
scope: window.document | ||
} | ||
@@ -112,0 +114,0 @@ ``` |
@@ -10,11 +10,12 @@ let {config, updateConfig} = require('./config'); | ||
class Uploader { | ||
constructor() { | ||
constructor(impressionId, config) { | ||
this.impressionId = impressionId; | ||
this.config = config; | ||
this.resendQueue = []; | ||
} | ||
start(impressionId) { | ||
this.impressionId = impressionId; | ||
start() { | ||
this.resendInterval = setInterval(()=>{ | ||
this._resendFailedData.call(this); | ||
}, config.resendInterval); | ||
}, this.config.resendInterval); | ||
} | ||
@@ -28,3 +29,3 @@ | ||
upload(data) { | ||
// resolve(true/false): uploaded success/fail. | ||
// resolve({status:-1/0/1, ...}): uploading success/fail. | ||
// reject(ErrorMessage): Errors occur when updating the config. | ||
@@ -40,5 +41,7 @@ return new Promise( (resolve, reject) => { | ||
if (resObj.msg == "config") { | ||
if (!updateConfig(resObj.data)) { | ||
resolve({status: -1, msg: `Data is uploaded, but errors occur when updating config.`}); | ||
}; | ||
resolve({ | ||
status: 1, | ||
msg: `Get config from server`, | ||
config: resObj.data | ||
}); | ||
} | ||
@@ -52,3 +55,6 @@ resolve({status: 0}); | ||
this._appendFailedData(data); | ||
resolve({status: -1, msg: `Fail to upload a bunch of data: ${err.message}`}); | ||
resolve({ | ||
status: -1, | ||
msg: `Fail to upload a bunch of data, ${err.message}` | ||
}); | ||
}) | ||
@@ -62,4 +68,11 @@ }); | ||
setConfig(config) { | ||
this.stop(); | ||
this.config = config; | ||
this.start(); | ||
} | ||
_resendFailedData() { | ||
let i = 0; | ||
let obj = this.resendQueue[i]; | ||
while (i < this.resendQueue.length) { | ||
@@ -85,4 +98,4 @@ if (obj.status == StatusEnum.SUCCESS) { | ||
_upload(encodedData) { | ||
if (config.enableGet) { | ||
return fetch(`${config.absoluteUrl}/api/upload-trace?websiteId=${config.websiteId}&impressionId=${this.impressionId}&data=${encodedData}`, { | ||
if (this.config.enableGet) { | ||
return fetch(`${this.config.absoluteUrl}/api/upload-trace?websiteId=${this.config.websiteId}&impressionId=${this.impressionId}&data=${encodedData}`, { | ||
method: "GET", | ||
@@ -92,3 +105,3 @@ credentials: "include" | ||
} else { | ||
return fetch(`${config.absoluteUrl}/api/upload-trace?websiteId=${config.websiteId}&impressionId=${this.impressionId}`, { | ||
return fetch(`${this.config.absoluteUrl}/api/upload-trace?websiteId=${this.config.websiteId}&impressionId=${this.impressionId}`, { | ||
method: "POST", | ||
@@ -95,0 +108,0 @@ credentials: "include", |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
51618
948
126
6