Skip to content

Commit

Permalink
Add a session ID property for easier management
Browse files Browse the repository at this point in the history
  • Loading branch information
philipwalton committed May 23, 2017
1 parent ffbc973 commit 0f6c052
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 29 deletions.
2 changes: 1 addition & 1 deletion lib/externs/session.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/**
* Session store data schema.
* @typedef {{
* id: (string|undefined),
* hitTime: (number|undefined),
* sessionCount: (number|undefined),
* isExpired: (boolean|undefined),
* }}
*/
Expand Down
66 changes: 41 additions & 25 deletions lib/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import MethodChain from './method-chain';
import Store from './store';
import {now} from './utilities';
import {now, uuid} from './utilities';


const SECONDS = 1000;
Expand Down Expand Up @@ -89,19 +89,31 @@ export default class Session {
// Do nothing.
}

// Creates the session store and adds change listeners.
/** @type {SessionStoreData} */
const defaultProps = {
hitTime: 0,
isExpired: false,
};
this.store = Store.getOrCreate(
tracker.get('trackingId'), 'session', defaultProps);

// Ensure the session has an ID.
if (!this.store.get().id) {
this.store.set(/** @type {SessionStoreData} */ ({id: uuid()}));
}
}

/**
* Accepts a tracker object and returns whether or not the session for that
* tracker has expired. A session can expire for two reasons:
* Returns the ID of the current session.
* @return {string}
*/
getId() {
return this.store.get().id;
}

/**
* Accepts a session ID and returns true if the specified session has
* evidentially expired. A session can expire for two reasons:
* - More than 30 minutes has elapsed since the previous hit
* was sent (The 30 minutes number is the Google Analytics default, but
* it can be modified in GA admin "Session settings").
Expand All @@ -112,27 +124,32 @@ export default class Session {
* Note: since real session boundaries are determined at processing time,
* this is just a best guess rather than a source of truth.
*
* @param {SessionStoreData=} sessionData An optional sessionData object
* which avoids an additional localStorage read if the data is known to
* be fresh.
* @return {boolean} True if the session has expired.
* @param {string} id The ID of a session to check for expiry.
* @return {boolean} True if the session has not exp
*/
isExpired(sessionData = this.store.get()) {
// True if the sessionControl field was set to 'end' on the previous hit.
isExpired(id = this.getId()) {
// If a session ID is passed and it doesn't match the current ID,
// assume it's from an expired session. If no ID is passed, assume the ID
// of the current session.
if (id != this.getId()) return true;

/** @type {SessionStoreData} */
const sessionData = this.store.get();

// `isExpired` will be `true` if the sessionControl field was set to
// 'end' on the previous hit.
if (sessionData.isExpired) return true;

const currentDate = new Date();
const oldHitTime = sessionData.hitTime;
const oldHitDate = oldHitTime && new Date(oldHitTime);

// Only consider a session expired if previous hit time data exists, and
// the previous hit time is greater than that session timeout period or
// the hits occurred on different days in the session timezone.
if (oldHitTime) {
if (currentDate - oldHitDate > (this.timeout * MINUTES)) {
// If more time has elapsed than the session expiry time,
// the session has expired.
return true;
} else if (this.datesAreDifferentInTimezone(currentDate, oldHitDate)) {
// A new day has started since the previous hit, which means the
// session has expired.
const currentDate = new Date();
const oldHitDate = new Date(oldHitTime);
if (currentDate - oldHitDate > (this.timeout * MINUTES) ||
this.datesAreDifferentInTimezone(currentDate, oldHitDate)) {
return true;
}
}
Expand All @@ -143,7 +160,7 @@ export default class Session {

/**
* Returns true if (and only if) the timezone date formatting is supported
* in the current browser and if the two dates are diffinitiabely not the
* in the current browser and if the two dates are definitively not the
* same date in the session timezone. Anything short of this returns false.
* @param {!Date} d1
* @param {!Date} d2
Expand All @@ -170,17 +187,16 @@ export default class Session {
return (model) => {
originalMethod(model);

const sessionData = this.store.get();
const isSessionExpired = this.isExpired(sessionData);
const sessionControl = model.get('sessionControl');

const sessionWillStart = sessionControl == 'start' || isSessionExpired;
const sessionWillStart = sessionControl == 'start' || this.isExpired();
const sessionWillEnd = sessionControl == 'end';

// Update the stored session data.
/** @type {SessionStoreData} */
const sessionData = this.store.get();
sessionData.hitTime = now();
if (sessionWillStart) {
sessionData.isExpired = false;
sessionData.id = uuid();
}
if (sessionWillEnd) {
sessionData.isExpired = true;
Expand Down
73 changes: 70 additions & 3 deletions test/unit/session-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,24 @@ describe('Session', () => {
});

describe('constructor', () => {
it('stores a unique ID', () => {
const session = new Session(tracker);

assert(session.getId());

session.destroy();
});

it('reuses a stored ID if found', () => {
localStorage.setItem(
'autotrack:UA-12345-1:session', JSON.stringify({id: 'foo'}));

const session = new Session(tracker);
assert.strictEqual(session.getId(), 'foo');

session.destroy();
});

it('sets the passed args on the instance', () => {
const session = new Session(tracker, 123, 'America/Los_Angeles');

Expand Down Expand Up @@ -86,11 +104,22 @@ describe('Session', () => {
});
});

describe('getId', () => {
it('returns the stored ID', () => {
const session = new Session(tracker);

assert(session.getId());

session.destroy();
});
});

describe('isExpired', () => {
it('returns true if the last hit was too long ago', () => {
const session = new Session(tracker);

session.store.set({hitTime: now() - (60 * MINUTES)});

assert(session.isExpired());

session.store.set({hitTime: now() - (15 * MINUTES)});
Expand Down Expand Up @@ -143,9 +172,17 @@ describe('Session', () => {
const session = new Session(tracker, 30, 'America/Los_Angeles');
session.store.set({hitTime: now()});

assert.doesNotThrow(() => {
session.isExpired();
});
assert.doesNotThrow(() => session.isExpired());

session.destroy();
});

it('accepts an optional session ID', () => {
const session = new Session(tracker);
session.store.set({hitTime: now()});

assert(!session.isExpired());
assert(session.isExpired('old-id'));

session.destroy();
});
Expand All @@ -167,6 +204,36 @@ describe('Session', () => {

session.destroy();
});

it('updates the session ID if the session has expired', () => {
const session = new Session(tracker);
const id = session.getId();
session.store.set({hitTime: now() - (60 * MINUTES)});

assert.strictEqual(id, session.getId());

// Start a new session by sending a hit, which should generate a new ID.
tracker.send('pageview');

assert.notStrictEqual(id, session.getId());

session.destroy();
});

it('updates the session ID if sessionControl was set to start', () => {
const session = new Session(tracker);
const id = session.getId();
session.store.set({hitTime: now()});

assert.strictEqual(id, session.getId());

// Start a new session via the sessionControl field.
tracker.send('pageview', {sessionControl: 'start'});

assert.notStrictEqual(id, session.getId());

session.destroy();
});
});

describe('destroy', () => {
Expand Down

0 comments on commit 0f6c052

Please sign in to comment.