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