humanbehavior-js 0.0.8 → 0.1.0
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/index.js +558 -184
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/react/index.js +197 -86
- package/dist/cjs/react/index.js.map +1 -1
- package/dist/esm/index.js +554 -185
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/react/index.js +195 -87
- package/dist/esm/react/index.js.map +1 -1
- package/dist/index.min.js +2 -2
- package/dist/index.min.js.map +1 -1
- package/dist/types/index.d.ts +115 -10
- package/dist/types/react/index.d.ts +8 -9
- package/package.json +3 -2
- package/readme.md +127 -105
- package/simple-spa.html +544 -0
- package/src/api.ts +9 -137
- package/src/index.ts +3 -0
- package/src/react/index.tsx +137 -81
- package/src/redact.ts +19 -17
- package/src/tracker.ts +498 -62
- package/src/utils/logger.ts +144 -0
package/src/api.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { logError, logInfo } from './utils/logger';
|
|
2
|
+
|
|
1
3
|
export const MAX_CHUNK_SIZE_BYTES = 1024 * 1024 * 10; // 10MB chunk size
|
|
2
4
|
|
|
3
5
|
export function isChunkSizeExceeded(currentChunk: any[], newEvent: any, sessionId: string): boolean {
|
|
@@ -147,7 +149,7 @@ export class HumanBehaviorAPI {
|
|
|
147
149
|
|
|
148
150
|
return results.flat();
|
|
149
151
|
} catch (error) {
|
|
150
|
-
|
|
152
|
+
logError('Error sending events:', error);
|
|
151
153
|
throw error;
|
|
152
154
|
}
|
|
153
155
|
}
|
|
@@ -173,7 +175,7 @@ export class HumanBehaviorAPI {
|
|
|
173
175
|
|
|
174
176
|
return await response.json();
|
|
175
177
|
} catch (error) {
|
|
176
|
-
|
|
178
|
+
logError('Error sending user data:', error);
|
|
177
179
|
throw error;
|
|
178
180
|
}
|
|
179
181
|
}
|
|
@@ -200,111 +202,22 @@ export class HumanBehaviorAPI {
|
|
|
200
202
|
|
|
201
203
|
return await response.json();
|
|
202
204
|
} catch (error) {
|
|
203
|
-
|
|
205
|
+
logError('Error authenticating user:', error);
|
|
204
206
|
throw error;
|
|
205
207
|
}
|
|
206
208
|
}
|
|
207
209
|
|
|
208
|
-
async sendSessionComplete(sessionId: string) {
|
|
209
|
-
const response = await fetch(`${this.baseUrl}/api/ingestion/sessionComplete`, {
|
|
210
|
-
method: 'POST',
|
|
211
|
-
headers: {
|
|
212
|
-
'Content-Type': 'application/json',
|
|
213
|
-
'Authorization': `Bearer ${this.apiKey}`
|
|
214
|
-
},
|
|
215
|
-
body: JSON.stringify({ sessionId })
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
if (!response.ok) {
|
|
219
|
-
throw new Error(`Failed to send session complete: ${response.statusText}`);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
210
|
|
|
223
|
-
async sendCustomEvent(eventName: string, eventProperties: Record<string, any>, sessionId: string) {
|
|
224
|
-
const maxRetries = 3;
|
|
225
|
-
let retryCount = 0;
|
|
226
|
-
|
|
227
|
-
while (retryCount < maxRetries) {
|
|
228
|
-
try {
|
|
229
|
-
const response = await fetch(`${this.baseUrl}/api/ingestion/customEvent`, {
|
|
230
|
-
method: 'POST',
|
|
231
|
-
headers: {
|
|
232
|
-
'Content-Type': 'application/json',
|
|
233
|
-
'Authorization': `Bearer ${this.apiKey}`
|
|
234
|
-
},
|
|
235
|
-
body: JSON.stringify({
|
|
236
|
-
name: eventName,
|
|
237
|
-
properties: eventProperties,
|
|
238
|
-
sessionId: sessionId,
|
|
239
|
-
timestamp: new Date().toISOString()
|
|
240
|
-
})
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
if (!response.ok) {
|
|
244
|
-
throw new Error(`Failed to send custom event: ${response.statusText}`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return await response.json();
|
|
248
|
-
} catch (error) {
|
|
249
|
-
retryCount++;
|
|
250
|
-
if (retryCount === maxRetries) {
|
|
251
|
-
console.error('Error sending custom event after max retries:', error);
|
|
252
|
-
throw error;
|
|
253
|
-
}
|
|
254
|
-
// Exponential backoff
|
|
255
|
-
await new Promise(resolve => setTimeout(resolve, Math.pow(2, retryCount) * 1000));
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
211
|
|
|
260
|
-
async sendCustomEvents(events: any[], sessionId: string) {
|
|
261
|
-
const maxRetries = 3;
|
|
262
|
-
let retryCount = 0;
|
|
263
|
-
|
|
264
|
-
while (retryCount < maxRetries) {
|
|
265
|
-
try {
|
|
266
|
-
const response = await fetch(`${this.baseUrl}/api/ingestion/customEvent/batch`, {
|
|
267
|
-
method: 'POST',
|
|
268
|
-
headers: {
|
|
269
|
-
'Content-Type': 'application/json',
|
|
270
|
-
'Authorization': `Bearer ${this.apiKey}`
|
|
271
|
-
},
|
|
272
|
-
body: JSON.stringify({
|
|
273
|
-
events: events.map(event => ({
|
|
274
|
-
...event,
|
|
275
|
-
sessionId: sessionId
|
|
276
|
-
}))
|
|
277
|
-
})
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
if (!response.ok) {
|
|
281
|
-
throw new Error(`Failed to send custom events: ${response.statusText}`);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return await response.json();
|
|
285
|
-
} catch (error) {
|
|
286
|
-
retryCount++;
|
|
287
|
-
if (retryCount === maxRetries) {
|
|
288
|
-
console.error('Error sending custom events after max retries:', error);
|
|
289
|
-
throw error;
|
|
290
|
-
}
|
|
291
|
-
// Exponential backoff
|
|
292
|
-
await new Promise(resolve => setTimeout(resolve, Math.pow(2, retryCount) * 1000));
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
212
|
|
|
297
|
-
|
|
213
|
+
|
|
214
|
+
public sendBeaconEvents(events: any[], sessionId: string) {
|
|
298
215
|
const data = new URLSearchParams()
|
|
299
216
|
data.append('events', encodeURIComponent(JSON.stringify(events)))
|
|
300
217
|
data.append('sessionId', encodeURIComponent(sessionId))
|
|
301
218
|
data.append('timestamp', encodeURIComponent(Date.now().toString()))
|
|
302
219
|
data.append('apiKey', encodeURIComponent(this.apiKey))
|
|
303
|
-
|
|
304
|
-
console.log('Session complete beacon sending');
|
|
305
|
-
localStorage.setItem('koalaware_session_complete', Date.now().toString());
|
|
306
|
-
data.append('sessionComplete', encodeURIComponent('true'))
|
|
307
|
-
}
|
|
220
|
+
|
|
308
221
|
|
|
309
222
|
const success = navigator.sendBeacon(
|
|
310
223
|
`${this.baseUrl}/api/ingestion/events`,
|
|
@@ -314,47 +227,6 @@ export class HumanBehaviorAPI {
|
|
|
314
227
|
// KoalawareTracker.logToStorage(`Sending events beacon: ${this.baseUrl}/api/ingestion/events`);
|
|
315
228
|
// KoalawareTracker.logToStorage(`Events beacon success: ${success}`);
|
|
316
229
|
}
|
|
230
|
+
|
|
317
231
|
|
|
318
|
-
public sendBeaconSessionComplete(sessionId: string) {
|
|
319
|
-
const data = new URLSearchParams()
|
|
320
|
-
data.append('sessionId', sessionId)
|
|
321
|
-
data.append('apiKey', this.apiKey)
|
|
322
|
-
data.append('sessionComplete', 'true')
|
|
323
|
-
|
|
324
|
-
const success = navigator.sendBeacon(
|
|
325
|
-
`${this.baseUrl}/api/ingestion/sessionComplete`,
|
|
326
|
-
data
|
|
327
|
-
);
|
|
328
|
-
|
|
329
|
-
// KoalawareTracker.logToStorage(`Sending completion beacon: ${this.baseUrl}/api/ingestion/sessionComplete`);
|
|
330
|
-
// KoalawareTracker.logToStorage(`Complete beacon success: ${success}`);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
public sendBeaconCustomEvent(eventName: string, eventProperties: Record<string, any>, sessionId: string) {
|
|
334
|
-
const data = new URLSearchParams()
|
|
335
|
-
data.append('name', encodeURIComponent(eventName))
|
|
336
|
-
data.append('properties', encodeURIComponent(JSON.stringify(eventProperties)))
|
|
337
|
-
data.append('sessionId', encodeURIComponent(sessionId))
|
|
338
|
-
data.append('timestamp', encodeURIComponent(new Date().toISOString()))
|
|
339
|
-
data.append('apiKey', encodeURIComponent(this.apiKey))
|
|
340
|
-
|
|
341
|
-
return navigator.sendBeacon(
|
|
342
|
-
`${this.baseUrl}/api/ingestion/customEvent`,
|
|
343
|
-
data
|
|
344
|
-
);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
public sendBeaconCustomEvents(events: any[], sessionId: string) {
|
|
348
|
-
const data = new URLSearchParams()
|
|
349
|
-
data.append('events', encodeURIComponent(JSON.stringify(events.map(event => ({
|
|
350
|
-
...event,
|
|
351
|
-
sessionId: sessionId
|
|
352
|
-
})))))
|
|
353
|
-
data.append('apiKey', encodeURIComponent(this.apiKey))
|
|
354
|
-
|
|
355
|
-
return navigator.sendBeacon(
|
|
356
|
-
`${this.baseUrl}/api/ingestion/customEvent/batch`,
|
|
357
|
-
data
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
232
|
}
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,9 @@ export * from './api';
|
|
|
13
13
|
// Export redaction functionality
|
|
14
14
|
export * from './redact';
|
|
15
15
|
|
|
16
|
+
// Export logger functionality
|
|
17
|
+
export * from './utils/logger';
|
|
18
|
+
|
|
16
19
|
// Also export the tracker as the default export
|
|
17
20
|
export { HumanBehaviorTracker as default } from './tracker';
|
|
18
21
|
|
package/src/react/index.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, { useEffect, useState, createContext, useContext, ReactNode } from "react";
|
|
1
|
+
import React, { useEffect, useState, createContext, useContext, ReactNode, useCallback, useMemo, useRef } from "react";
|
|
2
2
|
import { HumanBehaviorTracker } from "..";
|
|
3
|
+
import { logError, logWarn, logDebug } from "../utils/logger";
|
|
3
4
|
|
|
4
5
|
// Check if we're in a browser environment
|
|
5
6
|
const isBrowser = () => typeof window !== 'undefined';
|
|
@@ -23,8 +24,14 @@ interface HumanBehaviorProviderProps {
|
|
|
23
24
|
apiKey?: string;
|
|
24
25
|
client?: HumanBehaviorTracker;
|
|
25
26
|
children: ReactNode;
|
|
27
|
+
options?: {
|
|
28
|
+
ingestionUrl?: string;
|
|
29
|
+
logLevel?: 'none' | 'error' | 'warn' | 'info' | 'debug';
|
|
30
|
+
redactFields?: string[];
|
|
31
|
+
};
|
|
26
32
|
}
|
|
27
33
|
|
|
34
|
+
// Default context to prevent unnecessary re-renders
|
|
28
35
|
const defaultContext: HumanBehaviorContextType = {
|
|
29
36
|
humanBehavior: null,
|
|
30
37
|
queueEvent: (event: any) => {
|
|
@@ -32,22 +39,38 @@ const defaultContext: HumanBehaviorContextType = {
|
|
|
32
39
|
if (typeof window === 'undefined') {
|
|
33
40
|
return;
|
|
34
41
|
}
|
|
35
|
-
|
|
42
|
+
logWarn('HumanBehavior not initialized yet, event queued:', event);
|
|
36
43
|
}
|
|
37
44
|
};
|
|
38
45
|
|
|
39
|
-
const HumanBehaviorContext = createContext<
|
|
46
|
+
const HumanBehaviorContext = createContext<HumanBehaviorTracker | null>(null);
|
|
40
47
|
|
|
41
|
-
export const HumanBehaviorProvider = ({ apiKey, client, children }: HumanBehaviorProviderProps) => {
|
|
48
|
+
export const HumanBehaviorProvider = ({ apiKey, client, children, options }: HumanBehaviorProviderProps) => {
|
|
42
49
|
const [humanBehavior, setHumanBehavior] = useState<HumanBehaviorTracker | null>(client || null);
|
|
43
50
|
const [eventQueue, setEventQueue] = useState<any[]>([]);
|
|
44
51
|
const [isMounted, setIsMounted] = useState(false);
|
|
45
52
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
53
|
+
|
|
54
|
+
// Use refs to avoid dependency issues in useEffect
|
|
55
|
+
const apiKeyRef = useRef(apiKey);
|
|
56
|
+
const clientRef = useRef(client);
|
|
57
|
+
const eventQueueRef = useRef(eventQueue);
|
|
58
|
+
|
|
59
|
+
// Update refs when props change
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
apiKeyRef.current = apiKey;
|
|
62
|
+
clientRef.current = client;
|
|
63
|
+
}, [apiKey, client]);
|
|
64
|
+
|
|
65
|
+
// Update eventQueue ref when queue changes
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
eventQueueRef.current = eventQueue;
|
|
68
|
+
}, [eventQueue]);
|
|
46
69
|
|
|
47
|
-
//
|
|
48
|
-
const queueEvent = (event: any) => {
|
|
70
|
+
// Memoized queueEvent function to prevent unnecessary re-renders
|
|
71
|
+
const queueEvent = useCallback((event: any) => {
|
|
49
72
|
setEventQueue(prev => [...prev, event]);
|
|
50
|
-
};
|
|
73
|
+
}, []);
|
|
51
74
|
|
|
52
75
|
// Handle mounting state
|
|
53
76
|
useEffect(() => {
|
|
@@ -66,15 +89,15 @@ export const HumanBehaviorProvider = ({ apiKey, client, children }: HumanBehavio
|
|
|
66
89
|
}
|
|
67
90
|
|
|
68
91
|
// If client is provided, use that
|
|
69
|
-
if (
|
|
70
|
-
setHumanBehavior(
|
|
92
|
+
if (clientRef.current) {
|
|
93
|
+
setHumanBehavior(clientRef.current);
|
|
71
94
|
setIsInitialized(true);
|
|
72
95
|
return;
|
|
73
96
|
}
|
|
74
97
|
|
|
75
98
|
// If no client is provided, apiKey is required
|
|
76
|
-
if (!
|
|
77
|
-
|
|
99
|
+
if (!apiKeyRef.current || apiKeyRef.current.trim() === '') {
|
|
100
|
+
logError("An apiKey is required when no client is provided");
|
|
78
101
|
return;
|
|
79
102
|
}
|
|
80
103
|
|
|
@@ -83,7 +106,10 @@ export const HumanBehaviorProvider = ({ apiKey, client, children }: HumanBehavio
|
|
|
83
106
|
}
|
|
84
107
|
|
|
85
108
|
// Create new tracker instance with the validated apiKey
|
|
86
|
-
const tracker = new HumanBehaviorTracker(
|
|
109
|
+
const tracker = new HumanBehaviorTracker(
|
|
110
|
+
apiKeyRef.current.trim(),
|
|
111
|
+
process.env.NEXT_PUBLIC_INGESTION_URL || 'https://ingestion.humanbehavior.ai'
|
|
112
|
+
);
|
|
87
113
|
setHumanBehavior(tracker);
|
|
88
114
|
|
|
89
115
|
// Wait for initialization to complete
|
|
@@ -91,10 +117,11 @@ export const HumanBehaviorProvider = ({ apiKey, client, children }: HumanBehavio
|
|
|
91
117
|
setIsInitialized(true);
|
|
92
118
|
|
|
93
119
|
// Process any queued events
|
|
94
|
-
|
|
95
|
-
|
|
120
|
+
const currentQueue = eventQueueRef.current;
|
|
121
|
+
if (currentQueue.length > 0) {
|
|
122
|
+
currentQueue.forEach(event => {
|
|
96
123
|
if (event.type === 'identify') {
|
|
97
|
-
|
|
124
|
+
logDebug('Processing queued identify event', event.userProperties);
|
|
98
125
|
tracker.addUserInfo(event.userProperties);
|
|
99
126
|
} else {
|
|
100
127
|
tracker.addEvent(event);
|
|
@@ -103,87 +130,116 @@ export const HumanBehaviorProvider = ({ apiKey, client, children }: HumanBehavio
|
|
|
103
130
|
setEventQueue([]); // Clear the queue
|
|
104
131
|
}
|
|
105
132
|
}).catch(error => {
|
|
106
|
-
|
|
133
|
+
logError('Failed to initialize HumanBehaviorTracker:', error);
|
|
107
134
|
});
|
|
108
|
-
}, [apiKey, client, eventQueue
|
|
135
|
+
}, [isMounted, humanBehavior]); // Removed apiKey, client, eventQueue from dependencies
|
|
136
|
+
|
|
137
|
+
// Memoized context value to prevent unnecessary re-renders
|
|
138
|
+
const contextValue = useMemo(() => {
|
|
139
|
+
if (!isMounted) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!isInitialized) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return humanBehavior;
|
|
148
|
+
}, [isMounted, isInitialized, humanBehavior]);
|
|
109
149
|
|
|
110
150
|
// If not in browser, render children without context
|
|
111
151
|
if (!(isBrowser())) {
|
|
112
152
|
return <>{children}</>;
|
|
113
153
|
}
|
|
114
154
|
|
|
115
|
-
// If not mounted yet, render children with queuing context
|
|
116
|
-
if (!isMounted) {
|
|
117
|
-
return (
|
|
118
|
-
<HumanBehaviorContext.Provider value={{ humanBehavior: null, queueEvent }}>
|
|
119
|
-
{children}
|
|
120
|
-
</HumanBehaviorContext.Provider>
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// If not initialized yet, render children with queuing context
|
|
125
|
-
if (!isInitialized) {
|
|
126
|
-
return (
|
|
127
|
-
<HumanBehaviorContext.Provider value={{ humanBehavior: null, queueEvent }}>
|
|
128
|
-
{children}
|
|
129
|
-
</HumanBehaviorContext.Provider>
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
155
|
return (
|
|
134
|
-
<HumanBehaviorContext.Provider value={
|
|
156
|
+
<HumanBehaviorContext.Provider value={contextValue}>
|
|
157
|
+
<HumanBehaviorPageView />
|
|
135
158
|
{children}
|
|
136
159
|
</HumanBehaviorContext.Provider>
|
|
137
160
|
);
|
|
138
161
|
};
|
|
139
162
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
163
|
+
// No-op implementation for server-side
|
|
164
|
+
const serverSideImplementation: HumanBehaviorInterface = {
|
|
165
|
+
addEvent: () => {},
|
|
166
|
+
addUserInfo: async () => {},
|
|
167
|
+
start: () => {},
|
|
168
|
+
stop: () => {},
|
|
169
|
+
viewLogs: () => {},
|
|
170
|
+
};
|
|
147
171
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
};
|
|
172
|
+
// Memoized queuing implementation for initialization period
|
|
173
|
+
const createQueuingImplementation = (queueEvent: (event: any) => void): HumanBehaviorInterface => ({
|
|
174
|
+
addEvent: (event: any) => {
|
|
175
|
+
queueEvent(event);
|
|
176
|
+
},
|
|
177
|
+
addUserInfo: async (userProperties: Record<string, any>) => {
|
|
178
|
+
queueEvent({
|
|
179
|
+
type: 'identify',
|
|
180
|
+
userProperties,
|
|
181
|
+
});
|
|
182
|
+
},
|
|
183
|
+
start: () => {
|
|
184
|
+
// Start will be called automatically when initialized
|
|
185
|
+
},
|
|
186
|
+
stop: () => {
|
|
187
|
+
// Stop is a no-op when not initialized
|
|
188
|
+
},
|
|
189
|
+
viewLogs: () => {
|
|
190
|
+
logWarn('Logs are not available until HumanBehaviorTracker is initialized');
|
|
158
191
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
export const useHumanBehavior = () => {
|
|
195
|
+
const tracker = useContext(HumanBehaviorContext);
|
|
196
|
+
if (!tracker) {
|
|
197
|
+
throw new Error('useHumanBehavior must be used within a HumanBehaviorProvider');
|
|
163
198
|
}
|
|
199
|
+
return tracker;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Automatic page tracking component (similar to PostHog's PostHogPageView)
|
|
203
|
+
function HumanBehaviorPageView() {
|
|
204
|
+
const tracker = useContext(HumanBehaviorContext);
|
|
164
205
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
if (tracker && typeof window !== 'undefined') {
|
|
208
|
+
// Track initial page load
|
|
209
|
+
tracker.trackPageView();
|
|
210
|
+
|
|
211
|
+
// Listen for route changes (for SPAs)
|
|
212
|
+
const handleRouteChange = () => {
|
|
213
|
+
tracker.trackPageView();
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Listen for popstate (back/forward navigation)
|
|
217
|
+
window.addEventListener('popstate', handleRouteChange);
|
|
218
|
+
|
|
219
|
+
// Listen for pushstate/replacestate (programmatic navigation)
|
|
220
|
+
const originalPushState = history.pushState;
|
|
221
|
+
const originalReplaceState = history.replaceState;
|
|
222
|
+
|
|
223
|
+
history.pushState = function(...args) {
|
|
224
|
+
originalPushState.apply(this, args);
|
|
225
|
+
handleRouteChange();
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
history.replaceState = function(...args) {
|
|
229
|
+
originalReplaceState.apply(this, args);
|
|
230
|
+
handleRouteChange();
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return () => {
|
|
234
|
+
window.removeEventListener('popstate', handleRouteChange);
|
|
235
|
+
history.pushState = originalPushState;
|
|
236
|
+
history.replaceState = originalReplaceState;
|
|
237
|
+
};
|
|
187
238
|
}
|
|
188
|
-
};
|
|
189
|
-
|
|
239
|
+
}, [tracker]);
|
|
240
|
+
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Export the tracker class for direct use
|
|
245
|
+
export { HumanBehaviorTracker };
|
package/src/redact.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Redaction functionality for sensitive input fields
|
|
2
2
|
// This module provides methods to redact sensitive input fields in event recordings
|
|
3
3
|
|
|
4
|
+
import { logDebug, logWarn } from './utils/logger';
|
|
5
|
+
|
|
4
6
|
// Check if we're in a browser environment
|
|
5
7
|
const isBrowser = typeof window !== 'undefined';
|
|
6
8
|
|
|
@@ -39,18 +41,18 @@ export class RedactionManager {
|
|
|
39
41
|
fields.forEach(field => this.userSelectedFields.add(field));
|
|
40
42
|
|
|
41
43
|
if (fields.length > 0) {
|
|
42
|
-
|
|
44
|
+
logDebug(`Redaction: Active for ${fields.length} field(s):`, fields);
|
|
43
45
|
|
|
44
46
|
// Debug: Check if elements exist
|
|
45
47
|
fields.forEach(selector => {
|
|
46
48
|
const elements = document.querySelectorAll(selector);
|
|
47
|
-
|
|
49
|
+
logDebug(`Redaction: Found ${elements.length} element(s) for selector '${selector}'`);
|
|
48
50
|
elements.forEach((el, index) => {
|
|
49
|
-
|
|
51
|
+
logDebug(`Redaction: Element ${index} for '${selector}':`, el);
|
|
50
52
|
});
|
|
51
53
|
});
|
|
52
54
|
} else {
|
|
53
|
-
|
|
55
|
+
logDebug('Redaction: Disabled - no fields selected');
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
|
|
@@ -85,7 +87,7 @@ export class RedactionManager {
|
|
|
85
87
|
if (processedEvent.data.source === 5) { // Input event
|
|
86
88
|
const shouldRedact = this.isFieldSelected(processedEvent.data);
|
|
87
89
|
if (shouldRedact) {
|
|
88
|
-
|
|
90
|
+
logDebug('Redaction: Processing input event for redaction');
|
|
89
91
|
this.redactInputEvent(processedEvent.data);
|
|
90
92
|
}
|
|
91
93
|
}
|
|
@@ -114,14 +116,14 @@ export class RedactionManager {
|
|
|
114
116
|
return;
|
|
115
117
|
}
|
|
116
118
|
|
|
117
|
-
|
|
119
|
+
logDebug('Redaction: Redacting input event with text:', inputData.text);
|
|
118
120
|
|
|
119
121
|
// Redact all text-related properties that could contain input data
|
|
120
122
|
const textProperties = ['text', 'value', 'content', 'data', 'input', 'textContent'];
|
|
121
123
|
textProperties.forEach(prop => {
|
|
122
124
|
if (inputData[prop] !== undefined && typeof inputData[prop] === 'string') {
|
|
123
125
|
inputData[prop] = this.redactedText;
|
|
124
|
-
|
|
126
|
+
logDebug(`Redaction: Redacted property '${prop}'`);
|
|
125
127
|
}
|
|
126
128
|
});
|
|
127
129
|
|
|
@@ -129,7 +131,7 @@ export class RedactionManager {
|
|
|
129
131
|
Object.keys(inputData).forEach(key => {
|
|
130
132
|
if (typeof inputData[key] === 'string' && inputData[key].length > 0) {
|
|
131
133
|
inputData[key] = this.redactedText;
|
|
132
|
-
|
|
134
|
+
logDebug(`Redaction: Redacted additional property '${key}'`);
|
|
133
135
|
}
|
|
134
136
|
});
|
|
135
137
|
|
|
@@ -137,11 +139,11 @@ export class RedactionManager {
|
|
|
137
139
|
if (inputData.attributes && typeof inputData.attributes === 'object') {
|
|
138
140
|
if (inputData.attributes.value && typeof inputData.attributes.value === 'string') {
|
|
139
141
|
inputData.attributes.value = this.redactedText;
|
|
140
|
-
|
|
142
|
+
logDebug('Redaction: Redacted nested value attribute');
|
|
141
143
|
}
|
|
142
144
|
}
|
|
143
145
|
|
|
144
|
-
|
|
146
|
+
logDebug('Redaction: Input event redaction complete');
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
/**
|
|
@@ -209,7 +211,7 @@ export class RedactionManager {
|
|
|
209
211
|
|
|
210
212
|
return false;
|
|
211
213
|
} catch (e) {
|
|
212
|
-
|
|
214
|
+
logWarn('Error checking if DOM change should be redacted:', e);
|
|
213
215
|
return false;
|
|
214
216
|
}
|
|
215
217
|
}
|
|
@@ -373,10 +375,10 @@ export class RedactionManager {
|
|
|
373
375
|
if (matchingElements.length > 0) {
|
|
374
376
|
// Check if any of these elements are currently focused
|
|
375
377
|
for (const el of matchingElements) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
378
|
+
if (el === document.activeElement) {
|
|
379
|
+
logDebug('Redaction: Found focused element matching selector:', selector);
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
380
382
|
}
|
|
381
383
|
}
|
|
382
384
|
}
|
|
@@ -385,7 +387,7 @@ export class RedactionManager {
|
|
|
385
387
|
// Look for any input element that might be the active one
|
|
386
388
|
const activeElement = document.activeElement;
|
|
387
389
|
if (activeElement && this.shouldRedactElement(activeElement as HTMLElement)) {
|
|
388
|
-
|
|
390
|
+
logDebug('Redaction: Active element should be redacted');
|
|
389
391
|
return true;
|
|
390
392
|
}
|
|
391
393
|
|
|
@@ -423,7 +425,7 @@ export class RedactionManager {
|
|
|
423
425
|
|
|
424
426
|
return false;
|
|
425
427
|
} catch (e) {
|
|
426
|
-
|
|
428
|
+
logWarn('Error checking if field should be redacted:', e);
|
|
427
429
|
return false;
|
|
428
430
|
}
|
|
429
431
|
}
|