Skip to content

Commit 0618a90

Browse files
authored
feat: eu dynamic configuration support (#439)
1 parent 87e3a64 commit 0618a90

File tree

9 files changed

+203
-2
lines changed

9 files changed

+203
-2
lines changed

src/amplitude-client.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import { version } from '../package.json';
1818
import DEFAULT_OPTIONS from './options';
1919
import getHost from './get-host';
2020
import baseCookie from './base-cookie';
21+
import { getEventLogApi } from './server-zone';
22+
import ConfigManager from './config-manager';
2123

2224
/**
2325
* AmplitudeClient SDK API - instance constructor.
@@ -78,7 +80,6 @@ AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, o
7880

7981
try {
8082
_parseConfig(this.options, opt_config);
81-
8283
if (isBrowserEnv() && window.Prototype !== undefined && Array.prototype.toJSON) {
8384
prototypeJsFix();
8485
utils.log.warn(
@@ -90,6 +91,11 @@ AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, o
9091
utils.log.warn('The cookieName option is deprecated. We will be ignoring it for newer cookies');
9192
}
9293

94+
if (this.options.serverZoneBasedApi) {
95+
this.options.apiEndpoint = getEventLogApi(this.options.serverZone);
96+
}
97+
this._refreshDynamicConfig();
98+
9399
this.options.apiKey = apiKey;
94100
this._storageSuffix =
95101
'_' + apiKey + (this._instanceName === Constants.DEFAULT_INSTANCE ? '' : '_' + this._instanceName);
@@ -1868,4 +1874,19 @@ AmplitudeClient.prototype.enableTracking = function enableTracking() {
18681874
this.runQueuedFunctions();
18691875
};
18701876

1877+
/**
1878+
* Find best server url if choose to enable dynamic configuration.
1879+
*/
1880+
AmplitudeClient.prototype._refreshDynamicConfig = function _refreshDynamicConfig() {
1881+
if (this.options.useDynamicConfig) {
1882+
ConfigManager.refresh(
1883+
this.options.serverZone,
1884+
this.options.forceHttps,
1885+
function () {
1886+
this.options.apiEndpoint = ConfigManager.ingestionEndpoint;
1887+
}.bind(this),
1888+
);
1889+
}
1890+
};
1891+
18711892
export default AmplitudeClient;

src/config-manager.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import Constants from './constants';
2+
import { getDynamicConfigApi } from './server-zone';
3+
/**
4+
* Dynamic Configuration
5+
* Find the best server url automatically based on app users' geo location.
6+
*/
7+
class ConfigManager {
8+
constructor() {
9+
if (!ConfigManager.instance) {
10+
this.ingestionEndpoint = Constants.EVENT_LOG_URL;
11+
ConfigManager.instance = this;
12+
}
13+
return ConfigManager.instance;
14+
}
15+
16+
refresh(serverZone, forceHttps, callback) {
17+
let protocol = 'https';
18+
if (!forceHttps && 'https:' !== window.location.protocol) {
19+
protocol = 'http';
20+
}
21+
const dynamicConfigUrl = protocol + '://' + getDynamicConfigApi(serverZone);
22+
const self = this;
23+
const isIE = window.XDomainRequest ? true : false;
24+
if (isIE) {
25+
const xdr = new window.XDomainRequest();
26+
xdr.open('GET', dynamicConfigUrl, true);
27+
xdr.onload = function () {
28+
const response = JSON.parse(xdr.responseText);
29+
self.ingestionEndpoint = response['ingestionEndpoint'];
30+
if (callback) {
31+
callback();
32+
}
33+
};
34+
xdr.onerror = function () {};
35+
xdr.ontimeout = function () {};
36+
xdr.onprogress = function () {};
37+
xdr.send();
38+
} else {
39+
var xhr = new XMLHttpRequest();
40+
xhr.open('GET', dynamicConfigUrl, true);
41+
xhr.onreadystatechange = function () {
42+
if (xhr.readyState === 4 && xhr.status === 200) {
43+
const response = JSON.parse(xhr.responseText);
44+
self.ingestionEndpoint = response['ingestionEndpoint'];
45+
if (callback) {
46+
callback();
47+
}
48+
}
49+
};
50+
xhr.send();
51+
}
52+
}
53+
}
54+
55+
const instance = new ConfigManager();
56+
57+
export default instance;

src/constants.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ export default {
55
MAX_PROPERTY_KEYS: 1000,
66
IDENTIFY_EVENT: '$identify',
77
GROUP_IDENTIFY_EVENT: '$groupidentify',
8+
EVENT_LOG_URL: 'api.amplitude.com',
9+
EVENT_LOG_EU_URL: 'api.eu.amplitude.com',
10+
DYNAMIC_CONFIG_URL: 'regionconfig.amplitude.com',
11+
DYNAMIC_CONFIG_EU_URL: 'regionconfig.eu.amplitude.com',
812

913
// localStorageKeys
1014
LAST_EVENT_ID: 'amplitude_lastEventId',

src/options.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Constants from './constants';
22
import language from './language';
3+
import { AmplitudeServerZone } from './server-zone';
34

45
/**
56
* Options used when initializing Amplitude
@@ -46,9 +47,12 @@ import language from './language';
4647
* @property {string} [unsentIdentifyKey=`amplitude_unsent_identify`] - localStorage key that stores unsent identifies.
4748
* @property {number} [uploadBatchSize=`100`] - The maximum number of events to send to the server per request.
4849
* @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.
50+
* @property {string} [serverZone] - For server zone related configuration, used for server api endpoint and dynamic configuration.
51+
* @property {boolean} [useDynamicConfig] - Enable dynamic configuration to find best server url for user.
52+
* @property {boolean} [serverZoneBasedApi] - To update api endpoint with serverZone change or not. For data residency, recommend to enable it unless using own proxy server.
4953
*/
5054
export default {
51-
apiEndpoint: 'api.amplitude.com',
55+
apiEndpoint: Constants.EVENT_LOG_URL,
5256
batchEvents: false,
5357
cookieExpiration: 365, // 12 months is for GDPR compliance
5458
cookieName: 'amplitude_id', // this is a deprecated option
@@ -107,4 +111,7 @@ export default {
107111
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
108112
'Cross-Origin-Resource-Policy': 'cross-origin',
109113
},
114+
serverZone: AmplitudeServerZone.US,
115+
useDynamicConfig: false,
116+
serverZoneBasedApi: false,
110117
};

src/server-zone.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import Constants from './constants';
2+
3+
/**
4+
* AmplitudeServerZone is for Data Residency and handling server zone related properties.
5+
* The server zones now are US and EU.
6+
*
7+
* For usage like sending data to Amplitude's EU servers, you need to configure the serverZone during nitializing.
8+
*/
9+
const AmplitudeServerZone = {
10+
US: 'US',
11+
EU: 'EU',
12+
};
13+
14+
const getEventLogApi = (serverZone) => {
15+
let eventLogUrl = Constants.EVENT_LOG_URL;
16+
switch (serverZone) {
17+
case AmplitudeServerZone.EU:
18+
eventLogUrl = Constants.EVENT_LOG_EU_URL;
19+
break;
20+
case AmplitudeServerZone.US:
21+
eventLogUrl = Constants.EVENT_LOG_URL;
22+
break;
23+
default:
24+
break;
25+
}
26+
return eventLogUrl;
27+
};
28+
29+
const getDynamicConfigApi = (serverZone) => {
30+
let dynamicConfigUrl = Constants.DYNAMIC_CONFIG_URL;
31+
switch (serverZone) {
32+
case AmplitudeServerZone.EU:
33+
dynamicConfigUrl = Constants.DYNAMIC_CONFIG_EU_URL;
34+
break;
35+
case AmplitudeServerZone.US:
36+
dynamicConfigUrl = Constants.DYNAMIC_CONFIG_URL;
37+
break;
38+
default:
39+
break;
40+
}
41+
return dynamicConfigUrl;
42+
};
43+
44+
export { AmplitudeServerZone, getEventLogApi, getDynamicConfigApi };

test/amplitude-client.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import queryString from 'query-string';
1010
import Identify from '../src/identify.js';
1111
import constants from '../src/constants.js';
1212
import { mockCookie, restoreCookie, getCookie } from './mock-cookie';
13+
import { AmplitudeServerZone } from '../src/server-zone.js';
1314

1415
// maintain for testing backwards compatability
1516
describe('AmplitudeClient', function () {
@@ -4089,4 +4090,30 @@ describe('AmplitudeClient', function () {
40894090
assert.isTrue(errCallback.calledOnce);
40904091
});
40914092
});
4093+
4094+
describe('eu dynamic configuration', function () {
4095+
it('EU serverZone should set apiEndpoint to EU', function () {
4096+
assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_URL);
4097+
amplitude.init(apiKey, null, { serverZone: AmplitudeServerZone.EU, serverZoneBasedApi: true });
4098+
assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_EU_URL);
4099+
});
4100+
4101+
it('EU serverZone without serverZoneBasedApi set should not affect apiEndpoint', function () {
4102+
assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_URL);
4103+
amplitude.init(apiKey, null, { serverZone: AmplitudeServerZone.EU, serverZoneBasedApi: false });
4104+
assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_URL);
4105+
});
4106+
4107+
it('EU serverZone with dynamic configuration should set apiEndpoint to EU', function () {
4108+
assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_URL);
4109+
amplitude.init(apiKey, null, {
4110+
serverZone: AmplitudeServerZone.EU,
4111+
serverZoneBasedApi: false,
4112+
useDynamicConfig: true,
4113+
});
4114+
server.respondWith('{"ingestionEndpoint": "api.eu.amplitude.com"}');
4115+
server.respond();
4116+
assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_EU_URL);
4117+
});
4118+
});
40924119
});

