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