Skip to content

Commit 029722d

Browse files
committed
Added ability to modify links allowing cross-domain tracking (closes snowplow#109)
1 parent fa03133 commit 029722d

5 files changed

Lines changed: 189 additions & 1 deletion

File tree

Gruntfile.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ module.exports = function(grunt) {
163163
runType: 'client',
164164
config: 'tests/intern.js',
165165
suites: [
166+
'tests/nonfunctional/helpers.js',
166167
'tests/nonfunctional/in_queue.js',
167168
'tests/nonfunctional/proxies.js'
168169
]

examples/web/async-large.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
encodeBase64: false, // Default is true
3030
appId: 'CFe23a', // Site ID can be anything you want. Set it if you're tracking more than one site in this account
3131
platform: 'mob',
32+
crossDomainLinker: function (link) {return link.id === 'legal'}, // Add duid and timestamp to the link with id "legal"
3233
contexts: {
3334
performanceTiming: true
3435
}
@@ -243,7 +244,8 @@ <h2>This shows how two users can simultaneously use Snowplow on the same page wi
243244
</p>
244245
<p>
245246
Middle click the links to trigger link click tracking:<br>
246-
<a href=http://en.wikipedia.org/wiki/Main_Page id=legal target="_blank" class="link out">Link</a>
247+
<!-- crossDomain functionality correctly alters a link with a multiple question marks and a fragment -->
248+
<a href=http://en.wikipedia.org/wiki/Main_Page?a=b?c#fragment id=legal target="_blank" class="link out">Link</a>
247249
<br>
248250
<a href=http://en.wikipedia.org/wiki/Main_Page id=illegal class="class barred">Link ignored by one user</a>
249251
</p>

src/js/lib/helpers.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,40 @@
207207
}
208208
}
209209

210+
/**
211+
* Add a name-value pair to the querystring of a URL
212+
*
213+
* @param string url URL to decorate
214+
* @param string name Name of the querystring pair
215+
* @param string value Value of the querystring pair
216+
*/
217+
object.decorateQuerystring = function (url, name, value) {
218+
var initialQsParams = name + '=' + value;
219+
var hashSplit = url.split('#');
220+
var qsSplit = hashSplit[0].split('?');
221+
var beforeQuerystring = qsSplit.shift();
222+
// Necessary because a querystring may contain multiple question marks
223+
var querystring = qsSplit.join('?');
224+
if (!querystring) {
225+
querystring = initialQsParams;
226+
} else {
227+
// Whether this is the first time the link has been decorated
228+
var initialDecoration = true;
229+
var qsFields = querystring.split('&');
230+
for (var i=0; i<qsFields.length; i++) {
231+
if (qsFields[i].substr(0, name.length + 1) === name + '=') {
232+
initialDecoration = false;
233+
qsFields[i] = initialQsParams;
234+
querystring = qsFields.join('&');
235+
break;
236+
}
237+
}
238+
if (initialDecoration) {
239+
querystring = initialQsParams + '&' + querystring;
240+
}
241+
}
242+
hashSplit[0] = beforeQuerystring + '?' + querystring;
243+
return hashSplit.join('#');
244+
}
245+
210246
}());

src/js/tracker.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
* 14. contexts, {}
7878
* 15. post, false
7979
* 16. bufferSize, 1
80+
* 17. crossDomainLinker, false
8081
*/
8182
object.Tracker = function Tracker(functionName, namespace, version, mutSnowplowState, argmap) {
8283

@@ -269,6 +270,41 @@
269270
}
270271
}
271272

273+
/**
274+
* Decorate the querystring of a single link
275+
*
276+
* @param event e The event targeting the link
277+
*/
278+
function linkDecorationHandler(e) {
279+
var duid = loadDomainUserIdCookie()[1];
280+
var tstamp = new Date().getTime();
281+
var initialQsParams = '_sp=' + duid + '.' + tstamp;
282+
var elt = e.target;
283+
if (elt.href) {
284+
elt.href = helpers.decorateQuerystring(elt.href, '_sp', duid + '.' + tstamp);
285+
}
286+
}
287+
288+
/**
289+
* Enable querystring decoration for links pasing a filter
290+
* Whenever such a link is clicked on or navigated to via the keyboard,
291+
* add "_sp={{duid}}.{{timestamp}}" to its querystring
292+
*
293+
* @param function crossDomainLinker Function used to determine which links to decorate
294+
*/
295+
function decorateLinks(crossDomainLinker) {
296+
for (var i=0; i<document.links.length; i++) {
297+
var elt = document.links[i];
298+
if (!elt.spDecorationEnabled && crossDomainLinker(elt)) {
299+
helpers.addEventListener(elt, 'click', linkDecorationHandler);
300+
helpers.addEventListener(elt, 'mousedown', linkDecorationHandler);
301+
302+
// Don't add event listeners more than once
303+
elt.spDecorationEnabled = true;
304+
}
305+
}
306+
}
307+
272308
/*
273309
* Initializes an empty ecommerce
274310
* transaction and line items
@@ -841,6 +877,9 @@
841877
*/
842878
updateDomainHash();
843879

