@trynova-ai/shadow-js
Version:
A TypeScript library for user session tracking.
174 lines (173 loc) • 7.13 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.BrowserPlugin = void 0;
class BrowserPlugin {
constructor(scrubRules = []) {
this.scrubRules = [];
this.scrubRules = scrubRules;
}
setup(shadow) {
const listeners = [
{ type: 'click', handler: (event) => this.handleEvent('click', event, shadow) },
{ type: 'change', handler: (event) => this.handleEvent('change', event, shadow) },
{ type: 'submit', handler: (event) => this.handleEvent('submit', event, shadow) },
{ type: 'load', handler: (event) => this.handleEvent('load', event, shadow) }
];
this.setupListeners(listeners);
}
setupListeners(events) {
events.forEach(event => {
window.addEventListener(event.type, event.handler, { capture: true, passive: true });
});
}
handleEvent(eventType, event, shadow) {
try {
const target = this.resolveTargetElement(event.target);
const payload = this.eventPayload(eventType, target);
// Apply scrubbing to the event payload
const scrubbedPayload = this.applyScrubbingRules(payload);
shadow.capture(scrubbedPayload);
}
catch (error) {
console.error(`Error handling ${eventType} event:`, error);
}
}
eventPayload(eventType, target) {
try {
const payload = {
action: eventType.toUpperCase(),
page: window.location.href,
previousPage: document.referrer ? document.referrer : '',
timestamp: new Date().toISOString(),
element: target ? {
tag: target.tagName || '',
css: target instanceof HTMLElement ? target.style.cssText : '',
attributes: this.getAttributes(target),
innerText: target instanceof HTMLElement ? target.innerText : '',
} : null,
};
// Check if the event is a change event
if (eventType === 'change' && target instanceof HTMLInputElement) {
payload.action = 'INPUT';
}
else if (eventType === 'change' && target instanceof HTMLSelectElement) {
payload.action = 'SELECT';
}
else if (eventType === 'change' && target instanceof HTMLTextAreaElement) {
payload.action = 'INPUT';
}
return payload;
}
catch (error) {
console.error(`Error creating event payload for ${eventType} event:`, error);
return null;
}
}
applyScrubbingRules(payload) {
if (!payload.element)
return payload;
const element = payload.element;
this.scrubRules.forEach((rule) => {
if (rule.type === 'id' && element.attributes.id === rule.match) {
// For ID-based rules, scrub innerText and the "value" attribute
this.scrubData(element, rule, 'innerText');
if (element.attributes['value']) {
this.scrubData(element, rule, 'value', true);
}
}
else if (rule.type === 'regex') {
const regex = new RegExp(rule.match);
// For regex-based rules, scrub innerText and any matching attribute values
if (regex.test(element.innerText || '')) {
this.scrubData(element, rule, 'innerText');
}
Object.keys(element.attributes).forEach((attrKey) => {
const attrValue = element.attributes[attrKey];
if (regex.test(attrValue)) {
this.scrubData(element, rule, attrKey, true);
}
});
}
});
return payload;
}
scrubData(element, rule, key, isAttribute = false) {
if (rule.method === 'mask') {
if (isAttribute) {
element.attributes[key] = '****'; // Mask sensitive attribute value
}
else {
element[key] = '****'; // Mask sensitive innerText
}
}
else if (rule.method === 'randomize') {
const randomValue = Math.random().toString(36).substr(2, 10);
if (isAttribute) {
element.attributes[key] = randomValue; // Randomize sensitive attribute value
}
else {
element[key] = randomValue; // Randomize sensitive innerText
}
}
}
resolveTargetElement(target) {
try {
let targetElement = target;
// Traverse up the DOM tree to find the nearest parent that is either a <button> or has role="button"
while (targetElement && targetElement !== document.body) {
if (targetElement.tagName.toLowerCase() === 'button' || targetElement.getAttribute('role') === 'button') {
break;
}
targetElement = targetElement.parentElement;
}
// If no button was found or the element doesn't have id/class, find the nearest parent with id or class
if (!(targetElement === null || targetElement === void 0 ? void 0 : targetElement.id) && !this.getClassNames(targetElement)) {
targetElement = target; // Start again from the original clicked element
while (targetElement && targetElement !== document.body) {
if (targetElement.id || this.getClassNames(targetElement)) {
break; // Found a parent with an id or class, use this
}
targetElement = targetElement.parentElement;
}
}
return targetElement || target;
}
catch (error) {
console.error('Error resolving target element:', error);
return target;
}
}
getAttributes(element) {
try {
const attrs = {};
if (element) {
for (let i = 0; i < element.attributes.length; i++) {
attrs[element.attributes[i].name] = element.attributes[i].value;
}
}
return attrs;
}
catch (error) {
console.error('Error getting element attributes:', error);
return {};
}
}
getClassNames(element) {
try {
if (!element)
return '';
if (element instanceof HTMLElement) {
return element.className;
}
else if (element instanceof SVGElement && typeof element.className === 'object') {
return element.className.baseVal; // SVG className is an object with baseVal
}
return '';
}
catch (error) {
console.error('Error getting element class names:', error);
return '';
}
}
}
exports.BrowserPlugin = BrowserPlugin;