Skip to content

Commit

Permalink
feat: eu dynamic configuration support (#439)
Browse files Browse the repository at this point in the history
  • Loading branch information
qingzhuozhen authored Oct 28, 2021
1 parent 87e3a64 commit 0618a90
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 2 deletions.
23 changes: 22 additions & 1 deletion src/amplitude-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand All @@ -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);
Expand Down Expand Up @@ -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;
57 changes: 57 additions & 0 deletions src/config-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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) {
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;
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;
4 changes: 4 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
9 changes: 8 additions & 1 deletion src/options.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Constants from './constants';
import language from './language';
import { AmplitudeServerZone } from './server-zone';

/**
* Options used when initializing Amplitude
Expand Down Expand Up @@ -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} [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.
*/
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
Expand Down Expand Up @@ -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: false,
};
44 changes: 44 additions & 0 deletions src/server-zone.js
Original file line number Diff line number Diff line change
@@ -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 };
27 changes: 27 additions & 0 deletions test/amplitude-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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);
});
});
});
23 changes: 23 additions & 0 deletions test/config-manager.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
16 changes: 16 additions & 0 deletions test/server-zone.js
Original file line number Diff line number Diff line change
@@ -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(''));
});
});
2 changes: 2 additions & 0 deletions test/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

0 comments on commit 0618a90

Please sign in to comment.