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.