test/config-manager.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import sinon from 'sinon';
2+
import ConfigManager from '../src/config-manager';
3+
import { AmplitudeServerZone } from '../src/server-zone';
4+
import Constants from '../src/constants';
5+
6+
describe('ConfigManager', function () {
7+
let server;
8+
beforeEach(function () {
9+
server = sinon.fakeServer.create();
10+
});
11+
12+
afterEach(function () {
13+
server.restore();
14+
});
15+
16+
it('ConfigManager should support EU zone', function () {
17+
ConfigManager.refresh(AmplitudeServerZone.EU, true, function () {
18+
assert.equal(Constants.EVENT_LOG_EU_URL, ConfigManager.ingestionEndpoint);
19+
});
20+
server.respondWith('{"ingestionEndpoint": "api.eu.amplitude.com"}');
21+
server.respond();
22+
});
23+
});

test/server-zone.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { AmplitudeServerZone, getEventLogApi, getDynamicConfigApi } from '../src/server-zone';
2+
import Constants from '../src/constants';
3+
4+
describe('AmplitudeServerZone', function () {
5+
it('getEventLogApi should return correct event log url', function () {
6+
assert.equal(Constants.EVENT_LOG_URL, getEventLogApi(AmplitudeServerZone.US));
7+
assert.equal(Constants.EVENT_LOG_EU_URL, getEventLogApi(AmplitudeServerZone.EU));
8+
assert.equal(Constants.EVENT_LOG_URL, getEventLogApi(''));
9+
});
10+
11+
it('getDynamicConfigApi should return correct dynamic config url', function () {
12+
assert.equal(Constants.DYNAMIC_CONFIG_URL, getDynamicConfigApi(AmplitudeServerZone.US));
13+
assert.equal(Constants.DYNAMIC_CONFIG_EU_URL, getDynamicConfigApi(AmplitudeServerZone.EU));
14+
assert.equal(Constants.DYNAMIC_CONFIG_URL, getDynamicConfigApi(''));
15+
});
16+
});

test/tests.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ import './revenue.js';
1414
import './base-cookie.js';
1515
import './top-domain.js';
1616
import './base64Id.js';
17+
import './server-zone.js';
18+
import './config-manager.js';

0 commit comments

Comments
 (0)