New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

mouselog

Package Overview
Dependencies
Maintainers
2
Versions
57
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mouselog - npm Package Compare versions

Comparing version 0.0.9 to 0.1.0-beta1

debugger.js

663

build/mouselog.js

@@ -94,3 +94,3 @@ (function webpackUniversalModuleDefinition(root, factory) {

/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 1);
/******/ return __webpack_require__(__webpack_require__.s = 2);
/******/ })

@@ -102,127 +102,129 @@ /************************************************************************/

let debugMode = false;
let outputElement;
function activate(id) {
debugMode = true;
if (id) {
outputElement = window.document.getElementById(id);
if (!outputElement) {
console.log("Fail to find the output element.");
}
}
}
// Default config
let config = {
// Type: string, REQUIRED
// Endpoint Url
uploadEndpoint: "http://localhost:9000",
function write(info) {
if (debugMode) {
if (outputElement) {
let p = document.createElement("p");
p.style.display = "block";
p.style.fontSize = "10px";
p.style.margin = "2px";
let t = document.createTextNode(info);
p.appendChild(t);
outputElement.appendChild(p);
}
console.log(info);
}
}
// Type: string
// Website ID
websiteId: "unknown",
module.exports = {activate, write};
// Endpoint type, "absolute" or "relative"
endpointType: "absolute",
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
// upload protocol, "https" or "http"
// If you declare it in `uploadEndpoint`, this property will be ignored.
uploadProtocol: "https",
var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;(function (name, context, definition) {
if ( true && module.exports) module.exports = definition();
else if (true) !(__WEBPACK_AMD_DEFINE_FACTORY__ = (definition),
__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
(__WEBPACK_AMD_DEFINE_FACTORY__.call(exports, __webpack_require__, exports, module)) :
__WEBPACK_AMD_DEFINE_FACTORY__),
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
else {}
})('urljoin', this, function () {
// Upload mode, "periodic" or "event-triggered"
uploadMode: "periodic",
function normalize (strArray) {
var resultArray = [];
if (strArray.length === 0) { return ''; }
// 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,
if (typeof strArray[0] !== 'string') {
throw new TypeError('Url must be a string. Received ' + strArray[0]);
}
// Type: number
// If `uploadMode` == "event-triggered"
// The website interaction data will be uploaded when every `frequency` events are captured.
frequency: 50,
// If the first part is a plain protocol, we combine it with the next part.
if (strArray[0].match(/^[^/:]+:\/*$/) && strArray.length > 1) {
var first = strArray.shift();
strArray[0] = first + strArray[0];
}
// Type: bool
// Use GET method to upload data? (stringified data will be embedded in URI)
enableGet: false,
// There must be two or three slashes in the file protocol, two slashes in anything else.
if (strArray[0].match(/^file:\/\/\//)) {
strArray[0] = strArray[0].replace(/^([^/:]+):\/*/, '$1:///');
} else {
strArray[0] = strArray[0].replace(/^([^/:]+):\/*/, '$1://');
}
// Type: number
// Time interval for resending the failed trace data
resendInterval: 3000,
}
for (var i = 0; i < strArray.length; i++) {
var component = strArray[i];
// ----------------------------
if (typeof component !== 'string') {
throw new TypeError('Url must be a string. Received ' + component);
}
let requiredParams = [
"uploadEndpoint",
];
if (component === '') { continue; }
// 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.`);
}
});
config = Object.assign(config, params);
config.absoluteUrl = formatUrl();
} catch(err) {
console.log(err);
return false;
}
return true;
}
if (i > 0) {
// Removing the starting slashes for each component but the first.
component = component.replace(/^[\/]+/, '');
}
if (i < strArray.length - 1) {
// Removing the ending slashes for each component but the last.
component = component.replace(/[\/]+$/, '');
} else {
// For the last component we will combine multiple slashes to a single one.
component = component.replace(/[\/]+$/, '/');
}
let updateConfig = (params) => {
// Generate new config
return buildConfig(params);
}
resultArray.push(component);
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 = `${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;
}
module.exports = { config, buildConfig, updateConfig }
var str = resultArray.join('/');
// Each input component is now separated by a single slash except the possible first plain protocol part.
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
// remove trailing slash before parameters or hash
str = str.replace(/\/(\?|&|#[^!])/g, '$1');
let mouselog = __webpack_require__(2);
// replace ? in parameters with &
var parts = str.split('?');
str = parts.shift() + (parts.length > 0 ? '?': '') + parts.join('&');
function run(config) {
mouselog.run(config);
}
return str;
}
function stop() {
mouselog.stop();
}
return function () {
var input;
module.exports = { run, stop };
if (typeof arguments[0] === 'object') {
input = arguments[0];
} else {
input = [].slice.call(arguments);
}
return normalize(input);
};
});
/***/ }),
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
/***/ (function(module, 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 { Config } = __webpack_require__(7);
const debug = __webpack_require__(0);
let targetEvents = [

@@ -240,10 +242,18 @@ "mousemove",

];
let pageLoadTime = new Date();
let uploader;
let impressionId;
let eventsList;
let pageLoadTime;
let uploadIdx;
let uploadInterval;
let hiddenProperty = 'hidden' in document ? 'hidden' :
'webkitHidden' in document ? 'webkitHidden' :
'mozHidden' in document ? 'mozHidden' :
null;
let visibilityChangeEvent = hiddenProperty ? hiddenProperty.replace(/hidden/i, 'visibilitychange') : null;
function maxNumber(...nums) {
let res = nums[0];
for (let i = 1; i < nums.length; ++i) {
res = res > nums[i] ? res : nums[i];
}
return res;
}
function getRelativeTimestampInSeconds() {

@@ -262,135 +272,212 @@ let diff = new Date() - pageLoadTime;

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;
}
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 uploadTrace() {
let trace = newTrace();
trace.events = eventsList;
eventsList = [];
return uploader.upload(trace); // This is a promise
}
_clearBuffer() {
this.eventsList = [];
}
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;
_newTrace() {
let trace = {
id: '0',
idx: this.uploadIdx,
url: window.location.hostname ? window.location.hostname : "localhost",
path: window.location.pathname,
width: maxNumber(document.body.scrollWidth, window.innerWidth),
height: maxNumber(document.body.scrollHeight, window.innerHeight),
pageLoadTime: pageLoadTime,
events: []
}
this.uploadIdx += 1;
return trace;
}
let x = evt.pageX;
let y = evt.pageY;
if (x === undefined) {
x = evt.changedTouches[0].pageX;
y = evt.changedTouches[0].pageY;
_onVisibilityChange(evt) {
if (window.document[hiddenProperty]) {
// the page is not activated
this._pause();
} else {
// the page is activated
this._resume();
}
}
let tmpEvt = {
id: eventsList.length,
timestamp: getRelativeTimestampInSeconds(),
type: evt.type,
x: x,
y: y,
button: getButton(evt.button)
_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)
}
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();
}
}
if (evt.type == "wheel") {
tmpEvt.deltaX = evt.deltaX;
tmpEvt.deltaY = evt.deltaY;
_fetchConfigFromServer() {
// Upload an empty trace to fetch config from server
let trace = this._newTrace();
return this.uploader.upload(trace); // This is a promise
}
eventsList.push(tmpEvt);
if ( config.uploadMode == "event-triggered" && eventsList.length % config.frequency == 0 ) {
uploadTrace();
_uploadTrace() {
let trace = this._newTrace();
trace.events = this.eventsList;
this.eventsList = [];
return this.uploader.upload(trace); // This is a promise
}
}
function clearBuffer() {
eventsList = [];
}
_periodUploadTimeout() {
clearTimeout(this.uploadTimeout);
this.uploadTimeout = setTimeout(() => {
if (this.eventsList.length > 0) {
this._uploadTrace();
}
}, this.config.uploadPeriod);
}
// 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.`});
_periodUploadInterval() {
clearInterval(this.uploadInterval);
this.uploadInterval = setInterval(() => {
if (this.eventsList.length > 0) {
this._uploadTrace();
}
}, this.config.uploadPeriod);
}
_runCollector() {
targetEvents.forEach( s => {
this.config.scope.addEventListener(s, (evt) => this._mouseHandler(evt));
});
if (this.config.uploadMode === "periodic") {
this._periodUploadInterval();
}
if (this.config.uploadMode === "mixed") {
this._periodUploadTimeout();
}
}
_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);
debug.write("Successfully update config from backend.");
} else {
throw new Error(`Unable to update config with server config.`);
}
} else {
throw new Error(`Fail to get config from server.`);
}
}).catch(err => {
debug.write(err);
});
window.onbeforeunload = (evt) => {
if (this.eventsList.length != 0) {
this._uploadTrace();
}
});
}
return {status: 0};
} else {
resolve({status: -1, msg: `Fail to initialize config.`});
return {status: -1, msg: `Invalid configuration.`};
}
})
}
}
function runCollector() {
targetEvents.forEach( s => {
window.document.addEventListener(s, (evt) => mouseHandler(evt));
});
_pause() {
this._stopCollector();
}
if (config.uploadMode === "periodic") {
uploadInterval = setInterval(() => {
if (eventsList.length != 0) {
uploadTrace();
}
}, config.uploadPeriod);
_resume() {
this._runCollector();
}
}
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);
console.log("Mouselog agent is activated!");
run(config) {
let res = this._init(config);
if (res.status == 0) {
if (visibilityChangeEvent) {
document.addEventListener(visibilityChangeEvent, (evt)=>this._onVisibilityChange(evt));
}
this._runCollector();
this.uploader.start(this.impressionId);
debug.write("Mouselog agent is activated!");
debug.write(`Website ID: ${this.config.websiteId}`);
debug.write(`Impression ID: ${this.impressionId}`);
debug.write(`User-Agent: ${navigator.userAgent}`);
debug.write(`Page load time: ${pageLoadTime}`);
} else {
console.log(res.msg);
console.log("Fail to initialize Mouselog agent.");
debug.write(res.msg);
debug.write("Fail to initialize Mouselog agent.");
}
})
}
}
function stop() {
uploader.stop();
stopCollector();
clearBuffer();
console.log("Mouselog agent is stopped!");
debug(config, debugOutputElementId) {
debug.activate(debugOutputElementId);
this.run(config);
}
stop() {
this.uploader.stop();
this._stopCollector();
this._clearBuffer();
debug.write(`Mouselog agent ${this.impressionId} is stopped!`);
}
}
module.exports = { Mouselog };
/***/ }),

