Skip to content

Commit 952f67a

Browse files
committed
add hasSessionTimedOut helper to unify timeout logic and updated tests
1 parent c9c08e0 commit 952f67a

File tree

2 files changed

+207
-21
lines changed

2 files changed

+207
-21
lines changed

src/sessionManager.ts

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,7 @@ export default function SessionManager(
3333

3434
this.initialize = function (): void {
3535
if (mpInstance._Store.sessionId) {
36-
const sessionTimeoutInMilliseconds: number =
37-
mpInstance._Store.SDKConfig.sessionTimeout * 60000;
38-
39-
if (
40-
new Date() >
41-
new Date(
42-
mpInstance._Store.dateLastEventSent.getTime() +
43-
sessionTimeoutInMilliseconds
44-
)
45-
) {
36+
if (hasSessionTimedOut(mpInstance._Store.dateLastEventSent.getTime())) {
4637
self.endSession();
4738
self.startNewSession();
4839
} else {
@@ -151,9 +142,6 @@ export default function SessionManager(
151142
return;
152143
}
153144

154-
let sessionTimeoutInMilliseconds: number;
155-
let timeSinceLastEventSent: number;
156-
157145
const cookies: IPersistenceMinified =
158146
mpInstance._Persistence.getPersistence();
159147

@@ -172,15 +160,10 @@ export default function SessionManager(
172160
}
173161

174162
if (cookies?.gs?.les) {
175-
sessionTimeoutInMilliseconds =
176-
mpInstance._Store.SDKConfig.sessionTimeout * 60000;
177-
const newDate: number = new Date().getTime();
178-
timeSinceLastEventSent = newDate - cookies.gs.les;
179-
180-
if (timeSinceLastEventSent < sessionTimeoutInMilliseconds) {
181-
self.setSessionTimer();
182-
} else {
163+
if (hasSessionTimedOut(cookies.gs.les)) {
183164
performSessionEnd();
165+
} else {
166+
self.setSessionTimer();
184167
}
185168
}
186169
mpInstance._timeOnSiteTimer?.resetTimer();
@@ -224,6 +207,27 @@ export default function SessionManager(
224207
}
225208
};
226209

210+
/**
211+
* Checks if the session has expired based on the last event timestamp
212+
* @param lastEventTimestamp - Unix timestamp in milliseconds of the last event
213+
* @returns true if the session has expired, false otherwise
214+
*/
215+
function hasSessionTimedOut(lastEventTimestamp: number): boolean {
216+
const sessionTimeoutInMilliseconds: number =
217+
mpInstance._Store.SDKConfig.sessionTimeout * 60000;
218+
const timeSinceLastEvent: number =
219+
new Date().getTime() - lastEventTimestamp;
220+
221+
return timeSinceLastEvent >= sessionTimeoutInMilliseconds;
222+
}
223+
224+
/**
225+
* Performs session end operations:
226+
* - Logs a SessionEnd event
227+
* - Clears session start date
228+
* - Nullifies the session ID and related data
229+
* - Resets the time-on-site timer
230+
*/
227231
function performSessionEnd(): void {
228232
mpInstance._Events.logEvent({
229233
messageType: Types.MessageType.SessionEnd,

test/src/tests-session-manager.ts

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,188 @@ describe('SessionManager', () => {
297297
});
298298
});
299299

300+
describe('#hasSessionTimedOut', () => {
301+
it('should return true when elapsed time exceeds session timeout', () => {
302+
const timePassed = 35 * (MILLIS_IN_ONE_SEC * 60); // 35 minutes
303+
304+
mParticle.init(apiKey, window.mParticle.config);
305+
const mpInstance = mParticle.getInstance();
306+
307+
mpInstance._Store.sessionId = 'OLD-ID';
308+
const timeLastEventSent = mpInstance._Store.dateLastEventSent.getTime();
309+
mpInstance._Store.dateLastEventSent = new Date(timeLastEventSent - timePassed);
310+
311+
// initialize() uses hasSessionTimedOut internally
312+
mpInstance._SessionManager.initialize();
313+
314+
// Should have created a new session because timeout was exceeded
315+
expect(mpInstance._Store.sessionId).to.not.equal('OLD-ID');
316+
});
317+
318+
it('should return false when elapsed time is within session timeout', () => {
319+
const timePassed = 15 * (MILLIS_IN_ONE_SEC * 60); // 15 minutes
320+
321+
mParticle.init(apiKey, window.mParticle.config);
322+
const mpInstance = mParticle.getInstance();
323+
324+
mpInstance._Store.sessionId = 'OLD-ID';
325+
const timeLastEventSent = mpInstance._Store.dateLastEventSent.getTime();
326+
mpInstance._Store.dateLastEventSent = new Date(timeLastEventSent - timePassed);
327+
328+
// initialize() uses hasSessionTimedOut internally
329+
mpInstance._SessionManager.initialize();
330+
331+
// Should have kept the old session because timeout was not exceeded
332+
expect(mpInstance._Store.sessionId).to.equal('OLD-ID');
333+
});
334+
335+
it('should work consistently with both in-memory and persisted timestamps', () => {
336+
const now = new Date();
337+
const thirtyOneMinutesAgo = new Date();
338+
thirtyOneMinutesAgo.setMinutes(now.getMinutes() - 31);
339+
340+
mParticle.init(apiKey, window.mParticle.config);
341+
const mpInstance = mParticle.getInstance();
342+
343+
// Test with in-memory store (via initialize)
344+
mpInstance._Store.sessionId = 'TEST-ID';
345+
mpInstance._Store.dateLastEventSent = thirtyOneMinutesAgo;
346+
mpInstance._SessionManager.initialize();
347+
348+
// Session should have expired (default timeout is 30 minutes)
349+
expect(mpInstance._Store.sessionId).to.not.equal('TEST-ID');
350+
351+
// Test with persistence (via endSession)
352+
const newSessionId = mpInstance._Store.sessionId;
353+
sinon.stub(mpInstance._Persistence, 'getPersistence').returns({
354+
gs: {
355+
les: thirtyOneMinutesAgo.getTime(),
356+
sid: newSessionId,
357+
},
358+
});
359+
360+
mpInstance._SessionManager.endSession();
361+
362+
// Session should have ended (same timeout logic)
363+
expect(mpInstance._Store.sessionId).to.equal(null);
364+
});
365+
366+
it('should return true when elapsed time equals session timeout exactly', () => {
367+
const now = new Date();
368+
const exactlyThirtyMinutesAgo = new Date();
369+
exactlyThirtyMinutesAgo.setMinutes(now.getMinutes() - 30);
370+
371+
mParticle.init(apiKey, window.mParticle.config);
372+
const mpInstance = mParticle.getInstance();
373+
374+
sinon.stub(mpInstance._Persistence, 'getPersistence').returns({
375+
gs: {
376+
les: exactlyThirtyMinutesAgo.getTime(),
377+
sid: 'TEST-ID',
378+
},
379+
});
380+
381+
mpInstance._SessionManager.endSession();
382+
383+
// At exactly 30 minutes, session should be expired
384+
expect(mpInstance._Store.sessionId).to.equal(null);
385+
});
386+
});
387+
388+
describe('#performSessionEnd', () => {
389+
it('should log a SessionEnd event', () => {
390+
mParticle.init(apiKey, window.mParticle.config);
391+
const mpInstance = mParticle.getInstance();
392+
const eventSpy = sinon.spy(mpInstance._Events, 'logEvent');
393+
394+
mpInstance._SessionManager.endSession(true);
395+
396+
// Find the SessionEnd event call
397+
const sessionEndCall = eventSpy.getCalls().find(call =>
398+
call.args[0]?.messageType === MessageType.SessionEnd
399+
);
400+
401+
expect(sessionEndCall).to.not.be.undefined;
402+
expect(sessionEndCall.args[0]).to.eql({
403+
messageType: MessageType.SessionEnd,
404+
});
405+
});
406+
407+
it('should clear sessionStartDate', () => {
408+
mParticle.init(apiKey, window.mParticle.config);
409+
const mpInstance = mParticle.getInstance();
410+
411+
const sessionStartDate = mpInstance._Store.sessionStartDate;
412+
expect(sessionStartDate).to.not.be.null;
413+
414+
mpInstance._SessionManager.endSession(true);
415+
416+
expect(mpInstance._Store.sessionStartDate).to.equal(null);
417+
});
418+
419+
it('should nullify session ID and session attributes', () => {
420+
mParticle.init(apiKey, window.mParticle.config);
421+
const mpInstance = mParticle.getInstance();
422+
423+
// Set up session data
424+
mpInstance._Store.sessionAttributes = { testAttr: 'value' };
425+
mpInstance._Store.localSessionAttributes = { localAttr: 'value' };
426+
427+
expect(mpInstance._Store.sessionId).to.not.be.null;
428+
429+
mpInstance._SessionManager.endSession(true);
430+
431+
expect(mpInstance._Store.sessionId).to.equal(null);
432+
expect(mpInstance._Store.sessionAttributes).to.eql({});
433+
expect(mpInstance._Store.localSessionAttributes).to.eql({});
434+
});
435+
436+
it('should reset timeOnSiteTimer if it exists', () => {
437+
mParticle.init(apiKey, window.mParticle.config);
438+
const mpInstance = mParticle.getInstance();
439+
440+
// Timer should exist since workspaceToken is present in config
441+
expect(mpInstance._timeOnSiteTimer).to.exist;
442+
443+
const resetTimerSpy = sinon.spy(mpInstance._timeOnSiteTimer, 'resetTimer');
444+
445+
mpInstance._SessionManager.endSession(true);
446+
447+
expect(resetTimerSpy.called).to.equal(true);
448+
});
449+
450+
it('should handle missing timeOnSiteTimer gracefully', () => {
451+
mParticle.init(apiKey, window.mParticle.config);
452+
const mpInstance = mParticle.getInstance();
453+
454+
// Explicitly remove the timer to test the optional chaining behavior
455+
mpInstance._timeOnSiteTimer = undefined;
456+
457+
expect(() => {
458+
mpInstance._SessionManager.endSession(true);
459+
}).to.not.throw();
460+
461+
// Session should still end properly
462+
expect(mpInstance._Store.sessionId).to.equal(null);
463+
});
464+
465+
it('should perform all session end operations', () => {
466+
mParticle.init(apiKey, window.mParticle.config);
467+
const mpInstance = mParticle.getInstance();
468+
469+
const eventSpy = sinon.spy(mpInstance._Events, 'logEvent');
470+
const persistenceSpy = sinon.spy(mpInstance._Persistence, 'update');
471+
472+
mpInstance._SessionManager.endSession(true);
473+
474+
// Verify all operations happened
475+
expect(eventSpy.called).to.equal(true);
476+
expect(mpInstance._Store.sessionStartDate).to.equal(null);
477+
expect(mpInstance._Store.sessionId).to.equal(null);
478+
expect(persistenceSpy.called).to.equal(true);
479+
});
480+
});
481+
300482
describe('#endSession', () => {
301483
it('should end a session', () => {
302484
mParticle.init(apiKey, window.mParticle.config);

0 commit comments

Comments
 (0)