humanbehavior-js 0.0.4 → 0.0.7
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 +897 -598
- 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 +896 -599
- 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 +113 -4
- package/dist/types/react/index.d.ts +1 -1
- package/package.json +3 -14
- package/rollup.config.js +106 -0
- package/src/api.ts +348 -0
- package/src/index.ts +22 -0
- package/src/react/index.tsx +189 -0
- package/src/redact.ts +474 -0
- package/src/tracker.ts +349 -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/dist/types/index.d.ts
CHANGED
|
@@ -1,3 +1,85 @@
|
|
|
1
|
+
interface RedactionOptions {
|
|
2
|
+
redactedText?: string;
|
|
3
|
+
excludeSelectors?: string[];
|
|
4
|
+
userFields?: string[];
|
|
5
|
+
}
|
|
6
|
+
declare class RedactionManager {
|
|
7
|
+
private redactedText;
|
|
8
|
+
private userSelectedFields;
|
|
9
|
+
private excludeSelectors;
|
|
10
|
+
constructor(options?: RedactionOptions);
|
|
11
|
+
/**
|
|
12
|
+
* Set specific fields to be redacted
|
|
13
|
+
* @param fields Array of CSS selectors for fields to redact
|
|
14
|
+
*/
|
|
15
|
+
setFieldsToRedact(fields: string[]): void;
|
|
16
|
+
/**
|
|
17
|
+
* Check if redaction is currently active (has fields selected)
|
|
18
|
+
*/
|
|
19
|
+
isActive(): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Get the currently selected fields for redaction
|
|
22
|
+
*/
|
|
23
|
+
getSelectedFields(): string[];
|
|
24
|
+
/**
|
|
25
|
+
* Process an event and redact sensitive data if needed
|
|
26
|
+
*/
|
|
27
|
+
processEvent(event: any): any;
|
|
28
|
+
/**
|
|
29
|
+
* Redact sensitive data in input events
|
|
30
|
+
*/
|
|
31
|
+
private redactInputEvent;
|
|
32
|
+
/**
|
|
33
|
+
* Redact sensitive data in DOM mutation events
|
|
34
|
+
*/
|
|
35
|
+
private redactDOMEvent;
|
|
36
|
+
/**
|
|
37
|
+
* Check if a DOM change should be redacted based on its ID
|
|
38
|
+
*/
|
|
39
|
+
private shouldRedactDOMChange;
|
|
40
|
+
/**
|
|
41
|
+
* Redact sensitive data in mouse/touch interaction events
|
|
42
|
+
*/
|
|
43
|
+
private redactMouseEvent;
|
|
44
|
+
/**
|
|
45
|
+
* Redact sensitive data in full snapshot events
|
|
46
|
+
*/
|
|
47
|
+
private redactFullSnapshot;
|
|
48
|
+
/**
|
|
49
|
+
* Recursively redact sensitive data in DOM nodes
|
|
50
|
+
*/
|
|
51
|
+
private redactNode;
|
|
52
|
+
/**
|
|
53
|
+
* Check if a node should be redacted based on its attributes
|
|
54
|
+
*/
|
|
55
|
+
private shouldRedactNode;
|
|
56
|
+
/**
|
|
57
|
+
* Check if a CSS selector would match a node based on its attributes
|
|
58
|
+
*/
|
|
59
|
+
private selectorMatchesNode;
|
|
60
|
+
/**
|
|
61
|
+
* Basic selector matching for environments where matches() is not available
|
|
62
|
+
*/
|
|
63
|
+
private basicSelectorMatch;
|
|
64
|
+
/**
|
|
65
|
+
* Check if an event is from a field that should be redacted
|
|
66
|
+
*/
|
|
67
|
+
private isFieldSelected;
|
|
68
|
+
/**
|
|
69
|
+
* Check if an element should be redacted based on user-selected fields
|
|
70
|
+
*/
|
|
71
|
+
private shouldRedactElement;
|
|
72
|
+
/**
|
|
73
|
+
* Get the original value of a redacted element (for debugging)
|
|
74
|
+
*/
|
|
75
|
+
getOriginalValue(element: HTMLElement): string | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Check if an element is currently being redacted
|
|
78
|
+
*/
|
|
79
|
+
isElementRedacted(element: HTMLElement): boolean;
|
|
80
|
+
}
|
|
81
|
+
declare const redactionManager: RedactionManager;
|
|
82
|
+
|
|
1
83
|
declare global {
|
|
2
84
|
interface Window {
|
|
3
85
|
HumanBehaviorTracker: typeof HumanBehaviorTracker;
|
|
@@ -18,13 +100,19 @@ declare class HumanBehaviorTracker {
|
|
|
18
100
|
private apiKey;
|
|
19
101
|
private initialized;
|
|
20
102
|
initializationPromise: Promise<void> | null;
|
|
21
|
-
|
|
103
|
+
private redactionManager;
|
|
104
|
+
constructor(apiKey: string | undefined, ingestionUrl: string | undefined);
|
|
22
105
|
private init;
|
|
23
106
|
private ensureInitialized;
|
|
24
107
|
static logToStorage(message: string): void;
|
|
25
108
|
private setupPageUnloadHandler;
|
|
26
109
|
viewLogs(): void;
|
|
27
|
-
|
|
110
|
+
addUserInfo(userProperties: Record<string, any>): Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* Authenticate user using existing userInfo data
|
|
113
|
+
* @param authFields Array of field names to check for existing users (e.g., ['email', 'phoneNumber'])
|
|
114
|
+
*/
|
|
115
|
+
auth(authFields: string[]): Promise<void>;
|
|
28
116
|
customEvent(eventName: string, eventProperties?: Record<string, any>): Promise<void>;
|
|
29
117
|
start(): Promise<void>;
|
|
30
118
|
stop(): Promise<void>;
|
|
@@ -33,6 +121,24 @@ declare class HumanBehaviorTracker {
|
|
|
33
121
|
private flush;
|
|
34
122
|
private setCookie;
|
|
35
123
|
private getCookie;
|
|
124
|
+
/**
|
|
125
|
+
* Start redaction functionality for sensitive input fields
|
|
126
|
+
* @param options Optional configuration for redaction behavior
|
|
127
|
+
*/
|
|
128
|
+
redact(options?: RedactionOptions): Promise<void>;
|
|
129
|
+
/**
|
|
130
|
+
* Set specific fields to be redacted during session recording
|
|
131
|
+
* @param fields Array of CSS selectors for fields to redact (e.g., ['input[type="password"]', '#email-field'])
|
|
132
|
+
*/
|
|
133
|
+
setRedactedFields(fields: string[]): void;
|
|
134
|
+
/**
|
|
135
|
+
* Check if redaction is currently active
|
|
136
|
+
*/
|
|
137
|
+
isRedactionActive(): boolean;
|
|
138
|
+
/**
|
|
139
|
+
* Get the currently selected fields for redaction
|
|
140
|
+
*/
|
|
141
|
+
getRedactedFields(): string[];
|
|
36
142
|
}
|
|
37
143
|
|
|
38
144
|
declare const MAX_CHUNK_SIZE_BYTES: number;
|
|
@@ -41,8 +147,9 @@ declare function validateSingleEventSize(event: any, sessionId: string): void;
|
|
|
41
147
|
declare class HumanBehaviorAPI {
|
|
42
148
|
private apiKey;
|
|
43
149
|
private baseUrl;
|
|
44
|
-
constructor({ apiKey }: {
|
|
150
|
+
constructor({ apiKey, ingestionUrl }: {
|
|
45
151
|
apiKey: string;
|
|
152
|
+
ingestionUrl: string;
|
|
46
153
|
});
|
|
47
154
|
init(sessionId: string, userId: string | null): Promise<{
|
|
48
155
|
sessionId: any;
|
|
@@ -51,6 +158,7 @@ declare class HumanBehaviorAPI {
|
|
|
51
158
|
sendEvents(events: any[], sessionId: string, userId: string): Promise<void>;
|
|
52
159
|
sendEventsChunked(events: any[], sessionId: string): Promise<any[]>;
|
|
53
160
|
sendUserData(userId: string, userData: Record<string, any>, sessionId: string): Promise<any>;
|
|
161
|
+
sendUserAuth(userId: string, userData: Record<string, any>, sessionId: string, authFields: string[]): Promise<any>;
|
|
54
162
|
sendSessionComplete(sessionId: string): Promise<void>;
|
|
55
163
|
sendCustomEvent(eventName: string, eventProperties: Record<string, any>, sessionId: string): Promise<any>;
|
|
56
164
|
sendCustomEvents(events: any[], sessionId: string): Promise<any>;
|
|
@@ -60,4 +168,5 @@ declare class HumanBehaviorAPI {
|
|
|
60
168
|
sendBeaconCustomEvents(events: any[], sessionId: string): boolean;
|
|
61
169
|
}
|
|
62
170
|
|
|
63
|
-
export { HumanBehaviorAPI, HumanBehaviorTracker, MAX_CHUNK_SIZE_BYTES, HumanBehaviorTracker as default, isChunkSizeExceeded, validateSingleEventSize };
|
|
171
|
+
export { HumanBehaviorAPI, HumanBehaviorTracker, MAX_CHUNK_SIZE_BYTES, RedactionManager, HumanBehaviorTracker as default, isChunkSizeExceeded, redactionManager, validateSingleEventSize };
|
|
172
|
+
export type { RedactionOptions };
|
|
@@ -3,7 +3,7 @@ import { HumanBehaviorTracker } from '..';
|
|
|
3
3
|
|
|
4
4
|
interface HumanBehaviorInterface {
|
|
5
5
|
addEvent: (event: any) => void;
|
|
6
|
-
|
|
6
|
+
addUserInfo: (userProperties: Record<string, any>) => Promise<void>;
|
|
7
7
|
start: () => void;
|
|
8
8
|
stop: () => void;
|
|
9
9
|
viewLogs: () => void;
|
package/package.json
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "humanbehavior-js",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "SDK for HumanBehavior session and event recording",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
7
7
|
"module": "./dist/esm/index.js",
|
|
8
8
|
"types": "./dist/types/index.d.ts",
|
|
9
9
|
"unpkg": "dist/index.min.js",
|
|
10
|
-
"files": [
|
|
11
|
-
"dist",
|
|
12
|
-
"react.js",
|
|
13
|
-
"react.d.ts",
|
|
14
|
-
"index.js",
|
|
15
|
-
"index.d.ts"
|
|
16
|
-
],
|
|
17
10
|
"exports": {
|
|
18
11
|
".": {
|
|
19
12
|
"import": "./dist/esm/index.js",
|
|
@@ -45,15 +38,11 @@
|
|
|
45
38
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
46
39
|
"@rollup/plugin-terser": "^0.4.4",
|
|
47
40
|
"@rollup/plugin-typescript": "^12.1.2",
|
|
48
|
-
"@types/
|
|
49
|
-
"@types/express": "^5.0.1",
|
|
41
|
+
"@types/node": "^24.0.3",
|
|
50
42
|
"rollup": "^4.36.0",
|
|
51
43
|
"rollup-plugin-dts": "^6.2.1",
|
|
52
|
-
"ts-loader": "^9.5.2",
|
|
53
44
|
"tslib": "^2.8.1",
|
|
54
|
-
"typescript": "^5.8.2"
|
|
55
|
-
"webpack": "^5.98.0",
|
|
56
|
-
"webpack-cli": "^6.0.1"
|
|
45
|
+
"typescript": "^5.8.2"
|
|
57
46
|
},
|
|
58
47
|
"publishConfig": {
|
|
59
48
|
"access": "public"
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import typescript from '@rollup/plugin-typescript';
|
|
2
|
+
import resolve from '@rollup/plugin-node-resolve';
|
|
3
|
+
import commonjs from '@rollup/plugin-commonjs';
|
|
4
|
+
import terser from '@rollup/plugin-terser';
|
|
5
|
+
import dts from 'rollup-plugin-dts';
|
|
6
|
+
|
|
7
|
+
// Only React should be external since it's typically provided by the host application
|
|
8
|
+
const external = ['react', 'react-dom'];
|
|
9
|
+
|
|
10
|
+
// Global variables for UMD build
|
|
11
|
+
const globals = {
|
|
12
|
+
react: 'React',
|
|
13
|
+
'react-dom': 'ReactDOM'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default [
|
|
17
|
+
// Main SDK bundle
|
|
18
|
+
{
|
|
19
|
+
input: 'src/index.ts',
|
|
20
|
+
output: [
|
|
21
|
+
{
|
|
22
|
+
file: 'dist/cjs/index.js',
|
|
23
|
+
format: 'cjs',
|
|
24
|
+
name: 'HumanBehaviorTracker',
|
|
25
|
+
globals,
|
|
26
|
+
exports: 'named',
|
|
27
|
+
sourcemap: true
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
file: 'dist/esm/index.js',
|
|
31
|
+
format: 'es',
|
|
32
|
+
sourcemap: true
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
file: 'dist/index.min.js',
|
|
36
|
+
format: 'umd',
|
|
37
|
+
name: 'HumanBehaviorTracker',
|
|
38
|
+
globals,
|
|
39
|
+
exports: 'auto',
|
|
40
|
+
sourcemap: true,
|
|
41
|
+
plugins: [terser()]
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
plugins: [
|
|
45
|
+
resolve(),
|
|
46
|
+
commonjs(),
|
|
47
|
+
typescript({
|
|
48
|
+
tsconfig: './tsconfig.json',
|
|
49
|
+
declaration: false
|
|
50
|
+
})
|
|
51
|
+
],
|
|
52
|
+
external
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// React component bundle
|
|
56
|
+
{
|
|
57
|
+
input: './src/react/index.tsx',
|
|
58
|
+
output: [
|
|
59
|
+
{
|
|
60
|
+
file: 'dist/cjs/react/index.js',
|
|
61
|
+
format: 'cjs',
|
|
62
|
+
name: 'HumanBehaviorReact',
|
|
63
|
+
globals: {
|
|
64
|
+
...globals,
|
|
65
|
+
'..': 'HumanBehaviorTracker'
|
|
66
|
+
},
|
|
67
|
+
exports: 'named',
|
|
68
|
+
sourcemap: true
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
file: 'dist/esm/react/index.js',
|
|
72
|
+
format: 'es',
|
|
73
|
+
sourcemap: true
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
plugins: [
|
|
77
|
+
resolve(),
|
|
78
|
+
commonjs(),
|
|
79
|
+
typescript({
|
|
80
|
+
tsconfig: './tsconfig.json',
|
|
81
|
+
declaration: false
|
|
82
|
+
})
|
|
83
|
+
],
|
|
84
|
+
external: [...external, '..']
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
// Type definition bundles - generate these separately
|
|
88
|
+
{
|
|
89
|
+
input: 'src/index.ts',
|
|
90
|
+
output: {
|
|
91
|
+
file: 'dist/types/index.d.ts',
|
|
92
|
+
format: 'es'
|
|
93
|
+
},
|
|
94
|
+
plugins: [dts()],
|
|
95
|
+
external
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
input: 'src/react/index.tsx',
|
|
99
|
+
output: {
|
|
100
|
+
file: 'dist/types/react/index.d.ts',
|
|
101
|
+
format: 'es'
|
|
102
|
+
},
|
|
103
|
+
plugins: [dts()],
|
|
104
|
+
external: [...external, '..']
|
|
105
|
+
}
|
|
106
|
+
];
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
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
|
+
const response = await fetch(`${this.baseUrl}/api/ingestion/init`, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: {
|
|
36
|
+
'Content-Type': 'application/json',
|
|
37
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
sessionId: sessionId,
|
|
41
|
+
endUserId: userId
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new Error(`Failed to initialize ingestion: ${response.statusText}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const responseJson = await response.json();
|
|
50
|
+
return {
|
|
51
|
+
sessionId: responseJson.sessionId,
|
|
52
|
+
endUserId: responseJson.endUserId
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async sendEvents(events: any[], sessionId: string, userId: string) {
|
|
57
|
+
const response = await fetch(`${this.baseUrl}/api/ingestion/events`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: {
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
sessionId,
|
|
65
|
+
events: events,
|
|
66
|
+
endUserId: userId
|
|
67
|
+
})
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
throw new Error(`Failed to send events: ${response.statusText}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async sendEventsChunked(events: any[], sessionId: string) {
|
|
76
|
+
try {
|
|
77
|
+
const results = [];
|
|
78
|
+
let currentChunk: any[] = [];
|
|
79
|
+
|
|
80
|
+
for (const event of events) {
|
|
81
|
+
if (isChunkSizeExceeded(currentChunk, event, sessionId)) {
|
|
82
|
+
// If current chunk is not empty, send it first
|
|
83
|
+
if (currentChunk.length > 0) {
|
|
84
|
+
const response = await fetch(`${this.baseUrl}/api/ingestion/events`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: {
|
|
87
|
+
'Content-Type': 'application/json',
|
|
88
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify({
|
|
91
|
+
sessionId,
|
|
92
|
+
events: currentChunk
|
|
93
|
+
})
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
throw new Error(`Failed to send events: ${response.statusText}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
results.push(await response.json());
|
|
101
|
+
currentChunk = [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Validate single event size
|
|
105
|
+
validateSingleEventSize(event, sessionId);
|
|
106
|
+
|
|
107
|
+
// Start new chunk with this event
|
|
108
|
+
currentChunk = [event];
|
|
109
|
+
} else {
|
|
110
|
+
// Add event to current chunk
|
|
111
|
+
currentChunk.push(event);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Send any remaining events
|
|
116
|
+
if (currentChunk.length > 0) {
|
|
117
|
+
const response = await fetch(`${this.baseUrl}/api/ingestion/events`, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: {
|
|
120
|
+
'Content-Type': 'application/json',
|
|
121
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
sessionId,
|
|
125
|
+
events: currentChunk
|
|
126
|
+
})
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
throw new Error(`Failed to send events: ${response.statusText}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
results.push(await response.json());
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return results.flat();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('Error sending events:', error);
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async sendUserData(userId: string, userData: Record<string, any>, sessionId: string) {
|
|
144
|
+
try {
|
|
145
|
+
const response = await fetch(`${this.baseUrl}/api/ingestion/user`, {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
headers: {
|
|
148
|
+
'Content-Type': 'application/json',
|
|
149
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
150
|
+
},
|
|
151
|
+
body: JSON.stringify({
|
|
152
|
+
userId: userId,
|
|
153
|
+
userAttributes: userData,
|
|
154
|
+
sessionId: sessionId
|
|
155
|
+
})
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
throw new Error(`Failed to send user data: ${response.statusText} with API key: ${this.apiKey}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return await response.json();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('Error sending user data:', error);
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async sendUserAuth(userId: string, userData: Record<string, any>, sessionId: string, authFields: string[]) {
|
|
170
|
+
try {
|
|
171
|
+
const response = await fetch(`${this.baseUrl}/api/ingestion/user/auth`, {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: {
|
|
174
|
+
'Content-Type': 'application/json',
|
|
175
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
176
|
+
},
|
|
177
|
+
body: JSON.stringify({
|
|
178
|
+
userId: userId,
|
|
179
|
+
userAttributes: userData,
|
|
180
|
+
sessionId: sessionId,
|
|
181
|
+
authFields: authFields
|
|
182
|
+
})
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (!response.ok) {
|
|
186
|
+
throw new Error(`Failed to authenticate user: ${response.statusText} with API key: ${this.apiKey}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return await response.json();
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error('Error authenticating user:', error);
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async sendSessionComplete(sessionId: string) {
|
|
197
|
+
const response = await fetch(`${this.baseUrl}/api/ingestion/sessionComplete`, {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
headers: {
|
|
200
|
+
'Content-Type': 'application/json',
|
|
201
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
202
|
+
},
|
|
203
|
+
body: JSON.stringify({ sessionId })
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (!response.ok) {
|
|
207
|
+
throw new Error(`Failed to send session complete: ${response.statusText}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async sendCustomEvent(eventName: string, eventProperties: Record<string, any>, sessionId: string) {
|
|
212
|
+
const maxRetries = 3;
|
|
213
|
+
let retryCount = 0;
|
|
214
|
+
|
|
215
|
+
while (retryCount < maxRetries) {
|
|
216
|
+
try {
|
|
217
|
+
const response = await fetch(`${this.baseUrl}/api/ingestion/customEvent`, {
|
|
218
|
+
method: 'POST',
|
|
219
|
+
headers: {
|
|
220
|
+
'Content-Type': 'application/json',
|
|
221
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
222
|
+
},
|
|
223
|
+
body: JSON.stringify({
|
|
224
|
+
name: eventName,
|
|
225
|
+
properties: eventProperties,
|
|
226
|
+
sessionId: sessionId,
|
|
227
|
+
timestamp: new Date().toISOString()
|
|
228
|
+
})
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (!response.ok) {
|
|
232
|
+
throw new Error(`Failed to send custom event: ${response.statusText}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return await response.json();
|
|
236
|
+
} catch (error) {
|
|
237
|
+
retryCount++;
|
|
238
|
+
if (retryCount === maxRetries) {
|
|
239
|
+
console.error('Error sending custom event after max retries:', error);
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
// Exponential backoff
|
|
243
|
+
await new Promise(resolve => setTimeout(resolve, Math.pow(2, retryCount) * 1000));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async sendCustomEvents(events: any[], sessionId: string) {
|
|
249
|
+
const maxRetries = 3;
|
|
250
|
+
let retryCount = 0;
|
|
251
|
+
|
|
252
|
+
while (retryCount < maxRetries) {
|
|
253
|
+
try {
|
|
254
|
+
const response = await fetch(`${this.baseUrl}/api/ingestion/customEvent/batch`, {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
headers: {
|
|
257
|
+
'Content-Type': 'application/json',
|
|
258
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
259
|
+
},
|
|
260
|
+
body: JSON.stringify({
|
|
261
|
+
events: events.map(event => ({
|
|
262
|
+
...event,
|
|
263
|
+
sessionId: sessionId
|
|
264
|
+
}))
|
|
265
|
+
})
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
if (!response.ok) {
|
|
269
|
+
throw new Error(`Failed to send custom events: ${response.statusText}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return await response.json();
|
|
273
|
+
} catch (error) {
|
|
274
|
+
retryCount++;
|
|
275
|
+
if (retryCount === maxRetries) {
|
|
276
|
+
console.error('Error sending custom events after max retries:', error);
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
// Exponential backoff
|
|
280
|
+
await new Promise(resolve => setTimeout(resolve, Math.pow(2, retryCount) * 1000));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
public sendBeaconEvents(events: any[], sessionId: string, isSessionComplete: boolean = false) {
|
|
286
|
+
const data = new URLSearchParams()
|
|
287
|
+
data.append('events', encodeURIComponent(JSON.stringify(events)))
|
|
288
|
+
data.append('sessionId', encodeURIComponent(sessionId))
|
|
289
|
+
data.append('timestamp', encodeURIComponent(Date.now().toString()))
|
|
290
|
+
data.append('apiKey', encodeURIComponent(this.apiKey))
|
|
291
|
+
if (isSessionComplete) {
|
|
292
|
+
console.log('Session complete beacon sending');
|
|
293
|
+
localStorage.setItem('koalaware_session_complete', Date.now().toString());
|
|
294
|
+
data.append('sessionComplete', encodeURIComponent('true'))
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const success = navigator.sendBeacon(
|
|
298
|
+
`${this.baseUrl}/api/ingestion/events`,
|
|
299
|
+
data
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// KoalawareTracker.logToStorage(`Sending events beacon: ${this.baseUrl}/api/ingestion/events`);
|
|
303
|
+
// KoalawareTracker.logToStorage(`Events beacon success: ${success}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
public sendBeaconSessionComplete(sessionId: string) {
|
|
307
|
+
const data = new URLSearchParams()
|
|
308
|
+
data.append('sessionId', sessionId)
|
|
309
|
+
data.append('apiKey', this.apiKey)
|
|
310
|
+
data.append('sessionComplete', 'true')
|
|
311
|
+
|
|
312
|
+
const success = navigator.sendBeacon(
|
|
313
|
+
`${this.baseUrl}/api/ingestion/sessionComplete`,
|
|
314
|
+
data
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
// KoalawareTracker.logToStorage(`Sending completion beacon: ${this.baseUrl}/api/ingestion/sessionComplete`);
|
|
318
|
+
// KoalawareTracker.logToStorage(`Complete beacon success: ${success}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
public sendBeaconCustomEvent(eventName: string, eventProperties: Record<string, any>, sessionId: string) {
|
|
322
|
+
const data = new URLSearchParams()
|
|
323
|
+
data.append('name', encodeURIComponent(eventName))
|
|
324
|
+
data.append('properties', encodeURIComponent(JSON.stringify(eventProperties)))
|
|
325
|
+
data.append('sessionId', encodeURIComponent(sessionId))
|
|
326
|
+
data.append('timestamp', encodeURIComponent(new Date().toISOString()))
|
|
327
|
+
data.append('apiKey', encodeURIComponent(this.apiKey))
|
|
328
|
+
|
|
329
|
+
return navigator.sendBeacon(
|
|
330
|
+
`${this.baseUrl}/api/ingestion/customEvent`,
|
|
331
|
+
data
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
public sendBeaconCustomEvents(events: any[], sessionId: string) {
|
|
336
|
+
const data = new URLSearchParams()
|
|
337
|
+
data.append('events', encodeURIComponent(JSON.stringify(events.map(event => ({
|
|
338
|
+
...event,
|
|
339
|
+
sessionId: sessionId
|
|
340
|
+
})))))
|
|
341
|
+
data.append('apiKey', encodeURIComponent(this.apiKey))
|
|
342
|
+
|
|
343
|
+
return navigator.sendBeacon(
|
|
344
|
+
`${this.baseUrl}/api/ingestion/customEvent/batch`,
|
|
345
|
+
data
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
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
|
+
}
|