(function() { /* Guard to prevent multiple execution */ if (window.__pegaAnalyticsInitialized) return; window.__pegaAnalyticsInitialized = true; /* Toggle verbose console logs: set window.__pegaAnalyticsDebug = true in DevTools */ const isDebug = () => Boolean(window.__pegaAnalyticsDebug); function logDebug(...args) { if (isDebug()) console.log('[Pega Analytics]', ...args); } /* List of keys automatically redacted to prevent sending PII to Google */ const PII_KEYS = new Set([ 'password', 'ssn', 'socialsecurity', 'email', 'phone', 'taxid', 'creditcard', 'accountnumber', 'dob', 'birthdate', 'fullname' ]); /** * Sanitizes payloads by redacting sensitive keys, stripping circular refs, * and flattening nested Pega objects into top-level GA4-compatible parameters. */ function sanitizeAndFlattenPayload(obj, prefix = '', depth = 0) { if (depth > 3 || obj === null || typeof obj !== 'object') return {}; const result = {}; for (const [key, val] of Object.entries(obj)) { if (!key || typeof key !== 'string') continue; const normalizedKey = key.toLowerCase().replace(/[^a-z]/g, ''); // 1. Redact PII if (PII_KEYS.has(normalizedKey)) { result[`${prefix}${key}`] = '[REDACTED_PII]'; continue; } // 2. Process Primitives & Truncate Strings (>100 chars get dropped by GA4) if (val === null || val === undefined) { continue; } else if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') { const stringVal = String(val); result[`${prefix}${key}`] = stringVal.length > 100 ? `${stringVal.substring(0, 97)}...` : val; } else if (typeof val === 'object' && !Array.isArray(val)) { // Flatten nested structures (e.g. payload.context.caseID -> context_caseID) const nested = sanitizeAndFlattenPayload(val, `${prefix}${key}_`, depth + 1); Object.assign(result, nested); } } return result; } /** * Normalizes Pega event strings into GA4-compliant lower_snake_case format. * Example: "ASSIGNMENT_SUBMITTED" -> "pega_assignment_submitted" */ function normalizeEventName(rawEvent) { if (!rawEvent) return 'pega_unknown_event'; const cleanName = rawEvent .replace(/([a-z])([A-Z])/g, '$1_$2') .toLowerCase() .replace(/[^a-z0-9_]/g, '_') .replace(/_+/g, '_'); return cleanName.startsWith('pega_') ? cleanName : `pega_${cleanName}`; } /** * Centralized event tracking dispatcher. */ function trackGoogleEvent(eventName, rawParams = {}) { const formattedEvent = normalizeEventName(eventName); const safeParams = sanitizeAndFlattenPayload(rawParams); logDebug(`Fired Event: "${formattedEvent}"`, safeParams); // 1. Push to Google Tag Manager DataLayer window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: formattedEvent, pega: safeParams }); // 2. Dispatch directly to Google Analytics 4 (if gtag.js is present) if (typeof window.gtag === 'function') { window.gtag('event', formattedEvent, safeParams); } } /** * Main PCore event subscription workflow. */ function initializePegaAnalytics() { logDebug("PCore ready. Setting up event subscriptions..."); try { const PCore = window.PCore; const pubSubUtils = PCore.getPubSubUtils(); const eventRegistry = PCore.getEvents(); /* Collect all standard Pega event categories */ const eventCategories = [ eventRegistry.getCaseEvent?.(), eventRegistry.getContainerEvent?.(), eventRegistry.getDataEvent?.() ].filter(Boolean); /* Set excluded events if needed (left empty to capture all, including CASE_OPENED) */ const excludedEvents = new Set(); const allEvents = new Set(); eventCategories.forEach(category => { Object.keys(category).forEach(key => { if (typeof category[key] === 'string' && !excludedEvents.has(category[key])) { allEvents.add(category[key]); } }); }); /* Register dynamic event listeners */ allEvents.forEach(eventType => { const subscriptionItemName = `Analytics_${eventType}`; pubSubUtils.subscribe( eventType, (payload) => { trackGoogleEvent(eventType, { pega_event_type: eventType, ...(payload || {}) }); }, subscriptionItemName, false ); }); /* Register specialized UI & Navigation events */ const { URL_NAVIGATION } = PCore.getConstants().PUB_SUB_EVENTS; const { SHOW_PAGE_DONE } = PCore.getConstants().PUB_SUB_EVENTS.UI_EVENTS; // Handle SPA Navigation Events pubSubUtils.subscribe( URL_NAVIGATION, () => { const navigationUtils = PCore.getNavigationUtils?.(); const searchTerm = navigationUtils?.getURLParam?.('searchTerm'); trackGoogleEvent('url_navigation', { search_term: searchTerm || '', page_location: window.location.href, page_path: window.location.pathname }); }, 'Analytics_URL_NAVIGATION', false ); // Handle Page Completion / Virtual Pageviews pubSubUtils.subscribe( SHOW_PAGE_DONE, ({ target, pageName, pageClass }) => { if (target !== 'app/primary' || !pageName) return; const pagePayload = { target_container: target, page_name: pageName, page_class: pageClass || '', page_location: window.location.pathname + window.location.search }; // Pega-specific page done event trackGoogleEvent('page_done', pagePayload); // Native GA4 page_view event dispatch if (typeof window.gtag === 'function') { window.gtag('event', 'page_view', { page_title: pageName, page_location: window.location.href, page_path: window.location.pathname }); } }, 'Analytics_SHOW_PAGE_DONE', false ); logDebug(`Successfully registered ${allEvents.size + 2} event listeners.`); } catch (error) { console.error("[Pega Analytics Error]: Failed to bind PubSub listeners.", error); } } /** * Safely check and poll for window.PCore availability. */ function waitForPCore() { const maxAttempts = 50; let attempts = 0; const interval = setInterval(() => { attempts++; if (window.PCore && window.PCore.getEvents && window.PCore.getPubSubUtils) { clearInterval(interval); initializePegaAnalytics(); } else if (attempts >= maxAttempts) { clearInterval(interval); console.error("[Pega Analytics Error]: PCore load timeout."); } }, 200); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", waitForPCore); } else { waitForPCore(); } })();