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/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
+ };