├── .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 | ![Rockstat sheme](https://rockstat.ru/media/rockstat_v3_arch.png?3) 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 | --------------------------------------------------------------------------------