From da465c76bf3ce4414db80aa6ccc2419d9704c14d Mon Sep 17 00:00:00 2001 From: Qingzhuo Zhen Date: Fri, 22 Oct 2021 21:24:39 -0700 Subject: [PATCH 1/3] feat: eu dynamic configuration support --- src/amplitude-client.js | 23 ++++++++++++++++- src/config-manager.js | 54 ++++++++++++++++++++++++++++++++++++++++ src/constants.js | 4 +++ src/options.js | 9 ++++++- src/server-zone.js | 44 ++++++++++++++++++++++++++++++++ test/amplitude-client.js | 27 ++++++++++++++++++++ test/config-manager.js | 23 +++++++++++++++++ test/server-zone.js | 16 ++++++++++++ test/tests.js | 2 ++ 9 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 src/config-manager.js create mode 100644 src/server-zone.js create mode 100644 test/config-manager.js create mode 100644 test/server-zone.js diff --git a/src/amplitude-client.js b/src/amplitude-client.js index 0f43917d..701d2934 100644 --- a/src/amplitude-client.js +++ b/src/amplitude-client.js @@ -18,6 +18,8 @@ import { version } from '../package.json'; import DEFAULT_OPTIONS from './options'; import getHost from './get-host'; import baseCookie from './base-cookie'; +import { getEventLogApi } from './server-zone'; +import ConfigManager from './config-manager'; /** * AmplitudeClient SDK API - instance constructor. @@ -78,7 +80,6 @@ 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) { prototypeJsFix(); utils.log.warn( @@ -90,6 +91,11 @@ AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, o utils.log.warn('The cookieName option is deprecated. We will be ignoring it for newer cookies'); } + if (this.options.serverZoneBasedApi) { + this.options.apiEndpoint = getEventLogApi(this.options.serverZone); + } + this._refreshDynamicConfig(); + this.options.apiKey = apiKey; this._storageSuffix = '_' + apiKey + (this._instanceName === Constants.DEFAULT_INSTANCE ? '' : '_' + this._instanceName); @@ -1868,4 +1874,19 @@ AmplitudeClient.prototype.enableTracking = function enableTracking() { this.runQueuedFunctions(); }; +/** + * Find best server url if choose to enable dynamic configuration. + */ +AmplitudeClient.prototype._refreshDynamicConfig = function _refreshDynamicConfig() { + if (this.options.useDynamicConfig) { + ConfigManager.refresh( + this.options.serverZone, + this.options.forceHttps, + function () { + this.options.apiEndpoint = ConfigManager.ingestionEndpoint; + }.bind(this), + ); + } +}; + export default AmplitudeClient; diff --git a/src/config-manager.js b/src/config-manager.js new file mode 100644 index 00000000..dd918201 --- /dev/null +++ b/src/config-manager.js @@ -0,0 +1,54 @@ +import Constants from './constants'; +import { getDynamicConfigApi } from './server-zone'; +/** + * Dynamic Configuration + * Find the best server url automatically based on app users' geo location. + */ +class ConfigManager { + constructor() { + if (!ConfigManager.instance) { + this.ingestionEndpoint = Constants.EVENT_LOG_URL; + ConfigManager.instance = this; + } + return ConfigManager.instance; + } + + refresh(serverZone, forceHttps, callback) { + const protocol = forceHttps ? 'https' : 'https:' === window.location.protocol ? 'https' : 'http'; + const dynamicConfigUrl = protocol + '://' + getDynamicConfigApi(serverZone); + const self = this; + const isIE = window.XDomainRequest ? true : false; + if (isIE) { + const xdr = new window.XDomainRequest(); + xdr.open('GET', dynamicConfigUrl, true); + xdr.onload = function () { + const response = JSON.parse(xdr.responseText); + self.ingestionEndpoint = response['ingestionEndpoint']; + if (callback) { + callback(); + } + }; + xdr.onerror = function () {}; + xdr.ontimeout = function () {}; + xdr.onprogress = function () {}; + xdr.send(); + } else { + var xhr = new XMLHttpRequest(); + xhr.open('GET', dynamicConfigUrl, true); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 200) { + const response = JSON.parse(xhr.responseText); + self.ingestionEndpoint = response['ingestionEndpoint']; + if (callback) { + callback(); + } + } + }; + xhr.send(); + } + } +} + +const instance = new ConfigManager(); + +export default instance; diff --git a/src/constants.js b/src/constants.js index 86bdb9da..5ad6fd41 100644 --- a/src/constants.js +++ b/src/constants.js @@ -5,6 +5,10 @@ export default { MAX_PROPERTY_KEYS: 1000, IDENTIFY_EVENT: '$identify', GROUP_IDENTIFY_EVENT: '$groupidentify', + EVENT_LOG_URL: 'api.amplitude.com', + EVENT_LOG_EU_URL: 'api.eu.amplitude.com', + DYNAMIC_CONFIG_URL: 'regionconfig.amplitude.com', + DYNAMIC_CONFIG_EU_URL: 'regionconfig.eu.amplitude.com', // localStorageKeys LAST_EVENT_ID: 'amplitude_lastEventId', diff --git a/src/options.js b/src/options.js index cffa1112..b29da9aa 100644 --- a/src/options.js +++ b/src/options.js @@ -1,5 +1,6 @@ import Constants from './constants'; import language from './language'; +import { AmplitudeServerZone } from './server-zone'; /** * Options used when initializing Amplitude @@ -46,9 +47,12 @@ import language from './language'; * @property {string} [unsentIdentifyKey=`amplitude_unsent_identify`] - localStorage key that stores unsent identifies. * @property {number} [uploadBatchSize=`100`] - The maximum number of events to send to the server per request. * @property {Object} [headers=`{ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }`] - Headers attached to an event(s) upload network request. Custom header properties are merged with this object. + * @property {string} [serveZone] - For server zone related configuration, used for server api endpoint and dynamic configuration. + * @property {boolean} [useDynamicConfig] - Enable dynamic configuration to find best server url for user. + * @property {boolean} [serverZoneBasedApi] - To update api endpoint with serverZone change or not. For data residency, recommend to enable it unless using own proxy server. */ export default { - apiEndpoint: 'api.amplitude.com', + apiEndpoint: Constants.EVENT_LOG_URL, batchEvents: false, cookieExpiration: 365, // 12 months is for GDPR compliance cookieName: 'amplitude_id', // this is a deprecated option @@ -107,4 +111,7 @@ export default { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'Cross-Origin-Resource-Policy': 'cross-origin', }, + serverZone: AmplitudeServerZone.US, + useDynamicConfig: false, + serverZoneBasedApi: true, }; diff --git a/src/server-zone.js b/src/server-zone.js new file mode 100644 index 00000000..b1d2d6b2 --- /dev/null +++ b/src/server-zone.js @@ -0,0 +1,44 @@ +import Constants from './constants'; + +/** + * AmplitudeServerZone is for Data Residency and handling server zone related properties. + * The server zones now are US and EU. + * + * For usage like sending data to Amplitude's EU servers, you need to configure the serverZone during nitializing. + */ +const AmplitudeServerZone = { + US: 'US', + EU: 'EU', +}; + +const getEventLogApi = (serverZone) => { + let eventLogUrl = Constants.EVENT_LOG_URL; + switch (serverZone) { + case AmplitudeServerZone.EU: + eventLogUrl = Constants.EVENT_LOG_EU_URL; + break; + case AmplitudeServerZone.US: + eventLogUrl = Constants.EVENT_LOG_URL; + break; + default: + break; + } + return eventLogUrl; +}; + +const getDynamicConfigApi = (serverZone) => { + let dynamicConfigUrl = Constants.DYNAMIC_CONFIG_URL; + switch (serverZone) { + case AmplitudeServerZone.EU: + dynamicConfigUrl = Constants.DYNAMIC_CONFIG_EU_URL; + break; + case AmplitudeServerZone.US: + dynamicConfigUrl = Constants.DYNAMIC_CONFIG_URL; + break; + default: + break; + } + return dynamicConfigUrl; +}; + +export { AmplitudeServerZone, getEventLogApi, getDynamicConfigApi }; diff --git a/test/amplitude-client.js b/test/amplitude-client.js index 2a11551f..0742a72b 100644 --- a/test/amplitude-client.js +++ b/test/amplitude-client.js @@ -10,6 +10,7 @@ import queryString from 'query-string'; import Identify from '../src/identify.js'; import constants from '../src/constants.js'; import { mockCookie, restoreCookie, getCookie } from './mock-cookie'; +import { AmplitudeServerZone } from '../src/server-zone.js'; // maintain for testing backwards compatability describe('AmplitudeClient', function () { @@ -4089,4 +4090,30 @@ describe('AmplitudeClient', function () { assert.isTrue(errCallback.calledOnce); }); }); + + describe('eu dynamic configuration', function () { + it('EU serverZone should set apiEndpoint to EU', function () { + assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_URL); + amplitude.init(apiKey, null, { serverZone: AmplitudeServerZone.EU, serverZoneBasedApi: true }); + assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_EU_URL); + }); + + it('EU serverZone without serverZoneBasedApi set should not affect apiEndpoint', function () { + assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_URL); + amplitude.init(apiKey, null, { serverZone: AmplitudeServerZone.EU, serverZoneBasedApi: false }); + assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_URL); + }); + + it('EU serverZone with dynamic configuration should set apiEndpoint to EU', function () { + assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_URL); + amplitude.init(apiKey, null, { + serverZone: AmplitudeServerZone.EU, + serverZoneBasedApi: false, + useDynamicConfig: true, + }); + server.respondWith('{"ingestionEndpoint": "api.eu.amplitude.com"}'); + server.respond(); + assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_EU_URL); + }); + }); }); diff --git a/test/config-manager.js b/test/config-manager.js new file mode 100644 index 00000000..ea182378 --- /dev/null +++ b/test/config-manager.js @@ -0,0 +1,23 @@ +import sinon from 'sinon'; +import ConfigManager from '../src/config-manager'; +import { AmplitudeServerZone } from '../src/server-zone'; +import Constants from '../src/constants'; + +describe('ConfigManager', function () { + let server; + beforeEach(function () { + server = sinon.fakeServer.create(); + }); + + afterEach(function () { + server.restore(); + }); + + it('ConfigManager should support EU zone', function () { + ConfigManager.refresh(AmplitudeServerZone.EU, true, function () { + assert.equal(Constants.EVENT_LOG_EU_URL, ConfigManager.ingestionEndpoint); + }); + server.respondWith('{"ingestionEndpoint": "api.eu.amplitude.com"}'); + server.respond(); + }); +}); diff --git a/test/server-zone.js b/test/server-zone.js new file mode 100644 index 00000000..7b16168d --- /dev/null +++ b/test/server-zone.js @@ -0,0 +1,16 @@ +import { AmplitudeServerZone, getEventLogApi, getDynamicConfigApi } from '../src/server-zone'; +import Constants from '../src/constants'; + +describe('AmplitudeServerZone', function () { + it('getEventLogApi should return correct event log url', function () { + assert.equal(Constants.EVENT_LOG_URL, getEventLogApi(AmplitudeServerZone.US)); + assert.equal(Constants.EVENT_LOG_EU_URL, getEventLogApi(AmplitudeServerZone.EU)); + assert.equal(Constants.EVENT_LOG_URL, getEventLogApi('')); + }); + + it('getDynamicConfigApi should return correct dynamic config url', function () { + assert.equal(Constants.DYNAMIC_CONFIG_URL, getDynamicConfigApi(AmplitudeServerZone.US)); + assert.equal(Constants.DYNAMIC_CONFIG_EU_URL, getDynamicConfigApi(AmplitudeServerZone.EU)); + assert.equal(Constants.DYNAMIC_CONFIG_URL, getDynamicConfigApi('')); + }); +}); diff --git a/test/tests.js b/test/tests.js index 93ef56d9..452f2ad0 100644 --- a/test/tests.js +++ b/test/tests.js @@ -14,3 +14,5 @@ import './revenue.js'; import './base-cookie.js'; import './top-domain.js'; import './base64Id.js'; +import './server-zone.js'; +import './config-manager.js'; From bb023f7d8098a7f8a3109187f91e864889bb5603 Mon Sep 17 00:00:00 2001 From: Qingzhuo Zhen Date: Mon, 25 Oct 2021 21:58:17 -0700 Subject: [PATCH 2/3] patch: resolve comments --- src/config-manager.js | 5 ++++- src/options.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/config-manager.js b/src/config-manager.js index dd918201..a2f93c35 100644 --- a/src/config-manager.js +++ b/src/config-manager.js @@ -14,7 +14,10 @@ class ConfigManager { } refresh(serverZone, forceHttps, callback) { - const protocol = forceHttps ? 'https' : 'https:' === window.location.protocol ? 'https' : 'http'; + let protocol = 'https'; + if (!forceHttps && 'https:' !== window.location.protocol) { + protocol = 'http'; + } const dynamicConfigUrl = protocol + '://' + getDynamicConfigApi(serverZone); const self = this; const isIE = window.XDomainRequest ? true : false; diff --git a/src/options.js b/src/options.js index b29da9aa..efceaf2e 100644 --- a/src/options.js +++ b/src/options.js @@ -47,7 +47,7 @@ import { AmplitudeServerZone } from './server-zone'; * @property {string} [unsentIdentifyKey=`amplitude_unsent_identify`] - localStorage key that stores unsent identifies. * @property {number} [uploadBatchSize=`100`] - The maximum number of events to send to the server per request. * @property {Object} [headers=`{ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }`] - Headers attached to an event(s) upload network request. Custom header properties are merged with this object. - * @property {string} [serveZone] - For server zone related configuration, used for server api endpoint and dynamic configuration. + * @property {string} [serverZone] - For server zone related configuration, used for server api endpoint and dynamic configuration. * @property {boolean} [useDynamicConfig] - Enable dynamic configuration to find best server url for user. * @property {boolean} [serverZoneBasedApi] - To update api endpoint with serverZone change or not. For data residency, recommend to enable it unless using own proxy server. */ From 746ccf31853109cb59df7b09ddd2b2ed68a442bc Mon Sep 17 00:00:00 2001 From: Qingzhuo Zhen Date: Wed, 27 Oct 2021 10:22:35 -0700 Subject: [PATCH 3/3] patch: update serverZoneBasedApi default to be false, so we don;t accidenctly overwrite custome server url --- src/options.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/options.js b/src/options.js index efceaf2e..3d0f6140 100644 --- a/src/options.js +++ b/src/options.js @@ -113,5 +113,5 @@ export default { }, serverZone: AmplitudeServerZone.US, useDynamicConfig: false, - serverZoneBasedApi: true, + serverZoneBasedApi: false, };