From 7581abfab60601e65f2b8d4a2e0c8a692004198c Mon Sep 17 00:00:00 2001 From: Kevin Pagtakhan Date: Thu, 9 Dec 2021 10:09:57 -0800 Subject: [PATCH 1/2] feat: support web worker env --- Makefile | 1 + karma-web-worker.conf.js | 28 +++++++++++++++++++ package.json | 1 + src/amplitude-client.js | 21 ++++++++------ src/base-cookie.js | 3 ++ src/base64.js | 9 +++--- src/config-manager.js | 7 +++-- src/cookiestorage.js | 3 +- src/get-host.js | 17 ++++++++++-- src/get-location.js | 4 ++- src/global-scope.js | 2 ++ src/index.js | 3 +- src/localstorage.js | 21 +++++++++----- src/metadata-storage.js | 11 ++++---- src/top-domain.js | 3 ++ src/utils.js | 8 +++++- src/worker-storage.js | 34 +++++++++++++++++++++++ src/xhr.js | 21 ++++++++++++-- test/global-scope.js | 7 +++++ test/tests.js | 2 ++ test/utils.js | 6 ++++ test/web-worker.js | 59 ++++++++++++++++++++++++++++++++++++++++ test/worker-storage.js | 48 ++++++++++++++++++++++++++++++++ yarn.lock | 33 ++++++++++++++++++++-- 24 files changed, 313 insertions(+), 39 deletions(-) create mode 100644 karma-web-worker.conf.js create mode 100644 src/global-scope.js create mode 100644 src/worker-storage.js create mode 100644 test/global-scope.js create mode 100644 test/web-worker.js create mode 100644 test/worker-storage.js diff --git a/Makefile b/Makefile index 3ea337b8..29764aac 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,7 @@ clean: test: build @$(KARMA) start karma.conf.js + @$(KARMA) start karma-web-worker.conf.js test-sauce: build @$(KARMA) start karma.conf.js --browsers sauce_chrome_windows diff --git a/karma-web-worker.conf.js b/karma-web-worker.conf.js new file mode 100644 index 00000000..cfe6f3c5 --- /dev/null +++ b/karma-web-worker.conf.js @@ -0,0 +1,28 @@ +module.exports = config => { + config.set({ + frameworks: ['mocha-webworker'], + files: [ + { + pattern: 'test/web-worker.js', + included: false, + }, + { + pattern: 'amplitude.js', + included: false, + }, + ], + browsers: ['ChromeHeadless'], + autoWatch: false, + singleRun: true, + reporters: ['mocha'], + client: { + mochaWebWorker: { + pattern: ['test/web-worker.js', 'amplitude.js'], + worker: 'Worker', + mocha: { + ui: 'bdd' + } + } + } + }); +}; diff --git a/package.json b/package.json index a2ed7769..a0323f4d 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "karma-firefox-launcher": "^1.0.1", "karma-mocha": "^1.3.0", "karma-mocha-reporter": "^2.2.5", + "karma-mocha-webworker": "^1.3.0", "karma-sauce-launcher": "^2.0.2", "karma-sinon": "^1.0.5", "karma-sourcemap-loader": "^0.3.7", diff --git a/src/amplitude-client.js b/src/amplitude-client.js index 14f4af5b..0643e570 100644 --- a/src/amplitude-client.js +++ b/src/amplitude-client.js @@ -19,6 +19,7 @@ import getHost from './get-host'; import baseCookie from './base-cookie'; import { AmplitudeServerZone, getEventLogApi } from './server-zone'; import ConfigManager from './config-manager'; +import GlobalScope from './global-scope'; /** * AmplitudeClient SDK API - instance constructor. @@ -28,7 +29,7 @@ import ConfigManager from './config-manager'; * @example var amplitudeClient = new AmplitudeClient(); */ var AmplitudeClient = function AmplitudeClient(instanceName) { - if (!isBrowserEnv()) { + if (!isBrowserEnv() && !utils.isWebWorkerEnvironment()) { utils.log.warn( 'amplitude-js will not work in a non-browser environment. If you are planning to add Amplitude to a node environment, please use @amplitude/node', ); @@ -80,7 +81,11 @@ AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, o try { _parseConfig(this.options, opt_config); - if (isBrowserEnv() && window.Prototype !== undefined && Array.prototype.toJSON) { + if ( + (isBrowserEnv() || utils.isWebWorkerEnvironment()) && + GlobalScope.Prototype !== undefined && + Array.prototype.toJSON + ) { prototypeJsFix(); utils.log.warn( 'Prototype.js injected Array.prototype.toJSON. Deleting Array.prototype.toJSON to prevent double-stringify', @@ -243,7 +248,7 @@ AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, o // Monitoring just page exits because that is the most requested feature for now // "If you're specifically trying to detect page unload events, the pagehide event is the best option." // https://developer.mozilla.org/en-US/docs/Web/API/Window/pagehide_event - window.addEventListener( + GlobalScope.addEventListener( 'pagehide', () => { handleVisibilityChange(); @@ -254,7 +259,7 @@ AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, o } } catch (err) { utils.log.error(err); - if (type(opt_config.onError) === 'function') { + if (opt_config && type(opt_config.onError) === 'function') { opt_config.onError(err); } } @@ -272,7 +277,7 @@ AmplitudeClient.prototype.deleteLowerLevelDomainCookies = function () { const cookieHost = this.options.domain && this.options.domain[0] === '.' ? this.options.domain.slice(1) : this.options.domain; - if (!cookieHost) { + if (!cookieHost || !utils.isWebWorkerEnvironment()) { return; } @@ -751,14 +756,14 @@ var _sendParamsReferrerUserProperties = function _sendParamsReferrerUserProperti * @private */ AmplitudeClient.prototype._getReferrer = function _getReferrer() { - return document.referrer; + return typeof document !== 'undefined' ? document.referrer : ''; }; /** * @private */ AmplitudeClient.prototype._getUrlParams = function _getUrlParams() { - return location.search; + return GlobalScope.location.search; }; /** @@ -1779,7 +1784,7 @@ AmplitudeClient.prototype.sendEvents = function sendEvents() { } this._sending = true; } - var protocol = this.options.forceHttps ? 'https' : 'https:' === window.location.protocol ? 'https' : 'http'; + var protocol = this.options.forceHttps ? 'https' : 'https:' === GlobalScope.location.protocol ? 'https' : 'http'; var url = protocol + '://' + this.options.apiEndpoint; // fetch events to send diff --git a/src/base-cookie.js b/src/base-cookie.js index ef6457ee..36765247 100644 --- a/src/base-cookie.js +++ b/src/base-cookie.js @@ -95,6 +95,9 @@ const sortByEventTime = (cookies) => { // test that cookies are enabled - navigator.cookiesEnabled yields false positives in IE, need to test directly const areCookiesEnabled = (opts = {}) => { const cookieName = Constants.COOKIE_TEST_PREFIX + base64Id(); + if (typeof document === 'undefined') { + return false; + } let _areCookiesEnabled = false; try { const uid = String(new Date()); diff --git a/src/base64.js b/src/base64.js index c16a8a10..4a085dfc 100644 --- a/src/base64.js +++ b/src/base64.js @@ -1,4 +1,5 @@ import UTF8 from './utf8'; +import GlobalScope from './global-scope'; /* * Base64 encoder/decoder @@ -9,8 +10,8 @@ var Base64 = { encode: function (input) { try { - if (window.btoa && window.atob) { - return window.btoa(unescape(encodeURIComponent(input))); + if (GlobalScope.btoa && GlobalScope.atob) { + return GlobalScope.btoa(unescape(encodeURIComponent(input))); } } catch (e) { //log(e); @@ -53,8 +54,8 @@ var Base64 = { decode: function (input) { try { - if (window.btoa && window.atob) { - return decodeURIComponent(escape(window.atob(input))); + if (GlobalScope.btoa && GlobalScope.atob) { + return decodeURIComponent(escape(GlobalScope.atob(input))); } } catch (e) { //log(e); diff --git a/src/config-manager.js b/src/config-manager.js index a2f93c35..294133e2 100644 --- a/src/config-manager.js +++ b/src/config-manager.js @@ -1,5 +1,6 @@ import Constants from './constants'; import { getDynamicConfigApi } from './server-zone'; +import GlobalScope from './global-scope'; /** * Dynamic Configuration * Find the best server url automatically based on app users' geo location. @@ -15,14 +16,14 @@ class ConfigManager { refresh(serverZone, forceHttps, callback) { let protocol = 'https'; - if (!forceHttps && 'https:' !== window.location.protocol) { + if (!forceHttps && 'https:' !== GlobalScope.location.protocol) { protocol = 'http'; } const dynamicConfigUrl = protocol + '://' + getDynamicConfigApi(serverZone); const self = this; - const isIE = window.XDomainRequest ? true : false; + const isIE = GlobalScope.XDomainRequest ? true : false; if (isIE) { - const xdr = new window.XDomainRequest(); + const xdr = new GlobalScope.XDomainRequest(); xdr.open('GET', dynamicConfigUrl, true); xdr.onload = function () { const response = JSON.parse(xdr.responseText); diff --git a/src/cookiestorage.js b/src/cookiestorage.js index e420f51b..367c4fea 100644 --- a/src/cookiestorage.js +++ b/src/cookiestorage.js @@ -6,6 +6,7 @@ import Cookie from './cookie'; import localStorage from './localstorage'; import baseCookie from './base-cookie'; +import GlobalScope from './global-scope'; var cookieStorage = function () { this.storage = null; @@ -43,7 +44,7 @@ cookieStorage.prototype.getStorage = function () { this._options.expirationDays = opts.expirationDays || this._options.expirationDays; // localStorage is specific to subdomains this._options.domain = - opts.domain || this._options.domain || (window && window.location && window.location.hostname); + opts.domain || this._options.domain || (GlobalScope && GlobalScope.location && GlobalScope.location.hostname); return (this._options.secure = opts.secure || false); }, get: function (name) { diff --git a/src/get-host.js b/src/get-host.js index 51dde916..8c717448 100644 --- a/src/get-host.js +++ b/src/get-host.js @@ -1,7 +1,18 @@ +import GlobalScope from './global-scope'; + const getHost = (url) => { - const a = document.createElement('a'); - a.href = url; - return a.hostname || location.hostname; + if (url) { + if (typeof document !== 'undefined') { + const a = document.createElement('a'); + a.href = url; + return a.hostname || GlobalScope.location.hostname; + } + if (typeof URL === 'function') { + const u = new URL(url); + return u.hostname || GlobalScope.location.hostname; + } + } + return GlobalScope.location.hostname; }; export default getHost; diff --git a/src/get-location.js b/src/get-location.js index 0b8457f1..ad8ba3d4 100644 --- a/src/get-location.js +++ b/src/get-location.js @@ -1,5 +1,7 @@ +import GlobalScope from './global-scope'; + const getLocation = () => { - return window.location; + return GlobalScope.location; }; export default getLocation; diff --git a/src/global-scope.js b/src/global-scope.js new file mode 100644 index 00000000..f7aadec0 --- /dev/null +++ b/src/global-scope.js @@ -0,0 +1,2 @@ +const GlobalScope = typeof window !== 'undefined' ? window : self; +export default GlobalScope; diff --git a/src/index.js b/src/index.js index 239d0b2a..591a6ff4 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,8 @@ // Entry point import Amplitude from './amplitude'; +import GlobalScope from './global-scope'; -const old = (typeof window !== 'undefined' && window.amplitude) || {}; +const old = (typeof GlobalScope !== 'undefined' && GlobalScope.amplitude) || {}; const newInstance = new Amplitude(); newInstance._q = old._q || []; diff --git a/src/localstorage.js b/src/localstorage.js index 9cf2aed4..53a2828d 100644 --- a/src/localstorage.js +++ b/src/localstorage.js @@ -2,10 +2,14 @@ * Implement localStorage to support Firefox 2-3 and IE 5-7 */ +import GlobalScope from './global-scope'; +import WorkerStorage from './worker-storage'; +import utils from './utils'; + var localStorage; if (!BUILD_COMPAT_LOCAL_STORAGE) { - localStorage = window.localStorage; + localStorage = GlobalScope.localStorage; } if (BUILD_COMPAT_LOCAL_STORAGE) { @@ -14,9 +18,9 @@ if (BUILD_COMPAT_LOCAL_STORAGE) { var uid = new Date(); var result; try { - window.localStorage.setItem(uid, uid); - result = window.localStorage.getItem(uid) === String(uid); - window.localStorage.removeItem(uid); + GlobalScope.localStorage.setItem(uid, uid); + result = GlobalScope.localStorage.getItem(uid) === String(uid); + GlobalScope.localStorage.removeItem(uid); return result; } catch (e) { // localStorage not available @@ -25,12 +29,12 @@ if (BUILD_COMPAT_LOCAL_STORAGE) { }; if (windowLocalStorageAvailable()) { - localStorage = window.localStorage; - } else if (typeof window !== 'undefined' && window.globalStorage) { + localStorage = GlobalScope.localStorage; + } else if (typeof GlobalScope !== 'undefined' && GlobalScope.globalStorage) { // Firefox 2-3 use globalStorage // See https://developer.mozilla.org/en/dom/storage#globalStorage try { - localStorage = window.globalStorage[window.location.hostname]; + localStorage = GlobalScope.globalStorage[GlobalScope.location.hostname]; } catch (e) { // Something bad happened... } @@ -85,6 +89,9 @@ if (BUILD_COMPAT_LOCAL_STORAGE) { } else { /* Nothing we can do ... */ } + } else if (utils.isWebWorkerEnvironment()) { + // Web worker + localStorage = new WorkerStorage(); } if (!localStorage) { /* eslint-disable no-unused-vars */ diff --git a/src/metadata-storage.js b/src/metadata-storage.js index 5bfd116b..c719a992 100644 --- a/src/metadata-storage.js +++ b/src/metadata-storage.js @@ -10,6 +10,7 @@ import getLocation from './get-location'; import ampLocalStorage from './localstorage'; import topDomain from './top-domain'; import utils from './utils'; +import GlobalScope from './global-scope'; const storageOptionExists = { [Constants.STORAGE_COOKIES]: true, @@ -88,8 +89,8 @@ class MetadataStorage { switch (this.storage) { case Constants.STORAGE_SESSION: - if (window.sessionStorage) { - window.sessionStorage.setItem(this.storageKey, value); + if (GlobalScope.sessionStorage) { + GlobalScope.sessionStorage.setItem(this.storageKey, value); } break; case Constants.STORAGE_LOCAL: @@ -131,7 +132,7 @@ class MetadataStorage { } if (!str) { try { - str = window.sessionStorage && window.sessionStorage.getItem(this.storageKey); + str = GlobalScope.sessionStorage && GlobalScope.sessionStorage.getItem(this.storageKey); } catch (e) { utils.log.info(`window.sessionStorage unavailable. Reason: "${e}"`); } @@ -187,8 +188,8 @@ class MetadataStorage { } if (!str) { try { - str = window.sessionStorage && window.sessionStorage.getItem(this.storageKey); - window.sessionStorage.clear(); + str = GlobalScope.sessionStorage && GlobalScope.sessionStorage.getItem(this.storageKey); + GlobalScope.sessionStorage.clear(); } catch (e) { utils.log.info(`window.sessionStorage unavailable. Reason: "${e}"`); } diff --git a/src/top-domain.js b/src/top-domain.js index bce73ea8..c033de4d 100644 --- a/src/top-domain.js +++ b/src/top-domain.js @@ -1,6 +1,7 @@ import baseCookie from './base-cookie'; import base64Id from './base64Id'; import getHost from './get-host'; +import utils from './utils'; // Utility that finds top level domain to write to const topDomain = (url) => { @@ -9,6 +10,8 @@ const topDomain = (url) => { const levels = []; const cname = '_tldtest_' + base64Id(); + if (utils.isWebWorkerEnvironment()) return ''; + for (let i = parts.length - 2; i >= 0; --i) { levels.push(parts.slice(i).join('.')); } diff --git a/src/utils.js b/src/utils.js index aee67369..a163576e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,4 +1,5 @@ import constants from './constants'; +import GlobalScope from './global-scope'; import type from './type'; var logLevels = { @@ -54,7 +55,7 @@ var isEmptyString = function isEmptyString(str) { var sessionStorageEnabled = function sessionStorageEnabled() { try { - if (window.sessionStorage) { + if (GlobalScope.sessionStorage) { return true; } } catch (e) { @@ -273,12 +274,17 @@ var getQueryParam = function getQueryParam(name, query) { return results === null ? undefined : decodeURIComponent(results[1].replace(/\+/g, ' ')); }; +const isWebWorkerEnvironment = () => { + return typeof WorkerGlobalScope !== 'undefined'; +}; + export default { setLogLevel, getLogLevel, logLevels, log, isEmptyString, + isWebWorkerEnvironment, getQueryParam, sessionStorageEnabled, truncate, diff --git a/src/worker-storage.js b/src/worker-storage.js new file mode 100644 index 00000000..ede2911a --- /dev/null +++ b/src/worker-storage.js @@ -0,0 +1,34 @@ +export default class WorkerStorage { + constructor() { + this.map = new Map(); + this.length = 0; + } + + key(index) { + const keys = Array.from(this.map.keys()); + const key = keys[index]; + return this.map.get(key); + } + + getItem(key) { + return this.map.get(key); + } + + setItem(key, value) { + if (!this.map.has(key)) { + this.length += 1; + } + this.map.set(key, value); + } + + removeItem(key) { + if (this.map.has(key)) { + this.length -= 1; + this.map.delete(key); + } + } + + clear() { + this.map.clear(); + } +} diff --git a/src/xhr.js b/src/xhr.js index 0c235d84..8a532314 100644 --- a/src/xhr.js +++ b/src/xhr.js @@ -1,4 +1,5 @@ import queryString from 'query-string'; +import GlobalScope from './global-scope'; /* * Simple AJAX request object @@ -16,9 +17,9 @@ function setHeaders(xhr, headers) { } Request.prototype.send = function (callback) { - var isIE = window.XDomainRequest ? true : false; + var isIE = GlobalScope.XDomainRequest ? true : false; if (isIE) { - var xdr = new window.XDomainRequest(); + var xdr = new GlobalScope.XDomainRequest(); xdr.open('POST', this.url, true); xdr.onload = function () { callback(200, xdr.responseText); @@ -34,7 +35,7 @@ Request.prototype.send = function (callback) { xdr.ontimeout = function () {}; xdr.onprogress = function () {}; xdr.send(queryString.stringify(this.data)); - } else { + } else if (typeof XMLHttpRequest !== 'undefined') { var xhr = new XMLHttpRequest(); xhr.open('POST', this.url, true); xhr.onreadystatechange = function () { @@ -44,6 +45,20 @@ Request.prototype.send = function (callback) { }; setHeaders(xhr, this.headers); xhr.send(queryString.stringify(this.data)); + } else { + let responseStatus = undefined; + fetch(this.url, { + method: 'POST', + headers: this.headers, + body: queryString.stringify(this.data), + }) + .then((response) => { + responseStatus = response.status; + return response.text(); + }) + .then((responseText) => { + callback(responseStatus, responseText); + }); } //log('sent request to ' + this.url + ' with data ' + decodeURIComponent(queryString(this.data))); }; diff --git a/test/global-scope.js b/test/global-scope.js new file mode 100644 index 00000000..4906f590 --- /dev/null +++ b/test/global-scope.js @@ -0,0 +1,7 @@ +import GlobalScope from '../src/global-scope'; + +describe('GlobalScope', function () { + it('should return true', function () { + assert.isTrue(GlobalScope === window); + }); +}); diff --git a/test/tests.js b/test/tests.js index 452f2ad0..4d48df49 100644 --- a/test/tests.js +++ b/test/tests.js @@ -16,3 +16,5 @@ import './top-domain.js'; import './base64Id.js'; import './server-zone.js'; import './config-manager.js'; +import './worker-storage.js'; +import './global-scope.js'; diff --git a/test/utils.js b/test/utils.js index 8fe65ec1..76278412 100644 --- a/test/utils.js +++ b/test/utils.js @@ -254,4 +254,10 @@ describe('utils', function () { assert.deepEqual(utils.validateProperties(properties), expected); }); }); + + describe('isWebWorkerEnvironment', function () { + it('should return false', function () { + assert.isFalse(utils.isWebWorkerEnvironment()); + }); + }); }); diff --git a/test/web-worker.js b/test/web-worker.js new file mode 100644 index 00000000..427177ba --- /dev/null +++ b/test/web-worker.js @@ -0,0 +1,59 @@ +// eslint-disable-next-line no-undef +importScripts('/base/amplitude.js'); + +var isTrue = function (a) { + if (!a) { + throw new Error('Assertion failed: object is falsey.'); + } +}; + +describe('web worker', function () { + describe('init', () => { + it('should call init successfully', () => { + let successCallbackCalled = false; + let errorCallbackCalled = false; + amplitude.init( + 'API_KEY', + undefined, + { + onError: function onError() { + errorCallbackCalled = true; + }, + eventUploadThreshold: 1, + }, + function callback() { + successCallbackCalled = true; + }, + ); + isTrue(amplitude.getInstance()._isInitialized); + isTrue(amplitude.getInstance()._newSession); + isTrue(successCallbackCalled); + isTrue(errorCallbackCalled === false); + }); + }); + + describe('logEvent', () => { + it('should call log event successfully', () => { + let sendEventsCalled = false; + let errorCallbackCalled = false; + amplitude.getInstance().sendEvents = () => { + sendEventsCalled = true; + }; + amplitude.logEvent( + 'event', + {}, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + function errorCallback() { + errorCallbackCalled = true; + }, + ); + isTrue(sendEventsCalled); + isTrue(errorCallbackCalled === false); + }); + }); +}); diff --git a/test/worker-storage.js b/test/worker-storage.js new file mode 100644 index 00000000..b5640df4 --- /dev/null +++ b/test/worker-storage.js @@ -0,0 +1,48 @@ +import WorkerStorage from '../src/worker-storage'; + +describe('WorkerStorage', function () { + describe('constructor', function () { + it('should return an instance', function () { + const workerStorage = new WorkerStorage(); + assert.isTrue(workerStorage.map instanceof Map); + assert.isTrue(workerStorage.length === 0); + }); + }); + + describe('key', function () { + it('should return one', function () { + const workerStorage = new WorkerStorage(); + workerStorage.setItem('0', 'zero'); + workerStorage.setItem('1', 'one'); + assert.isTrue(workerStorage.key(1) === 'one'); + }); + }); + + describe('setItem/getItem', function () { + it('should assign and return zero', function () { + const workerStorage = new WorkerStorage(); + workerStorage.setItem('0', 'zero'); + assert.isTrue(workerStorage.getItem('0') === 'zero'); + }); + }); + + describe('removeItem', function () { + it('should remove single item', function () { + const workerStorage = new WorkerStorage(); + workerStorage.setItem('0', 'zero'); + workerStorage.removeItem('0'); + assert.isTrue(workerStorage.getItem('0') === undefined); + }); + }); + + describe('clear', function () { + it('should clear storage', function () { + const workerStorage = new WorkerStorage(); + workerStorage.setItem('0', 'zero'); + workerStorage.setItem('1', 'one'); + workerStorage.clear(); + assert.isTrue(workerStorage.getItem('0') === undefined); + assert.isTrue(workerStorage.getItem('1') === undefined); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 97f30f89..f56b2f56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4370,6 +4370,15 @@ json5@^2.1.0: dependencies: minimist "^1.2.0" +jsonbird@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/jsonbird/-/jsonbird-2.2.2.tgz#9a56fff493bb10c18b1717d3e559e70d375d5a94" + integrity sha512-48n9HTL6Vxhr6WqX78ROH5NddK//ZnSdu1ZnPyyOl9IzF2PyRmwC8nCKPiRFo1wx7/Byq5YezCqokq9T/McLhw== + dependencies: + jsonparse "^1.2.0" + readable-stream "^2.1.4" + shortid "^2.2.6" + jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" @@ -4443,6 +4452,14 @@ karma-mocha-reporter@^2.2.5: log-symbols "^2.1.0" strip-ansi "^4.0.0" +karma-mocha-webworker@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/karma-mocha-webworker/-/karma-mocha-webworker-1.3.0.tgz#b5a4301b59ba86a08ee5b5f0aef1edb863becb26" + integrity sha1-taQwG1m6hqCO5bXwrvHtuGO+yyY= + dependencies: + jsonbird "^2.0.0" + minimatch "^3.0.3" + karma-mocha@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-1.3.0.tgz#eeaac7ffc0e201eb63c467440d2b69c7cf3778bf" @@ -5212,7 +5229,7 @@ min-indent@^1.0.0: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== -minimatch@^3.0.2, minimatch@^3.0.4: +minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -5364,6 +5381,11 @@ nan@^2.9.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552" integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw== +nanoid@^2.1.0: + version "2.1.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280" + integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -6557,7 +6579,7 @@ read@1, read@~1.0.1, read@~1.0.7: dependencies: mute-stream "~0.0.4" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -7216,6 +7238,13 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +shortid@^2.2.6: + version "2.2.16" + resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.16.tgz#b742b8f0cb96406fd391c76bfc18a67a57fe5608" + integrity sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g== + dependencies: + nanoid "^2.1.0" + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" From 679c3185ea2bf3f36e510a39b17a29e3f6d93556 Mon Sep 17 00:00:00 2001 From: Kevin Pagtakhan Date: Mon, 20 Dec 2021 09:31:21 -0800 Subject: [PATCH 2/2] feat: support web worker env --- karma-web-worker.conf.js | 10 +- test/web-worker.js | 209 ++++++++++++++++++++++++++++++++------- 2 files changed, 185 insertions(+), 34 deletions(-) diff --git a/karma-web-worker.conf.js b/karma-web-worker.conf.js index cfe6f3c5..877af17f 100644 --- a/karma-web-worker.conf.js +++ b/karma-web-worker.conf.js @@ -10,6 +10,10 @@ module.exports = config => { pattern: 'amplitude.js', included: false, }, + { + pattern: 'node_modules/sinon/pkg/sinon.js', + included: false, + }, ], browsers: ['ChromeHeadless'], autoWatch: false, @@ -17,7 +21,11 @@ module.exports = config => { reporters: ['mocha'], client: { mochaWebWorker: { - pattern: ['test/web-worker.js', 'amplitude.js'], + pattern: [ + 'test/web-worker.js', + 'amplitude.js', + 'node_modules/sinon/pkg/sinon.js' + ], worker: 'Worker', mocha: { ui: 'bdd' diff --git a/test/web-worker.js b/test/web-worker.js index 427177ba..67822dba 100644 --- a/test/web-worker.js +++ b/test/web-worker.js @@ -1,5 +1,8 @@ -// eslint-disable-next-line no-undef +/* eslint-disable no-undef */ importScripts('/base/amplitude.js'); +importScripts('/base/node_modules/sinon/pkg/sinon.js'); +const { sandbox } = sinon; +/* eslint-enable no-undef */ var isTrue = function (a) { if (!a) { @@ -8,52 +11,192 @@ var isTrue = function (a) { }; describe('web worker', function () { + let sbox; + beforeEach(function () { + sbox = sandbox.create(); + }); + + afterEach(function () { + sbox.restore(); + }); + describe('init', () => { - it('should call init successfully', () => { - let successCallbackCalled = false; - let errorCallbackCalled = false; + it('should init successfully', () => { + const onSuccess = sbox.spy(); + const onError = sbox.spy(); amplitude.init( 'API_KEY', undefined, { - onError: function onError() { - errorCallbackCalled = true; - }, + onError: onError, eventUploadThreshold: 1, }, - function callback() { - successCallbackCalled = true; - }, + onSuccess, ); isTrue(amplitude.getInstance()._isInitialized); isTrue(amplitude.getInstance()._newSession); - isTrue(successCallbackCalled); - isTrue(errorCallbackCalled === false); + isTrue(onSuccess.calledOnce); + isTrue(onError.notCalled); }); }); describe('logEvent', () => { - it('should call log event successfully', () => { - let sendEventsCalled = false; - let errorCallbackCalled = false; - amplitude.getInstance().sendEvents = () => { - sendEventsCalled = true; - }; - amplitude.logEvent( - 'event', - {}, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - function errorCallback() { - errorCallbackCalled = true; - }, - ); - isTrue(sendEventsCalled); - isTrue(errorCallbackCalled === false); + it('should log event successfully', () => { + const onError = sbox.spy(); + const sendEvents = sbox.stub(amplitude.getInstance(), 'sendEvents').returns(undefined); + amplitude + .getInstance() + .logEvent('event', {}, undefined, undefined, undefined, undefined, undefined, undefined, onError); + isTrue(sendEvents.calledOnce); + isTrue(onError.notCalled); + }); + }); + + describe('logEventWithGroups', () => { + it('should log event with groups successfully', () => { + const callback = sbox.spy(); + const onError = sbox.spy(); + const outOfSession = false; + const sendEvents = sbox.stub(amplitude.getInstance(), 'sendEvents').returns(undefined); + amplitude.getInstance().logEventWithGroups('event', {}, undefined, callback, onError, outOfSession); + isTrue(sendEvents.calledOnce); + isTrue(onError.notCalled); + }); + }); + + describe('sendEvents', () => { + it('should send event successfully', () => { + const _unsentCount = sbox.stub(amplitude.getInstance(), '_unsentCount').returns(1); + const _mergeEventsAndIdentifys = sbox.stub(amplitude.getInstance(), '_mergeEventsAndIdentifys').returns({ + eventsToSend: [{ event: {} }], + }); + const _logErrorsOnEvents = sbox.stub(amplitude.getInstance(), '_logErrorsOnEvents').returns(); + amplitude.getInstance().sendEvents(); + isTrue(_unsentCount.callCount === 2); + isTrue(_mergeEventsAndIdentifys.calledOnce); + isTrue(_logErrorsOnEvents.notCalled); + }); + }); + + describe('identify', () => { + it('should identify successfully', () => { + const callback = sbox.spy(); + const errorCallback = sbox.spy(); + const outOfSession = false; + const sendEvents = sbox.stub(amplitude.getInstance(), 'sendEvents').returns(undefined); + const identity = new amplitude.Identify().set('colors', ['rose', 'gold']); + amplitude.getInstance().identify(identity, callback, errorCallback, outOfSession); + isTrue(sendEvents.calledOnce); + isTrue(errorCallback.notCalled); + }); + }); + + describe('groupIdentify', () => { + it('should group identify successfully', () => { + const callback = sbox.spy(); + const errorCallback = sbox.spy(); + const outOfSession = false; + const sendEvents = sbox.stub(amplitude.getInstance(), 'sendEvents').returns(undefined); + const identity = new amplitude.Identify().set('colors', ['rose', 'gold']); + amplitude.getInstance().groupIdentify('groupType', 'groupName', identity, callback, errorCallback, outOfSession); + isTrue(sendEvents.calledOnce); + isTrue(errorCallback.notCalled); + }); + }); + + describe('logRevenue', () => { + it('should log revenue successfully', () => { + const _logEvent = sbox.stub(amplitude.getInstance(), '_logEvent').returns(undefined); + amplitude.logRevenue(1, 1, 'asdf'); + isTrue(_logEvent.calledOnce); + }); + }); + + describe('logRevenueV2', () => { + it('should log revenue successfully', () => { + const revenue = new amplitude.Revenue().setProductId('productIdentifier').setPrice(10.99); + const sendEvents = sbox.stub(amplitude.getInstance(), 'sendEvents').returns(undefined); + amplitude.logRevenueV2(revenue); + isTrue(sendEvents.calledOnce); + }); + }); + + describe('setGroup', () => { + it('should set group successfully', () => { + const onError = sbox.spy(); + const sendEvents = sbox.stub(amplitude.getInstance(), 'sendEvents').returns(undefined); + amplitude.getInstance().setGroup('groupType', 'groupName'); + isTrue(sendEvents.calledOnce); + isTrue(onError.notCalled); + }); + }); + + describe('setUserProperties', () => { + it('should set user properties successfully', () => { + const identify = sbox.stub(amplitude.getInstance(), 'identify').returns(undefined); + amplitude.getInstance().setUserProperties({ a: 1 }); + isTrue(identify.calledOnce); + }); + }); + + describe('setVersionName', () => { + it('should set version name successfully', () => { + amplitude.getInstance().setVersionName('1.1.1'); + isTrue(amplitude.getInstance().options.versionName === '1.1.1'); + }); + }); + + describe('enableTracking', () => { + it('should set domain successfully', () => { + const runQueuedFunctions = sbox.stub(amplitude.getInstance(), 'runQueuedFunctions').returns(undefined); + amplitude.getInstance().enableTracking(); + isTrue(runQueuedFunctions.calledOnce); + }); + }); + + describe('setGlobalUserProperties', () => { + it('should set global user properties successfully', () => { + const setUserProperties = sbox.stub(amplitude.getInstance(), 'setUserProperties').returns(undefined); + amplitude.getInstance().setGlobalUserProperties(); + isTrue(setUserProperties.calledOnce); + }); + }); + + describe('clearUserProperties', () => { + it('should call set user properties successfully', () => { + const identify = sbox.stub(amplitude.getInstance(), 'identify').returns(undefined); + amplitude.getInstance().clearUserProperties(); + isTrue(identify.calledOnce); + }); + }); + + describe('regenerateDeviceId', () => { + it('should regenerate device id successfully', () => { + const setDeviceId = sbox.stub(amplitude.getInstance(), 'setDeviceId').returns(undefined); + amplitude.getInstance().regenerateDeviceId(); + isTrue(setDeviceId.calledOnce); + }); + }); + + describe('setSessionId', () => { + it('should set new session id successfully', () => { + amplitude.getInstance().setSessionId(123); + isTrue(amplitude.getInstance().getSessionId() === 123); + }); + }); + + describe('resetSessionId', () => { + it('should set new session id successfully', () => { + amplitude.getInstance().resetSessionId(); + isTrue(amplitude.getInstance().getSessionId() !== 123); + }); + }); + + describe('setUseDynamicConfig', () => { + it('should set use dynamic config successfully', () => { + const _refreshDynamicConfig = sbox.stub(amplitude.getInstance(), '_refreshDynamicConfig').returns(undefined); + amplitude.getInstance().setUseDynamicConfig(true); + isTrue(_refreshDynamicConfig.calledOnce); }); }); });