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