Skip to content

Commit

Permalink
feat: support web worker env (#467)
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinpagtakhan authored Dec 21, 2021
1 parent af2ec5d commit 52abaf0
Show file tree
Hide file tree
Showing 24 changed files with 464 additions and 39 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions karma-web-worker.conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module.exports = config => {
config.set({
frameworks: ['mocha-webworker'],
files: [
{
pattern: 'test/web-worker.js',
included: false,
},
{
pattern: 'amplitude.js',
included: false,
},
{
pattern: 'node_modules/sinon/pkg/sinon.js',
included: false,
},
],
browsers: ['ChromeHeadless'],
autoWatch: false,
singleRun: true,
reporters: ['mocha'],
client: {
mochaWebWorker: {
pattern: [
'test/web-worker.js',
'amplitude.js',
'node_modules/sinon/pkg/sinon.js'
],
worker: 'Worker',
mocha: {
ui: 'bdd'
}
}
}
});
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
21 changes: 13 additions & 8 deletions src/amplitude-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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',
);
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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();
Expand All @@ -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);
}
}
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
};

/**
Expand Down Expand Up @@ -1780,7 +1785,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
Expand Down
3 changes: 3 additions & 0 deletions src/base-cookie.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
9 changes: 5 additions & 4 deletions src/base64.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import UTF8 from './utf8';
import GlobalScope from './global-scope';

/*
* Base64 encoder/decoder
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
7 changes: 4 additions & 3 deletions src/config-manager.js
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/cookiestorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
17 changes: 14 additions & 3 deletions src/get-host.js
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 3 additions & 1 deletion src/get-location.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import GlobalScope from './global-scope';

const getLocation = () => {
return window.location;
return GlobalScope.location;
};

export default getLocation;
2 changes: 2 additions & 0 deletions src/global-scope.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const GlobalScope = typeof window !== 'undefined' ? window : self;

This comment has been minimized.

Copy link
@cdierkens

cdierkens Dec 22, 2021

@kevinpagtakhan self isn't available in a node scope so we can't directly replace window checks with GlobalScope in the current form.

This means you'll get a Uncaught ReferenceError: self is not defined in a node context.

We'll need to check that self is defined as well.

const GlobalScope = typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : undefined;

Or if you dislike nested terneries

const GlobalScope = typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : undefined;


function getGlobalScope() {
  if (typeof window !== 'undefined') {
    return window;
  }

  if (typeof self !== 'undefined') {
    return self;
  }
}

const GlobalScope = getGlobalScope();

This comment has been minimized.

Copy link
@kevinpagtakhan

kevinpagtakhan Dec 23, 2021

Author Contributor

Thanks for flagging this, @cdierkens. We received another report that v8.14.0 breaks support for server side rendering, relating to this change. While this SDK isn't build for node context unlike https://github.com/amplitude/Amplitude-Node, we are putting minimal effort to support specific cases of server side rendering. We're currently working on getting that resolved.

export default GlobalScope;
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -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 || [];

Expand Down
21 changes: 14 additions & 7 deletions src/localstorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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...
}
Expand Down Expand Up @@ -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 */
Expand Down
11 changes: 6 additions & 5 deletions src/metadata-storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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}"`);
}
Expand Down Expand Up @@ -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}"`);
}
Expand Down
3 changes: 3 additions & 0 deletions src/top-domain.js
Original file line number Diff line number Diff line change
@@ -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) => {
Expand All @@ -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('.'));
}
Expand Down
Loading

0 comments on commit 52abaf0

Please sign in to comment.