Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: eu dynamic configuration support #439

Merged
merged 3 commits into from
Oct 28, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/amplitude-client.js
Original file line number Diff line number Diff line change
@@ -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;
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
@@ -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',
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
@@ -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
@@ -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,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated to false, to not breaking current customers with custom proxy server.

};
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
@@ -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);
});
});
});
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
@@ -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';