1// Conversion-event autotracking for mentalnote (added 2026-04-30). 2// This file is also the single acquisition-linking layer for the marketing 3// site. It keeps a bounded local first-touch record, while cross-domain CTAs 4// retain their authored current campaign and receive only safe bridge fields. 5// It never reads or stores clinical form fields, note text, recordings, or 6// generated output. 7(function (root, factory) { 8 var api = factory(); 9 if (typeof module === 'object' && module.exports) module.exports = api; 10 if (!root || !root.document || !root.location) return; 11 12 var ACQUISITION_STORAGE_KEY = 'mentalnote_acquisition_v1'; 13 var acquisition = {}; 14 var acquisitionInstalled = false; 15 16 function hasAdvertisingConsent() { 17 return !!( 18 root.GTConsent 19 && typeof root.GTConsent.hasAdvertisingConsent === 'function' 20 && root.GTConsent.hasAdvertisingConsent() 21 ); 22 } 23 24 function hasConsentDecision() { 25 return !!( 26 root.GTConsent 27 && typeof root.GTConsent.hasConsentDecision === 'function' 28 && root.GTConsent.hasConsentDecision() 29 ); 30 } 31 32 function isConsentUndecided() { 33 return !hasConsentDecision() && !hasAdvertisingConsent(); 34 } 35 36 function replaceAcquisition(next) { 37 Object.keys(acquisition).forEach(function (key) { delete acquisition[key]; }); 38 Object.keys(next || {}).forEach(function (key) { acquisition[key] = next[key]; }); 39 } 40 41 function syncAcquisitionConsent() { 42 if (!hasAdvertisingConsent()) { 43 api.purgeAcquisition(api.safeStorage(root, {}), ACQUISITION_STORAGE_KEY); 44 api.clearAttributionTargets(root); 45 if (isConsentUndecided()) { 46 // No decision yet: nothing is read from or written to storage, but the 47 // visitor's own current-page attribution may travel on the web-app 48 // handoff URL. The web app holds it in memory and persists it only if 49 // advertising measurement is granted there; a Reject at either end 50 // discards it. 51 api.installUndecidedPassthrough(root, { 52 rawAdIdentifiersEnabled: api.DEFAULT_RAW_AD_IDENTIFIERS_ENABLED, 53 shouldPassThrough: isConsentUndecided, 54 }); 55 } else { 56 api.installCurrentPageHandoff(root); 57 } 58 replaceAcquisition({}); 59 acquisitionInstalled = false; 60 return; 61 } 62 if (acquisitionInstalled) return; 63 try { 64 replaceAcquisition(api.installAcquisition(root, { 65 storageKey: ACQUISITION_STORAGE_KEY, 66 isConsentGranted: hasAdvertisingConsent, 67 rawAdIdentifiersEnabled: api.DEFAULT_RAW_AD_IDENTIFIERS_ENABLED, 68 onAcquisitionChanged: replaceAcquisition, 69 })); 70 acquisitionInstalled = true; 71 } catch (_) { 72 // Attribution is an enhancement. A restricted browser must still keep 73 // normal links and the site's existing event handling usable. 74 } 75 } 76 77 syncAcquisitionConsent(); 78 if (typeof root.addEventListener === 'function') { 79 root.addEventListener('gt:consent-changed', syncAcquisitionConsent); 80 } 81 82 var PAID_VARIANTS = { 83 'ai-soap-note-generator-for-therapists': 'mn_paid_01', 84 'ai-progress-note-scribe-for-therapists': 'mn_paid_02', 85 'therapy-session-to-clinical-note': 'mn_paid_03', 86 }; 87 88 function hasAnalyticsConsent() { 89 return !!( 90 root.GTConsent 91 && typeof root.GTConsent.hasAnalyticsConsent === 'function' 92 && root.GTConsent.hasAnalyticsConsent() 93 ); 94 } 95 96 function safeEventPath() { 97 if (root.GTConsent && typeof root.GTConsent.safeAnalyticsPath === 'function') { 98 return root.GTConsent.safeAnalyticsPath(root.location.pathname); 99 } 100 return '/content'; 101 } 102 103 function sendEvent(name, params) { 104 if (hasAnalyticsConsent() && typeof root.gtag === 'function') { 105 root.gtag('event', name, params); 106 return true; 107 } 108 return false; 109 } 110 111 function toolSlug() { 112 var match = root.location.pathname.match(/\/tools\/([a-z0-9-]+?)(?:\.html)?\/?$/); 113 return match && match[1] !== 'index' ? match[1] : 'unknown'; 114 } 115 116 function onToolPage() { 117 return toolSlug() !== 'unknown'; 118 } 119 120 function paidLandingContext() { 121 var body = root.document.body; 122 if (!body || !body.getAttribute) return null; 123 var landingSlug = api.safeCampaignValue(body.getAttribute('data-paid-landing') || ''); 124 var landingVariant = PAID_VARIANTS[landingSlug] || ''; 125 if (!landingVariant) return null; 126 return { 127 landing_variant: landingVariant, 128 campaign_variant: landingVariant.replace('paid', 'campaign'), 129 }; 130 } 131 132 root.document.addEventListener('click', function (event) { 133 var anchor = event.target && event.target.closest && event.target.closest('a[href]'); 134 if (!anchor) return; 135 var href = anchor.getAttribute('href') || ''; 136 var originalHref = anchor.getAttribute('data-attribution-original-href') || href; 137 138 if (/(?:appsource|marketplace)\.microsoft\.com/.test(href)) { 139 sendEvent('install_cta_click', { 140 cta_location: safeEventPath(), 141 cta_href: api.safeTrackedHref(href, root.location.href), 142 }); 143 return; 144 } 145 146 if (/^\/go\/webapp/.test(originalHref) || /^https:\/\/app\.mentalnote\.ai/.test(href)) { 147 var params = { 148 cta_location: safeEventPath(), 149 cta_href: api.safeTrackedHref(href, root.location.href),
150 acquisition_present: api.ACQUISITION_KEYS.some(function (key) { 151 return !!acquisition[key]; 152 }) ? 'yes' : 'no', 153 }; 154 var paidContext = paidLandingContext(); 155 if (paidContext) { 156 params.landing_variant = paidContext.landing_variant; 157 params.campaign_variant = paidContext.campaign_variant; 158 params.cta_placement = api.safeCampaignValue(anchor.getAttribute('data-cta-placement') || '') || 'unknown'; 159 } 160 sendEvent('webapp_cta_click', params); 161 if (paidContext) sendEvent('paid_landing_cta_click', params); 162 } 163 }); 164 165 root.document.addEventListener('submit', function () { 166 if (onToolPage()) sendEvent('tool_form_submit', { tool_category: 'marketing_tool' }); 167 }, true); 168 169 if (root.navigator.clipboard && root.navigator.clipboard.writeText) { 170 var originalCopy = root.navigator.clipboard.writeText.bind(root.navigator.clipboard); 171 root.navigator.clipboard.writeText = function (text) { 172 try { 173 if (onToolPage()) sendEvent('tool_result_copied', { tool_category: 'marketing_tool' }); 174 } catch (_) {} 175 return originalCopy(text); 176 }; 177 } 178 179 var paidLandingViewSent = false; 180 181 function sendPaidLandingView(context) { 182 if (paidLandingViewSent || !context) return; 183 paidLandingViewSent = sendEvent('paid_landing_view', context); 184 } 185 186 function installPaidLandingEvents() { 187 var context = paidLandingContext(); 188 if (!context) return; 189 190 sendPaidLandingView(context); 191 192 var demo = root.document.querySelector('[data-paid-demo]'); 193 if (demo) { 194 demo.addEventListener('play', function onFirstPlay() { 195 demo.removeEventListener('play', onFirstPlay); 196 sendEvent('paid_landing_demo_start', context); 197 }); 198 } 199 200 var faqs = root.document.querySelectorAll('[data-paid-faq]'); 201 Array.prototype.forEach.call(faqs, function (faq, index) { 202 faq.addEventListener('toggle', function () { 203 if (!faq.open) return; 204 sendEvent('paid_landing_faq_open', { 205 landing_variant: context.landing_variant, 206 campaign_variant: context.campaign_variant, 207 faq_variant: 'question_' + String(index + 1), 208 }); 209 }); 210 }); 211 } 212 213 if (root.document.readyState === 'loading') { 214 root.document.addEventListener('DOMContentLoaded', installPaidLandingEvents, { once: true }); 215 } else { 216 installPaidLandingEvents(); 217 } 218 219 if (typeof root.addEventListener === 'function') { 220 root.addEventListener('gt:consent-changed', function () { 221 sendPaidLandingView(paidLandingContext()); 222 }); 223 } 224})(typeof window !== 'undefined' ? window : null, function () { 225 'use strict'; 226 227 var CAMPAIGN_KEYS = [ 228 'utm_source', 229 'utm_medium', 230 'utm_campaign', 231 'utm_content', 232 ]; 233 var CLICK_KEYS = ['gclid', 'gbraid', 'wbraid', 'msclkid']; 234 var ENTITY_ID_KEYS = ['campaign_id', 'ad_group_id', 'ad_id']; 235 var ENTITY_KEYS = ENTITY_ID_KEYS.concat([ 236 'target_id', 237 'match_type', 238 'network', 239 'device', 240 ]); 241 var RAW_AD_IDENTIFIER_KEYS = CLICK_KEYS.concat(ENTITY_KEYS); 242 var ACQUISITION_KEYS = CAMPAIGN_KEYS.concat(CLICK_KEYS, ENTITY_KEYS); 243 var CONTEXT_KEYS = ['gt_landing_path', 'gt_referrer']; 244 var ATTRIBUTION_KEYS = ACQUISITION_KEYS.concat(CONTEXT_KEYS); 245 var BRIDGE_KEYS = CLICK_KEYS.concat(ENTITY_KEYS, CONTEXT_KEYS); 246 var PAID_MEDIA = [ 247 'cpc', 248 'ppc', 249 'paid', 250 'paid-search', 251 'paid_search', 252 'paidsearch', 253 'paid-social', 254 'paid_social', 255 'paidsocial', 256 'display', 257 'video', 258 ]; 259 var CTA_PARAM_MAP = { 260 utm_source: 'cta_source', 261 utm_medium: 'cta_medium', 262 utm_campaign: 'cta_campaign', 263 utm_content: 'cta_content', 264 }; 265 var TOUCH_BRIDGE_KEYS = [ 266 'first_utm_source', 267 'first_utm_medium', 268 'first_utm_campaign', 269 'first_landing_path', 270 'first_referrer', 271 'first_captured_at', 272 'paid_first_utm_source', 273 'paid_first_utm_medium', 274 'paid_first_utm_campaign', 275 'paid_first_captured_at', 276 'paid_captured_at', 277 ].concat(Object.keys(CTA_PARAM_MAP).map(function (key) { 278 return CTA_PARAM_MAP[key]; 279 })); 280 var FORBIDDEN_SEARCH_KEYS = [ 281 'utm_term', 282 'keyword', 283 'query', 284 'search_term', 285 'searchterm', 286 'search_query', 287 ]; 288 var DEFAULT_TTL_MS = 90 * 24 * 60 * 60 * 1000; 289 var RAW_AD_IDENTIFIER_TTL_MS = 30 * 24 * 60 * 60 * 1000; 290 var MAX_TIMER_DELAY_MS = 2147483647; 291 // Production passes this flag only from syncAcquisitionConsent(), after the 292 // consent manager proves an active advertising-measurement grant. The helper 293 // APIs still require an explicit true option, so direct/test use fails closed. 294 var DEFAULT_RAW_AD_IDENTIFIERS_ENABLED = true;
295 var ADDIN_ATTRIBUTION_KEYS = ['mktcmpid', 'src']; 296 297 function sanitizeValue(value) { 298 if (typeof value !== 'string') return ''; 299 return value.replace(/[\u0000-\u001f\u007f]/g, '').trim().slice(0, 500); 300 } 301 302 function safeCampaignValue(value) { 303 var candidate = sanitizeValue(value).slice(0, 100); 304 return /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,99})$/.test(candidate) ? candidate : ''; 305 } 306 307 function safeClickId(value) { 308 var candidate = sanitizeValue(value).slice(0, 256); 309 return /^[A-Za-z0-9._~-]+$/.test(candidate) ? candidate : ''; 310 } 311 312 function safeEntityId(value) { 313 var candidate = sanitizeValue(value); 314 return candidate.length <= 20 && /^[0-9]{1,20}$/.test(candidate) ? candidate : ''; 315 } 316 317 function safeTargetId(value) { 318 var candidate = sanitizeValue(value).toLowerCase(); 319 if (candidate.length > 256) return ''; 320 return /^(?:aud|dat|dsa|hpi|kwd|kwl|loc|pla)-[0-9]+(?::(?:aud|dat|dsa|hpi|kwd|kwl|loc|pla)-[0-9]+)*$/.test(candidate) 321 ? candidate 322 : ''; 323 } 324 325 function safeEnum(value, allowed) { 326 var candidate = sanitizeValue(value).toLowerCase(); 327 return allowed.indexOf(candidate) !== -1 ? candidate : ''; 328 } 329 330 function safeEntityValue(key, value) { 331 if (ENTITY_ID_KEYS.indexOf(key) !== -1) return safeEntityId(value); 332 if (key === 'target_id') return safeTargetId(value); 333 if (key === 'match_type') return safeEnum(value, ['a', 'b', 'c', 'e', 'p']); 334 if (key === 'network') return safeEnum(value, ['a', 'd', 'e', 'g', 'gtv', 'o', 's', 'vp', 'x', 'ytv']); 335 if (key === 'device') return safeEnum(value, ['c', 'm', 't']); 336 return ''; 337 } 338 339 function readAcquisition(search, rawAdIdentifiersEnabled) { 340 var params = new URLSearchParams(search || ''); 341 var values = {}; 342 CAMPAIGN_KEYS.forEach(function (key) { 343 var value = safeCampaignValue(params.get(key) || ''); 344 if (value) values[key] = value; 345 }); 346 if (rawAdIdentifiersEnabled === true) { 347 CLICK_KEYS.forEach(function (key) { 348 var value = safeClickId(params.get(key) || ''); 349 if (value) values[key] = value; 350 }); 351 ENTITY_KEYS.forEach(function (key) { 352 var value = safeEntityValue(key, params.get(key) || ''); 353 if (value) values[key] = value; 354 }); 355 } 356 return values; 357 } 358 359 function safeLandingPath(pathname) { 360 if (typeof pathname !== 'string') return ''; 361 var path = sanitizeValue(pathname.split(/[?#]/)[0]).slice(0, 300); 362 return path.charAt(0) === '/' && path.slice(0, 2) !== '//' ? path : ''; 363 } 364 365 function safeReferrerOrigin(referrer, marketingHostname) { 366 if (!referrer) return ''; 367 try { 368 var url = new URL(referrer); 369 if (url.protocol !== 'https:' && url.protocol !== 'http:') return ''; 370 var referrerHost = url.hostname.toLowerCase().replace(/^www\./, ''); 371 var siteHost = String(marketingHostname || '').toLowerCase().replace(/^www\./, ''); 372 if (siteHost && (referrerHost === siteHost || referrerHost.slice(-(siteHost.length + 1)) === '.' + siteHost)) { 373 return ''; 374 } 375 return sanitizeValue(url.origin).slice(0, 300); 376 } catch (_) { 377 return ''; 378 } 379 } 380 381 function readTouchContext(pathname, referrer, marketingHostname) { 382 var values = {}; 383 var landingPath = safeLandingPath(pathname); 384 var referrerOrigin = safeReferrerOrigin(referrer, marketingHostname); 385 if (landingPath) values.gt_landing_path = landingPath; 386 if (referrerOrigin) values.gt_referrer = referrerOrigin; 387 return values; 388 } 389 390 function cleanAttributionValue(key, value) { 391 if (key === 'gt_landing_path') return safeLandingPath(value); 392 if (key === 'gt_referrer') return safeReferrerOrigin(value, ''); 393 if (CLICK_KEYS.indexOf(key) !== -1) return safeClickId(value); 394 if (ENTITY_KEYS.indexOf(key) !== -1) return safeEntityValue(key, value); 395 if (CAMPAIGN_KEYS.indexOf(key) !== -1) return safeCampaignValue(value); 396 return ''; 397 } 398 399 function hasValues(values) { 400 return !!values && Object.keys(values).length > 0;
401 } 402 403 function hasPaidSignal(values) { 404 if (!values) return false; 405 if (CLICK_KEYS.some(function (key) { return !!safeClickId(values[key] || ''); })) { 406 return true; 407 } 408 var medium = safeCampaignValue(values.utm_medium || '').toLowerCase(); 409 return PAID_MEDIA.indexOf(medium) !== -1; 410 } 411 412 function readStoredRecord(storage, storageKey, now, ttlMs, rawAdIdentifiersEnabled) { 413 if (!storage) return null; 414 try { 415 var parsed = JSON.parse(storage.getItem(storageKey) || 'null'); 416 if (!parsed || typeof parsed.capturedAt !== 'number' || !parsed.values) return null; 417 var ageMs = now - parsed.capturedAt; 418 if (ageMs >= ttlMs || parsed.capturedAt > now + 5 * 60 * 1000) { 419 storage.removeItem(storageKey); 420 return null; 421 } 422 var clean = {}; 423 var allowedKeys = CAMPAIGN_KEYS.concat(CONTEXT_KEYS); 424 var rawIdentifiersCurrent = rawAdIdentifiersEnabled === true 425 && ageMs < Math.min(ttlMs, RAW_AD_IDENTIFIER_TTL_MS); 426 if (rawIdentifiersCurrent) { 427 allowedKeys = allowedKeys.concat(RAW_AD_IDENTIFIER_KEYS); 428 } 429 allowedKeys.forEach(function (key) { 430 var value = cleanAttributionValue(key, parsed.values[key] || ''); 431 if (value) clean[key] = value; 432 }); 433 if (!hasValues(clean)) { 434 storage.removeItem(storageKey); 435 return null; 436 } 437 if ( 438 !rawIdentifiersCurrent && 439 RAW_AD_IDENTIFIER_KEYS.some(function (key) { 440 return Object.prototype.hasOwnProperty.call(parsed.values, key); 441 }) 442 ) { 443 writeStoredRecord(storage, storageKey, parsed.capturedAt, clean); 444 } 445 return { capturedAt: parsed.capturedAt, values: clean }; 446 } catch (_) { 447 return null; 448 } 449 } 450 451 function readStored(storage, storageKey, now, ttlMs, rawAdIdentifiersEnabled) { 452 var record = readStoredRecord( 453 storage, 454 storageKey, 455 now, 456 ttlMs, 457 rawAdIdentifiersEnabled, 458 ); 459 return record ? record.values : {}; 460 } 461 462 function ageFilterRecord(record, now, ttlMs, rawAdIdentifiersEnabled) { 463 if (!record || typeof record.capturedAt !== 'number' || !record.values) return null; 464 var ageMs = now - record.capturedAt; 465 if (ageMs >= ttlMs || record.capturedAt > now + 5 * 60 * 1000) return null; 466 var clean = {}; 467 var allowedKeys = CAMPAIGN_KEYS.concat(CONTEXT_KEYS); 468 if ( 469 rawAdIdentifiersEnabled === true 470 && ageMs < Math.min(ttlMs, RAW_AD_IDENTIFIER_TTL_MS) 471 ) { 472 allowedKeys = allowedKeys.concat(RAW_AD_IDENTIFIER_KEYS); 473 } 474 allowedKeys.forEach(function (key) { 475 var value = cleanAttributionValue(key, record.values[key] || ''); 476 if (value) clean[key] = value; 477 }); 478 return hasValues(clean) 479 ? { capturedAt: record.capturedAt, values: clean } 480 : null; 481 } 482 483 function writeStoredRecord(storage, storageKey, capturedAt, values) { 484 if (!storage || !hasValues(values)) return; 485 try { 486 storage.setItem(storageKey, JSON.stringify({ capturedAt: capturedAt, values: values })); 487 } catch (_) { 488 // Storage is best effort. The current navigation still carries the 489 // in-memory touch assembled below. 490 } 491 } 492 493 function buildIncomingTouch(search, context, rawAdIdentifiersEnabled) { 494 var incoming = readAcquisition(search, rawAdIdentifiersEnabled); 495 context = context || {}; 496 CONTEXT_KEYS.forEach(function (key) { 497 if (context[key] && !incoming[key]) incoming[key] = cleanAttributionValue(key, context[key]); 498 }); 499 return incoming; 500 } 501 502 function persistAcquisition( 503 storage, 504 storageKey, 505 search, 506 now, 507 ttlMs, 508 context, 509 rawAdIdentifiersEnabled, 510 ) { 511 // Match the backend's first-touch attribution contract. A later campaign 512 // must not silently replace the original acquisition record. 513 var firstTouch = readStored( 514 storage, 515 storageKey, 516 now, 517 ttlMs, 518 rawAdIdentifiersEnabled, 519 ); 520 if (hasValues(firstTouch)) return firstTouch; 521 var incoming = buildIncomingTouch(search, context, rawAdIdentifiersEnabled); 522 if (!hasValues(incoming)) return {}; 523 writeStoredRecord(storage, storageKey, now, incoming); 524 return incoming; 525 } 526 527 function persistPaidTouches( 528 storage, 529 storageKey, 530 search, 531 now, 532 ttlMs, 533 context, 534 rawAdIdentifiersEnabled, 535 ) { 536 var firstPaidKey = storageKey + '_paid_first'; 537 var latestPaidKey = storageKey + '_paid_latest'; 538 var incoming = buildIncomingTouch(search, context, rawAdIdentifiersEnabled); 539 var firstPaid = readStoredRecord( 540 storage, 541 firstPaidKey, 542 now, 543 ttlMs, 544 rawAdIdentifiersEnabled, 545 ); 546 var latestPaid = readStoredRecord( 547 storage, 548 latestPaidKey, 549 now, 550 ttlMs, 551 rawAdIdentifiersEnabled, 552 ); 553 554 if (firstPaid && !hasPaidSignal(firstPaid.values)) {
555 try { storage.removeItem(firstPaidKey); } catch (_) {} 556 firstPaid = null; 557 } 558 if (latestPaid && !hasPaidSignal(latestPaid.values)) { 559 try { storage.removeItem(latestPaidKey); } catch (_) {} 560 latestPaid = null; 561 } 562 563 if (hasPaidSignal(incoming)) { 564 if (!firstPaid) { 565 firstPaid = { capturedAt: now, values: incoming }; 566 writeStoredRecord(storage, firstPaidKey, now, incoming); 567 } 568 // Preserve the most recent provider click independently from immutable 569 // first touch so conversion uploads can match the converting visit. 570 latestPaid = { capturedAt: now, values: incoming }; 571 writeStoredRecord(storage, latestPaidKey, now, incoming); 572 } 573 574 return { firstPaid: firstPaid, latestPaid: latestPaid }; 575 } 576 577 function applyDefaults(url) { 578 var defaults = { 579 utm_source: 'mentalnote.ai', 580 utm_medium: 'cta', 581 utm_campaign: 'webapp', 582 }; 583 Object.keys(defaults).forEach(function (key) { 584 if (!url.searchParams.has(key)) url.searchParams.set(key, defaults[key]); 585 }); 586 } 587 588 function isMentalNoteAddinUrl(url) { 589 if (!url) return false; 590 var host = String(url.hostname || '').toLowerCase(); 591 return (host === 'appsource.microsoft.com' || host === 'marketplace.microsoft.com') 592 && /\/WA200006713(?:$|[/?])/i.test(url.pathname + url.search); 593 } 594 595 function addinDestination(source) { 596 if (source.hostname === 'mentalnote.ai' && /^\/go\/mentalnote\/?$/.test(source.pathname)) { 597 return new URL('https://marketplace.microsoft.com/en-us/product/office/WA200006713'); 598 } 599 return isMentalNoteAddinUrl(source) ? new URL(source.toString()) : null; 600 } 601 602 function applyAddinDefaults(url) { 603 var defaults = { 604 utm_source: 'mentalnote.ai', 605 utm_medium: 'cta', 606 utm_campaign: 'install', 607 }; 608 Object.keys(defaults).forEach(function (key) { 609 if (!url.searchParams.has(key)) url.searchParams.set(key, defaults[key]); 610 }); 611 } 612 613 function copyAuthoredAttribution(source, destination, rawAdIdentifiersEnabled) { 614 CAMPAIGN_KEYS.forEach(function (key) { 615 var value = safeCampaignValue(source.searchParams.get(key) || ''); 616 if (value) destination.searchParams.set(key, value); 617 }); 618 if (rawAdIdentifiersEnabled === true) { 619 CLICK_KEYS.forEach(function (key) { 620 var value = safeClickId(source.searchParams.get(key) || ''); 621 if (value) destination.searchParams.set(key, value); 622 }); 623 ENTITY_KEYS.forEach(function (key) { 624 var value = safeEntityValue(key, source.searchParams.get(key) || ''); 625 if (value) destination.searchParams.set(key, value); 626 }); 627 } 628 } 629 630 function normalizeDestinationAttribution(url, rawAdIdentifiersEnabled) { 631 TOUCH_BRIDGE_KEYS.forEach(function (key) { 632 url.searchParams.delete(key); 633 }); 634 CAMPAIGN_KEYS.forEach(function (key) { 635 if (!url.searchParams.has(key)) return; 636 var value = safeCampaignValue(url.searchParams.get(key) || ''); 637 if (value) url.searchParams.set(key, value); 638 else url.searchParams.delete(key); 639 }); 640 RAW_AD_IDENTIFIER_KEYS.forEach(function (key) { 641 if (!url.searchParams.has(key)) return; 642 if (rawAdIdentifiersEnabled !== true) { 643 url.searchParams.delete(key); 644 return; 645 } 646 var value = CLICK_KEYS.indexOf(key) !== -1 647 ? safeClickId(url.searchParams.get(key) || '') 648 : safeEntityValue(key, url.searchParams.get(key) || ''); 649 if (value) url.searchParams.set(key, value); 650 else url.searchParams.delete(key); 651 }); 652 FORBIDDEN_SEARCH_KEYS.forEach(function (key) { 653 url.searchParams.delete(key); 654 }); 655 } 656 657 function applyBridge(url, acquisition, rawAdIdentifiersEnabled) { 658 var bridgeKeys = CONTEXT_KEYS.slice(); 659 if (rawAdIdentifiersEnabled === true) { 660 bridgeKeys = bridgeKeys.concat(RAW_AD_IDENTIFIER_KEYS); 661 } 662 bridgeKeys.forEach(function (key) { 663 var value = cleanAttributionValue(key, acquisition[key] || ''); 664 if (value && !url.searchParams.has(key)) url.searchParams.set(key, value); 665 });
666 } 667 668 function applyCampaignBridge(url, acquisition) { 669 if (!hasPaidSignal(acquisition)) return; 670 var source = safeCampaignValue(acquisition.utm_source || ''); 671 var medium = safeCampaignValue(acquisition.utm_medium || ''); 672 // A click id without reviewed provider channel labels must not create a 673 // hybrid source/CTA campaign. The click id itself remains available. 674 if (!source || !medium) return; 675 CAMPAIGN_KEYS.forEach(function (key) { 676 var value = safeCampaignValue(acquisition[key] || ''); 677 if (value) url.searchParams.set(key, value); 678 }); 679 } 680 681 function applyCtaContext(url) { 682 Object.keys(CTA_PARAM_MAP).forEach(function (key) { 683 var value = safeCampaignValue(url.searchParams.get(key) || ''); 684 if (value) url.searchParams.set(CTA_PARAM_MAP[key], value); 685 }); 686 } 687 688 function applyTouchBridge(url, touches) { 689 touches = touches || {}; 690 var first = touches.first && touches.first.values; 691 var firstPaid = touches.firstPaid && touches.firstPaid.values; 692 if (first) { 693 [ 694 ['utm_source', 'first_utm_source'], 695 ['utm_medium', 'first_utm_medium'], 696 ['utm_campaign', 'first_utm_campaign'], 697 ['gt_landing_path', 'first_landing_path'], 698 ['gt_referrer', 'first_referrer'], 699 ].forEach(function (pair) { 700 var value = cleanAttributionValue(pair[0], first[pair[0]] || ''); 701 if (value) url.searchParams.set(pair[1], value); 702 }); 703 if (Number.isSafeInteger(touches.first.capturedAt)) { 704 url.searchParams.set('first_captured_at', String(touches.first.capturedAt)); 705 } 706 } 707 if (firstPaid) { 708 [ 709 ['utm_source', 'paid_first_utm_source'], 710 ['utm_medium', 'paid_first_utm_medium'], 711 ['utm_campaign', 'paid_first_utm_campaign'], 712 ].forEach(function (pair) { 713 var value = safeCampaignValue(firstPaid[pair[0]] || ''); 714 if (value) url.searchParams.set(pair[1], value); 715 }); 716 if (Number.isSafeInteger(touches.firstPaid.capturedAt)) { 717 url.searchParams.set('paid_first_captured_at', String(touches.firstPaid.capturedAt)); 718 } 719 } 720 if (touches.latestPaid && Number.isSafeInteger(touches.latestPaid.capturedAt)) { 721 url.searchParams.set('paid_captured_at', String(touches.latestPaid.capturedAt)); 722 } 723 } 724 725 function decorateHref( 726 href, 727 baseHref, 728 acquisition, 729 touches, 730 rawAdIdentifiersEnabled, 731 ) { 732 var source; 733 try { 734 source = new URL(href, baseHref); 735 } catch (_) { 736 return href; 737 } 738 var isRoute = source.hostname === 'mentalnote.ai' && /^\/go\/webapp\/?$/.test(source.pathname); 739 var isApp = source.hostname === 'app.mentalnote.ai'; 740 var addin = addinDestination(source); 741 if (!isRoute && !isApp && !addin) return href; 742 743 var destination = addin || (isRoute ? new URL('https://app.mentalnote.ai/') : new URL(source.toString())); 744 if (isRoute) { 745 copyAuthoredAttribution(source, destination, rawAdIdentifiersEnabled); 746 } 747 normalizeDestinationAttribution(destination, rawAdIdentifiersEnabled); 748 if (addin) applyAddinDefaults(destination); 749 else { 750 applyDefaults(destination); 751 applyCtaContext(destination); 752 } 753 // Provider acquisition and CTA placement are separate dimensions. Paid 754 // UTMs remain intact for attribution, while authored CTA semantics travel 755 // in cta_* fields. Search text is never stored or bridged. 756 applyCampaignBridge(destination, acquisition || {}); 757 applyBridge(destination, acquisition || {}, rawAdIdentifiersEnabled); 758 applyTouchBridge(destination, touches); 759 return destination.toString(); 760 } 761 762 function stripAttributionHref(href, baseHref) { 763 var source; 764 try { 765 source = new URL(href, baseHref); 766 } catch (_) { 767 return href; 768 } 769 var isRoute = source.hostname === 'mentalnote.ai' 770 && /^\/go\/(?:webapp|mentalnote|gentext|lexdraft)\/?$/.test(source.pathname); 771 var isTarget = source.hostname === 'app.mentalnote.ai' || isMentalNoteAddinUrl(source); 772 if (!isRoute && !isTarget) return href; 773 774 CAMPAIGN_KEYS 775 .concat(RAW_AD_IDENTIFIER_KEYS, CONTEXT_KEYS, TOUCH_BRIDGE_KEYS, FORBIDDEN_SEARCH_KEYS) 776 .concat(Object.keys(CTA_PARAM_MAP).map(function (key) { return CTA_PARAM_MAP[key]; }))
777 .concat(ADDIN_ATTRIBUTION_KEYS) 778 .forEach(function (key) { source.searchParams.delete(key); }); 779 780 if (source.hostname === 'mentalnote.ai') { 781 return source.pathname + source.search + source.hash; 782 } 783 return source.toString(); 784 } 785 786 function authoredAttributionHref(anchor, href, baseHref) { 787 if (!anchor || typeof anchor.getAttribute !== 'function') return href; 788 var source; 789 try { 790 source = new URL(href, baseHref); 791 } catch (_) { 792 return href; 793 } 794 [ 795 ['utm_source', 'data-attribution-source'], 796 ['utm_medium', 'data-attribution-medium'], 797 ['utm_campaign', 'data-attribution-campaign'], 798 ['utm_content', 'data-attribution-content'], 799 ].forEach(function (pair) { 800 var value = safeCampaignValue(anchor.getAttribute(pair[1]) || ''); 801 if (value) source.searchParams.set(pair[0], value); 802 }); 803 return source.toString(); 804 } 805 806 function safeTrackedHref(href, baseHref) { 807 try { 808 var url = new URL(href, baseHref); 809 return url.origin + url.pathname; 810 } catch (_) { 811 return String(href || '').split('?')[0].split('#')[0]; 812 } 813 } 814 815 function safeStorage(browser, options) { 816 try { 817 if (Object.prototype.hasOwnProperty.call(options, 'storage')) return options.storage; 818 return browser.localStorage; 819 } catch (_) { 820 // Some privacy modes throw while merely reading window.localStorage. 821 // Continue with current-page attribution and do not affect navigation. 822 return null; 823 } 824 } 825 826 function purgeAcquisition(storage, storageKey) { 827 if (!storage || typeof storage.removeItem !== 'function' || typeof storage.getItem !== 'function') return false; 828 var allPurged = true; 829 [storageKey, storageKey + '_paid_first', storageKey + '_paid_latest'].forEach(function (key) { 830 try { 831 storage.removeItem(key); 832 if (storage.getItem(key) === null) return; 833 if (typeof storage.setItem !== 'function') { 834 allPurged = false; 835 return; 836 } 837 storage.setItem(key, ''); 838 if (storage.getItem(key) !== '') allPurged = false; 839 } catch (_) { 840 allPurged = false; 841 } 842 }); 843 return allPurged; 844 } 845 846 function clearAttributionTargets(browser) { 847 if (!browser || !browser.document || !browser.document.querySelectorAll) return; 848 var anchors = browser.document.querySelectorAll('[data-attribution-original-href]'); 849 Array.prototype.forEach.call(anchors, function (anchor) { 850 var original = anchor.getAttribute('data-attribution-original-href') || ''; 851 if (original) anchor.setAttribute('href', original); 852 if (typeof anchor.removeAttribute === 'function') { 853 anchor.removeAttribute('data-attribution-original-href'); 854 } 855 }); 856 } 857 858 function installCurrentPageHandoff(browser) { 859 if (!browser || !browser.document || !browser.location) return {}; 860 // Before advertising consent, outbound product links remain functional but 861 // carry no acquisition, click, CTA, or marketplace campaign parameters. 862 863 function decorateAnchor(anchor) { 864 if (!anchor || !anchor.getAttribute) return; 865 var href = anchor.getAttribute('href') || ''; 866 var originalHref = anchor.getAttribute('data-attribution-original-href') || href; 867 var decorated = stripAttributionHref(originalHref, browser.location.href); 868 if (decorated === href) return; 869 if (!anchor.hasAttribute('data-attribution-original-href')) { 870 anchor.setAttribute('data-attribution-original-href', originalHref); 871 } 872 anchor.setAttribute('href', decorated); 873 } 874 875 function decorateAll() { 876 var anchors = browser.document.querySelectorAll( 877 'a[href^="/go/webapp"], a[href^="/go/mentalnote"], ' 878 + 'a[href^="/go/gentext"], a[href^="/go/lexdraft"], ' 879 + 'a[href^="https://app.mentalnote.ai"], ' 880 + 'a[href*="appsource.microsoft.com"], a[href*="marketplace.microsoft.com"]', 881 ); 882 Array.prototype.forEach.call(anchors, decorateAnchor); 883 } 884 885 if (browser.document.readyState === 'loading') { 886 browser.document.addEventListener('DOMContentLoaded', decorateAll, { once: true });
887 } else { 888 decorateAll(); 889 } 890 browser.document.addEventListener('click', function (event) { 891 var anchor = event.target && event.target.closest && event.target.closest('a[href]'); 892 decorateAnchor(anchor); 893 }, true); 894 return {}; 895 } 896 897 function passthroughAttributionHref(href, baseHref, pageHref, referrer, rawAdIdentifiersEnabled) { 898 var source; 899 try { 900 source = new URL(href, baseHref); 901 } catch (_) { 902 return href; 903 } 904 var isRoute = source.hostname === 'mentalnote.ai' && /^\/go\/webapp\/?$/.test(source.pathname); 905 var isApp = source.hostname === 'app.mentalnote.ai'; 906 // The Word add-in marketplace is a third party and never receives 907 // pass-through attribution; only the owned web-app handoff does. 908 if (!isRoute && !isApp) return href; 909 910 var live; 911 try { 912 live = new URL(pageHref); 913 } catch (_) { 914 return href; 915 } 916 var carriesAttribution = CAMPAIGN_KEYS.concat(RAW_AD_IDENTIFIER_KEYS).some(function (key) { 917 return !!live.searchParams.get(key); 918 }); 919 if (!carriesAttribution) return href; 920 921 var destination = isRoute ? new URL('https://app.mentalnote.ai/') : new URL(source.toString()); 922 if (isRoute) copyAuthoredAttribution(source, destination, rawAdIdentifiersEnabled); 923 normalizeDestinationAttribution(destination, rawAdIdentifiersEnabled); 924 applyDefaults(destination); 925 applyCtaContext(destination); 926 // The live copy only sets allowlisted, individually sanitized keys, so no 927 // second normalization pass is needed (it would scrub the cta_* context). 928 copyAuthoredAttribution(live, destination, rawAdIdentifiersEnabled); 929 var landingPath = safeLandingPath(live.pathname); 930 if (landingPath && !destination.searchParams.has('gt_landing_path')) { 931 destination.searchParams.set('gt_landing_path', landingPath); 932 } 933 var referrerOrigin = safeReferrerOrigin(referrer || '', 'mentalnote.ai'); 934 if (referrerOrigin && !destination.searchParams.has('gt_referrer')) { 935 destination.searchParams.set('gt_referrer', referrerOrigin); 936 } 937 return destination.toString(); 938 } 939 940 function installUndecidedPassthrough(browser, options) { 941 if (!browser || !browser.document || !browser.location) return {}; 942 options = options || {}; 943 var rawAdIdentifiersEnabled = options.rawAdIdentifiersEnabled === true; 944 var shouldPassThrough = typeof options.shouldPassThrough === 'function' 945 ? options.shouldPassThrough 946 : function () { return true; }; 947 948 function decorateAnchor(anchor) { 949 if (!anchor || !anchor.getAttribute) return; 950 if (!shouldPassThrough()) return; 951 var href = anchor.getAttribute('href') || ''; 952 var originalHref = anchor.getAttribute('data-attribution-original-href') || href; 953 var decorated = passthroughAttributionHref( 954 originalHref, 955 browser.location.href, 956 browser.location.href, 957 browser.document.referrer || '', 958 rawAdIdentifiersEnabled, 959 ); 960 if (decorated === href) return; 961 if (!anchor.hasAttribute('data-attribution-original-href')) { 962 anchor.setAttribute('data-attribution-original-href', originalHref); 963 } 964 anchor.setAttribute('href', decorated); 965 } 966 967 function decorateAll() { 968 var anchors = browser.document.querySelectorAll( 969 'a[href^="/go/webapp"], a[href^="https://app.mentalnote.ai"]', 970 ); 971 Array.prototype.forEach.call(anchors, decorateAnchor); 972 } 973 974 if (browser.document.readyState === 'loading') { 975 browser.document.addEventListener('DOMContentLoaded', decorateAll, { once: true }); 976 } else { 977 decorateAll(); 978 } 979 browser.document.addEventListener('click', function (event) { 980 var anchor = event.target && event.target.closest && event.target.closest('a[href]'); 981 decorateAnchor(anchor); 982 }, true); 983 return {}; 984 } 985 986 function installAcquisition(browser, options) { 987 options = options || {}; 988 var storageKey = options.storageKey || 'mentalnote_acquisition_v1'; 989 var ttlMs = options.ttlMs || DEFAULT_TTL_MS; 990 var installedAtCandidate = typeof options.now === 'function' 991 ? options.now() 992 : (typeof options.now === 'number' ? options.now : Date.now()); 993 var installedAt = typeof installedAtCandidate === 'number' && isFinite(installedAtCandidate) 994 ? installedAtCandidate 995 : Date.now(); 996 var rawAdIdentifiersEnabled = options.rawAdIdentifiersEnabled === true; 997 var storage = safeStorage(browser, options); 998 var isConsentGranted = typeof options.isConsentGranted === 'function' 999 ? options.isConsentGranted 1000 : function () { return true; }; 1001 var onAcquisitionChanged = typeof options.onAcquisitionChanged === 'function' 1002 ? options.onAcquisitionChanged 1003 : function () {}; 1004 var installToken = {}; 1005 var installSlot = '__mentalNoteAttributionInstall'; 1006 browser[installSlot] = installToken; 1007 var refreshTimer = null; 1008 1009 function currentTime() { 1010 var value = typeof options.now === 'function' ? options.now() : Date.now(); 1011 if (typeof options.now === 'number') value = options.now; 1012 return typeof value === 'number' && isFinite(value) ? value : Date.now(); 1013 } 1014 1015 function isActive() { 1016 return browser[installSlot] === installToken; 1017 } 1018 1019 function clearRefreshTimer() { 1020 if (refreshTimer !== null && typeof browser.clearTimeout === 'function') { 1021 browser.clearTimeout(refreshTimer); 1022 } 1023 refreshTimer = null; 1024 } 1025 if (!isConsentGranted()) { 1026 purgeAcquisition(storage, storageKey); 1027 clearAttributionTargets(browser); 1028 return {}; 1029 } 1030 var context = readTouchContext( 1031 browser.location.pathname, 1032 browser.document.referrer, 1033 browser.location.hostname, 1034 ); 1035 var firstTouch = persistAcquisition( 1036 storage, 1037 storageKey, 1038 browser.location.search, 1039 installedAt, 1040 ttlMs, 1041 context, 1042 rawAdIdentifiersEnabled, 1043 ); 1044 var paidTouches = persistPaidTouches( 1045 storage, 1046 storageKey, 1047 browser.location.search, 1048 installedAt, 1049 ttlMs, 1050 context, 1051 rawAdIdentifiersEnabled, 1052 ); 1053 var firstRecord = readStoredRecord( 1054 storage, 1055 storageKey, 1056 installedAt, 1057 ttlMs, 1058 rawAdIdentifiersEnabled, 1059 ) 1060 || (hasValues(firstTouch) ? { capturedAt: installedAt, values: firstTouch } : null); 1061 var initialTouches = { 1062 first: firstRecord, 1063 firstPaid: paidTouches.firstPaid, 1064 latestPaid: paidTouches.latestPaid, 1065 }; 1066 var initialAcquisition = paidTouches.latestPaid ? paidTouches.latestPaid.values : firstTouch; 1067 1068 function currentRecord(key, fallback, now) { 1069 return readStoredRecord( 1070 storage, 1071 key, 1072 now, 1073 ttlMs, 1074 rawAdIdentifiersEnabled, 1075 ) || ageFilterRecord(fallback, now, ttlMs, rawAdIdentifiersEnabled); 1076 } 1077 1078 function currentState() { 1079 var now = currentTime(); 1080 var touches = { 1081 first: currentRecord(storageKey, initialTouches.first, now), 1082 firstPaid: currentRecord( 1083 storageKey + '_paid_first', 1084 initialTouches.firstPaid, 1085 now, 1086 ), 1087 latestPaid: currentRecord( 1088 storageKey + '_paid_latest', 1089 initialTouches.latestPaid, 1090 now, 1091 ), 1092 }; 1093 var acquisition = touches.latestPaid 1094 ? touches.latestPaid.values 1095 : (touches.first ? touches.first.values : {});
1096 try { onAcquisitionChanged(acquisition); } catch (_) {} 1097 return { now: now, touches: touches, acquisition: acquisition }; 1098 } 1099 1100 function nextExpiryDelay(state) { 1101 var deadlines = []; 1102 ['first', 'firstPaid', 'latestPaid'].forEach(function (key) { 1103 var record = state.touches[key]; 1104 if (!record) return; 1105 deadlines.push(record.capturedAt + ttlMs); 1106 if ( 1107 rawAdIdentifiersEnabled 1108 && RAW_AD_IDENTIFIER_KEYS.some(function (rawKey) { 1109 return !!record.values[rawKey]; 1110 }) 1111 ) { 1112 deadlines.push( 1113 record.capturedAt + Math.min(ttlMs, RAW_AD_IDENTIFIER_TTL_MS), 1114 ); 1115 } 1116 }); 1117 var future = deadlines.filter(function (deadline) { 1118 return deadline > state.now; 1119 }); 1120 if (!future.length) return 0; 1121 return Math.min.apply(Math, future) - state.now; 1122 } 1123 1124 function scheduleRefresh(state) { 1125 clearRefreshTimer(); 1126 if (!isActive() || !isConsentGranted()) return; 1127 var remainingMs = nextExpiryDelay(state || currentState()); 1128 if (remainingMs <= 0 || typeof browser.setTimeout !== 'function') return; 1129 refreshTimer = browser.setTimeout(function () { 1130 refreshTimer = null; 1131 refreshAll(); 1132 }, Math.min(remainingMs, MAX_TIMER_DELAY_MS)); 1133 } 1134 1135 function decorateAnchor(anchor) { 1136 if (!isActive() || !isConsentGranted()) return; 1137 if (!anchor || !anchor.getAttribute) return; 1138 var state = currentState(); 1139 var href = anchor.getAttribute('href') || ''; 1140 var originalHref = anchor.getAttribute('data-attribution-original-href') || href; 1141 var decorated = decorateHref( 1142 authoredAttributionHref(anchor, originalHref, browser.location.href), 1143 browser.location.href, 1144 state.acquisition, 1145 state.touches, 1146 rawAdIdentifiersEnabled, 1147 ); 1148 if (decorated === href) return; 1149 if (!anchor.hasAttribute('data-attribution-original-href')) { 1150 anchor.setAttribute('data-attribution-original-href', originalHref); 1151 } 1152 anchor.setAttribute('href', decorated); 1153 } 1154 1155 function decorateAll() { 1156 if (!isActive() || !isConsentGranted()) { 1157 clearRefreshTimer(); 1158 clearAttributionTargets(browser); 1159 return; 1160 } 1161 var anchors = browser.document.querySelectorAll( 1162 'a[href^="/go/webapp"], a[href^="/go/mentalnote"], ' 1163 + 'a[href^="/go/gentext"], a[href^="/go/lexdraft"], ' 1164 + 'a[href^="https://app.mentalnote.ai"], ' 1165 + 'a[href*="appsource.microsoft.com"], a[href*="marketplace.microsoft.com"]', 1166 ); 1167 Array.prototype.forEach.call(anchors, decorateAnchor); 1168 } 1169 1170 function refreshAll() { 1171 decorateAll(); 1172 scheduleRefresh(currentState()); 1173 } 1174 1175 if (browser.document.readyState === 'loading') { 1176 browser.document.addEventListener('DOMContentLoaded', refreshAll, { once: true }); 1177 } else { 1178 refreshAll(); 1179 } 1180 browser.document.addEventListener('click', function (event) { 1181 if (!isActive() || !isConsentGranted()) return; 1182 var anchor = event.target && event.target.closest && event.target.closest('a[href]'); 1183 decorateAnchor(anchor); 1184 }, true); 1185 if (typeof browser.addEventListener === 'function') { 1186 browser.addEventListener('focus', refreshAll); 1187 } 1188 if (typeof browser.document.addEventListener === 'function') { 1189 browser.document.addEventListener('visibilitychange', function () { 1190 if (browser.document.hidden !== true) refreshAll(); 1191 }); 1192 } 1193 return initialAcquisition; 1194 } 1195 1196 return { 1197 CAMPAIGN_KEYS: CAMPAIGN_KEYS, 1198 CLICK_KEYS: CLICK_KEYS, 1199 ENTITY_KEYS: ENTITY_KEYS, 1200 RAW_AD_IDENTIFIER_KEYS: RAW_AD_IDENTIFIER_KEYS, 1201 ACQUISITION_KEYS: ACQUISITION_KEYS, 1202 CONTEXT_KEYS: CONTEXT_KEYS, 1203 ATTRIBUTION_KEYS: ATTRIBUTION_KEYS, 1204 BRIDGE_KEYS: BRIDGE_KEYS, 1205 PAID_MEDIA: PAID_MEDIA, 1206 CTA_PARAM_MAP: CTA_PARAM_MAP, 1207 TOUCH_BRIDGE_KEYS: TOUCH_BRIDGE_KEYS, 1208 DEFAULT_TTL_MS: DEFAULT_TTL_MS, 1209 RAW_AD_IDENTIFIER_TTL_MS: RAW_AD_IDENTIFIER_TTL_MS, 1210 MAX_TIMER_DELAY_MS: MAX_TIMER_DELAY_MS, 1211 DEFAULT_RAW_AD_IDENTIFIERS_ENABLED: DEFAULT_RAW_AD_IDENTIFIERS_ENABLED,
1212 sanitizeValue: sanitizeValue, 1213 safeCampaignValue: safeCampaignValue, 1214 safeClickId: safeClickId, 1215 safeEntityId: safeEntityId, 1216 safeTargetId: safeTargetId, 1217 readAcquisition: readAcquisition, 1218 safeLandingPath: safeLandingPath, 1219 safeReferrerOrigin: safeReferrerOrigin, 1220 readTouchContext: readTouchContext, 1221 hasPaidSignal: hasPaidSignal, 1222 readStoredRecord: readStoredRecord, 1223 readStored: readStored, 1224 ageFilterRecord: ageFilterRecord, 1225 persistAcquisition: persistAcquisition, 1226 persistPaidTouches: persistPaidTouches, 1227 decorateHref: decorateHref, 1228 stripAttributionHref: stripAttributionHref, 1229 authoredAttributionHref: authoredAttributionHref, 1230 safeTrackedHref: safeTrackedHref, 1231 safeStorage: safeStorage, 1232 purgeAcquisition: purgeAcquisition, 1233 clearAttributionTargets: clearAttributionTargets, 1234 installCurrentPageHandoff: installCurrentPageHandoff, 1235 passthroughAttributionHref: passthroughAttributionHref, 1236 installUndecidedPassthrough: installUndecidedPassthrough, 1237 installAcquisition: installAcquisition, 1238 }; 1239});
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.