PageSourceSearch

http://modernnomads.info/js/wng.googleanalytics.js

js modernnomads.info collected 2026-10-02 07:10:12 UTC 30,919 bytes, 761 lines download raw bytes

1/*globals window:false, document:false, FB:false, wng:true */
2
3// Options available: 
4//  {
5//   'url': null,
6//   'preliminaryUrl': null,
7//   'brand': null,
8//   'country_of_residence': null, // Optional on most sites
9//   'signed_in': null, // Optional 
10//   'quote': {
11//     'countries': null,
12//     'underwriter': null // Optional on most sites
13//     'price': null, // Full dollar value as int
14//     'plan': null,
15//     'campaign_code': null,
16//     'campaign_code_discount': null, // Int of %
17//     'duration': null,
18//     'ages': null,
19//     'type': null,
20//     'region': null
21//    }
22//  }
23
24var dataLayer = dataLayer || [];
25
26var wng = (function ($, undefined) {
27    var trackPageOnNextLoadCookieName = 'trackPageOnNextLoad';
28    var trackEventOnNextLoadCookiePrefix = 'trackEventOnNextLoad';
29
30    var dimensions = {
31        'quotePlan': 'dimension1',
32        'quoteDestinations': 'dimension2',
33        'quoteGroupedDuration': 'dimension3',
34        'quoteAges': 'dimension4',
35        'quoteGroupedAges': 'dimension5',
36        'quoteCampaignCode': 'dimension6',
37        'countryOfResidence': 'dimension7',
38        'quoteUnderwriter': 'dimension8',
39        'brand': 'dimension9',
40        'signedIn': 'dimension10'
41    };
42
43    var metrics = {
44        'quoteDurationDays': 'metric1',
45        'quoteCampaignCodeDiscount': 'metric2'
46    };
47
48    $.googleAnalytics = function (googleAnalyticsTrackingCode, googleTagManagerCode, options) {
49        var opt = options || {};
50        $.googleAnalytics.options = opt;
51
52        // Load UA javascript asynchronously
53        (function (i, s, o, g, r, a, m) {
54            i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {
55                (i[r].q = i[r].q || []).push(arguments)
56            }, i[r].l = 1 * new Date(); a = s.createElement(o),
57                m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
58        })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
59
60        ga('create', googleAnalyticsTrackingCode, {
61            'cookieDomain': utils.cookieDomainForHostname(document.location.hostname),
62            'siteSpeedSampleRate': 20,
63            'allowLinker': true
64        });
65
66        // Load the display features plugin to support display advertising features in GA
67        ga('require', 'displayfeatures');
68
69        // Set custom page url
70        if (opt.url) {
71            ga('set', 'page', opt.url);
72        }
73
74        // Set custom dimension for brand
75        if (opt.brand) {
76            ga('set', dimensions.brand, opt.brand);
77        }
78
79        // Contry of residence
80        if (opt.country_of_residence) {
81            ga('set', dimensions.countryOfResidence, opt.country_of_residence);
82        }
83
84        // Signed in status
85        ga('set', dimensions.signedIn, opt.signed_in || 'Unknown');
86
87        // Set custom variables for quote
88        if (opt.quote) {
89            var quote = opt.quote;
90
91            // Plan
92            if (quote.plan) {
93                ga('set', dimensions.quotePlan, quote.plan);
94            }
95
96            // Countries
97            if (quote.countries) {
98                var orderedCountries = utils.orderCountries(quote.countries);
99                var limitedCountries = utils.limitCountries(orderedCountries);
100                ga('set', dimensions.quoteDestinations, limitedCountries);
101            }
102
103            // Region - Legacy support
104            if (quote.region) {
105                ga('set', dimensions.quoteDestinations, quote.region);
106            }
107
108            // Duration
109            if (quote.duration) {
110                // Number vs string tested here for legacy support of older purchase paths
111                if (typeof quote.duration === 'number') {
112                    var groupedDuration = utils.groupDuration(quote.duration);
113                    ga('set', metrics.quoteDurationDays, Number(quote.duration));
114                    ga('set', dimensions.quoteGroupedDuration, groupedDuration);
115                } else if (typeof quote.duration === 'string') {
116                    ga('set', dimensions.quoteGroupedDuration, quote.duration);
117                }
118            }
119
120            // Ages 
121            if (quote.ages) {
122                var groupedAges = utils.groupAges(quote.ages);
123                var sortedAges = utils.sortNumericArray(quote.ages);
124                ga('set', dimensions.quoteAges, sortedAges.join(', '));
125                ga('set', dimensions.quoteGroupedAges, groupedAges);
126            }
127
128            // Type - Legacy support
129            if (quote.type) {
130                ga('set', dimensions.quoteAges, quote.type);
131            }
132
133            // Campaign code
134            if (quote.campaign_code) {
135                ga('set', dimensions.quoteCampaignCode, quote.campaign_code);
136            } else {
137                ga('set', dimensions.quoteCampaignCode, 'None');
138            }
139
140            // Campaign code discount
141            if (quote.campaign_code_discount) {
142                // Conversion in case it's using the old '10%' discont instead of an int
143                var numericString = String(quote.campaign_code_discount).replace('%', '');
144                ga('set', metrics.quoteCampaignCodeDiscount, Number(numericString));
145            } else {
146                ga('set', metrics.quoteCampaignCodeDiscount, 0);
147            }
148
149            // Underwriter
150            if (quote.underwriter) {
151                ga('set', dimensions.quoteUnderwriter, quote.underwriter);
152            }
153        }
154
155        // Callback to add site specific custom variables.
156        if (wng.googleAnalytics.addSiteSpecificCustomDimensionsOrMetrics) {
157            wng.googleAnalytics.addSiteSpecificCustomDimensionsOrMetrics(opt, utils);
158        }
159
160        // Track any urls passed in from the previous page
161        var urlFromLastPage = utils.getCookie(trackPageOnNextLoadCookieName);
162
163        if (urlFromLastPage) {
164            ga('send', 'pageview', { 'page': urlFromLastPage });
165            utils.deleteCookie(trackPageOnNextLoadCookieName);
166        }
167
168        // Track the preliminaryUrl if it exists
169        if (opt.preliminaryUrl) {
170            ga('send', 'pageview', { 'page': opt.preliminaryUrl });
171        }
172
173        // Track the main page load
174        ga('send', 'pageview');
175
176        // Track any events passed in from the previous page
177        var eventsFromLastPage = utils.findCookies(new RegExp("^" + trackEventOnNextLoadCookiePrefix));
178
179        for (var key in eventsFromLastPage) {
180            var decodedEvent = utils.decodeEvent(eventsFromLastPage[key]);
181            wng.googleAnalytics.trackEvent.apply(this, decodedEvent);
182            utils.deleteCookie(key);
183        }
184
185        // Set the fbAsyncInit value to be a callback that tracks FB social actions
186        window.fbAsyncInit = function () {
187            wng.googleAnalytics.listenForFacebookSocialEvents();
188        };
189
190        // On window load, listen for any twttr events. This is to give time for twitter buttons to set up the twttr object
191        utils.fireOnWindowLoad(function () {
192            wng.googleAnalytics.listenForTwitterSocialEvents();
193        });
194
195        // Set up for Google Tag Manager
196        dataLayer.push({ 'Custom Tracking URL': opt.url });
197
198        if (opt.preliminaryUrl) {
199            dataLayer.push({ 'Preliminary URL': opt.preliminaryUrl });
200        }
201
202        // Send country of residence to Google Tag Manager
203        if (opt.country_of_residence) {
204            dataLayer.push({ 'Country of Residence': opt.country_of_residence });
205        }
206
207        if (opt.quote) {
208            // Send quote destinations to Google Tag Manager
209            if (opt.quote.countries) {
210                dataLayer.push({ 'Destination Countries': utils.limitCountries(utils.orderCountries(opt.quote.countries)) });
211            }
212
213            // Send price to Google Tag Manager
214            if (opt.quote.price) {
215                dataLayer.push({ 'Quote Price': Number(opt.quote.price) });
216            }
217
218            // Send underwriter to Google Tag Manager
219            if (opt.quote.underwriter) {
220                dataLayer.push({ 'Underwriter': opt.quote.underwriter });
221            }
222        }
223
224        // Load Google Tag Manager
225        (function (w, d, s, l, i) {
226            w[l] = w[l] || [];
227            w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
228            var f = d.getElementsByTagName(s)[0], j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true;
229            j.src = '//www.googletagmanager.com/gtm.js?id=' + i + dl;
230            f.parentNode.insertBefore(j, f);
231        })(window, document, 'script', 'dataLayer', googleTagManagerCode);
232    };
233
234    // Track a virtual page, this is in addition to the inital page tracking
235    $.googleAnalytics.trackVirtualPage = function (url) {
236        ga('send', 'pageview', { 'page': url });
237    };
238
239    // Track a virtual page, but not until just before the next page is tracked.
240    // Useful for when the page will unload before a request can be sent to Google Analytics.
241    // Note that there can only be one page tracked this way.
242    $.googleAnalytics.trackVirtualPageOnNextLoad = function (url) {
243        utils.setCookie(trackPageOnNextLoadCookieName, url);
244    };
245
246    // Track an event
247    // Note: the value option must be an integer
248    $.googleAnalytics.trackEvent = function (category, action, eventOptions) {
249        var options = {};
250
251        if (eventOptions) {
252            if (eventOptions.label) {
253                options.eventLabel = eventOptions.label;
254            }
255
256            if (eventOptions.value) {
257                options.eventValue = eventOptions.value;
258            }
259        }
260
261        ga('send', 'event', category, action, options);
262    };
263
264    // Track an event, but not until just before the next page is tracked.
265    // Useful for when the page will unload before a request can be sent to Google Analytics.
266    (function () {
267        var trackEventOnNextLoadCounter = 1;
268
269        $.googleAnalytics.trackEventOnNextLoad = function (category, action, eventOptions) {
270            var encodedEvent = utils.encodeEvent(category, action, eventOptions);
271            var cookieName = trackEventOnNextLoadCookiePrefix + trackEventOnNextLoadCounter;
272            utils.setCookie(cookieName, encodedEvent);
273            trackEventOnNextLoadCounter++;
274        };
275    })();
276
277    // Create a transaction. Note order ID should be unique and allow the looking up of the order in the DB. Total should just be an int.
278    (function () {
279        var ecommercePluginLoaded = false;
280
281        var createProductAdders = function (transactionID, amendment) {
282
283            var basePolicyCategory = amendment ? 'Amendment' : 'Base policy';
284            var optionsCategory = amendment ? 'Amendment' : 'Options';
285
286            return {
287                'addBasePolicy': function (price) {
288                    var nameAndSku = amendment ? 'Base policy amendment' : 'Base policy';
289
290                    ga('ecommerce:addItem', {
291                        'id': String(transactionID),
292                        'name': nameAndSku,
293                        'sku': nameAndSku,
294                        'category': basePolicyCategory,
295                        'price': String(price),
296                        'quantity': 1
297                    });
298                },
299
300                // Add policy excess reduction, both newPolicyExcess and price should be an int
301                addPolicyExcessReduction: function (newPolicyExcess, price) {
302                    ga('ecommerce:addItem', {
303                        'id': String(transactionID),
304                        'name': 'Policy excess reduction',
305                        'sku': '$' + newPolicyExcess + ' policy excess',
306                        'category': optionsCategory,
307                        'price': String(price),
308                        'quantity': 1
309                    });
310                },
311
312                // Add car rental excess cover increase, both newExcessCover and price should be an int  
313                addCarRentalExcessIncrease: function (newExcessCover, price) {
314                    ga('ecommerce:addItem', {
315                        'id': String(transactionID),
316                        'name': 'Car excess cover increased',
317                        'sku': '$' + newExcessCover + ' car excess',
318                        'category': optionsCategory,
319                        'price': String(price),
320                        'quantity': 1
321                    });
322                },
323
324                // Add Business benefits cover, price should be an int
325                addBusinessBenefitCover: function (price) {
326                    ga('ecommerce:addItem', {
327                        'id': String(transactionID),
328                        'name': 'Business benefit cover',
329                        'sku': 'Business premium',
330                        'category': optionsCategory,
331                        'price': String(price),
332                        'quantity': 1
333                    });
334                },
335
336                // Add Snow sports cover, price should be an int
337                addSnowSportsCover: function (price) {
338                    ga('ecommerce:addItem', {
339                        'id': String(transactionID),
340                        'name': 'Snow sports cover',
341                        'sku': 'Snow sports premium',
342                        'category': optionsCategory,
343                        'price': String(price),
344                        'quantity': 1
345                    });
346                },
347
348                // Add aditional item, both value and price should be an int, description is a string of what the user entered as 'item name'
349                addAdditionalItem: function (value, description, price) {
350                    ga('ecommerce:addItem', {
351                        'id': String(transactionID),
352                        'name': 'Additional item',
353                        'sku': '$' + value + ' ' + description,
354                        'category': optionsCategory,
355                        'price': String(price),
356                        'quantity': 1
357                    });
358                },
359
360                // Add Footprints, price should be an int, projectName is a string
361                addFootprints: function (projectName, price) {
362                    ga('ecommerce:addItem', {
363                        'id': String(transactionID),
364                        'name': 'Footprints donation',
365                        'sku': projectName,
366                        'category': optionsCategory,
367                        'price': String(price),
368                        'quantity': 1
369                    });
370                },
371
372                // Add Pre-Ex, price should be an int
373                addPreEx: function (price) {
374                    ga('ecommerce:addItem', {
375                        'id': String(transactionID),
376                        'name': 'Pre-Ex cover',
377                        'sku': 'Medial premium',
378                        'category': optionsCategory,
379                        'price': String(price),
380                        'quantity': 1
381                    });
382                },
383
384                // TODO: Insurance app passes in a string as the level: e.g. 'Level 3' - Make this require a string and pass that straight in.
385                // Add a specific level of adventure cover, price & level should be an int
386                addAdventureCover: function (level, price) {
387                    ga('ecommerce:addItem', {
388                        'id': String(transactionID),
389                        'name': 'Adventure cover',
390                        'sku': 'Level ' + level,
391                        'category': optionsCategory,
392                        'price': String(price),
393                        'quantity': 1
394                    });
395                }
396            }
397        }
398
399        // For a list of supported currencies: https://developers.google.com/analytics/devguides/platform/currencies
400        $.googleAnalytics.createTransaction = function (transactionID, total, addProductsCallback, options) {
401            options = options || {};
402
403            if (!ecommercePluginLoaded) {
404                ga('require', 'ecommerce', 'ecommerce.js');
405                ecommercePluginLoaded = true;
406            }
407
408            var idSuffix = options['amendment'] ? ' (Amendment)' : '';
409
410            var transaction = {
411                'id': String(transactionID) + idSuffix,
412                'revenue': String(total)
413            };
414
415            if (options['currency']) {
416                transaction['currency'] = options['currency'];
417            }
418
419            // Create transaction
420            ga('ecommerce:addTransaction', transaction);
421
422            // Add products
423            addProductsCallback(createProductAdders(transactionID, options['amendment']));
424
425            // This has to be called after the transaction has been built to submit it.
426            ga('ecommerce:send');
427        };
428    })();
429
430    // This adds listeners to track Facebook likes, unlikes, and shares. This has to be called after the FB object has been created.
431    $.googleAnalytics.listenForFacebookSocialEvents = function () {
432        try {
433            if (FB && FB.Event && FB.Event.subscribe) {
434                // Likes
435                FB.Event.subscribe('edge.create', function (opt_target) {
436                    ga('send', 'social', 'Facebook', 'Like', opt_target);
437                });
438
439                // Unlikes
440                FB.Event.subscribe('edge.remove', function (opt_target) {
441                    ga('send', 'social', 'Facebook', 'Unlike', opt_target);
442                });
443
444                // Shares
445                FB.Event.subscribe('message.send', function (opt_target) {
446                    ga('send', 'social', 'Facebook', 'Send', opt_target);
447                });
448            }
449        } catch (e) { }
450    };
451
452    // This adds a listening to track Twitter tweets. This has to be called after the twttr object is created
453    $.googleAnalytics.listenForTwitterSocialEvents = function () {
454        try {
455            if (twttr && twttr.ready) {
456                twttr.ready(function () {
457                    // Tweets
458                    twttr.events.bind('tweet', function (intentEvent) {
459                        var tweetPath = null;
460                        // Check if the target is an iframe, if so, get the URL of the tweet path.
461                        if (intentEvent.target && intentEvent.target.nodeName == 'IFRAME') {
462                            tweetPath = utils.paramsFromUrl(intentEvent.target.src, true)['url'];
463                        }
464                        ga('send', 'social', 'Twitter', 'Tweet', tweetPath);
465                    });
466                });
467            }
468        } catch (e) { }
469    };
470
471    // Load a script tag when window load fires
472    $.googleAnalytics.loadScriptOnLoad = function (url) {
473        utils.fireOnWindowLoad(function () {
474            utils.loadScript(url);
475        });
476    };
477
478    // Call this whenever an internal, cross domain link or form is clicked, let submit / click continue as normal.
479    $.googleAnalytics.decorateCrossDomainLinkOrForm = function (element) {
480        ga(function (tracker) {
481            var linker = new window.gaplugins.Linker(tracker);
482            linker.decorate(element);
483        })
484    }
485
486    // Get linker param to append to a link or form. Shouldn't be needed much, should instead use auto linking or decorateCrossDomainLinkOrForm
487    // If GA plugin isn't loaded this will return a blank string (the closure won't execute before the return in that case)
488    $.googleAnalytics.crossDomainLinkParameter = function () {
489        var linkerParam = '';
490
491        ga(function (tracker) {
492            linkerParam = tracker.get('linkerParam');
493        });
494
495        return linkerParam;
496    }
497
498    var utils = $.googleAnalytics.utils = {
499        // Convert an int value for days into a standardised string format
500        groupDuration: function (days) {
501            var approximateMonth = 365 / 12;
502
503            if (days < 28) {
504                return days + (days == 1 ? ' day' : ' days');
505            } else if (days < Math.round(3 * approximateMonth)) {
506                return Math.floor(days / 7) + ' weeks';
507            } else {
508                return Math.floor((days + 1) / approximateMonth) + ' months'; // +1 on days to fudge the numbers slightly, so 91 days = 3 months (instead of 91.25+), and 12 months is guaranteed for 365 / 12 * 12 in case of a rounding error
509            }
510        },
511
512        // Convert days into weeks
513        weekDuration: function (days) {
514            var weeks = Math.floor(days / 7);
515
516            return weeks + (weeks == 1 ? ' week' : ' weeks');
517        },
518
519        // Group an array of ages into meaningful strings
520        groupAges: function (ages) {
521            var groupedAges = [];
522
523            var sortedAges = utils.sortNumericArray(ages);
524
525            for (var i = 0; i < sortedAges.length; i++) {
526                var groupedAge;
527                var age = sortedAges[i];
528
529                if (age < 18) {
530                    groupedAge = 'Under 18';
531                } else if (age < 25) {
532                    groupedAge = '18 - 24';
533                } else if (age < 35) {
534                    groupedAge = '25 - 34';
535                } else if (age < 45) {
536                    groupedAge = '35 - 44';
537                } else if (age >= 45) {
538                    groupedAge = '45+';
539                }
540
541                groupedAges.push(groupedAge);
542            }
543
544            return groupedAges.join(', ');
545        },
546
547        // Order the country list alphabetically
548        orderCountries: function (countryList) {
549            var countries = countryList.split('|');
550
551            countries.sort();
552
553            return countries.join('|');
554        },
555
556        // Limit countries to 100 characters max, truncating before the | - this is important for custom dimensions as they have a max length (speced 150 bytes)
557        limitCountries: function (countryList) {
558            var match = countryList.match(/^(.{0,100})(?:\|.*)?$/);
559
560            return match[1];
561        },
562
563        // Set an individual cookie
564        setCookie: function (name, value, options) {
565            options = options || {};
566
567            var cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value);
568
569            if (options.expires) {
570                var expires;
571
572                if (typeof options.expires == 'number') {
573                    // A number of days was passed in, convert it into a date from now
574                    expires = new Date();
575                    expires.setDate(expires.getDate() + options.expires);
576                } else {
577                    // A date was passed in, just use the date
578                    expires = options.expires;
579                }
580
581                cookie += '; expires=' + expires.toUTCString();
582            }
583
584            // Even though cookies normally default to current path, it is more useful to default to root for analytics
585            cookie += '; path=' + (options.path || '/');
586
587            // Even though cookies normally default to current domain, it is more useful to default to parent domain for analytics. 
588            // Note that this means we lose the ability to set a domain without a . prefix, as the only way to do that is to not add a 'domain=' to the cookie.
589            cookie += '; domain=' + (options.domain || utils.cookieDomainForHostname(document.location.hostname));
590
591            if (options.secure) {
592                cookie += '; secure';
593            }
594
595            document.cookie = cookie;
596
597            return cookie;
598        },
599
600        // Get an individual cookie by name.
601        // Keep in mind, it's possible to have multiple cookies with the same name, eg: one for .demo.com, another for subdomain.demo.com, and a third for .demo.com/cheese.
602        // There is no way to differentiate between these, and a random one will be returned. In other words, don't reuse names between domain / path levels.
603        getCookie: function (name) {
604            var cookies = utils.getCookies();
605
606            for (var cookieName in cookies) {
607                if (cookieName == name) {
608                    return cookies[cookieName];
609                }
610            }
611
612            return null;
613        },
614
615        // Finds cookies by name based on a passed in regex. Returns an filtered object of cookies
616        findCookies: function (pattern) {
617            var allCookies = utils.getCookies();
618            var matchingCookies = {};
619
620            for (var cookieName in allCookies) {
621                if (cookieName.match(pattern)) {
622                    matchingCookies[cookieName] = allCookies[cookieName];
623                }
624            }
625
626            return matchingCookies;
627        },
628
629        // Returns an object of all cookies decoded
630        getCookies: function () {
631            var rawCookies = document.cookie.split('; ');
632            var cookies = {};
633
634            for (var i = 0; i < rawCookies.length; i++) {
635                var rawCookie = rawCookies[i].split('=');
636                cookies[decodeURIComponent(rawCookie[0])] = decodeURIComponent(rawCookie[1] || ''); // IE saves empty cookie strings as just the cookie name, sans =, so cookie[1] might be null
637            }
638
639            return cookies;
640        },
641
642        // Remove a cookie, this is done by setting a cookie with a date of yesterday.
643        // Keep in mind that if you specify path or domain when you create the cookie, you have to also specify them when you destroy it.
644        deleteCookie: function (name, options) {
645            options = options || {};
646            options.expires = -1;
647
648            utils.setCookie(name, '', options);
649        },
650
651        // Accepts an event, and encodes as a string - makes use of encodeURIComponent to make sure things can be split safely later
652        encodeEvent: function (category, action, eventOptions) {
653            eventOptions = eventOptions || {};
654
655            var categoryString = 'category:' + encodeURIComponent(category);
656            var actionString = 'action:' + encodeURIComponent(action);
657            var labelString = 'label:' + encodeURIComponent(eventOptions['label'] || '');
658            var valueString = 'value:' + (eventOptions['value'] || '');
659
660            return [categoryString, actionString, labelString, valueString].join(';');
661        },
662
663        // Accepts an encoded event, and returns an array of arguments
664        decodeEvent: function (encodedEvent) {
665            var match = encodedEvent.match(/^category:(.+);action:(.+);label:(.+)?;value:(.+)?$/);
666            var options = {};
667
668            if (match[3]) {
669                options['label'] = decodeURIComponent(match[3]);
670            }
671
672            if (match[4]) {
673                options['value'] = Number(match[4]);
674            }
675
676            return [decodeURIComponent(match[1]), decodeURIComponent(match[2]), options];
677        },
678
679        // Attaches passed in callbacks to the browsers window load event
680        fireOnWindowLoad: function (toFire) {
681            if (window.addEventListener) {
682                // One for normal browsers
683                window.addEventListener('load', toFire, false);
684            } else if (window.attachEvent) {
685                // One for IE 8 and below
686                window.attachEvent('onload', toFire);
687            }
688        },
689
690        // Load a script tag
691        loadScript: function (url) {
692            var script = document.createElement('script');
693            script.type = 'text/javascript';
694            script.async = true;
695            script.src = url;
696            var firstScriptTag = document.getElementsByTagName('script')[0];
697            firstScriptTag.parentNode.insertBefore(script, firstScriptTag);
698        },
699
700        // Sort a numeric array ascending
701        sortNumericArray: function (array) {
702            // .slice(0) copies the array to prevent sort in place
703            return array.slice(0).sort(function (a, b) {
704                return a - b;
705            });
706        },
707
708        // Return the appropriate cookie domain for a given hostname
709        cookieDomainForHostname: function (hostname) {
710            var knownRootDomains = ['travelinsurancedirect.com.au', 'worldnomads.com'];
711
712            for (var i = 0; i < knownRootDomains.length; i++) {
713                var knownRootDomain = knownRootDomains[i];
714                var expectedIndex = hostname.length - knownRootDomain.length;
715
716                // Check if the hostname ends with a known root domain
717                if (hostname.indexOf(knownRootDomain, expectedIndex) !== -1) {
718                    return '.' + knownRootDomain;
719                }
720            };
721
722            return hostname.replace(/^(www|service)/, '');;
723        },
724
725        paramsFromUrl: function (url, paramsInHash) {
726            var splitOn = paramsInHash ? '#' : '?';
727
728            // Recurse through the array of pairs, building an object of params as we go
729            var parse = function (params, pairs) {
730                var pair = pairs[0];
731                var parts = pair.split('=');
732                var key = decodeURIComponent(parts[0]);
733                var value = decodeURIComponent(parts.slice(1).join('='));
734
735                // Handle multiple parameters of the same name
736                if (typeof params[key] === "undefined") {
737                    params[key] = value;
738                } else {
739                    params[key] = [].concat(params[key], value);
740                }
741
742                return pairs.length == 1 ? params : parse(params, pairs.slice(1))
743            }
744
745            // Get rid of everything up to and including any first ?
746            var searchString = url.split(splitOn).slice(1).join(splitOn);
747
748            if (searchString.length == 0) {
749                return {};
750            } else {
751                return parse({}, searchString.split('&'));
752            }
753        }
754    };
755
756    return $;
757
758})(wng || {});
759
760// Namespaced under wng, but also assigned to window.googleAnalytics for backwards compatibility across brands
761window.googleAnalytics = wng.googleAnalytics;

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.