├── .gitignore ├── Gruntfile.js ├── LICENSE ├── README.md ├── config.eslintrc ├── demo ├── demo.html └── src │ └── demo.js ├── dest └── wix-client-recorder.min.js ├── package.json └── src └── wix-client-recorder.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 'use strict'; 3 | 4 | grunt.initConfig({ 5 | uglify: { 6 | all: { 7 | files: { 8 | 'dest/wix-client-recorder.min.js': ['src/**/*.js'] 9 | } 10 | } 11 | 12 | }, 13 | eslint: { 14 | options: { 15 | configFile: 'config.eslintrc' 16 | }, 17 | all: { 18 | src: ["src/**/*.js"] 19 | } 20 | } 21 | }); 22 | 23 | grunt.loadNpmTasks('grunt-contrib-uglify'); 24 | grunt.loadNpmTasks("gruntify-eslint"); 25 | 26 | grunt.registerTask('default', ['eslint', 'uglify']); 27 | }; 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Daniel Wolbe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wix-client-recorder 2 | Record and replay mouse movements. 3 | An utility written in vanilla javascript. 4 | 5 | ## What's That? 6 | This is a small util which allows you to record basic mouse events (move, clicks, scroll) and events and replay then later. 7 | 8 | ## What Can I Do With It? 9 | You can replay a client's session for many reasons: replicating a bug, see user's behaviour, etc.. 10 | 11 | ## A Demonstration, Please 12 | 1. git clone this repo. 13 | 2. Open demo/src/demo.html on your browser. 14 | 3. Play! 15 | 16 | ## Cool! How do I Use It? 17 | 1. You'll have to include wix-client-recorder.min.js in your project. 18 | 2. You can call any of the following functions from your javascript call: 19 | Recorder.start - to start a fresh reording of a page. 20 | Recorder.stop - stop current reocrding 21 | Recorder.play - playback the current recording, or play some saved recording 22 | Recorder.printTable - print recording to console as table 23 | Recorder.printJson - print recording to console as json 24 | Recorder.getSteps - get an object with all recorded steps 25 | Recorder.setDebug - set to true if you want a verbose messages on the console. 26 | 27 | ## I want to contribute to this utility - how can I do that? 28 | 1. install npm (https://www.npmjs.com/) 29 | 2. install Grunt (http://gruntjs.com/) 30 | 3. git clone git@github.com:danielwix/wix-client-recorder.git 31 | 4. cd wix-client-recorder 32 | 5. run: npm i && grunt 33 | 6. modify some code. 34 | Don't forget to run grunt before you commit. 35 | -------------------------------------------------------------------------------- /config.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-array-constructor": 2, 4 | "no-catch-shadow": 2, 5 | "comma-dangle": 2, 6 | "no-cond-assign": 2, 7 | "no-constant-condition": 2, 8 | "no-control-regex": 2, 9 | "no-div-regex": 1, 10 | "no-else-return": 2, 11 | "no-empty-character-class": 2, 12 | "no-empty-label": 2, 13 | "no-eq-null": 2, 14 | "no-extend-native": 2, 15 | "no-extra-boolean-cast": 2, 16 | "no-unneeded-ternary": 2, 17 | "strict": [2, "function"], 18 | "no-inner-declarations": [2, "functions"], 19 | "no-iterator": 2, 20 | "no-labels": 2, 21 | "no-lone-blocks": 2, 22 | "no-lonely-if": 2, 23 | "no-loop-func": 2, 24 | "no-mixed-requires": [0, false], 25 | "no-negated-in-lhs": 2, 26 | "no-nested-ternary": 2, 27 | "no-new-require": 2, 28 | "no-octal-escape": 2, 29 | "no-path-concat": 1, 30 | "no-process-exit": 2, 31 | "no-proto": 2, 32 | "no-redeclare": 2, 33 | "no-regex-spaces": 2, 34 | "no-restricted-modules": 1, 35 | "no-script-url": 2, 36 | "no-sequences": 2, 37 | "no-shadow": 2, 38 | "no-shadow-restricted-names": 2, 39 | "no-spaced-func": 2, 40 | "semi-spacing": [2, {"before": false, "after": true}], 41 | "no-sparse-arrays": 2, 42 | "no-sync": 0, 43 | "no-undef": 2, 44 | "no-unused-expressions": 2, 45 | "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], 46 | "no-warning-comments": [0, {"terms": ["todo", "fixme", "xxx"], "location": "start"}], 47 | "yoda": 2, 48 | 49 | "block-scoped-var": 2, 50 | "brace-style": [2, "1tbs", {"allowSingleLine": true}], 51 | "consistent-return": 2, 52 | "consistent-this": [1, "self"], 53 | "curly": [2, "all"], 54 | "default-case": 0, 55 | "func-names": 0, 56 | "func-style": [2, "declaration"], 57 | "max-depth": [0, 4], 58 | "max-len": [0, 80, 4], 59 | "max-nested-callbacks": [0, 2], 60 | "handle-callback-err": 0, 61 | "one-var": 0, 62 | "sort-vars": 0, 63 | "space-after-keywords": [2, "always"], 64 | "space-infix-ops": 2, 65 | "space-return-throw-case": 2, 66 | "strict": 2, 67 | "valid-typeof": 2, 68 | "wrap-regex": 0, 69 | 70 | "no-alert": 2, 71 | "no-caller": 2, 72 | "no-bitwise": 0, 73 | "no-console": 0, 74 | "no-underscore-dangle": 0, 75 | "no-debugger": 2, 76 | "no-dupe-keys": 2, 77 | "no-empty": 2, 78 | "no-eval": 2, 79 | "no-ex-assign": 2, 80 | "no-extra-parens": 2, 81 | "no-extra-semi": 2, 82 | "no-floating-decimal": 2, 83 | "no-func-assign": 2, 84 | "no-invalid-regexp": 2, 85 | "no-implied-eval": 2, 86 | "no-with": 2, 87 | "no-fallthrough": 2, 88 | "no-unreachable": 2, 89 | "no-undef-init": 2, 90 | "no-octal": 2, 91 | "no-obj-calls": 2, 92 | "no-new-wrappers": 2, 93 | "no-new": 2, 94 | "no-new-func": 2, 95 | "no-native-reassign": 2, 96 | "no-plusplus": 0, 97 | "no-delete-var": 2, 98 | "no-return-assign": 2, 99 | "no-new-object": 2, 100 | "no-label-var": 2, 101 | "no-ternary": 0, 102 | "no-self-compare": 2, 103 | "no-use-before-define": [2, "nofunc"], 104 | "valid-jsdoc": 0, 105 | "eol-last": 0, 106 | "no-undefined": 0, 107 | "no-mixed-spaces-and-tabs": [2, true], 108 | "no-trailing-spaces": 0, 109 | "no-extra-bind": 2, 110 | 111 | "camelcase": 0, 112 | "dot-notation": 2, 113 | "eqeqeq": 2, 114 | "new-parens": 2, 115 | "guard-for-in": 2, 116 | "radix": 1, 117 | "new-cap": 2, 118 | "semi": 2, 119 | "use-isnan": 2, 120 | "quotes": [2, "single", "avoid-escape"], 121 | "max-params": [0, 3], 122 | "max-statements": [0, 10], 123 | "complexity": [0, 11], 124 | "wrap-iife": 2, 125 | "no-multi-str": 2, 126 | 127 | "quote-props": [2, "as-needed"], 128 | "no-multi-spaces": 2, 129 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}], 130 | "comma-spacing": 2, 131 | "space-unary-ops": [2, {"words": true, "nonwords": false}], 132 | "indent": [2, 4, {"SwitchCase": 1}], 133 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 134 | "space-before-blocks": [2, "always"], 135 | "operator-linebreak": [2, "after"], 136 | "dot-location": [2, "property"], 137 | "semi-spacing": [2, {"before": false, "after": true}], 138 | "array-bracket-spacing": [2, "never"], 139 | "object-curly-spacing": [2, "never"], 140 | "no-throw-literal": 2 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /demo/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo Page for Wix Client Recorder 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/src/demo.js: -------------------------------------------------------------------------------- 1 | /*eslint-env browser*/ 2 | (function () { 3 | 'use strict'; 4 | 5 | function loadPlaybackFromInsertRecording() { 6 | var loadedPlayback = document.getElementById('insert-recording').value; 7 | var loadedStepsAsJson = JSON.parse( loadedPlayback); 8 | Recorder.play(loadedStepsAsJson); 9 | } 10 | 11 | function pasteJsonToTextArea() { 12 | document.getElementById('insert-recording').value = JSON.stringify(Recorder.getSteps()); 13 | } 14 | 15 | window.Demo = { 16 | loadPlaybackFromInsertRecording: loadPlaybackFromInsertRecording, 17 | pasteJsonToTextArea: pasteJsonToTextArea 18 | }; 19 | 20 | }()); -------------------------------------------------------------------------------- /dest/wix-client-recorder.min.js: -------------------------------------------------------------------------------- 1 | (function(){"use strict";function a(a){d("move",a.pageX,a.pageY)}function b(a){d("click",a.pageX,a.pageY)}function c(){d("scroll",window.scrollX,window.scrollY)}function d(a,b,c){u.push({type:a,x:b,y:c,time:Date.now()})}function e(){u=[],v=0,w=0}function f(){r("Recording started"),e(),c(),document.addEventListener("mousemove",a),document.addEventListener("click",b),document.addEventListener("scroll",c)}function g(){r("Recording stopped"),document.removeEventListener("mousemove",a),document.removeEventListener("click",b),document.removeEventListener("scroll",c)}function h(a,b){for(var c=document.elementFromPoint(a,b);c&&!c.click;)c=c.parentNode;c&&c.click&&c.click()}function i(a){a=a||u,g(),r("Playing recording with",a.length,"steps"),n();var b,c,d=0,e=a.length;!function f(){c=a[d],"move"===c.type&&m(c.x,c.y),"click"===c.type&&h(c.x,c.y),"scroll"===c.type&&window.scrollTo(c.x,c.y),v=w,w=c.time,d++,d===e?(r("Finished playing recording"),window.clearTimeout(b),o()):b=window.setTimeout(f,w-v)}()}function j(){console.table(p())}function k(){console.log(JSON.stringify(p()))}function l(){var a=document.createElement("div");return a.style.borderRadius="50%",a.style.background="red",a.style.width="10px",a.style.height="10px",a.style.position="fixed",a.style.top=0,a.style.left=0,a.style.zIndex=999,a}function m(a,b){x.style.left=a+"px",x.style.top=b+"px"}function n(){x=x||l(),document.body.appendChild(x)}function o(){document.body.removeChild(x)}function p(){return u}function q(a){y=a}function r(){y&&s(arguments)}function s(a){var b=Array.prototype.slice.call(a),c=(new Date).toString();console.log.apply(console,[c].concat(b))}var t,u,v,w,x,y=!1;t={start:f,stop:g,play:i,printTable:j,printJson:k,getSteps:p,setDebug:q},"undefined"!=typeof module&&"undefined"!=typeof module.exports?module.exports=t:"function"==typeof this.define&&this.define.amd?this.define(function(){return t}):this.Recorder=t}).call(this); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wix-client-recorder", 3 | "version": "0.0.1", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/danielwix/wix-client-recorder" 7 | }, 8 | "dependencies": { 9 | "grunt": "^0.4.5", 10 | "grunt-contrib-uglify": "^0.11.0", 11 | "gruntify-eslint": "^1.3.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/wix-client-recorder.js: -------------------------------------------------------------------------------- 1 | /*eslint-env browser,node*/ 2 | (function () { 3 | 'use strict'; 4 | 5 | var Recorder; 6 | 7 | var localSteps, preTime, nowTime, cursor; 8 | var debug = false; 9 | 10 | function onMouseMove(e) { 11 | addEvent('move', e.pageX, e.pageY); 12 | } 13 | 14 | function onClick(e) { 15 | addEvent('click', e.pageX, e.pageY); 16 | } 17 | 18 | function onScroll() { 19 | addEvent('scroll', window.scrollX, window.scrollY); 20 | } 21 | 22 | function addEvent(type, x, y) { 23 | localSteps.push({type: type, x: x, y: y, time: Date.now()}); 24 | } 25 | 26 | function resetParams() { 27 | localSteps = []; 28 | preTime = 0; 29 | nowTime = 0; 30 | } 31 | 32 | function start() { 33 | log('Recording started'); 34 | resetParams(); 35 | onScroll(); 36 | document.addEventListener('mousemove', onMouseMove); 37 | document.addEventListener('click', onClick); 38 | document.addEventListener('scroll', onScroll); 39 | } 40 | 41 | function stop() { 42 | log('Recording stopped'); 43 | document.removeEventListener('mousemove', onMouseMove); 44 | document.removeEventListener('click', onClick); 45 | document.removeEventListener('scroll', onScroll); 46 | } 47 | 48 | function clickOnElementAtPosition(x, y) { 49 | var clickCandidate = document.elementFromPoint(x, y); 50 | 51 | while (clickCandidate && !clickCandidate.click) { 52 | clickCandidate = clickCandidate.parentNode; 53 | } 54 | 55 | if (clickCandidate && clickCandidate.click) { 56 | clickCandidate.click(); 57 | } 58 | } 59 | 60 | function play(steps) { 61 | steps = steps || localSteps; 62 | stop(); 63 | 64 | log('Playing recording with', steps.length, 'steps'); 65 | 66 | showCursor(); 67 | 68 | var stepNumber = 0; 69 | var stepsCount = steps.length; 70 | var timeout, currentStep; 71 | 72 | (function animate() { 73 | currentStep = steps[stepNumber]; 74 | if (currentStep.type === 'move') { 75 | setCursorPosition(currentStep.x, currentStep.y); 76 | } 77 | 78 | if (currentStep.type === 'click') { 79 | clickOnElementAtPosition(currentStep.x, currentStep.y); 80 | } 81 | 82 | if (currentStep.type === 'scroll') { 83 | window.scrollTo(currentStep.x, currentStep.y); 84 | } 85 | 86 | preTime = nowTime; 87 | nowTime = currentStep.time; 88 | 89 | stepNumber++; 90 | 91 | if (stepNumber === stepsCount) { 92 | log('Finished playing recording'); 93 | window.clearTimeout(timeout); 94 | hideCursor(); 95 | } else { 96 | timeout = window.setTimeout(animate, nowTime - preTime); 97 | } 98 | 99 | }()); 100 | } 101 | 102 | function printTable() { 103 | console.table(getSteps()); 104 | } 105 | 106 | function printJson() { 107 | console.log(JSON.stringify(getSteps())); 108 | } 109 | 110 | function getCursor() { 111 | var cursorNode = document.createElement('div'); 112 | 113 | cursorNode.style.borderRadius = '50%'; 114 | cursorNode.style.background = 'red'; 115 | cursorNode.style.width = '10px'; 116 | cursorNode.style.height = '10px'; 117 | cursorNode.style.position = 'fixed'; 118 | cursorNode.style.top = 0; 119 | cursorNode.style.left = 0; 120 | cursorNode.style.zIndex = 999; 121 | 122 | return cursorNode; 123 | } 124 | 125 | function setCursorPosition(x, y) { 126 | cursor.style.left = x + 'px'; 127 | cursor.style.top = y + 'px'; 128 | } 129 | 130 | function showCursor() { 131 | cursor = cursor || getCursor(); 132 | document.body.appendChild(cursor); 133 | } 134 | 135 | function hideCursor() { 136 | document.body.removeChild(cursor); 137 | } 138 | 139 | function getSteps() { 140 | return localSteps; 141 | } 142 | 143 | function setDebug(enabled) { 144 | debug = enabled; 145 | } 146 | 147 | function log() { 148 | if (debug) { 149 | logWithTimestamp(arguments); 150 | } 151 | } 152 | 153 | function logWithTimestamp(args) { 154 | var argsAsArray = Array.prototype.slice.call(args); 155 | var now = new Date().toString(); 156 | 157 | console.log.apply(console, [now].concat(argsAsArray)); 158 | } 159 | 160 | Recorder = { 161 | start: start, 162 | stop: stop, 163 | play: play, 164 | printTable: printTable, 165 | printJson: printJson, 166 | getSteps: getSteps, 167 | setDebug: setDebug 168 | }; 169 | 170 | // Export for use in server and client. 171 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 172 | module.exports = Recorder; 173 | } else if (typeof this.define === 'function' && this.define.amd) { 174 | this.define(function () { 175 | return Recorder; 176 | }); 177 | } else { 178 | this.Recorder = Recorder; 179 | } 180 | }).call(this); 181 | --------------------------------------------------------------------------------