UNPKG

palzintrack

Version:

Palzin Track Client

671 lines (562 loc) 19.1 kB
((window) => { const { screen: { width, height }, navigator: { language }, location, innerWidth, innerHeight, localStorage, document, history } = window; const { hostname, pathname, search } = location; const { currentScript, referrer } = document; if (!currentScript) return; const _data = 'data-'; const _false = 'false'; const _true = 'true'; const attr = currentScript.getAttribute.bind(currentScript); const apiToken = attr(_data + 'api-key'); const userId = attr(_data + 'user-id'); const ua = window.navigator.userAgent; const projectId = attr(_data + 'project-id'); const hostUrl = 'https://api.palzin.live/v1/'; const tags = attr(_data + 'tags'); const autoTrack = attr(_data + 'auto-track') !== _false; const excludeSearch = attr(_data + 'exclude-search') === _true; const domain = attr(_data + 'domains') || ''; const domains = domain.split(',').map((n) => n.trim()); const host = hostUrl || '__COLLECT_API_HOST__' || currentScript.src.split('/').slice(0, -1).join('/'); const endpoint = `${host.replace(/\/$/, '')}__COLLECT_API_ENDPOINT__`; const screen = `${width}x${height}`; const vp_size = Math.max( document.documentElement.clientWidth || 0, window.innerWidth || 0 ) + 'x' + Math.max( document.documentElement.clientHeight || 0, window.innerHeight || 0 ); const eventRegex = /data-pt-event-([\w-_]+)/; const eventNameAttribute = _data + 'pt-event'; const delayDuration = 300; const query = location.search; const utm_source = // eslint-disable-next-line no-undef new URLSearchParams(window.location.search).get('utm_source') || undefined; const EVENT_CHANNEL = 'events'; let href = ''; const sameHost = document.referrer.indexOf(location.protocol + '//' + location.host) === 0; const pageReferrer = sameHost ? null : document.referrer; const path = (function () { let loc = location; const canonical = document.querySelector("link[rel='canonical'][href]"); if (canonical) { const a = document.createElement('a'); a.href = canonical.href; if ( a.hostname.replace(/^www\./, '') === location.hostname.replace(/^www\./, '') ) { loc = a; } } return loc.pathname + loc.search || '/'; })(); /* Helper functions */ const encode = (str) => { if (!str) { return undefined; } try { const result = decodeURI(str); if (result !== str) { return result; } } catch { return str; } return encodeURI(str); }; const parseURL = (url) => { return excludeSearch ? url.split('?')[0] : url; }; const getPayload = async () => ({ projectId, hostname, screen, language, userId, vp_size, utm_source, anonymousId: await getAnonymousId(), title: encode(title), url: encode(currentUrl), referrer: encode(currentRef), tags: tags ? tags : undefined }); /* Event handlers */ const handlePush = (state, title, url) => { if (!url) return; currentRef = currentUrl; currentUrl = parseURL(url.toString()); if (currentUrl !== currentRef) { // eslint-disable-next-line no-undef setTimeout(track, delayDuration); } }; const handleInsights = () => {}; const handlePathChanges = () => { const hook = (_this, method, callback) => { const orig = _this[method]; return (...args) => { callback.apply(null, args); return orig.apply(_this, args); }; }; history.pushState = hook(history, 'pushState', handlePush); history.replaceState = hook(history, 'replaceState', handlePush); }; const handleTitleChanges = () => { // eslint-disable-next-line no-undef const observer = new MutationObserver(([entry]) => { title = entry && entry.target ? entry.target.text : undefined; }); const node = document.querySelector('head > title'); if (node) { observer.observe(node, { subtree: true, characterData: true, childList: true }); } }; const handleClicks = () => { document.addEventListener( 'click', async (e) => { const isSpecialTag = tagName => ['BUTTON', 'A'].includes(tagName); const trackElement = async el => { const attr = el.getAttribute.bind(el); const eventName = attr(eventNameAttribute); // eslint-disable-next-line no-undef console.log(el); if (eventName) { const eventData = {}; el.getAttributeNames().forEach(name => { const match = name.match(eventRegex); if (match) { eventData[match[1]] = attr(name); } }); // eslint-disable-next-line no-undef console.log(eventData); // eslint-disable-next-line no-undef console.log(eventName); return trackClickEvent(eventName, eventData); } }; const findParentTag = (rootElem, maxSearchDepth) => { let currentElement = rootElem; for (let i = 0; i < maxSearchDepth; i++) { if (isSpecialTag(currentElement.tagName)) { return currentElement; } currentElement = currentElement.parentElement; if (!currentElement) { return null; } } }; const el = e.target; // eslint-disable-next-line no-undef console.log(el); // eslint-disable-next-line no-undef console.log(el.tagName); // eslint-disable-next-line no-undef console.log(findParentTag(el.parentNode, 10)); const parentElement = isSpecialTag(el.tagName) ? el : findParentTag(el, 10); // eslint-disable-next-line no-undef console.log(parentElement); if (el.tagName === 'FORM') { const eventName = el.getAttribute(eventNameAttribute); if (eventName) { e.preventDefault(); // Prevent default form submission const eventData = {}; const formTags = await extractFormTags(el); Object.assign(eventData, formTags); // eslint-disable-next-line no-undef console.log("in el.tagname === form"); // eslint-disable-next-line no-undef console.log(eventData); await trackClickEvent(eventName, eventData); el.submit(); } } else if (parentElement) { const { href, target } = parentElement; const eventName = parentElement.getAttribute(eventNameAttribute); if (eventName) { if (parentElement.tagName === 'A') { // eslint-disable-next-line no-undef console.log('Its tagname is A'); const external = target === '_blank' || e.ctrlKey || e.shiftKey || e.metaKey || (e.button && e.button === 1); if (eventName && href) { if (!external) { e.preventDefault(); } return trackElement(parentElement).then(() => { if (!external) location.href = href; }); } } else if (parentElement.tagName === 'BUTTON') { // eslint-disable-next-line no-undef console.log('Its tagname is BUTTON') return trackElement(parentElement); } } } else { // eslint-disable-next-line no-undef console.log('completely off track'); return trackElement(el); } }, true, ); }; /* Tracking functions */ const trackingDisabled = () => (localStorage && localStorage.getItem('pt.disabled')) || (domain && !domains.includes(hostname)); const send = async (payload, type = 'analytics') => { if (trackingDisabled()) return; // eslint-disable-next-line no-undef const trackHeaders = new Headers(); trackHeaders.append('Content-Type', 'application/json'); trackHeaders.append('Authorization', 'Bearer ' + apiToken); if (typeof cache !== 'undefined') { trackHeaders.append('x-pt-m2cache', cache); } try { const anonymousId = await getAnonymousId(); const analyticsPayload = { project: projectId, hostname: hostname, token: apiToken, query: query, path, utm_source: utm_source, ua: ua, anonymousId: anonymousId, screen: payload.screen, user_id: payload.userId, vp_size: payload.vp_size, title: payload.title, url: encode(currentUrl) ?? payload.url, language, referrer: encode(currentRef) ?? pageReferrer, tags: tags ? tags : undefined }; const trackBodyValue = JSON.stringify(analyticsPayload); trackHeaders.append('Content-Length', trackBodyValue.length.toString()); const routePath = hostUrl + 'track'; const res = await fetch(routePath, { method: 'POST', headers: trackHeaders, body: trackBodyValue }); const text = await res.text(); return (cache = text); } catch { /* empty */ } }; // Function to get value from persistence storage (Cookie, LocalStorage, Memory) const getFromPersistence = async (key) => { // Check if Cookie is available if (isCookieSupported()) { const cookieValue = getPTCookie(key); if (cookieValue) { return cookieValue; } } // Check if LocalStorage is available if (isLocalStorageSupported()) { const localStorageValue = localStorage.getItem(key); if (localStorageValue) { return localStorageValue; } } // If no persistence storage available, return null return null; }; // Function to persist value to persistence storage (Cookie, LocalStorage) const persistToPersistence = async (key, value) => { // Set Cookie if (isCookieSupported()) { setCookie(key, value); } // Set LocalStorage if (isLocalStorageSupported()) { localStorage.setItem(key, value); } }; // Function to generate a unique ID const generateUniqueId = () => { // Generate a UUID or any other unique ID algorithm // For simplicity, let's use a random number here return 'pt_' + Math.random().toString(36).substr(2, 9); }; // Function to check if cookie is supported const isCookieSupported = () => { return typeof document !== 'undefined' && typeof document.cookie !== 'undefined'; }; // Function to check if local storage is supported const isLocalStorageSupported = () => { return typeof window !== 'undefined' && typeof window.localStorage !== 'undefined'; }; // Function to set cookie const setCookie = (key, value) => { const expires = new Date(); expires.setTime(expires.getTime() + (30 * 24 * 60 * 60 * 1000)); // 30 days let cookieString = key + '=' + value + '; path=/; expires=' + expires.toUTCString(); domains.forEach((domain) => { cookieString += '; domain=' + domain; }); document.cookie = cookieString; }; // Function to get cookie value by name const getPTCookie = (name) => { const cookies = document.cookie.split(';'); for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i].trim(); // Check if this cookie name is the one we're looking for if (cookie.startsWith(name + '=')) { return cookie.substring(name.length + 1); } } return null; }; // Function to get anonymous id const getAnonymousId = async () => { let anonymousId = ''; // Check if the anonymousId exists in persistence storage const storageKey = 'pt.anonymousId'; const storedId = await getFromPersistence(storageKey); if (storedId) { anonymousId = storedId; } else { // Generate a new anonymousId anonymousId = generateUniqueId(); // Persist the anonymousId await persistToPersistence(storageKey, anonymousId); } return anonymousId; }; const sendIdentity = async (payload, type = 'identify') => { if (trackingDisabled()) return; // eslint-disable-next-line no-undef const trackHeaders = new Headers(); trackHeaders.append('Content-Type', 'application/json'); trackHeaders.append('Authorization', 'Bearer ' + apiToken); if (typeof cache !== 'undefined') { trackHeaders.append('x-pt-m2cache', cache); } try { const identityPayload = payload; const trackBodyValue = JSON.stringify(identityPayload); trackHeaders.append('Content-Length', trackBodyValue.length.toString()); const routePath = hostUrl + 'identity'; const res = await fetch(routePath, { method: 'POST', headers: trackHeaders, body: trackBodyValue }); const text = await res.text(); return (cache = text); } catch { /* empty */ } }; const trackClickEvent = async (eventName, eventData) => { // eslint-disable-next-line no-undef console.log("inside-trackClickEvent") const tags = {}; for (const [key, value] of Object.entries(eventData)) { if (key.startsWith('tags')) { const tagKey = camelToKebab(key).replace('tags-', ''); tags[tagKey] = value; } } // eslint-disable-next-line no-undef console.log(eventData); // Check if the event element is within a form with data-pt-event and data-pt-event-channel attributes const formElement = findParentForm(eventData.target); while (formElement && (formElement !== null || formElement !== undefined)) { if(formElement.getAttribute('data-pt-event') && formElement.getAttribute('data-pt-event-channel')) { // eslint-disable-next-line no-undef console.log("inside-formElement") // Add input, select, and dropdown values as tags const formTags = await extractFormTags(formElement); Object.assign(tags, formTags); } } // eslint-disable-next-line no-undef console.log(tags); const formValues = {}; // Assuming 'target' is a form object containing form data for (const key of Object.keys(eventData)) { const value = eventData[key]; if (value && !key.toLowerCase().includes('password')) { formValues[camelToKebab(key)] = value.toString(); } } const eventPayload = { channel: eventData.channel || EVENT_CHANNEL, source: 'web', project: projectId, event: eventName, description: eventData.description, icon: eventData.icon || '👆', notify: eventData.notify || false, user_id: eventData.userid, tags: { ...tags, ...(Object.keys(tags).length ? {} : filterTags(formValues)) } }; return await sendEvents(eventPayload, 'event'); }; // Function to find the parent form element of the clicked element const findParentForm = async (element) => { // eslint-disable-next-line no-undef console.log('findParentForm'); // eslint-disable-next-line no-undef console.log(element); while (element && (element !== null || element !== undefined)) { if (element.nodeName.toUpperCase() === 'FORM'){ if(element.hasAttribute('data-pt-event') && element.hasAttribute('data-pt-event-channel')) { return element; } return null; } return null; } return null; }; // Function to extract input, select, and dropdown values from a form element const extractFormTags = async (formElement) => { const formTags = {}; const inputElements = formElement.querySelectorAll('input, select, textarea'); for (const inputElement of inputElements) { const tagName = inputElement.getAttribute('data-pt-event-tags'); const tagValue = inputElement.value; if (tagName && tagValue) { formTags[tagName] = tagValue; } } return formTags; }; function filterTags(tags) { return Object.fromEntries( Object.entries(tags).filter(([key, value]) => value !== undefined) ); } const sendEvents = async (payload, type = 'event') => { if (trackingDisabled()) return; // eslint-disable-next-line no-undef const eventHeaders = new Headers(); eventHeaders.append('Content-Type', 'application/json'); eventHeaders.append('Authorization', 'Bearer ' + apiToken); if (typeof cache !== 'undefined') { eventHeaders.append('x-pt-m1cache', cache); } try { const routePath = hostUrl + 'log'; const bodyValues = JSON.stringify(payload); eventHeaders.append('Content-Length', bodyValues.length.toString()); const res = await fetch(routePath, { method: 'POST', headers: eventHeaders, body: bodyValues }) .then((response) => response.json()) .then((data) => { if (data.message === 'Event published successfully.') { // Handle success // eslint-disable-next-line no-undef console.log('Success MSG:'); // Redirect user or do any other actions } else { // Handle other cases // eslint-disable-next-line no-undef console.log('Failed MSG:'); // Redirect user or do any other actions } location.href = href; }) .catch((error) => { // eslint-disable-next-line no-undef console.error('Fetch error: ', error); location.href = href; }); const text = await res.text(); return (cache = text); } catch { /* empty */ } }; function camelToKebab(str) { return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase(); } const track = (obj, data) => { if (typeof obj === 'string') { return send({ ...getPayload(), name: obj, tags: typeof data === 'object' ? data : undefined }); } else if (typeof obj === 'object') { return send(obj); } else if (typeof obj === 'function') { return send(obj(getPayload())); } return send(getPayload()); }; const identify = (data) => sendIdentity({ ...getPayload(), data }, 'identify'); /* Start */ if (!window.pt) { window.pt = { track, identify }; } let currentUrl = `${pathname}${search}`; let currentRef = referrer !== hostname ? referrer : ''; let title = document.title; let cache; let initialized; if (autoTrack && !trackingDisabled()) { handlePathChanges(); handleTitleChanges(); handleClicks(); const init = () => { if (document.readyState === 'complete' && !initialized) { track(); initialized = true; } }; document.addEventListener("hashchange", () => { track(); }); document.addEventListener('readystatechange', init, true); init(); } })(window);