Skip to content

Commit 452506e

Browse files
jamessnowplowPaul Boocock
authored andcommitted
Add activity tracking callback mechanism (close #765)
1 parent fa335c6 commit 452506e

12 files changed

+1083
-24
lines changed

examples/web/activity-tracking.html

Lines changed: 419 additions & 0 deletions
Large diffs are not rendered by default.

npm-shrinkwrap.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"grunt-contrib-concat": "^1.0.1",
3131
"grunt-contrib-uglify": "^4.0.1",
3232
"jest": "^24.9.0",
33+
"jest-date-mock": "^1.0.7",
3334
"jest-dev-server": "^4.3.0",
3435
"js-base64": "^2.4.9",
3536
"lodash": "^4.17.15",
@@ -66,5 +67,8 @@
6667
"test:unit": "jest tests/unit/*.spec.js",
6768
"test:e2e:sauce": "docker pull snowplow/snowplow-micro && wdio tests/wdio.sauce.conf.js",
6869
"test:e2e:local": "docker pull snowplow/snowplow-micro && wdio tests/wdio.local.conf.js"
70+
},
71+
"jest": {
72+
"setupFiles": ["jest-date-mock"]
6973
}
7074
}

src/js/tracker.js

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@
150150
configReferrerUrl = locationArray[2],
151151

152152
// Holder of the logPagePing interval
153-
pagePingInterval,
153+
pagePingInterval = null,
154+
pagePingCallbackInterval = null,
154155

155156
customReferrer,
156157

@@ -348,7 +349,11 @@
348349
preservePageViewId = false,
349350

350351
// Whether first trackPageView was fired and pageViewId should not be changed anymore until reload
351-
pageViewSent = false;
352+
pageViewSent = false,
353+
354+
// Activity tracking config for callback and pacge ping variants
355+
activityTrackingConfig = {},
356+
activityTrackingCallbackConfig = {};
352357

353358
// Object to house gdpr Basis context values
354359
let gdprBasisData = {};
@@ -1550,7 +1555,7 @@
15501555
// Send ping (to log that user has stayed on page)
15511556
var now = new Date();
15521557

1553-
if (activityTrackingEnabled && !activityTrackingInstalled) {
1558+
if ((activityTrackingConfig.activityTrackingEnabled || activityTrackingCallbackConfig.activityTrackingEnabled) && !activityTrackingInstalled) {
15541559
activityTrackingInstalled = true;
15551560

15561561
// Add mousewheel event handler, detect passive event listeners for performance
@@ -1607,22 +1612,39 @@
16071612

16081613
// Periodic check for activity.
16091614
lastActivityTime = now.getTime();
1610-
clearInterval(pagePingInterval);
1611-
pagePingInterval = setInterval(function heartBeat() {
1612-
var now = new Date();
1613-
1614-
// There was activity during the heart beat period;
1615-
// on average, this is going to overstate the visitDuration by configHeartBeatTimer/2
1616-
if ((lastActivityTime + configHeartBeatTimer) > now.getTime()) {
1617-
// Send ping if minimum visit time has elapsed
1618-
if (configMinimumVisitTime < now.getTime()) {
1619-
logPagePing(finalizeContexts(context, contextCallback)); // Grab the min/max globals
1620-
}
1621-
}
1622-
}, configHeartBeatTimer);
1615+
}
1616+
1617+
if (activityTrackingConfig.activityTrackingEnabled && pagePingInterval === null && activityTrackingInstalled) {
1618+
pagePingInterval = activityInterval({
1619+
...activityTrackingConfig,
1620+
callback: args => logPagePing({ context: finalizeContexts(context, contextCallback), ...args }) // Grab the min/max globals
1621+
});
1622+
}
1623+
1624+
if (activityTrackingCallbackConfig.activityTrackingEnabled && pagePingCallbackInterval === null && activityTrackingInstalled) {
1625+
pagePingCallbackInterval = activityInterval(activityTrackingCallbackConfig);
16231626
}
16241627
}
16251628