880+
if (argmap.crossDomainLinker) {
881+
decorateLinks(argmap.crossDomainLinker);
882+
}
844883

845884
/************************************************************
846885
* Public data and methods
@@ -1014,6 +1053,15 @@
10141053
configDoNotTrack = enable && (dnt === 'yes' || dnt === '1');
10151054
},
10161055

1056+
/**
1057+
* Enable querystring decoration for links pasing a filter
1058+
*
1059+
* @param function crossDomainLinker Function used to determine which links to decorate
1060+
*/
1061+
crossDomainLinker: function (crossDomainLinkerCriterion) {
1062+
decorateLinks(crossDomainLinkerCriterion);
1063+
},
1064+
10171065
/**
10181066
* Add click listener to a specific link element.
10191067
* When clicked, Piwik will log the click automatically.

tests/nonfunctional/helpers.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* JavaScript tracker for Snowplow: tests/nonfunctional/helpers.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+
35+
define([
36+
"intern!object",
37+
"intern/chai!assert",
38+
"intern/dojo/node!../../src/js/lib/helpers"
39+
], function (registerSuite, assert, helpers) {
40+
41+
var decorateQuerystring = helpers.decorateQuerystring;
42+
43+
registerSuite({
44+
name: "decorateQuerystring test",
45+
"Decorate a URL with no querystring or fragment": function () {
46+
var url = 'http://www.example.com';
47+
var expected = 'http://www.example.com?_sp=a.b';
48+
var actual = decorateQuerystring(url, '_sp', 'a.b');
49+
assert.equal(actual, expected);
50+
},
51+
52+
"Decorate a URL with a fragment but no querystring": function () {
53+
var url = 'http://www.example.com#fragment';
54+
var expected = 'http://www.example.com?_sp=a.b#fragment';
55+
var actual = decorateQuerystring(url, '_sp', 'a.b');
56+
assert.equal(actual, expected);
57+
},
58+
59+
"Decorate a URL with an empty querystring": function () {
60+
var url = 'http://www.example.com?';
61+
var expected = 'http://www.example.com?_sp=a.b';
62+
var actual = decorateQuerystring(url, '_sp', 'a.b');
63+
assert.equal(actual, expected);
64+
},
65+
66+
"Decorate a URL with a nonempty querystring": function () {
67+
var url = 'http://www.example.com?name=value';
68+
var expected = 'http://www.example.com?_sp=a.b&name=value';
69+
var actual = decorateQuerystring(url, '_sp', 'a.b');
70+
assert.equal(actual, expected);
71+
},
72+
73+
"Override an existing field": function () {
74+
var url = 'http://www.example.com?_sp=outdated';
75+
var expected = 'http://www.example.com?_sp=a.b';
76+
var actual = decorateQuerystring(url, '_sp', 'a.b');
77+
assert.equal(actual, expected);
78+
},
79+
80+
"Decorate a URL whose querystring contains multiple question marks": function () {
81+
var url = 'http://www.example.com?test=working?&name=value';
82+
var expected = 'http://www.example.com?_sp=a.b&test=working?&name=value';
83+
var actual = decorateQuerystring(url, '_sp', 'a.b');
84+
assert.equal(actual, expected);
85+
},
86+
87+
"Override a field in a querystring containing a question mark": function () {
88+
var url = 'http://www.example.com?test=working?&_sp=outdated';
89+
var expected = 'http://www.example.com?test=working?&_sp=a.b';
90+
var actual = decorateQuerystring(url, '_sp', 'a.b');
91+
assert.equal(actual, expected);
92+
},
93+
94+
"Decorate a querystring with multiple ?s and #s": function () {
95+
var url = 'http://www.example.com?test=working?&_sp=outdated?&?name=value#fragment?#?#';
96+
var expected = 'http://www.example.com?test=working?&_sp=a.b&?name=value#fragment?#?#';
97+
var actual = decorateQuerystring(url, '_sp', 'a.b');
98+
assert.equal(actual, expected);
99+
},
100+
});
101+
});

0 commit comments

Comments
 (0)