diff --git a/src/amplitude-client.js b/src/amplitude-client.js index 987bd751..76d24926 100644 --- a/src/amplitude-client.js +++ b/src/amplitude-client.js @@ -17,7 +17,7 @@ import base64Id from './base64Id'; import DEFAULT_OPTIONS from './options'; import getHost from './get-host'; import baseCookie from './base-cookie'; -import { getEventLogApi } from './server-zone'; +import { AmplitudeServerZone, getEventLogApi } from './server-zone'; import ConfigManager from './config-manager'; /** @@ -858,15 +858,33 @@ AmplitudeClient.prototype.setDomain = function setDomain(domain) { * Sets an identifier for the current user. * @public * @param {string} userId - identifier to set. Can be null. + * @param {boolean} startNewSession - (optional) if start a new session or not * @example amplitudeClient.setUserId('joe@gmail.com'); */ -AmplitudeClient.prototype.setUserId = function setUserId(userId) { +AmplitudeClient.prototype.setUserId = function setUserId(userId, startNewSession = false) { + if (!utils.validateInput(startNewSession, 'startNewSession', 'boolean')) { + return; + } + if (this._shouldDeferCall()) { return this._q.push(['setUserId'].concat(Array.prototype.slice.call(arguments, 0))); } try { this.options.userId = (userId !== undefined && userId !== null && '' + userId) || null; + if (startNewSession) { + if (this.options.unsetParamsReferrerOnNewSession) { + this._unsetUTMParams(); + } + this._newSession = true; + this._sessionId = new Date().getTime(); + + // only capture UTM params and referrer if new session + if (this.options.saveParamsReferrerOncePerSession) { + this._trackParamsAndReferrer(); + } + } + _saveCookieData(this); } catch (e) { utils.log.error(e); @@ -1232,6 +1250,7 @@ AmplitudeClient.prototype._logEvent = function _logEvent( timestamp, callback, errorCallback, + outOfSession, ) { _loadCookieData(this); // reload cookie before each log event to sync event meta-data between windows and tabs @@ -1257,7 +1276,13 @@ AmplitudeClient.prototype._logEvent = function _logEvent( } var sequenceNumber = this.nextSequenceNumber(); var eventTime = type(timestamp) === 'number' ? timestamp : new Date().getTime(); - if (!this._sessionId || !this._lastEventTime || eventTime - this._lastEventTime > this.options.sessionTimeout) { + if (outOfSession) { + this._sessionId = -1; + } else if ( + !this._sessionId || + !this._lastEventTime || + eventTime - this._lastEventTime > this.options.sessionTimeout + ) { this._sessionId = eventTime; } this._lastEventTime = eventTime; @@ -1383,13 +1408,20 @@ AmplitudeClient.prototype._limitEventsQueued = function _limitEventsQueued(queue * @param {Amplitude~eventCallback} opt_error_callback - (optional) a callback function to run after the event logging * fails. The failure can be from the request being malformed or from a network failure * Note: the server response code and response body from the event upload are passed to the callback function. + * @param {boolean} outOfSession - (optional) if this event is out of session or not * @example amplitudeClient.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15}); */ -AmplitudeClient.prototype.logEvent = function logEvent(eventType, eventProperties, opt_callback, opt_error_callback) { +AmplitudeClient.prototype.logEvent = function logEvent( + eventType, + eventProperties, + opt_callback, + opt_error_callback, + outOfSession = false, +) { if (this._shouldDeferCall()) { return this._q.push(['logEvent'].concat(Array.prototype.slice.call(arguments, 0))); } - return this.logEventWithTimestamp(eventType, eventProperties, null, opt_callback, opt_error_callback); + return this.logEventWithTimestamp(eventType, eventProperties, null, opt_callback, opt_error_callback, outOfSession); }; /** @@ -1403,6 +1435,7 @@ AmplitudeClient.prototype.logEvent = function logEvent(eventType, eventPropertie * @param {Amplitude~eventCallback} opt_error_callback - (optional) a callback function to run after the event logging * fails. The failure can be from the request being malformed or from a network failure * Note: the server response code and response body from the event upload are passed to the callback function. + * @param {boolean} outOfSession - (optional) if out of the sessioin or not * @example amplitudeClient.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15}); */ AmplitudeClient.prototype.logEventWithTimestamp = function logEvent( @@ -1411,6 +1444,7 @@ AmplitudeClient.prototype.logEventWithTimestamp = function logEvent( timestamp, opt_callback, opt_error_callback, + outOfSession = false, ) { if (this._shouldDeferCall()) { return this._q.push(['logEventWithTimestamp'].concat(Array.prototype.slice.call(arguments, 0))); @@ -1435,6 +1469,13 @@ AmplitudeClient.prototype.logEventWithTimestamp = function logEvent( }); return -1; } + + if (!utils.validateInput(outOfSession, 'outOfSession', 'boolean')) { + _logErrorsWithCallbacks(opt_callback, opt_error_callback, 0, 'No request sent', { + reason: 'Invalid outOfSession value', + }); + } + return this._logEvent( eventType, eventProperties, @@ -1445,6 +1486,7 @@ AmplitudeClient.prototype.logEventWithTimestamp = function logEvent( timestamp, opt_callback, opt_error_callback, + outOfSession, ); }; @@ -1473,6 +1515,7 @@ AmplitudeClient.prototype.logEventWithGroups = function ( groups, opt_callback, opt_error_callback, + outOfSession = false, ) { if (this._shouldDeferCall()) { return this._q.push(['logEventWithGroups'].concat(Array.prototype.slice.call(arguments, 0))); @@ -1490,7 +1533,25 @@ AmplitudeClient.prototype.logEventWithGroups = function ( }); return -1; } - return this._logEvent(eventType, eventProperties, null, null, groups, null, null, opt_callback, opt_error_callback); + + if (!utils.validateInput(outOfSession, 'outOfSession', 'boolean')) { + _logErrorsWithCallbacks(event.callback, event.errorCallback, 0, 'No request sent', { + reason: 'Invalid outOfSession value', + }); + } + + return this._logEvent( + eventType, + eventProperties, + null, + null, + groups, + null, + null, + opt_callback, + opt_error_callback, + outOfSession, + ); }; /** @@ -1847,7 +1908,10 @@ AmplitudeClient.prototype.__VERSION__ = function getVersion() { * @param {string} name - Custom library name * @param {string} version - Custom library version */ -AmplitudeClient.prototype.setLibrary = function setLibrary(name, version) { +AmplitudeClient.prototype.setLibrary = function setLibrary( + name = this.options.libraryName, + version = this.options.libraryVersion, +) { this.options.library = { name: name, version: version }; }; @@ -1897,4 +1961,140 @@ AmplitudeClient.prototype._refreshDynamicConfig = function _refreshDynamicConfig } }; +/** + * Returns the deviceId value. + * @public + * @return {string} Id of current device. + */ +AmplitudeClient.prototype.getDeviceId = function getDeviceId() { + return this.options.deviceId; +}; + +/** + * Returns the userId. + * @public + * @return {string} Id of current user. + */ +AmplitudeClient.prototype.getUserId = function getUserId() { + return this.options.userId; +}; + +/** + * Set a custom session expiration time. + * @public + * @param {number} timeInMillis - session expireation time in milliseconds. + */ +AmplitudeClient.prototype.setMinTimeBetweenSessionsMillis = function setMinTimeBetweenSessionsMillis(timeInMillis) { + if (!utils.validateInput(timeInMillis, 'timeInMillis', 'number')) { + return; + } + + if (this._shouldDeferCall()) { + return this._q.push(['setMinTimeBetweenSessionsMillis'].concat(Array.prototype.slice.call(arguments, 0))); + } + + try { + this.options.sessionTimeout = timeInMillis; + } catch (e) { + utils.log.error(e); + } +}; + +/** + * Sets minimum number of events to batch together per request if batchEvents is true. + * @public + * @param {number} eventUploadThreshold - The number of the event upload threshold. Default value is 30. + * @example amplitudeClient.setEventUploadThreshold(10); + */ +AmplitudeClient.prototype.setEventUploadThreshold = function setEventUploadThreshold(eventUploadThreshold) { + if (!utils.validateInput(eventUploadThreshold, 'eventUploadThreshold', 'number')) { + return; + } + + if (this._shouldDeferCall()) { + return this._q.push(['setEventUploadThreshold'].concat(Array.prototype.slice.call(arguments, 0))); + } + + try { + this.options.eventUploadThreshold = eventUploadThreshold; + } catch (e) { + utils.log.error(e); + } +}; + +/** + * Dynamically adjust server URL + * @public + * @param {bool} useDynamicConfig - if enable dynamic config or not. + * @example amplitudeClient.setUseDynamicConfig(true); + */ +AmplitudeClient.prototype.setUseDynamicConfig = function setUseDynamicConfig(useDynamicConfig) { + if (!utils.validateInput(useDynamicConfig, 'useDynamicConfig', 'boolean')) { + return; + } + + if (this._shouldDeferCall()) { + return this._q.push(['setUseDynamicConfig'].concat(Array.prototype.slice.call(arguments, 0))); + } + + try { + this.options.useDynamicConfig = useDynamicConfig; + this._refreshDynamicConfig(); + } catch (e) { + utils.log.error(e); + } +}; + +/** + * Sets the server zone, used for server api endpoint and dynamic configuration. + * @public + * @param {string} serverZone - the server zone value. AmplitudeServerZone.US or AmplitudeServerZone.EU. + * @param {bool} serverZoneBasedApi - (optional) update api endpoint with serverZone change or not. For data residency, recommend to enable it unless using own proxy server. + * @example amplitudeClient.setServerZone('joe@gmail.com', true); + */ +AmplitudeClient.prototype.setServerZone = function setServerZone(serverZone, serverZoneBasedApi = true) { + if ( + (serverZone !== AmplitudeServerZone.EU && serverZone !== AmplitudeServerZone.US) || + !utils.validateInput(serverZoneBasedApi, 'serverZoneBasedApi', 'boolean') + ) { + return; + } + + if (this._shouldDeferCall()) { + return this._q.push(['setServerZone'].concat(Array.prototype.slice.call(arguments, 0))); + } + + try { + this.options.serverZone = serverZone; + this.options.serverZoneBasedApi = serverZoneBasedApi; + if (serverZoneBasedApi) { + this.options.apiEndpoint = getEventLogApi(this.options.serverZone); + } + } catch (e) { + utils.log.error(e); + } +}; + +/** + * Sets the server URL for the request. + * @public + * @param {string} serverUrl - The value of the server URL. + * @example amplitudeClient.setServerUrl('api.amplitude.com'); + */ +AmplitudeClient.prototype.setServerUrl = function setServerUrl(serverUrl) { + if (!utils.validateInput(serverUrl, 'serverUrl', 'string')) { + return; + } + + if (this._shouldDeferCall()) { + return this._q.push(['setServerUrl'].concat(Array.prototype.slice.call(arguments, 0))); + } + + try { + this.options.apiEndpoint = serverUrl; + } catch (e) { + utils.log.error(e); + } +}; + export default AmplitudeClient; diff --git a/src/amplitude-snippet.js b/src/amplitude-snippet.js index f31bb491..7d59b2d4 100644 --- a/src/amplitude-snippet.js +++ b/src/amplitude-snippet.js @@ -79,6 +79,14 @@ 'logEventWithGroups', 'setSessionId', 'resetSessionId', + 'getDeviceId', + 'getUserId', + 'setMinTimeBetweenSessionsMillis', + 'setEventUploadThreshold', + 'setUseDynamicConfig', + 'setServerZone', + 'setServerUrl', + 'sendEvents', 'setLibrary', ]; function setUpProxy(instance) { diff --git a/test/amplitude-client.js b/test/amplitude-client.js index f60aea56..78bb6018 100644 --- a/test/amplitude-client.js +++ b/test/amplitude-client.js @@ -2840,6 +2840,37 @@ describe('AmplitudeClient', function () { }); }); + describe('logEvent with outOfSession', function () { + this.beforeEach(function () { + reset(); + }); + + it('should reset the sessionId', function () { + amplitude.init(apiKey); + amplitude.logEvent('Event Type', null, null, null, true); + assert.equal(amplitude._sessionId, -1); + }); + }); + + describe('setEventUploadThreshold', function () { + beforeEach(function () { + reset(); + }); + + it('should not set eventUploadThreshold with invalid eventUploadThreshold value', function () { + amplitude.init(apiKey); + let previousEventUploadThreshold = amplitude.options.eventUploadThreshold; + amplitude.setEventUploadThreshold('invalid eventUploadThreshold'); + assert.equal(amplitude.options.eventUploadThreshold, previousEventUploadThreshold); + }); + + it('should set eventUploadThreshold', function () { + amplitude.init(apiKey); + amplitude.setEventUploadThreshold(5); + assert.equal(amplitude.options.eventUploadThreshold, 5); + }); + }); + describe('optOut', function () { beforeEach(function () { amplitude.init(apiKey); @@ -2916,6 +2947,26 @@ describe('AmplitudeClient', function () { }); }); + describe('setOptOut', function () { + beforeEach(function () { + reset(); + }); + + it('should not set optOut with invalid input', function () { + amplitude.init(apiKey); + let previousOptOut = amplitude.options.optOut; + amplitude.setOptOut('invalid sessionTimeOut'); + assert.equal(amplitude.options.optOut, previousOptOut); + }); + + it('should set optOut', function () { + amplitude.init(apiKey); + let optOut = true; + amplitude.setOptOut(optOut); + assert.equal(amplitude.options.optOut, true); + }); + }); + describe('gatherUtm', function () { var clock; beforeEach(function () { @@ -4121,6 +4172,7 @@ describe('AmplitudeClient', function () { beforeEach(function () { reset(); }); + it('should use default library options', function () { amplitude.init(apiKey); amplitude.logEvent('Event Type 1'); @@ -4148,5 +4200,224 @@ describe('AmplitudeClient', function () { assert.equal(name, 'test-library'); assert.equal(version, '1.0-test'); }); + + it('should use the customize library name and default library version', function () { + amplitude.init(apiKey); + amplitude.setLibrary('test-library', undefined); + amplitude.logEvent('Event Type'); + + const { name, version } = JSON.parse(queryString.parse(server.requests[0].requestBody).e)[0].library; + + assert.equal(name, 'test-library'); + assert.equal(version, amplitude.options.library.version); + }); + + it('should use the customize library version and default library name', function () { + amplitude.init(apiKey); + amplitude.setLibrary(undefined, '1.0-test'); + amplitude.logEvent('Event Type'); + + const { name, version } = JSON.parse(queryString.parse(server.requests[0].requestBody).e)[0].library; + + assert.equal(name, amplitude.options.library.name); + assert.equal(version, '1.0-test'); + }); + }); + + describe('setUseDynamicConfig', function () { + beforeEach(function () { + reset(); + }); + + it('EU serverZone should not set apiEndpoint to EU because of invalid useDynamicConfig value', function () { + assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_URL); + amplitude.init(apiKey, null, { + serverZone: AmplitudeServerZone.EU, + }); + amplitude.setUseDynamicConfig('invalid useDynamicConfig'); + assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_URL); + }); + + it('should not set apiEndpoint to EU because because of dynamic configuration not enable', function () { + assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_URL); + amplitude.init(apiKey, null, { + serverZone: AmplitudeServerZone.EU, + }); + amplitude.setUseDynamicConfig(false); + assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_URL); + }); + + it('EU serverZone with dynamic configuration enable should set apiEndpoint to EU', function () { + assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_URL); + amplitude.init(apiKey, null, { + serverZone: AmplitudeServerZone.EU, + }); + amplitude.setUseDynamicConfig(true); + server.respondWith('{"ingestionEndpoint": "api.eu.amplitude.com"}'); + server.respond(); + assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_EU_URL); + }); + }); + + describe('setServerUrl', function () { + beforeEach(function () { + reset(); + }); + + it('should not set serverUrl because of invalid serverUrl input', function () { + assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_URL); + amplitude.init(apiKey); + amplitude.setServerUrl(100); + assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_URL); + }); + + it('should set serverUrl with valid serverUrl input', function () { + assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_URL); + amplitude.init(apiKey); + amplitude.setServerUrl(constants.EVENT_LOG_EU_URL); + assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_EU_URL); + }); + }); + + describe('setServerZone', function () { + beforeEach(function () { + reset(); + }); + + it('should not set serverZone with invalid serverZone value', function () { + amplitude.init(apiKey); + let previousServerZone = amplitude.options.serverZone; + amplitude.setServerZone('invalid serverZone'); + assert.equal(amplitude.options.serverZone, previousServerZone); + }); + + it('should not set serverZone with invalid serverZoneBasedApi value', function () { + amplitude.init(apiKey); + assert.equal(amplitude.options.serverZone, AmplitudeServerZone.US); + amplitude.setServerZone(AmplitudeServerZone.EU, 'invalid serverZoneBasedApi'); + assert.equal(amplitude.options.serverZone, AmplitudeServerZone.US); + }); + + it('should set serverZone to EU', function () { + amplitude.init(apiKey); + assert.equal(amplitude.options.serverZone, AmplitudeServerZone.US); + amplitude.setServerZone(AmplitudeServerZone.EU); + assert.equal(amplitude.options.serverZone, AmplitudeServerZone.EU); + assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_EU_URL); + }); + + it('should set serverZone to EU and keep the default serverUrl with serverZoneBasedApi is false', function () { + amplitude.init(apiKey); + assert.equal(amplitude.options.serverZone, AmplitudeServerZone.US); + amplitude.setServerZone(AmplitudeServerZone.EU, false); + assert.equal(amplitude.options.serverZone, AmplitudeServerZone.EU); + assert.equal(amplitude.options.apiEndpoint, constants.EVENT_LOG_URL); + }); + }); + + describe('getUserId', function () { + beforeEach(function () { + reset(); + }); + + it('should get userId', function () { + amplitude.init(apiKey, userId); + let currentUserId = amplitude.getUserId(); + assert.equal(currentUserId, userId); + }); + + it('should get userId null', function () { + amplitude.init(apiKey); + assert.equal(amplitude.getUserId(), null); + }); + }); + + describe('getDeviceId', function () { + beforeEach(function () { + reset(); + }); + + it('should get a random deviceId', function () { + amplitude.init(apiKey, userId); + assert.lengthOf(amplitude.getDeviceId(), 22); + }); + + it('should get deviceId', function () { + const currentDeviceId = 'aa_bb_cc'; + amplitude.init(apiKey, null, { deviceId: currentDeviceId }); + assert.equal(amplitude.getDeviceId(), currentDeviceId); + }); + }); + + describe('setMinTimeBetweenSessionsMillis', function () { + beforeEach(function () { + reset(); + }); + + it('should not set sessionTimeout with invalid input', function () { + amplitude.init(apiKey); + let previousSessionTimeOut = amplitude.options.sessionTimeout; + let newSessionTimeOut = 'invalid sessionTimeOut'; + amplitude.setMinTimeBetweenSessionsMillis(newSessionTimeOut); + assert.equal(amplitude.options.sessionTimeout, previousSessionTimeOut); + }); + + it('should set sessionTimeout', function () { + amplitude.init(apiKey); + let newSessionTimeOut = 100; + amplitude.setMinTimeBetweenSessionsMillis(newSessionTimeOut); + assert.equal(amplitude.options.sessionTimeout, newSessionTimeOut); + }); + }); + + describe('setUserId', function () { + let clock, startTime; + beforeEach(function () { + reset(); + startTime = Date.now(); + clock = sinon.useFakeTimers(startTime); + amplitude.init(apiKey); + }); + + it('should not renew the session id with invalid startNewSession input', function () { + var amplitude = new AmplitudeClient(); + // set up initial session + var sessionId = 1000; + clock.tick(sessionId); + amplitude.init(apiKey); + assert.equal(amplitude.getSessionId(), startTime); + assert.equal(amplitude.options.userId, null); + + amplitude.setUserId('test user', 'invalid startNewSession'); + assert.notEqual(amplitude.getSessionId(), new Date().getTime()); + assert.notEqual(amplitude.options.userId, 'test user'); + assert.equal(amplitude.options.userId, null); + }); + + it('should set user id and renew the session id with current timestemp', function () { + var amplitude = new AmplitudeClient(); + // set up initial session + var sessionId = 1000; + clock.tick(sessionId); + amplitude.init(apiKey); + assert.equal(amplitude.getSessionId(), startTime); + + amplitude.setUserId('test user', true); + assert.equal(amplitude.getSessionId(), new Date().getTime()); + assert.equal(amplitude.options.userId, 'test user'); + }); + + it('should continue the old session', function () { + var amplitude = new AmplitudeClient(); + // set up initial session + var sessionId = 1000; + clock.tick(sessionId); + amplitude.init(apiKey); + assert.equal(amplitude.getSessionId(), startTime); + + amplitude.setUserId('test user'); + assert.equal(amplitude.getSessionId(), startTime); + assert.equal(amplitude.options.userId, 'test user'); + }); }); });