├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── cjs └── index.js ├── esm └── index.js ├── index.js ├── min.js ├── new.js ├── package.json ├── rollup ├── babel.config.js └── new.config.js └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | rollup/ 4 | test/ 5 | package-lock.json 6 | .travis.yml 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | git: 5 | depth: 1 6 | branches: 7 | only: 8 | - master 9 | after_success: 10 | - "npm run coveralls" 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2019, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # data-telemetry 2 | 3 | [![Build Status](https://travis-ci.com/WebReflection/data-telemetry.svg?branch=master)](https://travis-ci.com/WebReflection/data-telemetry) [![Coverage Status](https://coveralls.io/repos/github/WebReflection/flatted/badge.svg?branch=master)](https://coveralls.io/github/WebReflection/flatted?branch=master) 4 | 5 | A simple event tracker for user surfing sessions. 6 | 7 | ```js 8 | import {Session} from 'data-telemetry'; 9 | 10 | const session = new Session( 11 | document.body, // the root node to search for [data-telemetry] 12 | // it's the document itself by default 13 | true, // overwrite last move to reduce the amount of events 14 | // it's true as default 15 | false // use pointer events instead of mouse 16 | // it defaults to typeof PointerEvent check 17 | ); 18 | 19 | // ... after a while ... 20 | // log the list of registered records 21 | console.log(session.events); 22 | ``` 23 | 24 | The session is serializable as JSON too via `JSON.stringify(session)`, resulting in its list of events as records. 25 | 26 | ### Events 27 | 28 | `cancel`, `down`, `enter`, `leave`, `move`, `out`, `over`, and `up` are automatically transformed via `mouse` or `pointer` prefix. 29 | 30 | The value `all` will try to setup all possible events per element. 31 | 32 | To enable any telemetry event, use the `data-telemetry` attribute as shown in the following example: 33 | 34 | ```html 35 |
36 | 37 | 40 |
41 | ``` 42 | 43 | The session can be confined per container, and every node will add an event to the list once such event happens. 44 | 45 | ### Records 46 | 47 | Each record will contain the following details: 48 | 49 | * `target`, stored as CSS selector 50 | * `type`, the event type 51 | * `key`, the key info, if available 52 | * `x` and `y`, the event pageX/Y coordinates, if available 53 | * `primary`, the pointerevents `primary` detail, if available 54 | * `time` the `timeStamp` of the event 55 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const asCSS = el => { 3 | const details = []; 4 | if (el.id) 5 | details.push(`#${el.id}`); 6 | else { 7 | const parent = el.closest('[id]'); 8 | const id = parent ? `#${parent.id} ` : ''; 9 | details.push(id + el.nodeName.toLowerCase()); 10 | } 11 | details.push.apply(details, el.classList); 12 | return details.join('.'); 13 | }; 14 | 15 | const transform = (name, pointerEvents) => 16 | (pointerEvents ? 'pointer' : 'mouse') + name; 17 | 18 | const types = { 19 | cancel: pointerEvents => transform('cancel', pointerEvents), 20 | down: pointerEvents => transform('down', pointerEvents), 21 | enter: pointerEvents => transform('enter', pointerEvents), 22 | leave: pointerEvents => transform('leave', pointerEvents), 23 | move: pointerEvents => transform('move', pointerEvents), 24 | out: pointerEvents => transform('out', pointerEvents), 25 | over: pointerEvents => transform('over', pointerEvents), 26 | up: pointerEvents => transform('up', pointerEvents) 27 | }; 28 | 29 | class Session { 30 | constructor( 31 | // the node to search for data-telemetry 32 | root = document, 33 | // store only last mouse/pointermove event 34 | overwriteLastMove = true, 35 | // use pointer events instead of mouse events 36 | pointerEvents = typeof PointerEvent === 'function' 37 | ) { 38 | this.events = []; 39 | this.root = root; 40 | this.overwriteLastMove = overwriteLastMove; 41 | const elements = root.querySelectorAll('[data-telemetry]'); 42 | for (let i = 0, {length} = elements; i < length; i++) 43 | { 44 | const el = elements[i]; 45 | const telemetry = el.dataset.telemetry.split(/,[ \t\n\r]*/); 46 | for (let i = 0, {length} = telemetry; i < length; i++) 47 | { 48 | const type = telemetry[i]; 49 | if (type === 'all') { 50 | for (let key in el) { 51 | if (/^on/.test(key)) 52 | el.addEventListener(key, this, true); 53 | } 54 | } 55 | else { 56 | el.addEventListener( 57 | types.hasOwnProperty(type) ? 58 | types[type](pointerEvents) : type, 59 | this, 60 | true 61 | ); 62 | } 63 | } 64 | } 65 | } 66 | handleEvent(event) { 67 | const {overwriteLastMove, events} = this; 68 | const { 69 | target, 70 | type, 71 | key, 72 | pageX: x, pageY: y, 73 | isPrimary: primary, 74 | timeStamp: time, 75 | isTrusted 76 | } = event; 77 | if (!isTrusted) 78 | return; 79 | const {length} = events; 80 | const record = { 81 | target: asCSS(target), 82 | type, 83 | key, 84 | x, y, 85 | primary, 86 | time 87 | }; 88 | if (overwriteLastMove && length > 0 && /move$/.test(type)) 89 | { 90 | if (/move$/.test(events[length - 1].type)) 91 | { 92 | events[length - 1] = record; 93 | return; 94 | } 95 | } 96 | events.push(record); 97 | } 98 | toJSON() { 99 | return { 100 | root: asCSS(this.root), 101 | events: this.events 102 | }; 103 | } 104 | } 105 | exports.Session = Session; 106 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | const asCSS = el => { 2 | const details = []; 3 | if (el.id) 4 | details.push(`#${el.id}`); 5 | else { 6 | const parent = el.closest('[id]'); 7 | const id = parent ? `#${parent.id} ` : ''; 8 | details.push(id + el.nodeName.toLowerCase()); 9 | } 10 | details.push.apply(details, el.classList); 11 | return details.join('.'); 12 | }; 13 | 14 | const transform = (name, pointerEvents) => 15 | (pointerEvents ? 'pointer' : 'mouse') + name; 16 | 17 | const types = { 18 | cancel: pointerEvents => transform('cancel', pointerEvents), 19 | down: pointerEvents => transform('down', pointerEvents), 20 | enter: pointerEvents => transform('enter', pointerEvents), 21 | leave: pointerEvents => transform('leave', pointerEvents), 22 | move: pointerEvents => transform('move', pointerEvents), 23 | out: pointerEvents => transform('out', pointerEvents), 24 | over: pointerEvents => transform('over', pointerEvents), 25 | up: pointerEvents => transform('up', pointerEvents) 26 | }; 27 | 28 | export class Session { 29 | constructor( 30 | // the node to search for data-telemetry 31 | root = document, 32 | // store only last mouse/pointermove event 33 | overwriteLastMove = true, 34 | // use pointer events instead of mouse events 35 | pointerEvents = typeof PointerEvent === 'function' 36 | ) { 37 | this.events = []; 38 | this.root = root; 39 | this.overwriteLastMove = overwriteLastMove; 40 | const elements = root.querySelectorAll('[data-telemetry]'); 41 | for (let i = 0, {length} = elements; i < length; i++) 42 | { 43 | const el = elements[i]; 44 | const telemetry = el.dataset.telemetry.split(/,[ \t\n\r]*/); 45 | for (let i = 0, {length} = telemetry; i < length; i++) 46 | { 47 | const type = telemetry[i]; 48 | if (type === 'all') { 49 | for (let key in el) { 50 | if (/^on/.test(key)) 51 | el.addEventListener(key, this, true); 52 | } 53 | } 54 | else { 55 | el.addEventListener( 56 | types.hasOwnProperty(type) ? 57 | types[type](pointerEvents) : type, 58 | this, 59 | true 60 | ); 61 | } 62 | } 63 | } 64 | } 65 | handleEvent(event) { 66 | const {overwriteLastMove, events} = this; 67 | const { 68 | target, 69 | type, 70 | key, 71 | pageX: x, pageY: y, 72 | isPrimary: primary, 73 | timeStamp: time, 74 | isTrusted 75 | } = event; 76 | if (!isTrusted) 77 | return; 78 | const {length} = events; 79 | const record = { 80 | target: asCSS(target), 81 | type, 82 | key, 83 | x, y, 84 | primary, 85 | time 86 | }; 87 | if (overwriteLastMove && length > 0 && /move$/.test(type)) 88 | { 89 | if (/move$/.test(events[length - 1].type)) 90 | { 91 | events[length - 1] = record; 92 | return; 93 | } 94 | } 95 | events.push(record); 96 | } 97 | toJSON() { 98 | return { 99 | root: asCSS(this.root), 100 | events: this.events 101 | }; 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var dataTelemetry = (function (exports) { 2 | 'use strict'; 3 | 4 | function _classCallCheck(instance, Constructor) { 5 | if (!(instance instanceof Constructor)) { 6 | throw new TypeError("Cannot call a class as a function"); 7 | } 8 | } 9 | 10 | function _defineProperties(target, props) { 11 | for (var i = 0; i < props.length; i++) { 12 | var descriptor = props[i]; 13 | descriptor.enumerable = descriptor.enumerable || false; 14 | descriptor.configurable = true; 15 | if ("value" in descriptor) descriptor.writable = true; 16 | Object.defineProperty(target, descriptor.key, descriptor); 17 | } 18 | } 19 | 20 | function _createClass(Constructor, protoProps, staticProps) { 21 | if (protoProps) _defineProperties(Constructor.prototype, protoProps); 22 | if (staticProps) _defineProperties(Constructor, staticProps); 23 | return Constructor; 24 | } 25 | 26 | var asCSS = function asCSS(el) { 27 | var details = []; 28 | if (el.id) details.push("#".concat(el.id));else { 29 | var parent = el.closest('[id]'); 30 | var id = parent ? "#".concat(parent.id, " ") : ''; 31 | details.push(id + el.nodeName.toLowerCase()); 32 | } 33 | details.push.apply(details, el.classList); 34 | return details.join('.'); 35 | }; 36 | 37 | var transform = function transform(name, pointerEvents) { 38 | return (pointerEvents ? 'pointer' : 'mouse') + name; 39 | }; 40 | 41 | var types = { 42 | cancel: function cancel(pointerEvents) { 43 | return transform('cancel', pointerEvents); 44 | }, 45 | down: function down(pointerEvents) { 46 | return transform('down', pointerEvents); 47 | }, 48 | enter: function enter(pointerEvents) { 49 | return transform('enter', pointerEvents); 50 | }, 51 | leave: function leave(pointerEvents) { 52 | return transform('leave', pointerEvents); 53 | }, 54 | move: function move(pointerEvents) { 55 | return transform('move', pointerEvents); 56 | }, 57 | out: function out(pointerEvents) { 58 | return transform('out', pointerEvents); 59 | }, 60 | over: function over(pointerEvents) { 61 | return transform('over', pointerEvents); 62 | }, 63 | up: function up(pointerEvents) { 64 | return transform('up', pointerEvents); 65 | } 66 | }; 67 | var Session = 68 | /*#__PURE__*/ 69 | function () { 70 | function Session() { 71 | var root = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document; 72 | var overwriteLastMove = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; 73 | var pointerEvents = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : typeof PointerEvent === 'function'; 74 | 75 | _classCallCheck(this, Session); 76 | 77 | this.events = []; 78 | this.root = root; 79 | this.overwriteLastMove = overwriteLastMove; 80 | var elements = root.querySelectorAll('[data-telemetry]'); 81 | 82 | for (var i = 0, length = elements.length; i < length; i++) { 83 | var el = elements[i]; 84 | var telemetry = el.dataset.telemetry.split(/,[ \t\n\r]*/); 85 | 86 | for (var _i = 0, _length = telemetry.length; _i < _length; _i++) { 87 | var type = telemetry[_i]; 88 | 89 | if (type === 'all') { 90 | for (var key in el) { 91 | if (/^on/.test(key)) el.addEventListener(key, this, true); 92 | } 93 | } else { 94 | el.addEventListener(types.hasOwnProperty(type) ? types[type](pointerEvents) : type, this, true); 95 | } 96 | } 97 | } 98 | } 99 | 100 | _createClass(Session, [{ 101 | key: "handleEvent", 102 | value: function handleEvent(event) { 103 | var overwriteLastMove = this.overwriteLastMove, 104 | events = this.events; 105 | var target = event.target, 106 | type = event.type, 107 | key = event.key, 108 | x = event.pageX, 109 | y = event.pageY, 110 | primary = event.isPrimary, 111 | time = event.timeStamp, 112 | isTrusted = event.isTrusted; 113 | if (!isTrusted) return; 114 | var length = events.length; 115 | var record = { 116 | target: asCSS(target), 117 | type: type, 118 | key: key, 119 | x: x, 120 | y: y, 121 | primary: primary, 122 | time: time 123 | }; 124 | 125 | if (overwriteLastMove && length > 0 && /move$/.test(type)) { 126 | if (/move$/.test(events[length - 1].type)) { 127 | events[length - 1] = record; 128 | return; 129 | } 130 | } 131 | 132 | events.push(record); 133 | } 134 | }, { 135 | key: "toJSON", 136 | value: function toJSON() { 137 | return { 138 | root: asCSS(this.root), 139 | events: this.events 140 | }; 141 | } 142 | }]); 143 | 144 | return Session; 145 | }(); 146 | 147 | exports.Session = Session; 148 | 149 | return exports; 150 | 151 | }({})); 152 | -------------------------------------------------------------------------------- /min.js: -------------------------------------------------------------------------------- 1 | var dataTelemetry=function(e){"use strict";function r(e,t){for(var n=0;n{const t=[];if(e.id)t.push(`#${e.id}`);else{const s=e.closest("[id]"),o=s?`#${s.id} `:"";t.push(o+e.nodeName.toLowerCase())}return t.push.apply(t,e.classList),t.join(".")},s=(e,t)=>(t?"pointer":"mouse")+e,o={cancel:e=>s("cancel",e),down:e=>s("down",e),enter:e=>s("enter",e),leave:e=>s("leave",e),move:e=>s("move",e),out:e=>s("out",e),over:e=>s("over",e),up:e=>s("up",e)};return e.Session=class{constructor(e=document,t=!0,s="function"==typeof PointerEvent){this.events=[],this.root=e,this.overwriteLastMove=t;const n=e.querySelectorAll("[data-telemetry]");for(let e=0,{length:t}=n;e0&&/move$/.test(r)&&/move$/.test(o[p-1].type)?o[p-1]=d:o.push(d)}toJSON(){return{root:t(this.root),events:this.events}}},e}({}); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "data-telemetry", 3 | "version": "0.1.2", 4 | "description": "A simple event tracker for user surfing sessions", 5 | "main": "cjs/index.js", 6 | "scripts": { 7 | "build": "npm run cjs && npm run rollup:new && npm run rollup:babel && npm run min && npm run test", 8 | "cjs": "ascjs esm cjs", 9 | "rollup:new": "rollup --config rollup/new.config.js", 10 | "rollup:babel": "rollup --config rollup/babel.config.js", 11 | "min": "uglifyjs index.js --support-ie8 --comments=/^!/ -c -m -o min.js", 12 | "coveralls": "cat ./coverage/lcov.info | coveralls", 13 | "test": "istanbul cover test/index.js" 14 | }, 15 | "keywords": [ 16 | "telemetry", 17 | "user", 18 | "browser", 19 | "navigation", 20 | "session" 21 | ], 22 | "author": "Andrea Giammarchi", 23 | "license": "ISC", 24 | "devDependencies": { 25 | "@babel/core": "^7.6.0", 26 | "@babel/preset-env": "^7.6.0", 27 | "ascjs": "^3.0.1", 28 | "basichtml": "^1.1.1", 29 | "istanbul": "^0.4.5", 30 | "rollup": "^1.21.4", 31 | "rollup-plugin-babel": "^4.3.3", 32 | "rollup-plugin-node-resolve": "^5.2.0", 33 | "rollup-plugin-terser": "^5.1.2", 34 | "uglify-js": "^3.6.0" 35 | }, 36 | "module": "esm/index.js", 37 | "unpkg": "min.js", 38 | "dependencies": {}, 39 | "repository": { 40 | "type": "git", 41 | "url": "git+https://github.com/WebReflection/data-telemetry.git" 42 | }, 43 | "bugs": { 44 | "url": "https://github.com/WebReflection/data-telemetry/issues" 45 | }, 46 | "homepage": "https://github.com/WebReflection/data-telemetry#readme" 47 | } 48 | -------------------------------------------------------------------------------- /rollup/babel.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import babel from 'rollup-plugin-babel'; 3 | 4 | export default { 5 | input: './esm/index.js', 6 | plugins: [ 7 | 8 | resolve({module: true}), 9 | babel({presets: ['@babel/preset-env']}) 10 | ], 11 | 12 | output: { 13 | exports: 'named', 14 | file: './index.js', 15 | format: 'iife', 16 | name: 'dataTelemetry' 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /rollup/new.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import {terser} from 'rollup-plugin-terser'; 3 | 4 | export default { 5 | input: './esm/index.js', 6 | plugins: [ 7 | 8 | resolve({module: true}), 9 | terser() 10 | ], 11 | 12 | output: { 13 | exports: 'named', 14 | file: './new.js', 15 | format: 'iife', 16 | name: 'dataTelemetry' 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('basichtml').init(); 2 | 3 | document.body.dataset.telemetry = 'cancel, down, enter, leave, move, out, over, up, click, keypress'; 4 | document.body.classList.add('test', 'more'); 5 | document.querySelectorAll = function () { 6 | return [document.body]; 7 | }; 8 | Object.prototype.closest = () => ({id: 'parent'}); 9 | 10 | const {Session} = require('../cjs'); 11 | 12 | let session = new Session(); 13 | 14 | let event = document.createEvent('Event'); 15 | event.initEvent('click', false, false); 16 | document.body.dispatchEvent(event); 17 | 18 | event.isTrusted = true; 19 | document.body.dispatchEvent(event); 20 | 21 | document.body.id = 'da-body'; 22 | document.body.dispatchEvent(event); 23 | 24 | event = document.createEvent('Event'); 25 | event.initEvent('move', false, false); 26 | event.isTrusted = true; 27 | event.target = document.body; 28 | session.handleEvent(event); 29 | 30 | session = new Session(document, true, true); 31 | 32 | session.handleEvent(event); 33 | session.handleEvent(event); 34 | 35 | const all = JSON.parse(JSON.stringify(session)); 36 | console.log(all); 37 | console.assert(all.events.length === 1, 'only one move event expected'); 38 | 39 | const result = all.events.pop(); 40 | console.assert(result.target === '#da-body.test.more', 'correct target'); 41 | console.assert(result.type === 'move', 'correct type'); 42 | console.assert(/^[0-9.]+$/.test(result.time), 'correct time'); 43 | 44 | document.body.dataset.telemetry = 'all'; 45 | document.body.onwhatever = null; 46 | session = new Session(); 47 | --------------------------------------------------------------------------------