Skip to content

Commit

Permalink
fix(cookies): AMP-35904 dedup cookies (#390)
Browse files Browse the repository at this point in the history
* fix(cookie): add constants for cookie prop indexes

* fix(cookie): dedup cookie on load

* fix(cookie): make getLastEventTime more resilient

* fix(cookie): fix event time sort

* fix(cookie): clarify cookie constant comment

* fix(cookie): clean up getAll method

* fix(cookie): add unit tests for cookie parsing and sorting

* fix(cookie): return empty array from getAll on exception

* fix(cookie): remove duplicate test

* fix(cookie): refactor getLastEventTime and add warning logs for malformed cookie

Co-authored-by: AJ Horst <AJ_Horst@intuit.com>
  • Loading branch information
ajhorst and AJ Horst authored May 12, 2021
1 parent 6c8c961 commit 10cf8c8
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 16 deletions.
48 changes: 48 additions & 0 deletions src/base-cookie.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ const get = (name) => {
}
};

const getAll = (name) => {
try {
const cookieArray = document.cookie.split(';').map((c) => c.trimStart());
let values = [];
for (let cookie of cookieArray) {
while (cookie.charAt(0) === ' ') {
cookie = cookie.substring(1);
}
if (cookie.indexOf(name) === 0) {
values.push(cookie.substring(name.length));
}
}

return values;
} catch (e) {
return [];
}
};