@@ -507,3 +594,4 @@ /* 3 */

let {config, updateConfig} = __webpack_require__(0);
let urljoin = __webpack_require__(1);
const debug = __webpack_require__(0);

@@ -517,11 +605,12 @@ let StatusEnum = {

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

@@ -535,9 +624,11 @@

upload(data) {
// resolve(true/false): uploaded success/fail.
// resolve({status:-1/0/1, ...}): uploading success/fail.
// reject(ErrorMessage): Errors occur when updating the config.
return new Promise( (resolve, reject) => {
let encodedData = JSON.stringify(data);
debug.write(`Uploading Pkg ${data.idx}, window size: ${data.width}*${data.height}, events count: ${data.events.length}`)
this._upload(encodedData).then(res => {
if (res.status == 200) {
res.json().then( resObj => {
debug.write(`Pkg ${data.idx} response=${JSON.stringify(resObj)}`);
if (resObj.status !== "ok") {

@@ -547,5 +638,7 @@ throw new Error("Response object status is not ok.");

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

@@ -558,4 +651,8 @@ resolve({status: 0});

}).catch(err => {
debug.write(`Pkg ${data.idx} failed, wait for resending. Error message: ${err.message}`);
this._appendFailedData(data);
resolve({status: -1, msg: `Fail to upload a bunch of data: ${err.message}`});
resolve({
status: -1,
msg: `Fail to upload data bunch #${data.idx}, ${err.message}`
});
})

@@ -569,5 +666,15 @@ });

setConfig(config) {
this.stop();
this.config = config;
this.start();
}
_resendFailedData() {
let i = 0;
if (this.resendQueue.length > 0) {
debug.write("Resending data...");
}
while (i < this.resendQueue.length) {
let obj = this.resendQueue[i];
if (obj.status == StatusEnum.SUCCESS) {

@@ -577,2 +684,3 @@ this.resendQueue.splice(i, 1); // Remove it from resendQueue

i += 1;
debug.write(`Resending Pkg ${obj.data.idx}`);
if (obj.status == StatusEnum.WAITING) {

@@ -593,4 +701,9 @@ obj.status = StatusEnum.SENDING;

_upload(encodedData) {
if (config.enableGet) {
return fetch(`${config.absoluteUrl}/api/upload-trace?websiteId=${config.websiteId}&impressionId=${this.impressionId}&data=${encodedData}`, {
let url = urljoin(
this.config.absoluteUrl,
'/api/upload-trace',
`?websiteId=${this.config.websiteId}&impressionId=${this.impressionId}`,
);
if (this.config.enableGet) {
return fetch(`${url}&data=${encodedData}`, {
method: "GET",

@@ -600,3 +713,3 @@ credentials: "include"

} else {
return fetch(`${config.absoluteUrl}/api/upload-trace?websiteId=${config.websiteId}&impressionId=${this.impressionId}`, {
return fetch(url, {
method: "POST",

@@ -620,4 +733,102 @@ credentials: "include",

/***/ }),
/* 7 */
/***/ (function(module, exports, __webpack_require__) {
const urljoin = __webpack_require__(1);
const debug = __webpack_require__(0);
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";
// Endpoint type, "absolute" or "relative"
this.endpointType = "absolute";
// Upload mode, "mixed", "periodic" or "event-triggered"
this.uploadMode = "mixed";
// 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;
// Type: number
// If `uploadMode` == "event-triggered"
// The website interaction data will be uploaded when every `frequency` events are captured.
this.frequency = 50;
// Type: bool
// Use GET method to upload data? (stringified data will be embedded in URI)
this.enableGet = false;
// Type: number
// Time interval for resending the failed trace data
this.resendInterval = 3000;
// Type: HTML DOM Element
// Capture the events occur in `this.scope`
this.scope = window.document;
// These parameters are required for runing a Mouselog agent
this._requiredParams = [
"uploadEndpoint",
]
// 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._formatUrl();
} catch(err) {
debug.write(err);
return false;
}
return true;
}
update(config) {
return this.build(config, true);
}
_formatUrl() {
if (this.endpointType == "relative") {
this.absoluteUrl = urljoin(window.location.origin, this.uploadEndpoint);
} else if (this.endpointType == "absolute") {
this.absoluteUrl = this.uploadEndpoint;
} else {
throw new Error('`endpointType` can only be "absolute" or "relative"');
}
}
}
module.exports = { Config };
/***/ })
/******/ ]);
});

@@ -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(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 i(o){if(t[o])return t[o].exports;var n=t[o]={i:o,l:!1,exports:{}};return e[o].call(n.exports,n,n.exports,i),n.l=!0,n.exports}return i.m=e,i.c=t,i.d=function(e,t,o){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(i.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(o,n,function(t){return e[t]}.bind(null,n));return o},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i(i.s=2)}([function(e,t){let i,o=!1;e.exports={activate:function(e){o=!0,e&&(i=window.document.getElementById(e),i||console.log("Fail to find the output element."))},write:function(e){if(o){if(i){let t=document.createElement("p");t.style.display="block",t.style.fontSize="10px",t.style.margin="2px";let o=document.createTextNode(e);t.appendChild(o),i.appendChild(t)}console.log(e)}}}},function(e,t,i){var o,n,s;s=function(){function e(e){var t=[];if(0===e.length)return"";if("string"!=typeof e[0])throw new TypeError("Url must be a string. Received "+e[0]);if(e[0].match(/^[^/:]+:\/*$/)&&e.length>1){var i=e.shift();e[0]=i+e[0]}e[0].match(/^file:\/\/\//)?e[0]=e[0].replace(/^([^/:]+):\/*/,"$1:///"):e[0]=e[0].replace(/^([^/:]+):\/*/,"$1://");for(var o=0;o<e.length;o++){var n=e[o];if("string"!=typeof n)throw new TypeError("Url must be a string. Received "+n);""!==n&&(o>0&&(n=n.replace(/^[\/]+/,"")),n=o<e.length-1?n.replace(/[\/]+$/,""):n.replace(/[\/]+$/,"/"),t.push(n))}var s=t.join("/"),r=(s=s.replace(/\/(\?|&|#[^!])/g,"$1")).split("?");return s=r.shift()+(r.length>0?"?":"")+r.join("&")}return function(){return e("object"==typeof arguments[0]?arguments[0]:[].slice.call(arguments))}},e.exports?e.exports=s():void 0===(n="function"==typeof(o=s)?o.call(t,i,t,e):o)||(e.exports=n)},function(e,t,i){const o=i(3),n=i(6);let{Config:s}=i(7);const r=i(0);let a=["mousemove","mousedown","mouseup","mouseclick","dblclick","contextmenu","wheel","torchstart","touchmove","touchend"],l=new Date,d="hidden"in document?"hidden":"webkitHidden"in document?"webkitHidden":"mozHidden"in document?"mozHidden":null,u=d?d.replace(/hidden/i,"visibilitychange"):null;function c(...e){let t=e[0];for(let i=1;i<e.length;++i)t=t>e[i]?t:e[i];return t}function h(){let e=new Date-l;return Math.trunc(e)/1e3}e.exports={Mouselog:class{constructor(){this.config=new s,this.impressionId=o(),this.mouselogLoadTime=new Date,this.uploader=new n,this.eventsList=[],this.eventsCount=0,this.uploadInterval,this.uploadTimeout}_clearBuffer(){this.eventsList=[]}_newTrace(){let e={id:"0",idx:this.uploadIdx,url:window.location.hostname?window.location.hostname:"localhost",path:window.location.pathname,width:c(document.body.scrollWidth,window.innerWidth),height:c(document.body.scrollHeight,window.innerHeight),pageLoadTime:l,events:[]};return this.uploadIdx+=1,e}_onVisibilityChange(e){window.document[d]?this._pause():this._resume()}_mouseHandler(e){if("contextmenu"===e.type&&0===e.pageX&&0===e.pageY)return;let t=e.pageX,i=e.pageY;void 0===t&&(t=e.changedTouches[0].pageX,i=e.changedTouches[0].pageY);let o={id:this.eventsCount,timestamp:h(),type:e.type,x:t,y:i,button:(n=e.button,"2"===n?"Right":"")};var n;"wheel"==e.type&&(o.deltaX=e.deltaX,o.deltaY=e.deltaY),this.eventsList.push(o),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 e=this._newTrace();return this.uploader.upload(e)}_uploadTrace(){let e=this._newTrace();return e.events=this.eventsList,this.eventsList=[],this.uploader.upload(e)}_periodUploadTimeout(){clearTimeout(this.uploadTimeout),this.uploadTimeout=setTimeout(()=>{this.eventsList.length>0&&this._uploadTrace()},this.config.uploadPeriod)}_periodUploadInterval(){clearInterval(this.uploadInterval),this.uploadInterval=setInterval(()=>{this.eventsList.length>0&&this._uploadTrace()},this.config.uploadPeriod)}_runCollector(){a.forEach(e=>{this.config.scope.addEventListener(e,e=>this._mouseHandler(e))}),"periodic"===this.config.uploadMode&&this._periodUploadInterval(),"mixed"===this.config.uploadMode&&this._periodUploadTimeout()}_stopCollector(){a.forEach(e=>{this.config.scope.removeEventListener(e,e=>this._mouseHandler(e))}),clearInterval(this.uploadInterval),clearTimeout(this.uploadTimeout)}_resetCollector(){this._stopCollector(),this._runCollector()}_init(e){return this.impressionId=o(),this._clearBuffer(),this.uploadIdx=0,this.uploader=new n(this.impressionId,this.config),this.uploader.setImpressionId(this.impressionId),this.config.build(e)?(this._fetchConfigFromServer().then(e=>{if(1!=e.status)throw new Error("Fail to get config from server.");if(!this.config.update(e.config))throw new Error("Unable to update config with server config.");this._resetCollector(),this.uploader.setConfig(this.config),r.write("Successfully update config from backend.")}).catch(e=>{r.write(e)}),window.onbeforeunload=e=>{0!=this.eventsList.length&&this._uploadTrace()},{status:0}):{status:-1,msg:"Invalid configuration."}}_pause(){this._stopCollector()}_resume(){this._runCollector()}run(e){let t=this._init(e);0==t.status?(u&&document.addEventListener(u,e=>this._onVisibilityChange(e)),this._runCollector(),this.uploader.start(this.impressionId),r.write("Mouselog agent is activated!"),r.write(`Website ID: ${this.config.websiteId}`),r.write(`Impression ID: ${this.impressionId}`),r.write(`User-Agent: ${navigator.userAgent}`),r.write(`Page load time: ${l}`)):(r.write(t.msg),r.write("Fail to initialize Mouselog agent."))}debug(e,t){r.activate(t),this.run(e)}stop(){this.uploader.stop(),this._stopCollector(),this._clearBuffer(),r.write(`Mouselog agent ${this.impressionId} is stopped!`)}}}},function(e,t,i){var o=i(4),n=i(5);e.exports=function(e,t,i){var s=t&&i||0;"string"==typeof e&&(t="binary"===e?new Array(16):null,e=null);var r=(e=e||{}).random||(e.rng||o)();if(r[6]=15&r[6]|64,r[8]=63&r[8]|128,t)for(var a=0;a<16;++a)t[s+a]=r[a];return t||n(r)}},function(e,t){var i="undefined"!=typeof crypto&&crypto.getRandomValues&&crypto.getRandomValues.bind(crypto)||"undefined"!=typeof msCrypto&&"function"==typeof window.msCrypto.getRandomValues&&msCrypto.getRandomValues.bind(msCrypto);if(i){var o=new Uint8Array(16);e.exports=function(){return i(o),o}}else{var n=new Array(16);e.exports=function(){for(var e,t=0;t<16;t++)0==(3&t)&&(e=4294967296*Math.random()),n[t]=e>>>((3&t)<<3)&255;return n}}},function(e,t){for(var i=[],o=0;o<256;++o)i[o]=(o+256).toString(16).substr(1);e.exports=function(e,t){var o=t||0,n=i;return[n[e[o++]],n[e[o++]],n[e[o++]],n[e[o++]],"-",n[e[o++]],n[e[o++]],"-",n[e[o++]],n[e[o++]],"-",n[e[o++]],n[e[o++]],"-",n[e[o++]],n[e[o++]],n[e[o++]],n[e[o++]],n[e[o++]],n[e[o++]]].join("")}},function(e,t,i){let o=i(1);const n=i(0);let s=0,r=1,a=2;e.exports=class{constructor(e,t){this.impressionId=e,this.config=t,this.resendQueue=[]}start(){this.resendInterval=setInterval(()=>{this._resendFailedData.call(this)},this.config.resendInterval)}stop(){clearInterval(this.resendInterval)}upload(e){return new Promise((t,i)=>{let o=JSON.stringify(e);n.write(`Uploading Pkg ${e.idx}, window size: ${e.width}*${e.height}, events count: ${e.events.length}`),this._upload(o).then(i=>{if(200!=i.status)throw new Error("Response status code is not 200.");i.json().then(i=>{if(n.write(`Pkg ${e.idx} response=${JSON.stringify(i)}`),"ok"!==i.status)throw new Error("Response object status is not ok.");"config"==i.msg&&t({status:1,msg:"Get config from server",config:i.data}),t({status:0})})}).catch(i=>{n.write(`Pkg ${e.idx} failed, wait for resending. Error message: ${i.message}`),this._appendFailedData(e),t({status:-1,msg:`Fail to upload data bunch #${e.idx}, ${i.message}`})})})}setImpressionId(e){this.impressionId=e}setConfig(e){this.stop(),this.config=e,this.start()}_resendFailedData(){let e=0;for(this.resendQueue.length>0&&n.write("Resending data...");e<this.resendQueue.length;){let t=this.resendQueue[e];t.status==a?this.resendQueue.splice(e,1):(e+=1,n.write(`Resending Pkg ${t.data.idx}`),t.status==s&&(t.status=r,this.upload(t.data).then(e=>{t.status=e?a:s})))}}_upload(e){let t=o(this.config.absoluteUrl,"/api/upload-trace",`?websiteId=${this.config.websiteId}&impressionId=${this.impressionId}`);return this.config.enableGet?fetch(`${t}&data=${e}`,{method:"GET",credentials:"include"}):fetch(t,{method:"POST",credentials:"include",body:e})}_appendFailedData(e){this.resendQueue.push({status:s,data:e})}}},function(e,t,i){const o=i(1),n=i(0);e.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(e,t=!1){try{this._requiredParams.forEach(t=>{if(!e.hasOwnProperty(t))throw new Error(`Param ${t} is required but not declared.`)}),Object.keys(e).forEach(i=>{this[i]&&!i.startsWith("_")&&"function"!=typeof this[i]&&(t&&i in this._ignoredParams||(this[i]=e[i]))}),this._formatUrl()}catch(e){return n.write(e),!1}return!0}update(e){return this.build(e,!0)}_formatUrl(){if("relative"==this.endpointType)this.absoluteUrl=o(window.location.origin,this.uploadEndpoint);else{if("absolute"!=this.endpointType)throw new Error('`endpointType` can only be "absolute" or "relative"');this.absoluteUrl=this.uploadEndpoint}}}}}])}));

@@ -0,95 +1,93 @@

const urljoin = require('url-join');
const debug = require('./debugger');
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._formatUrl();
} catch(err) {
debug.write(err);
return false;
}
return true;
}
let requiredParams = [
"uploadEndpoint",
];
// 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.`);
}
});
config = Object.assign(config, params);
config.absoluteUrl = formatUrl();
} catch(err) {
console.log(err);
return false;
update(config) {
return this.build(config, true);
}
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}`;
_formatUrl() {
if (this.endpointType == "relative") {
this.absoluteUrl = urljoin(window.location.origin, this.uploadEndpoint);
} else if (this.endpointType == "absolute") {
this.absoluteUrl = this.uploadEndpoint;
} else {
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;
}
module.exports = { config, buildConfig, updateConfig }
module.exports = { Config };
const uuidv4 = require('uuid/v4');
const Uploader = require('./uploader');
let { config, buildConfig } = require('./config');
let { Config } = require('./config');
const debug = require('./debugger');
let targetEvents = [

@@ -18,10 +18,18 @@ "mousemove",

];
let pageLoadTime = new Date();
let uploader;
let impressionId;
let eventsList;
let pageLoadTime;
let uploadIdx;
let uploadInterval;
let hiddenProperty = 'hidden' in document ? 'hidden' :
'webkitHidden' in document ? 'webkitHidden' :
'mozHidden' in document ? 'mozHidden' :
null;
let visibilityChangeEvent = hiddenProperty ? hiddenProperty.replace(/hidden/i, 'visibilitychange') : null;
function maxNumber(...nums) {
let res = nums[0];
for (let i = 1; i < nums.length; ++i) {
res = res > nums[i] ? res : nums[i];
}
return res;
}
function getRelativeTimestampInSeconds() {

@@ -40,132 +48,210 @@ let diff = new Date() - pageLoadTime;

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;
}
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 uploadTrace() {
let trace = newTrace();
trace.events = eventsList;
eventsList = [];
return uploader.upload(trace); // This is a promise
}
_clearBuffer() {
this.eventsList = [];
}
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;
_newTrace() {
let trace = {
id: '0',
idx: this.uploadIdx,
url: window.location.hostname ? window.location.hostname : "localhost",
path: window.location.pathname,
width: maxNumber(document.body.scrollWidth, window.innerWidth),
height: maxNumber(document.body.scrollHeight, window.innerHeight),
pageLoadTime: pageLoadTime,
events: []
}
this.uploadIdx += 1;
return trace;
}
let x = evt.pageX;
let y = evt.pageY;
if (x === undefined) {
x = evt.changedTouches[0].pageX;
y = evt.changedTouches[0].pageY;
_onVisibilityChange(evt) {
if (window.document[hiddenProperty]) {
// the page is not activated
this._pause();
} else {
// the page is activated
this._resume();
}
}
let tmpEvt = {
id: eventsList.length,
timestamp: getRelativeTimestampInSeconds(),
type: evt.type,
x: x,
y: y,
button: getButton(evt.button)
_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)
}
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();
}
}
if (evt.type == "wheel") {
tmpEvt.deltaX = evt.deltaX;
tmpEvt.deltaY = evt.deltaY;
_fetchConfigFromServer() {
// Upload an empty trace to fetch config from server
let trace = this._newTrace();
return this.uploader.upload(trace); // This is a promise
}
eventsList.push(tmpEvt);
if ( config.uploadMode == "event-triggered" && eventsList.length % config.frequency == 0 ) {
uploadTrace();
_uploadTrace() {
let trace = this._newTrace();
trace.events = this.eventsList;
this.eventsList = [];
return this.uploader.upload(trace); // This is a promise
}
}
function clearBuffer() {
eventsList = [];
}
_periodUploadTimeout() {
clearTimeout(this.uploadTimeout);
this.uploadTimeout = setTimeout(() => {
if (this.eventsList.length > 0) {
this._uploadTrace();
}
}, this.config.uploadPeriod);
}
// 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.`});
_periodUploadInterval() {
clearInterval(this.uploadInterval);
this.uploadInterval = setInterval(() => {
if (this.eventsList.length > 0) {
this._uploadTrace();
}
}, this.config.uploadPeriod);
}
_runCollector() {
targetEvents.forEach( s => {
this.config.scope.addEventListener(s, (evt) => this._mouseHandler(evt));
});
if (this.config.uploadMode === "periodic") {
this._periodUploadInterval();
}
if (this.config.uploadMode === "mixed") {
this._periodUploadTimeout();
}
}
_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);
debug.write("Successfully update config from backend.");
} else {
throw new Error(`Unable to update config with server config.`);
}
} else {
throw new Error(`Fail to get config from server.`);
}
}).catch(err => {
debug.write(err);
});
window.onbeforeunload = (evt) => {
if (this.eventsList.length != 0) {
this._uploadTrace();
}
});
}
return {status: 0};
} else {
resolve({status: -1, msg: `Fail to initialize config.`});
return {status: -1, msg: `Invalid configuration.`};
}
})
}
}
function runCollector() {
targetEvents.forEach( s => {
window.document.addEventListener(s, (evt) => mouseHandler(evt));
});
_pause() {
this._stopCollector();
}
if (config.uploadMode === "periodic") {
uploadInterval = setInterval(() => {
if (eventsList.length != 0) {
uploadTrace();
}
}, config.uploadPeriod);
_resume() {
this._runCollector();
}
}
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);
console.log("Mouselog agent is activated!");
run(config) {
let res = this._init(config);
if (res.status == 0) {
if (visibilityChangeEvent) {
document.addEventListener(visibilityChangeEvent, (evt)=>this._onVisibilityChange(evt));
}
this._runCollector();
this.uploader.start(this.impressionId);
debug.write("Mouselog agent is activated!");
debug.write(`Website ID: ${this.config.websiteId}`);
debug.write(`Impression ID: ${this.impressionId}`);
debug.write(`User-Agent: ${navigator.userAgent}`);
debug.write(`Page load time: ${pageLoadTime}`);
} else {
console.log(res.msg);
console.log("Fail to initialize Mouselog agent.");
debug.write(res.msg);
debug.write("Fail to initialize Mouselog agent.");
}
})
}
}
export function stop() {
uploader.stop();
stopCollector();
clearBuffer();
console.log("Mouselog agent is stopped!");
debug(config, debugOutputElementId) {
debug.activate(debugOutputElementId);
this.run(config);
}
stop() {
this.uploader.stop();
this._stopCollector();
this._clearBuffer();
debug.write(`Mouselog agent ${this.impressionId} is stopped!`);
}
}
module.exports = { Mouselog };
{
"name": "mouselog",
"version": "0.0.9",
"version": "0.1.0-beta1",
"description": "The mouse tracking agent for Mouselog.",

@@ -20,2 +20,3 @@ "main": "index.js",

"dependencies": {
"url-join": "^4.0.1",
"uuid": "^3.3.3"

@@ -22,0 +23,0 @@ },

@@ -15,3 +15,4 @@ [![NPM version](https://img.shields.io/npm/v/mouselog)](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 @@ ```

@@ -1,2 +0,3 @@

let {config, updateConfig} = require('./config');
let urljoin = require('url-join');
const debug = require('./debugger');

@@ -10,11 +11,12 @@ let StatusEnum = {

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,9 +30,11 @@

upload(data) {
// resolve(true/false): uploaded success/fail.
// resolve({status:-1/0/1, ...}): uploading success/fail.
// reject(ErrorMessage): Errors occur when updating the config.
return new Promise( (resolve, reject) => {
let encodedData = JSON.stringify(data);
debug.write(`Uploading Pkg ${data.idx}, window size: ${data.width}*${data.height}, events count: ${data.events.length}`)
this._upload(encodedData).then(res => {
if (res.status == 200) {
res.json().then( resObj => {
debug.write(`Pkg ${data.idx} response=${JSON.stringify(resObj)}`);
if (resObj.status !== "ok") {

@@ -40,5 +44,7 @@ throw new Error("Response object status is not ok.");

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

@@ -51,4 +57,8 @@ resolve({status: 0});

}).catch(err => {
debug.write(`Pkg ${data.idx} failed, wait for resending. Error message: ${err.message}`);
this._appendFailedData(data);
resolve({status: -1, msg: `Fail to upload a bunch of data: ${err.message}`});
resolve({
status: -1,
msg: `Fail to upload data bunch #${data.idx}, ${err.message}`
});
})

@@ -62,5 +72,15 @@ });

setConfig(config) {
this.stop();
this.config = config;
this.start();
}
_resendFailedData() {
let i = 0;
if (this.resendQueue.length > 0) {
debug.write("Resending data...");
}
while (i < this.resendQueue.length) {
let obj = this.resendQueue[i];
if (obj.status == StatusEnum.SUCCESS) {

@@ -70,2 +90,3 @@ this.resendQueue.splice(i, 1); // Remove it from resendQueue

i += 1;
debug.write(`Resending Pkg ${obj.data.idx}`);
if (obj.status == StatusEnum.WAITING) {

@@ -86,4 +107,9 @@ obj.status = StatusEnum.SENDING;

_upload(encodedData) {
if (config.enableGet) {
return fetch(`${config.absoluteUrl}/api/upload-trace?websiteId=${config.websiteId}&impressionId=${this.impressionId}&data=${encodedData}`, {
let url = urljoin(
this.config.absoluteUrl,
'/api/upload-trace',
`?websiteId=${this.config.websiteId}&impressionId=${this.impressionId}`,
);
if (this.config.enableGet) {
return fetch(`${url}&data=${encodedData}`, {
method: "GET",

@@ -93,3 +119,3 @@ credentials: "include"

} else {
return fetch(`${config.absoluteUrl}/api/upload-trace?websiteId=${config.websiteId}&impressionId=${this.impressionId}`, {
return fetch(url, {
method: "POST",

@@ -96,0 +122,0 @@ credentials: "include",

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc