1/** 2 * Detecting on click events 3 * @param {Event} event - The native click event being wrapped. 4 * @param {Element} target - Replacement analytics target for the proxy event. 5 */ 6function createTargetProxy(event, target) { 7 return new Proxy(event, { 8 get(obj, prop) { 9 return prop === 'target' ? target : obj[prop]; 10 } 11 }); 12} 13 14/** 15 * Normalize click targets so nested span clicks and tel links resolve to the 16 * anchor element used by the analytics handlers. 17 * @param {Event} event - Native click event. 18 * @returns {{target: Element|null, normalizedEvent: Event, telLink: HTMLAnchorElement|null}} 19 */ 20function normalizeClickTarget(event) { 21 let target = event.target; 22 let normalizedEvent = event; 23 24 if (target.tagName === 'SPAN') { 25 const anchorTag = target.closest('a'); 26 if (anchorTag) { 27 target = anchorTag; 28 normalizedEvent = createTargetProxy(event, anchorTag); 29 } 30 } 31 32 const telLink = target.closest && target.closest('a[href^="tel:"]'); 33 if (telLink) { 34 target = telLink; 35 normalizedEvent = createTargetProxy(event, telLink); 36 } 37 38 return { 39 target, 40 normalizedEvent, 41 telLink 42 }; 43} 44 45/** 46 * Safely return the analytics target from an event-like object. 47 * @param {Object} node - Native event or proxy event. 48 * @returns {Element|null} 49 */ 50function getAnalyticsTarget(node) { 51 if (!node || !node.target || typeof node.target.closest !== 'function') { 52 return null; 53 } 54 55 return node.target; 56} 57 58/** 59 * Read the analytics set-prop attribute from the current target, regardless of 60 * which legacy attribute name was used. 61 * @param {Element|null} target - Clicked analytics target. 62 * @returns {string} 63 */ 64function getEventSetProp(target) { 65 if (!target) return ''; 66 67 return target.getAttribute('data-ag-event-set-prop') 68 || target.getAttribute('data-aga-event-set-prop') 69 || target.getAttribute('data-sd-event-set-prop') 70 || target.getAttribute('data-sda-event-set-prop') 71 || ''; 72} 73 74/** 75 * Resolve the click-to-call department from explicit analytics attributes 76 * first, then fall back to link text/title heuristics. 77 * @param {Element|null} target - Clicked analytics target. 78 * @param {string} agEvent - Existing analytics event value. 79 * @returns {string} 80 */ 81function getClickToCallDepartment(target, agEvent) { 82 if (agEvent && agEvent.includes('clickToCall')) { 83 const eventParts = agEvent.split('|'); 84 if (eventParts[1]) { 85 return eventParts[1]; 86 } 87 } 88 89 const setProp = getEventSetProp(target); 90 if (setProp) { 91 const propParts = setProp.split('|'); 92 if (propParts[1]) { 93 return propParts[1]; 94 } 95 } 96 97 const linkText = ((target && (target.innerText || target.textContent)) || '').toLowerCase(); 98 const linkTitle = ((target && target.getAttribute('title')) || '').toLowerCase(); 99 const departmentText = linkText + ' ' + linkTitle; 100 101 if (departmentText.includes('service')) { 102 return 'Service'; 103 } 104 105 if (departmentText.includes('parts')) { 106 return 'Parts'; 107 } 108 109 return 'Sales'; 110} 111 112/** 113 * Determine which Shift Digital event should be emitted for the click based on 114 * explicit analytics metadata and global fallback link patterns. 115 * @param {string} agEvent - Existing analytics event value. 116 * @param {Element|null} target - Clicked analytics target. 117 * @param {HTMLAnchorElement|null} telLink - Resolved tel anchor, if any. 118 * @param {HTMLAnchorElement|null} googleMapsLink - Resolved Google Maps anchor, if any. 119 * @param {Element|null} scheduleServiceTrigger - Resolved schedule-service trigger, if any. 120 * @returns {string} 121 */ 122function resolveSDEvent(agEvent, target, telLink, googleMapsLink, scheduleServiceTrigger) { 123 if (telLink && (!agEvent || !agEvent.includes('clickToCall'))) { 124 return 'clickToCall|' + getClickToCallDepartment(target, agEvent); 125 } 126 127 if (typeof sd !== 'undefined' && googleMapsLink && (!agEvent || !agEvent.includes('getDirections'))) { 128 return 'getDirections'; 129 } 130 131 if (typeof sd !== 'undefined' && scheduleServiceTrigger && (!agEvent || !agEvent.includes('scheduleServiceClick'))) { 132 return 'scheduleServiceClick'; 133 } 134 135 return agEvent; 136} 137 138/** 139 * Determine whether an anchor points to a known Google Maps host used for 140 * directions links across the platform. 141 * @param {Element|null} target - Clicked analytics target. 142 * @returns {HTMLAnchorElement|null} 143 */ 144function getGoogleMapsLink(target) { 145 if (!target || typeof target.closest !== 'function') { 146 return null; 147 } 148 149 const anchor = target.closest('a[href]'); 150 if (!anchor || !anchor.href) { 151 return null; 152 } 153 154 try { 155 const url = new URL(anchor.href, window.location.origin); 156 const hostname = url.hostname.toLowerCase(); 157 const pathname = url.pathname.toLowerCase(); 158 159 if (hostname === 'maps.app.goo.gl' || hostname === 'maps.google.com') { 160 return anchor; 161 } 162 163 if ((hostname === 'www.google.com' || hostname === 'google.com') && pathname.startsWith('/maps')) { 164 return anchor; 165 } 166 } catch (error) { 167 return null; 168 } 169 170 return null; 171} 172 173/** 174 * Determine whether the clicked element is a schedule-service trigger, either 175 * through a direct service URL or the Toyota modal trigger class. 176 * @param {Element|null} target - Clicked analytics target. 177 * @returns {Element|null} 178 */ 179function getScheduleServiceTrigger(target) { 180 if (!target || typeof target.closest !== 'function') { 181 return null; 182 } 183 184 return target.closest('.catched-schedule-service-btn, a[href*="service/schedule-service"]'); 185} 186 187/** 188 * Send a simple Shift Digital event with only the event name in the dataLayer. 189 * @param {string} eventName - Shift Digital event name. 190 * @returns {void} 191 */ 192function sendSimpleSDEvent(eventName) { 193 sd('dataLayer', { 194 events: eventName 195 }); 196 sd('send'); 197} 198 199/** 200 * Track formFieldInteraction once per field element so a single field does not 201 * emit on every keystroke, while still allowing other fields in the same form 202 * to emit their own first interaction. 203 * @param {Element|null} fieldTarget - Interacted form field element. 204 * @returns {boolean} 205 */ 206function shouldTrackShiftDigitalFieldInteraction(fieldTarget) { 207 if (!fieldTarget || !fieldTarget.dataset) { 208 return false;
209 } 210 211 if (fieldTarget.dataset.sdFieldInteractionTracked === 'true') { 212 return false; 213 } 214 215 fieldTarget.dataset.sdFieldInteractionTracked = 'true'; 216 return true; 217} 218 219/** 220 * Normalize raw form field identifiers into Shift Digital's canonical 221 * formFieldName values so Gravity Forms field ids do not leak into payloads. 222 * @param {Element|null} fieldTarget - Interacted field element. 223 * @returns {string} 224 */ 225function getShiftDigitalFormFieldName(fieldTarget) { 226 if (!fieldTarget || typeof fieldTarget.getAttribute !== 'function') { 227 return 'other'; 228 } 229 230 const gfield = fieldTarget.closest ? fieldTarget.closest('.gfield') : null; 231 const fieldPart = fieldTarget.closest ? fieldTarget.closest('.name_first, .name_last, .name_middle, .ginput_left, .ginput_right, .ginput_full') : null; 232 const autocomplete = (fieldTarget.getAttribute('autocomplete') || '').toLowerCase(); 233 const signals = [ 234 autocomplete, 235 fieldTarget.getAttribute('aria-label') || '', 236 fieldPart && fieldPart.className ? fieldPart.className : '', 237 fieldPart && fieldPart.querySelector('label') ? fieldPart.querySelector('label').textContent : '', 238 gfield && gfield.querySelector('.gfield_label') ? gfield.querySelector('.gfield_label').textContent : '', 239 fieldTarget.getAttribute('placeholder') || '', 240 fieldTarget.getAttribute('name') || '', 241 fieldTarget.getAttribute('id') || '' 242 ].join(' ').toLowerCase(); 243 244 if (signals.includes('full name')) return 'firstname'; 245 if (autocomplete === 'given-name' || signals.includes('first')) return 'firstname'; 246 if (autocomplete === 'family-name' || signals.includes('last')) return 'lastname'; 247 if (autocomplete === 'additional-name' || signals.includes('middle')) return 'middlename'; 248 if (autocomplete === 'email' || signals.includes('mail')) return 'emailaddress'; 249 if (autocomplete === 'tel' || signals.includes('phone') || signals.includes('tel')) return 'phonenumber'; 250 if (autocomplete === 'postal-code' || signals.includes('zip') || signals.includes('postal')) return 'zipcode'; 251 if (autocomplete === 'address-level2' || signals.includes('city')) return 'city'; 252 if (autocomplete === 'address-level1' || signals.includes('state') || signals.includes('province') || signals.includes('region')) return 'state'; 253 if (autocomplete === 'street-address' || signals.includes('address') || signals.includes('street')) return 'address'; 254 if (signals.includes('comment') || signals.includes('message') || signals.includes('question') || signals.includes('notes')) return 'comments'; 255 256 return 'other'; 257} 258 259/** 260 * Send a Shift Digital form event with a shared payload shape so form event 261 * tracking stays consistent across static forms, quick forms, SRP, and VDP 262 * modal implementations. 263 * @param {Object} config - Shared Shift Digital form event configuration. 264 * @param {string} config.formType - Normalized Shift Digital form type. 265 * @param {string} config.displayType - Display type such as in-page or modal. 266 * @param {string} config.eventName - Shift Digital form event name. 267 * @param {string} [config.leadType] - Optional lead type when the sender has it. 268 * @param {Element} [config.fieldTarget] - Interacted form element for field-name extraction. 269 * @param {string}
269 [config.formFieldName] - Optional precomputed field name that skips extraction. 270 * @param {Object} [config.formVehicle] - Optional normalized vehicle payload. 271 * @returns {void} 272 */ 273function sendShiftDigitalFormEvent(config) { 274 if (typeof(sd) === "undefined") return; 275 276 const payload = { 277 formType: config.formType, 278 displayType: config.displayType, 279 events: config.eventName 280 }; 281 282 if (config.leadType) { 283 payload.leadType = config.leadType; 284 } 285 286 if (config.eventName === 'formFieldInteraction' && (config.formFieldName || config.fieldTarget)) { 287 payload.formFieldName = config.formFieldName || getShiftDigitalFormFieldName(config.fieldTarget); 288 } 289 290 if (config.formVehicle) { 291 payload.formVehicle = config.formVehicle; 292 } 293 294 sd('dataLayer', payload); 295 sd('send'); 296} 297 298/** 299 * Build the common Dealer Alchemist commerce payload shared by multiple CTA 300 * event builders. 301 * @param {Object} context - Normalized analytics context values. 302 * @returns {Object} 303 */ 304function buildAutoGoBaseArgs(context) { 305 return { 306 'item_id': context.vehicleId, 307 'item_number': context.vehicleStockNum, 308 'item_price': context.vehiclePrice, 309 'item_condition': context.vehicleCondition, 310 'item_year': context.vehicleYear, 311 'item_make': context.vehicleMake, 312 'item_model': context.vehicleModel, 313 'item_variant': context.vehicleVariant, 314 'item_color': context.vehicleColor, 315 'item_type': context.vehicleType, 316 'item_category': context.vehicleCategory, 317 'item_fuel_type': context.vehicleFuelType, 318 'item_inventory_date': context.vehicleInvDate, 319 }; 320} 321 322/** 323 * Build the Dealer Alchemist payload for team member vCard/media interactions. 324 * @param {Element|null} analyticsTarget - Clicked analytics target. 325 * @param {Object} context - Normalized analytics context values. 326 * @returns {Object} 327 */ 328function buildTeamContactArgs(analyticsTarget, context) { 329 const teamContactBtn = analyticsTarget ? analyticsTarget.closest('.dv-team-member-contact-card') : null; 330 if (!teamContactBtn) { 331 return {}; 332 } 333 334 return { 335 'event': 'asc_media_interaction', 336 'event_owner': 'DealerAlchemist', 337 'page_type': 'contact', 338 'event_action_result': 'download', 339 'media_type': 'vcard', 340 'affiliation': 'DealerAlchemist', 341 'link_url': teamContactBtn.getAttribute('href'), 342 'creative_name': teamContactBtn.getAttribute('title'), 343 'contact_name': teamContactBtn.getAttribute('data-contactname'), 344 'contact_team': teamContactBtn.getAttribute('data-contactteam'), 345 'contact_phone': teamContactBtn.getAttribute('data-contactphone'), 346 ...buildAutoGoBaseArgs(context), 347 }; 348} 349 350/** 351 * Build the Dealer Alchemist payload for team member email interactions. 352 * @param {Element|null} analyticsTarget - Clicked analytics target. 353 * @param {string} eventValue - Parsed department or event qualifier. 354 * @param {Object} context - Normalized analytics context values. 355 * @returns {Object} 356 */ 357function buildClickToEmailArgs(analyticsTarget, eventValue, context) { 358 const teamContactBtn = analyticsTarget ? analyticsTarget.closest('.dv-team-member-email') : null; 359 if (!teamContactBtn) { 360 return {}; 361 } 362 363 return { 364 'event': 'asc_click_to_email', 365 'event_owner': 'DealerAlchemist', 366 'page_type': window.location.pathname, 367 'department': eventValue, 368 'affiliation': 'DealerAlchemist', 369 'contact_name': teamContactBtn.getAttribute('data-contactname'), 370 'contact_team': teamContactBtn.getAttribute('data-contactteam'), 371 'contact_phone': teamContactBtn.getAttribute('data-contactphone'), 372 'contact_email': teamContactBtn.getAttribute('data-contactemail'), 373 ...buildAutoGoBaseArgs(context), 374 }; 375} 376 377function dispatchClickAnalytics(clickTarget, clickNode) { 378 let target = clickTarget; 379 let setnode = clickNode; 380 381 setTimeout(function () { 382 const telLink = target && typeof target.closest === 'function' ? target.closest('a[href^="tel:"]') : null; 383 const googleMapsLink = getGoogleMapsLink(target); 384 const scheduleServiceTrigger = getScheduleServiceTrigger(target); 385 let agEvent = target.getAttribute('data-ag-event') || target.getAttribute('data-aga-event') || target.getAttribute('data-sd-event') || target.getAttribute('data-sda-event'); 386 387 if (!agEvent && !telLink && !(typeof sd !== 'undefined' && googleMapsLink) && !(typeof sd !== 'undefined' && scheduleServiceTrigger)) return; 388 389 if (googleMapsLink) { 390 target = googleMapsLink; 391 setnode = createTargetProxy(clickNode, googleMapsLink); 392 } 393 394 if (scheduleServiceTrigger) { 395 target = scheduleServiceTrigger; 396 setnode = createTargetProxy(clickNode, scheduleServiceTrigger); 397 } 398 399 const sdEvent = resolveSDEvent(agEvent, target, telLink, googleMapsLink, scheduleServiceTrigger); 400 401 console.log(sdEvent, 'chromecdp'); 402 403 if (sdEvent) { 404 RunSDAnalytics(sdEvent, setnode); 405 } 406 407 if (agEvent) { 408 RunAutoGOAnalytics(agEvent, setnode); 409 } else if (sdEvent) { 410 RunAutoGOAnalytics(sdEvent, setnode); 411 } 412 }, 0); 413} 414 415window.addEventListener('click', function (event) { 416 const initialNormalizedClick = normalizeClickTarget(event); 417 dispatchClickAnalytics(initialNormalizedClick.target, initialNormalizedClick.normalizedEvent); 418}, true); 419 420/** 421 * Send AG Analytics with the event object 422 * @param {Object} ev - The event attribute object 423 * @param {Object} node - The event object 424 */ 425function RunAutoGOAnalytics(ev, node){ 426 // send the code to GA4 427 window.dataLayer = window.dataLayer || []; 428 let analyticsTarget = getAnalyticsTarget(node); 429 let vehicleId = ''; 430 let vehicleVin = ''; 431 let vehicleYear = ''; 432 let vehicleMake = ''; 433 let vehicleModel = ''; 434 let vehicleStockNum = ''; 435 let vehiclePrice = ''; 436 let vehicleDiscount = ''; 437 let vehicleCondition = ''; 438 let vehicleVariant = ''; 439 let vehicleColor = ''; 440 let vehicleType = ''; 441 let vehicleCategory = ''; 442 let vehicleFuelType = ''; 443 let vehicleInvDate = ''; 444 let dealerName = ''; 445 let analyticsContext = { 446 vehicleId, 447 vehicleVin, 448 vehicleYear, 449 vehicleMake, 450 vehicleModel, 451 vehicleStockNum, 452 vehiclePrice, 453 vehicleDiscount, 454 vehicleCondition, 455 vehicleVariant, 456 vehicleColor, 457 vehicleType, 458 vehicleCategory, 459 vehicleFuelType, 460 vehicleInvDate, 461 dealerName 462 }; 463 464 let data = ev.split("|"); 465 let eventName = data[0]; 466 let eventValue = data[1]; 467 468 if (ev.includes('teamContact')){ 469 let args = buildTeamContactArgs(analyticsTarget, analyticsContext); 470 if (isEmpty(args) === false){ 471 window.dataLayer.push(args); 472 } 473 }; 474 475 // GA4 dataLayer push for clickToCall intentionally removed from here (was 476 // buildClickToCallArgs(), scoped to .dv-team-member-phone only): it duplicated 477 // EventsManager's own click-to-call tracking (components/EventsManager/js/GA4/ 478 // modules/platform.js, clickToCall()), which fires for every tel: link on the 479 // page including team-member phone cards, and now carries the same 480 // contact_name/contact_team/contact_phone fields this used to add here. 481 // Shift Digital's click-to-call dispatch (RunSDAnalytics(), below) is 482 // untouched â EventsManager doesn't send anything to Shift Digital. 483 484 if (ev.includes('clickToEmail')) { 485 let args = buildClickToEmailArgs(analyticsTarget, eventValue, analyticsContext); 486 if (isEmpty(args) === false){ 487 window.dataLayer.push(args); 488 } 489 }; 490 491 492 493 // if (ev.includes('getDirections')) { 494 // console.log('send getdirections event'); 495 // }; 496}; 497 498/** 499 * Send SD Analytics with the event object 500 * @param {Object} ev - The event attribute 501 * @param {Object} node - The event object 502 */ 503function RunSDAnalytics(ev, node){ 504 // Check if SD is undefined 505 if (typeof(sd) === "undefined") return; 506 507 const analyticsTarget = getAnalyticsTarget(node); 508 if (!analyticsTarget) return; 509 510 let data = ev.split("|"); 511 let eventName = data[0]; 512 let eventValue = data[1]; 513 514 if(!eventValue){ 515 let attributeValue = analyticsTarget.getAttribute('data-ag-event-set-prop') || analyticsTarget.getAttribute('data-aga-event-set-prop') || analyticsTarget.getAttribute('data-sd-event-set-prop') || analyticsTarget.getAttribute('data-sda-event-set-prop'); 516 if (attributeValue) { 517 data = attributeValue.split("|"); 518 eventName = data[0]; 519 eventValue = data[1]; 520 } else if (!ev.includes('getDirections') && !ev.includes('scheduleServiceClick')) { 521 return; 522 } 523 } 524 525 526 if (ev.includes('clickToCall')) { 527 sd('dataLayer',{ 528 events: eventName, 529 clickToCallDepartment: eventValue, 530 }); 531 sd('send'); 532 } 533 534 if (ev.includes('getDirections')) { 535 sendSimpleSDEvent(eventName); 536 } 537 538 if (ev.includes('scheduleServiceClick')) { 539 sendSimpleSDEvent(eventName); 540 } 541 542}; 543 544/** 545 * Iteriate over object to see if its empty 546 * @param {Object} ev - object 547 */ 548function isEmpty(obj) { 549 for(var prop in obj) { 550 if(Object.prototype.hasOwnProperty.call(obj, prop)) { 551 return false;
552 } 553 } 554 return JSON.stringify(obj) === JSON.stringify({}); 555};
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.