Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Commit

Permalink
Reset activity on page view (close snowplow#750)
Browse files Browse the repository at this point in the history
  • Loading branch information
Paul Boocock authored and paulboocock committed Feb 7, 2020
1 parent d988458 commit d7319f7
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 59 deletions.
116 changes: 60 additions & 56 deletions src/js/tracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
* 23. cookieLifetime, 63072000
* 24. stateStorageStrategy, 'cookieAndLocalStorage'
* 25. maxLocalStorageQueueSize, 1000
* 26. resetActivityTrackingOnPageView, true
*/
object.Tracker = function Tracker(functionName, namespace, version, mutSnowplowState, argmap) {

Expand Down Expand Up @@ -181,14 +182,8 @@
// Maximum delay to wait for web bug image to be fetched (in milliseconds)
configTrackerPause = argmap.hasOwnProperty('pageUnloadTimer') ? argmap.pageUnloadTimer : 500,

// Whether appropriate values have been supplied to enableActivityTracking
activityTrackingEnabled = false,

// Minimum visit time after initial page view (in milliseconds)
configMinimumVisitTime,

// Recurring heart beat after initial ping (in milliseconds)
configHeartBeatTimer,
// Controls whether activity tracking page ping event timers are reset on page view events
resetActivityTrackingOnPageView = argmap.hasOwnProperty('resetActivityTrackingOnPageView') ? argmap.resetActivityTrackingOnPageView : true,

// Disallow hash tags in URL. TODO: Should this be set to true by default?
configDiscardHashTag,
Expand Down Expand Up @@ -266,9 +261,6 @@
// Unique ID for the tracker instance used to mark links which are being tracked
trackerId = functionName + '_' + namespace,

// Guard against installing the activity tracker more than once per Tracker instance
activityTrackingInstalled = false,

// Last activity timestamp
lastActivityTime,

Expand Down Expand Up @@ -345,8 +337,11 @@
pageViewSent = false,

// Activity tracking config for callback and pacge ping variants
activityTrackingConfig = {},
activityTrackingCallbackConfig = {};
activityTrackingConfig = {
enabled: false,
installed: false, // Guard against installing the activity tracker more than once per Tracker instance
configurations: {}
};

// Object to house gdpr Basis context values
let gdprBasisData = {};
Expand Down Expand Up @@ -1511,9 +1506,11 @@

// Send ping (to log that user has stayed on page)
var now = new Date();
var installingActivityTracking = false;

if ((activityTrackingConfig.activityTrackingEnabled || activityTrackingCallbackConfig.activityTrackingEnabled) && !activityTrackingInstalled) {
activityTrackingInstalled = true;
if (activityTrackingConfig.enabled && !activityTrackingConfig.installed) {
activityTrackingConfig.installed = true;
installingActivityTracking = true;

// Add mousewheel event handler, detect passive event listeners for performance
var detectPassiveEvents = {
Expand Down Expand Up @@ -1566,46 +1563,69 @@
helpers.addEventListener(windowAlias, 'resize', activityHandler);
helpers.addEventListener(windowAlias, 'focus', activityHandler);
helpers.addEventListener(windowAlias, 'blur', activityHandler);
}

if (activityTrackingConfig.enabled && (resetActivityTrackingOnPageView || installingActivityTracking)) {
// Periodic check for activity.
lastActivityTime = now.getTime();
}

if (activityTrackingConfig.activityTrackingEnabled && pagePingInterval === null && activityTrackingInstalled) {
pagePingInterval = activityInterval({
...activityTrackingConfig,
callback: args => logPagePing({ context: finalizeContexts(context, contextCallback), ...args }) // Grab the min/max globals
});
}
for (var key in activityTrackingConfig.configurations) {
if (activityTrackingConfig.configurations.hasOwnProperty(key)) {
const config = activityTrackingConfig.configurations[key];

if (activityTrackingCallbackConfig.activityTrackingEnabled && pagePingCallbackInterval === null && activityTrackingInstalled) {
pagePingCallbackInterval = activityInterval({
...activityTrackingCallbackConfig,
configMinimumVisitTime: lastActivityTime + activityTrackingCallbackConfig.configMinimumVisitLength * 1000,
callback: args => activityTrackingCallbackConfig.callback({ context: finalizeContexts(context, contextCallback), ...args })
});
//Clear page ping heartbeat on new page view
clearInterval(config.activityInterval);

config.activityInterval = activityInterval({
...config,
configLastActivityTime: lastActivityTime,
context: finalizeContexts(context, contextCallback)
});
}
}
}
}

function activityInterval({ activityTrackingEnabled, configHeartBeatTimer, configMinimumVisitTime, callback }) {
if (!activityTrackingEnabled) return;

function activityInterval({ configHeartBeatTimer, configMinimumVisitLength, configLastActivityTime, callback, context }) {
return setInterval(function heartBeat() {
var now = new Date();

// There was activity during the heart beat period;
// on average, this is going to overstate the visitDuration by configHeartBeatTimer/2
if ((lastActivityTime + configHeartBeatTimer) > now.getTime()) {
// Send ping if minimum visit time has elapsed
if (configMinimumVisitTime < now.getTime()) {
if (configLastActivityTime + configMinimumVisitLength * 1000 < now.getTime()) {
refreshUrl();
callback({ pageViewId: getPageViewId(), minXOffset, minYOffset, maxXOffset, maxYOffset });
callback({ context, pageViewId: getPageViewId(), minXOffset, minYOffset, maxXOffset, maxYOffset });
resetMaxScrolls();
}
}
}, configHeartBeatTimer);
}

/**
* Configure the activity tracking and
* ensures good values for min visit and heartbeat
*
* @param {int} [minimumVisitLength] The minimum length of a visit before the first page ping
* @param {int} [heartBeatDelay] The length between checks to see if we should send a page ping
* @param {function} [callback] A callback function to execute
*/
function configureActivityTracking(minimumVisitLength, heartBeatDelay, callback) {
if (minimumVisitLength === parseInt(minimumVisitLength, 10) &&
heartBeatDelay === parseInt(heartBeatDelay, 10)) {
return {
configMinimumVisitLength: minimumVisitLength,
configHeartBeatTimer: heartBeatDelay * 1000,
activityInterval: null,
callback
}
}

helpers.warn('Activity tracking not enabled, please provide integer values for minimumVisitLength and heartBeatDelay.')
return {};
};

/**
* Log that a user is still viewing a given page
* by sending a page ping.
Expand Down Expand Up @@ -1755,6 +1775,9 @@
* Public data and methods
************************************************************/

const userFingerprintingWarning = 'User Fingerprinting is no longer supported. This function will be removed in a future release.';
const argmapDeprecationWarning = ' is deprecated. Instead use the argmap argument on tracker initialisation: ';

/**
* Get the domain session index also known as current memorized visit count.
*
Expand Down Expand Up @@ -2026,17 +2049,8 @@
* @param int heartBeatDelay Seconds to wait between pings
*/
apiMethods.enableActivityTracking = function (minimumVisitLength, heartBeatDelay) {
if (minimumVisitLength === parseInt(minimumVisitLength, 10) &&
heartBeatDelay === parseInt(heartBeatDelay, 10)) {
activityTrackingConfig = {
activityTrackingEnabled: true,
configMinimumVisitTime: new Date().getTime() + minimumVisitLength * 1000,
configHeartBeatTimer: heartBeatDelay * 1000
}
} else {
helpers.warn("Activity tracking not enabled, please provide integer values " +
"for minimumVisitLength and heartBeatDelay.")
}
activityTrackingConfig.enabled = true;
activityTrackingConfig.configurations.pagePing = configureActivityTracking(minimumVisitLength, heartBeatDelay, logPagePing);
};

/**
Expand All @@ -2047,18 +2061,8 @@
* @param function callback function called with ping data
*/
apiMethods.enableActivityTrackingCallback = function (minimumVisitLength, heartBeatDelay, callback) {
if (minimumVisitLength === parseInt(minimumVisitLength, 10) &&
heartBeatDelay === parseInt(heartBeatDelay, 10)) {
activityTrackingCallbackConfig = {
activityTrackingEnabled: true,
configMinimumVisitTime: new Date().getTime() + minimumVisitLength * 1000,
configHeartBeatTimer: heartBeatDelay * 1000,
callback
}
} else {
helpers.warn("Activity tracking not enabled, please provide integer values " +
"for minimumVisitLength and heartBeatDelay.")
}
activityTrackingConfig.enabled = true;
activityTrackingConfig.configurations.callback = configureActivityTracking(minimumVisitLength, heartBeatDelay, callback);
};

/**
Expand Down
2 changes: 2 additions & 0 deletions tests/functional/activityCallback.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ describe('Activity tracking with callbacks', () => {
'expected init after 5s'
)

browser.execute(() => window.scrollTo(0,0))

const firstPageViewId = browser.execute(() => {
var pid
getCurrentPageViewId(function(id) {
Expand Down
113 changes: 110 additions & 3 deletions tests/unit/tracker.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ describe('Activity tracker behaviour', () => {
'',
{ outQueues },
{
resetActivityTrackingOnPageView: false,
stateStorageStrategy: 'cookies',
encodeBase64: false,
contexts: {
Expand Down Expand Up @@ -164,7 +165,7 @@ describe('Activity tracker behaviour', () => {
expect(countWithStaticValueEq(pageTwoTime)(outQueues)).toBe(0)
})

it('does not reset activity tracking on pageview', () => {
it('does not reset activity tracking on pageview when resetActivityTrackingOnPageView: false,', () => {
const outQueues = []
const Tracker = require('../../src/js/tracker').Tracker
const t = new Tracker(
Expand All @@ -173,6 +174,7 @@ describe('Activity tracker behaviour', () => {
'',
{ outQueues },
{
resetActivityTrackingOnPageView: false,
stateStorageStrategy: 'cookies',
encodeBase64: false,
contexts: {
Expand Down Expand Up @@ -201,11 +203,116 @@ describe('Activity tracker behaviour', () => {
const pps = getPPEvents(outQueues)

expect(F.size(pps)).toBe(1)
})

it('does reset activity tracking on pageview by default', () => {
const outQueues = []
const Tracker = require('../../src/js/tracker').Tracker
const t = new Tracker(
'',
'',
'',
{ outQueues },
{
stateStorageStrategy: 'cookies',
encodeBase64: false,
contexts: {
webPage: true,
},
}
)
t.enableActivityTracking(0, 30)
t.trackPageView()

const pp = F.head(pps)
advanceBy(15000)
jest.advanceTimersByTime(15000)

// activity on page one
t.updatePageActivity()
advanceBy(1000)

// shift to page two and trigger tick
t.trackPageView()
advanceBy(14000)
jest.advanceTimersByTime(15000)

// Activity began tracking on the first page but moved on before 30 seconds.
// Activity tracking should still not have fire despite being on site 30 seconds, as user has moved page.

const pps = getPPEvents(outQueues)

expect(F.size(pps)).toBe(0)
})

it('fires initial delayed activity tracking on first pageview and second pageview', () => {
const outQueues = []
const Tracker = require('../../src/js/tracker').Tracker
const t = new Tracker(
'',
'',
'',
{ outQueues },
{
stateStorageStrategy: 'cookies',
encodeBase64: false,
contexts: {
webPage: true,
},
}
)
t.enableActivityTracking(10, 5)

t.trackPageView()
const firstPageId = t.getPageViewId()
advanceBy(5000)
jest.advanceTimersByTime(5000)

// callback timer starts tracking
advanceBy(1000)
t.updatePageActivity()
advanceBy(4000)
jest.advanceTimersByTime(5000)

// page ping timer starts tracking
advanceBy(1000)
t.updatePageActivity()
advanceBy(4000)
jest.advanceTimersByTime(5000)

advanceBy(1000)
t.updatePageActivity()
advanceBy(4000)
jest.advanceTimersByTime(5000)

t.trackPageView()
const secondPageId = t.getPageViewId()
advanceBy(5000)
jest.advanceTimersByTime(5000)

// window for callbacks ticks
advanceBy(1000)
t.updatePageActivity()
advanceBy(4000)
jest.advanceTimersByTime(5000)

// window for page ping ticks
advanceBy(1000)
t.updatePageActivity()
advanceBy(4000)
jest.advanceTimersByTime(5000)

// Activity began tracking on the first page and tracked two page pings in 16 seconds.
// Activity tracking only fires one further event over next 11 seconds as a page view event occurs, resetting timer back to 10 seconds.

const pps = getPPEvents(outQueues)

expect(F.size(pps)).toBe(3)

const pph = F.head(pps)
const ppl = F.last(pps)

expect(firstPageId).toBe(extractPageId(pph))
expect(secondPageId).toBe(extractPageId(ppl))

expect(secondPageId).toBe(extractPageId(pp))
})
})

0 comments on commit d7319f7

Please sign in to comment.