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/api.ts
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
export const MAX_CHUNK_SIZE_BYTES = 1024 * 1024 * 10; // 10MB chunk size
|
|
2
|
+
|
|
3
|
+
export function isChunkSizeExceeded(currentChunk: any[], newEvent: any, sessionId: string): boolean {
|
|
4
|
+
const nextChunkSize = new TextEncoder().encode(JSON.stringify({
|
|
5
|
+
sessionId,
|
|
6
|
+
events: [...currentChunk, newEvent]
|
|
7
|
+
})).length;
|
|
8
|
+
|
|
9
|
+
return nextChunkSize > MAX_CHUNK_SIZE_BYTES;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function validateSingleEventSize(event: any, sessionId: string): void {
|
|
13
|
+
const singleEventSize = new TextEncoder().encode(JSON.stringify({
|
|
14
|
+
sessionId,
|
|
15
|
+
events: [event]
|
|
16
|
+
})).length;
|
|
17
|
+
|
|
18
|
+
if (singleEventSize > MAX_CHUNK_SIZE_BYTES) {
|
|
19
|
+
throw new Error(`Single event size (${singleEventSize} bytes) exceeds maximum chunk size (${MAX_CHUNK_SIZE_BYTES} bytes)`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class HumanBehaviorAPI {
|
|
24
|
+
private apiKey: string;
|
|
25
|
+
private baseUrl: string;
|
|
26
|
+
|
|
27
|
+
constructor({ apiKey, ingestionUrl }: { apiKey: string, ingestionUrl: string }) {
|
|
28
|
+
this.apiKey = apiKey;
|
|
29
|
+
this.baseUrl = ingestionUrl;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public async init(sessionId: string, userId: string | null) {
|
|
33
|
+
// Get current page URL and referrer if in browser environment
|
|
34
|
+
let entryURL = null;
|
|
35
|
+
let referrer = null;
|
|
36
|
+
|
|
37
|
+
if (typeof window !== 'undefined') {
|
|
38
|
+
entryURL = window.location.href;
|
|
39
|
+
referrer = document.referrer;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const response = await fetch(`${this.baseUrl}/api/ingestion/init`, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
47
|
+
'Referer': referrer || ''
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
sessionId: sessionId,
|
|
51
|
+
endUserId: userId,
|
|
52
|
+
entryURL: entryURL,
|
|
53
|
+
referrer: referrer
|
|
54
|
+
})
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
throw new Error(`Failed to initialize ingestion: ${response.statusText}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const responseJson = await response.json();
|
|
62
|
+
return {
|
|
63
|
+
sessionId: responseJson.sessionId,
|
|
64
|
+
endUserId: responseJson.endUserId
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async sendEvents(events: any[], sessionId: string, userId: string) {
|
|
69
|
+
const response = await fetch(`${this.baseUrl}/api/ingestion/events`, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: {
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify({
|
|
76
|
+
sessionId,
|
|
77
|
+
events: events,
|
|
78
|
+
endUserId: userId
|
|
79
|
+
})
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw new Error(`Failed to send events: ${response.statusText}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async sendEventsChunked(events: any[], sessionId: string) {
|
|
88
|
+
try {
|
|
89
|
+
const results = [];
|
|
90
|
+
let currentChunk: any[] = [];
|
|
91
|
+
|
|
92
|
+
for (const event of events) {
|
|
93
|
+
if (isChunkSizeExceeded(currentChunk, event, sessionId)) {
|
|
94
|
+
// If current chunk is not empty, send it first
|
|
95
|
+
if (currentChunk.length > 0) {
|
|
96
|
+
const response = await fetch(`${this.baseUrl}/api/ingestion/events`, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: {
|
|
99
|
+
'Content-Type': 'application/json',
|
|
100
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
101
|
+
},
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
sessionId,
|
|
104
|
+
events: currentChunk
|
|
105
|
+
})
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
throw new Error(`Failed to send events: ${response.statusText}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
results.push(await response.json());
|
|
113
|
+
currentChunk = [];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Validate single event size
|
|
117
|
+
validateSingleEventSize(event, sessionId);
|
|
118
|
+
|
|
119
|
+
// Start new chunk with this event
|
|
120
|
+
currentChunk = [event];
|
|
121
|
+
} else {
|
|
122
|
+
// Add event to current chunk
|
|
123
|
+
currentChunk.push(event);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Send any remaining events
|
|
128
|
+
if (currentChunk.length > 0) {
|
|
129
|
+
const response = await fetch(`${this.baseUrl}/api/ingestion/events`, {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: {
|
|
132
|
+
'Content-Type': 'application/json',
|
|
133
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
134
|
+
},
|
|
135
|
+
body: JSON.stringify({
|
|
136
|
+
sessionId,
|
|
137
|
+
events: currentChunk
|
|
138
|
+
})
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
throw new Error(`Failed to send events: ${response.statusText}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
results.push(await response.json());
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return results.flat();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error('Error sending events:', error);
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async sendUserData(userId: string, userData: Record<string, any>, sessionId: string) {
|
|
156
|
+
try {
|
|
157
|
+
const response = await fetch(`${this.baseUrl}/api/ingestion/user`, {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: {
|
|
160
|
+
'Content-Type': 'application/json',
|
|
161
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify({
|
|
164
|
+
userId: userId,
|
|
165
|
+
userAttributes: userData,
|
|
166
|
+
sessionId: sessionId
|
|
167
|
+
})
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
throw new Error(`Failed to send user data: ${response.statusText} with API key: ${this.apiKey}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return await response.json();
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error('Error sending user data:', error);
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async sendUserAuth(userId: string, userData: Record<string, any>, sessionId: string, authFields: string[]) {
|
|
182
|
+
try {
|
|
183
|
+
const response = await fetch(`${this.baseUrl}/api/ingestion/user/auth`, {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
headers: {
|
|
186
|
+
'Content-Type': 'application/json',
|
|
187
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
188
|
+
},
|
|
189
|
+
body: JSON.stringify({
|
|
190
|
+
userId: userId,
|
|
191
|
+
userAttributes: userData,
|
|
192
|
+
sessionId: sessionId,
|
|
193
|
+
authFields: authFields
|
|
194
|
+
})
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (!response.ok) {
|
|
198
|
+
throw new Error(`Failed to authenticate user: ${response.statusText} with API key: ${this.apiKey}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return await response.json();
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error('Error authenticating user:', error);
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
297
|
+
public sendBeaconEvents(events: any[], sessionId: string, isSessionComplete: boolean = false) {
|
|
298
|
+
const data = new URLSearchParams()
|
|
299
|
+
data.append('events', encodeURIComponent(JSON.stringify(events)))
|
|
300
|
+
data.append('sessionId', encodeURIComponent(sessionId))
|
|
301
|
+
data.append('timestamp', encodeURIComponent(Date.now().toString()))
|
|
302
|
+
data.append('apiKey', encodeURIComponent(this.apiKey))
|
|
303
|
+
if (isSessionComplete) {
|
|
304
|
+
console.log('Session complete beacon sending');
|
|
305
|
+
localStorage.setItem('koalaware_session_complete', Date.now().toString());
|
|
306
|
+
data.append('sessionComplete', encodeURIComponent('true'))
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const success = navigator.sendBeacon(
|
|
310
|
+
`${this.baseUrl}/api/ingestion/events`,
|
|
311
|
+
data
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// KoalawareTracker.logToStorage(`Sending events beacon: ${this.baseUrl}/api/ingestion/events`);
|
|
315
|
+
// KoalawareTracker.logToStorage(`Events beacon success: ${success}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
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
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main entry point for the HumanBehavior SDK
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { HumanBehaviorTracker } from './tracker';
|
|
6
|
+
|
|
7
|
+
// Export everything from the tracker module
|
|
8
|
+
export * from './tracker';
|
|
9
|
+
|
|
10
|
+
// Export everything from the API module
|
|
11
|
+
export * from './api';
|
|
12
|
+
|
|
13
|
+
// Export redaction functionality
|
|
14
|
+
export * from './redact';
|
|
15
|
+
|
|
16
|
+
// Also export the tracker as the default export
|
|
17
|
+
export { HumanBehaviorTracker as default } from './tracker';
|
|
18
|
+
|
|
19
|
+
// For UMD builds, expose the main class globally
|
|
20
|
+
if (typeof window !== 'undefined') {
|
|
21
|
+
(window as any).HumanBehaviorTracker = HumanBehaviorTracker;
|
|
22
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import React, { useEffect, useState, createContext, useContext, ReactNode } from "react";
|
|
2
|
+
import { HumanBehaviorTracker } from "..";
|
|
3
|
+
|
|
4
|
+
// Check if we're in a browser environment
|
|
5
|
+
const isBrowser = () => typeof window !== 'undefined';
|
|
6
|
+
|
|
7
|
+
// Define the public interface that components will interact with
|
|
8
|
+
interface HumanBehaviorInterface {
|
|
9
|
+
addEvent: (event: any) => void;
|
|
10
|
+
addUserInfo: (userProperties: Record<string, any>) => Promise<void>;
|
|
11
|
+
start: () => void;
|
|
12
|
+
stop: () => void;
|
|
13
|
+
viewLogs: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface HumanBehaviorContextType {
|
|
17
|
+
humanBehavior: HumanBehaviorTracker | null;
|
|
18
|
+
queueEvent: (event: any) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface HumanBehaviorProviderProps {
|
|
22
|
+
// Either provide an apiKey to create a new client, or provide an existing client
|
|
23
|
+
apiKey?: string;
|
|
24
|
+
client?: HumanBehaviorTracker;
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const defaultContext: HumanBehaviorContextType = {
|
|
29
|
+
humanBehavior: null,
|
|
30
|
+
queueEvent: (event: any) => {
|
|
31
|
+
// In server-side, just no-op
|
|
32
|
+
if (typeof window === 'undefined') {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
console.warn('HumanBehavior not initialized yet, event queued:', event);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const HumanBehaviorContext = createContext<HumanBehaviorContextType>(defaultContext);
|
|
40
|
+
|
|
41
|
+
export const HumanBehaviorProvider = ({ apiKey, client, children }: HumanBehaviorProviderProps) => {
|
|
42
|
+
const [humanBehavior, setHumanBehavior] = useState<HumanBehaviorTracker | null>(client || null);
|
|
43
|
+
const [eventQueue, setEventQueue] = useState<any[]>([]);
|
|
44
|
+
const [isMounted, setIsMounted] = useState(false);
|
|
45
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
46
|
+
|
|
47
|
+
// Function to queue events before initialization
|
|
48
|
+
const queueEvent = (event: any) => {
|
|
49
|
+
setEventQueue(prev => [...prev, event]);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Handle mounting state
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
setIsMounted(true);
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
// Only run in browser
|
|
59
|
+
if (!(isBrowser())) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Skip if not mounted yet (handles Next.js hydration)
|
|
64
|
+
if (!isMounted) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// If client is provided, use that
|
|
69
|
+
if (client) {
|
|
70
|
+
setHumanBehavior(client);
|
|
71
|
+
setIsInitialized(true);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// If no client is provided, apiKey is required
|
|
76
|
+
if (!apiKey || apiKey.trim() === '') {
|
|
77
|
+
console.error("An apiKey is required when no client is provided");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (humanBehavior !== null) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Create new tracker instance with the validated apiKey
|
|
86
|
+
const tracker = new HumanBehaviorTracker(apiKey.trim(), process.env.NEXT_PUBLIC_INGESTION_URL || 'https://ingestion.humanbehavior.ai');
|
|
87
|
+
setHumanBehavior(tracker);
|
|
88
|
+
|
|
89
|
+
// Wait for initialization to complete
|
|
90
|
+
tracker.initializationPromise?.then(() => {
|
|
91
|
+
setIsInitialized(true);
|
|
92
|
+
|
|
93
|
+
// Process any queued events
|
|
94
|
+
if (eventQueue.length > 0) {
|
|
95
|
+
eventQueue.forEach(event => {
|
|
96
|
+
if (event.type === 'identify') {
|
|
97
|
+
console.log('Processing queued identify event', event.userProperties);
|
|
98
|
+
tracker.addUserInfo(event.userProperties);
|
|
99
|
+
} else {
|
|
100
|
+
tracker.addEvent(event);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
setEventQueue([]); // Clear the queue
|
|
104
|
+
}
|
|
105
|
+
}).catch(error => {
|
|
106
|
+
console.error('Failed to initialize HumanBehaviorTracker:', error);
|
|
107
|
+
});
|
|
108
|
+
}, [apiKey, client, eventQueue, isMounted]);
|
|
109
|
+
|
|
110
|
+
// If not in browser, render children without context
|
|
111
|
+
if (!(isBrowser())) {
|
|
112
|
+
return <>{children}</>;
|
|
113
|
+
}
|
|
114
|
+
|
|
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
|
+
return (
|
|
134
|
+
<HumanBehaviorContext.Provider value={{ humanBehavior, queueEvent }}>
|
|
135
|
+
{children}
|
|
136
|
+
</HumanBehaviorContext.Provider>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const useHumanBehavior = (): HumanBehaviorInterface => {
|
|
141
|
+
const context = useContext(HumanBehaviorContext);
|
|
142
|
+
|
|
143
|
+
// Only throw if the hook is used outside of a provider entirely
|
|
144
|
+
if (context === null) {
|
|
145
|
+
throw new Error("useHumanBehavior must be used within a HumanBehaviorProvider");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// If we're in the server-side, return a no-op implementation
|
|
149
|
+
if (typeof window === 'undefined') {
|
|
150
|
+
console.warn('HumanBehavior is being used before being initialized');
|
|
151
|
+
return {
|
|
152
|
+
addEvent: () => {},
|
|
153
|
+
addUserInfo: async () => {},
|
|
154
|
+
start: () => {},
|
|
155
|
+
stop: () => {},
|
|
156
|
+
viewLogs: () => {},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// If we have an initialized tracker, return it as the interface
|
|
161
|
+
if (context.humanBehavior) {
|
|
162
|
+
return context.humanBehavior;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// If we reach here, we're in the initialization period where:
|
|
166
|
+
// - context.humanBehavior is null
|
|
167
|
+
// - context.queueEvent is available
|
|
168
|
+
// - we need to queue all operations until initialization completes
|
|
169
|
+
return {
|
|
170
|
+
addEvent: (event: any) => {
|
|
171
|
+
context.queueEvent(event);
|
|
172
|
+
},
|
|
173
|
+
addUserInfo: async (userProperties: Record<string, any>) => {
|
|
174
|
+
context.queueEvent({
|
|
175
|
+
type: 'identify',
|
|
176
|
+
userProperties,
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
start: () => {
|
|
180
|
+
// Start will be called automatically when initialized
|
|
181
|
+
},
|
|
182
|
+
stop: () => {
|
|
183
|
+
// Stop is a no-op when not initialized
|
|
184
|
+
},
|
|
185
|
+
viewLogs: () => {
|
|
186
|
+
console.warn('Logs are not available until HumanBehaviorTracker is initialized');
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
};
|