1(function () { 2 'use strict'; 3 4 var config = window.CinnamonAnalyticsFrontend || {}; 5 if (!config.eventsUrl || !config.tokenUrl) { 6 return; 7 } 8 9 var MAX_BATCH = Math.max(1, Math.min(20, Number(config.maxBatch) || 20)); 10 var conversionClasses = new Set(Array.isArray(config.conversionClasses) ? config.conversionClasses : []); 11 var browserEvents = new Set(Array.isArray(config.browserEvents) ? config.browserEvents : []); 12 var formStates = new WeakMap(); 13 var queue = []; 14 var flushTimer = 0; 15 var flushing = false; 16 var pendingKeepalive = false; 17 var token = typeof config.token === 'string' ? config.token : ''; 18 var tokenExpires = Number(config.tokenExpires) || 0; 19 var tokenPromise = null; 20 var sequence = 0; 21 // The live theme captures this exact global form collection when its bundle 22 // executes. Preserve that source-side distinction from the submitted form. 23 var legacyContactForms = Array.prototype.slice.call(document.querySelectorAll('div#contact.contact-container form.wpcf7-form')); 24 25 var ATTRIBUTION_KEYS = [ 26 'gclid', 'gbraid', 'wbraid', 'campaign', 'adgroup', 'keyword', 'infinity_vid', 27 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'ga_client_id' 28 ]; 29 var MARKETING_ATTRIBUTION_KEYS = [ 30 'gclid', 'gbraid', 'wbraid', 'campaign', 'adgroup', 'keyword', 'infinity_vid' 31 ]; 32 var STATISTICS_ATTRIBUTION_KEYS = [ 33 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'ga_client_id' 34 ]; 35 var gaClientId = ''; 36 var gaClientIdRequested = false; 37 var carehqGaEventIds = new Set(); 38 39 function validUuid(value) { 40 return typeof value === 'string' && /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i.test(value); 41 } 42 43 function uuid() { 44 if (window.crypto && typeof window.crypto.randomUUID === 'function') { 45 return window.crypto.randomUUID(); 46 } 47 var bytes = new Uint8Array(16); 48 if (window.crypto && typeof window.crypto.getRandomValues === 'function') { 49 window.crypto.getRandomValues(bytes); 50 } else { 51 for (var i = 0; i < bytes.length; i++) { 52 bytes[i] = Math.floor(Math.random() * 256); 53 } 54 } 55 bytes[6] = (bytes[6] & 15) | 64; 56 bytes[8] = (bytes[8] & 63) | 128; 57 var hex = Array.prototype.map.call(bytes, function (byte) { 58 return byte.toString(16).padStart(2, '0'); 59 }).join(''); 60 return hex.slice(0, 8) + '-' + hex.slice(8, 12) + '-' + hex.slice(12, 16) + '-' + hex.slice(16, 20) + '-' + hex.slice(20); 61 } 62 63 function storageGet(storage, key) { 64 try { 65 return storage ? storage.getItem(key) : null; 66 } catch (error) { 67 return null; 68 } 69 } 70 71 function storageSet(storage, key, value) { 72 try { 73 if (storage) { 74 storage.setItem(key, value); 75 } 76 } catch (error) { 77 // Storage can be unavailable in privacy modes; in-memory state remains valid. 78 } 79 } 80 81 function storageRemove(storage, key) { 82 try { 83 if (storage) { 84 storage.removeItem(key); 85 } 86 } catch (error) { 87 // Storage can be unavailable in privacy modes. 88 } 89 } 90 91 function storageObject(name) { 92 try { 93 return window[name] || null; 94 } catch (error) { 95 return null; 96 } 97 } 98 99 // Accessing the storage property itself can throw in sandboxed/private 100 // contexts, so resolve it once inside a guard before any collector work. 101 var sessionStore = storageObject('sessionStorage'); 102 var localStore = storageObject('localStorage'); 103 104 function pageUrl() { 105 try { 106 var url = new URL(window.location.href); 107 url.hash = ''; 108 url.search = ''; 109 url.username = ''; 110 url.password = ''; 111 return url.href.slice(0, 2048); 112 } catch (error) { 113 return ''; 114 } 115 } 116 117 function referrerUrl() { 118 try { 119 if (!document.referrer) { 120 return ''; 121 } 122 var url = new URL(document.referrer); 123 url.hash = ''; 124 url.search = ''; 125 url.username = ''; 126 url.password = ''; 127 return url.href.slice(0, 2048); 128 } catch (error) { 129 return ''; 130 } 131 } 132 133 function sessionUuid() { 134 var key = 'cinnamonAnalyticsSessionId'; 135 var current = consentSnapshot().statistics ? storageGet(sessionStore, key) : null; 136 if (!validUuid(current)) { 137 current = uuid(); 138 if (consentSnapshot().statistics) { 139 storageSet(sessionStore, key, current); 140 } 141 } 142 return current; 143 } 144 145 var sessionId = sessionUuid(); 146 var landingPage = ''; 147 var initialReferrer = null; 148 149 function consentSnapshot() { 150 var cookiebot = window.Cookiebot || null; 151 var consent = cookiebot && cookiebot.consent ? cookiebot.consent : null; 152 var known = !!(cookiebot && (cookiebot.hasResponse === true || cookiebot.consented === true || cookiebot.declined === true)); 153 if (!consent || !known) {
154 return { known: false, statistics: false, marketing: false, telemetry: false, source: 'unknown' }; 155 } 156 var statistics = consent.statistics === true; 157 var marketing = consent.marketing === true; 158 return { 159 known: true, 160 statistics: statistics, 161 marketing: marketing, 162 telemetry: statistics || marketing, 163 source: 'cookiebot' 164 }; 165 } 166 167 function initialiseStatisticsContext() { 168 if (!consentSnapshot().statistics) { 169 return false; 170 } 171 storageSet(sessionStore, 'cinnamonAnalyticsSessionId', sessionId); 172 if (!landingPage) { 173 landingPage = storageGet(sessionStore, 'cinnamonAnalyticsLandingPage') || pageUrl(); 174 storageSet(sessionStore, 'cinnamonAnalyticsLandingPage', landingPage); 175 } 176 if (initialReferrer === null) { 177 initialReferrer = storageGet(sessionStore, 'cinnamonAnalyticsInitialReferrer'); 178 if (initialReferrer === null) { 179 initialReferrer = referrerUrl(); 180 } 181 storageSet(sessionStore, 'cinnamonAnalyticsInitialReferrer', initialReferrer); 182 } 183 return true; 184 } 185 186 function integrationSnapshot() { 187 var bingTruthy = !!window.uetq; 188 var bingPush = !!(window.uetq && typeof window.uetq.push === 'function'); 189 return { 190 bingQueueTruthy: bingTruthy, 191 bingPush: bingPush, 192 bing: !bingTruthy || bingPush, 193 meta: typeof window.fbq === 'function', 194 infinity: typeof window._its === 'function' 195 }; 196 } 197 198 function legacyContactHomeSelected() { 199 for (var i = 0; i < legacyContactForms.length; i++) { 200 var select = legacyContactForms[i].querySelector('select[name="home-name"]'); 201 if (select) { 202 return String(select.value || '').trim() !== ''; 203 } 204 } 205 return false; 206 } 207 208 function namedElements(form, name) { 209 return Array.prototype.filter.call(form.querySelectorAll('[name]'), function (element) { 210 return element.getAttribute('name') === name; 211 }); 212 } 213 214 function fieldValue(form, name) { 215 if (!form || typeof form.querySelectorAll !== 'function') { 216 return ''; 217 } 218 var fields = namedElements(form, name); 219 if (!fields.length) { 220 return ''; 221 } 222 for (var i = 0; i < fields.length; i++) { 223 var field = fields[i]; 224 if ((field.type === 'checkbox' || field.type === 'radio') && !field.checked) { 225 continue; 226 } 227 if (String(field.value || '').trim() !== '') { 228 return String(field.value).trim().slice(0, 2048); 229 } 230 } 231 return ''; 232 } 233 234 function setHiddenField(form, name, value) { 235 value = value === null || typeof value === 'undefined' ? '' : String(value); 236 var fields = namedElements(form, name); 237 var field = fields.length ? fields[0] : null; 238 if (!field) { 239 field = document.createElement('input'); 240 field.type = 'hidden'; 241 field.name = name; 242 field.setAttribute('data-cinnamon-analytics-field', '1'); 243 form.appendChild(field); 244 } 245 field.value = value; 246 field.defaultValue = value; 247 field.setAttribute('value', value); 248 } 249 250 function removeManagedField(form, name) { 251 namedElements(form, name).forEach(function (field) { 252 if (field.getAttribute('data-cinnamon-analytics-field') === '1' && field.parentNode) { 253 field.parentNode.removeChild(field); 254 } 255 }); 256 } 257 258 function normaliseStoredValue(value) { 259 if (typeof value !== 'string') { 260 return ''; 261 } 262 value = value.trim(); 263 if (!value) { 264 return ''; 265 } 266 if (value.charAt(0) === '{') { 267 try { 268 var parsed = JSON.parse(value); 269 value = parsed && typeof parsed.value === 'string' ? parsed.value : ''; 270 } catch (error) { 271 return ''; 272 } 273 } 274 return String(value).trim().slice(0, 500); 275 } 276 277 function legacyStorageValue(key) { 278 var normalise = ['gclid', 'gbraid', 'wbraid'].indexOf(key) !== -1 ? normaliseClickId : normaliseStoredValue; 279 return normalise(storageGet(sessionStore, key)) || normalise(storageGet(localStore, key)); 280 } 281 282 function normaliseClickId(value) { 283 if (typeof value !== 'string' || value.length > 4096) { 284 return ''; 285 } 286 value = value.trim(); 287 if (value.charAt(0) === '{') { 288 try { 289 var record = JSON.parse(value); 290 if (!record || typeof record.value !== 'string' || typeof record.expiryDate !== 'number' || !Number.isFinite(record.expiryDate) || record.expiryDate <= Date.now()) { 291 return ''; 292 } 293 value = record.value.trim(); 294 } catch (error) { 295 return ''; 296 } 297 } 298 // Opaque URL-safe IDs only; never submit an object or a truncated token. 299 return /^[A-Za-z0-9._~-]{1,255}$/.test(value) ? value : ''; 300 } 301 302 function normaliseGaClientId(value) { 303 value = typeof value === 'string' || typeof value === 'number' ? String(value).trim() : ''; 304 return /^[A-Za-z0-9._-]{3,255}$/.test(value) ? value.slice(0, 255) : ''; 305 } 306 307 function gaCookieClientId() { 308 try { 309 var cookies = String(document.cookie || '').split(';'); 310 for (var i = 0; i < cookies.length; i++) { 311 var parts = cookies[i].trim().split('='); 312 if (parts.shift() !== '_ga') { 313 continue; 314 } 315 var value = decodeURIComponent(parts.join('=') || ''); 316 var segments = value.split('.'); 317 if (segments.length >= 4) { 318 return normaliseGaClientId(segments.slice(-2).join('.')); 319 } 320 } 321 } catch (error) { 322 return ''; 323 } 324 return ''; 325 } 326 327 function refreshManagedForms() { 328 Array.prototype.forEach.call(document.querySelectorAll('.wpcf7 > form, form.wpcf7-form'), function (form) { 329 var state = formStates.get(form); 330 if (state) { 331 refreshHiddenFields(form, state); 332 } 333 }); 334 } 335 336 function requestGaClientId() { 337 if (!consentSnapshot().statistics) { 338 return; 339 } 340 gaClientId = gaClientId || gaCookieClientId(); 341 if (gaClientId || gaClientIdRequested) { 342 return; 343 } 344 if (typeof window.gtag !== 'function' || !config.gaMeasurementId) { 345 return; 346 } 347 gaClientIdRequested = true; 348 try { 349 window.gtag('get', String(config.gaMeasurementId), 'client_id', function (value) { 350 var resolved = normaliseGaClientId(value); 351 if (!resolved || !consentSnapshot().statistics) { 352 gaClientIdRequested = false;
353 return; 354 } 355 gaClientId = resolved; 356 refreshManagedForms(); 357 }); 358 } catch (error) { 359 gaClientIdRequested = false; 360 // GA may be unavailable or consent-blocked; the cookie fallback remains optional. 361 } 362 } 363 364 function savedAttribution(storageKey) { 365 var value = storageGet(sessionStore, storageKey); 366 if (!value) { 367 return {}; 368 } 369 try { 370 var parsed = JSON.parse(value); 371 return parsed && typeof parsed === 'object' ? parsed : {}; 372 } catch (error) { 373 return {}; 374 } 375 } 376 377 function legacyPpcTracking() { 378 var raw = storageGet(sessionStore, 'ppcTracking') || storageGet(localStore, 'ppcTracking'); 379 if (!raw) { 380 return {}; 381 } 382 try { 383 var parsed = JSON.parse(raw); 384 return parsed && typeof parsed === 'object' ? parsed : {}; 385 } catch (error) { 386 return {}; 387 } 388 } 389 390 function pathAttribution() { 391 var match = String(window.location.href).match(/~cmp~(.*?)~ag~(.*?)~ar~.*?~kw~(.*?)(?:[/?#]|$)/i); 392 if (!match) { 393 return {}; 394 } 395 var decode = function (value) { 396 try { 397 return decodeURIComponent(value || '').slice(0, 500); 398 } catch (error) { 399 return String(value || '').slice(0, 500); 400 } 401 }; 402 return { campaign: decode(match[1]), adgroup: decode(match[2]), keyword: decode(match[3]) }; 403 } 404 405 function organicSearchSource() { 406 // Use the consented, original referrer, never the current internal page. 407 // These are source-side inferences, not GA4's computed session dimensions. 408 if (!initialiseStatisticsContext() || !initialReferrer) { 409 return ''; 410 } 411 try { 412 var referrer = new URL(initialReferrer); 413 if (referrer.protocol !== 'https:' && referrer.protocol !== 'http:') { 414 return ''; 415 } 416 var host = referrer.hostname.toLowerCase().replace(/\.$/, ''); 417 var googleDomains = [ 418 'google.com', 'google.co.uk', 'google.ie', 'google.fr', 'google.de', 419 'google.es', 'google.it', 'google.nl', 'google.be', 'google.pt', 420 'google.ch', 'google.at', 'google.dk', 'google.se', 'google.no', 421 'google.fi', 'google.pl', 'google.cz', 'google.co.in', 'google.com.au', 422 'google.co.nz', 'google.ca', 'google.co.za', 'google.co.jp', 'google.com.br' 423 ]; 424 var isGoogle = googleDomains.some(function (domain) { 425 return host === domain || host === 'www.' + domain || host === 'search.' + domain || host === 'encrypted.' + domain; 426 }); 427 if (isGoogle && /^\/(?:search|url|webhp)?\/?$/.test(referrer.pathname)) { 428 return 'google'; 429 } 430 if ((host === 'bing.com' || host === 'www.bing.com' || host === 'cn.bing.com') && /^\/(?:search)?\/?$/.test(referrer.pathname)) { 431 return 'bing'; 432 } 433 if (host === 'duckduckgo.com' || host === 'www.duckduckgo.com' || host === 'html.duckduckgo.com') { 434 return 'duckduckgo'; 435 } 436 if ((host === 'ecosia.org' || host === 'www.ecosia.org') && /^\/(?:search)?\/?$/.test(referrer.pathname)) { 437 return 'ecosia'; 438 } 439 if (host === 'search.yahoo.com' || /^[a-z]{2}\.search\.yahoo\.com$/.test(host)) { 440 return 'yahoo'; 441 } 442 if (host === 'search.brave.com') { 443 return 'brave'; 444 } 445 } catch (error) { 446 // Missing, malformed or unrecognised referrers stay unknown. 447 } 448 return ''; 449 } 450 451 function collectAttribution(form) { 452 var consent = consentSnapshot(); 453 if (!consent.telemetry) { 454 return {}; 455 } 456 var statisticsStored = consent.statistics ? savedAttribution('cinnamonAnalyticsStatisticsAttribution') : {}; 457 var marketingStored = consent.marketing ? savedAttribution('cinnamonAnalyticsMarketingAttribution') : {}; 458 var ppc = consent.marketing ? legacyPpcTracking() : {}; 459 var pathValues = consent.marketing ? pathAttribution() : {}; 460 var query = null; 461 try { 462 query = new URL(window.location.href).searchParams; 463 } catch (error) { 464 query = new URLSearchParams(); 465 } 466 var output = {}; 467 var statisticsOutput = {}; 468 var marketingOutput = {}; 469 if (consent.statistics) { 470 requestGaClientId(); 471 } 472 473 ATTRIBUTION_KEYS.forEach(function (key) { 474 var isMarketing = MARKETING_ATTRIBUTION_KEYS.indexOf(key) !== -1; 475 var isStatistics = STATISTICS_ATTRIBUTION_KEYS.indexOf(key) !== -1; 476 if ((!isMarketing && !isStatistics) || (isMarketing && !consent.marketing) || (isStatistics && !consent.statistics)) { 477 return; 478 } 479 var stored = isMarketing ? marketingStored : statisticsStored; 480 var current = key === 'ga_client_id' ? gaClientId : query.get(key); 481 if (isMarketing && (key === 'campaign' || key === 'adgroup' || key === 'keyword') && pathValues[key]) { 482 current = pathValues[key]; 483 } 484 var existing = fieldValue(form, key); 485 var fromPpc = isMarketing && ppc[key] ? String(ppc[key]).slice(0, 500) : ''; 486 var normalise = ['gclid', 'gbraid', 'wbraid'].indexOf(key) !== -1 ? normaliseClickId : normaliseStoredValue; 487 var value = normalise(current || '') || normalise(existing) || normalise(stored[
487key] || '') || normalise(fromPpc) || legacyStorageValue(key); 488 if (value) { 489 output[key] = value; 490 if (isMarketing) { 491 marketingOutput[key] = value; 492 } else { 493 statisticsOutput[key] = value; 494 } 495 } 496 }); 497 498 var hasPaidEvidence = !!(output.gclid || output.gbraid || output.wbraid || output.campaign || output.adgroup || output.keyword) || 499 ['gclid', 'gbraid', 'wbraid', 'msclkid', 'dclid'].some(function (key) { return query.has(key); }); 500 var campaignKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id', 'utm_source_platform']; 501 var hasExplicitCampaign = campaignKeys.some(function (key) { 502 return query.has(key) || !!fieldValue(form, key) || (key !== 'utm_source' && key !== 'utm_medium' && !!output[key]); 503 }); 504 var organicInferred = statisticsStored._organic_inferred === true; 505 // Unlike supplied campaign values, a previous organic fallback must yield 506 // if a subsequent page supplies stronger paid/campaign evidence. 507 if (organicInferred && (hasPaidEvidence || hasExplicitCampaign)) { 508 ['utm_source', 'utm_medium'].forEach(function (key) { 509 if (!normaliseStoredValue(query.get(key) || '') && !fieldValue(form, key) && !legacyStorageValue(key) && output[key] === statisticsStored[key]) { 510 delete output[key]; 511 delete statisticsOutput[key]; 512 } 513 }); 514 organicInferred = false; 515 } 516 517 // Google auto-tagging supplies an opaque click ID, not readable campaign 518 // metadata. Where both purposes are consented, make the source/medium 519 // explicit and reuse Cinnamon's legacy campaign value when it exists. 520 // Never invent a campaign name from GCLID/GBRAID/WBRAID. 521 if (consent.statistics && consent.marketing && (output.gclid || output.gbraid || output.wbraid)) { 522 if (!output.utm_source) { 523 output.utm_source = 'google'; 524 statisticsOutput.utm_source = 'google'; 525 } 526 if (!output.utm_medium) { 527 output.utm_medium = 'cpc'; 528 statisticsOutput.utm_medium = 'cpc'; 529 } 530 if (!output.utm_campaign && output.campaign) { 531 output.utm_campaign = output.campaign; 532 statisticsOutput.utm_campaign = output.campaign; 533 } 534 } 535 536 // Organic visits normally have no UTMs. Populate CareHQ's existing source 537 // and medium columns only when there is no explicit/session campaign or 538 // paid evidence. Do not mix a partial UTM campaign with inferred values. 539 // Checking URL key presence does not read or retain a denied click ID. 540 var hasCampaignEvidence = campaignKeys.some(function (key) { 541 return !!output[key] || query.has(key); 542 }); 543 if (consent.statistics && !hasPaidEvidence && !hasCampaignEvidence) { 544 var organicSource = organicSearchSource(); 545 if (organicSource) { 546 output.utm_source = organicSource; 547 output.utm_medium = 'organic'; 548 statisticsOutput.utm_source = organicSource; 549 statisticsOutput.utm_medium = 'organic'; 550 organicInferred = true; 551 } 552 } 553 554 if (consent.statistics) { 555 if (organicInferred && output.utm_medium === 'organic') { 556 // Private plugin-session provenance, not a submitted/CRM field. 557 statisticsOutput._organic_inferred = true; 558 } 559 storageSet(sessionStore, 'cinnamonAnalyticsStatisticsAttribution', JSON.stringify(statisticsOutput)); 560 } 561 if (consent.marketing) { 562 storageSet(sessionStore, 'cinnamonAnalyticsMarketingAttribution', JSON.stringify(marketingOutput)); 563 } 564 return output; 565 } 566 567 function capturePageAttribution() { 568 initialiseStatisticsContext(); 569 collectAttribution(null); 570 } 571 572 function fireCarehqCreatedGa4(form, detail) { 573 var apiResponse = detail && detail.apiResponse && typeof detail.apiResponse === 'object' ? detail.apiResponse : {}; 574 var marker = apiResponse.cinnamon_carehq; 575 if (!marker || marker.version !== 1 || marker.event !== 'carehq_enquiry_created' || marker.accepted !== true || marker.created !== true || !validUuid(marker.event_id)) { 576 return; 577 } 578 if (!consentSnapshot().statistics || typeof window.gtag !== 'function' || !config.gaMeasurementId || c
578arehqGaEventIds.has(marker.event_id)) { 579 return; 580 } 581 582 var formId = Number(detail.contactFormId) || Number(fieldValue(form, '_wpcf7')) || 0; 583 var classification = successClassificationInputs(formId, detail.inputs || []); 584 var attribution = collectAttribution(form); 585 var homeId = fieldValue(form, 'home-id') || String(Number(config.homeId) || ''); 586 var parameters = { 587 send_to: String(config.gaMeasurementId), 588 transport_type: 'beacon', 589 event_id: marker.event_id, 590 carehq_result: 'created', 591 form_id: String(formId) 592 }; 593 if (homeId) { 594 parameters.home_id = String(homeId).slice(0, 100); 595 } 596 if (classification.formType) { 597 parameters.enquiry_type = String(classification.formType).slice(0, 100); 598 } 599 ['utm_source', 'utm_medium', 'utm_campaign'].forEach(function (key) { 600 if (attribution[key]) { 601 parameters['cinnamon_' + key] = String(attribution[key]).slice(0, 100); 602 } 603 }); 604 605 carehqGaEventIds.add(marker.event_id); 606 try { 607 window.gtag('event', 'carehq_enquiry_created', parameters); 608 } catch (error) { 609 // GA4 availability must never affect the completed enquiry journey. 610 } 611 } 612 613 function formIdentity(form, index) { 614 var unitTag = fieldValue(form, '_wpcf7_unit_tag'); 615 var formId = fieldValue(form, '_wpcf7'); 616 return String(config.pageId || 0) + ':' + (unitTag || formId || String(index || 0)); 617 } 618 619 function findFormIndex(form) { 620 return Array.prototype.indexOf.call(document.querySelectorAll('.wpcf7 > form, form.wpcf7-form'), form); 621 } 622 623 function ensureFormState(form) { 624 if (!form || form.nodeType !== 1) { 625 return null; 626 } 627 var state = formStates.get(form); 628 if (!state) { 629 var key = 'cinnamonAnalyticsLogical:' + formIdentity(form, findFormIndex(form)); 630 var existing = fieldValue(form, '_cinnamon_analytics_submission_id'); 631 var logical = validUuid(existing) ? existing : (consentSnapshot().statistics ? storageGet(sessionStore, key) : null); 632 if (!validUuid(logical)) { 633 logical = uuid(); 634 } 635 state = { 636 logicalId: logical, 637 storageKey: key, 638 interactionLogged: false, 639 pendingRotate: false, 640 rotationTimer: 0 641 }; 642 formStates.set(form, state); 643 if (consentSnapshot().statistics) { 644 storageSet(sessionStore, key, logical); 645 } 646 } 647 refreshHiddenFields(form, state); 648 return state; 649 } 650 651 function refreshHiddenFields(form, state) { 652 var consent = consentSnapshot(); 653 // Explicit client-reported purpose flags let the server suppress legacy 654 // fallbacks when a category is denied or the CMP has no response yet. 655 setHiddenField(form, '_cinnamon_analytics_consent_statistics', consent.statistics ? '1' : '0'); 656 setHiddenField(form, '_cinnamon_analytics_consent_marketing', consent.marketing ? '1' : '0'); 657 setHiddenField(form, '_cinnamon_analytics_submission_id', state.logicalId); 658 setHiddenField(form, '_cinnamon_analytics_session_id', sessionId); 659 setHiddenField(form, '_cinnamon_analytics_page_id', String(Number(config.pageId) || 0)); 660 setHiddenField(form, '_cinnamon_analytics_submission_page', pageUrl()); 661 if (initialiseStatisticsContext()) { 662 setHiddenField(form, '_cinnamon_analytics_landing_page', String(landingPage || '').slice(0, 2048)); 663 setHiddenField(form, '_cinnamon_analytics_referrer', String(initialReferrer || '').slice(0, 2048)); 664 } else { 665 removeManagedField(form, '_cinnamon_analytics_landing_page'); 666 removeManagedField(form, '_cinnamon_analytics_referrer'); 667 } 668 var attribution = collectAttribution(form); 669 ATTRIBUTION_KEYS.forEach(function (key) { 670 var categoryAllowed = MARKETING_ATTRIBUTION_KEYS.indexOf(key) !== -1 ? consent.marketing : consent.statistics; 671 if (categoryAllowed) { 672 // Never modify a theme/CF7 field: doing so could change email, 673 // CareHQ or CF7 spam behavior. Evidence stays underscore-prefixed. 674 setHiddenField(form, '_cinnamon_analytics_attr_' + key, attribution[key] || ''); 675 } else { 676 removeManagedField(form, '_cinnamon_analytics_attr_' + key); 677 } 678 }); 679 } 680 681 function rotateLogicalSubmission(form, state) { 682 if (!state || !state.pendingRotate) { 683 return; 684 } 685 state.logicalId = uuid(); 686 state.interactionLogged = false;
687 state.pendingRotate = false; 688 if (consentSnapshot().statistics) { 689 storageSet(sessionStore, state.storageKey, state.logicalId); 690 } 691 refreshHiddenFields(form, state); 692 } 693 694 function formContext(form, state) { 695 var home = fieldValue(form, 'home-id') || fieldValue(form, 'home-name') || fieldValue(form, 'home-recruitment') || fieldValue(form, 'home-survey'); 696 var numericHome = /^\d+$/.test(home) ? Number(home) : Number(config.homeId) || 0; 697 return { 698 session_uuid: sessionId, 699 submission_uuid: state.logicalId, 700 form_id: Number(fieldValue(form, '_wpcf7')) || 0, 701 page_id: Number(config.pageId) || 0, 702 page_url: pageUrl(), 703 home_id: numericHome 704 }; 705 } 706 707 function inputMap(inputs) { 708 var mapped = {}; 709 (Array.isArray(inputs) ? inputs : []).forEach(function (item) { 710 if (item && typeof item.name === 'string' && typeof item.value !== 'object' && !Object.prototype.hasOwnProperty.call(mapped, item.name)) { 711 mapped[item.name] = String(item.value || '').slice(0, 500); 712 } 713 }); 714 return mapped; 715 } 716 717 function legacyInputValue(inputs, name, useLast) { 718 var found = ''; 719 (Array.isArray(inputs) ? inputs : []).some(function (item) { 720 if (!item || item.name !== name || typeof item.value === 'object') { 721 return false; 722 } 723 found = String(item.value || '').slice(0, 500); 724 return !useLast; 725 }); 726 return found; 727 } 728 729 function successClassificationInputs(formId, inputs) { 730 var values = inputMap(inputs); 731 var formTypeRaw = values['form-type'] || ''; 732 var formTypeValue = values['form-type-value'] || ''; 733 var enquiryTypeRaw = values['enquiry-type'] || ''; 734 var enquiryTypeValue = values['enquiry-type-value'] || ''; 735 var formType = formTypeValue || formTypeRaw; 736 var enquiryType = enquiryTypeValue || enquiryTypeRaw; 737 var homeSelected = String(values['home-name'] || '').trim() !== ''; 738 // The audited 34703 branch walks the full array (last matching value 739 // wins); the other legacy branches stop at the first form-type value. 740 var legacyFormType = legacyInputValue(inputs, 'form-type', formId === 34703); 741 var legacyEnquiryType = legacyInputValue(inputs, 'enquiry-type', true); 742 var legacyHomeSelected = legacyContactHomeSelected(); 743 744 return { 745 formType: formType, 746 formTypeRaw: formTypeRaw, 747 formTypeValue: formTypeValue, 748 enquiryType: enquiryType, 749 enquiryTypeRaw: enquiryTypeRaw, 750 enquiryTypeValue: enquiryTypeValue, 751 homeSelected: homeSelected, 752 legacyFormType: legacyFormType, 753 legacyEnquiryType: legacyEnquiryType, 754 legacyHomeSelected: legacyHomeSelected 755 }; 756 } 757 758 function buildEvent(form, state, kind, name, metadata) { 759 var context = formContext(form, state); 760 return { 761 id: uuid(), 762 kind: kind, 763 name: name, 764 occurred_at: Date.now(), 765 session_uuid: context.session_uuid, 766 submission_uuid: context.submission_uuid, 767 form_id: context.form_id, 768 page_id: context.page_id, 769 page_url: context.page_url, 770 home_id: context.home_id, 771 metadata: Object.assign({ 772 sequence: ++sequence, 773 unit_tag: fieldValue(form, '_wpcf7_unit_tag'), 774 container_post_id: Number(fieldValue(form, '_wpcf7_container_post')) || 0, 775 plugin_version: fieldValue(form, '_wpcf7_version') || String(config.pluginVersion || ''), 776 consent: consentSnapshot() 777 }, metadata || {}) 778 }; 779 } 780 781 function record(form, kind, name, metadata, consentRequirement) { 782 var consent = consentSnapshot(); 783 var consentDenied = consentRequirement === 'statistics' && !consent.statistics; 784 consentDenied = consentDenied || (consentRequirement === 'conversion' && !consent.statistics && !consent.marketing); 785 if (!form || consentDenied) { 786 return; 787 } 788 var state = ensureFormState(form); 789 if (!state) { 790 return; 791 } 792 queue.push({ payload: buildEvent(form, state, kind, name, metadata), retries: 0 }); 793 if (queue.length >= MAX_BATCH) { 794 flush(false); 795 } else { 796 scheduleFlush(); 797 } 798 } 799 800 function scheduleFlush() { 801 if (flushTimer) { 802 return; 803 } 804 flushTimer = window.setTimeout(function () { 805 flushTimer = 0; 806 flush(false); 807 }, 500); 808 } 809 810 function ensureToken(force) { 811 if (!force && token && tokenExpires > Math.floor(Date.now() / 1000) + 30) { 812 return Promise.resolve(token); 813 } 814 if (tokenPromise) { 815 return tokenPromise; 816 } 817 tokenPromise = window.fetch(config.tokenUrl, { 818 method: 'POST', 819 credentials: 'same-origin', 820 // Token requests are deliberately tiny and keepalive-safe so an 821 // unload-time drain is not lost while obtaining browser credentials. 822 keepalive: true, 823 headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, 824 body: JSON.stringify({ page_id: Number(config.pageId) || 0 }) 825 }).then(function (response) { 826 if (!response.ok) { 827 throw new Error('token'); 828 } 829 return response.json(); 830 }).then(function (data) { 831 if (!data || typeof data.token !== 'string') { 832 throw new Error('token'); 833 } 834 token = data.token; 835 tokenExpires = Number(data.expires) || 0; 836 return token; 837 }).finally(function () { 838 tokenPromise = null; 839 }); 840 return tokenPromise; 841 } 842 843 function sendBatch(batch, keepalive, refreshed) { 844 return ensureToken(!!refreshed).then(function (activeToken) { 845 return window.fetch(config.eventsUrl, { 846 method: 'POST', 847 credentials: 'same-origin', 848 // Every bounded collector request is keepalive-capable. This also 849 // protects an ordinary in-flight flush when pagehide races it. 850 keepalive: true, 851 headers: { 852 'Accept': 'application/json', 853 'Content-Type': 'application/json', 854 'X-Cinnamon-Analytics-Token': activeToken 855 }, 856 body: JSON.stringify({ token: activeToken, events: batch.map(function (item) { return item.payload; }) }) 857 }); 858 }).then(function (response) { 859 if ((response.status === 401 || response.status === 403) && !refreshed) { 860 token = ''; 861 tokenExpires = 0; 862 return sendBatch(batch, keepalive, true); 863 } 864 if (!response.ok) { 865 throw new Error('events'); 866 } 867 return response; 868 }); 869 } 870 871 function flush(keepalive) { 872 if (!queue.length) { 873 return; 874 } 875 if (flushing) { 876 pendingKeepalive = pendingKeepalive || !!keepalive; 877 return; 878 } 879 if (flushTimer) { 880 window.clearTimeout(flushTimer); 881 flushTimer = 0; 882 } 883 flushing = true; 884 var batch = queue.splice(0, MAX_BATCH); 885 sendBatch(batch, keepalive, false).catch(function () {
886 batch.reverse().forEach(function (item) { 887 if (item.retries < 2) { 888 item.retries++; 889 queue.unshift(item); 890 } 891 }); 892 }).finally(function () { 893 flushing = false; 894 var drainImmediately = pendingKeepalive || !!keepalive || document.visibilityState === 'hidden'; 895 pendingKeepalive = false; 896 if (queue.length) { 897 if (drainImmediately) { 898 flush(true); 899 } else { 900 scheduleFlush(); 901 } 902 } 903 }); 904 } 905 906 function initForms(root) { 907 var forms = []; 908 if (root && root.matches && root.matches('.wpcf7 > form, form.wpcf7-form')) { 909 forms.push(root); 910 } 911 if (root && root.querySelectorAll) { 912 forms = forms.concat(Array.prototype.slice.call(root.querySelectorAll('.wpcf7 > form, form.wpcf7-form'))); 913 } 914 forms.forEach(function (form) { 915 ensureFormState(form); 916 }); 917 } 918 919 function refreshFormsForConsent() { 920 var consent = consentSnapshot(); 921 // Remove the pre-split development key without importing it into a 922 // category whose current consent cannot be proven. 923 storageRemove(sessionStore, 'cinnamonAnalyticsAttribution'); 924 if (!consent.statistics) { 925 storageRemove(sessionStore, 'cinnamonAnalyticsSessionId'); 926 storageRemove(sessionStore, 'cinnamonAnalyticsLandingPage'); 927 storageRemove(sessionStore, 'cinnamonAnalyticsInitialReferrer'); 928 storageRemove(sessionStore, 'cinnamonAnalyticsStatisticsAttribution'); 929 landingPage = ''; 930 initialReferrer = null; 931 gaClientId = ''; 932 gaClientIdRequested = false; 933 } 934 if (!consent.marketing) { 935 storageRemove(sessionStore, 'cinnamonAnalyticsMarketingAttribution'); 936 } 937 capturePageAttribution(); 938 Array.prototype.forEach.call(document.querySelectorAll('.wpcf7 > form, form.wpcf7-form'), function (form) { 939 var state = ensureFormState(form); 940 if (!state) { 941 return; 942 } 943 if (!consent.statistics) { 944 storageRemove(sessionStore, state.storageKey); 945 } else { 946 storageSet(sessionStore, state.storageKey, state.logicalId); 947 } 948 refreshHiddenFields(form, state); 949 }); 950 } 951 952 // Capture phase runs before CF7's submit handler constructs FormData. This 953 // keeps correlation fields current for Enter-key and requestSubmit() paths. 954 document.addEventListener('submit', function (event) { 955 var form = event.target && event.target.matches && event.target.matches('.wpcf7 > form, form.wpcf7-form') ? event.target : null; 956 if (!form) { 957 return; 958 } 959 var state = ensureFormState(form); 960 if (state) { 961 refreshHiddenFields(form, state); 962 } 963 }, true); 964 965 document.addEventListener('focusin', function (event) { 966 var form = event.target && event.target.closest ? event.target.closest('.wpcf7 > form, form.wpcf7-form') : null; 967 if (!form) { 968 return; 969 } 970 var state = ensureFormState(form); 971 if (!state || state.interactionLogged || !consentSnapshot().statistics) { 972 return; 973 } 974 state.interactionLogged = true; 975 record(form, 'form_interaction', 'form_interaction_started', { 976 interaction: 'focusin', 977 element_tag: String(event.target.tagName || '').toLowerCase(), 978 element_type: String(event.target.type || '').toLowerCase(), 979 field_name: String(event.target.name || '').slice(0, 64) 980 }, 'statistics'); 981 }, true); 982 983 document.addEventListener('click', function (event) { 984 var element = event.target && event.target.closest ? event.target.closest('input, button, a') : null; 985 if (!element) { 986 return; 987 } 988 var matched = []; 989 conversionClasses.forEach(function (className) { 990 if (element.classList.contains(className)) { 991 matched.push(className); 992 } 993 }); 994 if (!matched.length) { 995 return; 996 } 997 var form = element.closest('.wpcf7 > form, form.wpcf7-form'); 998 if (!form) { 999 return; 1000 } 1001 matched.forEach(function (className) { 1002 record(form, 'conversion_click', className, { 1003 conversion_class: className, 1004 element_tag: String(element.tagName || '').toLowerCase(), 1005 element_type: String(element.type || '').toLowerCase() 1006 }, 'conversion'); 1007 }); 1008 }, true); 1009 1010 browserEvents.forEach(function (eventName) { 1011 document.addEventListener(eventName, function (event) { 1012 var form = event.target && event.target.matches && event.target.matches('form') ? event.target : (event.target && event.target.closest ? event.target.closest('form') : null); 1013 if (!form) { 1014 return; 1015 } 1016 var state = ensureFormState(form); 1017 if (!state) { 1018 return; 1019 } 1020 if (eventName === 'wpcf7beforesubmit') { 1021 refreshHiddenFields(form, state); 1022 } 1023 var detail = event.detail || {}; 1024 var metadata = { cf7_status: String(detail.status || '').toLowerCase().replace(/[^a-z0-9_]+/g, '_') }; 1025 1026 if (eventName === 'wpcf7mailsent') { 1027 var formId = Number(detail.contactFormId) || Number(fieldValue(form, '_wpcf7')) || 0; 1028 var classification = successClassificationInputs(formId, detail.inputs || []); 1029 var availability = integrationSnapshot(); 1030 metadata.bing_queue_available = availability.bingPush; 1031 metadata.bing_queue_truthy = availability.bingQueueTruthy; 1032 metadata.bing_push_available = availability.bingPush; 1033 metadata.meta_function_available = availability.meta; 1034 metadata.infinity_function_available = availability.infinity; 1035 metadata.form_type = classification.formType; 1036 metadata.form_type_raw = classification.formTypeRaw; 1037 metadata.form_type_value = classification.formTypeValue; 1038 metadata.enquiry_type = classification.enquiryType; 1039 metadata.enquiry_type_raw = classification.enquiryTypeRaw; 1040 metadata.enquiry_type_value = classification.enquiryTypeValue; 1041 metadata.home_selected = classification.homeSelected; 1042 metadata.legacy_form_type = classification.legacyFormType; 1043 metadata.legacy_enquiry_type = classification.legacyEnquiryType; 1044 metadata.legacy_home_selected = classification.legacyHomeSelected; 1045 fireCarehqCreatedGa4(form, detail); 1046 state.pendingRotate = true; 1047 window.clearTimeout(state.rotationTimer); 1048 state.rotationTimer = window.setTimeout(function () { 1049 rotateLogicalSubmission(form, state); 1050 }, 0); 1051 } 1052 1053 record(form, 'cf7_browser', eventName, metadata, eventName === 'wpcf7beforesubmit' ? 'statistics' : ''); 1054 1055 if (eventName === 'wpcf7submit' && state.pendingRotate) { 1056 window.clearTimeout(state.rotationTimer); 1057 state.rotationTimer = window.setTimeout(function () { 1058 rotateLogicalSubmission(form, state); 1059 }, 0); 1060 } 1061 }, true); 1062 }); 1063 1064 if (document.readyState === 'loading') { 1065 document.addEventListener('DOMContentLoaded', function () { 1066 capturePageAttribution(); 1067 initForms(document); 1068 }); 1069 } else { 1070 capturePageAttribution(); 1071 initForms(document); 1072 } 1073 1074 if (window.MutationObserver) { 1075 new MutationObserver(function (mutations) { 1076 mutations.forEach(function (mutation) { 1077 Array.prototype.forEach.call(mutation.addedNodes || [], function (node) { 1078 if (node && node.nodeType === 1) { 1079 initForms(node); 1080 } 1081 }); 1082 }); 1083 }).observe(document.documentElement, { childList: true, subtree: true }); 1084 } 1085 1086 ['CookiebotOnConsentReady', 'CookiebotOnAccept', 'CookiebotOnDecline'].forEach(function (eventName) { 1087 window.addEventListener(eventName, refreshFormsForConsent); 1088 }); 1089 1090 document.addEventListener('visibilitychange', function () { 1091 if (document.visibilityState === 'hidden') { 1092 flush(true); 1093 } 1094 }); 1095 window.addEventListener('pagehide', function () { flush(true); }); 1096})();
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.