PageSourceSearch

https://www.purinaproclub.com/themes/ssb/build/js/analyticsEvents.js?tlxaag

js purinaproclub.com collected 2026-10-02 10:09:48 UTC 18,714 bytes, 534 lines download raw bytes

1/* eslint-disable no-console */
2
3// click attributes events to check against
4const analyticsClickEventType = [
5  'module_interaction',
6  'nav_click',
7  'search',
8  'select_content',
9  'pagination',
10  'filter_applied',
11  'share',
12  'sweeps_email_submit',
13  'sweeps_email_code_submit',
14  'sweeps_final_submission',
15  'sweeps_photo_click',
16  'sweeps_video_click',
17  'sweeps_form_error',
18];
19// focus attributes events to check against
20const analyticsFocusEventType = [
21  'search_start',
22];
23
24const sweepsStepEventTypes = [
25  'sweeps_email_submit',
26  'sweeps_email_code_submit',
27  'sweeps_final_submission',
28];
29
30const sweepsErrorFeatureMap = {
31  registration: 'Initial Email Submit',
32  email_confirmation: 'Email Code Submit',
33  data_collection: 'Final Submit',
34};
35
36const sweepsNextStepKeyMap = {
37  registration: 'email_confirmation',
38  email_confirmation: 'data_collection',
39};
40const sweepsTrackedFormIds = ['primary_entry', 'secondary_entry'];
41
42let pendingSweepsStepEvent = null;
43const processedSweepsValidationRequests = new Set();
44
45const SWEEPS_ENTRY_FORM_SELECTOR = 'form[id^="webform-submission-primary-entry-"], form[id^="webform-submission-secondary-entry-"]';
46
47/**
48 * Gets the sweeps entry form element.
49 *
50 * @param {ParentNode|Element|Document} [root=document]
51 *   Optional root to scope the search to.
52 *
53 * @return {HTMLFormElement|null}
54 *   The sweeps form if found, otherwise null.
55 */
56function getSweepsEntryForm(root = document) {
57  if (typeof root.matches === 'function' && root.matches(SWEEPS_ENTRY_FORM_SELECTOR)) {
58    return root;
59  }
60
61  if (typeof root.querySelector === 'function') {
62    return root.querySelector(SWEEPS_ENTRY_FORM_SELECTOR);
63  }
64
65  return null;
66}
67
68/**
69 * Gets the current sweeps webform page key from the DOM.
70 *
71 * @param {ParentNode|Element|Document} [root=document]
72 *   Optional root to scope the search to.
73 *
74 * @return {string}
75 *   The page key (for example, registration) or an empty string.
76 */
77function getCurrentSweepsWebformKey(root = document) {
78  const sweepsForm = getSweepsEntryForm(root);
79  if (!sweepsForm) {
80    if (testAnalytics) {
81      console.log('[Sweeps Step Detection] No sweeps form found');
82    }
83    return '';
84  }
85
86  const pageElement = sweepsForm.querySelector('[data-webform-key]');
87  const webformKey = pageElement?.getAttribute('data-webform-key') || '';
88  if (testAnalytics) {
89    console.log('[Sweeps Step Detection] Found form, webform-key:', webformKey);
90  }
91  return webformKey;
92}
93
94/**
95 * Gets the sweeps webform id from a form element.
96 *
97 * @param {HTMLFormElement|null} form
98 *   The form element.
99 *
100 * @return {string}
101 *   The webform id (for example, primary_entry) or an empty string.
102 */
103function getSweepsFormId(form) {
104  if (!form || !form.id) {
105    return '';
106  }
107
108  if (form.id.startsWith('webform-submission-primary-entry-')) {
109    return 'primary_entry';
110  }
111
112  if (form.id.startsWith('webform-submission-secondary-entry-')) {
113    return 'secondary_entry';
114  }
115
116  return '';
117}
118
119/**
120 * Removes sweep wizard hash fragments from the browser URL for analytics.
121 *
122 * Some sweeps submissions keep a #enter... hash in the URL (for example
123 * #enter?page=2). Remove only that hash pattern so GTM/page_location does not
124 * include the wizard anchor on final submission events.
125 */
126function removeSweepsWizardHashForAnalytics() {
127  const hash = window.location.hash || '';
128  if (!hash) {
129    return;
130  }
131
132  // Keep only page=<value> from hash query params (for example #enter?page=4)
133  // so analytics emits a canonical final-submission URL.
134  const url = new URL(window.location.href);
135  const hashQueryIndex = hash.indexOf('?');
136  let pageValue = '';
137  if (hashQueryIndex !== -1) {
138    const hashParams = new URLSearchParams(hash.substring(hashQueryIndex + 1));
139    pageValue = hashParams.get('page') || '';
140  }
141
142  url.search = '';
143  if (pageValue) {
144    url.searchParams.set('page', pageValue);
145  }
146
147  url.hash = '';
148  const cleanUrl = url.pathname + (url.search ? url.search : '');
149  window.history.replaceState(window.history.state, document.title, cleanUrl);
150
151  if (testAnalytics) {
152    console.log('[Sweeps Step Analytics] Removed sweeps hash from URL:', hash);
153  }
154}
155
156// Target only the global search input, can extend if other inputs need focus events in future
157const headerSearchInput = document.querySelector('.headerSearch');
158
159// Feature flag to enable console logging of dataLayer.push events
160const urlParams = new URLSearchParams(window.location.search);
161const testAnalytics = urlParams.has('testAnalytics') &&
162  (window.location.hostname.endsWith('-ssb-demo.pantheonsite.io') ||
163  window.location.hostname.endsWith('.ddev.site') ||
164  /^pr-\d{3,}.+\.pantheonsite\.io$/.test(window.location.hostname));
165
166
167/**
168 * Pushes one sweeps validation error event.
169 *
170 * @param {string} step
171 *   Wizard step key.
172 * @param {{field?: string, code?: string, name?: string}} error
173 *   Structured validation error payload from drupalSettings.
174 */
175function pushSweepsValidationEvent(step, error) {
176  const eventParams = {
177    form_name: 'Sweeps: ' + document.title,
178    error_feature: sweepsErrorFeatureMap[step] || 'Unknown Step',
179    error_code: error.code || 'VALIDATION_ERROR',
180    error_name: error.name || 'Validation Error',
181    error_field: error.field || 'form',
182  };
183
184  dataLayer.push({
185    'event': 'sweeps_form_error',
186    'event_params': eventParams,
187  }, { event_params: undefined });
188
189  if (testAnalytics) {
190    console.log('[Sweeps Validation Analytics] dataLayer.push:', {
191      'event': 'sweeps_form_error',
192      'event_params': eventParams,
193    });
194  }
195}
196
197// Similar to document clicks, on focus grab the eventType from the event target, check it,
198// and then push eventType to data layer, no params needed
199if (headerSearchInput) {
200  headerSearchInput.addEventListener('focus', (event) => {
201    let searchInput = event.target;
202    if (analyticsFocusEventType.includes(searchInput.dataset.event)) {
203      let eventType = searchInput.dataset.event;
204      dataLayer.push({
205        'event': eventType,
206      });
207      if (testAnalytics) {
208        console.log('Event disabled due to logging being enabled.');
209        console.log('dataLayer.push:', { 'event': eventType });
210      }
211    }
212  });
213};
214// Fast filter form event listener to gather all form values and push to dataLayer
215document.addEventListener('DOMContentLoaded', function() {
216  // Once content is loaded, find all fastFilter forms on the page, there can be more than one
217  const forms = document.querySelectorAll('.fastFilterForm');
218  // Iterate over each form and attach a submit event listener
219  forms.forEach((form) => {
220    form.addEventListener('submit', (event) => {
221      if (testAnalytics) {
222        console.log('Form submit event disabled due to logging being enabled.');
223        event.preventDefault();
224      }
225      // find all the form elements
226      const elements = form.elements;
227      // New object to push filter values to
228      const formValues = {};
229      // New object to push individual filter values into
230      const individualFilters = {};
231      // Create an empty array to push selected filter values into
232      const selectedFilterValues = [];
233      // set the filter category to fast_filter_form
234      formValues['filter_category'] = 'fast_filter_form';
235      // Loop through all elements in form
236      for (const element of elements) {
237        // Get url of form and add to formValues object
238        if (element.name === 'uri') {
239          formValues['filter_url'] = element.value;
240        }
241        // get all elements with data-filter_value items in form and add text from each to selectedFilterValues array to combine into comma sperated filter_values
242        if (element.dataset.filter_value) {
243          selectedFilterValues.push(element.options[element.selectedIndex].innerText);
244          // pass each data filter value items to individual filters object to combine with comma separated filter_values parameter
245          individualFilters[element.dataset.filter_value] = element.options[element.selectedIndex].innerText;
246        }
247        // Nested loop to find all select elements and push their values to the dataLayer individually for reporting
248        if (element.tagName === 'SELECT') {
249          const filterSelectValue = element.options[element.selectedIndex].innerText;
250          dataLayer.push({
251            'event': 'filter_applied',
252            'event_params': {
253              'filter_value': filterSelectValue,
254            },
255          }, {event_params: undefined});
256          if (testAnalytics) {
257            console.log('dataLayer.push:', {
258              'event': 'filter_applied',
259              'event_params': { 'filter_value': filterSelectValue },
260            });
261          }
262        }
263      }
264      // Combine all form select values into one comma separated value in filter_values in event params
265      formValues['filter_values'] = selectedFilterValues.join(', ');
266      // Combine all form values and individual filters into one object to send to event params
267      const fastFilterEventParams = { ...formValues, ...individualFilters };
268      dataLayer.push({
269        'event': 'filter_applied',
270        'event_params': fastFilterEventParams,
271      }, {event_params: undefined});
272      if (testAnalytics) {
273        console.log('dataLayer.push:', {
274          'event': 'filter_applied',
275          'event_params': fastFilterEventParams,
276        });
277      }
278    });
279  });
280
281});
282// Fires event on click only if the data-event attribute is present and matches the attributes listed in the analyticsClickEventType array
283// Uses capture phase (3rd arg = true) so the event is caught before Drupal's AJAX handler calls stopPropagation()
284document.addEventListener('click', (event) => {
285  // pushing node.type from analyticsVars pageType in html.html.twig to window to grab and use here to add to
286  // filtered parameters for component links
287  const pageType = window.drupalPageType;
288  // set trigger to the event target
289  let trigger = event.target;
290  // check against analyticsClickEventType array and if the trigger has a data-event attribute, if it doesn't, bubble up to parentElement to find the data-event attribute
291  while (trigger && !analyticsClickEventType.includes(trigger.dataset.event)) {
292    trigger = trigger.parentElement;
293  }
294  // if trigger is null, return out of the function
295  if (trigger === null) {
296    return;
297  }
298  if (testAnalytics) {
299    // Don't block photo/video upload clicks — the AJAX upload must complete
300    // for the upload success events to fire.
301    const allowThrough = [...sweepsStepEventTypes, 'sweeps_photo_click', 'sweeps_video_click'];
302    if (!allowThrough.includes(trigger.dataset.event)) {
303      console.log('Event disabled due to logging being enabled.');
304      event.preventDefault();
305    }
306  }
307  // if trigger has a data-event attribute and it matches the analyticsClickEventType array, set the data
308  if (trigger && analyticsClickEventType.includes(trigger.dataset.event)) {
309    let trackingData = trigger.dataset;
310    // Pluck the 'event' from the data set and use for the dataLayer event
311    let eventType = trackingData.event;
312    // define empty eventParams object to assign filtered parameters
313    let eventParams = {};
314    // add all tracking data to object allParams to filter out eventType so it doesn't get sent with 'event_params'
315    let allParams = {...trackingData};
316    // remove 'event' type from allParams object and assign rest of values to new filteredParams object to send to the 'event_params'
317    // in dataLayer push event
318    let { event: _, ...filteredParams } = allParams;
319
320    // if a standard component module interaction, add the page type with the pageType variable taken from the window to filteredParams object
321    // otherwise assume a nav_click interaction and therefore no need to add page type since global
322    if (eventType === 'module_interaction' || eventType === 'select_content') {
323      eventParams = { ...filteredParams, page_type: pageType };
324    } else if (eventType.startsWith('sweeps_')) {
325      // For sweeps forms, submit the form name param.
326      eventParams = { ...filteredParams, form_name: `Sweeps: ${document.title}` };
327    } else {
328      eventParams = {...filteredParams};
329    }
330
331    if (sweepsStepEventTypes.includes(eventType)) {
332      pendingSweepsStepEvent = {
333        eventType,
334        eventParams,
335        sourceStepKey: getCurrentSweepsWebformKey(),
336      };
337      if (testAnalytics) {
338        console.log('[Sweeps Step Analytics] Pending step event:', pendingSweepsStepEvent);
339      }
340      return;
341    }
342
343    // if search form is submitted, let's get the input value that was entered to the search form and add with the event
344    if (eventType === 'search') {
345      let searchInput = trigger.parentElement.querySelector('input');
346      let searchInputValue = searchInput.value;
347      dataLayer.push({
348        'event': eventType,
349        'event_params': {
350          'search_term': searchInputValue,
351        },
352      }, {event_params: undefined});
353      if (testAnalytics) {
354        console.log('dataLayer.push:', {
355          'event': eventType,
356          'event_params': { search_term: searchInputValue },
357        });
358      }
359    } else {
360      // push the eventType and filtered eventParams to GTM and then set as undefined
361      dataLayer.push({
362        'event': eventType,
363        'event_params': eventParams
364      }, {event_params: undefined});
365      if (testAnalytics) {
366        console.log('dataLayer.push:', {
367          'event': eventType,
368          'event_params': eventParams,
369        });
370      }
371    }
372  }
373}, true);
374
375// Consume structured validation data from drupalSettings attached by server-side
376// validation callbacks and emit GA4 error events.
377Drupal.behaviors.ssbSweepsValidationAnalytics = {
378  attach: function(context, settings) {
379    const validation = settings.ssbSweeps?.validation;
380
381    if (!validation || !validation.requestId) {
382      return;
383    }
384
385    if (validation.formId && !sweepsTrackedFormIds.includes(validation.formId)) {
386      return;
387    }
388
389    if (processedSweepsValidationRequests.has(validation.requestId)) {
390      return;
391    }
392    processedSweepsValidationRequests.add(validation.requestId);
393
394    // Validation errors must suppress pending step-success analytics.
395    pendingSweepsStepEvent = null;
396
397    // errors is emitted as a JSON string to avoid Drupal's AJAX deep-merge
398    // accumulating stale array entries.
399    let errors = [];
400    if (typeof validation.errors === 'string') {
401      try {
402        const parsed = JSON.parse(validation.errors || '[]');
403        errors = Array.isArray(parsed) ? parsed : [];
404      }
405      catch (parseError) {
406        if (testAnalytics) {
407          console.warn('[Sweeps Validation Analytics] Invalid validation.errors payload', parseError);
408        }
409        errors = [];
410      }
411    }
412
413    errors.forEach((error) => {
414      pushSweepsValidationEvent(validation.step || '', error || {});
415    });
416  },
417};
418
419// Listen for successful file/video uploads on sweeps forms.
420// Uses Drupal.behaviors so it runs automatically after every AJAX response
421// (e.g., wizard page navigation, file widget replacement after upload).
422// When Drupal replaces the managed file widget after upload, the new element
423// arrives with fids already populated — we detect that and fire the event.
424Drupal.behaviors.sweepsUploadAnalytics = {
425  processed: new WeakSet(),
426  attach: function (context) {
427    const elements = (context || document).querySelectorAll('[data-upload-event]');
428    elements.forEach((el) => {
429      if (this.processed.has(el)) return;
430      this.processed.add(el);
431
432      const fidsInput = el.querySelector('input[name$="[fids]"]');
433      if (fidsInput && fidsInput.value) {
434        const uploadEvent = el.dataset.uploadEvent;
435        const eventParams = { form_name: 'Sweeps: ' + document.title };
436        dataLayer.push({
437          'event': uploadEvent,
438          'event_params': eventParams,
439        }, {event_params: undefined});
440        if (testAnalytics) {
441          console.log('dataLayer.push:', {
442            'event': uploadEvent,
443            'event_params': eventParams,
444          });
445        }
446      }
447    });
448  },
449};
450
451// Fire deferred sweeps step events only after the next AJAX response succeeds
452// without rendering validation errors.
453Drupal.behaviors.sweepsStepCompletionAnalytics = {
454  processedConfirmations: new WeakSet(),
455  attach: function(context) {
456    if (!pendingSweepsStepEvent) {
457      return;
458    }
459
460    const { eventType, eventParams, sourceStepKey } = pendingSweepsStepEvent;
461
462    // Final submission only fires when confirmation appears, and only after
463    // two animation frames so the success state is fully rendered.
464    if (eventType === 'sweeps_final_submission') {
465      const root = context || document;
466      const confirmationElement = root.querySelector('.webform-confirmation');
467      if (!confirmationElement || this.processedConfirmations.has(confirmationElement)) {
468        return;
469      }
470
471      this.processedConfirmations.add(confirmationElement);
472      pendingSweepsStepEvent = null;
473
474      requestAnimationFrame(() => {
475        requestAnimationFrame(() => {
476          removeSweepsWizardHashForAnalytics();
477          dataLayer.push({
478            'event': eventType,
479            'event_params': eventParams,
480          }, {event_params: undefined});
481
482          if (testAnalytics) {
483            console.log('[Sweeps Step Analytics] Final event fired after render:', {
484              'event': eventType,
485              'event_params': eventParams,
486            });
487          }
488        });
489      });
490      return;
491    }
492
493    const sweepsForm = getSweepsEntryForm();
494    if (!sweepsForm) {
495      return;
496    }
497
498    const currentStepKey = getCurrentSweepsWebformKey(sweepsForm);
499    const errorContainer = sweepsForm.querySelector('#form-error-messages');
500    const hasErrors = !!errorContainer && !!(
501      errorContainer.querySelector('[id^="error-message-"]') ||
502      errorContainer.querySelector('.messages__item')
503    );
504
505    const expectedNextStepKey = sweepsNextStepKeyMap[sourceStepKey];
506    const didAdvanceStep = expectedNextStepKey === currentStepKey;
507
508    if (!didAdvanceStep) {
509      if (testAnalytics) {
510        console.log('[Sweeps Step Analytics] Not fired yet:', {
511          eventType,
512          sourceStepKey,
513          expectedNextStepKey,
514          currentStepKey,
515          hasErrors,
516        });
517      }
518      return;
519    }
520
521    pendingSweepsStepEvent = null;
522    dataLayer.push({
523      'event': eventType,
524      'event_params': eventParams,
525    }, {event_params: undefined});
526
527    if (testAnalytics) {
528      console.log('[Sweeps Step Analytics] Step event fired:', {
529        'event': eventType,
530        'event_params': eventParams,
531      });
532    }
533  },
534};

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.