├── .dockerignore
├── .gitignore
├── src
├── functions
│ ├── toArray.js
│ ├── encodeObject.js
│ ├── packSemVer.js
│ ├── removeWww.js
│ ├── runOnStop.js
│ ├── autoDomain.js
│ ├── once.js
│ ├── isValidUid.js
│ ├── type.js
│ ├── stringHash.js
│ ├── escapeBase64.js
│ ├── getOpenStatMarks.js
│ ├── addEventListener.js
│ ├── each.js
│ ├── simpleHash.js
│ ├── msgCropper.js
│ ├── nextTick.js
│ ├── arrayIndexOf.js
│ ├── objectAssing.js
│ ├── when.js
│ ├── objectKeys.js
│ ├── createLogger.js
│ ├── btoa.js
│ ├── arrayIncludes.js
│ ├── cyrb53.js
│ ├── domEvents.js
│ └── pageSource.js
├── Browser.js
├── data
│ ├── uids.js
│ ├── performance.js
│ ├── pageDefaults.js
│ ├── browserCharacts.js
│ └── browserData.js
├── SelfishPerson.js
├── syncs
│ ├── YandexMetrika.js
│ └── GoogleAnalytics.js
├── index.js
├── trackers
│ ├── BrowserEventsTracker.js
│ ├── PageTracker.js
│ ├── ClickTracker.js
│ ├── FormTracker.js
│ ├── ActivityTracker.js
│ └── SessionTracker.js
├── extensions
│ └── web-push
│ │ ├── controller.js
│ │ └── lib.js
├── Constants.js
├── polyfill.js
├── lib
│ └── dom-utils
│ │ └── index.js
├── CookieStorageAdapter.js
├── LocalStorageAdapter.js
├── Transport.js
└── Tracker.js
├── .bumpversion.cfg
├── webpack.development.js
├── .editorconfig
├── Dockerfile
├── snippet
└── snippet.html
├── webpack.production.js
├── makefile
├── webpack.common.js
├── .travis.yml
├── README.md
├── package.json
└── LICENSE
/.dockerignore:
--------------------------------------------------------------------------------
1 | /.git
2 | /node_modules/
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | yarn-error.log
3 | dist
4 | .vscode
5 | .DS_Store
6 | .npmrc
--------------------------------------------------------------------------------
/src/functions/toArray.js:
--------------------------------------------------------------------------------
1 | export default function toArray(list) {
2 |
3 | if(!list) return;
4 | return Array.prototype.slice.call(list);
5 |
6 | }
7 |
--------------------------------------------------------------------------------
/src/functions/encodeObject.js:
--------------------------------------------------------------------------------
1 | import CBOR from 'cbor-js';
2 | import escapeBase64 from './escapeBase64';
3 |
4 | export default function encode(object) {
5 |
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/src/functions/packSemVer.js:
--------------------------------------------------------------------------------
1 | export function packSemVer(val) {
2 | const [major, minor, patch] = val.split('.');
3 | return Number(major || 0) * 1000000 + Number(minor || 0) * 1000 + Number(patch || 0);
4 | }
5 |
--------------------------------------------------------------------------------
/src/functions/removeWww.js:
--------------------------------------------------------------------------------
1 | export default function(domain){
2 | if (!domain) return domain;
3 | if (domain.substr(0, 4) === 'www.'){
4 | domain = domain.substr(4, domain.length - 4);
5 | }
6 | return domain;
7 | };
8 |
--------------------------------------------------------------------------------
/src/functions/runOnStop.js:
--------------------------------------------------------------------------------
1 | export default function runOnFinish(cb, wait) {
2 | let timeout;
3 | return function (...args) {
4 | clearTimeout(timeout);
5 | timeout = setTimeout(() => cb(...args), wait);
6 | };
7 | }
8 |
--------------------------------------------------------------------------------
/.bumpversion.cfg:
--------------------------------------------------------------------------------
1 | [bumpversion]
2 | current_version = 5.1.1
3 | commit = True
4 | tag = True
5 |
6 | [bumpversion:file:package.json]
7 |
8 | [bumpversion:file:src/Tracker.js]
9 |
10 | [bumpversion:file:snippet/snippet.html]
11 |
--------------------------------------------------------------------------------
/src/Browser.js:
--------------------------------------------------------------------------------
1 | export const nav = navigator;
2 | /** @type {Window} */
3 | export const win = window;
4 | /** @type {Document} */
5 | export const doc = document;
6 | /** @type {Document} */
7 | export const html = document.documentElement;
8 |
9 | export const body = document.body;
10 |
--------------------------------------------------------------------------------
/src/functions/autoDomain.js:
--------------------------------------------------------------------------------
1 | import removeWww from './removeWww';
2 |
3 | export default function (domain) {
4 |
5 | domain = removeWww(domain);
6 | if (!domain) return;
7 |
8 | const parts = domain.split('.');
9 | return parts.slice(parts.length > 2 ? 1 : 0).join('.')
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/src/data/uids.js:
--------------------------------------------------------------------------------
1 | // function getCryptoRandom(){
2 | // if (crypto){
3 | // crypto.getRandomValues()
4 | // }
5 | // }
6 |
7 |
8 |
9 |
10 | // export function uidsExperiments(params) {
11 |
12 |
13 |
14 |
15 | // return {"uids": {
16 |
17 | // }};
18 | // }
19 |
20 |
--------------------------------------------------------------------------------
/src/functions/once.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Запускает функцию лишь однажды
3 | * @param cb
4 | * @return {Function}
5 | */
6 | export default function once(cb) {
7 | let called = false;
8 | return function (...args) {
9 | if (called) return;
10 | called = true;
11 | cb(...args);
12 | };
13 | }
14 |
--------------------------------------------------------------------------------
/webpack.development.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const config = require('./webpack.common');
3 | // const DashboardPlugin = require('webpack-dashboard/plugin');
4 |
5 | config.devtool = 'source-map'
6 | // config.plugins.push(new DashboardPlugin());
7 |
8 | module.exports = config;
9 |
10 |
--------------------------------------------------------------------------------
/src/functions/isValidUid.js:
--------------------------------------------------------------------------------
1 | const uidRegexp = /^\d{10,25}$/;
2 | const uidRE = new RegExp('^[0-9]{10,22}$');
3 |
4 | export function isValidUid(uid) {
5 | if (!uid) return false;
6 | return uidRE.test(uid);
7 | };
8 |
9 | export function cleanUid(uid) {
10 | return isValidUid(uid) ? uid : undefined;
11 | }
12 |
--------------------------------------------------------------------------------
/src/functions/type.js:
--------------------------------------------------------------------------------
1 |
2 | export function isArray(arg) {
3 |
4 | return Array.isArray
5 | ? Array.isArray(arg)
6 | : Object.prototype.toString.call(arg) === '[object Array]';
7 |
8 | }
9 |
10 | export function isObject(obj) {
11 | return typeof obj === 'object' && obj !== 'function' && obj !== null;
12 | }
13 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # http://editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | indent_style = space
9 | indent_size = 2
10 | end_of_line = lf
11 | charset = utf-8
12 | trim_trailing_whitespace = true
13 | insert_final_newline = true
14 |
--------------------------------------------------------------------------------
/src/functions/stringHash.js:
--------------------------------------------------------------------------------
1 | export function hashCode(str) {
2 | var hash = 0,
3 | i, chr;
4 | if (str.length === 0) return hash;
5 | for (i = 0; i < str.length; i++) {
6 | chr = str.charCodeAt(i);
7 | hash = ((hash << 5) - hash) + chr;
8 | hash |= 0; // Convert to 32bit integer
9 | }
10 | return hash + 2147483647 + 1;
11 | };
12 |
--------------------------------------------------------------------------------
/src/functions/escapeBase64.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Makes base64 url safe
3 | * @param base64 {string}
4 | * @return {string}
5 | */
6 | export default function es(base64) {
7 |
8 | return base64.toString('base64')
9 | .replace(/\+/g, '-') // Convert '+' to '-'
10 | .replace(/\//g, '_') // Convert '/' to '_'
11 | .replace(/=+$/, ''); // Remove ending '='
12 | }
13 |
--------------------------------------------------------------------------------
/src/functions/getOpenStatMarks.js:
--------------------------------------------------------------------------------
1 | import {win} from "../Browser";
2 |
3 | export default function(value) {
4 | if(!value) return;
5 |
6 | if(value.indexOf(';') === -1 && win.atob) {
7 | value = win.atob(value);
8 | }
9 | value = value.split(';');
10 | return {
11 | os_source: value[0],
12 | os_campaign: value[1],
13 | os_content: value[2],
14 | os_place: value[3]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/SelfishPerson.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const STORE_KEY = 'slfsh';
4 |
5 | class SelfishPerson {
6 |
7 | constructor(tracker, options) {
8 | this.storage = tracker.localStorage;
9 | }
10 |
11 | saveConfig(options){
12 | this.storage.set(STORE_KEY, options);
13 | }
14 |
15 | getConfig(){
16 | return this.storage.get(STORE_KEY, {}, {});
17 | }
18 | }
19 |
20 | module.exports = SelfishPerson;
21 |
--------------------------------------------------------------------------------
/src/functions/addEventListener.js:
--------------------------------------------------------------------------------
1 | export default function addEventListener(element, eventType, eventHandler, useCapture) {
2 | if (element.addEventListener) {
3 | return element.addEventListener(eventType, eventHandler, useCapture);
4 | }
5 | else if (element.attachEvent) {
6 | return element.attachEvent('on' + eventType, eventHandler);
7 | }
8 | else {
9 | element['on' + eventType] = eventHandler;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/functions/each.js:
--------------------------------------------------------------------------------
1 | import {isObject, isArray} from './type';
2 |
3 | export default function each(arg, cb) {
4 | if(!arg || !cb) return;
5 | if(isArray(arg)){
6 | for (let i = 0; i < arg.length; i++) {
7 | cb(arg[i], i);
8 | }
9 | } else if (isObject(arg)){
10 | for (const key in arg) {
11 | if (arg.hasOwnProperty(key)) {
12 | cb(key, arg[key]);
13 | }
14 | }
15 | }
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/src/functions/simpleHash.js:
--------------------------------------------------------------------------------
1 | export default function(s) {
2 | /* Simple hash function. */
3 | let a = 1, c = 0, h, o;
4 | if (s) {
5 | a = 0;
6 | /*jshint plusplus:false bitwise:false*/
7 | for (h = s.length - 1; h >= 0; h--) {
8 | o = s.charCodeAt(h);
9 | a = (a<<6&268435455) + o + (o<<14);
10 | c = a & 266338304;
11 | a = c!==0?a^c>>21:a;
12 | }
13 | }
14 | return String(a);
15 | };
16 |
--------------------------------------------------------------------------------
/src/functions/msgCropper.js:
--------------------------------------------------------------------------------
1 | import {isArray} from './type';
2 | import each from './each';
3 |
4 | export default function msgCropper(msg, msgCropSchema) {
5 | const result = {};
6 | each(msgCropSchema, function (key, val) {
7 | if(!msg[key]) return;
8 | if (val === true) {
9 | result[key] = msg[key];
10 | } else if (isArray(val) ) {
11 | result[key] = {};
12 | each(val, function (key2) {
13 | if(!msg[key][key2]) return;
14 | result[key][key2] = msg[key][key2];
15 | })
16 | }
17 | });
18 | return result;
19 | }
20 |
--------------------------------------------------------------------------------
/src/functions/nextTick.js:
--------------------------------------------------------------------------------
1 | const callable = function (fn) {
2 | if (typeof fn !== 'function') throw new TypeError(fn + " is not a function");
3 | return fn;
4 | };
5 |
6 | export default (function () {
7 | // W3C Draft
8 | // http://dvcs.w3.org/hg/webperf/raw-file/tip/specs/setImmediate/Overview.html
9 | if (typeof setImmediate === 'function') {
10 | return function (cb) { setImmediate(callable(cb)); };
11 | }
12 |
13 | // Wide available standard
14 | if ((typeof setTimeout === 'function') || (typeof setTimeout === 'object')) {
15 | return function (cb) { setTimeout(callable(cb), 0); };
16 | }
17 |
18 | return null;
19 | }());
20 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20.14-alpine as builder
2 |
3 | WORKDIR /build
4 |
5 | ARG NPM_CONFIG_REGISTRY_ARG=https://registry.npmjs.org
6 | ENV NPM_CONFIG_REGISTRY=$NPM_CONFIG_REGISTRY_ARG
7 |
8 | COPY package.json .
9 | RUN npm i --loglevel http --platform=linux && npm cache clean --force
10 |
11 | COPY . .
12 | ENV NODE_ENV production
13 |
14 | RUN npm run build && rm -rf node_modules
15 |
16 | FROM alpine:3.18
17 |
18 | ARG NPM_CONFIG_REGISTRY_ARG=https://registry.npmjs.org
19 | ENV NPM_CONFIG_REGISTRY=$NPM_CONFIG_REGISTRY_ARG
20 |
21 | VOLUME /usr/share/web-sdk
22 | WORKDIR /usr/share/web-sdk
23 |
24 | COPY --from=builder /build /usr/share/web-sdk
25 |
26 | CMD ["/bin/ls", "/usr/share/web-sdk/"]
27 |
--------------------------------------------------------------------------------
/src/data/performance.js:
--------------------------------------------------------------------------------
1 | /**
2 | * https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming
3 | */
4 | import {win} from "../Browser";
5 |
6 | const perf = win.performance;
7 |
8 | export default function () {
9 |
10 | const timing = perf && perf.timing;
11 |
12 | if (!timing) return;
13 | const cs = timing.connectStart;
14 | const dc = timing.domComplete;
15 | const scs = timing.secureConnectionStart;
16 |
17 |
18 | return {
19 | cs: 0,
20 | ce: timing.connectEnd - cs,
21 | scs: scs ? scs - cs : -1,
22 | rqs: timing.requestStart - cs,
23 | rss: timing.responseStart - cs,
24 | rse: timing.responseEnd - cs,
25 | dl: timing.domLoading - cs,
26 | di: timing.domInteractive - cs,
27 | dc: dc ? dc - cs : -1,
28 | };
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/src/functions/arrayIndexOf.js:
--------------------------------------------------------------------------------
1 | export default function arrayIndexOf(array, searchElement, startFrom) {
2 |
3 | if (array.indexOf) {
4 | return array.indexOf(searchElement, startFrom);
5 | }
6 |
7 | if (this === undefined || this === null) {
8 | throw new TypeError(this + ' is not an object');
9 | }
10 |
11 | const arraylike = array instanceof String ? this.split('') : array;
12 | const length = Math.max(Math.min(arraylike.length, 9007199254740991), 0) || 0;
13 | let index = startFrom || 0;
14 |
15 | index = (index < 0 ? Math.max(length + index, 0) : index) - 1;
16 |
17 | while (++index < length) {
18 | if (index in arraylike && arraylike[index] === searchElement) {
19 | return index;
20 | }
21 | }
22 |
23 | return -1;
24 | };
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/functions/objectAssing.js:
--------------------------------------------------------------------------------
1 | import objectKeys from './objectKeys';
2 |
3 | export default Object.assign ? Object.assign : function(target, firstSource) {
4 |
5 | if (target === undefined || target === null) {
6 | return;
7 | }
8 |
9 | const to = Object(target);
10 | for (let i = 1; i < arguments.length; i++) {
11 | let nextSource = arguments[i];
12 | if (nextSource === undefined || nextSource === null) {
13 | continue;
14 | }
15 |
16 | const keysArray = objectKeys(Object(nextSource));
17 | for (let nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
18 | const nextKey = keysArray[nextIndex];
19 | const desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
20 | if (desc !== undefined && desc.enumerable) {
21 | to[nextKey] = nextSource[nextKey];
22 | }
23 | }
24 | }
25 | return to;
26 | }
27 |
--------------------------------------------------------------------------------
/src/functions/when.js:
--------------------------------------------------------------------------------
1 | import nextTick from './nextTick';
2 |
3 | /**
4 | * Loop on a short interval until `condition()` is true, then call `fn`.
5 | *
6 | * @param {Function} condition
7 | * @param {Function} fn
8 | * @param {number} [interval=25]
9 | * @param {number} [attemps=-1]
10 | */
11 |
12 | export default function (condition, fn, interval, attemps) {
13 | if (typeof condition !== 'function') throw new Error('condition must be a function');
14 | if (typeof fn !== 'function') throw new Error('fn must be a function');
15 | attemps = attemps || -1;
16 |
17 | if (condition()) return nextTick(fn);
18 |
19 | const counter = () => {
20 | return attemps < 0 || attemps > 0
21 | };
22 |
23 | const ref = setInterval(function () {
24 | if (!counter()) return clearInterval(ref);
25 | if (!condition()) return;
26 | nextTick(fn);
27 | clearInterval(ref);
28 | }, interval || 100);
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/src/syncs/YandexMetrika.js:
--------------------------------------------------------------------------------
1 | import createLogger from '../functions/createLogger';
2 | import Emitter from 'component-emitter';
3 | import when from '../functions/when';
4 | import {win} from '../Browser';
5 | import {
6 | INTERNAL_EVENT,
7 | EVENT_USER_PARAMS
8 | } from "../Constants";
9 |
10 | const log = createLogger('Alco YM');
11 |
12 | const YandexMetrika = function () {
13 |
14 | // Waiting YM load
15 | when(() => win.Ya && (win.Ya.Metrika || win.Ya.Metrika2) && win.Ya._metrika && win.Ya._metrika.counter, () => {
16 | try {
17 | // Getting YM ClientId
18 | const ymId = win.Ya._metrika.counter.getClientID();
19 |
20 | if (ymId) {
21 | this.emit(INTERNAL_EVENT, EVENT_USER_PARAMS, {ymId});
22 | }
23 |
24 | } catch (e) {
25 | log.error('Error:', e)
26 | }
27 | }, 25, 40);
28 |
29 | };
30 |
31 | Emitter(YandexMetrika.prototype);
32 |
33 | export default YandexMetrika;
34 |
--------------------------------------------------------------------------------
/src/data/pageDefaults.js:
--------------------------------------------------------------------------------
1 | import {
2 | win,
3 | doc,
4 | html,
5 | body
6 | } from "../Browser";
7 | import urlParse from "url-parse";
8 |
9 | export function getScheme() {
10 | const proto = win.location.protocol;
11 | return proto.substr(0, proto.length - 1)
12 | }
13 |
14 | export function pageDefaults(params) {
15 |
16 | params = params || {};
17 | params.short = params.short || false;
18 |
19 | const loc = win.location;
20 | const pageUrl = loc.href;
21 | const parsed = urlParse(pageUrl);
22 |
23 | return params.short ? {
24 | title: doc.title,
25 | ref: doc.referrer,
26 | url: pageUrl
27 | } : {
28 | title: doc.title,
29 | path: parsed.pathname,
30 | ref: doc.referrer,
31 | url: pageUrl,
32 | query: parsed.query,
33 | domain: parsed.hostname,
34 | scheme: getScheme()
35 | };
36 | }
37 |
38 | export function isHttps() {
39 | return getScheme() === 'https';
40 | }
41 |
--------------------------------------------------------------------------------
/src/functions/objectKeys.js:
--------------------------------------------------------------------------------
1 | export default Object.keys || (function () {
2 | const hasOwnProperty = Object.prototype.hasOwnProperty;
3 | const hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString');
4 | const dontEnums = [
5 | 'toString',
6 | 'toLocaleString',
7 | 'valueOf',
8 | 'hasOwnProperty',
9 | 'isPrototypeOf',
10 | 'propertyIsEnumerable',
11 | 'constructor'
12 | ];
13 | const dontEnumsLength = dontEnums.length;
14 |
15 | return function (obj) {
16 | if (typeof obj !== 'function' && (typeof obj !== 'object' || obj === null)) {
17 | throw new TypeError('Object.keys called on non-object');
18 | }
19 |
20 | const result = [];
21 |
22 | for (const prop in obj) {
23 | if (hasOwnProperty.call(obj, prop)) {
24 | result.push(prop);
25 | }
26 | }
27 |
28 | if (hasDontEnumBug) {
29 | for (let i = 0; i < dontEnumsLength; i++) {
30 | if (hasOwnProperty.call(obj, dontEnums[i])) {
31 | result.push(dontEnums[i]);
32 | }
33 | }
34 | }
35 | return result;
36 | };
37 | }());
38 |
--------------------------------------------------------------------------------
/src/functions/createLogger.js:
--------------------------------------------------------------------------------
1 | import {win} from '../Browser';
2 |
3 | const isProd = PRODUCTION;
4 |
5 |
6 | const logger = function (type, arr, prefix) {
7 | if (('console' in win) && (type in console)) {
8 |
9 | // Sending to remote log
10 | const call = Function.prototype.call;
11 | call.apply(call, [console[type], console].concat(prefix ? [prefix] : [])
12 | .concat(arr));
13 | }
14 | };
15 |
16 | export default function createLogger(name) {
17 |
18 | const prefix = () => {
19 | const time = (new Date()).toISOString().substring(11);
20 | return `${time} ${name}:`;
21 | };
22 |
23 | const log = function (...args) {
24 | if (!isProd || win._rst4_logger) {
25 | logger('info', args, prefix());
26 | }
27 | };
28 |
29 | log.info = function (...args) {
30 | if (!isProd || win._rst4_logger) {
31 | logger('info', args, prefix());
32 | }
33 | };
34 |
35 | log.warn = function (...args) {
36 | logger('warn', args, prefix());
37 | };
38 |
39 | log.error = (...args) => {
40 | logger('error', args, prefix());
41 | };
42 |
43 | return log;
44 | }
45 |
--------------------------------------------------------------------------------
/snippet/snippet.html:
--------------------------------------------------------------------------------
1 |
21 |
26 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import './polyfill';
2 | import "core-js/stable";
3 | import "regenerator-runtime/runtime";
4 |
5 |
6 |
7 | import {
8 | documentReady
9 | } from './functions/domEvents';
10 | import {
11 | win
12 | } from './Browser';
13 | import Tracker from './Tracker';
14 | import { packSemVer } from './functions/packSemVer';
15 |
16 | const wk = 'rstat4';
17 |
18 | if (win[wk]) {
19 | const holder = win[wk];
20 | if (!holder._loaded) {
21 | const tracker = new Tracker();
22 | tracker.configure({
23 | snippet: packSemVer(`${holder._sv}`)
24 | });
25 | // Attaching method to page
26 | const doCall = function (args) {
27 | args = args.slice(0);
28 | const method = args.shift();
29 | return tracker[method] ?
30 | tracker[method].apply(tracker, args) :
31 | // TODO: log errors to sentry if available
32 | console && console.warn && console.warn('called rockstat undefined method');
33 | };
34 |
35 | holder._q.map(doCall);
36 | holder.doCall = doCall;
37 | holder._loaded = true;
38 | holder._q = [];
39 | documentReady(() => {
40 | tracker.initialize();
41 | });
42 | } else {
43 | console && console.warn && console.warn('rockstat already loaded');
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/functions/btoa.js:
--------------------------------------------------------------------------------
1 | var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
2 | function InvalidCharacterError(message) {
3 | this.message = message;
4 | }
5 | InvalidCharacterError.prototype = new Error;
6 | InvalidCharacterError.prototype.name = 'InvalidCharacterError';
7 |
8 | // encoder
9 | // [https://gist.github.com/999166] by [https://github.com/nignag]
10 |
11 | export const bToA = (function () {
12 | window.btoa || function (input) {
13 | var str = String(input);
14 | for (
15 | // initialize result and counter
16 | var block, charCode, idx = 0, map = chars, output = '';
17 | // if the next str index does not exist:
18 | // change the mapping table to "="
19 | // check if d has no fractional digits
20 | str.charAt(idx | 0) || (map = '=', idx % 1);
21 | // "8 - idx % 1 * 8" generates the sequence 2, 4, 6, 8
22 | output += map.charAt(63 & block >> 8 - idx % 1 * 8)
23 | ) {
24 | charCode = str.charCodeAt(idx += 3 / 4);
25 | if (charCode > 0xFF) {
26 | throw new InvalidCharacterError("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");
27 | }
28 | block = block << 8 | charCode;
29 | }
30 | return output;
31 | }
32 | })();
33 |
--------------------------------------------------------------------------------
/webpack.production.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const config = require('./webpack.common');
3 | // const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
4 | // const MinifyPlugin = require("babel-minify-webpack-plugin");
5 | const TerserPlugin = require('terser-webpack-plugin');
6 |
7 |
8 | // const CleanWebpackPlugin = require('clean-webpack-plugin');
9 | // const MinifyPlugin = require("babel-minify-webpack-plugin");
10 |
11 | const minifyOpts = {}
12 | const pluginOpts = {}
13 |
14 | config.plugins = config.plugins.concat([
15 | // new MinifyPlugin(minifyOpts, pluginOpts)
16 | // new UglifyJSPlugin({
17 | // uglifyOptions: {
18 | // // ie8: false,
19 | // // ecma: 5,
20 | // // mangle: true,
21 | // // warnings: true,
22 | // output: {
23 | // // comments: 'all',
24 | // // beautify: false
25 | // },
26 | // }
27 | // }),
28 | // new CleanWebpackPlugin([config.output.path]),
29 | // new MinifyPlugin({
30 | // mangle: true, // <- option not yet supported
31 | // deadcode: true, // <- option not yet supported
32 | // comments: false,
33 | // })
34 | ]);
35 |
36 |
37 | config.optimization = {
38 | minimizer: [new TerserPlugin()],
39 | nodeEnv: 'production',
40 | },
41 |
42 | config.mode = 'production'
43 |
44 | module.exports = config;
45 |
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | BR := $(shell git branch | grep \* | cut -d ' ' -f2-)
2 | bump-patch:
3 | bumpversion patch
4 |
5 | bump-minor:
6 | bumpversion minor
7 |
8 | to_master:
9 | @echo $(BR)
10 | git checkout master && git merge $(BR) && git checkout $(BR)
11 |
12 | push:
13 | git push origin master --tags
14 |
15 | travis-trigger:
16 | curl -vv -s -X POST \
17 | -H "Content-Type: application/json" \
18 | -H "Accept: application/json" \
19 | -H "Travis-API-Version: 3" \
20 | -H "Authorization: token $$TRAVIS_TOKEN" \
21 | -d '{ "request": { "branch":"$(br)" }}' \
22 | https://api.travis-ci.com/repo/$(subst $(DEL),$(PERCENT)2F,$(repo))/requests
23 |
24 | # common
25 | # --build-arg NPM_CONFIG_REGISTRY_ARG=http://host.docker.internal:4873/
26 |
27 | build:
28 |
29 | docker build --platform linux/amd64 -t web-sdk .
30 |
31 | # latest
32 |
33 | tag-latest:
34 | docker tag web-sdk rockstat/web-sdk:latest
35 |
36 | push-latest:
37 | docker push rockstat/web-sdk:latest
38 |
39 |
40 | # ng
41 |
42 | tag-ng:
43 | docker tag web-sdk rockstat/web-sdk:ng
44 |
45 | push-ng:
46 | docker push rockstat/web-sdk:ng
47 |
48 | all-ng: build tag-ng push-ng
49 |
50 |
51 | #ng-dev
52 |
53 | build-ng-dev:
54 | docker build -t web-sdk:ng-dev .
55 |
56 | tag-ng-dev:
57 | docker tag web-sdk rockstat/web-sdk:ng-dev
58 |
59 | push-ng-dev:
60 | docker push rockstat/web-sdk:ng-dev
61 |
62 | all-ng-dev: build-ng-dev tag-ng-dev push-ng-dev
63 |
64 |
--------------------------------------------------------------------------------
/src/functions/arrayIncludes.js:
--------------------------------------------------------------------------------
1 | export default (() => {
2 | Array.prototype.includes || function (searchElement, fromIndex) {
3 |
4 | if (this == null) {
5 | throw new TypeError('"this" is null or not defined');
6 | }
7 |
8 | // 1. Let O be ? ToObject(this value).
9 | var o = Object(this);
10 |
11 | // 2. Let len be ? ToLength(? Get(O, "length")).
12 | var len = o.length >>> 0;
13 |
14 | // 3. If len is 0, return false.
15 | if (len === 0) {
16 | return false;
17 | }
18 |
19 | // 4. Let n be ? ToInteger(fromIndex).
20 | // (If fromIndex is undefined, this step produces the value 0.)
21 | var n = fromIndex | 0;
22 |
23 | // 5. If n ≥ 0, then
24 | // a. Let k be n.
25 | // 6. Else n < 0,
26 | // a. Let k be len + n.
27 | // b. If k < 0, let k be 0.
28 | var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
29 |
30 | function sameValueZero(x, y) {
31 | return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y));
32 | }
33 |
34 | // 7. Repeat, while k < len
35 | while (k < len) {
36 | // a. Let elementK be the result of ? Get(O, ! ToString(k)).
37 | // b. If SameValueZero(searchElement, elementK) is true, return true.
38 | if (sameValueZero(o[k], searchElement)) {
39 | return true;
40 | }
41 | // c. Increase k by 1.
42 | k++;
43 | }
44 |
45 | // 8. Return false
46 | return false;
47 | }
48 | })();
49 |
--------------------------------------------------------------------------------
/webpack.common.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const isProduction = (process.env.NODE_ENV === 'production');
4 |
5 | module.exports = {
6 | entry: './src/index.js',
7 | target: 'web',
8 | output: {
9 | path: __dirname + '/dist',
10 | filename: isProduction ? 'lib.js' : 'lib-dev.js'
11 | },
12 | module: {
13 | rules: [
14 | {
15 | test: /\.js$/,
16 | include: [
17 | // path.resolve(__dirname, 'src', 'lib', 'dom-utils'),
18 | path.resolve(__dirname, 'src'),
19 |
20 | ],
21 | use: {
22 | loader: "babel-loader",
23 | options: {
24 | presets: [
25 | ['@babel/preset-env', {
26 | targets: {
27 | browsers: ['>0.5%, not dead'],
28 | // browsers: []
29 | // "chrome": "60"
30 | },
31 | "useBuiltIns": "entry",
32 | // "corejs":"3.22",
33 | corejs: { version: 3, proposals: true },
34 | }]
35 | ],
36 | // plugins: ['@babel/plugin-transform-runtime']
37 | }
38 | }
39 | },
40 | ]
41 | },
42 | // stats: 'verbose',
43 | // stats: {
44 | // colors: true,
45 | // modules: true,
46 | // maxModules: 35,
47 | // performance: true,
48 | // },
49 | plugins: [
50 | new webpack.DefinePlugin({
51 | 'PRODUCTION': JSON.stringify(isProduction)
52 | }),
53 | ]
54 | };
55 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | dist: bionic
3 | language: node_js
4 | node_js:
5 | - 12
6 | branches:
7 | only:
8 | - master
9 | - dev
10 | env:
11 | global:
12 | - PROJECT_NAME=web-sdk
13 | - ORG=rockstat
14 | - IMAGE_NAME=$ORG/$PROJECT_NAME
15 | - TAG="$( [ $TRAVIS_BRANCH == 'master' ] && echo latest || echo $TRAVIS_BRANCH)"
16 | - secure: nRdqm6bygUw4FkKVxEQfjkpCppXEJ25tHQC7k6G9Yz4uwEEvn292I8FdoBgwVqI6E6luAh1IM/KlLQgo3c+XJAkjq75JdMN1YCVojRp27IkSyUXnSYYKtSrKkmfi2nexCQ060K+Cmy2dysEn1VM6Z7uRQl03dxPcyqQtbAFUl3qT6Ef9yMfUR5FGP+NUlrr+yS+/kIvfImZdEVPg/QpVYjuRUmJlXEnD7TZCl5vx7clkZPkcwtzc+q/5HLsjjHWhgdlL9pxsnGf9XWO1hWTGQes1xEjrTPkeYiPU+X6czDtsBsJrSivkBQTlDHoqk0jUFgIhSQJs+P8VTI5hrfZFJ6bvWn1y/2mrPGLG0+TujRiNJk3rZuWivQXWNZDDmVnzr50Gws0VNIb9rxFxaiOUvtSyf4OlsxPXUd/IX4aNGF0DvCQ5MWNne9879S+1/YPlONz/STH3JILIuK/KKrbEDggJHbbUWCQuOwWFOh3vQcXLl3b+LPzzNOyP9i9u4CPLC/kR+qqtcyLNVzC6AVsy3+H8KX30T1LxhyyNkfAMJdcZ2yVn/QA2G9r0nIfDO8c4NlWLqQBjB4qSwmzHodfa/jcxjXkJLwisf673Ps0O62xH01UR+uAwZqyN9iTT+TOmJjYxCcCRdf+1VBM5M/6VNeDXkv4G/0C8o4Tf5GNh/7w=
17 | services:
18 | - docker
19 | script:
20 | - cat Dockerfile | sed "s/\:latest/\:$TAG/g" > Dockerfile.tag
21 | - docker build -t $PROJECT_NAME -f Dockerfile.tag .
22 | after_script:
23 | - docker images
24 | before_deploy:
25 | - docker tag "$PROJECT_NAME" "$IMAGE_NAME:$TAG"
26 | - docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD"
27 | deploy:
28 | provider: script
29 | skip_cleanup: true
30 | script: docker push "$IMAGE_NAME:$TAG"
31 | on:
32 | all_branches: true
33 | after_deploy:
34 | - make repo=rockstat/front br=$TRAVIS_BRANCH travis-trigger
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Browser trackinkg library for Rockstat analytics and marketing automation platform
2 |
3 | It automaticaly tracks most of users actions and interratons: pages, clicks, forms, scroll, activity
4 | and send data to server usign xhr/beacon/websocket/image transports.
5 | Calcultating sessions based on local storage
6 |
7 | ## About Rockstat
8 |
9 | Is an open source platform for a web and product analytics.
10 | It consists of a set of components: JavaScript tracking client for web applications;
11 | server-side data collector; services for geo-coding and detecting client device type;
12 | a new server deployment system.
13 | [Read more](https://rockstat.ru/about)
14 |
15 | 
16 |
17 |
18 | ## Useful links
19 |
20 | - https://github.com/pierrec/js-cuint
21 | - https://github.com/bryc/code
22 |
23 |
24 | - https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html
25 |
26 |
27 | Reliability Problem
28 |
29 | The above methods all suffer from reliability problems, stemming from one core issue: There is not an ideal time in a page’s lifecycle to make the JavaScript call to send out the beacon.
30 |
31 | - unload and beforeunload are unreliable, and outright ignored by several major browsers.
32 | - pagehide and visibilitychange have issues on mobile platforms.
33 |
34 | https://github.com/WICG/pending-beacon
35 |
36 |
37 | ## Thanks
38 |
39 | - [BrowserStack](https://www.browserstack.com): great tool for manual and automated testing in browser
40 |
41 |
42 | ## License
43 |
44 |
45 | [LICENSE](LICENSE)
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@rockstat/web_sdk",
3 | "version": "5.1.1",
4 | "description": "Browser tracking library for Rockstat platform",
5 | "main": "dist/lib.js",
6 | "scripts": {
7 | "start:dev": "webpack --config webpack.development.js --mode development --watch",
8 | "dash": "webpack-dashboard",
9 | "build": "NODE_ENV=production webpack --config webpack.production.js --mode production",
10 | "build:dev": "NODE_ENV=development webpack --config webpack.development.js --mode development",
11 | "clean": "rimraf dist"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/rockstat/web-sdk.git"
16 | },
17 | "author": "Dmitry Rodin ",
18 | "license": "Apache-2.0",
19 | "homepage": "https://github.com/rockstat/web-sdk",
20 | "files": [
21 | "dist/lib.js",
22 | "dist/lib-dev.js",
23 | "README.md",
24 | "LICENCE",
25 | "snippet/snippet.html"
26 | ],
27 | "dependencies": {
28 | "component-emitter": "^1.3.1",
29 | "core-js": "^3.37.1",
30 | "js-cookie": "^2.2.1",
31 | "promise-polyfill": "^8.3.0",
32 | "punycode": "^2.3.1",
33 | "qs": "^6.12.1",
34 | "url-parse": "^1.5.10"
35 | },
36 | "devDependencies": {
37 | "@babel/core": "^7.24.7",
38 | "@babel/plugin-transform-runtime": "^7.24.7",
39 | "@babel/preset-env": "^7.24.7",
40 | "babel-loader": "^8.3.0",
41 | "regenerator-runtime": "^0.13.11",
42 | "rimraf": "^3.0.2",
43 | "terser": "^4.7.0",
44 | "webpack": "^5.91.0",
45 | "webpack-cli": "^5.1.4"
46 | },
47 | "browserslist": "> 0.25%, not dead"
48 | }
49 |
--------------------------------------------------------------------------------
/src/syncs/GoogleAnalytics.js:
--------------------------------------------------------------------------------
1 | import createLogger from '../functions/createLogger';
2 | import Emitter from 'component-emitter';
3 | import when from '../functions/when';
4 | import {win} from '../Browser';
5 | import {
6 | INTERNAL_EVENT,
7 | EVENT_USER_PARAMS
8 | } from "../Constants";
9 |
10 | const log = createLogger('RST/GASync');
11 |
12 | const GoogleAnalytics = function () {
13 |
14 | // Getting Google Analytics ClientId
15 |
16 | function get_ga_clientid() {
17 |
18 | }
19 |
20 | let ga4ClientId;
21 | when(
22 | () => {
23 | try {
24 | var parsed_cookies = {};
25 | document.cookie.split(';').forEach(function(el) {
26 | var splitCookie = el.split('=');
27 | var key = splitCookie[0].trim();
28 | var value = splitCookie[1];
29 | parsed_cookies[key] = value;
30 | });
31 | if(parsed_cookies["_ga"]){
32 | ga4ClientId = parsed_cookies["_ga"].substring(6);
33 | }
34 | return !!ga4ClientId;
35 | } catch(e){
36 | log.warn('Error while getting ga4 client id', e);
37 | }
38 | },
39 | () => {
40 | this.emit(INTERNAL_EVENT, EVENT_USER_PARAMS, {ga4ClientId});
41 | },
42 | 100,
43 | 100);
44 |
45 | // when(() => win.ga && win.ga.getAll && win.ga.getAll()[0], () => {
46 | // win.ga(() => {
47 | // try {
48 | // const gaId = win.ga.getAll()[0].get('clientId');
49 |
50 | // if (gaId) {
51 | // this.emit(INTERNAL_EVENT, EVENT_USER_PARAMS, {gaId});
52 | // }
53 | // } catch (e) {
54 | // log.error('Error while getting GA Client id:', e)
55 | // }
56 | // })
57 | // }, 25, 40);
58 | };
59 |
60 | Emitter(GoogleAnalytics.prototype);
61 |
62 |
63 | export default GoogleAnalytics;
64 |
--------------------------------------------------------------------------------
/src/functions/cyrb53.js:
--------------------------------------------------------------------------------
1 | /*
2 | cyrb53 (c) 2018 bryc (github.com/bryc)
3 | License: Public domain (or MIT if needed). Attribution appreciated.
4 | A fast and simple 53-bit string hash function with decent collision resistance.
5 | Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
6 | */
7 | export const cyrb53 = function(str, seed = 0) {
8 | let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
9 | for(let i = 0, ch; i < str.length; i++) {
10 | ch = str.charCodeAt(i);
11 | h1 = Math.imul(h1 ^ ch, 2654435761);
12 | h2 = Math.imul(h2 ^ ch, 1597334677);
13 | }
14 | h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
15 | h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
16 | h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
17 | h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
18 | return 4294967296 * (2097151 & h2) + (h1 >>> 0);
19 | };
20 |
21 | /*
22 | cyrb53a beta (c) 2023 bryc (github.com/bryc)
23 | License: Public domain (or MIT if needed). Attribution appreciated.
24 | This is a work-in-progress, and changes to the algorithm are expected.
25 | The original cyrb53 has a slight mixing bias in the low bits of h1.
26 | This doesn't affect collision rate, but I want to try to improve it.
27 | This new version has preliminary improvements in avalanche behavior.
28 | */
29 | export const cyrb53a_beta = function(str, seed = 0) {
30 | let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
31 | for(let i = 0, ch; i < str.length; i++) {
32 | ch = str.charCodeAt(i);
33 | h1 = Math.imul(h1 ^ ch, 0x85ebca77);
34 | h2 = Math.imul(h2 ^ ch, 0xc2b2ae3d);
35 | }
36 | h1 ^= Math.imul(h1 ^ (h2 >>> 15), 0x735a2d97);
37 | h2 ^= Math.imul(h2 ^ (h1 >>> 15), 0xcaf649a9);
38 | h1 ^= h2 >>> 16; h2 ^= h1 >>> 16;
39 | return 2097152 * (h2 >>> 0) + (h1 >>> 11);
40 | };
41 |
--------------------------------------------------------------------------------
/src/trackers/BrowserEventsTracker.js:
--------------------------------------------------------------------------------
1 | import objectAssing from '../functions/objectAssing';
2 | import createLogger from '../functions/createLogger';
3 | import Emitter from 'component-emitter';
4 | import once from '../functions/once';
5 | import {
6 | removeHandler,
7 | addHandler
8 | } from "../functions/domEvents";
9 | import { win, doc } from "../Browser";
10 | import {
11 | DOM_COMPLETE,
12 | DOM_BEFORE_UNLOAD,
13 | DOM_UNLOAD,
14 | INTERNAL_EVENT,
15 | EVENT,
16 | EVENT_PAGE_LOADED
17 | } from "../Constants";
18 |
19 | const log = createLogger('RST/BrowserEventsTracker');
20 |
21 | /**
22 | * Трекер отслеживающий базовые события браузера, такие, как завершение загруки, выгрузка страницы и тп.
23 | * @param options
24 | * @constructor
25 | */
26 | const BrowserEventsTracker = function (options) {
27 |
28 | this.options = objectAssing({
29 | unloadHandlers: true
30 | }, options);
31 |
32 | // Обработчик завершения загрузки страницы
33 | this.loadedHandler = once(() => {
34 |
35 | this.emit(INTERNAL_EVENT, DOM_COMPLETE);
36 | this.emit(EVENT, {
37 | name: EVENT_PAGE_LOADED
38 | });
39 | removeHandler(win, 'load', this.loadedHandler);
40 |
41 | });
42 |
43 | // Обработчик beforeunload, который вызывается перед непосредственной выгрузкой страницы
44 | this.beforeUnloadHandler = () => {
45 | this.emit(INTERNAL_EVENT, DOM_BEFORE_UNLOAD);
46 | removeHandler(win, 'beforeunload', this.beforeUnloadHandler);
47 | };
48 |
49 | // Обработчик unload
50 | this.unloadHandler = () => {
51 | this.emit(INTERNAL_EVENT, DOM_UNLOAD);
52 | removeHandler(win, 'unload', this.unloadHandler);
53 | };
54 | };
55 |
56 | Emitter(BrowserEventsTracker.prototype);
57 |
58 | BrowserEventsTracker.prototype.initialize = function () {
59 |
60 | addHandler(win, 'load', this.loadedHandler);
61 | if (this.options.unloadHandlers) {
62 | addHandler(win, 'beforeunload', this.beforeUnloadHandler);
63 | addHandler(win, 'unload', this.unloadHandler);
64 | }
65 | };
66 |
67 | export default BrowserEventsTracker;
68 |
--------------------------------------------------------------------------------
/src/trackers/PageTracker.js:
--------------------------------------------------------------------------------
1 | import objectAssing from '../functions/objectAssing';
2 | import { win, doc } from "../Browser";
3 | import Emitter from 'component-emitter';
4 | import {
5 | EVENT,
6 | EVENT_PAGEVIEW
7 | } from '../Constants';
8 | import createLogger from '../functions/createLogger';
9 |
10 | const log = createLogger('RST/PageTracker');
11 |
12 | const nn = (val) => val || '';
13 |
14 | /**
15 | *
16 | * @param options
17 | * @constructor
18 | */
19 | const PageTracker = function (options) {
20 |
21 | this.options = objectAssing({}, options);
22 | this.initialized = false;
23 | this.eventHandler = this.eventHandler.bind(this);
24 | this.initialize();
25 | };
26 | Emitter(PageTracker.prototype);
27 |
28 | PageTracker.prototype.eventHandler = function (e) {
29 | const event = {
30 | name: EVENT_PAGEVIEW,
31 | };
32 | this.emit(EVENT, event);
33 | };
34 |
35 |
36 |
37 | PageTracker.prototype.initialize = function () {
38 | // if (!win.addEventListener) return;
39 | // win.addEventListener('hashchange', this.eventHandler, true);
40 | this.initialized = true;
41 | };
42 |
43 | PageTracker.prototype.unload = function () {
44 | // if (!win.addEventListener) return;
45 | // win.removeEventListener('hashchange', this.eventHandler, true);
46 | this.initialized = false;
47 | };
48 |
49 | export default PageTracker;
50 |
51 |
52 | // class Hist {
53 | // currentUrl: String = ''
54 | // constructor() {
55 | // this.currentUrl = document.location.href || ''
56 | // }
57 |
58 | // url: () => String = () => {
59 | // return this.currentUrl
60 | // }
61 |
62 | // public urlChangeHandler = (event) => {
63 | // console.log("MY_HANDLER: location: " + document.location + ", state: " + JSON.stringify(event.state));
64 | // }
65 |
66 | // }
67 |
68 | // const h = new Hist()
69 |
70 | // function greeter(person) {
71 | // return "Hello, " + person + ' at ' + h.url();
72 | // }
73 |
74 | // let user = "Jane User";
75 |
76 | // document.body.textContent = greeter(user);
77 |
78 |
79 | // window.onpopstate = h.urlChangeHandler
80 |
81 |
82 | // window.onpopstate = ev =>
83 |
--------------------------------------------------------------------------------
/src/functions/domEvents.js:
--------------------------------------------------------------------------------
1 | import {doc, win} from '../Browser';
2 | import createLogger from './createLogger';
3 |
4 | const log = createLogger('RSP/DomEvents');
5 | // const checkPassiveSupport = () => {
6 | // let result = false;
7 | // try {
8 | // const options = Object.defineProperty({}, 'passive', {
9 | // get: function () {
10 | // result = true;
11 | // }
12 | // });
13 |
14 | // window.addEventListener('test', null, options);
15 | // } catch (err) {
16 | // }
17 | // return result;
18 | // };
19 |
20 |
21 | export const hasAddEL = !!win.addEventListener;
22 | // const passiveSupport = checkPassiveSupport();
23 |
24 | /**
25 | * @return {boolean}
26 | */
27 | const stateIsComplete = () => {
28 | return doc.readyState === 'loaded' || doc.readyState === 'complete';
29 | };
30 |
31 | /**
32 | * @return {boolean}
33 | */
34 | const stateIsInteractive = () => {
35 | return doc.readyState === 'interactive';
36 | };
37 |
38 | /**
39 | * @type {Boolean}
40 | */
41 | export const useCaptureSupport = hasAddEL;
42 |
43 | /**
44 | * @param elem {Element}
45 | * @param type {string}
46 | * @param handler {function}
47 | * @param useCapture {boolean}
48 | * @return {*}
49 | */
50 | export function addHandler(elem, type, handler, useCapture = false) {
51 | if (hasAddEL) {
52 | elem.addEventListener(type, handler, useCapture);
53 | } else {
54 | log('.addEventListener not supported');
55 | }
56 | }
57 |
58 | /**
59 | * @param elem Element
60 | * @param type string
61 | * @param handler
62 | * @param useCapture
63 | * @return {*}
64 | */
65 | export function removeHandler(elem, type, handler, useCapture = false) {
66 | if (hasAddEL) {
67 | elem.removeEventListener(type, handler, useCapture);
68 | }
69 | }
70 |
71 | /**
72 | *
73 | * @param cb function Callback function
74 | */
75 | export function documentReady(cb) {
76 |
77 | if (stateIsInteractive() || stateIsComplete()) {
78 | cb();
79 | return;
80 | }
81 |
82 | function loadedHandler() {
83 | removeHandler(doc, 'DOMContentLoaded', loadedHandler);
84 | cb();
85 | }
86 |
87 | addHandler(doc, 'DOMContentLoaded', loadedHandler);
88 | }
89 |
--------------------------------------------------------------------------------
/src/data/browserCharacts.js:
--------------------------------------------------------------------------------
1 | import {
2 | nav,
3 | win
4 | } from '../Browser';
5 | import {
6 | isHttps
7 | } from './pageDefaults';
8 |
9 | export const hasLocStoreSupport = (function () {
10 | try {
11 | const ls = localStorage;
12 | const test = '_rstest_';
13 | ls.setItem(test, test);
14 | const res = ls.getItem(test);
15 | ls.removeItem(test);
16 | return res === test;
17 | } catch (e) { }
18 | return false;
19 | })();
20 |
21 | export const hasPromiseSupport = (function () {
22 | return 'Promise' in win;
23 | })();
24 |
25 | export const hasAddELSupport = (function () {
26 | return 'addEventListener' in win;
27 | })();
28 |
29 | export const hasWebPushSupport = (function () {
30 | return ('serviceWorker' in nav && 'PushManager' in win) &&
31 | ('showNotification' in ServiceWorkerRegistration.prototype);
32 | })();
33 |
34 | export const hasBeaconSupport = (function () {
35 | return nav && ('sendBeacon' in nav);
36 | })();
37 |
38 | export const hasFetchSupport = (function () {
39 | return !!fetch;
40 | })();
41 |
42 |
43 |
44 | export const hasBlobSupport = (function () {
45 | return 'Blob' in win;
46 | })();
47 |
48 | export const hasXDRSupport = (function () {
49 | return !!window.XDomainRequest;
50 | })();
51 |
52 | export const hasXHRSupport = (function () {
53 | return !!win.XMLHttpRequest;
54 | })();
55 |
56 | export const hasXHRWithCreds = (function () {
57 | return hasXHRSupport && ('withCredentials' in new win.XMLHttpRequest());
58 | })();
59 |
60 | export const hasAnyXRSupport = (function () {
61 | return hasXHRSupport || hasXDRSupport;
62 | })();
63 |
64 | export const hasBase64Support = (function () {
65 | return win.atob && win.btoa;
66 | })();
67 |
68 | export const hasWSSupport = (function () {
69 | const protocol = isHttps() ? 'wss' : 'ws';
70 | if ('WebSocket' in window) {
71 | let protoBin = ('binaryType' in WebSocket.prototype);
72 | if (protoBin) {
73 | return protoBin;
74 | }
75 | try {
76 | return !!(new WebSocket(protocol + '://.').binaryType);
77 | } catch (e) { }
78 | }
79 | return false;
80 | })();
81 |
82 | export default {
83 | 'ls': hasLocStoreSupport,
84 | 'pr': hasPromiseSupport,
85 | 'ae': hasAddELSupport,
86 | 'sb': hasBeaconSupport,
87 | 'ab': hasBase64Support,
88 | 'wp': hasWebPushSupport
89 | };
90 |
--------------------------------------------------------------------------------
/src/extensions/web-push/controller.js:
--------------------------------------------------------------------------------
1 | import { PushClient } from './lib'
2 | import Emitter from 'component-emitter';
3 |
4 | export class PushController {
5 |
6 | constructor(tracker) {
7 |
8 | this.tracker = tracker
9 | this._pushClient = new PushClient(
10 | this._stateChangeListener,
11 | this._subscriptionUpdate,
12 | // window.gauntface.CONSTANTS.APPLICATION_KEYS.publicKey
13 | );
14 | window.rst_push = this
15 | }
16 |
17 | _stateChangeListener(state, data) {
18 | console.log('state', state)
19 | switch (state.id) {
20 | case 'UNSUPPORTED':
21 | this.showErrorMessage(
22 | 'Push Not Supported',
23 | data
24 | );
25 | break;
26 | case 'ERROR':
27 | this.showErrorMessage(
28 | 'Ooops a Problem Occurred',
29 | data
30 | );
31 | break;
32 | default:
33 | break;
34 | }
35 | }
36 |
37 | _subscriptionUpdate(subscription) {
38 | this._currentSubscription = subscription;
39 | console.log('new sub', subscription)
40 | if (!subscription) {
41 | return;
42 | }
43 | }
44 |
45 | subscribe() {
46 | this._pushClient.subscribeDevice();
47 | }
48 |
49 | unsubscribe() {
50 | this._pushClient.unsubscribeDevice();
51 | }
52 |
53 | run() {
54 | if (!navigator.serviceWorker) {
55 | console.warn('Service worker not supported.');
56 | return;
57 | }
58 | if (!('PushManager' in window)) {
59 | console.warn('Push not supported.');
60 | return;
61 | }
62 | appController.registerServiceWorker();
63 | }
64 |
65 | showErrorMessage(title, message) {
66 | console.warn(title, message);
67 | }
68 |
69 | registerServiceWorker() {
70 | // Check that service workers are supported
71 | if ('serviceWorker' in navigator) {
72 | navigator.serviceWorker.register('./service-worker.js')
73 | .catch((err) => {
74 | this.showErrorMessage(
75 | 'Unable to Register SW',
76 | 'Sorry this demo requires a service worker to work and it ' +
77 | 'failed to install - sorry :('
78 | );
79 | console.error(err);
80 | });
81 | } else {
82 | this.showErrorMessage(
83 | 'Service Worker Not Supported',
84 | 'Sorry this demo requires service worker support in your browser. ' +
85 | 'Please try this demo in Chrome or Firefox Nightly.'
86 | );
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Constants.js:
--------------------------------------------------------------------------------
1 | export const SERVICE_TRACK = 'track';
2 | export const SERVICE_LOG = 'log';
3 |
4 |
5 | export const EVENT = 'event';
6 | export const EVENT_PAGEVIEW = 'page';
7 | export const EVENT_IDENTIFY = 'identify';
8 | export const EVENT_SESSION = 'session';
9 | export const EVENT_SIMULATE_SESSION = 'simulate-session';
10 | export const EVENT_PAGE_LOADED = 'page_loaded';
11 | export const EVENT_ACTIVITY = 'activity';
12 | export const EVENT_SCROLL = 'scroll';
13 | export const EVENT_FORM_SUMBIT = 'form_submit';
14 | export const EVENT_FORM_INVALID = 'form_invalid';
15 | export const EVENT_FIELD_FOCUS = 'field_focus';
16 | export const EVENT_FIELD_BLUR = 'field_blur';
17 | export const EVENT_FIELD_CHANGE = 'field_change';
18 | export const EVENT_USER_PARAMS = 'user_params';
19 | export const EVENT_LINK_CLICK = 'link_click';
20 | export const EVENT_ELEMENT_CLICK = 'element_click';
21 | export const EVENT_PAGE_UNLOAD = 'page_unload';
22 |
23 |
24 | export const EVENTS_ADD_PERF = [EVENT_PAGEVIEW, EVENT_PAGE_LOADED];
25 |
26 | export const EVENTS_NO_SCROLL = [
27 | EVENT_SESSION,
28 | EVENT_PAGEVIEW,
29 | EVENT_PAGE_LOADED,
30 | EVENT_USER_PARAMS,
31 | EVENT_IDENTIFY
32 | ];
33 |
34 | export const EVENTS_ADD_SCROLL = [
35 | EVENT_FORM_SUMBIT,
36 | EVENT_FORM_INVALID,
37 | EVENT_FIELD_FOCUS,
38 | EVENT_FIELD_CHANGE,
39 | EVENT_FIELD_BLUR,
40 | EVENT_ACTIVITY,
41 | EVENT_SCROLL
42 | ];
43 |
44 | // ######
45 | export const EVENT_LOG = 'page_unload';
46 |
47 | // ######
48 | export const SERVER_MESSAGE = 'server_message';
49 | export const INTERNAL_EVENT = 'internal_event';
50 |
51 | // ######
52 | export const EVENT_OPTION_TERMINATOR = 'terminator';
53 | export const EVENT_OPTION_OUTBOUND = 'outbound';
54 | export const EVENT_OPTION_REQUEST = 'request';
55 | export const EVENT_OPTION_TRANSPORT_IMG = 'transport_img';
56 |
57 | // ######
58 | export const SESSION_INTERNAL = 'internal';
59 | export const SESSION_DIRECT = 'direct';
60 | export const SESSION_ORGANIC = 'organic';
61 | export const SESSION_CAMPAIGN = 'campaign';
62 | export const SESSION_REFERRAL = 'referral';
63 | export const SESSION_SOCIAL = 'social';
64 | export const SESSION_PARTNER = 'partner';
65 | export const SESSION_WEBVIEW = 'webview';
66 | export const SESSION_UNKNOWN = 'unknown';
67 |
68 | export const DOM_INTERACTIVE = 'dom_interactive';
69 | export const DOM_COMPLETE = 'dom_loaded';
70 | export const DOM_BEFORE_UNLOAD = 'before_unload';
71 | export const DOM_UNLOAD = 'unload';
72 |
73 | export const READY = 'ready';
74 |
75 | export const CB_DOM_EVENT = 'dom_event';
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/src/trackers/ClickTracker.js:
--------------------------------------------------------------------------------
1 | import objectAssing from '../functions/objectAssing';
2 | // import runOnStop from '../functions/runOnStop';
3 | // import objectKeys from '../functions/objectKeys';
4 | // import each from '../functions/each';
5 | import { win, doc } from "../Browser";
6 | import { closest } from '../lib/dom-utils';
7 | import Emitter from 'component-emitter';
8 | import {
9 | EVENT,
10 | EVENT_OPTION_OUTBOUND,
11 | EVENT_OPTION_TERMINATOR,
12 | EVENT_LINK_CLICK,
13 | EVENT_ELEMENT_CLICK
14 | } from '../Constants';
15 |
16 | const linkTag = 'a';
17 | const nn = (val) => val || '';
18 |
19 | /**
20 | *
21 | * @param options
22 | * @constructor
23 | */
24 | const ClickTracker = function (options) {
25 |
26 | this.options = objectAssing({}, options);
27 | this.initialized = false;
28 | this.eventHandler = this.eventHandler.bind(this);
29 | this.initialize();
30 | };
31 | Emitter(ClickTracker.prototype);
32 |
33 | ClickTracker.prototype.eventHandler = function (e) {
34 |
35 | const target = e.target || e.srcElement;
36 | const link = closest(target, linkTag, true);
37 |
38 | if (this.options.allClicks || !!link) {
39 |
40 | const draft = {
41 | name: EVENT_ELEMENT_CLICK,
42 | // target, link params
43 | data: {
44 | target: this.getTargetInfo(target)
45 | },
46 | options: {} // holder for link options
47 | }
48 |
49 | const event = !!link ? this.mutateToLinkClick(draft, link) : draft;
50 | this.emit(EVENT, event);
51 | }
52 | };
53 |
54 | ClickTracker.prototype.mutateToLinkClick = function (draft, link) {
55 |
56 | const loc = win.location;
57 | const outbound = (link.hostname !== loc.hostname) && ((link.hostname !== '') || (link.onclick === null));
58 |
59 | const linkData = {
60 | href: link.href,
61 | text: link.innerText,
62 | outbound: outbound
63 | }
64 | const linkOptions = {
65 | // [EVENT_OPTION_TERMINATOR]: true,
66 | [EVENT_OPTION_OUTBOUND]: outbound
67 | }
68 |
69 | return objectAssing({}, draft, {
70 | name: EVENT_LINK_CLICK,
71 | data: objectAssing({}, draft.data, linkData),
72 | options: objectAssing({}, draft.options, linkOptions)
73 | });
74 | }
75 |
76 | ClickTracker.prototype.getTargetInfo = function (target) {
77 |
78 | if (!target) {
79 | return {};
80 | }
81 | return {
82 | tag: nn(target.tagName && target.tagName.toLowerCase()),
83 | type: nn(target.getAttribute('type')),
84 | name: nn(target.getAttribute('name')),
85 | ph: nn(target.getAttribute('placeholder')),
86 | cls: nn(target.className),
87 | id: nn(target.id),
88 | href: nn(target.href),
89 | text: nn(target.innerText)
90 | };
91 | }
92 |
93 |
94 | ClickTracker.prototype.initialize = function () {
95 |
96 | if (!win.addEventListener) return;
97 | doc.addEventListener('click', this.eventHandler, true);
98 | this.initialized = true;
99 | };
100 |
101 | ClickTracker.prototype.unload = function () {
102 | doc.removeEventListener('click', this.eventHandler, true);
103 | };
104 |
105 | export default ClickTracker;
106 |
--------------------------------------------------------------------------------
/src/polyfill.js:
--------------------------------------------------------------------------------
1 | // Production steps of ECMA-262, Edition 5, 15.4.4.19
2 | // Reference: http://es5.github.io/#x15.4.4.19
3 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
4 | if (!Array.prototype.map) {
5 |
6 | Array.prototype.map = function(callback/*, thisArg*/) {
7 |
8 | var T, A, k;
9 |
10 | if (this == null) {
11 | throw new TypeError('this is null or not defined');
12 | }
13 |
14 | // 1. Let O be the result of calling ToObject passing the |this|
15 | // value as the argument.
16 | var O = Object(this);
17 |
18 | // 2. Let lenValue be the result of calling the Get internal
19 | // method of O with the argument "length".
20 | // 3. Let len be ToUint32(lenValue).
21 | var len = O.length >>> 0;
22 |
23 | // 4. If IsCallable(callback) is false, throw a TypeError exception.
24 | // See: http://es5.github.com/#x9.11
25 | if (typeof callback !== 'function') {
26 | throw new TypeError(callback + ' is not a function');
27 | }
28 |
29 | // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
30 | if (arguments.length > 1) {
31 | T = arguments[1];
32 | }
33 |
34 | // 6. Let A be a new array created as if by the expression new Array(len)
35 | // where Array is the standard built-in constructor with that name and
36 | // len is the value of len.
37 | A = new Array(len);
38 |
39 | // 7. Let k be 0
40 | k = 0;
41 |
42 | // 8. Repeat, while k < len
43 | while (k < len) {
44 |
45 | var kValue, mappedValue;
46 |
47 | // a. Let Pk be ToString(k).
48 | // This is implicit for LHS operands of the in operator
49 | // b. Let kPresent be the result of calling the HasProperty internal
50 | // method of O with argument Pk.
51 | // This step can be combined with c
52 | // c. If kPresent is true, then
53 | if (k in O) {
54 |
55 | // i. Let kValue be the result of calling the Get internal
56 | // method of O with argument Pk.
57 | kValue = O[k];
58 |
59 | // ii. Let mappedValue be the result of calling the Call internal
60 | // method of callback with T as the this value and argument
61 | // list containing kValue, k, and O.
62 | mappedValue = callback.call(T, kValue, k, O);
63 |
64 | // iii. Call the DefineOwnProperty internal method of A with arguments
65 | // Pk, Property Descriptor
66 | // { Value: mappedValue,
67 | // Writable: true,
68 | // Enumerable: true,
69 | // Configurable: true },
70 | // and false.
71 |
72 | // In browsers that support Object.defineProperty, use the following:
73 | // Object.defineProperty(A, k, {
74 | // value: mappedValue,
75 | // writable: true,
76 | // enumerable: true,
77 | // configurable: true
78 | // });
79 |
80 | // For best browser support, use the following:
81 | A[k] = mappedValue;
82 | }
83 | // d. Increase k by 1.
84 | k++;
85 | }
86 |
87 | // 9. return A
88 | return A;
89 | };
90 | }
91 |
92 |
93 | ;(function () {
94 |
95 | }());
96 |
--------------------------------------------------------------------------------
/src/lib/dom-utils/index.js:
--------------------------------------------------------------------------------
1 | // "author": {
2 | // "name": "Philip Walton",
3 | // "email": "philip@philipwalton.com",
4 | // "url": "http://philipwalton.com"
5 | // },
6 |
7 | /**
8 | * Returns an array of a DOM element's parent elements.
9 | * @param {!Element} element The DOM element whose parents to get.
10 | * @return {!Array} An array of all parent elemets, or an empty array if no
11 | * parent elements are found.
12 | */
13 | export function parents(element) {
14 | const list = [];
15 | while (element && element.parentNode && element.parentNode.nodeType == 1) {
16 | element = /** @type {!Element} */ (element.parentNode);
17 | list.push(element);
18 | }
19 | return list;
20 | }
21 |
22 |
23 | const proto = window.Element.prototype;
24 | const nativeMatches = proto.matches ||
25 | proto.matches ||
26 | proto.matchesSelector ||
27 | proto.webkitMatchesSelector ||
28 | proto.mozMatchesSelector ||
29 | proto.msMatchesSelector ||
30 | proto.oMatchesSelector;
31 |
32 |
33 | /**
34 | * Tests if a DOM elements matches any of the test DOM elements or selectors.
35 | * @param {Element} element The DOM element to test.
36 | * @param {Element|string|Array} test A DOM element, a CSS
37 | * selector, or an array of DOM elements or CSS selectors to match against.
38 | * @return {boolean} True of any part of the test matches.
39 | */
40 | export function matches(element, test) {
41 | // Validate input.
42 | if (element && element.nodeType == 1 && test) {
43 | // if test is a string or DOM element test it.
44 | if (typeof test == 'string' || test.nodeType == 1) {
45 | return element == test ||
46 | matchesSelector(element, /** @type {string} */ (test));
47 | } else if ('length' in test) {
48 | // if it has a length property iterate over the items
49 | // and return true if any match.
50 | for (let i = 0, item; item = test[i]; i++) {
51 | if (element == item || matchesSelector(element, item)) return true;
52 | }
53 | }
54 | }
55 | // Still here? Return false
56 | return false;
57 | }
58 |
59 |
60 | /**
61 | * Tests whether a DOM element matches a selector. This polyfills the native
62 | * Element.prototype.matches method across browsers.
63 | * @param {!Element} element The DOM element to test.
64 | * @param {string} selector The CSS selector to test element against.
65 | * @return {boolean} True if the selector matches.
66 | */
67 | function matchesSelector(element, selector) {
68 | if (typeof selector != 'string') return false;
69 | if (nativeMatches) return nativeMatches.call(element, selector);
70 | const nodes = element.parentNode.querySelectorAll(selector);
71 | for (let i = 0, node; node = nodes[i]; i++) {
72 | if (node == element) return true;
73 | }
74 | return false;
75 | }
76 |
77 |
78 | /**
79 | * Gets the closest parent element that matches the passed selector.
80 | * @param {Element} element The element whose parents to check.
81 | * @param {string} selector The CSS selector to match against.
82 | * @param {boolean=} shouldCheckSelf True if the selector should test against
83 | * the passed element itself.
84 | * @return {Element|undefined} The matching element or undefined.
85 | */
86 | export function closest(element, selector, shouldCheckSelf = false) {
87 | if (!(element && element.nodeType == 1 && selector)) return;
88 | const parentElements =
89 | (shouldCheckSelf ? [element] : []).concat(parents(element));
90 |
91 | for (let i = 0, parent; parent = parentElements[i]; i++) {
92 | if (matches(parent, selector)) return parent;
93 | }
94 | }
--------------------------------------------------------------------------------
/src/CookieStorageAdapter.js:
--------------------------------------------------------------------------------
1 | import objectAssign from './functions/objectAssing';
2 | import objectKeys from './functions/objectKeys';
3 | import Cookies from 'js-cookie';
4 | import { isHttps } from './data/pageDefaults';
5 |
6 | function CookieStorageAdapter(options) {
7 | options = options || {};
8 | // handle configuration
9 | this.secure = isHttps() // OLD VAL: options.allowHTTP !== true;
10 | this.domain = options.cookieDomain;
11 | // check cookie is enabled
12 | this.available = this.checkAvailability();
13 | this.prefix = options.cookiePrefix || '';
14 | this.path = options.cookiePath || '/';
15 | // exp date
16 | this.exp = new Date((new Date()).getTime() + 3 * 31536e+6);
17 |
18 | }
19 |
20 |
21 | CookieStorageAdapter.prototype.getPrefixedKey = function (key, options) {
22 |
23 | let prefix = this.prefix;
24 | return prefix + key;
25 | };
26 |
27 |
28 | CookieStorageAdapter.prototype.set = function (key, value, options = {}) {
29 |
30 | if (!this.available)
31 | return;
32 |
33 | key = this.getPrefixedKey(key, options);
34 |
35 | const exp = !!options.session ?
36 | undefined :
37 | (options.exp ?
38 | new Date((new Date()).getTime() + options.exp * 1000) :
39 | this.exp);
40 |
41 | Cookies.set(key, value, {
42 | expires: exp,
43 | domain: this.domain,
44 | secure: this.secure,
45 | path: this.path,
46 | sameSite: 'Lax'
47 | });
48 | };
49 |
50 | CookieStorageAdapter.prototype.get = function (key, options = {}) {
51 |
52 | if (!this.available)
53 | return;
54 |
55 | options = options || {};
56 | key = this.getPrefixedKey(key, options);
57 |
58 | return Cookies.get(key);
59 |
60 |
61 | };
62 |
63 | CookieStorageAdapter.prototype.inc = function (key, options) {
64 |
65 | if (!this.available)
66 | return;
67 |
68 | let counter = this.get(key, options) || 0;
69 | counter += 1;
70 | this.set(key, counter, options);
71 | return counter;
72 | };
73 |
74 | CookieStorageAdapter.prototype.rm = function (key, options) {
75 |
76 | if (!this.available)
77 | return;
78 |
79 | options = options || {};
80 | key = this.getPrefixedKey(key, options);
81 |
82 | Cookies.remove(key, {
83 | domain: this.domain,
84 | secure: true
85 | });
86 | };
87 |
88 |
89 | CookieStorageAdapter.prototype.getAllKeys = function (options) {
90 |
91 | if (!this.available)
92 | return [];
93 |
94 | const prefix = this.getPrefixedKey('', options);
95 | const result = [];
96 |
97 | let keys = objectKeys(Cookies.get());
98 | for (let i = 0; i < keys.length; i++) {
99 |
100 | const key = keys[i];
101 |
102 | if (key.substr(0, prefix.length) === prefix) {
103 | result.push(key.substr(prefix.length));
104 | }
105 | }
106 |
107 | return result;
108 | };
109 |
110 | CookieStorageAdapter.prototype.getAll = function (options) {
111 |
112 | if (!this.available)
113 | return {};
114 |
115 | options = options || {};
116 |
117 | const keys = this.getAllKeys(options);
118 | const results = {};
119 |
120 | for (let i = 0; i < keys.length; i++) {
121 |
122 | const key = keys[i];
123 | results[key] = this.get(key, options);
124 |
125 | }
126 |
127 | return results
128 | };
129 |
130 | CookieStorageAdapter.prototype.rmAll = function (options) {
131 |
132 | if (!this.available)
133 | return;
134 |
135 | options = options || {};
136 | const keys = this.getAllKeys(options);
137 |
138 | for (let i = 0; i < keys.length; i++) {
139 |
140 | this.rm(keys[i]);
141 |
142 | }
143 | };
144 |
145 | CookieStorageAdapter.prototype.checkAvailability = function () {
146 | return true;
147 | };
148 |
149 | export default CookieStorageAdapter;
150 |
--------------------------------------------------------------------------------
/src/data/browserData.js:
--------------------------------------------------------------------------------
1 | import { nav, win, body, html } from "../Browser";
2 | import objectAssing from '../functions/objectAssing';
3 | import each from '../functions/each';
4 |
5 | const not_present = 'not present'
6 |
7 |
8 | function getTimeZone(d) {
9 | const extracted = /\((.*)\)/.exec(d.toString());
10 | return extracted && extracted[1] || 'not present';
11 | }
12 |
13 | export function tstz() {
14 | const d = new Date();
15 | return {
16 | ts: d.getTime(),
17 | tz: getTimeZone(d),
18 | tzo: -d.getTimezoneOffset() * 1000
19 | }
20 | }
21 |
22 | export function binfo() {
23 | const d = new Date();
24 | return {
25 | plt: nav.platform || not_present,
26 | prd: nav.product || not_present
27 | }
28 | }
29 |
30 | function if1() {
31 | try {
32 | return win === win.top ? 0 : 1;
33 | } catch (e) { }
34 | }
35 |
36 | function if2() {
37 | try {
38 | return win.parent.frames.length > 0 ? 2 : 0;
39 | } catch (e) { }
40 | }
41 |
42 | function wh() {
43 | try {
44 | return {
45 | w: win.innerWidth || html.clientWidth || body.clientWidth,
46 | h: win.innerHeight || html.clientHeight || body.clientHeight
47 | };
48 | } catch (e) { }
49 | }
50 |
51 | function sr() {
52 | try {
53 | const s = win.screen;
54 | const orient = s.orientation || {};
55 | const aspRatio = win.devicePixelRatio
56 | return {
57 | tw: s.width || -1,
58 | th: s.height || -1,
59 | aw: s.availWidth || -1,
60 | ah: s.availHeight || -1,
61 | sopr: Math.round(aspRatio ? aspRatio * 1000 : -1),
62 | soa: orient.angle || -1,
63 | sot: orient.type || not_present
64 | };
65 |
66 | } catch (e) { }
67 | }
68 |
69 |
70 |
71 |
72 | const navConData = {};
73 | const navConKeys = ['type', 'effectiveType', 'downlinkMax', 'rtt'];
74 |
75 |
76 | /*
77 |
78 | Get connection type data
79 |
80 | TODO: Support changing connection type \
81 | using listener \
82 | navigator.connection.addEventListener('change', listener)
83 |
84 |
85 | */
86 | export function prepareNavConnection() {
87 | if (nav['connection']) {
88 | each(navConKeys, (k) => {
89 | if (nav.connection[k] && nav.connection[k] !== 'null') {
90 | navConData[k] = nav.connection[k];
91 | }
92 | });
93 | }
94 | }
95 |
96 |
97 | /**
98 | * Navigator extra
99 | *
100 | */
101 |
102 | const navExtraData = {};
103 |
104 | export function prepareNavExtra() {
105 | if (nav['hardwareConcurrency']) {
106 | navExtraData['hc'] = nav['hardwareConcurrency'];
107 | }
108 | if (nav['deviceMemory']) {
109 | navExtraData['dm'] = nav['deviceMemory'];
110 | }
111 | }
112 |
113 |
114 | /**
115 | * Battery
116 | */
117 |
118 | const navBatData = {}
119 |
120 |
121 | export function prepareBatData() {
122 | if (nav['getBattery']) {
123 | nav.getBattery().then(b => {
124 | navBatData['a'] = 1;
125 | navBatData['ch'] = Math.round(Number(b['charging']));
126 | navBatData['l'] = Math.round(Number(b['level']) * 100);
127 | }).catch((e) => {
128 | navBatData['err'] = String(e);
129 | });
130 | }
131 | }
132 |
133 |
134 |
135 | /*
136 | Get Client HINT data
137 |
138 | */
139 |
140 |
141 | const he_values = ['architecture', 'bitness', 'mobile', 'model', 'platform', 'platformVersion', 'uaFullVersion'];
142 | const storedUAData = {};
143 |
144 |
145 | export function prepareUAData() {
146 | if (nav['userAgentData'] && nav.userAgentData['getHighEntropyValues']) {
147 | nav.userAgentData.getHighEntropyValues(he_values).then(ua => {
148 | each(ua || {}, (k, v) => {
149 | if (he_values.indexOf(k) >= 0) {
150 | storedUAData[k] = v;
151 | }
152 | })
153 | }).catch((e) => {
154 | storedUAData['err'] = String(e);
155 | });
156 | }
157 | }
158 |
159 |
160 | export default function () {
161 | return objectAssing({
162 | if1: if1(),
163 | if2: if2(),
164 | uad: storedUAData,
165 | nc: navConData,
166 | ne: navExtraData,
167 | nb: navBatData
168 | }, wh(), sr(), binfo())
169 | }
170 |
--------------------------------------------------------------------------------
/src/LocalStorageAdapter.js:
--------------------------------------------------------------------------------
1 | import objectAssign from './functions/objectAssing';
2 | import objectKeys from './functions/objectKeys';
3 | import createLogger from './functions/createLogger';
4 |
5 | const log = createLogger('LocalStorage');
6 |
7 | function LocalStorageAdapter(options) {
8 | this.available = this.checkAvailability();
9 | this.prefix = options && options.prefix || '';
10 | }
11 |
12 | LocalStorageAdapter.prototype.isAvailable = function () {
13 | return this.available();
14 | };
15 |
16 |
17 | LocalStorageAdapter.prototype.getPrefixedKey = function (key, options) {
18 | let prefix = this.prefix;
19 | if (options && options.session === true) {
20 | prefix += 's:';
21 | }
22 | return prefix + key;
23 | };
24 |
25 | LocalStorageAdapter.prototype.set = function (key, value, options) {
26 |
27 | if (!this.available) {
28 | return;
29 | }
30 | const query_key = this.getPrefixedKey(key, options);
31 | try {
32 | const exp = options && options.exp
33 | ? Math.round((new Date()).getTime() / 1000) + options.exp
34 | : '';
35 |
36 | localStorage.setItem(query_key, exp + '|' + JSON.stringify(value));
37 |
38 | } catch (e) {
39 | log('Error:', e);
40 | log.warn('LockStorage didn\'t successfully save the \'{' + key + ': ' + value + '}\' pair, because the localStorage is full.');
41 | }
42 |
43 | };
44 |
45 | LocalStorageAdapter.prototype.get = function (key, options, missing) {
46 |
47 | if (!this.available) {
48 | return;
49 | }
50 |
51 | //const missing = undefined;
52 | options = options || {};
53 | const query_key = this.getPrefixedKey(key, options);
54 |
55 | let data;
56 |
57 | try {
58 |
59 | data = localStorage.getItem(query_key);
60 |
61 | if (data) {
62 |
63 | const nowSec = (new Date()).getTime() / 1000;
64 | const sepPos = data.indexOf('|');
65 |
66 | if (sepPos < 0) {
67 |
68 | log.warn('Wrong format. Missing separator');
69 |
70 | this.rm(key, options);
71 | return missing;
72 |
73 | }
74 |
75 | const exp = data.substr(0, sepPos);
76 | const value = data.substr(sepPos + 1);
77 |
78 | if (exp && nowSec > exp) {
79 |
80 | this.rm(key, options);
81 | return missing;
82 |
83 | }
84 |
85 | return JSON.parse(value);
86 |
87 | } else {
88 |
89 | return missing;
90 |
91 | }
92 |
93 | } catch (e) {
94 |
95 | log.warn('LocalStorageAdapter could not load the item with key ' + key);
96 | log(e);
97 | }
98 | };
99 |
100 | LocalStorageAdapter.prototype.inc = function (key, options) {
101 |
102 | if (!this.available) {
103 | return;
104 | }
105 |
106 | let counter = this.get(key, options) || 0;
107 | counter += 1;
108 | this.set(key, counter, options);
109 | return counter;
110 |
111 | };
112 |
113 | LocalStorageAdapter.prototype.rm = function (key, options) {
114 |
115 | if (!this.available) {
116 | return;
117 | }
118 |
119 | options = options || {};
120 | const query_key = this.getPrefixedKey(key, options);
121 |
122 | localStorage.removeItem(query_key);
123 |
124 | };
125 |
126 | LocalStorageAdapter.prototype.getAllKeys = function (options) {
127 |
128 | if (!this.available) {
129 | return [];
130 | }
131 |
132 | let keys = objectKeys(localStorage);
133 | const prefix = this.getPrefixedKey('', options);
134 | const result = [];
135 |
136 | for (let i = 0; i < keys.length; i++) {
137 |
138 | const key = keys[i];
139 |
140 | if (key.substr(0, prefix.length) === prefix) {
141 | result.push(key.substr(prefix.length));
142 | }
143 | }
144 |
145 | return result;
146 |
147 | };
148 |
149 | LocalStorageAdapter.prototype.getAll = function (options) {
150 |
151 | if (!this.available) {
152 | return {};
153 | }
154 |
155 | options = options || {};
156 |
157 | const keys = this.getAllKeys(options);
158 | const results = {};
159 |
160 | for (let i = 0; i < keys.length; i++) {
161 |
162 | const key = keys[i];
163 | results[key] = this.get(key, options);
164 |
165 | }
166 |
167 | return results;
168 | };
169 |
170 | LocalStorageAdapter.prototype.rmAll = function (options) {
171 |
172 | if (!this.available) {
173 | return;
174 | }
175 |
176 | options = options || {};
177 | const keys = this.getAllKeys(options);
178 |
179 | for (let i = 0; i < keys.length; i++) {
180 |
181 | this.rm(keys[i]);
182 |
183 | }
184 | };
185 |
186 | LocalStorageAdapter.prototype.checkAvailability = function () {
187 |
188 | try {
189 |
190 | const x = '__storage_test__';
191 | localStorage.setItem(x, x);
192 | localStorage.removeItem(x);
193 | return true;
194 | }
195 | catch (e) {
196 | return false;
197 | }
198 | };
199 |
200 | export default LocalStorageAdapter;
201 |
--------------------------------------------------------------------------------
/src/trackers/FormTracker.js:
--------------------------------------------------------------------------------
1 | import Emitter from 'component-emitter';
2 | import objectAssing from '../functions/objectAssing';
3 | import each from '../functions/each';
4 | import {
5 | win,
6 | doc
7 | } from '../Browser';
8 | import createLogger from '../functions/createLogger';
9 | import {
10 | EVENT,
11 | EVENT_OPTION_OUTBOUND,
12 | EVENT_OPTION_TERMINATOR
13 | } from '../Constants';
14 | import {
15 | useCaptureSupport,
16 | removeHandler,
17 | addHandler
18 | } from '../functions/domEvents';
19 | import { closest } from '../lib/dom-utils';
20 |
21 |
22 | const log = createLogger('RST/FormTracker');
23 |
24 | const formTag = 'form';
25 | const elementsTags = ['input', 'checkbox', 'radio', 'textarea', 'select']; // ?option ?button ?submit
26 |
27 | const formEvents = ['submit'];
28 | const elementEvents = ['focus', 'blur', 'change', 'invalid'];
29 |
30 | const nn = (val) => val || '';
31 |
32 |
33 |
34 | /**
35 | * Process event type
36 | * @param {string} event event type
37 | * @return {object}
38 | */
39 | function prepareType(event) {
40 | return {
41 | event: nn(event)
42 | };
43 | }
44 |
45 |
46 | /**
47 | *
48 | * @param element {Element}
49 | * @return {object}
50 | */
51 | function extractFormData(element) {
52 | if (!element) {
53 | return {
54 | ferr: 'Form element absent'
55 | };
56 | }
57 |
58 | return {
59 | fmthd: nn(element.getAttribute('method')),
60 | fact: nn(element.getAttribute('action')),
61 | fname: nn(element.getAttribute('name')),
62 | fcls: nn(element.className),
63 | fid: nn(element.id)
64 | };
65 | }
66 |
67 | /**
68 | *
69 | * @param element {Element}
70 | * @return {object}
71 | */
72 | function extractElementData(element) {
73 | if (!element) {
74 | return {
75 | eerr: 'Input element absent'
76 | };
77 | }
78 | return {
79 | etag: nn(element.tagName && element.tagName.toLowerCase()),
80 | etype: nn(element.getAttribute('type')),
81 | ename: nn(element.getAttribute('name')),
82 | eph: nn(element.getAttribute('placeholder')),
83 | ecl: nn(element.className),
84 | eid: nn(element.id)
85 | };
86 | }
87 |
88 | /**
89 | *
90 | * @param options {object}
91 | * @constructor
92 | */
93 | const FormTracker = function (options) {
94 |
95 | this.options = objectAssing({}, options);
96 | this.initialized = false;
97 |
98 | this.formEventHandler = this.formEventHandler.bind(this);
99 | this.elementEventHandler = this.elementEventHandler.bind(this);
100 |
101 | this.initialize();
102 |
103 | };
104 |
105 | Emitter(FormTracker.prototype);
106 |
107 | FormTracker.prototype.initialize = function () {
108 |
109 | if (!useCaptureSupport) {
110 | return log.warn('addEventListener not supported');
111 | }
112 |
113 | each(formEvents, (event) => {
114 | addHandler(doc, event, this.formEventHandler, true);
115 | });
116 |
117 | each(elementEvents, (event) => {
118 | addHandler(doc, event, this.elementEventHandler, true);
119 | });
120 |
121 | this.initialized = true;
122 | };
123 |
124 | /**
125 | * Handler for form element events
126 | * @param ev {Event} Dom event
127 | */
128 | FormTracker.prototype.formEventHandler = function (ev) {
129 |
130 | const target = ev.target || ev.srcElement;
131 | const type = ev.type;
132 |
133 | const form = closest(target, formTag, true);
134 |
135 | if (form) {
136 | const event = {
137 | name: `form_${type}`,
138 | data: {
139 | ...prepareType(type),
140 | ...extractFormData(form)
141 | },
142 | options: {
143 | // [EVENT_OPTION_TERMINATOR]: (type === 'submit')
144 | }
145 | };
146 |
147 | this.emit(EVENT, event);
148 | }
149 | };
150 |
151 | /**
152 | * Handler for form inputs events
153 | * @param ev {Event} Dom event
154 | */
155 | FormTracker.prototype.elementEventHandler = function (ev) {
156 | const {
157 | type,
158 | target
159 | } = ev;
160 |
161 | if (!target) {
162 | return;
163 | }
164 | const element = closest(target, elementsTags.join(','), true);
165 | const form = element && closest(element, formTag);
166 |
167 | if (element) {
168 | const event = {
169 | name: `field_${type}`,
170 | data: {
171 | ...prepareType(type),
172 | ...extractElementData(element),
173 | ...extractFormData(form)
174 | }
175 | };
176 | this.emit(EVENT, event);
177 | }
178 | };
179 |
180 | /**
181 | * Unload handler
182 | */
183 | FormTracker.prototype.unload = function () {
184 |
185 | if (!this.initialized) {
186 | return log('Not initialized');
187 | }
188 |
189 | each(formEvents, (event) => {
190 | removeHandler(doc, event, this.formEventHandler, true);
191 | });
192 |
193 | each(elementEvents, (event) => {
194 | removeHandler(doc, event, this.elementEventHandler, true);
195 | });
196 | };
197 |
198 |
199 | export default FormTracker;
200 |
--------------------------------------------------------------------------------
/src/trackers/ActivityTracker.js:
--------------------------------------------------------------------------------
1 | import objectAssing from '../functions/objectAssing';
2 | import runOnStop from '../functions/runOnStop';
3 | import objectKeys from '../functions/objectKeys';
4 | import each from '../functions/each';
5 | import Emitter from 'component-emitter';
6 | import {
7 | win,
8 | doc,
9 | html,
10 | body
11 | } from "../Browser";
12 | import {
13 | useCaptureSupport,
14 | removeHandler,
15 | addHandler
16 | } from "../functions/domEvents";
17 | import {
18 | EVENT,
19 | EVENT_ACTIVITY,
20 | EVENT_SCROLL,
21 | } from "../Constants";
22 |
23 | const scrollEvent = 'scroll';
24 | const activityEvents = [
25 | 'touchmove', 'touchstart', 'touchleave', 'touchenter', 'touchend', 'touchcancel',
26 | 'click', 'mouseup', 'mousedown', 'mousemove', 'mousewheel', 'mousewheel', 'wheel',
27 | 'scroll', 'keypress', 'keydown', 'keyup', 'resize', 'focus', 'blur'
28 | ];
29 |
30 | /**
31 | * Returns document height
32 | * @return {number}
33 | */
34 | const getDocumentHeight = function () {
35 |
36 | return Math.max(0, html.offsetHeight, html.scrollHeight, body.offsetHeight, body.scrollHeight, body.clientHeight);
37 |
38 | };
39 |
40 | /**
41 | * Return current top offset
42 | * @return {number|*}
43 | */
44 | const getTopOffset = function () {
45 | const value = win.scrollY || html.scrollTop;
46 | return value > 0 ? Math.round(value) : 0;
47 | };
48 |
49 |
50 | /**
51 | * Returns screen height
52 | * @return {number}
53 | */
54 | const getClientHeight = function () {
55 | return Math.max(0, win.innerHeight || html.clientHeight);
56 | };
57 |
58 | /**
59 | *
60 | * @param options
61 | * @constructor
62 | */
63 | const ActivityTracker = function (options) {
64 |
65 | this.options = objectAssing({
66 | flushInterval: 5,
67 | zeroEvents: false,
68 | scrollEvents: true,
69 | domEvents: true
70 | }, options);
71 |
72 | // Activity handling
73 | this.iteration = 0;
74 | this.active = 0;
75 | this.counter = {};
76 |
77 | // Scroll variables
78 | this.maxPageScroll = 0;
79 | this.scrollState = {};
80 |
81 | this.eventHandler = this.eventHandler.bind(this);
82 | this.scrollHandlerWrapper = runOnStop(
83 | (e) => {
84 | this.handleScroll()
85 | if (this.options.scrollEvents){
86 | this.fireScrollEvent(e)
87 | }
88 | },
89 | 500
90 | );
91 |
92 | if (useCaptureSupport) {
93 | each(activityEvents, (event) => {
94 | addHandler(doc, event, this.eventHandler, true);
95 | });
96 |
97 | if (this.options.domEvents){
98 | this.activityFlushInterval = setInterval(
99 | () => this.fireActivityEvent(),
100 | this.options.flushInterval * 1000
101 | )
102 | }
103 |
104 | }
105 | };
106 |
107 | Emitter(ActivityTracker.prototype);
108 |
109 | /**
110 | *
111 | * @param emitter
112 | * @return {ActivityTracker}
113 | */
114 | ActivityTracker.prototype.subscribe = function (emitter) {
115 | return this;
116 | };
117 |
118 | /**
119 | * Main events handler
120 | * @param event
121 | */
122 | ActivityTracker.prototype.eventHandler = function (event) {
123 |
124 | const type = event.type;
125 |
126 | if (type === scrollEvent) {
127 | this.scrollHandlerWrapper();
128 | }
129 | this.counter[type] = (this.counter[type] || 0) + 1;
130 | };
131 |
132 |
133 | ActivityTracker.prototype.fireScrollEvent = function (e) {
134 | const event = {
135 | name: EVENT_SCROLL
136 | };
137 |
138 | this.emit(EVENT, event);
139 |
140 | };
141 |
142 | ActivityTracker.prototype.handleScroll = function () {
143 |
144 | const clientHeight = getClientHeight();
145 | const topOffset = getTopOffset();
146 | const docHeight = getDocumentHeight();
147 | const hiddenHeight = docHeight - clientHeight;
148 | const currentScroll = Math.min(
149 | 100,
150 | Math.max(
151 | 0,
152 | 100 * Math.round(hiddenHeight && (topOffset / hiddenHeight) || 0)
153 | )
154 | );
155 |
156 | this.maxPageScroll = currentScroll > this.maxPageScroll ?
157 | currentScroll :
158 | this.maxPageScroll;
159 |
160 | this.scrollState = {
161 | dh: docHeight,
162 | ch: clientHeight,
163 | to: topOffset,
164 | cs: currentScroll,
165 | ms: this.maxPageScroll
166 | };
167 | };
168 |
169 | ActivityTracker.prototype.getPositionData = function () {
170 | this.handleScroll();
171 | return this.scrollState;
172 | };
173 |
174 |
175 | ActivityTracker.prototype.getEnrichmentData = function () {
176 | this.handleScroll();
177 | return {
178 | scroll: this.scrollState
179 | };
180 | };
181 |
182 |
183 | /**
184 | * Emitting activity event
185 | */
186 | ActivityTracker.prototype.fireActivityEvent = function () {
187 | this.iteration++;
188 | // check activity present or enabled zero events submittion
189 | if (objectKeys(this.counter).length > 0 || this.options.zeroEvents) {
190 | const event = {
191 | name: EVENT_ACTIVITY,
192 | data: {
193 | interval: this.options.flushInterval,
194 | iteration: this.iteration,
195 | active: ++this.active
196 | }
197 | };
198 | objectAssing(event.data, this.counter);
199 | this.emit(EVENT, event);
200 | this.counter = {};
201 | }
202 | };
203 |
204 | /**
205 | * Clear state
206 | */
207 | ActivityTracker.prototype.clear = function () {
208 | if (this.options.domEvents){
209 | this.fireActivityEvent();
210 | }
211 | // Activity handling
212 | this.iteration = 0;
213 | this.active = 0;
214 | this.counter = {};
215 | this.maxPageScroll = 0;
216 | this.scrollState = {};
217 | };
218 |
219 | /**
220 | * Unload handler
221 | */
222 | ActivityTracker.prototype.unload = function () {
223 | if (this.options.domEvents){
224 | this.fireActivityEvent();
225 | clearInterval(this.activityFlushInterval);
226 | }
227 | each(activityEvents, (event) => {
228 | removeHandler(doc, event, this.eventHandler);
229 | });
230 |
231 | };
232 |
233 |
234 | export default ActivityTracker;
235 |
--------------------------------------------------------------------------------
/src/extensions/web-push/lib.js:
--------------------------------------------------------------------------------
1 |
2 | export class PushClient {
3 |
4 | constructor(stateChangeCb, subscriptionUpdate, publicAppKey) {
5 | this._stateChangeCb = stateChangeCb;
6 | this._subscriptionUpdate = subscriptionUpdate;
7 |
8 | // this._publicApplicationKey = window.base64UrlToUint8Array(publicAppKey);
9 |
10 | this._state = {
11 | UNSUPPORTED: {
12 | id: 'UNSUPPORTED',
13 | interactive: false,
14 | pushEnabled: false,
15 | },
16 | INITIALISING: {
17 | id: 'INITIALISING',
18 | interactive: false,
19 | pushEnabled: false,
20 | },
21 | PERMISSION_DENIED: {
22 | id: 'PERMISSION_DENIED',
23 | interactive: false,
24 | pushEnabled: false,
25 | },
26 | PERMISSION_GRANTED: {
27 | id: 'PERMISSION_GRANTED',
28 | interactive: true,
29 | },
30 | PERMISSION_PROMPT: {
31 | id: 'PERMISSION_PROMPT',
32 | interactive: true,
33 | pushEnabled: false,
34 | },
35 | ERROR: {
36 | id: 'ERROR',
37 | interactive: false,
38 | pushEnabled: false,
39 | },
40 | STARTING_SUBSCRIBE: {
41 | id: 'STARTING_SUBSCRIBE',
42 | interactive: false,
43 | pushEnabled: true,
44 | },
45 | SUBSCRIBED: {
46 | id: 'SUBSCRIBED',
47 | interactive: true,
48 | pushEnabled: true,
49 | },
50 | STARTING_UNSUBSCRIBE: {
51 | id: 'STARTING_UNSUBSCRIBE',
52 | interactive: false,
53 | pushEnabled: false,
54 | },
55 | UNSUBSCRIBED: {
56 | id: 'UNSUBSCRIBED',
57 | interactive: true,
58 | pushEnabled: false,
59 | },
60 | };
61 |
62 | if (!('serviceWorker' in navigator)) {
63 | this._stateChangeCb(this._state.UNSUPPORTED, 'Service worker not ' +
64 | 'available on this browser');
65 | return;
66 | }
67 |
68 | if (!('PushManager' in window)) {
69 | this._stateChangeCb(this._state.UNSUPPORTED, 'PushManager not ' +
70 | 'available on this browser');
71 | return;
72 | }
73 |
74 | if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
75 | this._stateChangeCb(this._state.UNSUPPORTED, 'Showing Notifications ' +
76 | 'from a service worker is not available on this browser');
77 | return;
78 | }
79 |
80 | navigator.serviceWorker.ready
81 | .then(() => {
82 | this._stateChangeCb(this._state.INITIALISING);
83 | this.setUpPushPermission();
84 | });
85 | }
86 |
87 | _permissionStateChange(permissionState) {
88 | // If the notification permission is denied, it's a permanent block
89 | switch (permissionState) {
90 | case 'denied':
91 | this._stateChangeCb(this._state.PERMISSION_DENIED);
92 | break;
93 | case 'granted':
94 | this._stateChangeCb(this._state.PERMISSION_GRANTED);
95 | break;
96 | case 'default':
97 | this._stateChangeCb(this._state.PERMISSION_PROMPT);
98 | break;
99 | default:
100 | console.error('Unexpected permission state: ', permissionState);
101 | break;
102 | }
103 | }
104 |
105 | setUpPushPermission() {
106 | this._permissionStateChange(Notification.permission);
107 |
108 | return navigator.serviceWorker.ready
109 | .then((serviceWorkerRegistration) => {
110 | // Let's see if we have a subscription already
111 | return serviceWorkerRegistration.pushManager.getSubscription();
112 | })
113 | .then((subscription) => {
114 | if (!subscription) {
115 | // NOOP since we have no subscription and the permission state
116 | // will inform whether to enable or disable the push UI
117 | return;
118 | }
119 |
120 | this._stateChangeCb(this._state.SUBSCRIBED);
121 |
122 | // Update the current state with the
123 | // subscriptionid and endpoint
124 | this._subscriptionUpdate(subscription);
125 | })
126 | .catch((err) => {
127 | console.log('setUpPushPermission() ', err);
128 | this._stateChangeCb(this._state.ERROR, err);
129 | });
130 | }
131 |
132 | subscribeDevice() {
133 | this._stateChangeCb(this._state.STARTING_SUBSCRIBE);
134 |
135 | return new Promise((resolve, reject) => {
136 | if (Notification.permission === 'denied') {
137 | return reject(new Error('Push messages are blocked.'));
138 | }
139 |
140 | if (Notification.permission === 'granted') {
141 | return resolve();
142 | }
143 |
144 | if (Notification.permission === 'default') {
145 | Notification.requestPermission((result) => {
146 | if (result !== 'granted') {
147 | reject(new Error('Bad permission result'));
148 | }
149 |
150 | resolve();
151 | });
152 | }
153 | })
154 | .then(() => {
155 | // We need the service worker registration to access the push manager
156 | return navigator.serviceWorker.ready
157 | .then((serviceWorkerRegistration) => {
158 | return serviceWorkerRegistration.pushManager.subscribe(
159 | {
160 | userVisibleOnly: true,
161 | // applicationServerKey: this._publicApplicationKey,
162 | }
163 | );
164 | })
165 | .then((subscription) => {
166 | this._stateChangeCb(this._state.SUBSCRIBED);
167 | this._subscriptionUpdate(subscription);
168 | })
169 | .catch((subscriptionErr) => {
170 | this._stateChangeCb(this._state.ERROR, subscriptionErr);
171 | });
172 | })
173 | .catch(() => {
174 | // Check for a permission prompt issue
175 | this._permissionStateChange(Notification.permission);
176 | });
177 | }
178 |
179 | unsubscribeDevice() {
180 | // Disable the switch so it can't be changed while
181 | // we process permissions
182 | // window.PushDemo.ui.setPushSwitchDisabled(true);
183 |
184 | this._stateChangeCb(this._state.STARTING_UNSUBSCRIBE);
185 |
186 | navigator.serviceWorker.ready
187 | .then((serviceWorkerRegistration) => {
188 | return serviceWorkerRegistration.pushManager.getSubscription();
189 | })
190 | .then((pushSubscription) => {
191 | // Check we have everything we need to unsubscribe
192 | if (!pushSubscription) {
193 | this._stateChangeCb(this._state.UNSUBSCRIBED);
194 | this._subscriptionUpdate(null);
195 | return;
196 | }
197 |
198 | // You should remove the device details from the server
199 | // i.e. the pushSubscription.endpoint
200 | return pushSubscription.unsubscribe()
201 | .then(function(successful) {
202 | if (!successful) {
203 | // The unsubscribe was unsuccessful, but we can
204 | // remove the subscriptionId from our server
205 | // and notifications will stop
206 | // This just may be in a bad state when the user returns
207 | console.error('We were unable to unregister from push');
208 | }
209 | });
210 | })
211 | .then(() => {
212 | this._stateChangeCb(this._state.UNSUBSCRIBED);
213 | this._subscriptionUpdate(null);
214 | })
215 | .catch((err) => {
216 | console.error('Error thrown while revoking push notifications. ' +
217 | 'Most likely because push was never registered', err);
218 | });
219 | }
220 | }
--------------------------------------------------------------------------------
/src/Transport.js:
--------------------------------------------------------------------------------
1 | // import Sockette from 'sockette';
2 | import Emitter from 'component-emitter';
3 | import queryStringify from 'qs/lib/stringify';
4 | // import Promise from 'promise-polyfill';
5 |
6 | import {
7 | win,
8 | doc,
9 | nav
10 | } from './Browser';
11 | import {
12 | hasBeaconSupport,
13 | hasFetchSupport,
14 | hasXHRWithCreds,
15 | hasXHRSupport,
16 | hasXDRSupport,
17 | hasAnyXRSupport,
18 | } from './data/browserCharacts';
19 | import {
20 | EVENT_OPTION_OUTBOUND,
21 | EVENT_OPTION_TERMINATOR,
22 | EVENT_OPTION_REQUEST,
23 | EVENT_OPTION_TRANSPORT_IMG,
24 | SERVER_MESSAGE,
25 | INTERNAL_EVENT,
26 | SERVICE_TRACK,
27 | } from './Constants';
28 | import objectAssign from './functions/objectAssing';
29 | import createLogger from './functions/createLogger';
30 | import nextTick from './functions/nextTick'
31 | // import simpleHash from './functions/simpleHash';
32 | import { cyrb53 } from './functions/cyrb53';
33 | // import { isObject } from './functions/type';
34 |
35 | const HTTPS = 'https';
36 |
37 | const log = createLogger('RST/Transport');
38 | const noop = () => { };
39 |
40 | const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
41 |
42 | /**
43 | * Transport class containing general connecting methods
44 | * @param {Object} options transport options
45 | * @constructor
46 | * @property {Object} creds
47 | * @class
48 | *
49 | */
50 | export function Transport(options) {
51 | this.creds = {};
52 | this.options = objectAssign({
53 | responseTimeout: 10000
54 | },
55 | options
56 | );
57 | this.pathPrefix = options.pathPrefix;
58 | this.server = this.options.server;
59 | this.urlMark = this.options.urlMark;
60 | this.servicesMap = {
61 | 'track': 't4k'
62 | }
63 | this.msgCounter = new Date() - 1514764800000;
64 | this.waitCallers = {};
65 | };
66 |
67 |
68 | // Extending Emitter
69 | Emitter(Transport.prototype);
70 |
71 |
72 | /**
73 | * Set default credentials that used to send data to server
74 | * @param {Object} creds object containing creds
75 | * @returns {Transport}
76 | */
77 | Transport.prototype.setCreds = function (creds) {
78 | this.creds = objectAssign({}, creds);
79 | return this;
80 | }
81 |
82 |
83 | /**
84 | * Transform path to query url
85 | * @param {string} path
86 | * @param {Object} data
87 | * @returns {Transport}
88 | */
89 | Transport.prototype.makeURL = function (path, data = {}, proto = HTTPS) {
90 | const query = queryStringify(data);
91 | return `${proto}://${this.server}${this.pathPrefix}${path}?${query}`;
92 | }
93 |
94 |
95 | /*
96 | if (false && hasFetchSupport) {
97 | return fetch(postUrl, {
98 | method: 'POST',
99 | body: data,
100 | keepalive: true
101 | }).then((response) => {
102 | return response.text()
103 | .then((responseText) => {
104 | console.log('resp text', responseText, response.status, response.status === 200);
105 | if (response.status === 200) {
106 | try {
107 | return Promise.resolve(JSON.parse(responseText));
108 | } catch (e) {
109 | log.error(e)
110 | return Promise.reject(e);
111 | }
112 | }
113 | });
114 | }).catch((error) => {
115 | log.warn('Fetch failed', error);
116 | });
117 | }
118 | */
119 |
120 | /**
121 | *
122 | * @param msg {Object}
123 | * @param query {Array}
124 | * @param options {Object}
125 | * @returns {Promise}
126 | *
127 | * TODO: Use sendBeacon when unloading instead of img
128 | *
129 | */
130 | Transport.prototype.send = function (msg, options = {}) {
131 | const data = JSON.stringify(msg);
132 | const dig = cyrb53(data);
133 | const isRequest = !!options[EVENT_OPTION_REQUEST];
134 | // !options[EVENT_OPTION_TERMINATOR] || !!options[EVENT_OPTION_OUTBOUND] ||
135 | const useTransportImg = !!options[EVENT_OPTION_TRANSPORT_IMG];
136 | const _service = this.servicesMap[msg.service] || msg.service;
137 | const postPath = `/${this.urlMark}/${_service}.json`;
138 | const imgPath = `/${this.urlMark}/${_service}.gif`;
139 |
140 | const postUrlBeacon = this.makeURL(postPath, { "dig": dig, "td_trans": "b"});
141 | const postUrlFetch = this.makeURL(postPath, { "dig": dig, "td_trans": "f" });
142 | // Use extra transport - img
143 | // Send only part when using gif
144 | return new Promise((resolve, reject) => {
145 | if (useTransportImg) {
146 | try {
147 | const smallMsg = (msg.service === SERVICE_TRACK)
148 | ? this.options.msgCropper(msg) : msg;
149 | log.info(`Trying with img transport`, smallMsg);
150 |
151 | const imgUrl = this.makeURL(imgPath, objectAssign(smallMsg, this.creds));
152 |
153 | const img = win.Image ? (new Image(1, 1)) : doc.createElement('img');
154 | img.src = imgUrl;
155 | return nextTick(() => resolve());
156 | } catch (e) {
157 | return nextTick(() => reject(e));
158 | }
159 | }
160 |
161 | try {
162 |
163 | let beaconResult = false;
164 |
165 | if (this.options.allowSendBeacon && hasBeaconSupport) {
166 | log.info('Sending using beacon');
167 | beaconResult = nav.sendBeacon(postUrlBeacon, data);
168 | }
169 |
170 | if (beaconResult) {
171 | return nextTick(() => resolve());
172 | }
173 |
174 | if (hasFetchSupport) {
175 | log.info('Sending using fetch');
176 | fetch(postUrlFetch, {
177 | method: 'POST',
178 | body: data,
179 | keepalive: true
180 | }).then((response) => {
181 | return response.then((responseText) => {
182 | if (response.status === 200) {
183 | try {
184 | return JSON.parse(responseText);
185 | } catch (e) {
186 | return Promise.reject(e)
187 | }
188 | }
189 | });
190 | }).then((data) => {
191 | return resolve(data)
192 | }).catch((e) => {
193 | return reject(e)
194 | });
195 | }
196 | } catch (e) {
197 | log.warn('Fetch err', e);
198 | }
199 | return reject(e)
200 | });
201 |
202 |
203 | // => response.text()
204 | // .then((responseText) => {
205 | // console.log('resp text', responseText, response.status, response.status === 200);
206 | // if (response.status === 200) {
207 | // try {
208 | // resolve(JSON.parse(responseText));
209 | // } catch (e) {
210 | // log.error(e)
211 | // return reject(e);
212 | // }
213 | // }
214 | // }
215 |
216 |
217 |
218 | // return new Promise((resolve, reject) => {
219 |
220 | // const doSendBeacon = (attempt) => {
221 | // log.warn(`Sending beacon. Attempt ${attempt}`);
222 | // let res = nav.sendBeacon(postUrl, data);
223 | // if (res === true) {
224 | // return resolve();
225 | // }
226 | // if (attempt > 0) {
227 | // setTimeout(doSendBeacon(attempt - 1), 200);
228 | // } else {
229 | // nextTick(() => doSendImg());
230 | // }
231 | // }
232 |
233 | // const doFetch = () => {
234 |
235 |
236 |
237 | // }
238 |
239 | // // if (false && hasFetchSupport) {
240 | // // }
241 |
242 | // if (isRequest) {
243 | // return doFetch();
244 | // // return reject('Requested request transport, but method unavailable');
245 | // }
246 |
247 | // if (useTransportImg) {
248 | // return doSendImg();
249 | // }
250 | // } catch (error) {
251 | // log.warn('Beacon/XHR failed', error);
252 | // return reject(error);
253 | // }
254 |
255 | // try {
256 | // return this.sendIMG(this.makeURL(imgPath, objectAssign(smallMsg, this.creds)));
257 | // } catch (e) {
258 | // log.warn('Error during sending data using image', e);
259 | // return Promise.reject(e);
260 | // }
261 |
262 | // });
263 |
264 |
265 |
266 |
267 |
268 | };
269 |
--------------------------------------------------------------------------------
/src/trackers/SessionTracker.js:
--------------------------------------------------------------------------------
1 | import objectAssign from '../functions/objectAssing';
2 | import createLogger from '../functions/createLogger';
3 | import pageSource from '../functions/pageSource';
4 | import { cleanUid } from '../functions/isValidUid';
5 | import Emitter from 'component-emitter';
6 |
7 | import {
8 | EVENT_PAGEVIEW,
9 | EVENT_SESSION,
10 | SESSION_ORGANIC,
11 | SESSION_CAMPAIGN,
12 | SESSION_SOCIAL,
13 | SESSION_PARTNER,
14 | EVENT_USER_PARAMS,
15 | EVENT,
16 | EVENT_SIMULATE_SESSION
17 | } from '../Constants';
18 | import { tstz } from '../data/browserData';
19 |
20 | const KEY_LAST_EVENT_TS = 'levent';
21 | const KEY_LAST_CAMPAIGN = 'lcamp';
22 | const KEY_LAST_SESSION = 'lsess';
23 | const KEY_LAST_SESSION_TS = 'lsts';
24 | const KEY_SESSION_COUNTER = 'csess';
25 | const KEY_PAGES_COUNTER = 'scpages';
26 | const KEY_EVENTS_COUNTER = 'scevs';
27 | const KEY_UID = 'uid';
28 | const KEY_SESS_START = 'sstart';
29 | const KEY_USER_ID = 'userid';
30 | const KEY_USER_TRAITS = 'usertr';
31 | const KEY_USER_PARAMS = 'userpr';
32 |
33 | const log = createLogger('RST/SessionTracker');
34 |
35 | function SessionTracker(tracker, options) {
36 |
37 | this.localStorage = tracker.localStorage;
38 | this.cookieStorage = tracker.cookieStorage;
39 |
40 | this.storage = this.localStorage;
41 |
42 | this.options = options;
43 | this.lastSession = null;
44 | this.initialUid = null;
45 | this.userId = undefined;
46 | this.userTraits = undefined;
47 | this.uid = null;
48 | this.userParams = this.getUserParams();
49 | }
50 |
51 | Emitter(SessionTracker.prototype);
52 |
53 | SessionTracker.prototype.initialize = function (emitter) {
54 |
55 | // Fill last sessions
56 | if (!this.lastSession) {
57 | this.lastSession = this.storage.get(KEY_LAST_SESSION);
58 | this.lastCampaign = this.storage.get(KEY_LAST_CAMPAIGN);
59 | }
60 | emitter.on(EVENT_USER_PARAMS, this.setUserParams.bind(this));
61 | return this;
62 | };
63 |
64 |
65 | SessionTracker.prototype.fireSessionEvent = function () {
66 | this.emit(EVENT, {
67 | name: EVENT_SESSION,
68 | data: {}
69 | });
70 | };
71 |
72 |
73 | SessionTracker.prototype.getStoredUid = function () {
74 | return (
75 | cleanUid(this.cookieStorage.get(KEY_UID)) ||
76 | cleanUid(this.localStorage.get(KEY_UID))
77 | )
78 | };
79 |
80 |
81 | SessionTracker.prototype.setStoredUid = function (uid) {
82 |
83 | this.storage.set(KEY_UID, uid);
84 | this.cookieStorage.set(KEY_UID, uid);
85 |
86 | };
87 |
88 |
89 | SessionTracker.prototype.handleUid = function (uid) {
90 |
91 | log(`Handling initial ${uid}`);
92 | this.initialUid = uid;
93 | this.uid = this.getStoredUid() || this.initialUid;
94 | // Saving uid
95 | this.setStoredUid(this.uid);
96 | return this;
97 | };
98 |
99 | /**
100 | * Get user credentials
101 | * @returns {Object} containing main user credentials
102 | */
103 | SessionTracker.prototype.creds = function () {
104 | return {
105 | uid: this.uid
106 | }
107 | }
108 |
109 | SessionTracker.prototype.getUid = function () {
110 |
111 | return this.uid;
112 |
113 | };
114 |
115 | SessionTracker.prototype.getPageNum = function () {
116 | return this.storage.get(KEY_PAGES_COUNTER, {
117 | session: true
118 | });
119 | };
120 |
121 | /**
122 | * Get state of session events counter
123 | * @return {Number} current value
124 | */
125 | SessionTracker.prototype.getEventNum = function () {
126 | return this.storage.get(KEY_EVENTS_COUNTER, {
127 | session: true
128 | });
129 | };
130 |
131 |
132 | /**
133 | * Get state of sessions counter
134 | * @return {Number} current value
135 | */
136 | SessionTracker.prototype.getSessNum = function () {
137 | return this.storage.get(KEY_SESSION_COUNTER);
138 | };
139 |
140 |
141 | /**
142 | * Get session data
143 | * @return {Object} session data
144 | */
145 | SessionTracker.prototype.sessionData = function () {
146 |
147 | return objectAssign(
148 | {
149 | pageNum: this.getPageNum(),
150 | eventNum: this.getEventNum()
151 | },
152 | this.lastSession,
153 | {
154 | refHash: undefined
155 | }
156 | );
157 | };
158 |
159 | SessionTracker.prototype.getUserParams = function () {
160 |
161 | const params = this.storage.get(KEY_USER_PARAMS) || {};
162 | //
163 | // Here can be located paramans migration code
164 | //
165 | return params;
166 | };
167 |
168 | SessionTracker.prototype.setUserParams = function (params) {
169 |
170 | this.userParams = objectAssign(this.userParams, params);
171 | this.storage.set(KEY_USER_PARAMS, this.userParams);
172 |
173 | };
174 |
175 | /**
176 | *
177 | * @return {*}
178 | */
179 | SessionTracker.prototype.userData = function () {
180 |
181 | const id = this.storage.get(KEY_USER_ID);
182 | const traits = this.storage.get(KEY_USER_TRAITS);
183 | const params = this.storage.get(KEY_USER_PARAMS);
184 |
185 | return objectAssign(
186 | {},
187 | params,
188 | traits,
189 | tstz(),
190 | { id }
191 | );
192 | };
193 |
194 | SessionTracker.prototype.setUserData = function (data) {
195 |
196 | if (data.userId) {
197 | this.storage.set(KEY_USER_ID, String(data.userId));
198 | this.userId = data.userId;
199 | }
200 |
201 | if (data.userTraits) {
202 |
203 | const traits = this.storage.get(KEY_USER_TRAITS) || {};
204 | this.userTraits = objectAssign(traits, data.userTraits);
205 | this.storage.set(KEY_USER_TRAITS, this.userTraits);
206 | }
207 | };
208 |
209 |
210 | SessionTracker.prototype.handleEvent = function (name, data, page) {
211 |
212 | // Skipping own events
213 | if (name === EVENT_SESSION) {
214 | return null;
215 | }
216 |
217 | const lastEventTS = this.storage.get(KEY_LAST_EVENT_TS);
218 |
219 | // Setting last event
220 | const now = (new Date()).getTime();
221 | this.storage.set(KEY_LAST_EVENT_TS, now);
222 |
223 | // Starting new session if needed
224 |
225 | let source;
226 | let sourceRestart;
227 |
228 | const simulation = name === EVENT_SIMULATE_SESSION;
229 | const sessionTimedOut = lastEventTS === undefined || (now - lastEventTS) > this.options.sessionTimeout * 1000;
230 |
231 | if (sessionTimedOut || simulation || name === EVENT_PAGEVIEW) {
232 | source = pageSource(page);
233 | sourceRestart = this.sourceRestart(source);
234 | }
235 |
236 | const shouldRestart = sessionTimedOut || sourceRestart || simulation;
237 |
238 | if (shouldRestart) {
239 | this.restart(source, now);
240 | }
241 |
242 | // Increment counters
243 | if (name === EVENT_PAGEVIEW) {
244 | this.storage.inc(KEY_PAGES_COUNTER, {
245 | session: true
246 | });
247 | }
248 |
249 | this.storage.inc(KEY_EVENTS_COUNTER, {
250 | session: true
251 | });
252 |
253 |
254 | // Emitting session event
255 | if (shouldRestart) {
256 | this.fireSessionEvent();
257 | }
258 | };
259 |
260 | SessionTracker.prototype.sourceRestart = function (source) {
261 |
262 | let byRef = false;
263 | const pastSession = this.storage.get(KEY_LAST_SESSION);
264 |
265 | let pastSessionMarksHash = ''
266 |
267 | if (pastSession) {
268 | if (pastSession.marksHash) {
269 | pastSessionMarksHash = pastSession.marksHash;
270 | }
271 | byRef = pastSession.refHash !== source.refHash;
272 | }
273 |
274 | if (source.type === SESSION_PARTNER) {
275 | if (source.marksHash !== pastSessionMarksHash) {
276 | return true;
277 | }
278 | }
279 |
280 | if (source.type === SESSION_CAMPAIGN) {
281 | if (source.marksHash !== pastSessionMarksHash) {
282 | return true;
283 | }
284 | return byRef;
285 | }
286 |
287 | if (source.type === SESSION_ORGANIC) {
288 | return byRef;
289 | }
290 |
291 | // if (source.type === SESSION_SOCIAL) {
292 | // return byRef;
293 | // }
294 |
295 | return false;
296 |
297 | // // Override session if got organic or campaign
298 | // const bySource =
299 | // source.type === SESSION_PARTNER ||
300 | // source.type === SESSION_ORGANIC ||
301 | // source.type === SESSION_CAMPAIGN ||
302 | // source.type === SESSION_SOCIAL;
303 |
304 | // // Prevent restarting by refresh enter page
305 |
306 | // return bySource && byRef;
307 |
308 | };
309 |
310 | SessionTracker.prototype.restart = function (source, now) {
311 | log('restart session');
312 |
313 | source = source || {};
314 | now = now || (new Date()).getTime();
315 |
316 | source.num = this.storage.inc(KEY_SESSION_COUNTER);
317 | source.start = now;
318 |
319 | // Cleaning old session vars
320 | this.storage.rmAll({
321 | session: true
322 | });
323 |
324 | // Updating counters
325 | this.storage.set(KEY_LAST_SESSION_TS, now, {
326 | session: true
327 | });
328 | this.storage.set(KEY_PAGES_COUNTER, 0, {
329 | session: true
330 | });
331 | this.storage.set(KEY_EVENTS_COUNTER, 0, {
332 | session: true
333 | });
334 |
335 | // Set cookie with session start time (need for generating session id at app backend)
336 | this.cookieStorage.set(KEY_SESS_START, source.start, {
337 | session: true
338 | });
339 |
340 | // Saving last session
341 | this.storage.set(KEY_LAST_SESSION, source);
342 | this.lastSession = source;
343 |
344 | // Applying last campaign
345 | if (source.type === SESSION_CAMPAIGN) {
346 | this.storage.set(KEY_LAST_CAMPAIGN, source, {
347 | exp: 7776000
348 | });
349 | this.lastCampaign = source;
350 | }
351 | };
352 |
353 | export default SessionTracker;
354 |
--------------------------------------------------------------------------------
/src/functions/pageSource.js:
--------------------------------------------------------------------------------
1 | // import objectAssign from './objectAssing';
2 | // import getOsMarks from './getOpenStatMarks';
3 | import createLogger from './createLogger';
4 | import simpleHash from './simpleHash';
5 |
6 | import removeWww from './removeWww';
7 | import objectKeys from './objectKeys';
8 | import { isArray } from './type';
9 | import urlParse from 'url-parse';
10 | import punycode from 'punycode';
11 |
12 | import {
13 | SESSION_CAMPAIGN,
14 | SESSION_DIRECT,
15 | SESSION_INTERNAL,
16 | SESSION_ORGANIC,
17 | SESSION_REFERRAL,
18 | SESSION_SOCIAL,
19 | SESSION_PARTNER,
20 | SESSION_WEBVIEW,
21 | } from "../Constants";
22 |
23 | const qs = urlParse.qs;
24 |
25 | const ENGINE_GOOGLE = 'google';
26 | const ENGINE_YANDEX = 'yandex';
27 | const ENGINE_MAILRU = 'mailru';
28 | const ENGINE_RAMBLER = 'rambler';
29 | const ENGINE_BING = 'bing';
30 | const ENGINE_YAHOO = 'yahoo';
31 | const ENGINE_NIGMA = 'nigma';
32 | const ENGINE_DUCKDUCKGO = 'duckduckgo';
33 |
34 | const ENGINE_FACEBOOK = 'fb';
35 | const ENGINE_TWITTER = 'twitter';
36 | const ENGINE_TIKTOK = 'tiktok';
37 | const ENGINE_VK = 'vk';
38 | const ENGINE_OK = 'ok';
39 | const ENGINE_LINKEDIN = 'linkedin';
40 | const ENGINE_INSTAGRAM = 'instagram';
41 | const ENGINE_TELEGRAM = 'telegram';
42 | const ENGINE_YOUTUBE = 'youtube';
43 |
44 | const UTMS = ['utm_source', 'utm_campaign', 'utm_content', 'utm_medium', 'utm_term'];
45 | const PARTNER_IDS = ['pid', 'cid', 'sub1', 'sub2', 'sub3', 'sub4', 'sub5'];
46 | const YCLID = 'yclid';
47 | const GCLID = 'gclid';
48 | const FBCLID = 'fbclid';
49 | const WEBVIEW_PARAM = 'inWebView';
50 | const UID_PARAM = 'uid';
51 |
52 | const RULES = [
53 |
54 | { domain: 'plus.google.com', engine: ENGINE_GOOGLE, type: SESSION_SOCIAL },
55 | { domain: 'plus.url.google.com', engine: ENGINE_GOOGLE, type: SESSION_SOCIAL },
56 | { domain: 'google.com.', engine: ENGINE_GOOGLE, type: SESSION_ORGANIC },
57 | { domain: 'google.', engine: ENGINE_GOOGLE, type: SESSION_ORGANIC },
58 |
59 | { domain: 'yandex.', param: 'text', engine: ENGINE_YANDEX, type: SESSION_ORGANIC },
60 | { domain: 'go.mail.ru', param: 'q', engine: ENGINE_MAILRU, type: SESSION_ORGANIC },
61 | { domain: 'nigma.ru', param: 's', engine: ENGINE_NIGMA, type: SESSION_ORGANIC },
62 | { domain: 'rambler.ru', param: 'query', engine: ENGINE_RAMBLER, type: SESSION_ORGANIC },
63 | { domain: 'bing.com', param: 'q', engine: ENGINE_BING, type: SESSION_ORGANIC },
64 | { domain: 'yahoo.com', param: 'p', engine: ENGINE_YAHOO, type: SESSION_ORGANIC },
65 | { domain: 'duckduckgo.com', engine: ENGINE_DUCKDUCKGO, type: SESSION_ORGANIC },
66 |
67 | { domain: 'com.google.android.', engine: ENGINE_GOOGLE, type: SESSION_ORGANIC }, // app
68 |
69 | { domain: 'facebook.com', engine: ENGINE_FACEBOOK, type: SESSION_SOCIAL },
70 |
71 | { domain: 'tiktok.com', engine: ENGINE_TIKTOK, type: SESSION_SOCIAL },
72 |
73 | { domain: 'instagram.com', engine: ENGINE_INSTAGRAM, type: SESSION_SOCIAL }, //l.instagram.com
74 |
75 | { domain: 'org.telegram.', engine: ENGINE_TELEGRAM, type: SESSION_SOCIAL }, // app
76 | { domain: 'telegram.org', engine: ENGINE_TELEGRAM, type: SESSION_SOCIAL },
77 | { domain: 't.me', engine: ENGINE_TELEGRAM, type: SESSION_SOCIAL },
78 |
79 | { domain: 'vk.com', engine: ENGINE_VK, type: SESSION_SOCIAL }, // away.vk.com
80 | { domain: 'linkedin.com', engine: ENGINE_LINKEDIN, type: SESSION_SOCIAL },
81 | { domain: 'lnkd.in', engine: ENGINE_LINKEDIN, type: SESSION_SOCIAL },
82 |
83 | { domain: 'ok.ru', engine: ENGINE_OK, type: SESSION_SOCIAL },
84 |
85 | { domain: 't.co', engine: ENGINE_TWITTER, type: SESSION_SOCIAL },
86 |
87 | { domain: 'googlesyndication.com', engine: ENGINE_GOOGLE, type: SESSION_CAMPAIGN },
88 | { domain: 'googlesyndication.com', engine: ENGINE_GOOGLE, type: SESSION_CAMPAIGN },
89 | { domain: 'googleadservices.com', engine: ENGINE_GOOGLE, type: SESSION_CAMPAIGN },
90 | { domain: 'doubleclick.net', engine: ENGINE_GOOGLE, type: SESSION_CAMPAIGN },
91 |
92 | { domain: 'youtube.com', engine: ENGINE_YOUTUBE, type: SESSION_SOCIAL },
93 | ];
94 |
95 |
96 | const log = createLogger('RST/PageSource');
97 |
98 | const cleanQueryParam = function (val) {
99 |
100 | // processing multiple marks
101 | if (isArray(val)) val = val[0];
102 |
103 | // To string
104 | val = String(val);
105 |
106 | // processing url encoded marks
107 | if (val.indexOf('%') >= 0) {
108 | val = decodeURIComponent(val);
109 | }
110 |
111 | return val;
112 |
113 | };
114 |
115 |
116 | export default function pageSource(page) {
117 |
118 | // Initial session type
119 |
120 | const source = {
121 | type: SESSION_DIRECT,
122 | marks: {},
123 | marksHash: '',
124 | hasMarks: false,
125 | refHash: '',
126 | engine: undefined,
127 | refhost: '',
128 | uid: undefined
129 | };
130 |
131 | log.info(page)
132 |
133 | let query = {};
134 | let queryKeys = [];
135 | let has_utm = false;
136 | let has_partner_ids = false;
137 | let has_os = false;
138 | let has_yclid = false;
139 | let has_gclid = false;
140 | let has_fbclid = false;
141 | let is_webview = false;
142 |
143 | let marksCampaignString = ''
144 | let marksPartnerString = ''
145 |
146 |
147 | // Preparing query params
148 | if (page.query) {
149 | query = qs.parse(page.query);
150 | queryKeys = objectKeys(query);
151 | let queryParamVal = '';
152 |
153 | // Processing marks
154 | for (let i = 0; i < queryKeys.length; i++) {
155 | const key = queryKeys[i];
156 | // UTM
157 | for (let j = 0; j < UTMS.length; j++) {
158 | if (key === UTMS[j]) {
159 | queryParamVal = cleanQueryParam(query[key]);
160 | source.marks[key] = queryParamVal;
161 | marksCampaignString += queryParamVal + '|';
162 | source.hasMarks = true;
163 | has_utm = true;
164 | }
165 | }
166 | queryParamVal = '';
167 |
168 | // Partner
169 | for (let j = 0; j < PARTNER_IDS.length; j++) {
170 | if (key === PARTNER_IDS[j]) {
171 | queryParamVal = cleanQueryParam(query[key]);
172 | marksPartnerString += queryParamVal + '|'
173 | source.marks[key] = queryParamVal;
174 | source.hasMarks = true;
175 | has_partner_ids = true;
176 | }
177 | }
178 | queryParamVal = '';
179 |
180 | // WebView
181 | if (key === WEBVIEW_PARAM) {
182 | if (cleanQueryParam(query[key]) == 'true' || cleanQueryParam(query[key]) == '1') {
183 | is_webview = true;
184 | }
185 | }
186 |
187 |
188 | // YClid
189 | if (key === YCLID) {
190 | source.hasMarks = true;
191 | source.marks['has_' + key] = 1;
192 | source.marks[key] = query[key];
193 | marksCampaignString += query[key] + '|';
194 | has_yclid = true;
195 | }
196 |
197 | // GClid
198 | if (key === GCLID) {
199 | source.hasMarks = true;
200 | source.marks['has_' + key] = 1;
201 | source.marks[key] = query[key];
202 | marksCampaignString += query[key] + '|';
203 | has_gclid = true;
204 | }
205 |
206 | // FBClid
207 | if (key === FBCLID) {
208 | source.hasMarks = true;
209 | source.marks['has_' + key] = 1;
210 | source.marks[key] = query[key];
211 | // marksCampaignString += query[key] + '|';
212 | has_fbclid = true;
213 | }
214 |
215 | if (key === UID_PARAM) {
216 | source.uid = query[key];
217 | }
218 | }
219 | }
220 |
221 | // Processing ref
222 | const ref = page.ref ? urlParse(page.ref) : '';
223 | source.refHash = simpleHash(page.ref);
224 |
225 | // Direct with marks -> campaign
226 | // Direct with fbclid -> facebooj social
227 | // if (ref === '') {
228 | // if (has_utm || has_os || has_gclid || has_yclid) {
229 | // source.type = SESSION_CAMPAIGN;
230 | // source.marksHash = simpleHash(marksCampaignString)
231 | // }
232 | // if (has_partner_ids) {
233 | // source.type = SESSION_PARTNER;
234 | // source.marksHash = simpleHash(marksPartnerString)
235 | // }
236 | // if(has_fbclid){
237 | // source.type = SESSION_SOCIAL
238 | // source.engine = ENGINE_FACEBOOK
239 | // }
240 | // if (is_webview){
241 | // source.type = SESSION_WEBVIEW;
242 | // }
243 | // return source;
244 | // }
245 |
246 |
247 | /**
248 | * Sessions based on ref
249 | */
250 | if (ref !== '') {
251 |
252 | source.refhost = punycode.toUnicode(removeWww(ref.hostname));
253 |
254 | /**
255 | * ORGANIN/SOCIAL/CAMPAIGN(some) SESSION
256 | *
257 | * based on rules list
258 | *
259 | */
260 | if (source.type !== SESSION_INTERNAL) {
261 | const refDomainParts = source.refhost.split('.').reverse();
262 |
263 | for (let i = 0; i < RULES.length; i++) {
264 | const rule = RULES[i];
265 | const ruleDomainParts = rule.domain.split('.').reverse();
266 | const refDomainPartsClone = refDomainParts.slice();
267 |
268 | if (ruleDomainParts[0] === '') {
269 | ruleDomainParts.shift();
270 | refDomainPartsClone.shift();
271 | }
272 |
273 | if (refDomainPartsClone.slice(0, ruleDomainParts.length).join('.') === ruleDomainParts.join('.')) {
274 |
275 | source.type = rule.type;
276 | source.engine = rule.engine;
277 |
278 | if (rule.param && query[rule.param]) {
279 | source.keyword = cleanQueryParam(query[rule.param]);
280 | }
281 |
282 | break;
283 | }
284 | }
285 | }
286 |
287 | /**
288 | * REFERRAL SESSION
289 | *
290 | * Any source except rules list
291 | *
292 | */
293 | if (!source.engine) {
294 | source.type = SESSION_REFERRAL;
295 | }
296 |
297 | /**
298 | * INTERNAL SESSION
299 | *
300 | * Fired by current domain
301 | *
302 | */
303 | if (source.refhost === removeWww(page.domain)) {
304 | log.info('internal detect', ref, source.refhost, removeWww(page.hostname))
305 | // source.type = source.hasMarks ? SESSION_CAMPAIGN : SESSION_INTERNAL;
306 | source.type = SESSION_INTERNAL;
307 | // return source;
308 | }
309 | }
310 |
311 | // // Forcing campaign type id marks present
312 | // if (source.hasMarks) {
313 | // source.type = SESSION_CAMPAIGN;
314 | // }
315 |
316 | // Forcing campaign type id marks present
317 | // we dont use fbclid because Facebook adds that to each outgoing link
318 |
319 | /**
320 | * CAMPAIGN SESSION
321 | *
322 | * forced if campaign marks present
323 | *
324 | */
325 | if (has_utm || has_os || has_gclid || has_yclid) {
326 | source.type = SESSION_CAMPAIGN;
327 | source.marksHash = simpleHash(marksCampaignString)
328 | }
329 |
330 | /**
331 | * PARTNER SESSION
332 | *
333 | * forced if partner marks present
334 | *
335 | */
336 | if (has_partner_ids) {
337 | source.type = SESSION_PARTNER;
338 | source.marksHash = simpleHash(marksPartnerString)
339 | }
340 |
341 |
342 | /**
343 | * WEBVIEW SESSION
344 | *
345 | * Forced if webview parameter present
346 | *
347 | */
348 | if (is_webview) {
349 | source.type = SESSION_WEBVIEW;
350 | }
351 |
352 | return source;
353 | }
354 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2018 Dmitry Rodin
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/src/Tracker.js:
--------------------------------------------------------------------------------
1 | // import Promise from 'promise-polyfill';
2 | import objectAssign from './functions/objectAssing';
3 | import createLogger from './functions/createLogger';
4 | import LocalStorageAdapter from './LocalStorageAdapter';
5 | import CookieStorageAdapter from './CookieStorageAdapter';
6 | import {
7 | pageDefaults,
8 | isHttps
9 | } from './data/pageDefaults';
10 | import {
11 | hashCode
12 | } from './functions/stringHash';
13 | import autoDomain from './functions/autoDomain';
14 | import browserData, { prepareUAData, prepareNavConnection, prepareNavExtra, prepareBatData } from './data/browserData';
15 | import browserCharacts from './data/browserCharacts';
16 | import performanceData from './data/performance';
17 | import BrowserEventsTracker from './trackers/BrowserEventsTracker';
18 | import ActivityTracker from './trackers/ActivityTracker';
19 | import SessionTracker from './trackers/SessionTracker';
20 | import ClickTracker from './trackers/ClickTracker';
21 | import FormTracker from './trackers/FormTracker';
22 | import PageTracker from './trackers/PageTracker';
23 | import GoogleAnalytics from './syncs/GoogleAnalytics';
24 | import YandexMetrika from './syncs/YandexMetrika';
25 | // import { PixelSync } from './syncs/PixelSync';
26 | import {
27 | isObject
28 | } from './functions/type';
29 | import msgCropper from './functions/msgCropper';
30 | // import { PushController } from './extensions/web-push';
31 | import SelfishPerson from './SelfishPerson';
32 | import {
33 | Transport
34 | } from './Transport';
35 | import Emitter from 'component-emitter';
36 | import each from './functions/each';
37 | import {
38 | EVENT_PAGEVIEW,
39 | EVENT_IDENTIFY,
40 | EVENT_PAGE_UNLOAD,
41 | READY,
42 | EVENT,
43 | DOM_BEFORE_UNLOAD,
44 | EVENTS_ADD_PERF,
45 | EVENTS_NO_SCROLL,
46 | INTERNAL_EVENT,
47 | SERVER_MESSAGE,
48 | SERVICE_TRACK,
49 | EVENT_OPTION_REQUEST,
50 | SERVICE_LOG,
51 | EVENT_OPTION_TRANSPORT_IMG,
52 | } from './Constants';
53 | import {
54 | win
55 | } from './Browser';
56 | import { packSemVer } from './functions/packSemVer';
57 |
58 |
59 | const LIBRARY = 'web-sdk';
60 | const LIBVER = packSemVer('5.1.1');
61 |
62 | const noop = () => { };
63 | const asObject = (options) => {
64 | return isObject(options) ? options : {};
65 | }
66 | const log = createLogger('RST/Tracker');
67 |
68 | /**
69 | * Main Tracker class
70 | * @constructor
71 | * @property {Transport} transport class used for communicate with server
72 | */
73 | function Tracker() {
74 |
75 | log('starting RST Tracker');
76 |
77 | this.startTime = (new Date()).getTime();
78 | this.timeDelta = 0;
79 |
80 | const pd = pageDefaults();
81 | const [domain, domainHash] = this.buildProjectId(pd.domain);
82 |
83 | this.initialized = false;
84 | this.configured = false;
85 | this.valuableFields = undefined;
86 | this.queue = [];
87 | this.registry = {};
88 |
89 | this.options = {
90 | projectId: domainHash,
91 | sessionTimeout: 1800, // 30 min
92 | lastCampaignExpires: 7776000, // 3 month
93 | initialUid: 0,
94 | cookieDomain: domain,
95 | cookiePath: '/',
96 | // prefix for cookie stored at target website
97 | cookiePrefix: 'rst4-',
98 | lcPrefix: 'rst4:',
99 | pathPrefix: '',
100 | urlMark: 'band',
101 | server: null,
102 | wsServer: null,
103 | wsPath: '/wss',
104 | trackActivity: {
105 | zeroEvents: false,
106 | scrollEvents: true,
107 | domEvents: true
108 | },
109 | trackClicks: {
110 | allClicks: false
111 | },
112 | browserEvents: {
113 | unloadHandlers: true
114 | },
115 | trackForms: {},
116 | trackPages: false,
117 | allowHTTP: false,
118 | allowSendBeacon: true,
119 | allowXHR: true,
120 | activateWs: false,
121 | pixelSync: false,
122 | webPush: true,
123 | msgCropper: (msg) => msgCropper(msg, this.valuableFields)
124 | };
125 |
126 | this.syncs = [];
127 | this.trackers = [];
128 | this.transport = null;
129 | }
130 |
131 | Emitter(Tracker.prototype);
132 |
133 | /**
134 | * Handle events from queue and start accepting events
135 | */
136 | Tracker.prototype.initialize = function () {
137 |
138 | // Check is initialized
139 | if (this.initialized) {
140 | return;
141 | }
142 |
143 | // Check is HTTPS
144 | if (!isHttps() && !this.options.allowHTTP) {
145 | return log.warn('Works only over https');
146 | }
147 |
148 | log('Initializing...');
149 |
150 | // Check is configured
151 | if (!this.configured) {
152 | log.warn('Initializing before configuration yet complete');
153 | }
154 |
155 | // Constructing storage adapters (should be before any other actions)
156 | this.localStorage = new LocalStorageAdapter({
157 | prefix: this.options.lcPrefix
158 | });
159 | this.cookieStorage = new CookieStorageAdapter({
160 | cookieDomain: this.options.cookieDomain,
161 | cookiePrefix: this.options.cookiePrefix,
162 | cookiePath: this.options.cookiePath,
163 | allowHTTP: this.options.allowHTTP
164 | });
165 |
166 |
167 | // Getting and applying personal configuration
168 | this.selfish = new SelfishPerson(this, this.options);
169 | this.configure(this.selfish.getConfig());
170 |
171 | log(this.options);
172 |
173 | // Handling browser events
174 | this.browserEventsTracker = new BrowserEventsTracker(this.options.browserEvents);
175 | this.browserEventsTracker.initialize();
176 |
177 | // Session tracker
178 | this.sessionTracker = new SessionTracker(this, this.options)
179 | .initialize(this)
180 | .handleUid(this.options.initialUid);
181 |
182 | // Interract with server
183 | this.transport = new Transport(this.options)
184 | .setCreds(this.sessionTracker.creds());
185 | // For WS
186 | // .connect();
187 |
188 |
189 | // Main tracker
190 | this.trackers.push(
191 | this.browserEventsTracker,
192 | this.sessionTracker
193 | );
194 |
195 | // Other tracker
196 | if (this.options.trackActivity) {
197 | this.activityTracker = new ActivityTracker(asObject(this.options.trackActivity));
198 | this.trackers.push(this.activityTracker);
199 | }
200 |
201 | if (this.options.trackClicks) {
202 | this.clickTracker = new ClickTracker(asObject(this.options.trackClicks));
203 | this.trackers.push(this.clickTracker);
204 | }
205 |
206 | if (this.options.trackForms) {
207 | this.formTracker = new FormTracker(asObject(this.options.trackForms));
208 | this.trackers.push(this.formTracker);
209 | }
210 | this.formTracker = new FormTracker(asObject(this.options.trackForms));
211 | // Interract with server
212 | this.trackers.push(this.formTracker);
213 |
214 | if (this.options.trackPages) {
215 | this.pageTracker = new PageTracker(asObject(this.options.trackPages));
216 | this.trackers.push(this.pageTracker);
217 | }
218 |
219 |
220 | // if(this.options.webPush){
221 | // this.webPusb = new PushController(this);
222 | // }
223 |
224 | // Integrations
225 | this.syncs.push(
226 | new GoogleAnalytics(),
227 | new YandexMetrika()
228 | );
229 | // if (this.options.pixelSync) {
230 | // this.syncs.push(new PixelSync(asObject(this.options.pixelSync)));
231 | // }
232 |
233 | const plugins = [this.transport].concat(this.syncs, this.trackers)
234 | each(plugins, (plugin) => {
235 | // Listening events
236 | plugin.on(EVENT, ({ name, data, options }) => {
237 | log(`plugin event:${name}`);
238 | this.handle(name, data, options);
239 | });
240 | // Listening internal events
241 | plugin.on(INTERNAL_EVENT, (name, data) => {
242 | log(`plugin internal event:${name}`);
243 | this.emit(name, data);
244 | this.emit(INTERNAL_EVENT, name, data);
245 | });
246 | });
247 |
248 | // Fire ready
249 | this.emit(READY);
250 | this.on(DOM_BEFORE_UNLOAD, this.unload);
251 |
252 | this.initialized = true;
253 |
254 | // Handling queue
255 | this.queue.map(e => {
256 | this.handle_proxy.apply(this, e);
257 | });
258 | this.queue = [];
259 |
260 | // Preparing User Agent Data - https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData
261 | prepareUAData();
262 | prepareNavConnection();
263 | setInterval(function () {
264 | prepareNavConnection();
265 | }, 5000)
266 | prepareNavExtra();
267 | prepareBatData();
268 | setInterval(function () {
269 | prepareBatData();
270 | }, 5000)
271 |
272 | };
273 |
274 | /**
275 | * Check is initialized
276 | */
277 | Tracker.prototype.isInitialized = function () {
278 | return this.initialized;
279 | };
280 |
281 | /**
282 | * Applying configuration block. Can be called multiple times
283 | * @param {Object} options
284 | */
285 | Tracker.prototype.configure = function (options) {
286 | if (this.initialized) {
287 | return log.warn('Configuration cant be applied because already initialized');
288 | }
289 | this.configured = true;
290 | this.options = objectAssign(this.options, options);
291 | };
292 |
293 |
294 |
295 |
296 | /**
297 | * Proxy for Handling event method
298 | * @param {string} name
299 | * @param {Object|undefined} data
300 | * @param {Object} options - Event properties
301 | */
302 | Tracker.prototype.handle_proxy = function (name, data = {}, options = {}) {
303 | try {
304 | return this.handle(name, data, options);
305 | } catch (e) {
306 | log.warn('Catched event handle error', e)
307 | this.logOnServer(e);
308 | }
309 | }
310 |
311 | /**
312 | * Handling event
313 | * @param {string} name
314 | * @param {Object|undefined} data
315 | * @param {Object} options - Event properties
316 | */
317 | Tracker.prototype.handle = function (name, data = {}, options = {}) {
318 | if (!this.initialized) {
319 | return this.queue.push([name, data]);
320 | }
321 |
322 | log.info(`Handling ${name}`, { data });
323 | this.emit(EVENT, name, data, options);
324 |
325 | if (name === EVENT_PAGEVIEW) {
326 | each(this.trackers, (tracker) => {
327 | if (tracker.clear) {
328 | tracker.clear();
329 | }
330 | });
331 | }
332 |
333 | // Special handlers
334 | if (name === EVENT_IDENTIFY) {
335 | return this.sessionTracker.setUserData(data);
336 | }
337 |
338 | this.sessionTracker.handleEvent(name, data, pageDefaults());
339 |
340 | // Schema used for minify data at thin channels
341 | this.valuableFields = this.valuableFields || {
342 | name: true,
343 | data: true,
344 | projectId: true,
345 | uid: true,
346 | user: true,
347 | error: true,
348 | browser: ['ts', 'tzOffset'],
349 | sess: ['eventNum', 'pageNum', 'num'],
350 | };
351 |
352 | // Typical message to server. Can be cropped using {msgCropSchema}
353 | const msg = {
354 | service: SERVICE_TRACK,
355 | name: name,
356 | data: data,
357 | projectId: this.options.projectId,
358 | uid: this.sessionTracker.getUid(),
359 | user: { td: this.timeDelta, ...this.sessionTracker.userData() },
360 | page: pageDefaults(),
361 | sess: this.sessionTracker.sessionData(),
362 | char: browserCharacts,
363 | browser: browserData(),
364 | lib: this.getLibInfo()
365 | };
366 |
367 | if (EVENTS_ADD_PERF.indexOf(name) >= 0) {
368 | msg.perf = performanceData();
369 | }
370 |
371 | if (this.activityTracker && EVENTS_NO_SCROLL.indexOf(name) < 0) {
372 | msg.scroll = this.activityTracker.getPositionData();
373 | }
374 |
375 | // Sending to server
376 | return this.sendToServer(msg, options);
377 | };
378 |
379 | /**
380 | * Log remote: send to server log
381 | * @param {Object} msg
382 | */
383 | Tracker.prototype.logOnServer = function (msg) {
384 | if (this.isInitialized()) {
385 | this.sendToServer({
386 | service: SERVICE_LOG,
387 | name: 'log',
388 | msg: msg
389 | }, {
390 | [EVENT_OPTION_TRANSPORT_IMG]: true
391 | });
392 | }
393 | };
394 |
395 |
396 | /**
397 | * Log remote: send to server log
398 | * @param {Error} e
399 | */
400 | // Tracker.prototype.logError = function (e, errStack) {
401 | // if (this.isInitialized()) {
402 | // try {
403 | // errStack = e.stack;
404 | // } catch (e) {
405 | // errStack = 'Stack not available';
406 | // }
407 | // try {
408 | // this.sendToServer({
409 | // service: SERVICE_LOG,
410 | // name: 'log',
411 | // msg: String(e),
412 | // stack: errStack
413 | // }, {
414 | // [EVENT_OPTION_TRANSPORT_IMG]: true
415 | // });
416 | // } catch (e){
417 | // log.warn('sendToServer (logError) executed with error');
418 | // }
419 | // }
420 |
421 | // };
422 |
423 | /**
424 | *
425 | * @param {Object} msg message
426 | * @param {Object} options Object contains options
427 | */
428 | Tracker.prototype.sendToServer = function (msg, options) {
429 | return this.transport
430 | .send(msg, options)
431 | .then(() => {
432 | //none
433 | })
434 | .catch((e) => {
435 | log.warn('sendToServer executed with error');
436 | });
437 | };
438 |
439 | /**
440 | * Unload handler
441 | */
442 | Tracker.prototype.unload = function () {
443 | log('Unloading...');
444 | this.event(EVENT_PAGE_UNLOAD);
445 |
446 | each(this.trackers, (tracker) => {
447 | if (tracker.unload) {
448 | tracker.unload();
449 | }
450 | });
451 | };
452 |
453 | /**
454 | * Tracking event
455 | * @param name
456 | * @param data
457 | * @param options
458 | */
459 | Tracker.prototype.event = function (name, data, options) {
460 | this.handle_proxy(name, data, options);
461 | };
462 |
463 | /**
464 | * @param {string} service backend service name
465 | * @param {string} name event name
466 | * @param {object} data that will be passed to backend service
467 | * @returns {Promise} result received from server
468 | */
469 | Tracker.prototype.request = function (service, name, data = {}) {
470 | if (!isObject(data)) {
471 | throw new Error('"data" shold be an object');
472 | }
473 | return this.sendToServer(
474 | { service, name, data },
475 | { [EVENT_OPTION_REQUEST]: true }
476 | );
477 | }
478 |
479 | /**
480 | * @param {string} service backend service name
481 | * @param {string} name event name
482 | * @param {object} data that will be passed to backend service
483 | * @returns {Promise} result received from server
484 | */
485 | Tracker.prototype.notify = function (service, name, data = {}) {
486 | if (!isObject(data)) {
487 | throw new Error('"data" shold be an object');
488 | }
489 | this.sendToServer(
490 | { service, name, data },
491 | {}
492 | );
493 | }
494 |
495 | /**
496 | * Track page load
497 | */
498 | Tracker.prototype.page = function (data, options) {
499 | this.handle_proxy(EVENT_PAGEVIEW, data, options);
500 | };
501 |
502 | /**
503 | * Emit warn log message. For testing purposes.
504 | */
505 | Tracker.prototype.warn = function (msg) {
506 | log.warn(new Error(msg));
507 | };
508 |
509 | /**
510 | * Emit warn log message. For testing purposes.
511 | */
512 | Tracker.prototype.enableLogger = function () {
513 | win._rst4_logger = true;
514 | };
515 |
516 | /**
517 | * Adding user details
518 | */
519 | Tracker.prototype.identify = function (userId, userTraits) {
520 | if (isObject(userId)) {
521 | userTraits = userId;
522 | userId = undefined;
523 | }
524 | this.handle_proxy(EVENT_IDENTIFY, {
525 | userId,
526 | userTraits
527 | });
528 | };
529 |
530 | /**
531 | * Add external ready callback
532 | * @param cb
533 | */
534 | Tracker.prototype.onReady = function (cb) {
535 | return this.isInitialized() ?
536 | (cb || noop)() :
537 | this.on(READY, cb);
538 | };
539 |
540 | /**
541 | * Add external event callback
542 | * @param cb
543 | */
544 | Tracker.prototype.onEvent = function (cb) {
545 | this.on(EVENT, cb);
546 | };
547 |
548 | /**
549 | * Add external event callback
550 | * @param cb
551 | */
552 | Tracker.prototype.onInternalEvent = function (cb) {
553 | this.on(INTERNAL_EVENT, cb);
554 | };
555 |
556 |
557 | /**
558 | * Add external event callback
559 | * @param cb
560 | */
561 | Tracker.prototype.onServerMessage = function (cb) {
562 | this.on(SERVER_MESSAGE, cb);
563 | };
564 |
565 | /**
566 | * Returns Tracker uid
567 | * @return {String}
568 | */
569 | Tracker.prototype.getUid = function () {
570 | if (this.isInitialized()) {
571 | return this.sessionTracker.getUid();
572 | }
573 | };
574 |
575 | /**
576 | * Session number
577 | * @return {Number}
578 | */
579 | Tracker.prototype.getSessNum = function () {
580 | return this.sessionTracker.getSessNum();
581 | };
582 |
583 |
584 | /**
585 | * Implicit session restart
586 | */
587 | Tracker.prototype.sessionEvent = function () {
588 | this.sessionTracker.fireSessionEvent();
589 | };
590 |
591 | /**
592 | * Save personal config overrides
593 | * @param {Object} config
594 | */
595 | Tracker.prototype.setCustomConfig = function (config) {
596 | this.selfish.saveConfig(config);
597 | };
598 |
599 |
600 | Tracker.prototype.buildProjectId = function (domain) {
601 | domain = domain || pageDefaults().domain;
602 | domain = autoDomain(domain);
603 | return [domain, hashCode(domain)];
604 | };
605 |
606 |
607 | Tracker.prototype.getLibInfo = function () {
608 | return {
609 | id: LIBRARY,
610 | v: LIBVER,
611 | sv: this.options.snippet,
612 | st: this.startTime
613 | }
614 | };
615 |
616 |
617 | /**
618 | * Setter for the objects registry
619 | * @param {*} name
620 | * @param {*} obj
621 | */
622 | Tracker.prototype.set = function (name, obj) {
623 | this.registry[name] = obj;
624 | };
625 |
626 | Tracker.prototype.setTimeDelta = function (d) {
627 | this.timeDelta = d;
628 | };
629 |
630 | export default Tracker;
631 |
--------------------------------------------------------------------------------