diff --git a/CHANGELOG.md b/CHANGELOG.md index b06357f1..0899ab07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### 5.8.0 (December 6, 2019) +* Add support to defer saving an amplitude cookie and logging events until a user has opted in + ### 5.7.1 (December 2, 2019) * Fix issue where null unsentKey and unsentIdentifyKeys were causing log crashes diff --git a/README.md b/README.md index 3d7c7cfa..f0e67076 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Please see our [installation guide](https://amplitude.zendesk.com/hc/en-us/artic [![npm version](https://badge.fury.io/js/amplitude-js.svg)](https://badge.fury.io/js/amplitude-js) [![Bower version](https://badge.fury.io/bo/amplitude-js.svg)](https://badge.fury.io/bo/amplitude-js) -[5.7.1 - Released on December 3, 2019](https://github.com/amplitude/Amplitude-JavaScript/releases/latest) +[5.8.0 - Released on December 6, 2019](https://github.com/amplitude/Amplitude-JavaScript/releases/latest) # JavaScript SDK Reference # diff --git a/package.json b/package.json index a9b779ab..7eb9c59f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "amplitude-js", "author": "Amplitude ", - "version": "5.7.1", + "version": "5.8.0", "license": "MIT", "description": "Javascript library for Amplitude Analytics", "keywords": [ diff --git a/src/amplitude-client.js b/src/amplitude-client.js index f32473f1..d74f8026 100644 --- a/src/amplitude-client.js +++ b/src/amplitude-client.js @@ -79,6 +79,12 @@ AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, o this.options.apiKey = apiKey; this._storageSuffix = '_' + apiKey + this._legacyStorageSuffix; + var hasExistingCookie = !!this.cookieStorage.get(this.options.cookieName + this._storageSuffix); + if (opt_config && opt_config.deferInitialization && !hasExistingCookie) { + this._deferInitialization(apiKey, opt_userId, opt_config, opt_callback); + return; + } + _parseConfig(this.options, opt_config); if (type(this.options.logLevel) === 'string') { @@ -766,6 +772,10 @@ AmplitudeClient.prototype.saveEvents = function saveEvents() { * @example amplitudeClient.setDomain('.amplitude.com'); */ AmplitudeClient.prototype.setDomain = function setDomain(domain) { + if (this._shouldDeferCall()) { + return this._q.push(['setDomain'].concat(Array.prototype.slice.call(arguments, 0))); + } + if (!utils.validateInput(domain, 'domain', 'string')) { return; } @@ -791,6 +801,10 @@ AmplitudeClient.prototype.setDomain = function setDomain(domain) { * @example amplitudeClient.setUserId('joe@gmail.com'); */ AmplitudeClient.prototype.setUserId = function setUserId(userId) { + 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; _saveCookieData(this); @@ -813,6 +827,10 @@ AmplitudeClient.prototype.setUserId = function setUserId(userId) { * @example amplitudeClient.setGroup('orgId', 15); // this adds the current user to orgId 15. */ AmplitudeClient.prototype.setGroup = function(groupType, groupName) { + if (this._shouldDeferCall()) { + return this._q.push(['setGroup'].concat(Array.prototype.slice.call(arguments, 0))); + } + if (!this._apiKeySet('setGroup()') || !utils.validateInput(groupType, 'groupType', 'string') || utils.isEmptyString(groupType)) { return; @@ -831,6 +849,10 @@ AmplitudeClient.prototype.setGroup = function(groupType, groupName) { * @example: amplitude.setOptOut(true); */ AmplitudeClient.prototype.setOptOut = function setOptOut(enable) { + if (this._shouldDeferCall()) { + return this._q.push(['setOptOut'].concat(Array.prototype.slice.call(arguments, 0))); + } + if (!utils.validateInput(enable, 'enable', 'boolean')) { return; } @@ -868,6 +890,10 @@ AmplitudeClient.prototype.resetSessionId = function resetSessionId() { * @public */ AmplitudeClient.prototype.regenerateDeviceId = function regenerateDeviceId() { + if (this._shouldDeferCall()) { + return this._q.push(['regenerateDeviceId'].concat(Array.prototype.slice.call(arguments, 0))); + } + this.setDeviceId(UUID() + 'R'); }; @@ -880,6 +906,10 @@ AmplitudeClient.prototype.regenerateDeviceId = function regenerateDeviceId() { * @example amplitudeClient.setDeviceId('45f0954f-eb79-4463-ac8a-233a6f45a8f0'); */ AmplitudeClient.prototype.setDeviceId = function setDeviceId(deviceId) { + if (this._shouldDeferCall()) { + return this._q.push(['setDeviceId'].concat(Array.prototype.slice.call(arguments, 0))); + } + if (!utils.validateInput(deviceId, 'deviceId', 'string')) { return; } @@ -903,8 +933,8 @@ AmplitudeClient.prototype.setDeviceId = function setDeviceId(deviceId) { * @example amplitudeClient.setUserProperties({'gender': 'female', 'sign_up_complete': true}) */ AmplitudeClient.prototype.setUserProperties = function setUserProperties(userProperties) { - if (this._pendingReadStorage) { - return this._q.push(['identify', userProperties]); + if (this._shouldDeferCall()) { + return this._q.push(['setUserProperties'].concat(Array.prototype.slice.call(arguments, 0))); } if (!this._apiKeySet('setUserProperties()') || !utils.validateInput(userProperties, 'userProperties', 'object')) { return; @@ -931,6 +961,10 @@ AmplitudeClient.prototype.setUserProperties = function setUserProperties(userPro * @example amplitudeClient.clearUserProperties(); */ AmplitudeClient.prototype.clearUserProperties = function clearUserProperties(){ + if (this._shouldDeferCall()) { + return this._q.push(['clearUserProperties'].concat(Array.prototype.slice.call(arguments, 0))); + } + if (!this._apiKeySet('clearUserProperties()')) { return; } @@ -967,8 +1001,8 @@ var _convertProxyObjectToRealObject = function _convertProxyObjectToRealObject(i * amplitude.identify(identify); */ AmplitudeClient.prototype.identify = function(identify_obj, opt_callback) { - if (this._pendingReadStorage) { - return this._q.push(['identify', identify_obj, opt_callback]); + if (this._shouldDeferCall()) { + return this._q.push(['identify'].concat(Array.prototype.slice.call(arguments, 0))); } if (!this._apiKeySet('identify()')) { if (type(opt_callback) === 'function') { @@ -1002,8 +1036,8 @@ AmplitudeClient.prototype.identify = function(identify_obj, opt_callback) { }; AmplitudeClient.prototype.groupIdentify = function(group_type, group_name, identify_obj, opt_callback) { - if (this._pendingReadStorage) { - return this._q.push(['groupIdentify', group_type, group_name, identify_obj, opt_callback]); + if (this._shouldDeferCall()) { + return this._q.push(['groupIdentify'].concat(Array.prototype.slice.call(arguments, 0))); } if (!this._apiKeySet('groupIdentify()')) { if (type(opt_callback) === 'function') { @@ -1058,6 +1092,10 @@ AmplitudeClient.prototype.groupIdentify = function(group_type, group_name, ident * @example amplitudeClient.setVersionName('1.12.3'); */ AmplitudeClient.prototype.setVersionName = function setVersionName(versionName) { + if (this._shouldDeferCall()) { + return this._q.push(['setVersionName'].concat(Array.prototype.slice.call(arguments, 0))); + } + if (!utils.validateInput(versionName, 'versionName', 'string')) { return; } @@ -1217,8 +1255,8 @@ AmplitudeClient.prototype._limitEventsQueued = function _limitEventsQueued(queue * @example amplitudeClient.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15}); */ AmplitudeClient.prototype.logEvent = function logEvent(eventType, eventProperties, opt_callback) { - if (this._pendingReadStorage) { - return this._q.push(['logEvent', eventType, eventProperties, opt_callback]); + if (this._shouldDeferCall()) { + return this._q.push(['logEvent'].concat(Array.prototype.slice.call(arguments, 0))); } return this.logEventWithTimestamp(eventType, eventProperties, null, opt_callback); }; @@ -1234,8 +1272,8 @@ AmplitudeClient.prototype.logEvent = function logEvent(eventType, eventPropertie * @example amplitudeClient.logEvent('Clicked Homepage Button', {'finished_flow': false, 'clicks': 15}); */ AmplitudeClient.prototype.logEventWithTimestamp = function logEvent(eventType, eventProperties, timestamp, opt_callback) { - if (this._pendingReadStorage) { - return this._q.push(['logEventWithTimestamp', eventType, eventProperties, timestamp, opt_callback]); + if (this._shouldDeferCall()) { + return this._q.push(['logEventWithTimestamp'].concat(Array.prototype.slice.call(arguments, 0))); } if (!this._apiKeySet('logEvent()')) { if (type(opt_callback) === 'function') { @@ -1274,8 +1312,8 @@ AmplitudeClient.prototype.logEventWithTimestamp = function logEvent(eventType, e * @example amplitudeClient.logEventWithGroups('Clicked Button', null, {'orgId': 24}); */ AmplitudeClient.prototype.logEventWithGroups = function(eventType, eventProperties, groups, opt_callback) { - if (this._pendingReadStorage) { - return this._q.push(['logEventWithGroups', eventType, eventProperties, groups, opt_callback]); + if (this._shouldDeferCall()) { + return this._q.push(['logEventWithGroups'].concat(Array.prototype.slice.call(arguments, 0))); } if (!this._apiKeySet('logEventWithGroups()')) { if (type(opt_callback) === 'function') { @@ -1311,6 +1349,10 @@ var _isNumber = function _isNumber(n) { * amplitude.logRevenueV2(revenue); */ AmplitudeClient.prototype.logRevenueV2 = function logRevenueV2(revenue_obj) { + if (this._shouldDeferCall()) { + return this._q.push(['logRevenueV2'].concat(Array.prototype.slice.call(arguments, 0))); + } + if (!this._apiKeySet('logRevenueV2()')) { return; } @@ -1341,6 +1383,10 @@ if (BUILD_COMPAT_2_0) { * @example amplitudeClient.logRevenue(3.99, 1, 'product_1234'); */ AmplitudeClient.prototype.logRevenue = function logRevenue(price, quantity, product) { + if (this._shouldDeferCall()) { + return this._q.push(['logRevenue'].concat(Array.prototype.slice.call(arguments, 0))); + } + // Test that the parameters are of the right type. if (!this._apiKeySet('logRevenue()') || !_isNumber(price) || (quantity !== undefined && !_isNumber(quantity))) { // utils.log('Price and quantity arguments to logRevenue must be numbers'); @@ -1552,4 +1598,35 @@ if (BUILD_COMPAT_2_0) { */ AmplitudeClient.prototype.__VERSION__ = version; +/** + * Determines whether or not to push call to this._q or invoke it + * @private + */ +AmplitudeClient.prototype._shouldDeferCall = function _shouldDeferCall() { + return this._pendingReadStorage || this._initializationDeferred; +}; + +/** + * Defers Initialization by putting all functions into storage until users + * have accepted terms for tracking + * @private + */ +AmplitudeClient.prototype._deferInitialization = function _deferInitialization() { + this._initializationDeferred = true; + this._q.push(['init'].concat(Array.prototype.slice.call(arguments, 0))); +}; + +/** + * Enable tracking via logging events and dropping a cookie + * Intended to be used with the deferInitialization configuration flag + * This will drop a cookie and reset initialization deferred + * @public + */ +AmplitudeClient.prototype.enableTracking = function enableTracking() { + // This will call init (which drops the cookie) and will run any pending tasks + this._initializationDeferred = false; + _saveCookieData(this); + this.runQueuedFunctions(); +}; + export default AmplitudeClient; diff --git a/src/amplitude-snippet.js b/src/amplitude-snippet.js index 7d6ea141..fbed24a5 100644 --- a/src/amplitude-snippet.js +++ b/src/amplitude-snippet.js @@ -2,10 +2,10 @@ var amplitude = window.amplitude || {'_q':[],'_iq':{}}; var as = document.createElement('script'); as.type = 'text/javascript'; - as.integrity = 'sha384-Ik1BT1T0ZKcBQi93L3Lh8pYLQvUANkj37BjU140rtlIwQSj9ePR4dOoqfWj9u5qU'; + as.integrity = 'sha384-vYYnQ3LPdp/RkQjoKBTGSq0X5F73gXU3G2QopHaIfna0Ct1JRWzwrmEz115NzOta'; as.crossOrigin = 'anonymous'; as.async = true; - as.src = 'https://cdn.amplitude.com/libs/amplitude-5.7.1-min.gz.js'; + as.src = 'https://cdn.amplitude.com/libs/amplitude-5.8.0-min.gz.js'; as.onload = function() {if(!window.amplitude.runQueuedFunctions) {console.log('[Amplitude] Error: could not load SDK');}}; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(as, s); @@ -23,7 +23,7 @@ for (var j = 0; j < revenueFuncs.length; j++) {proxy(Revenue, revenueFuncs[j]);} amplitude.Revenue = Revenue; var funcs = ['init', 'logEvent', 'logRevenue', 'setUserId', 'setUserProperties', - 'setOptOut', 'setVersionName', 'setDomain', 'setDeviceId', + 'setOptOut', 'setVersionName', 'setDomain', 'setDeviceId', 'enableTracking', 'setGlobalUserProperties', 'identify', 'clearUserProperties', 'setGroup', 'logRevenueV2', 'regenerateDeviceId', 'groupIdentify', 'onInit', 'logEventWithTimestamp', 'logEventWithGroups', 'setSessionId', 'resetSessionId']; diff --git a/test/amplitude-client.js b/test/amplitude-client.js index 711575b8..ef25f6f3 100644 --- a/test/amplitude-client.js +++ b/test/amplitude-client.js @@ -3331,4 +3331,107 @@ describe('setVersionName', function() { assert.equal(cookieStorage.get(amplitude2.options.cookieName + '_' + apiKey).sessionId, newSessionId); }); }); + + describe('deferInitialization config', function () { + it('should keep tracking users who already have an amplitude cookie', function () { + var now = new Date().getTime(); + var cookieData = { + userId: 'test_user_id', + optOut: false, + sessionId: now, + lastEventTime: now, + eventId: 50, + identifyId: 60 + } + + cookie.set(amplitude.options.cookieName + keySuffix, cookieData); + amplitude.init(apiKey, null, { cookieExpiration: 365, deferInitialization: true }); + amplitude.identify(new Identify().set('prop1', 'value1')); + + var events = JSON.parse(queryString.parse(server.requests[0].requestBody).e); + assert.lengthOf(server.requests, 1, 'should have sent a request to Amplitude'); + assert.equal(events[0].event_type, '$identify'); + }); + describe('prior to opting into analytics', function () { + beforeEach(function () { + reset(); + amplitude.init(apiKey, null, { cookieExpiration: 365, deferInitialization: true }); + }); + it('should not initially drop a cookie if deferInitialization is set to true', function () { + var cookieData = cookie.get(amplitude.options.cookieName + '_' + apiKey); + assert.isNull(cookieData); + }); + it('should not send anything to amplitude', function () { + amplitude.identify(new Identify().set('prop1', 'value1')); + amplitude.logEvent('Event Type 1'); + amplitude.setDomain('.foobar.com'); + amplitude.setUserId(123456); + amplitude.setGroup('orgId', 15); + amplitude.setOptOut(true); + amplitude.regenerateDeviceId(); + amplitude.setDeviceId('deviceId'); + amplitude.setUserProperties({'prop': true, 'key': 'value'}); + amplitude.clearUserProperties(); + amplitude.groupIdentify(null, null, new amplitude.Identify().set('key', 'value')); + amplitude.setVersionName('testVersionName1'); + amplitude.logEventWithTimestamp('test', null, 2000, null); + amplitude.logEventWithGroups('Test', {'key': 'value' }, {group: 'abc'}); + amplitude.logRevenue(10.10); + + var revenue = new amplitude.Revenue().setProductId('testProductId').setQuantity(15).setPrice(10.99); + revenue.setRevenueType('testRevenueType').setEventProperties({'city': 'San Francisco'}); + amplitude.logRevenueV2(revenue); + + assert.lengthOf(server.requests, 0, 'should not send any requests to amplitude'); + assert.lengthOf(amplitude._unsentEvents, 0, 'should not queue events to be sent') + }); + }); + + describe('upon opting into analytics', function () { + beforeEach(function () { + reset(); + amplitude.init(apiKey, null, { cookieExpiration: 365, deferInitialization: true }); + }); + it('should drop a cookie', function () { + amplitude.enableTracking(); + var cookieData = cookie.get(amplitude.options.cookieName + '_' + apiKey); + assert.isNotNull(cookieData); + }); + it('should send pending calls and events', function () { + amplitude.identify(new Identify().set('prop1', 'value1')); + amplitude.logEvent('Event Type 1'); + amplitude.logEvent('Event Type 2'); + amplitude.logEventWithTimestamp('test', null, 2000, null); + assert.lengthOf(amplitude._unsentEvents, 0, 'should not have any pending events to be sent'); + amplitude.enableTracking(); + + assert.lengthOf(server.requests, 1, 'should have sent a request to Amplitude'); + var events = JSON.parse(queryString.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1, 'should have sent a request to Amplitude'); + assert.lengthOf(amplitude._unsentEvents, 3, 'should have saved the remaining events') + }); + it('should send new events', function () { + assert.lengthOf(amplitude._unsentEvents, 0, 'should start with no pending events to be sent'); + amplitude.identify(new Identify().set('prop1', 'value1')); + amplitude.logEvent('Event Type 1'); + amplitude.logEvent('Event Type 2'); + amplitude.logEventWithTimestamp('test', null, 2000, null); + assert.lengthOf(amplitude._unsentEvents, 0, 'should not have any pending events to be sent'); + + amplitude.enableTracking(); + assert.lengthOf(amplitude._unsentEvents, 3, 'should have saved the remaining events') + + amplitude.logEvent('Event Type 3'); + assert.lengthOf(amplitude._unsentEvents, 4, 'should save the new events') + }); + it('should not continue to deferInitialization if an amplitude cookie exists', function () { + amplitude.enableTracking(); + amplitude.init(apiKey, null, { cookieExpiration: 365, deferInitialization: true }); + amplitude.logEvent('Event Type 1'); + + var events = JSON.parse(queryString.parse(server.requests[0].requestBody).e); + assert.lengthOf(events, 1, 'should have sent a request to Amplitude'); + }); + }); + }); });