// Sliples UI Recorder v2 - Domain-based auth + JS/Network error capture (function() { 'use strict'; const CONFIG = {"apiKey": "", "endpoint": "https://sliples.agantis.in/api/v1", "projectId": null, "authMode": "domain"}; const SliplesRecorder = { sessionId: null, events: [], sequence: 0, isRecording: false, flushInterval: null, lastEventTime: null, getHeaders: function() { const h = {'Content-Type': 'application/json'}; if (CONFIG.authMode === 'api_key' && CONFIG.apiKey) h['X-API-Key'] = CONFIG.apiKey; return h; }, getCssSelector: function(el) { if (!el || el === document.body) return 'body'; if (el.id) return '#' + CSS.escape(el.id); const path = []; while (el && el !== document.body) { let selector = el.tagName.toLowerCase(); if (el.id) { selector = '#' + CSS.escape(el.id); path.unshift(selector); break; } if (el.className && typeof el.className === 'string') { const classes = el.className.trim().split(/\s+/).filter(c => c && !c.match(/^(hover|active|focus|ng-|\d)/)); if (classes.length) selector += '.' + classes.slice(0, 2).map(c => CSS.escape(c)).join('.'); } const parent = el.parentElement; if (parent) { const siblings = Array.from(parent.children).filter(c => c.tagName === el.tagName); if (siblings.length > 1) selector += ':nth-of-type(' + (siblings.indexOf(el) + 1) + ')'; } path.unshift(selector); el = parent; } return path.join(' > '); }, getXPath: function(el) { if (!el) return ''; if (el.id) return '//*[@id="' + el.id + '"]'; const parts = []; while (el && el.nodeType === Node.ELEMENT_NODE) { let index = 1, sibling = el.previousSibling; while (sibling) { if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === el.tagName) index++; sibling = sibling.previousSibling; } parts.unshift(el.tagName.toLowerCase() + '[' + index + ']'); el = el.parentNode; } return '/' + parts.join('/'); }, getLabelText: function(el) { if (el.labels && el.labels.length) return el.labels[0].textContent.trim(); const ariaLabel = el.getAttribute('aria-label'); if (ariaLabel) return ariaLabel; const labelledBy = el.getAttribute('aria-labelledby'); if (labelledBy) { const labelEl = document.getElementById(labelledBy); if (labelEl) return labelEl.textContent.trim(); } const parentLabel = el.closest('label'); if (parentLabel) return parentLabel.textContent.trim(); return null; }, getElementData: function(el) { if (!el || !el.tagName) return {}; return { selector_css: this.getCssSelector(el), selector_xpath: this.getXPath(el), selector_text: el.textContent ? el.textContent.trim().substring(0, 100) : null, selector_test_id: el.getAttribute('data-testid') || el.getAttribute('data-test-id'), selector_aria: el.getAttribute('aria-label'), tag_name: el.tagName.toLowerCase(), element_id: el.id || null, element_classes: el.className && typeof el.className === 'string' ? JSON.stringify(el.className.split(/\s+/).filter(Boolean)) : null, element_name: el.name || null, element_type: el.type || null, element_role: el.getAttribute('role'), label_text: this.getLabelText(el), placeholder: el.placeholder || null, }; }, record: function(type, el, extra) { if (!this.isRecording) return; const event = { sequence: this.sequence++, timestamp: new Date().toISOString(), event_type: type, url: window.location.href, ...this.getElementData(el), ...extra, }; this.events.push(event); this.lastEventTime = Date.now(); try { localStorage.setItem('_sliples_last_event', String(this.lastEventTime)); } catch(e) {} }, flush: async function() { if (!this.events.length || !this.sessionId) return; const batch = this.events.splice(0, this.events.length); try { const resp = await fetch(CONFIG.endpoint + '/recorder/sessions/' + this.sessionId + '/events', { method: 'POST', headers: this.getHeaders(), body: JSON.stringify({ events: batch }), }); if (!resp.ok) this.events.unshift(...batch); } catch (e) { this.events.unshift(...batch); } }, handleClick: function(e) { const rect = e.target.getBoundingClientRect(); this.record('click', e.target, { coordinates: { x: Math.round(e.clientX - rect.left), y: Math.round(e.clientY - rect.top) } }); }, handleInput: function(e) { clearTimeout(e.target._sliplesTimeout); const el = e.target; el._sliplesTimeout = setTimeout(() => { const isSensitive = el.type === 'password' || el.autocomplete === 'current-password' || el.autocomplete === 'new-password'; this.record('input', el, { value: isSensitive ? '***' : el.value }); }, 500); }, handleChange: function(e) { const el = e.target; const isSensitive = el.type === 'password' || el.autocomplete === 'current-password' || el.autocomplete === 'new-password'; this.record('change', el, { value: isSensitive ? '***' : el.value }); }, handleSubmit: function(e) { this.record('submit', e.target); }, handleKeydown: function(e) { if (['Enter', 'Tab', 'Escape', 'Backspace', 'Delete'].includes(e.key) || e.ctrlKey || e.metaKey) { this.record('keydown', e.target, { key_info: { key: e.key, code: e.code, ctrl: e.ctrlKey, alt: e.altKey, shift: e.shiftKey, meta: e.metaKey } }); } }, // JS error and network error capture setupErrorCapture: function() { // JS exceptions this._onError = (event) => { this.record('js_error', null, { extra_data: { message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno, stack: event.error ? event.error.stack : null, } }); }; window.addEventListener('error', this._onError); // Unhandled promise rejections this._onUnhandledRejection = (event) => { this.record('js_error', null, { extra_data: { message: 'Unhandled Promise Rejection', reason: event.reason ? (event.reason.message || String(event.reason)) : 'unknown', stack: event.reason && event.reason.stack ? event.reason.stack : null, } }); }; window.addEventListener('unhandledrejection', this._onUnhandledRejection); // Network error capture via fetch monkey-patch const origFetch = window.fetch; const self = this; window.fetch = function() { const url = arguments[0] instanceof Request ? arguments[0].url : String(arguments[0]); // Skip our own requests if (url.includes('/recorder/sessions/')) return origFetch.apply(this, arguments); return origFetch.apply(this, arguments).then(resp => { if (!resp.ok && resp.status >= 400) { self.record('network_error', null, { extra_data: { url: url, status: resp.status, statusText: resp.statusText, method: (arguments[1] && arguments[1].method) || 'GET' } }); } return resp; }).catch(err => { self.record('network_error', null, { extra_data: { url: url, error: err.message, method: (arguments[1] && arguments[1].method) || 'GET' } }); throw err; }); }; this._origFetch = origFetch; // XHR error capture const origXHROpen = XMLHttpRequest.prototype.open; const origXHRSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url) { this._sliplesMethod = method; this._sliplesUrl = url; return origXHROpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function() { const xhr = this; xhr.addEventListener('loadend', function() { if (xhr._sliplesUrl && !xhr._sliplesUrl.includes('/recorder/sessions/') && xhr.status >= 400) { self.record('network_error', null, { extra_data: { url: xhr._sliplesUrl, status: xhr.status, statusText: xhr.statusText, method: xhr._sliplesMethod } }); } }); xhr.addEventListener('error', function() { if (xhr._sliplesUrl && !xhr._sliplesUrl.includes('/recorder/sessions/')) { self.record('network_error', null, { extra_data: { url: xhr._sliplesUrl, error: 'Network error', method: xhr._sliplesMethod } }); } }); return origXHRSend.apply(this, arguments); }; this._origXHROpen = origXHROpen; this._origXHRSend = origXHRSend; }, teardownErrorCapture: function() { window.removeEventListener('error', this._onError); window.removeEventListener('unhandledrejection', this._onUnhandledRejection); if (this._origFetch) window.fetch = this._origFetch; if (this._origXHROpen) XMLHttpRequest.prototype.open = this._origXHROpen; if (this._origXHRSend) XMLHttpRequest.prototype.send = this._origXHRSend; }, start: async function(name) { if (this.isRecording || this._starting) return; this._starting = true; const sessionName = name || 'Recording ' + new Date().toLocaleString(); try { const resp = await fetch(CONFIG.endpoint + '/recorder/sessions', { method: 'POST', headers: this.getHeaders(), body: JSON.stringify({ name: sessionName, url: window.location.href, user_agent: navigator.userAgent, viewport_width: window.innerWidth, viewport_height: window.innerHeight, }), }); if (!resp.ok) throw new Error('Failed to start session: ' + resp.status); const data = await resp.json(); this.sessionId = data.session_id; try { localStorage.setItem('_sliples_session_id', this.sessionId); } catch(e) {} try { localStorage.setItem('_sliples_session_name', sessionName); } catch(e) {} try { localStorage.setItem('_sliples_last_event', String(Date.now())); } catch(e) {} this.isRecording = true; this.sequence = 0; this.events = []; document.addEventListener('click', this._handleClick, true); document.addEventListener('input', this._handleInput, true); document.addEventListener('change', this._handleChange, true); document.addEventListener('submit', this._handleSubmit, true); document.addEventListener('keydown', this._handleKeydown, true); this._navObserver = new MutationObserver(() => { if (this._lastUrl !== window.location.href) { this._lastUrl = window.location.href; this.record('navigation', null, { url: window.location.href }); } }); const observeTarget = document.body || document.documentElement; if (observeTarget) this._navObserver.observe(observeTarget, { childList: true, subtree: true }); this._lastUrl = window.location.href; this.setupErrorCapture(); this.flushInterval = setInterval(() => this.flush(), 3000); this._starting = false; console.log('[Sliples] Recording started. Session:', this.sessionId, '(' + sessionName + ')'); } catch (e) { this._starting = false; console.error('[Sliples] Failed to start recording:', e); } }, stop: async function() { if (!this.isRecording) return; this.isRecording = false; document.removeEventListener('click', this._handleClick, true); document.removeEventListener('input', this._handleInput, true); document.removeEventListener('change', this._handleChange, true); document.removeEventListener('submit', this._handleSubmit, true); document.removeEventListener('keydown', this._handleKeydown, true); if (this._navObserver) this._navObserver.disconnect(); this.teardownErrorCapture(); clearInterval(this.flushInterval); await this.flush(); try { const resp = await fetch(CONFIG.endpoint + '/recorder/sessions/' + this.sessionId + '/stop', { method: 'POST', headers: this.getHeaders(), }); if (resp.ok) { const data = await resp.json(); console.log('[Sliples] Recording stopped. Events:', data.event_count); } } catch (e) { console.error('[Sliples] Failed to stop recording:', e); } try { localStorage.removeItem('_sliples_session_id'); localStorage.removeItem('_sliples_session_name'); localStorage.removeItem('_sliples_last_event'); } catch(e) {} this.sessionId = null; }, resume: function(sessionId, sessionName) { if (this.isRecording) return; this.sessionId = sessionId; this.isRecording = true; this.sequence = 0; this.events = []; document.addEventListener('click', this._handleClick, true); document.addEventListener('input', this._handleInput, true); document.addEventListener('change', this._handleChange, true); document.addEventListener('submit', this._handleSubmit, true); document.addEventListener('keydown', this._handleKeydown, true); this._navObserver = new MutationObserver(() => { if (this._lastUrl !== window.location.href) { this._lastUrl = window.location.href; this.record('navigation', null, { url: window.location.href }); } }); const observeTarget = document.body || document.documentElement; if (observeTarget) this._navObserver.observe(observeTarget, { childList: true, subtree: true }); this._lastUrl = window.location.href; this.setupErrorCapture(); this.flushInterval = setInterval(() => this.flush(), 3000); console.log('[Sliples] Recording resumed. Session:', this.sessionId, '(' + sessionName + ')'); }, init: function() { this._handleClick = this.handleClick.bind(this); this._handleInput = this.handleInput.bind(this); this._handleChange = this.handleChange.bind(this); this._handleSubmit = this.handleSubmit.bind(this); this._handleKeydown = this.handleKeydown.bind(this); return this; }, }.init(); // Guard against double-execution (SPA re-renders, duplicate script tags) if (window.SliplesRecorder && (window.SliplesRecorder.isRecording || window.SliplesRecorder._starting)) return; window.SliplesRecorder = SliplesRecorder; // Auto-init: resume existing session or start new one (defer until DOM ready) function autoInit() { const STALE_MS = 15 * 60 * 1000; // 15 minutes function detectOS() { const ua = navigator.userAgent; if (/Windows/.test(ua)) return 'win'; if (/Mac/.test(ua)) return 'mac'; if (/Linux/.test(ua)) return 'linux'; if (/Android/.test(ua)) return 'android'; if (/iPhone|iPad/.test(ua)) return 'ios'; return 'other'; } function detectBrowser() { const ua = navigator.userAgent; if (/Edg\//.test(ua)) return 'edge'; if (/Chrome\//.test(ua)) return 'chrome'; if (/Firefox\//.test(ua)) return 'firefox'; if (/Safari\//.test(ua)) return 'safari'; return 'other'; } function timestamp() { const d = new Date(); return '' + d.getFullYear() + String(d.getMonth()+1).padStart(2,'0') + String(d.getDate()).padStart(2,'0') + String(d.getHours()).padStart(2,'0') + String(d.getMinutes()).padStart(2,'0'); } let resumed = false; try { const storedId = localStorage.getItem('_sliples_session_id'); const storedName = localStorage.getItem('_sliples_session_name'); const lastEvent = parseInt(localStorage.getItem('_sliples_last_event') || '0', 10); if (storedId && storedName && lastEvent) { const gap = Date.now() - lastEvent; if (gap < STALE_MS) { SliplesRecorder.resume(storedId, storedName); resumed = true; } else { // Gap too long — clear stale session localStorage.removeItem('_sliples_session_id'); localStorage.removeItem('_sliples_session_name'); localStorage.removeItem('_sliples_last_event'); } } } catch(e) {} if (!resumed) { const name = detectOS() + '-' + detectBrowser() + '-' + timestamp(); SliplesRecorder.start(name); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', autoInit); } else { autoInit(); } })();