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/react/index.cjs
CHANGED
|
@@ -12886,15 +12886,15 @@ class HumanBehaviorAPI {
|
|
|
12886
12886
|
}
|
|
12887
12887
|
}
|
|
12888
12888
|
|
|
12889
|
-
//
|
|
12890
|
-
//
|
|
12891
|
-
//
|
|
12892
|
-
// Check if we're in a browser environment
|
|
12893
|
-
const isBrowser$3 = typeof window !== 'undefined';
|
|
12889
|
+
// Simplified redaction functionality for HumanBehavior SDK
|
|
12890
|
+
// Since rrweb auto-redacts all input fields by default, this module only handles
|
|
12891
|
+
// selectively unredacting specific fields (except passwords which remain protected)
|
|
12894
12892
|
class RedactionManager {
|
|
12895
12893
|
constructor(options) {
|
|
12896
12894
|
this.redactedText = '[REDACTED]';
|
|
12897
|
-
this.
|
|
12895
|
+
this.unredactedFields = new Set(); // Fields that user wants to unredact
|
|
12896
|
+
this.redactedFields = new Set(); // Fields that user wants to redact
|
|
12897
|
+
this.redactionMode = 'privacy-first';
|
|
12898
12898
|
this.excludeSelectors = [
|
|
12899
12899
|
'[data-no-redact="true"]',
|
|
12900
12900
|
'.human-behavior-no-redact'
|
|
@@ -12905,419 +12905,180 @@ class RedactionManager {
|
|
|
12905
12905
|
if (options === null || options === void 0 ? void 0 : options.excludeSelectors) {
|
|
12906
12906
|
this.excludeSelectors = [...this.excludeSelectors, ...options.excludeSelectors];
|
|
12907
12907
|
}
|
|
12908
|
+
// Handle new redaction strategy
|
|
12909
|
+
if (options === null || options === void 0 ? void 0 : options.redactionStrategy) {
|
|
12910
|
+
this.redactionMode = options.redactionStrategy.mode;
|
|
12911
|
+
if (this.redactionMode === 'privacy-first') {
|
|
12912
|
+
// Privacy-first: everything redacted by default, unredact specific fields
|
|
12913
|
+
if (options.redactionStrategy.unredactFields) {
|
|
12914
|
+
this.setFieldsToUnredact(options.redactionStrategy.unredactFields);
|
|
12915
|
+
}
|
|
12916
|
+
}
|
|
12917
|
+
else {
|
|
12918
|
+
// Visibility-first: everything visible by default, redact specific fields
|
|
12919
|
+
if (options.redactionStrategy.redactFields) {
|
|
12920
|
+
this.setFieldsToRedact(options.redactionStrategy.redactFields);
|
|
12921
|
+
}
|
|
12922
|
+
}
|
|
12923
|
+
}
|
|
12924
|
+
// Handle legacy redactFields (backward compatibility)
|
|
12925
|
+
if (options === null || options === void 0 ? void 0 : options.legacyRedactFields) {
|
|
12926
|
+
this.setFieldsToUnredact(options.legacyRedactFields);
|
|
12927
|
+
}
|
|
12928
|
+
// Handle legacy userFields
|
|
12908
12929
|
if (options === null || options === void 0 ? void 0 : options.userFields) {
|
|
12909
|
-
this.
|
|
12930
|
+
this.setFieldsToUnredact(options.userFields);
|
|
12910
12931
|
}
|
|
12911
12932
|
}
|
|
12912
12933
|
/**
|
|
12913
|
-
* Set specific fields to be redacted
|
|
12914
|
-
* These selectors are used to configure rrweb's built-in masking
|
|
12934
|
+
* Set specific fields to be redacted (for visibility-first mode)
|
|
12915
12935
|
* @param fields Array of CSS selectors for fields to redact
|
|
12916
12936
|
*/
|
|
12917
12937
|
setFieldsToRedact(fields) {
|
|
12918
|
-
this.
|
|
12919
|
-
fields
|
|
12920
|
-
|
|
12921
|
-
|
|
12922
|
-
|
|
12923
|
-
|
|
12924
|
-
|
|
12925
|
-
|
|
12926
|
-
|
|
12927
|
-
|
|
12928
|
-
|
|
12929
|
-
|
|
12938
|
+
this.redactedFields.clear();
|
|
12939
|
+
// Always include password fields in redacted list
|
|
12940
|
+
const passwordFields = [
|
|
12941
|
+
'input[type="password"]',
|
|
12942
|
+
'input[type="password" i]',
|
|
12943
|
+
'[type="password"]',
|
|
12944
|
+
'[type="password" i]'
|
|
12945
|
+
];
|
|
12946
|
+
// Add password fields and user-specified fields
|
|
12947
|
+
[...passwordFields, ...fields].forEach(field => {
|
|
12948
|
+
this.redactedFields.add(field);
|
|
12949
|
+
});
|
|
12950
|
+
if (this.redactedFields.size > 0) {
|
|
12951
|
+
logDebug(`Redaction: Active for ${this.redactedFields.size} field(s):`, Array.from(this.redactedFields));
|
|
12930
12952
|
}
|
|
12931
12953
|
else {
|
|
12932
|
-
logDebug('Redaction:
|
|
12933
|
-
}
|
|
12934
|
-
}
|
|
12935
|
-
/**
|
|
12936
|
-
* Check if redaction is currently active (has fields selected)
|
|
12937
|
-
*/
|
|
12938
|
-
isActive() {
|
|
12939
|
-
return this.userSelectedFields.size > 0;
|
|
12940
|
-
}
|
|
12941
|
-
/**
|
|
12942
|
-
* Get the currently selected fields for redaction
|
|
12943
|
-
*/
|
|
12944
|
-
getSelectedFields() {
|
|
12945
|
-
return Array.from(this.userSelectedFields);
|
|
12946
|
-
}
|
|
12947
|
-
/**
|
|
12948
|
-
* Process an event and redact sensitive data if needed
|
|
12949
|
-
* NOTE: This method is no longer used - events are handled directly by rrweb
|
|
12950
|
-
* Kept for backward compatibility but not called in the current implementation
|
|
12951
|
-
*/
|
|
12952
|
-
processEvent(event) {
|
|
12953
|
-
// Only process if we have fields selected for redaction
|
|
12954
|
-
if (this.userSelectedFields.size === 0) {
|
|
12955
|
-
return event;
|
|
12956
|
-
}
|
|
12957
|
-
// Clone the event to avoid modifying the original
|
|
12958
|
-
const processedEvent = JSON.parse(JSON.stringify(event));
|
|
12959
|
-
// Handle different event types
|
|
12960
|
-
if (processedEvent.type === 3) { // IncrementalSnapshot
|
|
12961
|
-
if (processedEvent.data.source === 5) { // Input event
|
|
12962
|
-
const shouldRedact = this.isFieldSelected(processedEvent.data);
|
|
12963
|
-
if (shouldRedact) {
|
|
12964
|
-
logDebug('Redaction: Processing input event for redaction');
|
|
12965
|
-
this.redactInputEvent(processedEvent.data);
|
|
12966
|
-
}
|
|
12967
|
-
}
|
|
12968
|
-
// Also check for other sources that might contain text changes
|
|
12969
|
-
else if (processedEvent.data.source === 0) { // DOM mutations
|
|
12970
|
-
this.redactDOMEvent(processedEvent.data);
|
|
12971
|
-
}
|
|
12972
|
-
// Handle other sources that might contain text
|
|
12973
|
-
else if (processedEvent.data.source === 2) { // Mouse/Touch interaction
|
|
12974
|
-
this.redactMouseEvent(processedEvent.data);
|
|
12975
|
-
}
|
|
12976
|
-
}
|
|
12977
|
-
else if (processedEvent.type === 2) { // FullSnapshot
|
|
12978
|
-
this.redactFullSnapshot(processedEvent.data);
|
|
12954
|
+
logDebug('Redaction: No fields to redact');
|
|
12979
12955
|
}
|
|
12980
|
-
|
|
12956
|
+
this.applyRedactionClasses();
|
|
12981
12957
|
}
|
|
12982
12958
|
/**
|
|
12983
|
-
*
|
|
12959
|
+
* Set specific fields to be unredacted (everything else stays redacted by rrweb)
|
|
12960
|
+
* @param fields Array of CSS selectors for fields to unredact
|
|
12984
12961
|
*/
|
|
12985
|
-
|
|
12986
|
-
|
|
12987
|
-
|
|
12988
|
-
|
|
12989
|
-
|
|
12990
|
-
|
|
12991
|
-
|
|
12992
|
-
|
|
12993
|
-
|
|
12994
|
-
|
|
12995
|
-
inputData[prop] = this.redactedText;
|
|
12996
|
-
logDebug(`Redaction: Redacted property '${prop}'`);
|
|
12997
|
-
}
|
|
12998
|
-
});
|
|
12999
|
-
// Also check for any other string properties that might contain input data
|
|
13000
|
-
Object.keys(inputData).forEach(key => {
|
|
13001
|
-
if (typeof inputData[key] === 'string' && inputData[key].length > 0) {
|
|
13002
|
-
inputData[key] = this.redactedText;
|
|
13003
|
-
logDebug(`Redaction: Redacted additional property '${key}'`);
|
|
13004
|
-
}
|
|
12962
|
+
setFieldsToUnredact(fields) {
|
|
12963
|
+
this.unredactedFields.clear();
|
|
12964
|
+
// Filter out password fields (they cannot be unredacted)
|
|
12965
|
+
const validFields = fields.filter(field => {
|
|
12966
|
+
const isPasswordField = this.isPasswordSelector(field);
|
|
12967
|
+
if (isPasswordField) {
|
|
12968
|
+
logWarn(`Cannot unredact password field: ${field} - Password fields are always protected`);
|
|
12969
|
+
return false;
|
|
12970
|
+
}
|
|
12971
|
+
return true;
|
|
13005
12972
|
});
|
|
13006
|
-
|
|
13007
|
-
if (
|
|
13008
|
-
|
|
13009
|
-
inputData.attributes.value = this.redactedText;
|
|
13010
|
-
logDebug('Redaction: Redacted nested value attribute');
|
|
13011
|
-
}
|
|
13012
|
-
}
|
|
13013
|
-
logDebug('Redaction: Input event redaction complete');
|
|
13014
|
-
}
|
|
13015
|
-
/**
|
|
13016
|
-
* Redact sensitive data in DOM mutation events
|
|
13017
|
-
*/
|
|
13018
|
-
redactDOMEvent(domData) {
|
|
13019
|
-
// Check for text changes in DOM mutations
|
|
13020
|
-
if (domData.texts && Array.isArray(domData.texts)) {
|
|
13021
|
-
domData.texts.forEach((textChange) => {
|
|
13022
|
-
if (textChange.text && typeof textChange.text === 'string' &&
|
|
13023
|
-
this.shouldRedactDOMChange(textChange)) {
|
|
13024
|
-
textChange.text = this.redactedText;
|
|
13025
|
-
}
|
|
13026
|
-
});
|
|
13027
|
-
}
|
|
13028
|
-
// Also check for attribute changes that might contain input data
|
|
13029
|
-
if (domData.attributes && Array.isArray(domData.attributes)) {
|
|
13030
|
-
domData.attributes.forEach((attrChange) => {
|
|
13031
|
-
if (attrChange.attributes && attrChange.attributes.value &&
|
|
13032
|
-
typeof attrChange.attributes.value === 'string' &&
|
|
13033
|
-
this.shouldRedactDOMChange(attrChange)) {
|
|
13034
|
-
attrChange.attributes.value = this.redactedText;
|
|
13035
|
-
}
|
|
13036
|
-
});
|
|
12973
|
+
validFields.forEach(field => this.unredactedFields.add(field));
|
|
12974
|
+
if (validFields.length > 0) {
|
|
12975
|
+
logDebug(`Unredaction: Active for ${validFields.length} field(s):`, validFields);
|
|
13037
12976
|
}
|
|
13038
|
-
|
|
13039
|
-
|
|
13040
|
-
domData.adds.forEach((add) => {
|
|
13041
|
-
if (add.node && add.node.textContent && typeof add.node.textContent === 'string' &&
|
|
13042
|
-
this.shouldRedactDOMChange(add)) {
|
|
13043
|
-
add.node.textContent = this.redactedText;
|
|
13044
|
-
}
|
|
13045
|
-
});
|
|
12977
|
+
else {
|
|
12978
|
+
logDebug('Unredaction: No valid fields to unredact');
|
|
13046
12979
|
}
|
|
12980
|
+
this.applyUnredactionClasses();
|
|
13047
12981
|
}
|
|
13048
12982
|
/**
|
|
13049
|
-
*
|
|
12983
|
+
* Remove specific fields from unredaction (they become redacted again)
|
|
12984
|
+
* @param fields Array of CSS selectors for fields to redact
|
|
13050
12985
|
*/
|
|
13051
|
-
|
|
13052
|
-
|
|
13053
|
-
|
|
13054
|
-
|
|
13055
|
-
|
|
13056
|
-
|
|
13057
|
-
if (elementId !== undefined) {
|
|
13058
|
-
// Try to find the element by data-rrweb-id attribute
|
|
13059
|
-
let element = document.querySelector(`[data-rrweb-id="${elementId}"]`);
|
|
13060
|
-
if (element) {
|
|
13061
|
-
return this.shouldRedactElement(element);
|
|
13062
|
-
}
|
|
13063
|
-
}
|
|
13064
|
-
// Also check for nodeId which is another way rrweb identifies elements
|
|
13065
|
-
const nodeId = changeData.nodeId;
|
|
13066
|
-
if (nodeId !== undefined) {
|
|
13067
|
-
const element = document.querySelector(`[data-rrweb-id="${nodeId}"]`);
|
|
13068
|
-
if (element) {
|
|
13069
|
-
return this.shouldRedactElement(element);
|
|
13070
|
-
}
|
|
13071
|
-
}
|
|
13072
|
-
return false;
|
|
12986
|
+
redactFields(fields) {
|
|
12987
|
+
fields.forEach(field => {
|
|
12988
|
+
this.unredactedFields.delete(field);
|
|
12989
|
+
});
|
|
12990
|
+
if (this.unredactedFields.size > 0) {
|
|
12991
|
+
logDebug(`Unredaction: Removed ${fields.length} field(s), ${this.unredactedFields.size} remaining:`, Array.from(this.unredactedFields));
|
|
13073
12992
|
}
|
|
13074
|
-
|
|
13075
|
-
|
|
13076
|
-
return false;
|
|
12993
|
+
else {
|
|
12994
|
+
logDebug('Unredaction: All fields redacted');
|
|
13077
12995
|
}
|
|
12996
|
+
this.applyUnredactionClasses();
|
|
13078
12997
|
}
|
|
13079
12998
|
/**
|
|
13080
|
-
*
|
|
12999
|
+
* Clear all unredacted fields (everything becomes redacted again)
|
|
13081
13000
|
*/
|
|
13082
|
-
|
|
13083
|
-
|
|
13084
|
-
|
|
13085
|
-
|
|
13086
|
-
mouseData.text = this.redactedText;
|
|
13087
|
-
}
|
|
13001
|
+
clearUnredactedFields() {
|
|
13002
|
+
this.unredactedFields.clear();
|
|
13003
|
+
logDebug('Unredaction: All fields cleared, everything redacted');
|
|
13004
|
+
this.removeUnredactionClasses();
|
|
13088
13005
|
}
|
|
13089
13006
|
/**
|
|
13090
|
-
*
|
|
13007
|
+
* Check if any fields are currently unredacted
|
|
13091
13008
|
*/
|
|
13092
|
-
|
|
13093
|
-
|
|
13094
|
-
this.redactNode(snapshotData.node);
|
|
13095
|
-
}
|
|
13009
|
+
hasUnredactedFields() {
|
|
13010
|
+
return this.unredactedFields.size > 0;
|
|
13096
13011
|
}
|
|
13097
13012
|
/**
|
|
13098
|
-
*
|
|
13013
|
+
* Get the current redaction mode
|
|
13099
13014
|
*/
|
|
13100
|
-
|
|
13101
|
-
|
|
13102
|
-
return;
|
|
13103
|
-
// Check if this node should be redacted
|
|
13104
|
-
if (node.type === 2 && node.tagName &&
|
|
13105
|
-
(node.tagName.toLowerCase() === 'input' || node.tagName.toLowerCase() === 'textarea')) {
|
|
13106
|
-
// Check if this input/textarea should be redacted
|
|
13107
|
-
if (this.shouldRedactNode(node)) {
|
|
13108
|
-
// Redact value attribute
|
|
13109
|
-
if (node.attributes && node.attributes.value) {
|
|
13110
|
-
node.attributes.value = this.redactedText;
|
|
13111
|
-
}
|
|
13112
|
-
// Redact text content
|
|
13113
|
-
if (node.textContent) {
|
|
13114
|
-
node.textContent = this.redactedText;
|
|
13115
|
-
}
|
|
13116
|
-
}
|
|
13117
|
-
}
|
|
13118
|
-
// Recursively process child nodes
|
|
13119
|
-
if (node.childNodes && Array.isArray(node.childNodes)) {
|
|
13120
|
-
node.childNodes.forEach((childNode) => {
|
|
13121
|
-
this.redactNode(childNode);
|
|
13122
|
-
});
|
|
13123
|
-
}
|
|
13015
|
+
getRedactionMode() {
|
|
13016
|
+
return this.redactionMode;
|
|
13124
13017
|
}
|
|
13125
13018
|
/**
|
|
13126
|
-
*
|
|
13019
|
+
* Get the currently unredacted fields
|
|
13127
13020
|
*/
|
|
13128
|
-
|
|
13129
|
-
|
|
13130
|
-
return false;
|
|
13131
|
-
// Check if any of our selectors would match this node
|
|
13132
|
-
for (const selector of this.userSelectedFields) {
|
|
13133
|
-
if (this.selectorMatchesNode(selector, node)) {
|
|
13134
|
-
return true;
|
|
13135
|
-
}
|
|
13136
|
-
}
|
|
13137
|
-
return false;
|
|
13021
|
+
getUnredactedFields() {
|
|
13022
|
+
return Array.from(this.unredactedFields);
|
|
13138
13023
|
}
|
|
13139
13024
|
/**
|
|
13140
|
-
*
|
|
13141
|
-
|
|
13142
|
-
selectorMatchesNode(selector, node) {
|
|
13143
|
-
if (!node.attributes)
|
|
13144
|
-
return false;
|
|
13145
|
-
// Create a temporary element to test the selector
|
|
13146
|
-
try {
|
|
13147
|
-
const tempElement = document.createElement(node.tagName || 'div');
|
|
13148
|
-
// Copy attributes from the node to the temp element
|
|
13149
|
-
if (node.attributes) {
|
|
13150
|
-
Object.keys(node.attributes).forEach(key => {
|
|
13151
|
-
tempElement.setAttribute(key, node.attributes[key]);
|
|
13152
|
-
});
|
|
13153
|
-
}
|
|
13154
|
-
// Test if the selector matches this element
|
|
13155
|
-
return tempElement.matches(selector);
|
|
13156
|
-
}
|
|
13157
|
-
catch (e) {
|
|
13158
|
-
// If matches() is not supported or fails, fall back to basic attribute checking
|
|
13159
|
-
return this.basicSelectorMatch(selector, node);
|
|
13160
|
-
}
|
|
13161
|
-
}
|
|
13162
|
-
/**
|
|
13163
|
-
* Basic selector matching for environments where matches() is not available
|
|
13025
|
+
* Get CSS selectors for rrweb masking configuration
|
|
13026
|
+
* Returns null if no fields are unredacted (everything stays redacted)
|
|
13164
13027
|
*/
|
|
13165
|
-
|
|
13166
|
-
if (
|
|
13167
|
-
|
|
13168
|
-
|
|
13169
|
-
|
|
13170
|
-
const typeMatch = selector.match(/input\[type="([^"]+)"\]/);
|
|
13171
|
-
if (typeMatch && node.tagName === 'input' && node.attributes.type === typeMatch[1]) {
|
|
13172
|
-
return true;
|
|
13028
|
+
getMaskTextSelector() {
|
|
13029
|
+
if (this.redactionMode === 'privacy-first') {
|
|
13030
|
+
// Privacy-first: mask everything except unredacted fields
|
|
13031
|
+
if (this.unredactedFields.size === 0) {
|
|
13032
|
+
return null; // Everything stays redacted
|
|
13173
13033
|
}
|
|
13034
|
+
return Array.from(this.unredactedFields).join(',');
|
|
13174
13035
|
}
|
|
13175
|
-
|
|
13176
|
-
|
|
13177
|
-
|
|
13178
|
-
|
|
13179
|
-
}
|
|
13180
|
-
// Handle class selectors like '.sensitive-field'
|
|
13181
|
-
if (selector.startsWith('.')) {
|
|
13182
|
-
const className = selector.substring(1);
|
|
13183
|
-
return node.attributes.class && node.attributes.class.includes(className);
|
|
13184
|
-
}
|
|
13185
|
-
// Handle tag selectors like 'input'
|
|
13186
|
-
if (!selector.includes('[') && !selector.includes('.')) {
|
|
13187
|
-
return node.tagName && node.tagName.toLowerCase() === selector.toLowerCase();
|
|
13188
|
-
}
|
|
13189
|
-
return false;
|
|
13190
|
-
}
|
|
13191
|
-
/**
|
|
13192
|
-
* Check if an event is from a field that should be redacted
|
|
13193
|
-
*/
|
|
13194
|
-
isFieldSelected(eventData) {
|
|
13195
|
-
if (!isBrowser$3)
|
|
13196
|
-
return false;
|
|
13197
|
-
try {
|
|
13198
|
-
// For input events (source 5), we need to determine if this is a sensitive field
|
|
13199
|
-
if (eventData.source === 5) { // Input event
|
|
13200
|
-
const elementId = eventData.id;
|
|
13201
|
-
if (elementId !== undefined) {
|
|
13202
|
-
// Try to find the element by data-rrweb-id attribute
|
|
13203
|
-
let element = document.querySelector(`[data-rrweb-id="${elementId}"]`);
|
|
13204
|
-
if (element) {
|
|
13205
|
-
return this.shouldRedactElement(element);
|
|
13206
|
-
}
|
|
13207
|
-
// Fallback: Try to find by nodeId if available
|
|
13208
|
-
if (eventData.nodeId !== undefined) {
|
|
13209
|
-
element = document.querySelector(`[data-rrweb-id="${eventData.nodeId}"]`);
|
|
13210
|
-
if (element) {
|
|
13211
|
-
return this.shouldRedactElement(element);
|
|
13212
|
-
}
|
|
13213
|
-
}
|
|
13214
|
-
// More aggressive approach: Check all elements that match our selectors
|
|
13215
|
-
// and see if any of them are currently focused or have the same ID
|
|
13216
|
-
for (const selector of this.userSelectedFields) {
|
|
13217
|
-
const matchingElements = document.querySelectorAll(selector);
|
|
13218
|
-
if (matchingElements.length > 0) {
|
|
13219
|
-
// Check if any of these elements are currently focused
|
|
13220
|
-
for (const el of matchingElements) {
|
|
13221
|
-
if (el === document.activeElement) {
|
|
13222
|
-
logDebug('Redaction: Found focused element matching selector:', selector);
|
|
13223
|
-
return true;
|
|
13224
|
-
}
|
|
13225
|
-
}
|
|
13226
|
-
}
|
|
13227
|
-
}
|
|
13228
|
-
// If we still can't find it, try a more direct approach
|
|
13229
|
-
// Look for any input element that might be the active one
|
|
13230
|
-
const activeElement = document.activeElement;
|
|
13231
|
-
if (activeElement && this.shouldRedactElement(activeElement)) {
|
|
13232
|
-
logDebug('Redaction: Active element should be redacted');
|
|
13233
|
-
return true;
|
|
13234
|
-
}
|
|
13235
|
-
return false;
|
|
13236
|
-
}
|
|
13237
|
-
}
|
|
13238
|
-
// For other event types, try to find the element
|
|
13239
|
-
const elementId = eventData.id;
|
|
13240
|
-
if (elementId !== undefined) {
|
|
13241
|
-
// First try to find by data-rrweb-id attribute
|
|
13242
|
-
let element = document.querySelector(`[data-rrweb-id="${elementId}"]`);
|
|
13243
|
-
if (element) {
|
|
13244
|
-
return this.shouldRedactElement(element);
|
|
13245
|
-
}
|
|
13246
|
-
}
|
|
13247
|
-
// Also check for nodeId which is another way rrweb identifies elements
|
|
13248
|
-
const nodeId = eventData.nodeId;
|
|
13249
|
-
if (nodeId !== undefined) {
|
|
13250
|
-
const element = document.querySelector(`[data-rrweb-id="${nodeId}"]`);
|
|
13251
|
-
if (element) {
|
|
13252
|
-
return this.shouldRedactElement(element);
|
|
13253
|
-
}
|
|
13254
|
-
}
|
|
13255
|
-
// For DOM mutations, check if the target element should be redacted
|
|
13256
|
-
if (eventData.target && eventData.target.id) {
|
|
13257
|
-
const element = document.querySelector(`[data-rrweb-id="${eventData.target.id}"]`);
|
|
13258
|
-
if (element) {
|
|
13259
|
-
return this.shouldRedactElement(element);
|
|
13260
|
-
}
|
|
13036
|
+
else {
|
|
13037
|
+
// Visibility-first: mask only redacted fields
|
|
13038
|
+
if (this.redactedFields.size === 0) {
|
|
13039
|
+
return null; // Nothing to redact
|
|
13261
13040
|
}
|
|
13262
|
-
return
|
|
13263
|
-
}
|
|
13264
|
-
catch (e) {
|
|
13265
|
-
logWarn('Error checking if field should be redacted:', e);
|
|
13266
|
-
return false;
|
|
13267
|
-
}
|
|
13268
|
-
}
|
|
13269
|
-
/**
|
|
13270
|
-
* Get CSS selectors for rrweb masking configuration
|
|
13271
|
-
* Used to configure rrweb's maskTextSelector option
|
|
13272
|
-
*/
|
|
13273
|
-
getMaskTextSelector() {
|
|
13274
|
-
if (this.userSelectedFields.size === 0) {
|
|
13275
|
-
return null;
|
|
13041
|
+
return Array.from(this.redactedFields).join(',');
|
|
13276
13042
|
}
|
|
13277
|
-
return Array.from(this.userSelectedFields).join(',');
|
|
13278
13043
|
}
|
|
13279
13044
|
/**
|
|
13280
|
-
*
|
|
13045
|
+
* Apply redaction classes to DOM elements (for visibility-first mode)
|
|
13046
|
+
* Adds 'rr-mask' class to elements that should be redacted
|
|
13281
13047
|
*/
|
|
13282
|
-
|
|
13283
|
-
if (this.
|
|
13284
|
-
return
|
|
13048
|
+
applyRedactionClasses() {
|
|
13049
|
+
if (this.redactedFields.size === 0) {
|
|
13050
|
+
return;
|
|
13285
13051
|
}
|
|
13286
|
-
//
|
|
13287
|
-
|
|
13052
|
+
// Add 'rr-mask' class to redacted elements
|
|
13053
|
+
this.redactedFields.forEach(selector => {
|
|
13288
13054
|
try {
|
|
13289
|
-
|
|
13290
|
-
|
|
13291
|
-
|
|
13055
|
+
const elements = document.querySelectorAll(selector);
|
|
13056
|
+
elements.forEach(element => {
|
|
13057
|
+
element.classList.add('rr-mask');
|
|
13058
|
+
});
|
|
13059
|
+
logDebug(`Added rr-mask class to ${elements.length} element(s) for selector: ${selector}`);
|
|
13292
13060
|
}
|
|
13293
13061
|
catch (e) {
|
|
13294
|
-
// Invalid selector, skip
|
|
13295
13062
|
logWarn(`Invalid selector: ${selector}`);
|
|
13296
13063
|
}
|
|
13297
|
-
}
|
|
13298
|
-
return false;
|
|
13064
|
+
});
|
|
13299
13065
|
}
|
|
13300
13066
|
/**
|
|
13301
|
-
* Apply
|
|
13302
|
-
*
|
|
13303
|
-
* This enables rrweb's built-in masking functionality
|
|
13067
|
+
* Apply unredaction classes to DOM elements
|
|
13068
|
+
* Removes 'rr-mask' class from elements that should be unredacted
|
|
13304
13069
|
*/
|
|
13305
|
-
|
|
13306
|
-
if (this.
|
|
13070
|
+
applyUnredactionClasses() {
|
|
13071
|
+
if (this.unredactedFields.size === 0) {
|
|
13307
13072
|
return;
|
|
13308
13073
|
}
|
|
13309
|
-
// Remove
|
|
13310
|
-
|
|
13311
|
-
element.classList.remove('rr-mask');
|
|
13312
|
-
});
|
|
13313
|
-
// Add redaction classes to matching elements
|
|
13314
|
-
this.userSelectedFields.forEach(selector => {
|
|
13074
|
+
// Remove 'rr-mask' class from unredacted elements
|
|
13075
|
+
this.unredactedFields.forEach(selector => {
|
|
13315
13076
|
try {
|
|
13316
13077
|
const elements = document.querySelectorAll(selector);
|
|
13317
13078
|
elements.forEach(element => {
|
|
13318
|
-
element.classList.
|
|
13079
|
+
element.classList.remove('rr-mask');
|
|
13319
13080
|
});
|
|
13320
|
-
logDebug(`
|
|
13081
|
+
logDebug(`Removed rr-mask class from ${elements.length} element(s) for selector: ${selector}`);
|
|
13321
13082
|
}
|
|
13322
13083
|
catch (e) {
|
|
13323
13084
|
logWarn(`Invalid selector: ${selector}`);
|
|
@@ -13325,7 +13086,26 @@ class RedactionManager {
|
|
|
13325
13086
|
});
|
|
13326
13087
|
}
|
|
13327
13088
|
/**
|
|
13328
|
-
*
|
|
13089
|
+
* Remove all unredaction classes from DOM elements
|
|
13090
|
+
*/
|
|
13091
|
+
removeUnredactionClasses() {
|
|
13092
|
+
// Note: This doesn't add 'rr-mask' classes back - rrweb handles that automatically
|
|
13093
|
+
logDebug('Unredaction classes removed');
|
|
13094
|
+
}
|
|
13095
|
+
/**
|
|
13096
|
+
* Check if a selector represents a password field
|
|
13097
|
+
*/
|
|
13098
|
+
isPasswordSelector(selector) {
|
|
13099
|
+
const passwordPatterns = [
|
|
13100
|
+
'input[type="password"]',
|
|
13101
|
+
'input[type="password" i]',
|
|
13102
|
+
'[type="password"]',
|
|
13103
|
+
'[type="password" i]'
|
|
13104
|
+
];
|
|
13105
|
+
return passwordPatterns.some(pattern => selector.toLowerCase().includes(pattern.toLowerCase().replace(/[\[\]]/g, '')));
|
|
13106
|
+
}
|
|
13107
|
+
/**
|
|
13108
|
+
* Get the original value of an element (for debugging)
|
|
13329
13109
|
*/
|
|
13330
13110
|
getOriginalValue(element) {
|
|
13331
13111
|
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
@@ -13334,10 +13114,51 @@ class RedactionManager {
|
|
|
13334
13114
|
return undefined;
|
|
13335
13115
|
}
|
|
13336
13116
|
/**
|
|
13337
|
-
* Check if an element is currently
|
|
13117
|
+
* Check if an element is currently unredacted
|
|
13338
13118
|
*/
|
|
13339
|
-
|
|
13340
|
-
return this.
|
|
13119
|
+
isElementUnredacted(element) {
|
|
13120
|
+
return this.shouldUnredactElement(element);
|
|
13121
|
+
}
|
|
13122
|
+
/**
|
|
13123
|
+
* Check if an element should be unredacted
|
|
13124
|
+
*/
|
|
13125
|
+
shouldUnredactElement(element) {
|
|
13126
|
+
if (this.redactionMode === 'privacy-first') {
|
|
13127
|
+
// Privacy-first: check if element is in unredacted fields
|
|
13128
|
+
if (this.unredactedFields.size === 0) {
|
|
13129
|
+
return false; // Nothing unredacted
|
|
13130
|
+
}
|
|
13131
|
+
// Check if any selector matches this element
|
|
13132
|
+
for (const selector of this.unredactedFields) {
|
|
13133
|
+
try {
|
|
13134
|
+
if (element.matches(selector)) {
|
|
13135
|
+
return true;
|
|
13136
|
+
}
|
|
13137
|
+
}
|
|
13138
|
+
catch (e) {
|
|
13139
|
+
logWarn(`Invalid selector: ${selector}`);
|
|
13140
|
+
}
|
|
13141
|
+
}
|
|
13142
|
+
return false;
|
|
13143
|
+
}
|
|
13144
|
+
else {
|
|
13145
|
+
// Visibility-first: check if element is NOT in redacted fields
|
|
13146
|
+
if (this.redactedFields.size === 0) {
|
|
13147
|
+
return true; // Nothing redacted, everything visible
|
|
13148
|
+
}
|
|
13149
|
+
// Check if any selector matches this element
|
|
13150
|
+
for (const selector of this.redactedFields) {
|
|
13151
|
+
try {
|
|
13152
|
+
if (element.matches(selector)) {
|
|
13153
|
+
return false; // Element is redacted
|
|
13154
|
+
}
|
|
13155
|
+
}
|
|
13156
|
+
catch (e) {
|
|
13157
|
+
logWarn(`Invalid selector: ${selector}`);
|
|
13158
|
+
}
|
|
13159
|
+
}
|
|
13160
|
+
return true; // Element is not redacted
|
|
13161
|
+
}
|
|
13341
13162
|
}
|
|
13342
13163
|
}
|
|
13343
13164
|
// Export a default instance
|
|
@@ -13906,11 +13727,22 @@ class HumanBehaviorTracker {
|
|
|
13906
13727
|
});
|
|
13907
13728
|
// Store canvas recording preference
|
|
13908
13729
|
tracker.recordCanvas = (_a = options === null || options === void 0 ? void 0 : options.recordCanvas) !== null && _a !== void 0 ? _a : false;
|
|
13909
|
-
// Set
|
|
13730
|
+
// Set unredacted fields if specified (legacy support)
|
|
13910
13731
|
if (options === null || options === void 0 ? void 0 : options.redactFields) {
|
|
13911
|
-
tracker.
|
|
13912
|
-
|
|
13913
|
-
|
|
13732
|
+
tracker.setUnredactedFields(options.redactFields);
|
|
13733
|
+
}
|
|
13734
|
+
// Handle new redaction strategy
|
|
13735
|
+
if (options === null || options === void 0 ? void 0 : options.redactionStrategy) {
|
|
13736
|
+
if (options.redactionStrategy.mode === 'privacy-first') {
|
|
13737
|
+
if (options.redactionStrategy.unredactFields) {
|
|
13738
|
+
tracker.setUnredactedFields(options.redactionStrategy.unredactFields);
|
|
13739
|
+
}
|
|
13740
|
+
}
|
|
13741
|
+
else {
|
|
13742
|
+
if (options.redactionStrategy.redactFields) {
|
|
13743
|
+
tracker.setRedactedFields(options.redactionStrategy.redactFields);
|
|
13744
|
+
}
|
|
13745
|
+
}
|
|
13914
13746
|
}
|
|
13915
13747
|
// Setup automatic tracking if enabled
|
|
13916
13748
|
if ((options === null || options === void 0 ? void 0 : options.enableAutomaticTracking) !== false) {
|
|
@@ -13957,7 +13789,10 @@ class HumanBehaviorTracker {
|
|
|
13957
13789
|
ingestionUrl: ingestionUrl || defaultIngestionUrl
|
|
13958
13790
|
});
|
|
13959
13791
|
this.apiKey = apiKey;
|
|
13960
|
-
this.redactionManager = new RedactionManager(
|
|
13792
|
+
this.redactionManager = new RedactionManager({
|
|
13793
|
+
redactionStrategy: options === null || options === void 0 ? void 0 : options.redactionStrategy,
|
|
13794
|
+
legacyRedactFields: options === null || options === void 0 ? void 0 : options.redactFields // For backward compatibility
|
|
13795
|
+
});
|
|
13961
13796
|
// Initialize property manager
|
|
13962
13797
|
this.propertyManager = new PropertyManager({
|
|
13963
13798
|
enableAutomaticProperties: (options === null || options === void 0 ? void 0 : options.enableAutomaticProperties) !== false,
|
|
@@ -14535,6 +14370,31 @@ class HumanBehaviorTracker {
|
|
|
14535
14370
|
if (!userResponse.ok) {
|
|
14536
14371
|
throw new Error(`Failed to identify user: ${userResponse.statusText}`);
|
|
14537
14372
|
}
|
|
14373
|
+
// Get IP info and GeoIP data
|
|
14374
|
+
try {
|
|
14375
|
+
const ipResponse = yield fetch(`${this.api['baseUrl']}/api/ingestion/ip-info`, {
|
|
14376
|
+
method: 'POST',
|
|
14377
|
+
headers: {
|
|
14378
|
+
'Content-Type': 'application/json',
|
|
14379
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
14380
|
+
},
|
|
14381
|
+
body: JSON.stringify({
|
|
14382
|
+
sessionId: this.sessionId,
|
|
14383
|
+
clientIP: null, // Let server detect from headers
|
|
14384
|
+
ipDetectionMethod: 'headers',
|
|
14385
|
+
timestamp: new Date().toISOString()
|
|
14386
|
+
})
|
|
14387
|
+
});
|
|
14388
|
+
if (ipResponse.ok) {
|
|
14389
|
+
logDebug('✅ IP info and GeoIP data retrieved successfully');
|
|
14390
|
+
}
|
|
14391
|
+
else {
|
|
14392
|
+
logDebug(`⚠️ IP info request failed: ${ipResponse.statusText}`);
|
|
14393
|
+
}
|
|
14394
|
+
}
|
|
14395
|
+
catch (error) {
|
|
14396
|
+
logDebug(`⚠️ IP info request error: ${error}`);
|
|
14397
|
+
}
|
|
14538
14398
|
// Don't update endUserId - keep it as the original UUID
|
|
14539
14399
|
return originalEndUserId || '';
|
|
14540
14400
|
});
|
|
@@ -14574,7 +14434,7 @@ class HumanBehaviorTracker {
|
|
|
14574
14434
|
// ✅ HUMANBEHAVIOR'S CUSTOM SETTINGS
|
|
14575
14435
|
maskTextSelector: this.redactionManager.getMaskTextSelector() || undefined,
|
|
14576
14436
|
maskTextFn: undefined,
|
|
14577
|
-
maskAllInputs:
|
|
14437
|
+
maskAllInputs: this.redactionManager.getRedactionMode() === 'privacy-first', // Configurable based on strategy
|
|
14578
14438
|
maskInputOptions: { password: true }, // HumanBehavior default
|
|
14579
14439
|
maskInputFn: undefined,
|
|
14580
14440
|
slimDOMOptions: {},
|
|
@@ -14884,15 +14744,23 @@ class HumanBehaviorTracker {
|
|
|
14884
14744
|
});
|
|
14885
14745
|
}
|
|
14886
14746
|
/**
|
|
14887
|
-
* Set specific fields to be redacted
|
|
14888
|
-
*
|
|
14889
|
-
* @param fields Array of CSS selectors for fields to redact (e.g., ['input[type="password"]', '#email-field'])
|
|
14747
|
+
* Set specific fields to be redacted (for visibility-first mode)
|
|
14748
|
+
* @param fields Array of CSS selectors for fields to redact
|
|
14890
14749
|
*/
|
|
14891
14750
|
setRedactedFields(fields) {
|
|
14892
14751
|
this.redactionManager.setFieldsToRedact(fields);
|
|
14893
|
-
// ✅
|
|
14894
|
-
this.
|
|
14895
|
-
|
|
14752
|
+
// ✅ RESTART RECORDING WITH NEW SETTINGS - Ensures redaction is applied
|
|
14753
|
+
if (this.recordInstance) {
|
|
14754
|
+
this.restartWithNewRedaction();
|
|
14755
|
+
}
|
|
14756
|
+
}
|
|
14757
|
+
/**
|
|
14758
|
+
* Set specific fields to be unredacted (everything else stays redacted by rrweb)
|
|
14759
|
+
* @param fields Array of CSS selectors for fields to unredact (e.g., ['#username', '#comment'])
|
|
14760
|
+
*/
|
|
14761
|
+
setUnredactedFields(fields) {
|
|
14762
|
+
this.redactionManager.setFieldsToUnredact(fields);
|
|
14763
|
+
// ✅ RESTART RECORDING WITH NEW SETTINGS - Ensures unredaction is applied
|
|
14896
14764
|
if (this.recordInstance) {
|
|
14897
14765
|
this.restartWithNewRedaction();
|
|
14898
14766
|
}
|
|
@@ -14904,16 +14772,37 @@ class HumanBehaviorTracker {
|
|
|
14904
14772
|
}
|
|
14905
14773
|
}
|
|
14906
14774
|
/**
|
|
14907
|
-
* Check if
|
|
14775
|
+
* Check if any fields are currently unredacted
|
|
14908
14776
|
*/
|
|
14909
|
-
|
|
14910
|
-
return this.redactionManager.
|
|
14777
|
+
hasUnredactedFields() {
|
|
14778
|
+
return this.redactionManager.hasUnredactedFields();
|
|
14911
14779
|
}
|
|
14912
14780
|
/**
|
|
14913
|
-
* Get the currently
|
|
14781
|
+
* Get the currently unredacted fields
|
|
14914
14782
|
*/
|
|
14915
|
-
|
|
14916
|
-
return this.redactionManager.
|
|
14783
|
+
getUnredactedFields() {
|
|
14784
|
+
return this.redactionManager.getUnredactedFields();
|
|
14785
|
+
}
|
|
14786
|
+
/**
|
|
14787
|
+
* Remove specific fields from unredaction (they become redacted again)
|
|
14788
|
+
* @param fields Array of CSS selectors for fields to redact
|
|
14789
|
+
*/
|
|
14790
|
+
redactFields(fields) {
|
|
14791
|
+
this.redactionManager.redactFields(fields);
|
|
14792
|
+
// ✅ RESTART RECORDING WITH NEW SETTINGS - Ensures redaction is updated
|
|
14793
|
+
if (this.recordInstance) {
|
|
14794
|
+
this.restartWithNewRedaction();
|
|
14795
|
+
}
|
|
14796
|
+
}
|
|
14797
|
+
/**
|
|
14798
|
+
* Clear all unredacted fields (everything becomes redacted again)
|
|
14799
|
+
*/
|
|
14800
|
+
clearUnredactedFields() {
|
|
14801
|
+
this.redactionManager.clearUnredactedFields();
|
|
14802
|
+
// ✅ RESTART RECORDING WITH NEW SETTINGS - Ensures redaction is updated
|
|
14803
|
+
if (this.recordInstance) {
|
|
14804
|
+
this.restartWithNewRedaction();
|
|
14805
|
+
}
|
|
14917
14806
|
}
|
|
14918
14807
|
/**
|
|
14919
14808
|
* Get the current session ID
|
|
@@ -15199,22 +15088,22 @@ const useHumanBehavior = () => {
|
|
|
15199
15088
|
}
|
|
15200
15089
|
return tracker;
|
|
15201
15090
|
};
|
|
15202
|
-
// Custom hook for managing
|
|
15091
|
+
// Custom hook for managing unredaction fields dynamically
|
|
15203
15092
|
const useRedaction = () => {
|
|
15204
15093
|
const tracker = useHumanBehavior();
|
|
15205
|
-
const
|
|
15206
|
-
tracker.
|
|
15094
|
+
const setUnredactedFields = React.useCallback((fields) => {
|
|
15095
|
+
tracker.setUnredactedFields(fields);
|
|
15207
15096
|
}, [tracker]);
|
|
15208
|
-
const
|
|
15209
|
-
return tracker.
|
|
15097
|
+
const hasUnredactedFields = React.useCallback(() => {
|
|
15098
|
+
return tracker.hasUnredactedFields();
|
|
15210
15099
|
}, [tracker]);
|
|
15211
|
-
const
|
|
15212
|
-
return tracker.
|
|
15100
|
+
const getUnredactedFields = React.useCallback(() => {
|
|
15101
|
+
return tracker.getUnredactedFields();
|
|
15213
15102
|
}, [tracker]);
|
|
15214
15103
|
return {
|
|
15215
|
-
|
|
15216
|
-
|
|
15217
|
-
|
|
15104
|
+
setUnredactedFields,
|
|
15105
|
+
hasUnredactedFields,
|
|
15106
|
+
getUnredactedFields
|
|
15218
15107
|
};
|
|
15219
15108
|
};
|
|
15220
15109
|
// Custom hook for managing user info
|