diff --git a/src/amplitude-client.js b/src/amplitude-client.js index 1d85f5ca..4ac34e8c 100644 --- a/src/amplitude-client.js +++ b/src/amplitude-client.js @@ -116,7 +116,8 @@ AmplitudeClient.prototype.init = function init(apiKey, opt_userId, opt_config, o expirationDays: this.options.cookieExpiration, domain: this.options.domain, secure: this.options.secureCookie, - sameSite: this.options.sameSiteCookie + sameSite: this.options.sameSiteCookie, + storage: this.options.storage }); const hasOldCookie = !!this.cookieStorage.get(this._oldCookiename); diff --git a/src/constants.js b/src/constants.js index 96fc6f10..16844823 100644 --- a/src/constants.js +++ b/src/constants.js @@ -21,6 +21,13 @@ export default { COOKIE_TEST_PREFIX: 'amp_cookie_test', COOKIE_PREFIX: "amp", + // Storage options + STORAGE_DEFAULT: '', + STORAGE_COOKIES: 'cookies', + STORAGE_NONE: 'none', + STORAGE_LOCAL: 'localStorage', + STORAGE_SESSION: 'sessionStorage', + // revenue keys REVENUE_EVENT: 'revenue_amount', REVENUE_PRODUCT_ID: '$productId', diff --git a/src/metadata-storage.js b/src/metadata-storage.js index 5e085211..45ac4433 100644 --- a/src/metadata-storage.js +++ b/src/metadata-storage.js @@ -5,13 +5,22 @@ import Base64 from './base64'; import baseCookie from './base-cookie'; +import Constants from './constants'; import getLocation from './get-location'; import localStorage from './localstorage'; // jshint ignore:line import topDomain from './top-domain'; +const storageOptionExists = { + [Constants.STORAGE_COOKIES]: true, + [Constants.STORAGE_NONE]: true, + [Constants.STORAGE_LOCAL]: true, + [Constants.STORAGE_SESSION]: true, +}; + /** * MetadataStorage involves SDK data persistance * storage priority: cookies -> localStorage -> in memory + * This priority can be overriden by setting the storage options. * if in localStorage, unable track users between subdomains * if in memory, then memory can't be shared between different tabs */ @@ -23,6 +32,7 @@ class MetadataStorage { secure, sameSite, expirationDays, + storage, }) { this.storageKey = storageKey; this.domain = domain; @@ -38,14 +48,23 @@ class MetadataStorage { domain || (writableTopDomain ? '.' + writableTopDomain : null); } - this.disableCookieStorage = - disableCookies || - !baseCookie.areCookiesEnabled({ - domain: this.cookieDomain, - secure: this.secure, - sameSite: this.sameSite, - expirationDays: this.expirationDays, - }); + if (storageOptionExists[storage]) { + this.storage = storage; + } else { + const disableCookieStorage = + disableCookies || + !baseCookie.areCookiesEnabled({ + domain: this.cookieDomain, + secure: this.secure, + sameSite: this.sameSite, + expirationDays: this.expirationDays, + }); + if (disableCookieStorage) { + this.storage = Constants.STORAGE_LOCAL; + } else { + this.storage = Constants.STORAGE_COOKIES; + } + } } getCookieStorageKey() { @@ -73,6 +92,9 @@ class MetadataStorage { identifyId, sequenceNumber, }) { + if (this.storage === Constants.STORAGE_NONE) { + return; + } const value = [ deviceId, Base64.encode(userId || ''), // used to convert not unicode to alphanumeric since cookies only use alphanumeric @@ -84,26 +106,37 @@ class MetadataStorage { sequenceNumber ? sequenceNumber.toString(32) : '0', ].join('.'); - if (this.disableCookieStorage) { - localStorage.setItem(this.storageKey, value); - } else { - baseCookie.set(this.getCookieStorageKey(), value, { - domain: this.cookieDomain, - secure: this.secure, - sameSite: this.sameSite, - expirationDays: this.expirationDays, - }); + switch (this.storage) { + case Constants.STORAGE_SESSION: + if (window.sessionStorage) { + window.sessionStorage.setItem(this.storageKey, value); + } + break; + case Constants.STORAGE_LOCAL: + localStorage.setItem(this.storageKey, value); + break; + case Constants.STORAGE_COOKIES: + baseCookie.set(this.getCookieStorageKey(), value, { + domain: this.cookieDomain, + secure: this.secure, + sameSite: this.sameSite, + expirationDays: this.expirationDays, + }); + break; } } load() { let str; - if (!this.disableCookieStorage) { + if (this.storage === Constants.STORAGE_COOKIES) { str = baseCookie.get(this.getCookieStorageKey() + '='); } if (!str) { str = localStorage.getItem(this.storageKey); } + if (!str) { + str = window.sessionStorage && window.sessionStorage.getItem(this.storageKey); + } if (!str) { return null; diff --git a/src/options.js b/src/options.js index 1793cef8..e37d8598 100644 --- a/src/options.js +++ b/src/options.js @@ -1,3 +1,4 @@ +import Constants from './constants'; import language from './language'; let platform = 'Web'; @@ -19,7 +20,7 @@ export default { sameSiteCookie: 'Lax', // cookie privacy policy cookieForceUpgrade: false, deferInitialization: false, - disableCookies: false, + disableCookies: false, // this is a deprecated option deviceIdFromUrlParam: false, domain: '', eventUploadPeriodMillis: 30 * 1000, // 30s @@ -39,6 +40,7 @@ export default { saveParamsReferrerOncePerSession: true, secureCookie: false, sessionTimeout: 30 * 60 * 1000, + storage: Constants.STORAGE_DEFAULT, trackingOptions: { city: true, country: true, diff --git a/test/amplitude-client.js b/test/amplitude-client.js index a8c72532..97fdb949 100644 --- a/test/amplitude-client.js +++ b/test/amplitude-client.js @@ -24,13 +24,16 @@ describe('AmplitudeClient', function() { var userId = 'user'; var amplitude; var server; + var sandbox beforeEach(function() { amplitude = new AmplitudeClient(); server = sinon.fakeServer.create(); + sandbox = sinon.sandbox.create(); }); afterEach(function() { + sandbox.restore(); server.restore(); }); @@ -304,8 +307,8 @@ describe('AmplitudeClient', function() { cookie.set(oldCookieName, cookieData); amplitude.init(apiKey, null, { cookieForceUpgrade: true }); - const cookieData = cookie.getRaw(cookieName); - assert.equal('old_device_id', cookieData.slice(0, 'old_device_id'.length)); + const cookieRawData = cookie.getRaw(cookieName); + assert.equal('old_device_id', cookieRawData.slice(0, 'old_device_id'.length)); }); it('should delete the old old cookie if forceUpgrade is on', function(){ @@ -323,8 +326,8 @@ describe('AmplitudeClient', function() { cookie.set(oldCookieName, cookieData); amplitude.init(apiKey, null, { cookieForceUpgrade: true }); - const cookieData = cookie.get(oldCookieName); - assert.isNull(cookieData); + const cookieRawData = cookie.get(oldCookieName); + assert.isNull(cookieRawData); }); it('should use device id from the old cookie if a new cookie does not exist', function(){ @@ -447,6 +450,39 @@ describe('AmplitudeClient', function() { assert.equal(amplitude2._sequenceNumber, 70); }); + it('should not persist anything if storage options is none', function() { + const clock = sandbox.useFakeTimers(); + clock.tick(1000); + + const amplitude2 = new AmplitudeClient(); + amplitude2.init(apiKey, null, {storage: 'none'}); + clock.tick(10); + + const amplitude3 = new AmplitudeClient(); + amplitude3.init(apiKey, null, {storage: 'none'}); + + assert.notEqual(amplitude2._sessionId, amplitude3._sessionId); + }); + + it('should load sessionId if storage options is sessionStorage', function() { + const clock = sandbox.useFakeTimers(); + clock.tick(1000); + // Disable cookies read. + sandbox.stub(baseCookie, 'get').returns(null); + + const amplitude2 = new AmplitudeClient(); + amplitude2.init(apiKey, null, {storage: 'sessionStorage'}); + clock.tick(10); + + // Clear local storage to make sure it's not used. + localStorage.clear(); + + const amplitude3 = new AmplitudeClient(); + amplitude3.init(apiKey, null, {storage: 'sessionStorage'}); + + assert.equal(amplitude2._sessionId, amplitude3._sessionId); + }); + it('should load saved events from localStorage for default instance', function() { var existingEvent = '[{"device_id":"test_device_id","user_id":"test_user_id","timestamp":1453769146589,' + '"event_id":49,"session_id":1453763315544,"event_type":"clicked","version_name":"Web","platform":"Web"' +