humanbehavior-js 0.4.22 → 0.4.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/angular/index.cjs +276 -387
- package/dist/cjs/angular/index.cjs.map +1 -1
- package/dist/cjs/index.cjs +272 -383
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/install-wizard.cjs +5 -5
- package/dist/cjs/install-wizard.cjs.map +1 -1
- package/dist/cjs/react/index.cjs +282 -393
- package/dist/cjs/react/index.cjs.map +1 -1
- package/dist/cjs/remix/index.cjs +272 -383
- package/dist/cjs/remix/index.cjs.map +1 -1
- package/dist/cjs/svelte/index.cjs +272 -383
- package/dist/cjs/svelte/index.cjs.map +1 -1
- package/dist/cjs/vue/index.cjs +272 -383
- package/dist/cjs/vue/index.cjs.map +1 -1
- package/dist/cjs/wizard/index.cjs +5 -5
- package/dist/cjs/wizard/index.cjs.map +1 -1
- package/dist/cli/ai-auto-install.js +5 -5
- package/dist/cli/ai-auto-install.js.map +1 -1
- package/dist/cli/auto-install.js +5 -5
- package/dist/cli/auto-install.js.map +1 -1
- package/dist/esm/angular/index.js +276 -387
- package/dist/esm/angular/index.js.map +1 -1
- package/dist/esm/index.js +272 -383
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/install-wizard.js +5 -5
- package/dist/esm/install-wizard.js.map +1 -1
- package/dist/esm/react/index.js +282 -393
- package/dist/esm/react/index.js.map +1 -1
- package/dist/esm/remix/index.js +272 -383
- package/dist/esm/remix/index.js.map +1 -1
- package/dist/esm/svelte/index.js +272 -383
- package/dist/esm/svelte/index.js.map +1 -1
- package/dist/esm/vue/index.js +272 -383
- package/dist/esm/vue/index.js.map +1 -1
- package/dist/esm/wizard/index.js +5 -5
- package/dist/esm/wizard/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/types/angular/index.d.ts +39 -9
- package/dist/types/index.d.ts +74 -59
- package/dist/types/install-wizard.d.ts +1 -1
- package/dist/types/react/index.d.ts +40 -10
- package/dist/types/remix/index.d.ts +37 -7
- package/dist/types/svelte/index.d.ts +37 -7
- package/dist/types/wizard/index.d.ts +1 -1
- package/package.json +1 -1
- package/readme.md +59 -5
- package/src/angular/index.ts +4 -4
- package/src/react/index.tsx +10 -10
- package/src/redact.ts +205 -399
- package/src/tracker.ts +103 -19
- package/src/wizard/core/install-wizard.ts +5 -5
package/dist/cjs/index.cjs
CHANGED
|
@@ -12901,15 +12901,15 @@ class HumanBehaviorAPI {
|
|
|
12901
12901
|
}
|
|
12902
12902
|
}
|
|
12903
12903
|
|
|
12904
|
-
//
|
|
12905
|
-
//
|
|
12906
|
-
//
|
|
12907
|
-
// Check if we're in a browser environment
|
|
12908
|
-
const isBrowser$2 = typeof window !== 'undefined';
|
|
12904
|
+
// Simplified redaction functionality for HumanBehavior SDK
|
|
12905
|
+
// Since rrweb auto-redacts all input fields by default, this module only handles
|
|
12906
|
+
// selectively unredacting specific fields (except passwords which remain protected)
|
|
12909
12907
|
class RedactionManager {
|
|
12910
12908
|
constructor(options) {
|
|
12911
12909
|
this.redactedText = '[REDACTED]';
|
|
12912
|
-
this.
|
|
12910
|
+
this.unredactedFields = new Set(); // Fields that user wants to unredact
|
|
12911
|
+
this.redactedFields = new Set(); // Fields that user wants to redact
|
|
12912
|
+
this.redactionMode = 'privacy-first';
|
|
12913
12913
|
this.excludeSelectors = [
|
|
12914
12914
|
'[data-no-redact="true"]',
|
|
12915
12915
|
'.human-behavior-no-redact'
|
|
@@ -12920,419 +12920,180 @@ class RedactionManager {
|
|
|
12920
12920
|
if (options === null || options === void 0 ? void 0 : options.excludeSelectors) {
|
|
12921
12921
|
this.excludeSelectors = [...this.excludeSelectors, ...options.excludeSelectors];
|
|
12922
12922
|
}
|
|
12923
|
+
// Handle new redaction strategy
|
|
12924
|
+
if (options === null || options === void 0 ? void 0 : options.redactionStrategy) {
|
|
12925
|
+
this.redactionMode = options.redactionStrategy.mode;
|
|
12926
|
+
if (this.redactionMode === 'privacy-first') {
|
|
12927
|
+
// Privacy-first: everything redacted by default, unredact specific fields
|
|
12928
|
+
if (options.redactionStrategy.unredactFields) {
|
|
12929
|
+
this.setFieldsToUnredact(options.redactionStrategy.unredactFields);
|
|
12930
|
+
}
|
|
12931
|
+
}
|
|
12932
|
+
else {
|
|
12933
|
+
// Visibility-first: everything visible by default, redact specific fields
|
|
12934
|
+
if (options.redactionStrategy.redactFields) {
|
|
12935
|
+
this.setFieldsToRedact(options.redactionStrategy.redactFields);
|
|
12936
|
+
}
|
|
12937
|
+
}
|
|
12938
|
+
}
|
|
12939
|
+
// Handle legacy redactFields (backward compatibility)
|
|
12940
|
+
if (options === null || options === void 0 ? void 0 : options.legacyRedactFields) {
|
|
12941
|
+
this.setFieldsToUnredact(options.legacyRedactFields);
|
|
12942
|
+
}
|
|
12943
|
+
// Handle legacy userFields
|
|
12923
12944
|
if (options === null || options === void 0 ? void 0 : options.userFields) {
|
|
12924
|
-
this.
|
|
12945
|
+
this.setFieldsToUnredact(options.userFields);
|
|
12925
12946
|
}
|
|
12926
12947
|
}
|
|
12927
12948
|
/**
|
|
12928
|
-
* Set specific fields to be redacted
|
|
12929
|
-
* These selectors are used to configure rrweb's built-in masking
|
|
12949
|
+
* Set specific fields to be redacted (for visibility-first mode)
|
|
12930
12950
|
* @param fields Array of CSS selectors for fields to redact
|
|
12931
12951
|
*/
|
|
12932
12952
|
setFieldsToRedact(fields) {
|
|
12933
|
-
this.
|
|
12934
|
-
fields
|
|
12935
|
-
|
|
12936
|
-
|
|
12937
|
-
|
|
12938
|
-
|
|
12939
|
-
|
|
12940
|
-
|
|
12941
|
-
|
|
12942
|
-
|
|
12943
|
-
|
|
12944
|
-
|
|
12953
|
+
this.redactedFields.clear();
|
|
12954
|
+
// Always include password fields in redacted list
|
|
12955
|
+
const passwordFields = [
|
|
12956
|
+
'input[type="password"]',
|
|
12957
|
+
'input[type="password" i]',
|
|
12958
|
+
'[type="password"]',
|
|
12959
|
+
'[type="password" i]'
|
|
12960
|
+
];
|
|
12961
|
+
// Add password fields and user-specified fields
|
|
12962
|
+
[...passwordFields, ...fields].forEach(field => {
|
|
12963
|
+
this.redactedFields.add(field);
|
|
12964
|
+
});
|
|
12965
|
+
if (this.redactedFields.size > 0) {
|
|
12966
|
+
logDebug(`Redaction: Active for ${this.redactedFields.size} field(s):`, Array.from(this.redactedFields));
|
|
12945
12967
|
}
|
|
12946
12968
|
else {
|
|
12947
|
-
logDebug('Redaction:
|
|
12948
|
-
}
|
|
12949
|
-
}
|
|
12950
|
-
/**
|
|
12951
|
-
* Check if redaction is currently active (has fields selected)
|
|
12952
|
-
*/
|
|
12953
|
-
isActive() {
|
|
12954
|
-
return this.userSelectedFields.size > 0;
|
|
12955
|
-
}
|
|
12956
|
-
/**
|
|
12957
|
-
* Get the currently selected fields for redaction
|
|
12958
|
-
*/
|
|
12959
|
-
getSelectedFields() {
|
|
12960
|
-
return Array.from(this.userSelectedFields);
|
|
12961
|
-
}
|
|
12962
|
-
/**
|
|
12963
|
-
* Process an event and redact sensitive data if needed
|
|
12964
|
-
* NOTE: This method is no longer used - events are handled directly by rrweb
|
|
12965
|
-
* Kept for backward compatibility but not called in the current implementation
|
|
12966
|
-
*/
|
|
12967
|
-
processEvent(event) {
|
|
12968
|
-
// Only process if we have fields selected for redaction
|
|
12969
|
-
if (this.userSelectedFields.size === 0) {
|
|
12970
|
-
return event;
|
|
12971
|
-
}
|
|
12972
|
-
// Clone the event to avoid modifying the original
|
|
12973
|
-
const processedEvent = JSON.parse(JSON.stringify(event));
|
|
12974
|
-
// Handle different event types
|
|
12975
|
-
if (processedEvent.type === 3) { // IncrementalSnapshot
|
|
12976
|
-
if (processedEvent.data.source === 5) { // Input event
|
|
12977
|
-
const shouldRedact = this.isFieldSelected(processedEvent.data);
|
|
12978
|
-
if (shouldRedact) {
|
|
12979
|
-
logDebug('Redaction: Processing input event for redaction');
|
|
12980
|
-
this.redactInputEvent(processedEvent.data);
|
|
12981
|
-
}
|
|
12982
|
-
}
|
|
12983
|
-
// Also check for other sources that might contain text changes
|
|
12984
|
-
else if (processedEvent.data.source === 0) { // DOM mutations
|
|
12985
|
-
this.redactDOMEvent(processedEvent.data);
|
|
12986
|
-
}
|
|
12987
|
-
// Handle other sources that might contain text
|
|
12988
|
-
else if (processedEvent.data.source === 2) { // Mouse/Touch interaction
|
|
12989
|
-
this.redactMouseEvent(processedEvent.data);
|
|
12990
|
-
}
|
|
12991
|
-
}
|
|
12992
|
-
else if (processedEvent.type === 2) { // FullSnapshot
|
|
12993
|
-
this.redactFullSnapshot(processedEvent.data);
|
|
12969
|
+
logDebug('Redaction: No fields to redact');
|
|
12994
12970
|
}
|
|
12995
|
-
|
|
12971
|
+
this.applyRedactionClasses();
|
|
12996
12972
|
}
|
|
12997
12973
|
/**
|
|
12998
|
-
*
|
|
12974
|
+
* Set specific fields to be unredacted (everything else stays redacted by rrweb)
|
|
12975
|
+
* @param fields Array of CSS selectors for fields to unredact
|
|
12999
12976
|
*/
|
|
13000
|
-
|
|
13001
|
-
|
|
13002
|
-
|
|
13003
|
-
|
|
13004
|
-
|
|
13005
|
-
|
|
13006
|
-
|
|
13007
|
-
|
|
13008
|
-
|
|
13009
|
-
|
|
13010
|
-
inputData[prop] = this.redactedText;
|
|
13011
|
-
logDebug(`Redaction: Redacted property '${prop}'`);
|
|
13012
|
-
}
|
|
13013
|
-
});
|
|
13014
|
-
// Also check for any other string properties that might contain input data
|
|
13015
|
-
Object.keys(inputData).forEach(key => {
|
|
13016
|
-
if (typeof inputData[key] === 'string' && inputData[key].length > 0) {
|
|
13017
|
-
inputData[key] = this.redactedText;
|
|
13018
|
-
logDebug(`Redaction: Redacted additional property '${key}'`);
|
|
13019
|
-
}
|
|
12977
|
+
setFieldsToUnredact(fields) {
|
|
12978
|
+
this.unredactedFields.clear();
|
|
12979
|
+
// Filter out password fields (they cannot be unredacted)
|
|
12980
|
+
const validFields = fields.filter(field => {
|
|
12981
|
+
const isPasswordField = this.isPasswordSelector(field);
|
|
12982
|
+
if (isPasswordField) {
|
|
12983
|
+
logWarn(`Cannot unredact password field: ${field} - Password fields are always protected`);
|
|
12984
|
+
return false;
|
|
12985
|
+
}
|
|
12986
|
+
return true;
|
|
13020
12987
|
});
|
|
13021
|
-
|
|
13022
|
-
if (
|
|
13023
|
-
|
|
13024
|
-
inputData.attributes.value = this.redactedText;
|
|
13025
|
-
logDebug('Redaction: Redacted nested value attribute');
|
|
13026
|
-
}
|
|
13027
|
-
}
|
|
13028
|
-
logDebug('Redaction: Input event redaction complete');
|
|
13029
|
-
}
|
|
13030
|
-
/**
|
|
13031
|
-
* Redact sensitive data in DOM mutation events
|
|
13032
|
-
*/
|
|
13033
|
-
redactDOMEvent(domData) {
|
|
13034
|
-
// Check for text changes in DOM mutations
|
|
13035
|
-
if (domData.texts && Array.isArray(domData.texts)) {
|
|
13036
|
-
domData.texts.forEach((textChange) => {
|
|
13037
|
-
if (textChange.text && typeof textChange.text === 'string' &&
|
|
13038
|
-
this.shouldRedactDOMChange(textChange)) {
|
|
13039
|
-
textChange.text = this.redactedText;
|
|
13040
|
-
}
|
|
13041
|
-
});
|
|
13042
|
-
}
|
|
13043
|
-
// Also check for attribute changes that might contain input data
|
|
13044
|
-
if (domData.attributes && Array.isArray(domData.attributes)) {
|
|
13045
|
-
domData.attributes.forEach((attrChange) => {
|
|
13046
|
-
if (attrChange.attributes && attrChange.attributes.value &&
|
|
13047
|
-
typeof attrChange.attributes.value === 'string' &&
|
|
13048
|
-
this.shouldRedactDOMChange(attrChange)) {
|
|
13049
|
-
attrChange.attributes.value = this.redactedText;
|
|
13050
|
-
}
|
|
13051
|
-
});
|
|
12988
|
+
validFields.forEach(field => this.unredactedFields.add(field));
|
|
12989
|
+
if (validFields.length > 0) {
|
|
12990
|
+
logDebug(`Unredaction: Active for ${validFields.length} field(s):`, validFields);
|
|
13052
12991
|
}
|
|
13053
|
-
|
|
13054
|
-
|
|
13055
|
-
domData.adds.forEach((add) => {
|
|
13056
|
-
if (add.node && add.node.textContent && typeof add.node.textContent === 'string' &&
|
|
13057
|
-
this.shouldRedactDOMChange(add)) {
|
|
13058
|
-
add.node.textContent = this.redactedText;
|
|
13059
|
-
}
|
|
13060
|
-
});
|
|
12992
|
+
else {
|
|
12993
|
+
logDebug('Unredaction: No valid fields to unredact');
|
|
13061
12994
|
}
|
|
12995
|
+
this.applyUnredactionClasses();
|
|
13062
12996
|
}
|
|
13063
12997
|
/**
|
|
13064
|
-
*
|
|
12998
|
+
* Remove specific fields from unredaction (they become redacted again)
|
|
12999
|
+
* @param fields Array of CSS selectors for fields to redact
|
|
13065
13000
|
*/
|
|
13066
|
-
|
|
13067
|
-
|
|
13068
|
-
|
|
13069
|
-
|
|
13070
|
-
|
|
13071
|
-
|
|
13072
|
-
if (elementId !== undefined) {
|
|
13073
|
-
// Try to find the element by data-rrweb-id attribute
|
|
13074
|
-
let element = document.querySelector(`[data-rrweb-id="${elementId}"]`);
|
|
13075
|
-
if (element) {
|
|
13076
|
-
return this.shouldRedactElement(element);
|
|
13077
|
-
}
|
|
13078
|
-
}
|
|
13079
|
-
// Also check for nodeId which is another way rrweb identifies elements
|
|
13080
|
-
const nodeId = changeData.nodeId;
|
|
13081
|
-
if (nodeId !== undefined) {
|
|
13082
|
-
const element = document.querySelector(`[data-rrweb-id="${nodeId}"]`);
|
|
13083
|
-
if (element) {
|
|
13084
|
-
return this.shouldRedactElement(element);
|
|
13085
|
-
}
|
|
13086
|
-
}
|
|
13087
|
-
return false;
|
|
13001
|
+
redactFields(fields) {
|
|
13002
|
+
fields.forEach(field => {
|
|
13003
|
+
this.unredactedFields.delete(field);
|
|
13004
|
+
});
|
|
13005
|
+
if (this.unredactedFields.size > 0) {
|
|
13006
|
+
logDebug(`Unredaction: Removed ${fields.length} field(s), ${this.unredactedFields.size} remaining:`, Array.from(this.unredactedFields));
|
|
13088
13007
|
}
|
|
13089
|
-
|
|
13090
|
-
|
|
13091
|
-
return false;
|
|
13008
|
+
else {
|
|
13009
|
+
logDebug('Unredaction: All fields redacted');
|
|
13092
13010
|
}
|
|
13011
|
+
this.applyUnredactionClasses();
|
|
13093
13012
|
}
|
|
13094
13013
|
/**
|
|
13095
|
-
*
|
|
13014
|
+
* Clear all unredacted fields (everything becomes redacted again)
|
|
13096
13015
|
*/
|
|
13097
|
-
|
|
13098
|
-
|
|
13099
|
-
|
|
13100
|
-
|
|
13101
|
-
mouseData.text = this.redactedText;
|
|
13102
|
-
}
|
|
13016
|
+
clearUnredactedFields() {
|
|
13017
|
+
this.unredactedFields.clear();
|
|
13018
|
+
logDebug('Unredaction: All fields cleared, everything redacted');
|
|
13019
|
+
this.removeUnredactionClasses();
|
|
13103
13020
|
}
|
|
13104
13021
|
/**
|
|
13105
|
-
*
|
|
13022
|
+
* Check if any fields are currently unredacted
|
|
13106
13023
|
*/
|
|
13107
|
-
|
|
13108
|
-
|
|
13109
|
-
this.redactNode(snapshotData.node);
|
|
13110
|
-
}
|
|
13024
|
+
hasUnredactedFields() {
|
|
13025
|
+
return this.unredactedFields.size > 0;
|
|
13111
13026
|
}
|
|
13112
13027
|
/**
|
|
13113
|
-
*
|
|
13028
|
+
* Get the current redaction mode
|
|
13114
13029
|
*/
|
|
13115
|
-
|
|
13116
|
-
|
|
13117
|
-
return;
|
|
13118
|
-
// Check if this node should be redacted
|
|
13119
|
-
if (node.type === 2 && node.tagName &&
|
|
13120
|
-
(node.tagName.toLowerCase() === 'input' || node.tagName.toLowerCase() === 'textarea')) {
|
|
13121
|
-
// Check if this input/textarea should be redacted
|
|
13122
|
-
if (this.shouldRedactNode(node)) {
|
|
13123
|
-
// Redact value attribute
|
|
13124
|
-
if (node.attributes && node.attributes.value) {
|
|
13125
|
-
node.attributes.value = this.redactedText;
|
|
13126
|
-
}
|
|
13127
|
-
// Redact text content
|
|
13128
|
-
if (node.textContent) {
|
|
13129
|
-
node.textContent = this.redactedText;
|
|
13130
|
-
}
|
|
13131
|
-
}
|
|
13132
|
-
}
|
|
13133
|
-
// Recursively process child nodes
|
|
13134
|
-
if (node.childNodes && Array.isArray(node.childNodes)) {
|
|
13135
|
-
node.childNodes.forEach((childNode) => {
|
|
13136
|
-
this.redactNode(childNode);
|
|
13137
|
-
});
|
|
13138
|
-
}
|
|
13030
|
+
getRedactionMode() {
|
|
13031
|
+
return this.redactionMode;
|
|
13139
13032
|
}
|
|
13140
13033
|
/**
|
|
13141
|
-
*
|
|
13034
|
+
* Get the currently unredacted fields
|
|
13142
13035
|
*/
|
|
13143
|
-
|
|
13144
|
-
|
|
13145
|
-
return false;
|
|
13146
|
-
// Check if any of our selectors would match this node
|
|
13147
|
-
for (const selector of this.userSelectedFields) {
|
|
13148
|
-
if (this.selectorMatchesNode(selector, node)) {
|
|
13149
|
-
return true;
|
|
13150
|
-
}
|
|
13151
|
-
}
|
|
13152
|
-
return false;
|
|
13036
|
+
getUnredactedFields() {
|
|
13037
|
+
return Array.from(this.unredactedFields);
|
|
13153
13038
|
}
|
|
13154
13039
|
/**
|
|
13155
|
-
*
|
|
13156
|
-
|
|
13157
|
-
selectorMatchesNode(selector, node) {
|
|
13158
|
-
if (!node.attributes)
|
|
13159
|
-
return false;
|
|
13160
|
-
// Create a temporary element to test the selector
|
|
13161
|
-
try {
|
|
13162
|
-
const tempElement = document.createElement(node.tagName || 'div');
|
|
13163
|
-
// Copy attributes from the node to the temp element
|
|
13164
|
-
if (node.attributes) {
|
|
13165
|
-
Object.keys(node.attributes).forEach(key => {
|
|
13166
|
-
tempElement.setAttribute(key, node.attributes[key]);
|
|
13167
|
-
});
|
|
13168
|
-
}
|
|
13169
|
-
// Test if the selector matches this element
|
|
13170
|
-
return tempElement.matches(selector);
|
|
13171
|
-
}
|
|
13172
|
-
catch (e) {
|
|
13173
|
-
// If matches() is not supported or fails, fall back to basic attribute checking
|
|
13174
|
-
return this.basicSelectorMatch(selector, node);
|
|
13175
|
-
}
|
|
13176
|
-
}
|
|
13177
|
-
/**
|
|
13178
|
-
* Basic selector matching for environments where matches() is not available
|
|
13040
|
+
* Get CSS selectors for rrweb masking configuration
|
|
13041
|
+
* Returns null if no fields are unredacted (everything stays redacted)
|
|
13179
13042
|
*/
|
|
13180
|
-
|
|
13181
|
-
if (
|
|
13182
|
-
|
|
13183
|
-
|
|
13184
|
-
|
|
13185
|
-
const typeMatch = selector.match(/input\[type="([^"]+)"\]/);
|
|
13186
|
-
if (typeMatch && node.tagName === 'input' && node.attributes.type === typeMatch[1]) {
|
|
13187
|
-
return true;
|
|
13043
|
+
getMaskTextSelector() {
|
|
13044
|
+
if (this.redactionMode === 'privacy-first') {
|
|
13045
|
+
// Privacy-first: mask everything except unredacted fields
|
|
13046
|
+
if (this.unredactedFields.size === 0) {
|
|
13047
|
+
return null; // Everything stays redacted
|
|
13188
13048
|
}
|
|
13049
|
+
return Array.from(this.unredactedFields).join(',');
|
|
13189
13050
|
}
|
|
13190
|
-
|
|
13191
|
-
|
|
13192
|
-
|
|
13193
|
-
|
|
13194
|
-
}
|
|
13195
|
-
// Handle class selectors like '.sensitive-field'
|
|
13196
|
-
if (selector.startsWith('.')) {
|
|
13197
|
-
const className = selector.substring(1);
|
|
13198
|
-
return node.attributes.class && node.attributes.class.includes(className);
|
|
13199
|
-
}
|
|
13200
|
-
// Handle tag selectors like 'input'
|
|
13201
|
-
if (!selector.includes('[') && !selector.includes('.')) {
|
|
13202
|
-
return node.tagName && node.tagName.toLowerCase() === selector.toLowerCase();
|
|
13203
|
-
}
|
|
13204
|
-
return false;
|
|
13205
|
-
}
|
|
13206
|
-
/**
|
|
13207
|
-
* Check if an event is from a field that should be redacted
|
|
13208
|
-
*/
|
|
13209
|
-
isFieldSelected(eventData) {
|
|
13210
|
-
if (!isBrowser$2)
|
|
13211
|
-
return false;
|
|
13212
|
-
try {
|
|
13213
|
-
// For input events (source 5), we need to determine if this is a sensitive field
|
|
13214
|
-
if (eventData.source === 5) { // Input event
|
|
13215
|
-
const elementId = eventData.id;
|
|
13216
|
-
if (elementId !== undefined) {
|
|
13217
|
-
// Try to find the element by data-rrweb-id attribute
|
|
13218
|
-
let element = document.querySelector(`[data-rrweb-id="${elementId}"]`);
|
|
13219
|
-
if (element) {
|
|
13220
|
-
return this.shouldRedactElement(element);
|
|
13221
|
-
}
|
|
13222
|
-
// Fallback: Try to find by nodeId if available
|
|
13223
|
-
if (eventData.nodeId !== undefined) {
|
|
13224
|
-
element = document.querySelector(`[data-rrweb-id="${eventData.nodeId}"]`);
|
|
13225
|
-
if (element) {
|
|
13226
|
-
return this.shouldRedactElement(element);
|
|
13227
|
-
}
|
|
13228
|
-
}
|
|
13229
|
-
// More aggressive approach: Check all elements that match our selectors
|
|
13230
|
-
// and see if any of them are currently focused or have the same ID
|
|
13231
|
-
for (const selector of this.userSelectedFields) {
|
|
13232
|
-
const matchingElements = document.querySelectorAll(selector);
|
|
13233
|
-
if (matchingElements.length > 0) {
|
|
13234
|
-
// Check if any of these elements are currently focused
|
|
13235
|
-
for (const el of matchingElements) {
|
|
13236
|
-
if (el === document.activeElement) {
|
|
13237
|
-
logDebug('Redaction: Found focused element matching selector:', selector);
|
|
13238
|
-
return true;
|
|
13239
|
-
}
|
|
13240
|
-
}
|
|
13241
|
-
}
|
|
13242
|
-
}
|
|
13243
|
-
// If we still can't find it, try a more direct approach
|
|
13244
|
-
// Look for any input element that might be the active one
|
|
13245
|
-
const activeElement = document.activeElement;
|
|
13246
|
-
if (activeElement && this.shouldRedactElement(activeElement)) {
|
|
13247
|
-
logDebug('Redaction: Active element should be redacted');
|
|
13248
|
-
return true;
|
|
13249
|
-
}
|
|
13250
|
-
return false;
|
|
13251
|
-
}
|
|
13252
|
-
}
|
|
13253
|
-
// For other event types, try to find the element
|
|
13254
|
-
const elementId = eventData.id;
|
|
13255
|
-
if (elementId !== undefined) {
|
|
13256
|
-
// First try to find by data-rrweb-id attribute
|
|
13257
|
-
let element = document.querySelector(`[data-rrweb-id="${elementId}"]`);
|
|
13258
|
-
if (element) {
|
|
13259
|
-
return this.shouldRedactElement(element);
|
|
13260
|
-
}
|
|
13261
|
-
}
|
|
13262
|
-
// Also check for nodeId which is another way rrweb identifies elements
|
|
13263
|
-
const nodeId = eventData.nodeId;
|
|
13264
|
-
if (nodeId !== undefined) {
|
|
13265
|
-
const element = document.querySelector(`[data-rrweb-id="${nodeId}"]`);
|
|
13266
|
-
if (element) {
|
|
13267
|
-
return this.shouldRedactElement(element);
|
|
13268
|
-
}
|
|
13269
|
-
}
|
|
13270
|
-
// For DOM mutations, check if the target element should be redacted
|
|
13271
|
-
if (eventData.target && eventData.target.id) {
|
|
13272
|
-
const element = document.querySelector(`[data-rrweb-id="${eventData.target.id}"]`);
|
|
13273
|
-
if (element) {
|
|
13274
|
-
return this.shouldRedactElement(element);
|
|
13275
|
-
}
|
|
13051
|
+
else {
|
|
13052
|
+
// Visibility-first: mask only redacted fields
|
|
13053
|
+
if (this.redactedFields.size === 0) {
|
|
13054
|
+
return null; // Nothing to redact
|
|
13276
13055
|
}
|
|
13277
|
-
return
|
|
13278
|
-
}
|
|
13279
|
-
catch (e) {
|
|
13280
|
-
logWarn('Error checking if field should be redacted:', e);
|
|
13281
|
-
return false;
|
|
13282
|
-
}
|
|
13283
|
-
}
|
|
13284
|
-
/**
|
|
13285
|
-
* Get CSS selectors for rrweb masking configuration
|
|
13286
|
-
* Used to configure rrweb's maskTextSelector option
|
|
13287
|
-
*/
|
|
13288
|
-
getMaskTextSelector() {
|
|
13289
|
-
if (this.userSelectedFields.size === 0) {
|
|
13290
|
-
return null;
|
|
13056
|
+
return Array.from(this.redactedFields).join(',');
|
|
13291
13057
|
}
|
|
13292
|
-
return Array.from(this.userSelectedFields).join(',');
|
|
13293
13058
|
}
|
|
13294
13059
|
/**
|
|
13295
|
-
*
|
|
13060
|
+
* Apply redaction classes to DOM elements (for visibility-first mode)
|
|
13061
|
+
* Adds 'rr-mask' class to elements that should be redacted
|
|
13296
13062
|
*/
|
|
13297
|
-
|
|
13298
|
-
if (this.
|
|
13299
|
-
return
|
|
13063
|
+
applyRedactionClasses() {
|
|
13064
|
+
if (this.redactedFields.size === 0) {
|
|
13065
|
+
return;
|
|
13300
13066
|
}
|
|
13301
|
-
//
|
|
13302
|
-
|
|
13067
|
+
// Add 'rr-mask' class to redacted elements
|
|
13068
|
+
this.redactedFields.forEach(selector => {
|
|
13303
13069
|
try {
|
|
13304
|
-
|
|
13305
|
-
|
|
13306
|
-
|
|
13070
|
+
const elements = document.querySelectorAll(selector);
|
|
13071
|
+
elements.forEach(element => {
|
|
13072
|
+
element.classList.add('rr-mask');
|
|
13073
|
+
});
|
|
13074
|
+
logDebug(`Added rr-mask class to ${elements.length} element(s) for selector: ${selector}`);
|
|
13307
13075
|
}
|
|
13308
13076
|
catch (e) {
|
|
13309
|
-
// Invalid selector, skip
|
|
13310
13077
|
logWarn(`Invalid selector: ${selector}`);
|
|
13311
13078
|
}
|
|
13312
|
-
}
|
|
13313
|
-
return false;
|
|
13079
|
+
});
|
|
13314
13080
|
}
|
|
13315
13081
|
/**
|
|
13316
|
-
* Apply
|
|
13317
|
-
*
|
|
13318
|
-
* This enables rrweb's built-in masking functionality
|
|
13082
|
+
* Apply unredaction classes to DOM elements
|
|
13083
|
+
* Removes 'rr-mask' class from elements that should be unredacted
|
|
13319
13084
|
*/
|
|
13320
|
-
|
|
13321
|
-
if (this.
|
|
13085
|
+
applyUnredactionClasses() {
|
|
13086
|
+
if (this.unredactedFields.size === 0) {
|
|
13322
13087
|
return;
|
|
13323
13088
|
}
|
|
13324
|
-
// Remove
|
|
13325
|
-
|
|
13326
|
-
element.classList.remove('rr-mask');
|
|
13327
|
-
});
|
|
13328
|
-
// Add redaction classes to matching elements
|
|
13329
|
-
this.userSelectedFields.forEach(selector => {
|
|
13089
|
+
// Remove 'rr-mask' class from unredacted elements
|
|
13090
|
+
this.unredactedFields.forEach(selector => {
|
|
13330
13091
|
try {
|
|
13331
13092
|
const elements = document.querySelectorAll(selector);
|
|
13332
13093
|
elements.forEach(element => {
|
|
13333
|
-
element.classList.
|
|
13094
|
+
element.classList.remove('rr-mask');
|
|
13334
13095
|
});
|
|
13335
|
-
logDebug(`
|
|
13096
|
+
logDebug(`Removed rr-mask class from ${elements.length} element(s) for selector: ${selector}`);
|
|
13336
13097
|
}
|
|
13337
13098
|
catch (e) {
|
|
13338
13099
|
logWarn(`Invalid selector: ${selector}`);
|
|
@@ -13340,7 +13101,26 @@ class RedactionManager {
|
|
|
13340
13101
|
});
|
|
13341
13102
|
}
|
|
13342
13103
|
/**
|
|
13343
|
-
*
|
|
13104
|
+
* Remove all unredaction classes from DOM elements
|
|
13105
|
+
*/
|
|
13106
|
+
removeUnredactionClasses() {
|
|
13107
|
+
// Note: This doesn't add 'rr-mask' classes back - rrweb handles that automatically
|
|
13108
|
+
logDebug('Unredaction classes removed');
|
|
13109
|
+
}
|
|
13110
|
+
/**
|
|
13111
|
+
* Check if a selector represents a password field
|
|
13112
|
+
*/
|
|
13113
|
+
isPasswordSelector(selector) {
|
|
13114
|
+
const passwordPatterns = [
|
|
13115
|
+
'input[type="password"]',
|
|
13116
|
+
'input[type="password" i]',
|
|
13117
|
+
'[type="password"]',
|
|
13118
|
+
'[type="password" i]'
|
|
13119
|
+
];
|
|
13120
|
+
return passwordPatterns.some(pattern => selector.toLowerCase().includes(pattern.toLowerCase().replace(/[\[\]]/g, '')));
|
|
13121
|
+
}
|
|
13122
|
+
/**
|
|
13123
|
+
* Get the original value of an element (for debugging)
|
|
13344
13124
|
*/
|
|
13345
13125
|
getOriginalValue(element) {
|
|
13346
13126
|
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
@@ -13349,10 +13129,51 @@ class RedactionManager {
|
|
|
13349
13129
|
return undefined;
|
|
13350
13130
|
}
|
|
13351
13131
|
/**
|
|
13352
|
-
* Check if an element is currently
|
|
13132
|
+
* Check if an element is currently unredacted
|
|
13353
13133
|
*/
|
|
13354
|
-
|
|
13355
|
-
return this.
|
|
13134
|
+
isElementUnredacted(element) {
|
|
13135
|
+
return this.shouldUnredactElement(element);
|
|
13136
|
+
}
|
|
13137
|
+
/**
|
|
13138
|
+
* Check if an element should be unredacted
|
|
13139
|
+
*/
|
|
13140
|
+
shouldUnredactElement(element) {
|
|
13141
|
+
if (this.redactionMode === 'privacy-first') {
|
|
13142
|
+
// Privacy-first: check if element is in unredacted fields
|
|
13143
|
+
if (this.unredactedFields.size === 0) {
|
|
13144
|
+
return false; // Nothing unredacted
|
|
13145
|
+
}
|
|
13146
|
+
// Check if any selector matches this element
|
|
13147
|
+
for (const selector of this.unredactedFields) {
|
|
13148
|
+
try {
|
|
13149
|
+
if (element.matches(selector)) {
|
|
13150
|
+
return true;
|
|
13151
|
+
}
|
|
13152
|
+
}
|
|
13153
|
+
catch (e) {
|
|
13154
|
+
logWarn(`Invalid selector: ${selector}`);
|
|
13155
|
+
}
|
|
13156
|
+
}
|
|
13157
|
+
return false;
|
|
13158
|
+
}
|
|
13159
|
+
else {
|
|
13160
|
+
// Visibility-first: check if element is NOT in redacted fields
|
|
13161
|
+
if (this.redactedFields.size === 0) {
|
|
13162
|
+
return true; // Nothing redacted, everything visible
|
|
13163
|
+
}
|
|
13164
|
+
// Check if any selector matches this element
|
|
13165
|
+
for (const selector of this.redactedFields) {
|
|
13166
|
+
try {
|
|
13167
|
+
if (element.matches(selector)) {
|
|
13168
|
+
return false; // Element is redacted
|
|
13169
|
+
}
|
|
13170
|
+
}
|
|
13171
|
+
catch (e) {
|
|
13172
|
+
logWarn(`Invalid selector: ${selector}`);
|
|
13173
|
+
}
|
|
13174
|
+
}
|
|
13175
|
+
return true; // Element is not redacted
|
|
13176
|
+
}
|
|
13356
13177
|
}
|
|
13357
13178
|
}
|
|
13358
13179
|
// Export a default instance
|
|
@@ -13921,11 +13742,22 @@ class HumanBehaviorTracker {
|
|
|
13921
13742
|
});
|
|
13922
13743
|
// Store canvas recording preference
|
|
13923
13744
|
tracker.recordCanvas = (_a = options === null || options === void 0 ? void 0 : options.recordCanvas) !== null && _a !== void 0 ? _a : false;
|
|
13924
|
-
// Set
|
|
13745
|
+
// Set unredacted fields if specified (legacy support)
|
|
13925
13746
|
if (options === null || options === void 0 ? void 0 : options.redactFields) {
|
|
13926
|
-
tracker.
|
|
13927
|
-
|
|
13928
|
-
|
|
13747
|
+
tracker.setUnredactedFields(options.redactFields);
|
|
13748
|
+
}
|
|
13749
|
+
// Handle new redaction strategy
|
|
13750
|
+
if (options === null || options === void 0 ? void 0 : options.redactionStrategy) {
|
|
13751
|
+
if (options.redactionStrategy.mode === 'privacy-first') {
|
|
13752
|
+
if (options.redactionStrategy.unredactFields) {
|
|
13753
|
+
tracker.setUnredactedFields(options.redactionStrategy.unredactFields);
|
|
13754
|
+
}
|
|
13755
|
+
}
|
|
13756
|
+
else {
|
|
13757
|
+
if (options.redactionStrategy.redactFields) {
|
|
13758
|
+
tracker.setRedactedFields(options.redactionStrategy.redactFields);
|
|
13759
|
+
}
|
|
13760
|
+
}
|
|
13929
13761
|
}
|
|
13930
13762
|
// Setup automatic tracking if enabled
|
|
13931
13763
|
if ((options === null || options === void 0 ? void 0 : options.enableAutomaticTracking) !== false) {
|
|
@@ -13972,7 +13804,10 @@ class HumanBehaviorTracker {
|
|
|
13972
13804
|
ingestionUrl: ingestionUrl || defaultIngestionUrl
|
|
13973
13805
|
});
|
|
13974
13806
|
this.apiKey = apiKey;
|
|
13975
|
-
this.redactionManager = new RedactionManager(
|
|
13807
|
+
this.redactionManager = new RedactionManager({
|
|
13808
|
+
redactionStrategy: options === null || options === void 0 ? void 0 : options.redactionStrategy,
|
|
13809
|
+
legacyRedactFields: options === null || options === void 0 ? void 0 : options.redactFields // For backward compatibility
|
|
13810
|
+
});
|
|
13976
13811
|
// Initialize property manager
|
|
13977
13812
|
this.propertyManager = new PropertyManager({
|
|
13978
13813
|
enableAutomaticProperties: (options === null || options === void 0 ? void 0 : options.enableAutomaticProperties) !== false,
|
|
@@ -14550,6 +14385,31 @@ class HumanBehaviorTracker {
|
|
|
14550
14385
|
if (!userResponse.ok) {
|
|
14551
14386
|
throw new Error(`Failed to identify user: ${userResponse.statusText}`);
|
|
14552
14387
|
}
|
|
14388
|
+
// Get IP info and GeoIP data
|
|
14389
|
+
try {
|
|
14390
|
+
const ipResponse = yield fetch(`${this.api['baseUrl']}/api/ingestion/ip-info`, {
|
|
14391
|
+
method: 'POST',
|
|
14392
|
+
headers: {
|
|
14393
|
+
'Content-Type': 'application/json',
|
|
14394
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
14395
|
+
},
|
|
14396
|
+
body: JSON.stringify({
|
|
14397
|
+
sessionId: this.sessionId,
|
|
14398
|
+
clientIP: null, // Let server detect from headers
|
|
14399
|
+
ipDetectionMethod: 'headers',
|
|
14400
|
+
timestamp: new Date().toISOString()
|
|
14401
|
+
})
|
|
14402
|
+
});
|
|
14403
|
+
if (ipResponse.ok) {
|
|
14404
|
+
logDebug('✅ IP info and GeoIP data retrieved successfully');
|
|
14405
|
+
}
|
|
14406
|
+
else {
|
|
14407
|
+
logDebug(`⚠️ IP info request failed: ${ipResponse.statusText}`);
|
|
14408
|
+
}
|
|
14409
|
+
}
|
|
14410
|
+
catch (error) {
|
|
14411
|
+
logDebug(`⚠️ IP info request error: ${error}`);
|
|
14412
|
+
}
|
|
14553
14413
|
// Don't update endUserId - keep it as the original UUID
|
|
14554
14414
|
return originalEndUserId || '';
|
|
14555
14415
|
});
|
|
@@ -14589,7 +14449,7 @@ class HumanBehaviorTracker {
|
|
|
14589
14449
|
// ✅ HUMANBEHAVIOR'S CUSTOM SETTINGS
|
|
14590
14450
|
maskTextSelector: this.redactionManager.getMaskTextSelector() || undefined,
|
|
14591
14451
|
maskTextFn: undefined,
|
|
14592
|
-
maskAllInputs:
|
|
14452
|
+
maskAllInputs: this.redactionManager.getRedactionMode() === 'privacy-first', // Configurable based on strategy
|
|
14593
14453
|
maskInputOptions: { password: true }, // HumanBehavior default
|
|
14594
14454
|
maskInputFn: undefined,
|
|
14595
14455
|
slimDOMOptions: {},
|
|
@@ -14899,15 +14759,23 @@ class HumanBehaviorTracker {
|
|
|
14899
14759
|
});
|
|
14900
14760
|
}
|
|
14901
14761
|
/**
|
|
14902
|
-
* Set specific fields to be redacted
|
|
14903
|
-
*
|
|
14904
|
-
* @param fields Array of CSS selectors for fields to redact (e.g., ['input[type="password"]', '#email-field'])
|
|
14762
|
+
* Set specific fields to be redacted (for visibility-first mode)
|
|
14763
|
+
* @param fields Array of CSS selectors for fields to redact
|
|
14905
14764
|
*/
|
|
14906
14765
|
setRedactedFields(fields) {
|
|
14907
14766
|
this.redactionManager.setFieldsToRedact(fields);
|
|
14908
|
-
// ✅
|
|
14909
|
-
this.
|
|
14910
|
-
|
|
14767
|
+
// ✅ RESTART RECORDING WITH NEW SETTINGS - Ensures redaction is applied
|
|
14768
|
+
if (this.recordInstance) {
|
|
14769
|
+
this.restartWithNewRedaction();
|
|
14770
|
+
}
|
|
14771
|
+
}
|
|
14772
|
+
/**
|
|
14773
|
+
* Set specific fields to be unredacted (everything else stays redacted by rrweb)
|
|
14774
|
+
* @param fields Array of CSS selectors for fields to unredact (e.g., ['#username', '#comment'])
|
|
14775
|
+
*/
|
|
14776
|
+
setUnredactedFields(fields) {
|
|
14777
|
+
this.redactionManager.setFieldsToUnredact(fields);
|
|
14778
|
+
// ✅ RESTART RECORDING WITH NEW SETTINGS - Ensures unredaction is applied
|
|
14911
14779
|
if (this.recordInstance) {
|
|
14912
14780
|
this.restartWithNewRedaction();
|
|
14913
14781
|
}
|
|
@@ -14919,16 +14787,37 @@ class HumanBehaviorTracker {
|
|
|
14919
14787
|
}
|
|
14920
14788
|
}
|
|
14921
14789
|
/**
|
|
14922
|
-
* Check if
|
|
14790
|
+
* Check if any fields are currently unredacted
|
|
14923
14791
|
*/
|
|
14924
|
-
|
|
14925
|
-
return this.redactionManager.
|
|
14792
|
+
hasUnredactedFields() {
|
|
14793
|
+
return this.redactionManager.hasUnredactedFields();
|
|
14926
14794
|
}
|
|
14927
14795
|
/**
|
|
14928
|
-
* Get the currently
|
|
14796
|
+
* Get the currently unredacted fields
|
|
14929
14797
|
*/
|
|
14930
|
-
|
|
14931
|
-
return this.redactionManager.
|
|
14798
|
+
getUnredactedFields() {
|
|
14799
|
+
return this.redactionManager.getUnredactedFields();
|
|
14800
|
+
}
|
|
14801
|
+
/**
|
|
14802
|
+
* Remove specific fields from unredaction (they become redacted again)
|
|
14803
|
+
* @param fields Array of CSS selectors for fields to redact
|
|
14804
|
+
*/
|
|
14805
|
+
redactFields(fields) {
|
|
14806
|
+
this.redactionManager.redactFields(fields);
|
|
14807
|
+
// ✅ RESTART RECORDING WITH NEW SETTINGS - Ensures redaction is updated
|
|
14808
|
+
if (this.recordInstance) {
|
|
14809
|
+
this.restartWithNewRedaction();
|
|
14810
|
+
}
|
|
14811
|
+
}
|
|
14812
|
+
/**
|
|
14813
|
+
* Clear all unredacted fields (everything becomes redacted again)
|
|
14814
|
+
*/
|
|
14815
|
+
clearUnredactedFields() {
|
|
14816
|
+
this.redactionManager.clearUnredactedFields();
|
|
14817
|
+
// ✅ RESTART RECORDING WITH NEW SETTINGS - Ensures redaction is updated
|
|
14818
|
+
if (this.recordInstance) {
|
|
14819
|
+
this.restartWithNewRedaction();
|
|
14820
|
+
}
|
|
14932
14821
|
}
|
|
14933
14822
|
/**
|
|
14934
14823
|
* Get the current session ID
|