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/src/redact.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Simplified redaction functionality for HumanBehavior SDK
|
|
2
|
+
// Since rrweb auto-redacts all input fields by default, this module only handles
|
|
3
|
+
// selectively unredacting specific fields (except passwords which remain protected)
|
|
4
4
|
|
|
5
5
|
import { logDebug, logWarn } from './utils/logger';
|
|
6
6
|
|
|
@@ -10,12 +10,20 @@ const isBrowser = typeof window !== 'undefined';
|
|
|
10
10
|
export interface RedactionOptions {
|
|
11
11
|
redactedText?: string;
|
|
12
12
|
excludeSelectors?: string[];
|
|
13
|
-
userFields?: string[]; // Fields that the user wants to
|
|
13
|
+
userFields?: string[]; // Fields that the user wants to unredact (legacy)
|
|
14
|
+
redactionStrategy?: {
|
|
15
|
+
mode: 'privacy-first' | 'visibility-first';
|
|
16
|
+
unredactFields?: string[]; // Fields to make visible (when mode: 'privacy-first')
|
|
17
|
+
redactFields?: string[]; // Fields to hide (when mode: 'visibility-first')
|
|
18
|
+
};
|
|
19
|
+
legacyRedactFields?: string[]; // For backward compatibility
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
export class RedactionManager {
|
|
17
23
|
private redactedText: string = '[REDACTED]';
|
|
18
|
-
private
|
|
24
|
+
private unredactedFields: Set<string> = new Set(); // Fields that user wants to unredact
|
|
25
|
+
private redactedFields: Set<string> = new Set(); // Fields that user wants to redact
|
|
26
|
+
private redactionMode: 'privacy-first' | 'visibility-first' = 'privacy-first';
|
|
19
27
|
private excludeSelectors: string[] = [
|
|
20
28
|
'[data-no-redact="true"]',
|
|
21
29
|
'.human-behavior-no-redact'
|
|
@@ -28,468 +36,201 @@ export class RedactionManager {
|
|
|
28
36
|
if (options?.excludeSelectors) {
|
|
29
37
|
this.excludeSelectors = [...this.excludeSelectors, ...options.excludeSelectors];
|
|
30
38
|
}
|
|
31
|
-
if (options?.userFields) {
|
|
32
|
-
this.setFieldsToRedact(options.userFields);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Set specific fields to be redacted using CSS selectors
|
|
38
|
-
* These selectors are used to configure rrweb's built-in masking
|
|
39
|
-
* @param fields Array of CSS selectors for fields to redact
|
|
40
|
-
*/
|
|
41
|
-
public setFieldsToRedact(fields: string[]): void {
|
|
42
|
-
this.userSelectedFields.clear();
|
|
43
|
-
fields.forEach(field => this.userSelectedFields.add(field));
|
|
44
39
|
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
// Handle new redaction strategy
|
|
41
|
+
if (options?.redactionStrategy) {
|
|
42
|
+
this.redactionMode = options.redactionStrategy.mode;
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
logDebug('Redaction: Disabled - no fields selected');
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Check if redaction is currently active (has fields selected)
|
|
63
|
-
*/
|
|
64
|
-
public isActive(): boolean {
|
|
65
|
-
return this.userSelectedFields.size > 0;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Get the currently selected fields for redaction
|
|
70
|
-
*/
|
|
71
|
-
public getSelectedFields(): string[] {
|
|
72
|
-
return Array.from(this.userSelectedFields);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Process an event and redact sensitive data if needed
|
|
77
|
-
* NOTE: This method is no longer used - events are handled directly by rrweb
|
|
78
|
-
* Kept for backward compatibility but not called in the current implementation
|
|
79
|
-
*/
|
|
80
|
-
public processEvent(event: any): any {
|
|
81
|
-
// Only process if we have fields selected for redaction
|
|
82
|
-
if (this.userSelectedFields.size === 0) {
|
|
83
|
-
return event;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Clone the event to avoid modifying the original
|
|
87
|
-
const processedEvent = JSON.parse(JSON.stringify(event));
|
|
88
|
-
|
|
89
|
-
// Handle different event types
|
|
90
|
-
if (processedEvent.type === 3) { // IncrementalSnapshot
|
|
91
|
-
if (processedEvent.data.source === 5) { // Input event
|
|
92
|
-
const shouldRedact = this.isFieldSelected(processedEvent.data);
|
|
93
|
-
if (shouldRedact) {
|
|
94
|
-
logDebug('Redaction: Processing input event for redaction');
|
|
95
|
-
this.redactInputEvent(processedEvent.data);
|
|
44
|
+
if (this.redactionMode === 'privacy-first') {
|
|
45
|
+
// Privacy-first: everything redacted by default, unredact specific fields
|
|
46
|
+
if (options.redactionStrategy.unredactFields) {
|
|
47
|
+
this.setFieldsToUnredact(options.redactionStrategy.unredactFields);
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
// Visibility-first: everything visible by default, redact specific fields
|
|
51
|
+
if (options.redactionStrategy.redactFields) {
|
|
52
|
+
this.setFieldsToRedact(options.redactionStrategy.redactFields);
|
|
96
53
|
}
|
|
97
|
-
}
|
|
98
|
-
// Also check for other sources that might contain text changes
|
|
99
|
-
else if (processedEvent.data.source === 0) { // DOM mutations
|
|
100
|
-
this.redactDOMEvent(processedEvent.data);
|
|
101
|
-
}
|
|
102
|
-
// Handle other sources that might contain text
|
|
103
|
-
else if (processedEvent.data.source === 2) { // Mouse/Touch interaction
|
|
104
|
-
this.redactMouseEvent(processedEvent.data);
|
|
105
54
|
}
|
|
106
55
|
}
|
|
107
|
-
|
|
108
|
-
|
|
56
|
+
|
|
57
|
+
// Handle legacy redactFields (backward compatibility)
|
|
58
|
+
if (options?.legacyRedactFields) {
|
|
59
|
+
this.setFieldsToUnredact(options.legacyRedactFields);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Handle legacy userFields
|
|
63
|
+
if (options?.userFields) {
|
|
64
|
+
this.setFieldsToUnredact(options.userFields);
|
|
109
65
|
}
|
|
110
|
-
|
|
111
|
-
return processedEvent;
|
|
112
66
|
}
|
|
113
67
|
|
|
114
68
|
/**
|
|
115
|
-
*
|
|
69
|
+
* Set specific fields to be redacted (for visibility-first mode)
|
|
70
|
+
* @param fields Array of CSS selectors for fields to redact
|
|
116
71
|
*/
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (!this.isFieldSelected(inputData)) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
72
|
+
public setFieldsToRedact(fields: string[]): void {
|
|
73
|
+
this.redactedFields.clear();
|
|
122
74
|
|
|
123
|
-
|
|
75
|
+
// Always include password fields in redacted list
|
|
76
|
+
const passwordFields = [
|
|
77
|
+
'input[type="password"]',
|
|
78
|
+
'input[type="password" i]',
|
|
79
|
+
'[type="password"]',
|
|
80
|
+
'[type="password" i]'
|
|
81
|
+
];
|
|
124
82
|
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (inputData[prop] !== undefined && typeof inputData[prop] === 'string') {
|
|
129
|
-
inputData[prop] = this.redactedText;
|
|
130
|
-
logDebug(`Redaction: Redacted property '${prop}'`);
|
|
131
|
-
}
|
|
83
|
+
// Add password fields and user-specified fields
|
|
84
|
+
[...passwordFields, ...fields].forEach(field => {
|
|
85
|
+
this.redactedFields.add(field);
|
|
132
86
|
});
|
|
133
87
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
logDebug(`Redaction: Redacted additional property '${key}'`);
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// Handle nested objects that might contain text data
|
|
143
|
-
if (inputData.attributes && typeof inputData.attributes === 'object') {
|
|
144
|
-
if (inputData.attributes.value && typeof inputData.attributes.value === 'string') {
|
|
145
|
-
inputData.attributes.value = this.redactedText;
|
|
146
|
-
logDebug('Redaction: Redacted nested value attribute');
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
logDebug('Redaction: Input event redaction complete');
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Redact sensitive data in DOM mutation events
|
|
155
|
-
*/
|
|
156
|
-
private redactDOMEvent(domData: any): void {
|
|
157
|
-
// Check for text changes in DOM mutations
|
|
158
|
-
if (domData.texts && Array.isArray(domData.texts)) {
|
|
159
|
-
domData.texts.forEach((textChange: any) => {
|
|
160
|
-
if (textChange.text && typeof textChange.text === 'string' &&
|
|
161
|
-
this.shouldRedactDOMChange(textChange)) {
|
|
162
|
-
textChange.text = this.redactedText;
|
|
163
|
-
}
|
|
164
|
-
});
|
|
88
|
+
if (this.redactedFields.size > 0) {
|
|
89
|
+
logDebug(`Redaction: Active for ${this.redactedFields.size} field(s):`, Array.from(this.redactedFields));
|
|
90
|
+
} else {
|
|
91
|
+
logDebug('Redaction: No fields to redact');
|
|
165
92
|
}
|
|
166
93
|
|
|
167
|
-
|
|
168
|
-
if (domData.attributes && Array.isArray(domData.attributes)) {
|
|
169
|
-
domData.attributes.forEach((attrChange: any) => {
|
|
170
|
-
if (attrChange.attributes && attrChange.attributes.value &&
|
|
171
|
-
typeof attrChange.attributes.value === 'string' &&
|
|
172
|
-
this.shouldRedactDOMChange(attrChange)) {
|
|
173
|
-
attrChange.attributes.value = this.redactedText;
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Check for any other properties that might contain text data
|
|
179
|
-
if (domData.adds && Array.isArray(domData.adds)) {
|
|
180
|
-
domData.adds.forEach((add: any) => {
|
|
181
|
-
if (add.node && add.node.textContent && typeof add.node.textContent === 'string' &&
|
|
182
|
-
this.shouldRedactDOMChange(add)) {
|
|
183
|
-
add.node.textContent = this.redactedText;
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
}
|
|
94
|
+
this.applyRedactionClasses();
|
|
187
95
|
}
|
|
188
96
|
|
|
189
97
|
/**
|
|
190
|
-
*
|
|
98
|
+
* Set specific fields to be unredacted (everything else stays redacted by rrweb)
|
|
99
|
+
* @param fields Array of CSS selectors for fields to unredact
|
|
191
100
|
*/
|
|
192
|
-
|
|
193
|
-
|
|
101
|
+
public setFieldsToUnredact(fields: string[]): void {
|
|
102
|
+
this.unredactedFields.clear();
|
|
194
103
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (element) {
|
|
203
|
-
return this.shouldRedactElement(element);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Also check for nodeId which is another way rrweb identifies elements
|
|
208
|
-
const nodeId = changeData.nodeId;
|
|
209
|
-
if (nodeId !== undefined) {
|
|
210
|
-
const element = document.querySelector(`[data-rrweb-id="${nodeId}"]`) as HTMLElement;
|
|
211
|
-
if (element) {
|
|
212
|
-
return this.shouldRedactElement(element);
|
|
213
|
-
}
|
|
104
|
+
// Filter out password fields (they cannot be unredacted)
|
|
105
|
+
const validFields = fields.filter(field => {
|
|
106
|
+
const isPasswordField = this.isPasswordSelector(field);
|
|
107
|
+
if (isPasswordField) {
|
|
108
|
+
logWarn(`Cannot unredact password field: ${field} - Password fields are always protected`);
|
|
109
|
+
return false;
|
|
214
110
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
* Redact sensitive data in mouse/touch interaction events
|
|
225
|
-
*/
|
|
226
|
-
private redactMouseEvent(mouseData: any): void {
|
|
227
|
-
// Mouse events typically don't contain text data, but check for any text properties
|
|
228
|
-
if (mouseData.text && typeof mouseData.text === 'string' &&
|
|
229
|
-
this.isFieldSelected(mouseData)) {
|
|
230
|
-
mouseData.text = this.redactedText;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Redact sensitive data in full snapshot events
|
|
236
|
-
*/
|
|
237
|
-
private redactFullSnapshot(snapshotData: any): void {
|
|
238
|
-
if (snapshotData.node && snapshotData.node.type === 2) { // Element node
|
|
239
|
-
this.redactNode(snapshotData.node);
|
|
111
|
+
return true;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
validFields.forEach(field => this.unredactedFields.add(field));
|
|
115
|
+
|
|
116
|
+
if (validFields.length > 0) {
|
|
117
|
+
logDebug(`Unredaction: Active for ${validFields.length} field(s):`, validFields);
|
|
118
|
+
} else {
|
|
119
|
+
logDebug('Unredaction: No valid fields to unredact');
|
|
240
120
|
}
|
|
121
|
+
|
|
122
|
+
this.applyUnredactionClasses();
|
|
241
123
|
}
|
|
242
124
|
|
|
243
125
|
/**
|
|
244
|
-
*
|
|
126
|
+
* Remove specific fields from unredaction (they become redacted again)
|
|
127
|
+
* @param fields Array of CSS selectors for fields to redact
|
|
245
128
|
*/
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
// Redact value attribute
|
|
256
|
-
if (node.attributes && node.attributes.value) {
|
|
257
|
-
node.attributes.value = this.redactedText;
|
|
258
|
-
}
|
|
259
|
-
// Redact text content
|
|
260
|
-
if (node.textContent) {
|
|
261
|
-
node.textContent = this.redactedText;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Recursively process child nodes
|
|
267
|
-
if (node.childNodes && Array.isArray(node.childNodes)) {
|
|
268
|
-
node.childNodes.forEach((childNode: any) => {
|
|
269
|
-
this.redactNode(childNode);
|
|
270
|
-
});
|
|
129
|
+
public redactFields(fields: string[]): void {
|
|
130
|
+
fields.forEach(field => {
|
|
131
|
+
this.unredactedFields.delete(field);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (this.unredactedFields.size > 0) {
|
|
135
|
+
logDebug(`Unredaction: Removed ${fields.length} field(s), ${this.unredactedFields.size} remaining:`, Array.from(this.unredactedFields));
|
|
136
|
+
} else {
|
|
137
|
+
logDebug('Unredaction: All fields redacted');
|
|
271
138
|
}
|
|
139
|
+
|
|
140
|
+
this.applyUnredactionClasses();
|
|
272
141
|
}
|
|
273
142
|
|
|
274
143
|
/**
|
|
275
|
-
*
|
|
144
|
+
* Clear all unredacted fields (everything becomes redacted again)
|
|
276
145
|
*/
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (this.selectorMatchesNode(selector, node)) {
|
|
283
|
-
return true;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return false;
|
|
146
|
+
public clearUnredactedFields(): void {
|
|
147
|
+
this.unredactedFields.clear();
|
|
148
|
+
logDebug('Unredaction: All fields cleared, everything redacted');
|
|
149
|
+
|
|
150
|
+
this.removeUnredactionClasses();
|
|
288
151
|
}
|
|
289
152
|
|
|
290
153
|
/**
|
|
291
|
-
* Check if
|
|
154
|
+
* Check if any fields are currently unredacted
|
|
292
155
|
*/
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
// Create a temporary element to test the selector
|
|
297
|
-
try {
|
|
298
|
-
const tempElement = document.createElement(node.tagName || 'div');
|
|
299
|
-
|
|
300
|
-
// Copy attributes from the node to the temp element
|
|
301
|
-
if (node.attributes) {
|
|
302
|
-
Object.keys(node.attributes).forEach(key => {
|
|
303
|
-
tempElement.setAttribute(key, node.attributes[key]);
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Test if the selector matches this element
|
|
308
|
-
return tempElement.matches(selector);
|
|
309
|
-
} catch (e) {
|
|
310
|
-
// If matches() is not supported or fails, fall back to basic attribute checking
|
|
311
|
-
return this.basicSelectorMatch(selector, node);
|
|
312
|
-
}
|
|
156
|
+
public hasUnredactedFields(): boolean {
|
|
157
|
+
return this.unredactedFields.size > 0;
|
|
313
158
|
}
|
|
314
159
|
|
|
315
160
|
/**
|
|
316
|
-
*
|
|
161
|
+
* Get the current redaction mode
|
|
317
162
|
*/
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
// Handle simple selectors like 'input[type="password"]'
|
|
322
|
-
if (selector.includes('input[type=')) {
|
|
323
|
-
const typeMatch = selector.match(/input\[type="([^"]+)"\]/);
|
|
324
|
-
if (typeMatch && node.tagName === 'input' && node.attributes.type === typeMatch[1]) {
|
|
325
|
-
return true;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Handle ID selectors like '#email'
|
|
330
|
-
if (selector.startsWith('#')) {
|
|
331
|
-
const id = selector.substring(1);
|
|
332
|
-
return node.attributes.id === id;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Handle class selectors like '.sensitive-field'
|
|
336
|
-
if (selector.startsWith('.')) {
|
|
337
|
-
const className = selector.substring(1);
|
|
338
|
-
return node.attributes.class && node.attributes.class.includes(className);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Handle tag selectors like 'input'
|
|
342
|
-
if (!selector.includes('[') && !selector.includes('.')) {
|
|
343
|
-
return node.tagName && node.tagName.toLowerCase() === selector.toLowerCase();
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return false;
|
|
163
|
+
public getRedactionMode(): 'privacy-first' | 'visibility-first' {
|
|
164
|
+
return this.redactionMode;
|
|
347
165
|
}
|
|
348
166
|
|
|
349
167
|
/**
|
|
350
|
-
*
|
|
168
|
+
* Get the currently unredacted fields
|
|
351
169
|
*/
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
try {
|
|
356
|
-
// For input events (source 5), we need to determine if this is a sensitive field
|
|
357
|
-
if (eventData.source === 5) { // Input event
|
|
358
|
-
const elementId = eventData.id;
|
|
359
|
-
if (elementId !== undefined) {
|
|
360
|
-
// Try to find the element by data-rrweb-id attribute
|
|
361
|
-
let element = document.querySelector(`[data-rrweb-id="${elementId}"]`) as HTMLElement;
|
|
362
|
-
|
|
363
|
-
if (element) {
|
|
364
|
-
return this.shouldRedactElement(element);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Fallback: Try to find by nodeId if available
|
|
368
|
-
if (eventData.nodeId !== undefined) {
|
|
369
|
-
element = document.querySelector(`[data-rrweb-id="${eventData.nodeId}"]`) as HTMLElement;
|
|
370
|
-
if (element) {
|
|
371
|
-
return this.shouldRedactElement(element);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// More aggressive approach: Check all elements that match our selectors
|
|
376
|
-
// and see if any of them are currently focused or have the same ID
|
|
377
|
-
for (const selector of this.userSelectedFields) {
|
|
378
|
-
const matchingElements = document.querySelectorAll(selector);
|
|
379
|
-
if (matchingElements.length > 0) {
|
|
380
|
-
// Check if any of these elements are currently focused
|
|
381
|
-
for (const el of matchingElements) {
|
|
382
|
-
if (el === document.activeElement) {
|
|
383
|
-
logDebug('Redaction: Found focused element matching selector:', selector);
|
|
384
|
-
return true;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// If we still can't find it, try a more direct approach
|
|
391
|
-
// Look for any input element that might be the active one
|
|
392
|
-
const activeElement = document.activeElement;
|
|
393
|
-
if (activeElement && this.shouldRedactElement(activeElement as HTMLElement)) {
|
|
394
|
-
logDebug('Redaction: Active element should be redacted');
|
|
395
|
-
return true;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return false;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// For other event types, try to find the element
|
|
403
|
-
const elementId = eventData.id;
|
|
404
|
-
if (elementId !== undefined) {
|
|
405
|
-
// First try to find by data-rrweb-id attribute
|
|
406
|
-
let element = document.querySelector(`[data-rrweb-id="${elementId}"]`) as HTMLElement;
|
|
407
|
-
|
|
408
|
-
if (element) {
|
|
409
|
-
return this.shouldRedactElement(element);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Also check for nodeId which is another way rrweb identifies elements
|
|
414
|
-
const nodeId = eventData.nodeId;
|
|
415
|
-
if (nodeId !== undefined) {
|
|
416
|
-
const element = document.querySelector(`[data-rrweb-id="${nodeId}"]`) as HTMLElement;
|
|
417
|
-
if (element) {
|
|
418
|
-
return this.shouldRedactElement(element);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// For DOM mutations, check if the target element should be redacted
|
|
423
|
-
if (eventData.target && eventData.target.id) {
|
|
424
|
-
const element = document.querySelector(`[data-rrweb-id="${eventData.target.id}"]`) as HTMLElement;
|
|
425
|
-
if (element) {
|
|
426
|
-
return this.shouldRedactElement(element);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
return false;
|
|
431
|
-
} catch (e) {
|
|
432
|
-
logWarn('Error checking if field should be redacted:', e);
|
|
433
|
-
return false;
|
|
434
|
-
}
|
|
170
|
+
public getUnredactedFields(): string[] {
|
|
171
|
+
return Array.from(this.unredactedFields);
|
|
435
172
|
}
|
|
436
173
|
|
|
437
174
|
/**
|
|
438
175
|
* Get CSS selectors for rrweb masking configuration
|
|
439
|
-
*
|
|
176
|
+
* Returns null if no fields are unredacted (everything stays redacted)
|
|
440
177
|
*/
|
|
441
178
|
public getMaskTextSelector(): string | null {
|
|
442
|
-
if (this.
|
|
443
|
-
|
|
179
|
+
if (this.redactionMode === 'privacy-first') {
|
|
180
|
+
// Privacy-first: mask everything except unredacted fields
|
|
181
|
+
if (this.unredactedFields.size === 0) {
|
|
182
|
+
return null; // Everything stays redacted
|
|
183
|
+
}
|
|
184
|
+
return Array.from(this.unredactedFields).join(',');
|
|
185
|
+
} else {
|
|
186
|
+
// Visibility-first: mask only redacted fields
|
|
187
|
+
if (this.redactedFields.size === 0) {
|
|
188
|
+
return null; // Nothing to redact
|
|
189
|
+
}
|
|
190
|
+
return Array.from(this.redactedFields).join(',');
|
|
444
191
|
}
|
|
445
|
-
return Array.from(this.userSelectedFields).join(',');
|
|
446
192
|
}
|
|
447
193
|
|
|
448
194
|
/**
|
|
449
|
-
*
|
|
195
|
+
* Apply redaction classes to DOM elements (for visibility-first mode)
|
|
196
|
+
* Adds 'rr-mask' class to elements that should be redacted
|
|
450
197
|
*/
|
|
451
|
-
public
|
|
452
|
-
if (this.
|
|
453
|
-
return
|
|
198
|
+
public applyRedactionClasses(): void {
|
|
199
|
+
if (this.redactedFields.size === 0) {
|
|
200
|
+
return;
|
|
454
201
|
}
|
|
455
202
|
|
|
456
|
-
//
|
|
457
|
-
|
|
203
|
+
// Add 'rr-mask' class to redacted elements
|
|
204
|
+
this.redactedFields.forEach(selector => {
|
|
458
205
|
try {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
206
|
+
const elements = document.querySelectorAll(selector);
|
|
207
|
+
elements.forEach(element => {
|
|
208
|
+
element.classList.add('rr-mask');
|
|
209
|
+
});
|
|
210
|
+
logDebug(`Added rr-mask class to ${elements.length} element(s) for selector: ${selector}`);
|
|
462
211
|
} catch (e) {
|
|
463
|
-
// Invalid selector, skip
|
|
464
212
|
logWarn(`Invalid selector: ${selector}`);
|
|
465
213
|
}
|
|
466
|
-
}
|
|
467
|
-
return false;
|
|
214
|
+
});
|
|
468
215
|
}
|
|
469
216
|
|
|
470
217
|
/**
|
|
471
|
-
* Apply
|
|
472
|
-
*
|
|
473
|
-
* This enables rrweb's built-in masking functionality
|
|
218
|
+
* Apply unredaction classes to DOM elements
|
|
219
|
+
* Removes 'rr-mask' class from elements that should be unredacted
|
|
474
220
|
*/
|
|
475
|
-
public
|
|
476
|
-
if (this.
|
|
221
|
+
public applyUnredactionClasses(): void {
|
|
222
|
+
if (this.unredactedFields.size === 0) {
|
|
477
223
|
return;
|
|
478
224
|
}
|
|
479
225
|
|
|
480
|
-
// Remove
|
|
481
|
-
|
|
482
|
-
element.classList.remove('rr-mask');
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
// Add redaction classes to matching elements
|
|
486
|
-
this.userSelectedFields.forEach(selector => {
|
|
226
|
+
// Remove 'rr-mask' class from unredacted elements
|
|
227
|
+
this.unredactedFields.forEach(selector => {
|
|
487
228
|
try {
|
|
488
229
|
const elements = document.querySelectorAll(selector);
|
|
489
230
|
elements.forEach(element => {
|
|
490
|
-
element.classList.
|
|
231
|
+
element.classList.remove('rr-mask');
|
|
491
232
|
});
|
|
492
|
-
logDebug(`
|
|
233
|
+
logDebug(`Removed rr-mask class from ${elements.length} element(s) for selector: ${selector}`);
|
|
493
234
|
} catch (e) {
|
|
494
235
|
logWarn(`Invalid selector: ${selector}`);
|
|
495
236
|
}
|
|
@@ -497,7 +238,31 @@ export class RedactionManager {
|
|
|
497
238
|
}
|
|
498
239
|
|
|
499
240
|
/**
|
|
500
|
-
*
|
|
241
|
+
* Remove all unredaction classes from DOM elements
|
|
242
|
+
*/
|
|
243
|
+
public removeUnredactionClasses(): void {
|
|
244
|
+
// Note: This doesn't add 'rr-mask' classes back - rrweb handles that automatically
|
|
245
|
+
logDebug('Unredaction classes removed');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Check if a selector represents a password field
|
|
250
|
+
*/
|
|
251
|
+
private isPasswordSelector(selector: string): boolean {
|
|
252
|
+
const passwordPatterns = [
|
|
253
|
+
'input[type="password"]',
|
|
254
|
+
'input[type="password" i]',
|
|
255
|
+
'[type="password"]',
|
|
256
|
+
'[type="password" i]'
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
return passwordPatterns.some(pattern =>
|
|
260
|
+
selector.toLowerCase().includes(pattern.toLowerCase().replace(/[\[\]]/g, ''))
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get the original value of an element (for debugging)
|
|
501
266
|
*/
|
|
502
267
|
public getOriginalValue(element: HTMLElement): string | undefined {
|
|
503
268
|
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
@@ -507,10 +272,51 @@ export class RedactionManager {
|
|
|
507
272
|
}
|
|
508
273
|
|
|
509
274
|
/**
|
|
510
|
-
* Check if an element is currently
|
|
275
|
+
* Check if an element is currently unredacted
|
|
511
276
|
*/
|
|
512
|
-
public
|
|
513
|
-
return this.
|
|
277
|
+
public isElementUnredacted(element: HTMLElement): boolean {
|
|
278
|
+
return this.shouldUnredactElement(element);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Check if an element should be unredacted
|
|
283
|
+
*/
|
|
284
|
+
public shouldUnredactElement(element: HTMLElement): boolean {
|
|
285
|
+
if (this.redactionMode === 'privacy-first') {
|
|
286
|
+
// Privacy-first: check if element is in unredacted fields
|
|
287
|
+
if (this.unredactedFields.size === 0) {
|
|
288
|
+
return false; // Nothing unredacted
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Check if any selector matches this element
|
|
292
|
+
for (const selector of this.unredactedFields) {
|
|
293
|
+
try {
|
|
294
|
+
if (element.matches(selector)) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
} catch (e) {
|
|
298
|
+
logWarn(`Invalid selector: ${selector}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
} else {
|
|
303
|
+
// Visibility-first: check if element is NOT in redacted fields
|
|
304
|
+
if (this.redactedFields.size === 0) {
|
|
305
|
+
return true; // Nothing redacted, everything visible
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Check if any selector matches this element
|
|
309
|
+
for (const selector of this.redactedFields) {
|
|
310
|
+
try {
|
|
311
|
+
if (element.matches(selector)) {
|
|
312
|
+
return false; // Element is redacted
|
|
313
|
+
}
|
|
314
|
+
} catch (e) {
|
|
315
|
+
logWarn(`Invalid selector: ${selector}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return true; // Element is not redacted
|
|
319
|
+
}
|
|
514
320
|
}
|
|
515
321
|
}
|
|
516
322
|
|