├── .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 | [](https://travis-ci.com/WebReflection/data-telemetry) [](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 |
38 | send stuff
39 |
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 |
--------------------------------------------------------------------------------