1629+
function activityInterval({ activityTrackingEnabled, configHeartBeatTimer, configMinimumVisitTime, callback }) {
1630+
if (!activityTrackingEnabled) return;
1631+
1632+
return setInterval(function heartBeat() {
1633+
var now = new Date();
1634+
1635+
// There was activity during the heart beat period;
1636+
// on average, this is going to overstate the visitDuration by configHeartBeatTimer/2
1637+
if ((lastActivityTime + configHeartBeatTimer) > now.getTime()) {
1638+
// Send ping if minimum visit time has elapsed
1639+
if (configMinimumVisitTime < now.getTime()) {
1640+
refreshUrl();
1641+
callback({ pageViewId: getPageViewId(), minXOffset, minYOffset, maxXOffset, maxYOffset });
1642+
resetMaxScrolls();
1643+
}
1644+
}
1645+
}, configHeartBeatTimer);
1646+
}
1647+
16261648
/**
16271649
* Log that a user is still viewing a given page
16281650
* by sending a page ping.
@@ -1631,8 +1653,7 @@
16311653
*
16321654
* @param context object Custom context relating to the event
16331655
*/
1634-
function logPagePing(context) {
1635-
refreshUrl();
1656+
function logPagePing({ context, minXOffset, minYOffset, maxXOffset, maxYOffset }) {
16361657
var newDocumentTitle = documentAlias.title;
16371658
if (newDocumentTitle !== lastDocumentTitle) {
16381659
lastDocumentTitle = newDocumentTitle;
@@ -1647,7 +1668,6 @@
16471668
cleanOffset(minYOffset),
16481669
cleanOffset(maxYOffset),
16491670
addCommonContexts(context));
1650-
resetMaxScrolls();
16511671
}
16521672

16531673
/**
@@ -2052,9 +2072,33 @@
20522072
apiMethods.enableActivityTracking = function (minimumVisitLength, heartBeatDelay) {
20532073
if (minimumVisitLength === parseInt(minimumVisitLength, 10) &&
20542074
heartBeatDelay === parseInt(heartBeatDelay, 10)) {
2055-
activityTrackingEnabled = true;
2056-
configMinimumVisitTime = new Date().getTime() + minimumVisitLength * 1000;
2057-
configHeartBeatTimer = heartBeatDelay * 1000;
2075+
activityTrackingConfig = {
2076+
activityTrackingEnabled: true,
2077+
configMinimumVisitTime: new Date().getTime() + minimumVisitLength * 1000,
2078+
configHeartBeatTimer: heartBeatDelay * 1000
2079+
}
2080+
} else {
2081+
helpers.warn("Activity tracking not enabled, please provide integer values " +
2082+
"for minimumVisitLength and heartBeatDelay.")
2083+
}
2084+
};
2085+
2086+
/**
2087+
* Enables page activity tracking (replaces collector ping with callback).
2088+
*
2089+
* @param int minimumVisitLength Seconds to wait before sending first page ping
2090+
* @param int heartBeatDelay Seconds to wait between pings
2091+
* @param function callback function called with ping data
2092+
*/
2093+
apiMethods.enableActivityTrackingCallback = function (minimumVisitLength, heartBeatDelay, callback) {
2094+
if (minimumVisitLength === parseInt(minimumVisitLength, 10) &&
2095+
heartBeatDelay === parseInt(heartBeatDelay, 10)) {
2096+
activityTrackingCallbackConfig = {
2097+
activityTrackingEnabled: true,
2098+
configMinimumVisitTime: new Date().getTime() + minimumVisitLength * 1000,
2099+
configHeartBeatTimer: heartBeatDelay * 1000,
2100+
callback
2101+
}
20582102
} else {
20592103
helpers.warn("Activity tracking not enabled, please provide integer values " +
20602104
"for minimumVisitLength and heartBeatDelay.")
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* JavaScript tracker for Snowplow: tests/functional/activityCallback.spec.js
3+
*
4+
* Significant portions copyright 2010 Anthon Pang. Remainder copyright
5+
* 2012-2014 Snowplow Analytics Ltd. All rights reserved.
6+
*
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions are
9+
* met:
10+
*
11+
* * Redistributions of source code must retain the above copyright
12+
* notice, this list of conditions and the following disclaimer.
13+
*
14+
* * Redistributions in binary form must reproduce the above copyright
15+
* notice, this list of conditions and the following disclaimer in the
16+
* documentation and/or other materials provided with the distribution.
17+
*
18+
* * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the
19+
* names of their contributors may be used to endorse or promote products
20+
* derived from this software without specific prior written permission.
21+
*
22+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
25+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
26+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
27+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
28+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
29+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
30+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
31+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33+
*/
34+
import F from 'lodash/fp'
35+
36+
describe('Activity tracking with callbacks', () => {
37+
if (
38+
F.isMatch(
39+
{ version: '12604.5.6.1.1', browserName: 'safari' },
40+
browser.capabilities
41+
)
42+
) {
43+
fit('skipping in safari 11 (saucelabs)', () => {})
44+
}
45+
if (
46+
F.isMatch(
47+
{ version: '12603.3.8', browserName: 'safari' },
48+
browser.capabilities
49+
)
50+
) {
51+
// the safari driver sauce uses for safari 10 doesnt support
52+
// setting cookies, so this whole suite fails
53+
// https://github.com/webdriverio/webdriverio/issues/2004
54+
fit('skipping in safari 10 (saucelabs)', () => {})
55+
}
56+
57+
it('reports events on scroll', () => {
58+
browser.url('/activity-callback.html?test1')
59+
browser.waitUntil(
60+
() => $('#init').getText() === 'true',
61+
5000,
62+
'expected init after 5s'
63+
)
64+
65+
$('#bottomRight').scrollIntoView()
66+
67+
browser.waitUntil(
68+
() => +$('#numEvents').getText() >= 1,
69+
10000,
70+
'expected >= 1 event after 10s'
71+
)
72+
const [maxX, maxY] = browser.execute(() => {
73+
return [findMaxX(), findMaxY()]
74+
})
75+
76+
expect(maxX).toBeGreaterThan(100)
77+
expect(maxY).toBeGreaterThan(100)
78+
})
79+
80+
it('carries pageviewid change through and resets scroll', () => {
81+
browser.url('/activity-callback.html?test2')
82+
browser.waitUntil(
83+
() => $('#init').getText() === 'true',
84+
5000,
85+
'expected init after 5s'
86+
)
87+
88+
const firstPageViewId = browser.execute(() => {
89+
var pid
90+
getCurrentPageViewId(function(id) {
91+
pid = id
92+
})
93+
return pid
94+
})
95+
96+
$('#bottomRight').scrollIntoView()
97+
$('#middle').scrollIntoView()
98+
browser.waitUntil(
99+
() => +$('#numEvents').getText() >= 1,
100+
10000,
101+
'expected >= 1 event after 10s'
102+
)
103+
104+
browser.execute(() => {
105+
trackPageView()
106+
})
107+
$('#bottomRight').scrollIntoView()
108+
109+
browser.waitUntil(
110+
() => +$('#numEvents').getText() > 1,
111+
10000,
112+
'expected > 1 event after 10s'
113+
)
114+
115+
const secondPageViewId = browser.execute(() => {
116+
var pid
117+
getCurrentPageViewId(function(id) {
118+
pid = id
119+
})
120+
return pid
121+
})
122+
123+
// sanity check
124+
expect(firstPageViewId).not.toEqual(secondPageViewId)
125+
126+
const first = browser.execute(id => {
127+
return findLastEventForPageViewId(id)
128+
}, firstPageViewId)
129+
const second = browser.execute(id => {
130+
return findLastEventForPageViewId(id)
131+
}, secondPageViewId)
132+
133+
const getMinXY = F.at(['minXOffset', 'minYOffset'])
134+
135+
// the first page view starts at 0,0
136+
expect(getMinXY(first)).toEqual([0, 0])
137+
138+
// but the second starts at #bottomRight and only moves as far as #middle
139+
// so there is no way it can get to 0,0
140+
const [secondX, secondY] = getMinXY(second)
141+
expect(secondX).toBeGreaterThan(0)
142+
expect(secondY).toBeGreaterThan(0)
143+
})
144+
})

0 commit comments

Comments
 (0)