humanbehavior-js 0.4.15 → 0.4.17
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/wizard/index.cjs +6 -8
- package/dist/cjs/wizard/index.cjs.map +1 -1
- package/dist/cli/ai-auto-install.js +6 -8
- package/dist/cli/ai-auto-install.js.map +1 -1
- package/dist/esm/wizard/index.js +6 -8
- package/dist/esm/wizard/index.js.map +1 -1
- package/package/WIZARD_USAGE_GUIDE.md +381 -0
- package/package/canvas-recording-demo.html +143 -0
- package/package/clean-console-demo.html +39 -0
- package/package/dist/cjs/angular/index.cjs +14354 -0
- package/package/dist/cjs/angular/index.cjs.map +1 -0
- package/package/dist/cjs/index.cjs +14323 -0
- package/package/dist/cjs/index.cjs.map +1 -0
- package/package/dist/cjs/install-wizard.cjs +1530 -0
- package/package/dist/cjs/install-wizard.cjs.map +1 -0
- package/package/dist/cjs/react/index.cjs +14478 -0
- package/package/dist/cjs/react/index.cjs.map +1 -0
- package/package/dist/cjs/remix/index.cjs +14452 -0
- package/package/dist/cjs/remix/index.cjs.map +1 -0
- package/package/dist/cjs/svelte/index.cjs +14308 -0
- package/package/dist/cjs/svelte/index.cjs.map +1 -0
- package/package/dist/cjs/vue/index.cjs +14317 -0
- package/package/dist/cjs/vue/index.cjs.map +1 -0
- package/package/dist/cjs/wizard/index.cjs +3446 -0
- package/package/dist/cjs/wizard/index.cjs.map +1 -0
- package/package/dist/cli/ai-auto-install.cjs +57161 -0
- package/package/dist/cli/ai-auto-install.cjs.map +1 -0
- package/package/dist/cli/ai-auto-install.js +1969 -0
- package/package/dist/cli/ai-auto-install.js.map +1 -0
- package/package/dist/cli/auto-install.cjs +56352 -0
- package/package/dist/cli/auto-install.cjs.map +1 -0
- package/package/dist/cli/auto-install.js +1957 -0
- package/package/dist/cli/auto-install.js.map +1 -0
- package/package/dist/esm/angular/index.js +14350 -0
- package/package/dist/esm/angular/index.js.map +1 -0
- package/package/dist/esm/index.js +14309 -0
- package/package/dist/esm/index.js.map +1 -0
- package/package/dist/esm/install-wizard.js +1507 -0
- package/package/dist/esm/install-wizard.js.map +1 -0
- package/package/dist/esm/react/index.js +14472 -0
- package/package/dist/esm/react/index.js.map +1 -0
- package/package/dist/esm/remix/index.js +14448 -0
- package/package/dist/esm/remix/index.js.map +1 -0
- package/package/dist/esm/svelte/index.js +14306 -0
- package/package/dist/esm/svelte/index.js.map +1 -0
- package/package/dist/esm/vue/index.js +14315 -0
- package/package/dist/esm/vue/index.js.map +1 -0
- package/package/dist/esm/wizard/index.js +3415 -0
- package/package/dist/esm/wizard/index.js.map +1 -0
- package/package/dist/index.min.js +2 -0
- package/package/dist/index.min.js.map +1 -0
- package/package/dist/types/angular/index.d.ts +267 -0
- package/package/dist/types/index.d.ts +373 -0
- package/package/dist/types/install-wizard.d.ts +156 -0
- package/package/dist/types/react/index.d.ts +255 -0
- package/package/dist/types/remix/index.d.ts +246 -0
- package/package/dist/types/svelte/index.d.ts +232 -0
- package/package/dist/types/vue/index.d.ts +15 -0
- package/package/dist/types/wizard/index.d.ts +523 -0
- package/package/package.json +105 -0
- package/package/readme.md +281 -0
- package/package/rollup.config.js +422 -0
- package/package/simple-demo.html +26 -0
- package/package/simple-spa.html +838 -0
- package/package/src/angular/index.ts +79 -0
- package/package/src/api.ts +376 -0
- package/package/src/index.ts +28 -0
- package/package/src/react/AutoInstallWizard.tsx +557 -0
- package/package/src/react/browser.ts +8 -0
- package/package/src/react/index.tsx +308 -0
- package/package/src/redact.ts +521 -0
- package/package/src/remix/index.ts +16 -0
- package/package/src/svelte/index.ts +14 -0
- package/package/src/tracker.ts +1319 -0
- package/package/src/types/clack.d.ts +31 -0
- package/package/src/utils/logger.ts +144 -0
- package/package/src/vue/index.ts +29 -0
- package/package/src/wizard/README.md +114 -0
- package/package/src/wizard/ai/ai-install-wizard.ts +897 -0
- package/package/src/wizard/ai/manual-framework-wizard.ts +238 -0
- package/package/src/wizard/cli/ai-auto-install.ts +243 -0
- package/package/src/wizard/cli/auto-install.ts +224 -0
- package/package/src/wizard/core/install-wizard.ts +1744 -0
- package/package/src/wizard/index.ts +23 -0
- package/package/src/wizard/services/centralized-ai-service.ts +668 -0
- package/package/src/wizard/services/remote-ai-service.ts +240 -0
- package/package/tsconfig.json +24 -0
- package/package.json +1 -1
- package/src/wizard/cli/ai-auto-install.ts +4 -6
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
// Redaction functionality for sensitive input fields
|
|
2
|
+
// This module provides methods to configure rrweb's built-in masking
|
|
3
|
+
// Uses CSS selectors and classes for reliable redaction without event corruption
|
|
4
|
+
|
|
5
|
+
import { logDebug, logWarn } from './utils/logger';
|
|
6
|
+
|
|
7
|
+
// Check if we're in a browser environment
|
|
8
|
+
const isBrowser = typeof window !== 'undefined';
|
|
9
|
+
|
|
10
|
+
export interface RedactionOptions {
|
|
11
|
+
redactedText?: string;
|
|
12
|
+
excludeSelectors?: string[];
|
|
13
|
+
userFields?: string[]; // Fields that the user wants to redact
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class RedactionManager {
|
|
17
|
+
private redactedText: string = '[REDACTED]';
|
|
18
|
+
private userSelectedFields: Set<string> = new Set(); // User-selected fields to redact
|
|
19
|
+
private excludeSelectors: string[] = [
|
|
20
|
+
'[data-no-redact="true"]',
|
|
21
|
+
'.human-behavior-no-redact'
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
constructor(options?: RedactionOptions) {
|
|
25
|
+
if (options?.redactedText) {
|
|
26
|
+
this.redactedText = options.redactedText;
|
|
27
|
+
}
|
|
28
|
+
if (options?.excludeSelectors) {
|
|
29
|
+
this.excludeSelectors = [...this.excludeSelectors, ...options.excludeSelectors];
|
|
30
|
+
}
|
|
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
|
+
|
|
45
|
+
if (fields.length > 0) {
|
|
46
|
+
logDebug(`Redaction: Active for ${fields.length} field(s):`, fields);
|
|
47
|
+
|
|
48
|
+
// Debug: Check if elements exist
|
|
49
|
+
fields.forEach(selector => {
|
|
50
|
+
const elements = document.querySelectorAll(selector);
|
|
51
|
+
logDebug(`Redaction: Found ${elements.length} element(s) for selector '${selector}'`);
|
|
52
|
+
elements.forEach((el, index) => {
|
|
53
|
+
logDebug(`Redaction: Element ${index} for '${selector}':`, el);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
} else {
|
|
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);
|
|
96
|
+
}
|
|
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
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if (processedEvent.type === 2) { // FullSnapshot
|
|
108
|
+
this.redactFullSnapshot(processedEvent.data);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return processedEvent;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Redact sensitive data in input events
|
|
116
|
+
*/
|
|
117
|
+
private redactInputEvent(inputData: any): void {
|
|
118
|
+
// Check if this input event is from a field we want to redact
|
|
119
|
+
if (!this.isFieldSelected(inputData)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
logDebug('Redaction: Redacting input event with text:', inputData.text);
|
|
124
|
+
|
|
125
|
+
// Redact all text-related properties that could contain input data
|
|
126
|
+
const textProperties = ['text', 'value', 'content', 'data', 'input', 'textContent'];
|
|
127
|
+
textProperties.forEach(prop => {
|
|
128
|
+
if (inputData[prop] !== undefined && typeof inputData[prop] === 'string') {
|
|
129
|
+
inputData[prop] = this.redactedText;
|
|
130
|
+
logDebug(`Redaction: Redacted property '${prop}'`);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Also check for any other string properties that might contain input data
|
|
135
|
+
Object.keys(inputData).forEach(key => {
|
|
136
|
+
if (typeof inputData[key] === 'string' && inputData[key].length > 0) {
|
|
137
|
+
inputData[key] = this.redactedText;
|
|
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
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Also check for attribute changes that might contain input data
|
|
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
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Check if a DOM change should be redacted based on its ID
|
|
191
|
+
*/
|
|
192
|
+
private shouldRedactDOMChange(changeData: any): boolean {
|
|
193
|
+
if (!isBrowser) return false;
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
// Check if this change has an ID that we can use to find the element
|
|
197
|
+
const elementId = changeData.id;
|
|
198
|
+
if (elementId !== undefined) {
|
|
199
|
+
// Try to find the element by data-rrweb-id attribute
|
|
200
|
+
let element = document.querySelector(`[data-rrweb-id="${elementId}"]`) as HTMLElement;
|
|
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
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return false;
|
|
217
|
+
} catch (e) {
|
|
218
|
+
logWarn('Error checking if DOM change should be redacted:', e);
|
|
219
|
+
return false;
|
|
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);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Recursively redact sensitive data in DOM nodes
|
|
245
|
+
*/
|
|
246
|
+
private redactNode(node: any): void {
|
|
247
|
+
if (!node) return;
|
|
248
|
+
|
|
249
|
+
// Check if this node should be redacted
|
|
250
|
+
if (node.type === 2 && node.tagName &&
|
|
251
|
+
(node.tagName.toLowerCase() === 'input' || node.tagName.toLowerCase() === 'textarea')) {
|
|
252
|
+
|
|
253
|
+
// Check if this input/textarea should be redacted
|
|
254
|
+
if (this.shouldRedactNode(node)) {
|
|
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
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Check if a node should be redacted based on its attributes
|
|
276
|
+
*/
|
|
277
|
+
private shouldRedactNode(node: any): boolean {
|
|
278
|
+
if (!node.attributes) return false;
|
|
279
|
+
|
|
280
|
+
// Check if any of our selectors would match this node
|
|
281
|
+
for (const selector of this.userSelectedFields) {
|
|
282
|
+
if (this.selectorMatchesNode(selector, node)) {
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Check if a CSS selector would match a node based on its attributes
|
|
292
|
+
*/
|
|
293
|
+
private selectorMatchesNode(selector: string, node: any): boolean {
|
|
294
|
+
if (!node.attributes) return false;
|
|
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
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Basic selector matching for environments where matches() is not available
|
|
317
|
+
*/
|
|
318
|
+
private basicSelectorMatch(selector: string, node: any): boolean {
|
|
319
|
+
if (!node.attributes) return false;
|
|
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;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Check if an event is from a field that should be redacted
|
|
351
|
+
*/
|
|
352
|
+
private isFieldSelected(eventData: any): boolean {
|
|
353
|
+
if (!isBrowser) return false;
|
|
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
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Get CSS selectors for rrweb masking configuration
|
|
439
|
+
* Used to configure rrweb's maskTextSelector option
|
|
440
|
+
*/
|
|
441
|
+
public getMaskTextSelector(): string | null {
|
|
442
|
+
if (this.userSelectedFields.size === 0) {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
return Array.from(this.userSelectedFields).join(',');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Check if an element should be redacted (for rrweb maskTextFn/maskInputFn)
|
|
450
|
+
*/
|
|
451
|
+
public shouldRedactElement(element: HTMLElement): boolean {
|
|
452
|
+
if (this.userSelectedFields.size === 0) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Check if any selector matches this element
|
|
457
|
+
for (const selector of this.userSelectedFields) {
|
|
458
|
+
try {
|
|
459
|
+
if (element.matches(selector)) {
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
} catch (e) {
|
|
463
|
+
// Invalid selector, skip
|
|
464
|
+
logWarn(`Invalid selector: ${selector}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Apply rrweb masking classes to DOM elements
|
|
472
|
+
* Adds 'rr-mask' class to elements that should be redacted
|
|
473
|
+
* This enables rrweb's built-in masking functionality
|
|
474
|
+
*/
|
|
475
|
+
public applyRedactionClasses(): void {
|
|
476
|
+
if (this.userSelectedFields.size === 0) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Remove existing redaction classes
|
|
481
|
+
document.querySelectorAll('.rr-mask').forEach(element => {
|
|
482
|
+
element.classList.remove('rr-mask');
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Add redaction classes to matching elements
|
|
486
|
+
this.userSelectedFields.forEach(selector => {
|
|
487
|
+
try {
|
|
488
|
+
const elements = document.querySelectorAll(selector);
|
|
489
|
+
elements.forEach(element => {
|
|
490
|
+
element.classList.add('rr-mask');
|
|
491
|
+
});
|
|
492
|
+
logDebug(`Applied rr-mask class to ${elements.length} element(s) for selector: ${selector}`);
|
|
493
|
+
} catch (e) {
|
|
494
|
+
logWarn(`Invalid selector: ${selector}`);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Get the original value of a redacted element (for debugging)
|
|
501
|
+
*/
|
|
502
|
+
public getOriginalValue(element: HTMLElement): string | undefined {
|
|
503
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
504
|
+
return element.value;
|
|
505
|
+
}
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Check if an element is currently being redacted
|
|
511
|
+
*/
|
|
512
|
+
public isElementRedacted(element: HTMLElement): boolean {
|
|
513
|
+
return this.shouldRedactElement(element);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Export a default instance
|
|
518
|
+
export const redactionManager = new RedactionManager();
|
|
519
|
+
|
|
520
|
+
// Export the class for custom instances
|
|
521
|
+
export default RedactionManager;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { HumanBehaviorTracker } from '../index.js';
|
|
2
|
+
import type { LoaderFunctionArgs } from '@remix-run/node';
|
|
3
|
+
|
|
4
|
+
// Remix-specific loader helper
|
|
5
|
+
export function createHumanBehaviorLoader() {
|
|
6
|
+
return async ({ request }: LoaderFunctionArgs) => {
|
|
7
|
+
return {
|
|
8
|
+
ENV: {
|
|
9
|
+
HUMANBEHAVIOR_API_KEY: process.env.HUMANBEHAVIOR_API_KEY,
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Re-export React components for convenience
|
|
16
|
+
export { HumanBehaviorProvider, useHumanBehavior } from '../react/index.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { HumanBehaviorTracker } from '../index.js';
|
|
2
|
+
|
|
3
|
+
// Create a Svelte store-like interface for HumanBehavior
|
|
4
|
+
export const humanBehaviorStore = {
|
|
5
|
+
init: (apiKey: string, options?: {
|
|
6
|
+
ingestionUrl?: string;
|
|
7
|
+
logLevel?: 'none' | 'error' | 'warn' | 'info' | 'debug';
|
|
8
|
+
redactFields?: string[];
|
|
9
|
+
suppressConsoleErrors?: boolean;
|
|
10
|
+
recordCanvas?: boolean; // Enable canvas recording with PostHog-style protection
|
|
11
|
+
}) => {
|
|
12
|
+
return HumanBehaviorTracker.init(apiKey, options);
|
|
13
|
+
}
|
|
14
|
+
};
|