Skip to content
Merged
Show file tree
Hide file tree
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 #
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "amplitude-js",
"author": "Amplitude <support@amplitude.com>",
"version": "5.7.1",
"version": "5.8.0",
"license": "MIT",
"description": "Javascript library for Amplitude Analytics",
"keywords": [
Expand Down
101 changes: 89 additions & 12 deletions src/amplitude-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

moved into the try catch to ensure that this._storageSuffix is defined

Copy link
Member

Choose a reason for hiding this comment

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

i kind of like that you're using the cookie to signal if the user has accepted tracking

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i feel like that's the safest bet to carry over. i haven't fully thought of the case where our cookie expires tho.... 😬

if (opt_config && opt_config.deferInitialization && !hasExistingCookie) {
Copy link
Member

Choose a reason for hiding this comment

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

note for existing users, they won't get the option to defer tracking because they will already have a cookie, i guess we just need to communicate that to our customers and the people that requested this feature

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, this will only work on new users without existing cookies and they have to add the config flag

this._deferInitialization(apiKey, opt_userId, opt_config, opt_callback);
return;
}

_parseConfig(this.options, opt_config);

if (type(this.options.logLevel) === 'string') {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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');
};

Expand All @@ -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;
}
Expand All @@ -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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
};
Expand All @@ -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') {
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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;
6 changes: 3 additions & 3 deletions src/amplitude-snippet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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'];
Expand Down
103 changes: 103 additions & 0 deletions test/amplitude-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});
});