const set = (name, value, opts) => {
let expires = value !== null ? opts.expirationDays : -1;
if (expires) {
Expand All @@ -47,6 +66,32 @@ const set = (name, value, opts) => {
document.cookie = str;
};

const getLastEventTime = (cookie = '') => {
const strValue = cookie.split('.')[Constants.LAST_EVENT_TIME_INDEX];

let parsedValue;
if (strValue) {
parsedValue = parseInt(strValue, 32);
}

if (parsedValue) {
return parsedValue;
} else {
utils.log.warn(`unable to parse malformed cookie: ${cookie}`);
return 0;
}
};

const sortByEventTime = (cookies) => {
return [...cookies].sort((c1, c2) => {
const t1 = getLastEventTime(c1);
const t2 = getLastEventTime(c2);
// sort c1 first if its last event time is more recent
// i.e its event time integer is larger that c2's
return t2 - t1;
});
};

// test that cookies are enabled - navigator.cookiesEnabled yields false positives in IE, need to test directly
const areCookiesEnabled = (opts = {}) => {
const cookieName = Constants.COOKIE_TEST_PREFIX + base64Id();
Expand All @@ -68,5 +113,8 @@ const areCookiesEnabled = (opts = {}) => {
export default {
set,
get,
getAll,
getLastEventTime,
sortByEventTime,
areCookiesEnabled,
};
10 changes: 10 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ export default {
OPT_OUT: 'amplitude_optOut',
USER_ID: 'amplitude_userId',

// indexes of properties in cookie v2 storage format
DEVICE_ID_INDEX: 0,
USER_ID_INDEX: 1,
OPT_OUT_INDEX: 2,
SESSION_ID_INDEX: 3,
LAST_EVENT_TIME_INDEX: 4,
EVENT_ID_INDEX: 5,
IDENTIFY_ID_INDEX: 6,
SEQUENCE_NUMBER_INDEX: 7,

COOKIE_TEST_PREFIX: 'amp_cookie_test',
COOKIE_PREFIX: 'amp',

Expand Down
47 changes: 31 additions & 16 deletions src/metadata-storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,20 +96,35 @@ class MetadataStorage {
ampLocalStorage.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,
});
this.saveCookie(value);
break;
}
}

saveCookie(value) {
baseCookie.set(this.getCookieStorageKey(), value, {
domain: this.cookieDomain,
secure: this.secure,
sameSite: this.sameSite,
expirationDays: this.expirationDays,
});
}

load() {
let str;
if (this.storage === Constants.STORAGE_COOKIES) {
str = baseCookie.get(this.getCookieStorageKey() + '=');
const cookieKey = this.getCookieStorageKey() + '=';
const allCookies = baseCookie.getAll(cookieKey);
if (allCookies.length === 0 || allCookies.length === 1) {
str = allCookies[0];
} else {
// dedup cookies by deleting them all and restoring
// the one with the most recent event time
const latestCookie = baseCookie.sortByEventTime(allCookies)[0];
allCookies.forEach(() => baseCookie.set(this.getCookieStorageKey(), null, {}));
this.saveCookie(latestCookie);
str = baseCookie.get(cookieKey);
}
}
if (!str) {
str = ampLocalStorage.getItem(this.storageKey);
Expand All @@ -129,23 +144,23 @@ class MetadataStorage {
const values = str.split('.');

let userId = null;
if (values[1]) {
if (values[Constants.USER_ID_INDEX]) {
try {
userId = Base64.decode(values[1]);
userId = Base64.decode(values[Constants.USER_ID_INDEX]);
} catch (e) {
userId = null;
}
}

return {
deviceId: values[0],
deviceId: values[Constants.DEVICE_ID_INDEX],
userId,
optOut: values[2] === '1',
sessionId: parseInt(values[3], 32),
lastEventTime: parseInt(values[4], 32),
eventId: parseInt(values[5], 32),
identifyId: parseInt(values[6], 32),
sequenceNumber: parseInt(values[7], 32),
optOut: values[Constants.OPT_OUT_INDEX] === '1',
sessionId: parseInt(values[Constants.SESSION_ID_INDEX], 32),
lastEventTime: parseInt(values[Constants.LAST_EVENT_TIME_INDEX], 32),
eventId: parseInt(values[Constants.EVENT_ID_INDEX], 32),
identifyId: parseInt(values[Constants.IDENTIFY_ID_INDEX], 32),
sequenceNumber: parseInt(values[Constants.SEQUENCE_NUMBER_INDEX], 32),
};
}
}
Expand Down
45 changes: 45 additions & 0 deletions test/base-cookie.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,50 @@ describe('cookie', function () {
spyLogWarning.restore();
});
});

describe('getLastEventTime tests', () => {
it('should return 0 if cookie is undefined', () => {
const cookieStr = undefined;
const lastEventTime = cookie.getLastEventTime(cookieStr);
assert.equal(lastEventTime, 0);
});

it('should return 0 if cookie is an empty string', () => {
const cookieStr = '';
const lastEventTime = cookie.getLastEventTime(cookieStr);
assert.equal(lastEventTime, 0);
});

it('should return 0 if cookie is a malformed cookie', () => {
const cookieStr = 'asdfasdfasdfasdf';
const lastEventTime = cookie.getLastEventTime(cookieStr);
assert.equal(lastEventTime, 0);
});

it('should return a number thats base 32 encoded and put into the amplitude cookie format', () => {
const originalTime = 1620698180822;
const cookieStr = `....${originalTime.toString(32)}...`;
const lastEventTime = cookie.getLastEventTime(cookieStr);
assert.equal(lastEventTime, originalTime);
});
});

describe('sortByEventTime tests', () => {
it('should sort cookies by last event time from greatest to least', () => {
const firstTime = 10;
const secondTime = 20;
const thirdTime = 30;
const invalidTime = '';

const cookieArray = [secondTime, invalidTime, thirdTime, firstTime].map((t) => `....${t.toString(32)}...`);
const sortedCookieArray = cookie.sortByEventTime(cookieArray);

assert.notEqual(cookieArray, sortedCookieArray); // returns a shallow copy, not the same array
assert.equal(sortedCookieArray[0], cookieArray[2]); // third time
assert.equal(sortedCookieArray[1], cookieArray[0]); // second time
assert.equal(sortedCookieArray[2], cookieArray[3]); // first time
assert.equal(sortedCookieArray[3], cookieArray[1]); // invalid time
});
});
});
});

0 comments on commit 10cf8c8

Please sign in to comment.