humanbehavior-js 0.0.5 → 0.0.8
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 +912 -596
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/react/index.js +38 -82
- package/dist/cjs/react/index.js.map +1 -1
- package/dist/esm/index.js +911 -597
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/react/index.js +38 -82
- 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 +111 -3
- package/dist/types/react/index.d.ts +1 -1
- package/package.json +3 -14
- package/readme.md +116 -28
- package/rollup.config.js +106 -0
- package/src/api.ts +360 -0
- package/src/index.ts +22 -0
- package/src/react/index.tsx +189 -0
- package/src/redact.ts +474 -0
- package/src/tracker.ts +361 -0
- package/tsconfig.json +24 -0
- package/index.d.ts +0 -2
- package/index.js +0 -3
- package/react.d.ts +0 -2
- package/react.js +0 -2
package/src/tracker.ts
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import * as rrweb from 'rrweb';
|
|
2
|
+
import { v1 as uuidv1 } from 'uuid';
|
|
3
|
+
import { HumanBehaviorAPI } from './api';
|
|
4
|
+
import { RedactionManager, RedactionOptions } from './redact';
|
|
5
|
+
|
|
6
|
+
// Check if we're in a browser environment
|
|
7
|
+
const isBrowser = typeof window !== 'undefined';
|
|
8
|
+
|
|
9
|
+
// Add type declaration at the top level
|
|
10
|
+
declare global {
|
|
11
|
+
interface Window {
|
|
12
|
+
HumanBehaviorTracker: typeof HumanBehaviorTracker;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class HumanBehaviorTracker {
|
|
17
|
+
private eventIngestionQueue: any[] = [];
|
|
18
|
+
private queueSizeBytes: number = 0;
|
|
19
|
+
private rejectedEvents: any[] = [];
|
|
20
|
+
private isProcessingRejectedEvents: boolean = false;
|
|
21
|
+
|
|
22
|
+
private sessionId: string;
|
|
23
|
+
private userProperties: Record<string, any> = {};
|
|
24
|
+
private isProcessing: boolean = false;
|
|
25
|
+
private flushInterval: number | null = null;
|
|
26
|
+
private readonly FLUSH_INTERVAL_MS = 5000; // Flush every 5 seconds
|
|
27
|
+
private api: HumanBehaviorAPI;
|
|
28
|
+
private endUserId: string | null = null;
|
|
29
|
+
private apiKey: string;
|
|
30
|
+
private initialized: boolean = false;
|
|
31
|
+
public initializationPromise: Promise<void> | null = null;
|
|
32
|
+
private redactionManager: RedactionManager;
|
|
33
|
+
|
|
34
|
+
constructor(apiKey: string | undefined, ingestionUrl?: string) {
|
|
35
|
+
if (!apiKey) {
|
|
36
|
+
throw new Error('Human Behavior API Key is required');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ========================================
|
|
40
|
+
// DEVELOPER: Choose your ingestion server
|
|
41
|
+
// ========================================
|
|
42
|
+
// Uncomment ONE of the following lines to select your server:
|
|
43
|
+
|
|
44
|
+
// AWS Development Server
|
|
45
|
+
const defaultIngestionUrl = 'http://3.137.217.33:3000';
|
|
46
|
+
|
|
47
|
+
// Vercel Production Server
|
|
48
|
+
// const defaultIngestionUrl = 'https://ingestion-server.vercel.app';
|
|
49
|
+
|
|
50
|
+
// Local Development Server
|
|
51
|
+
// const defaultIngestionUrl = 'http://localhost:3000';
|
|
52
|
+
|
|
53
|
+
this.api = new HumanBehaviorAPI({
|
|
54
|
+
apiKey: apiKey,
|
|
55
|
+
ingestionUrl: ingestionUrl || defaultIngestionUrl
|
|
56
|
+
});
|
|
57
|
+
this.apiKey = apiKey;
|
|
58
|
+
this.redactionManager = new RedactionManager();
|
|
59
|
+
|
|
60
|
+
// Check for existing session ID and last activity time in localStorage
|
|
61
|
+
const existingSessionId = isBrowser ? localStorage.getItem('human_behavior_session_id') : null;
|
|
62
|
+
const lastActivity = isBrowser ? localStorage.getItem('human_behavior_last_activity') : null;
|
|
63
|
+
|
|
64
|
+
// If we have a last activity time, check if it's within 30 minutes
|
|
65
|
+
const thirtyMinutesAgo = Date.now() - (30 * 60 * 1000);
|
|
66
|
+
const shouldUseExistingSession = lastActivity && parseInt(lastActivity) > thirtyMinutesAgo;
|
|
67
|
+
this.sessionId = (existingSessionId && shouldUseExistingSession) ? existingSessionId : uuidv1();
|
|
68
|
+
|
|
69
|
+
// Store the session ID if it's new
|
|
70
|
+
if ((!existingSessionId || !shouldUseExistingSession) && isBrowser) {
|
|
71
|
+
localStorage.setItem('human_behavior_session_id', this.sessionId);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Start initialization immediately
|
|
75
|
+
this.initializationPromise = this.init();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private async init(): Promise<void> {
|
|
79
|
+
try {
|
|
80
|
+
const userId = this.getCookie(`human_behavior_end_user_id_${this.apiKey}`);
|
|
81
|
+
const { sessionId, endUserId } = await this.api.init(this.sessionId, userId);
|
|
82
|
+
this.sessionId = sessionId;
|
|
83
|
+
this.endUserId = endUserId;
|
|
84
|
+
this.setCookie(`human_behavior_end_user_id_${this.apiKey}`, endUserId, 365);
|
|
85
|
+
|
|
86
|
+
// Only setup browser-specific handlers when in browser environment
|
|
87
|
+
if (isBrowser) {
|
|
88
|
+
this.setupPageUnloadHandler();
|
|
89
|
+
this.start();
|
|
90
|
+
this.processRejectedEvents();
|
|
91
|
+
} else {
|
|
92
|
+
console.warn('HumanBehaviorTracker initialized in a non-browser environment. Session tracking is disabled.');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.initialized = true;
|
|
96
|
+
console.log('HumanBehaviorTracker initialized');
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('Failed to initialize HumanBehaviorTracker:', error);
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async ensureInitialized(): Promise<void> {
|
|
104
|
+
if (!this.initializationPromise) {
|
|
105
|
+
throw new Error('HumanBehaviorTracker initialization failed');
|
|
106
|
+
}
|
|
107
|
+
await this.initializationPromise;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public static logToStorage(message: string) {
|
|
111
|
+
try {
|
|
112
|
+
const logs = JSON.parse(localStorage.getItem('human_behavior_logs') || '[]');
|
|
113
|
+
logs.push(`${new Date().toISOString()}: ${message}`);
|
|
114
|
+
localStorage.setItem('human_behavior_logs', JSON.stringify(logs));
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.error('Failed to log to storage:', e);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private setupPageUnloadHandler() {
|
|
121
|
+
if (!isBrowser) return;
|
|
122
|
+
|
|
123
|
+
console.log('Setting up page unload handler');
|
|
124
|
+
|
|
125
|
+
// Handle visibility changes for sending events
|
|
126
|
+
window.addEventListener('visibilitychange', () => {
|
|
127
|
+
// Only send events when page becomes hidden
|
|
128
|
+
if (document.visibilityState === 'hidden') {
|
|
129
|
+
console.log('Page hidden - sending pending events');
|
|
130
|
+
this.api.sendBeaconEvents(this.eventIngestionQueue, this.sessionId);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Handle actual page unload/close
|
|
135
|
+
window.addEventListener('beforeunload', () => {
|
|
136
|
+
// Update last activity time
|
|
137
|
+
localStorage.setItem('human_behavior_last_activity', Date.now().toString());
|
|
138
|
+
|
|
139
|
+
// Send final events
|
|
140
|
+
this.api.sendBeaconEvents(this.eventIngestionQueue, this.sessionId);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Update activity timestamp periodically
|
|
144
|
+
setInterval(() => {
|
|
145
|
+
localStorage.setItem('human_behavior_last_activity', Date.now().toString());
|
|
146
|
+
}, 60000); // Update every minute
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public viewLogs() {
|
|
150
|
+
try {
|
|
151
|
+
const logs = JSON.parse(localStorage.getItem('human_behavior_logs') || '[]');
|
|
152
|
+
console.log('HumanBehavior Logs:', logs);
|
|
153
|
+
localStorage.removeItem('human_behavior_logs'); // Clear logs after viewing
|
|
154
|
+
} catch (e) {
|
|
155
|
+
console.error('Failed to read logs:', e);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
public async addUserInfo(userProperties: Record<string, any>) {
|
|
160
|
+
await this.ensureInitialized();
|
|
161
|
+
if (!this.endUserId) {
|
|
162
|
+
throw new Error('Cannot add user info before tracker initialization');
|
|
163
|
+
}
|
|
164
|
+
this.userProperties = userProperties;
|
|
165
|
+
await this.api.sendUserData(this.endUserId, userProperties, this.sessionId);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Authenticate user using existing userInfo data
|
|
170
|
+
* @param authFields Array of field names to check for existing users (e.g., ['email', 'phoneNumber'])
|
|
171
|
+
*/
|
|
172
|
+
public async auth(authFields: string[]) {
|
|
173
|
+
await this.ensureInitialized();
|
|
174
|
+
if (!this.endUserId) {
|
|
175
|
+
throw new Error('Cannot authenticate before tracker initialization');
|
|
176
|
+
}
|
|
177
|
+
if (!this.userProperties || Object.keys(this.userProperties).length === 0) {
|
|
178
|
+
throw new Error('No user info available. Call addUserInfo() first.');
|
|
179
|
+
}
|
|
180
|
+
await this.api.sendUserAuth(this.endUserId, this.userProperties, this.sessionId, authFields);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public async customEvent(eventName: string, eventProperties: Record<string, any> = {}) {
|
|
184
|
+
await this.ensureInitialized();
|
|
185
|
+
this.api.sendBeaconCustomEvent(eventName, eventProperties, this.sessionId);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public async start() {
|
|
189
|
+
await this.ensureInitialized();
|
|
190
|
+
if (!isBrowser) return;
|
|
191
|
+
|
|
192
|
+
// Start periodic flushing
|
|
193
|
+
this.flushInterval = window.setInterval(() => {
|
|
194
|
+
this.flush();
|
|
195
|
+
}, this.FLUSH_INTERVAL_MS);
|
|
196
|
+
|
|
197
|
+
// Start recording with redaction enabled
|
|
198
|
+
rrweb.record({
|
|
199
|
+
emit: (event) => {
|
|
200
|
+
this.addEvent(event);
|
|
201
|
+
},
|
|
202
|
+
inlineStylesheet: true,
|
|
203
|
+
recordCanvas: true,
|
|
204
|
+
collectFonts: true,
|
|
205
|
+
blockClass: 'rr-block',
|
|
206
|
+
ignoreClass: 'rr-ignore',
|
|
207
|
+
maskTextClass: 'rr-ignore'
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
public async stop() {
|
|
212
|
+
await this.ensureInitialized();
|
|
213
|
+
if (!isBrowser) return;
|
|
214
|
+
|
|
215
|
+
if (this.flushInterval) {
|
|
216
|
+
clearInterval(this.flushInterval);
|
|
217
|
+
this.flushInterval = null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
public async addEvent(event: any) {
|
|
222
|
+
await this.ensureInitialized();
|
|
223
|
+
|
|
224
|
+
// Process event through redaction manager if active
|
|
225
|
+
const processedEvent = this.redactionManager.processEvent(event);
|
|
226
|
+
|
|
227
|
+
const eventSize = new TextEncoder().encode(JSON.stringify(processedEvent)).length;
|
|
228
|
+
this.eventIngestionQueue.push(processedEvent);
|
|
229
|
+
this.queueSizeBytes += eventSize;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private async processRejectedEvents() {
|
|
233
|
+
if (this.isProcessingRejectedEvents || this.rejectedEvents.length === 0) return;
|
|
234
|
+
|
|
235
|
+
this.isProcessingRejectedEvents = true;
|
|
236
|
+
try {
|
|
237
|
+
// Create a new session ID for rejected events
|
|
238
|
+
const newSessionId = uuidv1();
|
|
239
|
+
if (isBrowser) {
|
|
240
|
+
localStorage.setItem('human_behavior_session_id', newSessionId);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Try to send rejected events with new session ID using beacon
|
|
244
|
+
// sendBeacon returns true if the request was queued successfully
|
|
245
|
+
this.api.sendBeaconEvents(this.rejectedEvents, newSessionId);
|
|
246
|
+
|
|
247
|
+
// Clear rejected events and update session ID
|
|
248
|
+
// Note: We can't verify if the beacon data was actually sent,
|
|
249
|
+
// but we clear the events to prevent duplicate sending attempts
|
|
250
|
+
this.rejectedEvents = [];
|
|
251
|
+
this.sessionId = newSessionId;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error('Failed to process rejected events:', error);
|
|
254
|
+
} finally {
|
|
255
|
+
this.isProcessingRejectedEvents = false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private async flush() {
|
|
260
|
+
// Prevent concurrent flushes
|
|
261
|
+
if (this.isProcessing || !this.initialized) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
this.isProcessing = true;
|
|
266
|
+
try {
|
|
267
|
+
// Swap the current queue with an empty one atomically
|
|
268
|
+
const eventsToProcess = this.eventIngestionQueue;
|
|
269
|
+
this.eventIngestionQueue = [];
|
|
270
|
+
this.queueSizeBytes = 0;
|
|
271
|
+
|
|
272
|
+
if (eventsToProcess.length > 0) {
|
|
273
|
+
console.log('Flushing events:', eventsToProcess);
|
|
274
|
+
try {
|
|
275
|
+
await this.api.sendEvents(eventsToProcess, this.sessionId, this.endUserId!);
|
|
276
|
+
} catch (error: any) {
|
|
277
|
+
// If we get a 400 error, store events for retry
|
|
278
|
+
if (error.message?.includes('ERROR: Session already completed')) {
|
|
279
|
+
console.log('Session expired, storing events for retry');
|
|
280
|
+
this.rejectedEvents.push(...eventsToProcess);
|
|
281
|
+
this.processRejectedEvents();
|
|
282
|
+
} else {
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} finally {
|
|
288
|
+
this.isProcessing = false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Add helper methods for cookie management
|
|
293
|
+
private setCookie(name: string, value: string, daysToExpire: number) {
|
|
294
|
+
if (!isBrowser) return;
|
|
295
|
+
const date = new Date();
|
|
296
|
+
date.setTime(date.getTime() + (daysToExpire * 24 * 60 * 60 * 1000));
|
|
297
|
+
const expires = `expires=${date.toUTCString()}`;
|
|
298
|
+
document.cookie = `${name}=${value};${expires};path=/;SameSite=Lax`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private getCookie(name: string): string | null {
|
|
302
|
+
if (!isBrowser) return null;
|
|
303
|
+
const nameEQ = name + "=";
|
|
304
|
+
const ca = document.cookie.split(';');
|
|
305
|
+
for (let i = 0; i < ca.length; i++) {
|
|
306
|
+
let c = ca[i];
|
|
307
|
+
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
|
|
308
|
+
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Start redaction functionality for sensitive input fields
|
|
315
|
+
* @param options Optional configuration for redaction behavior
|
|
316
|
+
*/
|
|
317
|
+
public async redact(options?: RedactionOptions): Promise<void> {
|
|
318
|
+
await this.ensureInitialized();
|
|
319
|
+
if (!isBrowser) {
|
|
320
|
+
console.warn('Redaction is only available in browser environments');
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Create a new redaction manager with the provided options
|
|
325
|
+
this.redactionManager = new RedactionManager(options);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Set specific fields to be redacted during session recording
|
|
330
|
+
* @param fields Array of CSS selectors for fields to redact (e.g., ['input[type="password"]', '#email-field'])
|
|
331
|
+
*/
|
|
332
|
+
public setRedactedFields(fields: string[]): void {
|
|
333
|
+
if (!isBrowser) {
|
|
334
|
+
console.warn('Redaction is only available in browser environments');
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
this.redactionManager.setFieldsToRedact(fields);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Check if redaction is currently active
|
|
343
|
+
*/
|
|
344
|
+
public isRedactionActive(): boolean {
|
|
345
|
+
return this.redactionManager.isActive();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get the currently selected fields for redaction
|
|
350
|
+
*/
|
|
351
|
+
public getRedactedFields(): string[] {
|
|
352
|
+
return this.redactionManager.getSelectedFields();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Only expose to window object in browser environments
|
|
357
|
+
if (isBrowser) {
|
|
358
|
+
window.HumanBehaviorTracker = HumanBehaviorTracker;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export default HumanBehaviorTracker;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"outDir": "./dist/",
|
|
4
|
+
"sourceMap": true,
|
|
5
|
+
"noImplicitAny": true,
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"target": "es2015",
|
|
8
|
+
"downlevelIteration": true,
|
|
9
|
+
"jsx": "react",
|
|
10
|
+
"allowJs": true,
|
|
11
|
+
"moduleResolution": "node",
|
|
12
|
+
"allowSyntheticDefaultImports": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"esModuleInterop": true,
|
|
16
|
+
"strict": true,
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"forceConsistentCasingInFileNames": true,
|
|
19
|
+
"lib": ["dom", "dom.iterable", "esnext"]
|
|
20
|
+
},
|
|
21
|
+
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
|
22
|
+
|
|
23
|
+
"exclude": ["node_modules", "dist", "test-website"]
|
|
24
|
+
}
|
package/index.d.ts
DELETED
package/index.js
DELETED
package/react.d.ts
DELETED
package/react.js
DELETED