@prodact.ai/sdk 0.0.2 → 0.0.6
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/cdn/sdk.global.js +367 -0
- package/dist/cdn/sdk.global.js.map +1 -0
- package/dist/index.js +2153 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2136 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.js +2906 -409
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +2921 -425
- package/dist/react.mjs.map +1 -1
- package/package.json +11 -2
- package/dist/index.d.mts +0 -135
- package/dist/index.d.ts +0 -135
- package/dist/react.d.mts +0 -244
- package/dist/react.d.ts +0 -244
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,16 +17,1757 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
21
31
|
var src_exports = {};
|
|
22
32
|
__export(src_exports, {
|
|
33
|
+
ProactiveBehaviorTracker: () => ProactiveBehaviorTracker,
|
|
23
34
|
ProduckSDK: () => ProduckSDK,
|
|
24
|
-
|
|
35
|
+
SessionRecorder: () => SessionRecorder,
|
|
36
|
+
UserFlowTracker: () => UserFlowTracker,
|
|
37
|
+
createProduck: () => createProduck,
|
|
38
|
+
createSessionRecorder: () => createSessionRecorder,
|
|
39
|
+
createUserFlowTracker: () => createUserFlowTracker,
|
|
40
|
+
defaultProactiveConfig: () => defaultProactiveConfig
|
|
25
41
|
});
|
|
26
42
|
module.exports = __toCommonJS(src_exports);
|
|
27
43
|
|
|
44
|
+
// src/recording/utils.ts
|
|
45
|
+
function generateSessionId() {
|
|
46
|
+
const timestamp = Date.now().toString(36);
|
|
47
|
+
const randomPart = Math.random().toString(36).substring(2, 15);
|
|
48
|
+
return `rec_${timestamp}_${randomPart}`;
|
|
49
|
+
}
|
|
50
|
+
function getDeviceInfo() {
|
|
51
|
+
if (typeof window === "undefined") {
|
|
52
|
+
return {
|
|
53
|
+
userAgent: "unknown",
|
|
54
|
+
screenWidth: 0,
|
|
55
|
+
screenHeight: 0,
|
|
56
|
+
viewportWidth: 0,
|
|
57
|
+
viewportHeight: 0,
|
|
58
|
+
devicePixelRatio: 1,
|
|
59
|
+
language: "en",
|
|
60
|
+
platform: "unknown"
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
userAgent: navigator.userAgent,
|
|
65
|
+
screenWidth: screen.width,
|
|
66
|
+
screenHeight: screen.height,
|
|
67
|
+
viewportWidth: window.innerWidth,
|
|
68
|
+
viewportHeight: window.innerHeight,
|
|
69
|
+
devicePixelRatio: window.devicePixelRatio || 1,
|
|
70
|
+
language: navigator.language,
|
|
71
|
+
platform: navigator.platform
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
var DEFAULT_PRIVACY_CONFIG = {
|
|
75
|
+
maskAllInputs: true,
|
|
76
|
+
maskAllText: false,
|
|
77
|
+
maskSelectors: [".sensitive", "[data-sensitive]", ".pii", "[data-pii]"],
|
|
78
|
+
blockSelectors: [".no-record", "[data-no-record]"],
|
|
79
|
+
maskInputTypes: ["password", "email", "tel", "credit-card"],
|
|
80
|
+
maskCharacter: "*"
|
|
81
|
+
};
|
|
82
|
+
var DEFAULT_PERFORMANCE_CONFIG = {
|
|
83
|
+
batchEvents: true,
|
|
84
|
+
batchSize: 50,
|
|
85
|
+
batchTimeout: 5e3,
|
|
86
|
+
compress: true,
|
|
87
|
+
mouseMoveThrottle: 50,
|
|
88
|
+
maxDuration: 30 * 60 * 1e3
|
|
89
|
+
// 30 minutes
|
|
90
|
+
};
|
|
91
|
+
var DEFAULT_CAPTURE_CONFIG = {
|
|
92
|
+
console: true,
|
|
93
|
+
network: false,
|
|
94
|
+
performance: false,
|
|
95
|
+
errors: true,
|
|
96
|
+
customEvents: true
|
|
97
|
+
};
|
|
98
|
+
function mergeConfig(userConfig) {
|
|
99
|
+
return {
|
|
100
|
+
enabled: userConfig?.enabled ?? false,
|
|
101
|
+
samplingRate: userConfig?.samplingRate ?? 1,
|
|
102
|
+
privacy: { ...DEFAULT_PRIVACY_CONFIG, ...userConfig?.privacy },
|
|
103
|
+
performance: { ...DEFAULT_PERFORMANCE_CONFIG, ...userConfig?.performance },
|
|
104
|
+
capture: { ...DEFAULT_CAPTURE_CONFIG, ...userConfig?.capture },
|
|
105
|
+
endpoint: userConfig?.endpoint ?? "",
|
|
106
|
+
onStart: userConfig?.onStart ?? (() => {
|
|
107
|
+
}),
|
|
108
|
+
onStop: userConfig?.onStop ?? (() => {
|
|
109
|
+
}),
|
|
110
|
+
onError: userConfig?.onError ?? (() => {
|
|
111
|
+
})
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function shouldRecordSession(samplingRate) {
|
|
115
|
+
if (samplingRate >= 1) return true;
|
|
116
|
+
if (samplingRate <= 0) return false;
|
|
117
|
+
return Math.random() < samplingRate;
|
|
118
|
+
}
|
|
119
|
+
function compressEvents(events) {
|
|
120
|
+
try {
|
|
121
|
+
const json = JSON.stringify(events);
|
|
122
|
+
if (typeof btoa !== "undefined") {
|
|
123
|
+
return btoa(encodeURIComponent(json));
|
|
124
|
+
}
|
|
125
|
+
return json;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.warn("[Recording] Failed to compress events:", error);
|
|
128
|
+
return JSON.stringify(events);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function isBrowser() {
|
|
132
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
133
|
+
}
|
|
134
|
+
function getCurrentUrl() {
|
|
135
|
+
if (!isBrowser()) return "";
|
|
136
|
+
return window.location.href;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/recording/recorder.ts
|
|
140
|
+
var SessionRecorder = class {
|
|
141
|
+
constructor(config) {
|
|
142
|
+
this.state = "idle";
|
|
143
|
+
this.sessionId = null;
|
|
144
|
+
this.sdkSessionToken = null;
|
|
145
|
+
this.apiUrl = "";
|
|
146
|
+
this.sdkKey = null;
|
|
147
|
+
// Recording internals
|
|
148
|
+
this.stopRecording = null;
|
|
149
|
+
this.events = [];
|
|
150
|
+
this.eventCount = 0;
|
|
151
|
+
this.batchIndex = 0;
|
|
152
|
+
this.batchesSent = 0;
|
|
153
|
+
this.errorCount = 0;
|
|
154
|
+
this.startTime = null;
|
|
155
|
+
this.batchTimeout = null;
|
|
156
|
+
this.maxDurationTimeout = null;
|
|
157
|
+
// Cleanup handlers
|
|
158
|
+
this.consoleCleanup = null;
|
|
159
|
+
this.errorCleanup = null;
|
|
160
|
+
this.config = mergeConfig(config);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Initialize the recorder with SDK context
|
|
164
|
+
*/
|
|
165
|
+
initialize(sdkSessionToken, apiUrl, sdkKey) {
|
|
166
|
+
this.sdkSessionToken = sdkSessionToken;
|
|
167
|
+
this.apiUrl = apiUrl;
|
|
168
|
+
this.sdkKey = sdkKey || null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Update configuration
|
|
172
|
+
*/
|
|
173
|
+
updateConfig(config) {
|
|
174
|
+
this.config = mergeConfig({ ...this.config, ...config });
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Start recording session
|
|
178
|
+
*/
|
|
179
|
+
async start() {
|
|
180
|
+
if (!isBrowser()) {
|
|
181
|
+
console.warn("[Recording] Cannot record outside browser environment");
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
if (!this.config.enabled) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
if (this.state === "recording") {
|
|
188
|
+
console.warn("[Recording] Already recording");
|
|
189
|
+
return this.sessionId;
|
|
190
|
+
}
|
|
191
|
+
if (!shouldRecordSession(this.config.samplingRate)) {
|
|
192
|
+
console.log("[Recording] Session not sampled for recording");
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
const rrweb = await import("rrweb");
|
|
197
|
+
this.sessionId = generateSessionId();
|
|
198
|
+
this.events = [];
|
|
199
|
+
this.eventCount = 0;
|
|
200
|
+
this.batchIndex = 0;
|
|
201
|
+
this.batchesSent = 0;
|
|
202
|
+
this.errorCount = 0;
|
|
203
|
+
this.startTime = /* @__PURE__ */ new Date();
|
|
204
|
+
this.state = "recording";
|
|
205
|
+
const rrwebOptions = {
|
|
206
|
+
emit: (event) => this.handleEvent(event),
|
|
207
|
+
maskAllInputs: this.config.privacy.maskAllInputs,
|
|
208
|
+
maskTextSelector: this.config.privacy.maskSelectors?.join(",") || void 0,
|
|
209
|
+
blockSelector: this.config.privacy.blockSelectors?.join(",") || void 0,
|
|
210
|
+
maskInputOptions: {
|
|
211
|
+
password: true,
|
|
212
|
+
email: this.config.privacy.maskInputTypes?.includes("email"),
|
|
213
|
+
tel: this.config.privacy.maskInputTypes?.includes("tel")
|
|
214
|
+
},
|
|
215
|
+
sampling: {
|
|
216
|
+
mousemove: true,
|
|
217
|
+
mouseInteraction: true,
|
|
218
|
+
scroll: 150,
|
|
219
|
+
media: 800,
|
|
220
|
+
input: "last"
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
this.stopRecording = rrweb.record(rrwebOptions);
|
|
224
|
+
if (this.config.capture.console) {
|
|
225
|
+
this.setupConsoleCapture();
|
|
226
|
+
}
|
|
227
|
+
if (this.config.capture.errors) {
|
|
228
|
+
this.setupErrorCapture();
|
|
229
|
+
}
|
|
230
|
+
if (this.config.performance.maxDuration) {
|
|
231
|
+
this.maxDurationTimeout = setTimeout(() => {
|
|
232
|
+
console.log("[Recording] Max duration reached, stopping");
|
|
233
|
+
this.stop();
|
|
234
|
+
}, this.config.performance.maxDuration);
|
|
235
|
+
}
|
|
236
|
+
await this.sendSessionStart();
|
|
237
|
+
this.config.onStart(this.sessionId);
|
|
238
|
+
console.log("[Recording] Started session:", this.sessionId);
|
|
239
|
+
return this.sessionId;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error("[Recording] Failed to start:", error);
|
|
242
|
+
this.state = "idle";
|
|
243
|
+
this.config.onError(error instanceof Error ? error : new Error(String(error)));
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Stop recording
|
|
249
|
+
*/
|
|
250
|
+
async stop() {
|
|
251
|
+
if (this.state !== "recording" && this.state !== "paused") {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
if (this.stopRecording) {
|
|
256
|
+
this.stopRecording();
|
|
257
|
+
this.stopRecording = null;
|
|
258
|
+
}
|
|
259
|
+
if (this.consoleCleanup) {
|
|
260
|
+
this.consoleCleanup();
|
|
261
|
+
this.consoleCleanup = null;
|
|
262
|
+
}
|
|
263
|
+
if (this.errorCleanup) {
|
|
264
|
+
this.errorCleanup();
|
|
265
|
+
this.errorCleanup = null;
|
|
266
|
+
}
|
|
267
|
+
if (this.batchTimeout) {
|
|
268
|
+
clearTimeout(this.batchTimeout);
|
|
269
|
+
this.batchTimeout = null;
|
|
270
|
+
}
|
|
271
|
+
if (this.maxDurationTimeout) {
|
|
272
|
+
clearTimeout(this.maxDurationTimeout);
|
|
273
|
+
this.maxDurationTimeout = null;
|
|
274
|
+
}
|
|
275
|
+
if (this.events.length > 0) {
|
|
276
|
+
await this.sendBatch(true);
|
|
277
|
+
}
|
|
278
|
+
await this.sendSessionEnd();
|
|
279
|
+
const sessionId = this.sessionId;
|
|
280
|
+
const eventCount = this.eventCount;
|
|
281
|
+
this.state = "stopped";
|
|
282
|
+
this.config.onStop(sessionId || "", eventCount);
|
|
283
|
+
console.log("[Recording] Stopped session:", sessionId, "Events:", eventCount);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.error("[Recording] Error stopping:", error);
|
|
286
|
+
this.config.onError(error instanceof Error ? error : new Error(String(error)));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Pause recording
|
|
291
|
+
*/
|
|
292
|
+
pause() {
|
|
293
|
+
if (this.state !== "recording") return;
|
|
294
|
+
this.state = "paused";
|
|
295
|
+
console.log("[Recording] Paused");
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Resume recording
|
|
299
|
+
*/
|
|
300
|
+
resume() {
|
|
301
|
+
if (this.state !== "paused") return;
|
|
302
|
+
this.state = "recording";
|
|
303
|
+
console.log("[Recording] Resumed");
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Add a custom event to the recording
|
|
307
|
+
*/
|
|
308
|
+
addCustomEvent(event) {
|
|
309
|
+
if (this.state !== "recording" || !this.config.capture.customEvents) return;
|
|
310
|
+
const customEvent = {
|
|
311
|
+
type: 5,
|
|
312
|
+
// rrweb custom event type
|
|
313
|
+
data: {
|
|
314
|
+
tag: "custom",
|
|
315
|
+
payload: event
|
|
316
|
+
},
|
|
317
|
+
timestamp: event.timestamp || Date.now()
|
|
318
|
+
};
|
|
319
|
+
this.handleEvent(customEvent);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Get current recording stats
|
|
323
|
+
*/
|
|
324
|
+
getStats() {
|
|
325
|
+
const duration = this.startTime ? Date.now() - this.startTime.getTime() : 0;
|
|
326
|
+
return {
|
|
327
|
+
state: this.state,
|
|
328
|
+
sessionId: this.sessionId,
|
|
329
|
+
eventCount: this.eventCount,
|
|
330
|
+
startTime: this.startTime,
|
|
331
|
+
duration,
|
|
332
|
+
batchesSent: this.batchesSent,
|
|
333
|
+
errors: this.errorCount
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Check if recording is active
|
|
338
|
+
*/
|
|
339
|
+
isRecording() {
|
|
340
|
+
return this.state === "recording";
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Get session ID
|
|
344
|
+
*/
|
|
345
|
+
getSessionId() {
|
|
346
|
+
return this.sessionId;
|
|
347
|
+
}
|
|
348
|
+
// ============ Private Methods ============
|
|
349
|
+
/**
|
|
350
|
+
* Handle incoming event from rrweb
|
|
351
|
+
*/
|
|
352
|
+
handleEvent(event) {
|
|
353
|
+
if (this.state !== "recording") return;
|
|
354
|
+
this.events.push(event);
|
|
355
|
+
this.eventCount++;
|
|
356
|
+
if (this.config.performance.batchEvents) {
|
|
357
|
+
if (this.events.length >= this.config.performance.batchSize) {
|
|
358
|
+
this.sendBatch();
|
|
359
|
+
} else if (!this.batchTimeout) {
|
|
360
|
+
this.batchTimeout = setTimeout(() => {
|
|
361
|
+
this.batchTimeout = null;
|
|
362
|
+
if (this.events.length > 0) {
|
|
363
|
+
this.sendBatch();
|
|
364
|
+
}
|
|
365
|
+
}, this.config.performance.batchTimeout);
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
this.sendBatch();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Send event batch to backend
|
|
373
|
+
*/
|
|
374
|
+
async sendBatch(isFinal = false) {
|
|
375
|
+
if (this.events.length === 0) return;
|
|
376
|
+
const eventsToSend = [...this.events];
|
|
377
|
+
this.events = [];
|
|
378
|
+
const batch = {
|
|
379
|
+
sessionId: this.sessionId,
|
|
380
|
+
sdkSessionToken: this.sdkSessionToken,
|
|
381
|
+
events: eventsToSend,
|
|
382
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
383
|
+
batchIndex: this.batchIndex++,
|
|
384
|
+
isFinal
|
|
385
|
+
};
|
|
386
|
+
try {
|
|
387
|
+
const endpoint = this.config.endpoint || `${this.apiUrl}/session-recordings/events`;
|
|
388
|
+
const headers = { "Content-Type": "application/json" };
|
|
389
|
+
if (this.sdkKey) {
|
|
390
|
+
headers["X-SDK-Key"] = this.sdkKey;
|
|
391
|
+
}
|
|
392
|
+
const body = this.config.performance.compress ? JSON.stringify({ ...batch, events: void 0, compressedEvents: compressEvents(eventsToSend) }) : JSON.stringify(batch);
|
|
393
|
+
await fetch(endpoint, {
|
|
394
|
+
method: "POST",
|
|
395
|
+
headers,
|
|
396
|
+
body
|
|
397
|
+
});
|
|
398
|
+
this.batchesSent++;
|
|
399
|
+
} catch (error) {
|
|
400
|
+
console.error("[Recording] Failed to send batch:", error);
|
|
401
|
+
this.errorCount++;
|
|
402
|
+
this.events = [...eventsToSend, ...this.events];
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Send session start notification
|
|
407
|
+
*/
|
|
408
|
+
async sendSessionStart() {
|
|
409
|
+
const session = {
|
|
410
|
+
id: this.sessionId,
|
|
411
|
+
sdkSessionToken: this.sdkSessionToken,
|
|
412
|
+
startUrl: getCurrentUrl(),
|
|
413
|
+
startTime: this.startTime,
|
|
414
|
+
deviceInfo: getDeviceInfo()
|
|
415
|
+
};
|
|
416
|
+
try {
|
|
417
|
+
const endpoint = `${this.apiUrl}/session-recordings/start`;
|
|
418
|
+
const headers = { "Content-Type": "application/json" };
|
|
419
|
+
if (this.sdkKey) {
|
|
420
|
+
headers["X-SDK-Key"] = this.sdkKey;
|
|
421
|
+
}
|
|
422
|
+
await fetch(endpoint, {
|
|
423
|
+
method: "POST",
|
|
424
|
+
headers,
|
|
425
|
+
body: JSON.stringify(session)
|
|
426
|
+
});
|
|
427
|
+
} catch (error) {
|
|
428
|
+
console.error("[Recording] Failed to send session start:", error);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Send session end notification
|
|
433
|
+
*/
|
|
434
|
+
async sendSessionEnd() {
|
|
435
|
+
try {
|
|
436
|
+
const endpoint = `${this.apiUrl}/session-recordings/end`;
|
|
437
|
+
const headers = { "Content-Type": "application/json" };
|
|
438
|
+
if (this.sdkKey) {
|
|
439
|
+
headers["X-SDK-Key"] = this.sdkKey;
|
|
440
|
+
}
|
|
441
|
+
await fetch(endpoint, {
|
|
442
|
+
method: "POST",
|
|
443
|
+
headers,
|
|
444
|
+
body: JSON.stringify({
|
|
445
|
+
sessionId: this.sessionId,
|
|
446
|
+
endTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
447
|
+
eventCount: this.eventCount
|
|
448
|
+
})
|
|
449
|
+
});
|
|
450
|
+
} catch (error) {
|
|
451
|
+
console.error("[Recording] Failed to send session end:", error);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Setup console log capture
|
|
456
|
+
*/
|
|
457
|
+
setupConsoleCapture() {
|
|
458
|
+
const originalConsole = {
|
|
459
|
+
log: console.log,
|
|
460
|
+
info: console.info,
|
|
461
|
+
warn: console.warn,
|
|
462
|
+
error: console.error,
|
|
463
|
+
debug: console.debug
|
|
464
|
+
};
|
|
465
|
+
const captureConsole = (level) => {
|
|
466
|
+
return (...args) => {
|
|
467
|
+
originalConsole[level](...args);
|
|
468
|
+
if (args[0]?.toString().includes("[Recording]")) return;
|
|
469
|
+
const logEvent = {
|
|
470
|
+
level,
|
|
471
|
+
message: args.map(
|
|
472
|
+
(arg) => typeof arg === "object" ? JSON.stringify(arg) : String(arg)
|
|
473
|
+
).join(" "),
|
|
474
|
+
timestamp: Date.now()
|
|
475
|
+
};
|
|
476
|
+
this.addCustomEvent({
|
|
477
|
+
type: "console",
|
|
478
|
+
data: logEvent
|
|
479
|
+
});
|
|
480
|
+
};
|
|
481
|
+
};
|
|
482
|
+
console.log = captureConsole("log");
|
|
483
|
+
console.info = captureConsole("info");
|
|
484
|
+
console.warn = captureConsole("warn");
|
|
485
|
+
console.error = captureConsole("error");
|
|
486
|
+
console.debug = captureConsole("debug");
|
|
487
|
+
this.consoleCleanup = () => {
|
|
488
|
+
console.log = originalConsole.log;
|
|
489
|
+
console.info = originalConsole.info;
|
|
490
|
+
console.warn = originalConsole.warn;
|
|
491
|
+
console.error = originalConsole.error;
|
|
492
|
+
console.debug = originalConsole.debug;
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Setup error capture
|
|
497
|
+
*/
|
|
498
|
+
setupErrorCapture() {
|
|
499
|
+
const errorHandler = (event) => {
|
|
500
|
+
const errorEvent = {
|
|
501
|
+
type: "error",
|
|
502
|
+
message: event.message,
|
|
503
|
+
filename: event.filename,
|
|
504
|
+
lineno: event.lineno,
|
|
505
|
+
colno: event.colno,
|
|
506
|
+
timestamp: Date.now()
|
|
507
|
+
};
|
|
508
|
+
this.addCustomEvent({
|
|
509
|
+
type: "error",
|
|
510
|
+
data: errorEvent
|
|
511
|
+
});
|
|
512
|
+
};
|
|
513
|
+
const rejectionHandler = (event) => {
|
|
514
|
+
const errorEvent = {
|
|
515
|
+
type: "unhandledrejection",
|
|
516
|
+
message: event.reason?.message || String(event.reason),
|
|
517
|
+
stack: event.reason?.stack,
|
|
518
|
+
timestamp: Date.now()
|
|
519
|
+
};
|
|
520
|
+
this.addCustomEvent({
|
|
521
|
+
type: "error",
|
|
522
|
+
data: errorEvent
|
|
523
|
+
});
|
|
524
|
+
};
|
|
525
|
+
window.addEventListener("error", errorHandler);
|
|
526
|
+
window.addEventListener("unhandledrejection", rejectionHandler);
|
|
527
|
+
this.errorCleanup = () => {
|
|
528
|
+
window.removeEventListener("error", errorHandler);
|
|
529
|
+
window.removeEventListener("unhandledrejection", rejectionHandler);
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Destroy the recorder
|
|
534
|
+
*/
|
|
535
|
+
destroy() {
|
|
536
|
+
this.stop();
|
|
537
|
+
this.sessionId = null;
|
|
538
|
+
this.sdkSessionToken = null;
|
|
539
|
+
this.events = [];
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
function createSessionRecorder(config) {
|
|
543
|
+
return new SessionRecorder(config);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/proactive.ts
|
|
547
|
+
var ProactiveBehaviorTracker = class {
|
|
548
|
+
constructor(config, callbacks) {
|
|
549
|
+
this.isTracking = false;
|
|
550
|
+
// Tracking state
|
|
551
|
+
this.scrollDepth = 0;
|
|
552
|
+
this.pageLoadTime = 0;
|
|
553
|
+
this.clickCount = 0;
|
|
554
|
+
this.lastActivityTime = 0;
|
|
555
|
+
this.currentUrl = "";
|
|
556
|
+
this.previousUrl = "";
|
|
557
|
+
// Trigger state
|
|
558
|
+
this.triggeredSet = /* @__PURE__ */ new Set();
|
|
559
|
+
this.lastTriggerTime = 0;
|
|
560
|
+
this.proactiveMessageCount = 0;
|
|
561
|
+
// Listeners
|
|
562
|
+
this.scrollHandler = null;
|
|
563
|
+
this.clickHandler = null;
|
|
564
|
+
this.mouseMoveHandler = null;
|
|
565
|
+
this.mouseLeaveHandler = null;
|
|
566
|
+
this.visibilityHandler = null;
|
|
567
|
+
// Timers
|
|
568
|
+
this.timeCheckInterval = null;
|
|
569
|
+
this.idleCheckInterval = null;
|
|
570
|
+
this.config = config;
|
|
571
|
+
this.callbacks = callbacks;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Start tracking user behavior
|
|
575
|
+
*/
|
|
576
|
+
start() {
|
|
577
|
+
if (!this.config.enabled || this.isTracking) return;
|
|
578
|
+
if (typeof window === "undefined") return;
|
|
579
|
+
if (this.config.respectUserPreference) {
|
|
580
|
+
const optOut = localStorage.getItem("produck_proactive_optout");
|
|
581
|
+
if (optOut === "true") return;
|
|
582
|
+
}
|
|
583
|
+
this.isTracking = true;
|
|
584
|
+
this.pageLoadTime = Date.now();
|
|
585
|
+
this.lastActivityTime = Date.now();
|
|
586
|
+
this.currentUrl = window.location.href;
|
|
587
|
+
this.setupScrollTracking();
|
|
588
|
+
this.setupClickTracking();
|
|
589
|
+
this.setupIdleTracking();
|
|
590
|
+
this.setupExitIntentTracking();
|
|
591
|
+
this.setupTimeTracking();
|
|
592
|
+
this.setupPageChangeTracking();
|
|
593
|
+
console.log("[Produck] Proactive behavior tracking started");
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Stop tracking
|
|
597
|
+
*/
|
|
598
|
+
stop() {
|
|
599
|
+
if (!this.isTracking) return;
|
|
600
|
+
this.isTracking = false;
|
|
601
|
+
if (this.scrollHandler) {
|
|
602
|
+
window.removeEventListener("scroll", this.scrollHandler);
|
|
603
|
+
}
|
|
604
|
+
if (this.clickHandler) {
|
|
605
|
+
document.removeEventListener("click", this.clickHandler);
|
|
606
|
+
}
|
|
607
|
+
if (this.mouseMoveHandler) {
|
|
608
|
+
document.removeEventListener("mousemove", this.mouseMoveHandler);
|
|
609
|
+
}
|
|
610
|
+
if (this.mouseLeaveHandler) {
|
|
611
|
+
document.removeEventListener("mouseleave", this.mouseLeaveHandler);
|
|
612
|
+
}
|
|
613
|
+
if (this.visibilityHandler) {
|
|
614
|
+
document.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
615
|
+
}
|
|
616
|
+
if (this.timeCheckInterval) {
|
|
617
|
+
clearInterval(this.timeCheckInterval);
|
|
618
|
+
}
|
|
619
|
+
if (this.idleCheckInterval) {
|
|
620
|
+
clearInterval(this.idleCheckInterval);
|
|
621
|
+
}
|
|
622
|
+
console.log("[Produck] Proactive behavior tracking stopped");
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Reset tracking state (e.g., on page change)
|
|
626
|
+
*/
|
|
627
|
+
reset() {
|
|
628
|
+
this.scrollDepth = 0;
|
|
629
|
+
this.clickCount = 0;
|
|
630
|
+
this.pageLoadTime = Date.now();
|
|
631
|
+
this.lastActivityTime = Date.now();
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Get current tracking stats
|
|
635
|
+
*/
|
|
636
|
+
getStats() {
|
|
637
|
+
return {
|
|
638
|
+
scrollDepth: this.scrollDepth,
|
|
639
|
+
timeOnPage: Date.now() - this.pageLoadTime,
|
|
640
|
+
clickCount: this.clickCount,
|
|
641
|
+
idleTime: Date.now() - this.lastActivityTime,
|
|
642
|
+
proactiveMessageCount: this.proactiveMessageCount
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Check if a trigger should fire
|
|
647
|
+
*/
|
|
648
|
+
shouldTrigger(trigger) {
|
|
649
|
+
if (this.config.maxProactiveMessages && this.proactiveMessageCount >= this.config.maxProactiveMessages) {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
if (this.config.globalCooldown && Date.now() - this.lastTriggerTime < this.config.globalCooldown) {
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
const triggerKey = `${trigger.type}-${trigger.threshold || "default"}`;
|
|
656
|
+
if (trigger.onlyOnce && this.triggeredSet.has(triggerKey)) {
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
const lastTriggerKey = `${triggerKey}-lastTime`;
|
|
660
|
+
const lastTime = parseInt(sessionStorage.getItem(lastTriggerKey) || "0", 10);
|
|
661
|
+
if (trigger.cooldown && Date.now() - lastTime < trigger.cooldown) {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Fire a trigger
|
|
668
|
+
*/
|
|
669
|
+
fireTrigger(trigger, context) {
|
|
670
|
+
if (!this.shouldTrigger(trigger)) return;
|
|
671
|
+
const triggerKey = `${trigger.type}-${trigger.threshold || "default"}`;
|
|
672
|
+
const lastTriggerKey = `${triggerKey}-lastTime`;
|
|
673
|
+
this.triggeredSet.add(triggerKey);
|
|
674
|
+
this.lastTriggerTime = Date.now();
|
|
675
|
+
this.proactiveMessageCount++;
|
|
676
|
+
sessionStorage.setItem(lastTriggerKey, Date.now().toString());
|
|
677
|
+
const message = trigger.message || this.getDefaultMessage(trigger.type, context);
|
|
678
|
+
this.callbacks.onTrigger(context, message);
|
|
679
|
+
if (this.callbacks.onMessage) {
|
|
680
|
+
this.callbacks.onMessage(message);
|
|
681
|
+
}
|
|
682
|
+
console.log("[Produck] Proactive trigger fired:", trigger.type, context);
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Get default message for trigger type
|
|
686
|
+
*/
|
|
687
|
+
getDefaultMessage(type, context) {
|
|
688
|
+
switch (type) {
|
|
689
|
+
case "scroll":
|
|
690
|
+
return `I see you're exploring the page! Is there something specific you're looking for?`;
|
|
691
|
+
case "time":
|
|
692
|
+
return `You've been here a while. Would you like some help finding what you need?`;
|
|
693
|
+
case "clicks":
|
|
694
|
+
return `Looks like you're navigating around. Can I help you find something?`;
|
|
695
|
+
case "idle":
|
|
696
|
+
return `Still there? Let me know if you have any questions!`;
|
|
697
|
+
case "exit":
|
|
698
|
+
return `Wait! Before you go, is there anything I can help you with?`;
|
|
699
|
+
case "pageChange":
|
|
700
|
+
return `I see you're exploring different pages. Need any guidance?`;
|
|
701
|
+
default:
|
|
702
|
+
return `How can I help you today?`;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Setup scroll depth tracking
|
|
707
|
+
*/
|
|
708
|
+
setupScrollTracking() {
|
|
709
|
+
const scrollTriggers = this.config.triggers.filter((t) => t.type === "scroll" && t.enabled);
|
|
710
|
+
if (scrollTriggers.length === 0) return;
|
|
711
|
+
this.scrollHandler = () => {
|
|
712
|
+
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
|
713
|
+
const scrollPercent = docHeight > 0 ? window.scrollY / docHeight * 100 : 0;
|
|
714
|
+
this.scrollDepth = Math.max(this.scrollDepth, scrollPercent);
|
|
715
|
+
this.lastActivityTime = Date.now();
|
|
716
|
+
scrollTriggers.forEach((trigger) => {
|
|
717
|
+
if (trigger.threshold && this.scrollDepth >= trigger.threshold) {
|
|
718
|
+
this.fireTrigger(trigger, {
|
|
719
|
+
triggerType: "scroll",
|
|
720
|
+
scrollDepth: this.scrollDepth,
|
|
721
|
+
currentUrl: window.location.href,
|
|
722
|
+
pageTitle: document.title
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
};
|
|
727
|
+
window.addEventListener("scroll", this.scrollHandler, { passive: true });
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Setup click tracking
|
|
731
|
+
*/
|
|
732
|
+
setupClickTracking() {
|
|
733
|
+
const clickTriggers = this.config.triggers.filter((t) => t.type === "clicks" && t.enabled);
|
|
734
|
+
if (clickTriggers.length === 0) return;
|
|
735
|
+
this.clickHandler = () => {
|
|
736
|
+
this.clickCount++;
|
|
737
|
+
this.lastActivityTime = Date.now();
|
|
738
|
+
clickTriggers.forEach((trigger) => {
|
|
739
|
+
if (trigger.threshold && this.clickCount >= trigger.threshold) {
|
|
740
|
+
this.fireTrigger(trigger, {
|
|
741
|
+
triggerType: "clicks",
|
|
742
|
+
clickCount: this.clickCount,
|
|
743
|
+
currentUrl: window.location.href,
|
|
744
|
+
pageTitle: document.title
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
};
|
|
749
|
+
document.addEventListener("click", this.clickHandler);
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Setup idle tracking
|
|
753
|
+
*/
|
|
754
|
+
setupIdleTracking() {
|
|
755
|
+
const idleTriggers = this.config.triggers.filter((t) => t.type === "idle" && t.enabled);
|
|
756
|
+
if (idleTriggers.length === 0) return;
|
|
757
|
+
this.mouseMoveHandler = () => {
|
|
758
|
+
this.lastActivityTime = Date.now();
|
|
759
|
+
};
|
|
760
|
+
document.addEventListener("mousemove", this.mouseMoveHandler, { passive: true });
|
|
761
|
+
this.idleCheckInterval = setInterval(() => {
|
|
762
|
+
const idleTime = Date.now() - this.lastActivityTime;
|
|
763
|
+
idleTriggers.forEach((trigger) => {
|
|
764
|
+
if (trigger.threshold && idleTime >= trigger.threshold) {
|
|
765
|
+
this.fireTrigger(trigger, {
|
|
766
|
+
triggerType: "idle",
|
|
767
|
+
idleTime,
|
|
768
|
+
currentUrl: window.location.href,
|
|
769
|
+
pageTitle: document.title
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
}, 1e3);
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Setup exit intent tracking
|
|
777
|
+
*/
|
|
778
|
+
setupExitIntentTracking() {
|
|
779
|
+
const exitTriggers = this.config.triggers.filter((t) => t.type === "exit" && t.enabled);
|
|
780
|
+
if (exitTriggers.length === 0) return;
|
|
781
|
+
this.mouseLeaveHandler = (e) => {
|
|
782
|
+
if (e.clientY <= 0) {
|
|
783
|
+
exitTriggers.forEach((trigger) => {
|
|
784
|
+
this.fireTrigger(trigger, {
|
|
785
|
+
triggerType: "exit",
|
|
786
|
+
currentUrl: window.location.href,
|
|
787
|
+
pageTitle: document.title
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
document.addEventListener("mouseleave", this.mouseLeaveHandler);
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Setup time on page tracking
|
|
796
|
+
*/
|
|
797
|
+
setupTimeTracking() {
|
|
798
|
+
const timeTriggers = this.config.triggers.filter((t) => t.type === "time" && t.enabled);
|
|
799
|
+
if (timeTriggers.length === 0) return;
|
|
800
|
+
this.timeCheckInterval = setInterval(() => {
|
|
801
|
+
const timeOnPage = Date.now() - this.pageLoadTime;
|
|
802
|
+
timeTriggers.forEach((trigger) => {
|
|
803
|
+
if (trigger.threshold && timeOnPage >= trigger.threshold) {
|
|
804
|
+
this.fireTrigger(trigger, {
|
|
805
|
+
triggerType: "time",
|
|
806
|
+
timeOnPage,
|
|
807
|
+
currentUrl: window.location.href,
|
|
808
|
+
pageTitle: document.title
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
}, 1e3);
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Setup SPA page change tracking
|
|
816
|
+
*/
|
|
817
|
+
setupPageChangeTracking() {
|
|
818
|
+
const pageTriggers = this.config.triggers.filter((t) => t.type === "pageChange" && t.enabled);
|
|
819
|
+
if (pageTriggers.length === 0) return;
|
|
820
|
+
this.visibilityHandler = () => {
|
|
821
|
+
if (document.visibilityState === "visible") {
|
|
822
|
+
const newUrl = window.location.href;
|
|
823
|
+
if (newUrl !== this.currentUrl) {
|
|
824
|
+
this.previousUrl = this.currentUrl;
|
|
825
|
+
this.currentUrl = newUrl;
|
|
826
|
+
this.scrollDepth = 0;
|
|
827
|
+
this.clickCount = 0;
|
|
828
|
+
this.pageLoadTime = Date.now();
|
|
829
|
+
pageTriggers.forEach((trigger) => {
|
|
830
|
+
this.fireTrigger(trigger, {
|
|
831
|
+
triggerType: "pageChange",
|
|
832
|
+
currentUrl: this.currentUrl,
|
|
833
|
+
previousUrl: this.previousUrl,
|
|
834
|
+
pageTitle: document.title
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
document.addEventListener("visibilitychange", this.visibilityHandler);
|
|
841
|
+
window.addEventListener("popstate", () => {
|
|
842
|
+
const newUrl = window.location.href;
|
|
843
|
+
if (newUrl !== this.currentUrl) {
|
|
844
|
+
this.previousUrl = this.currentUrl;
|
|
845
|
+
this.currentUrl = newUrl;
|
|
846
|
+
pageTriggers.forEach((trigger) => {
|
|
847
|
+
this.fireTrigger(trigger, {
|
|
848
|
+
triggerType: "pageChange",
|
|
849
|
+
currentUrl: this.currentUrl,
|
|
850
|
+
previousUrl: this.previousUrl,
|
|
851
|
+
pageTitle: document.title
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Manually trigger a proactive message (for custom integrations)
|
|
859
|
+
*/
|
|
860
|
+
triggerManual(message, context) {
|
|
861
|
+
this.proactiveMessageCount++;
|
|
862
|
+
this.lastTriggerTime = Date.now();
|
|
863
|
+
const fullContext = {
|
|
864
|
+
triggerType: "time",
|
|
865
|
+
// default
|
|
866
|
+
currentUrl: window.location.href,
|
|
867
|
+
pageTitle: document.title,
|
|
868
|
+
...context
|
|
869
|
+
};
|
|
870
|
+
this.callbacks.onTrigger(fullContext, message);
|
|
871
|
+
if (this.callbacks.onMessage) {
|
|
872
|
+
this.callbacks.onMessage(message);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Opt out of proactive messages
|
|
877
|
+
*/
|
|
878
|
+
optOut() {
|
|
879
|
+
if (typeof localStorage !== "undefined") {
|
|
880
|
+
localStorage.setItem("produck_proactive_optout", "true");
|
|
881
|
+
}
|
|
882
|
+
this.stop();
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Opt in to proactive messages
|
|
886
|
+
*/
|
|
887
|
+
optIn() {
|
|
888
|
+
if (typeof localStorage !== "undefined") {
|
|
889
|
+
localStorage.removeItem("produck_proactive_optout");
|
|
890
|
+
}
|
|
891
|
+
this.start();
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
var defaultProactiveConfig = {
|
|
895
|
+
enabled: false,
|
|
896
|
+
triggers: [
|
|
897
|
+
{
|
|
898
|
+
type: "scroll",
|
|
899
|
+
enabled: true,
|
|
900
|
+
threshold: 75,
|
|
901
|
+
// 75% scroll depth
|
|
902
|
+
onlyOnce: true,
|
|
903
|
+
cooldown: 6e4
|
|
904
|
+
// 1 minute
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
type: "time",
|
|
908
|
+
enabled: true,
|
|
909
|
+
threshold: 3e4,
|
|
910
|
+
// 30 seconds
|
|
911
|
+
onlyOnce: true
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
type: "idle",
|
|
915
|
+
enabled: true,
|
|
916
|
+
threshold: 6e4,
|
|
917
|
+
// 1 minute idle
|
|
918
|
+
cooldown: 12e4
|
|
919
|
+
// 2 minutes between
|
|
920
|
+
},
|
|
921
|
+
{
|
|
922
|
+
type: "exit",
|
|
923
|
+
enabled: true,
|
|
924
|
+
onlyOnce: true
|
|
925
|
+
}
|
|
926
|
+
],
|
|
927
|
+
maxProactiveMessages: 3,
|
|
928
|
+
globalCooldown: 3e4,
|
|
929
|
+
// 30 seconds between any proactive message
|
|
930
|
+
respectUserPreference: true
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
// src/user-flows/utils.ts
|
|
934
|
+
function isBrowser2() {
|
|
935
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
936
|
+
}
|
|
937
|
+
function generateSessionId2() {
|
|
938
|
+
return "ufs_" + Date.now().toString(36) + "_" + Math.random().toString(36).substring(2, 11);
|
|
939
|
+
}
|
|
940
|
+
function getCurrentUrl2() {
|
|
941
|
+
if (!isBrowser2()) return "";
|
|
942
|
+
return window.location.href;
|
|
943
|
+
}
|
|
944
|
+
function getCurrentPath() {
|
|
945
|
+
if (!isBrowser2()) return "";
|
|
946
|
+
return window.location.pathname;
|
|
947
|
+
}
|
|
948
|
+
function getCurrentTitle() {
|
|
949
|
+
if (!isBrowser2()) return "";
|
|
950
|
+
return document.title || "";
|
|
951
|
+
}
|
|
952
|
+
function shouldTrackSession(samplingRate = 1) {
|
|
953
|
+
return Math.random() < samplingRate;
|
|
954
|
+
}
|
|
955
|
+
function getDeviceInfo2() {
|
|
956
|
+
if (!isBrowser2()) {
|
|
957
|
+
return {
|
|
958
|
+
deviceType: "desktop",
|
|
959
|
+
browser: "unknown",
|
|
960
|
+
os: "unknown",
|
|
961
|
+
viewportWidth: 0,
|
|
962
|
+
viewportHeight: 0,
|
|
963
|
+
userAgent: ""
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
const ua = navigator.userAgent;
|
|
967
|
+
let deviceType = "desktop";
|
|
968
|
+
if (/Mobi|Android/i.test(ua)) {
|
|
969
|
+
deviceType = /Tablet|iPad/i.test(ua) ? "tablet" : "mobile";
|
|
970
|
+
}
|
|
971
|
+
let browser = "unknown";
|
|
972
|
+
if (ua.includes("Firefox")) browser = "Firefox";
|
|
973
|
+
else if (ua.includes("Edg")) browser = "Edge";
|
|
974
|
+
else if (ua.includes("Chrome")) browser = "Chrome";
|
|
975
|
+
else if (ua.includes("Safari")) browser = "Safari";
|
|
976
|
+
else if (ua.includes("Opera") || ua.includes("OPR")) browser = "Opera";
|
|
977
|
+
let os = "unknown";
|
|
978
|
+
if (ua.includes("Windows")) os = "Windows";
|
|
979
|
+
else if (ua.includes("Mac OS")) os = "macOS";
|
|
980
|
+
else if (ua.includes("Linux")) os = "Linux";
|
|
981
|
+
else if (ua.includes("Android")) os = "Android";
|
|
982
|
+
else if (ua.includes("iOS") || ua.includes("iPhone") || ua.includes("iPad")) os = "iOS";
|
|
983
|
+
return {
|
|
984
|
+
deviceType,
|
|
985
|
+
browser,
|
|
986
|
+
os,
|
|
987
|
+
viewportWidth: window.innerWidth,
|
|
988
|
+
viewportHeight: window.innerHeight,
|
|
989
|
+
userAgent: ua
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
function getElementSelector(element, depth = 5) {
|
|
993
|
+
const parts = [];
|
|
994
|
+
let current = element;
|
|
995
|
+
let currentDepth = 0;
|
|
996
|
+
while (current && current !== document.body && currentDepth < depth) {
|
|
997
|
+
let selector = current.tagName.toLowerCase();
|
|
998
|
+
if (current.id) {
|
|
999
|
+
selector += `#${current.id}`;
|
|
1000
|
+
parts.unshift(selector);
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
if (current.className && typeof current.className === "string") {
|
|
1004
|
+
const classes = current.className.trim().split(/\s+/);
|
|
1005
|
+
if (classes.length > 0 && classes[0]) {
|
|
1006
|
+
selector += `.${classes[0]}`;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
const parent = current.parentElement;
|
|
1010
|
+
if (parent) {
|
|
1011
|
+
const siblings = Array.from(parent.children).filter(
|
|
1012
|
+
(child) => child.tagName === current.tagName
|
|
1013
|
+
);
|
|
1014
|
+
if (siblings.length > 1) {
|
|
1015
|
+
const index = siblings.indexOf(current) + 1;
|
|
1016
|
+
selector += `:nth-child(${index})`;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
parts.unshift(selector);
|
|
1020
|
+
current = current.parentElement;
|
|
1021
|
+
currentDepth++;
|
|
1022
|
+
}
|
|
1023
|
+
return parts.join(" > ");
|
|
1024
|
+
}
|
|
1025
|
+
function getElementXPath(element) {
|
|
1026
|
+
if (!element) return "";
|
|
1027
|
+
if (element.id) {
|
|
1028
|
+
return `//*[@id="${element.id}"]`;
|
|
1029
|
+
}
|
|
1030
|
+
const parts = [];
|
|
1031
|
+
let current = element;
|
|
1032
|
+
while (current && current.nodeType === Node.ELEMENT_NODE) {
|
|
1033
|
+
let index = 0;
|
|
1034
|
+
let sibling = current.previousElementSibling;
|
|
1035
|
+
while (sibling) {
|
|
1036
|
+
if (sibling.tagName === current.tagName) {
|
|
1037
|
+
index++;
|
|
1038
|
+
}
|
|
1039
|
+
sibling = sibling.previousElementSibling;
|
|
1040
|
+
}
|
|
1041
|
+
const tagName = current.tagName.toLowerCase();
|
|
1042
|
+
const part = index > 0 ? `${tagName}[${index + 1}]` : tagName;
|
|
1043
|
+
parts.unshift(part);
|
|
1044
|
+
current = current.parentElement;
|
|
1045
|
+
}
|
|
1046
|
+
return "/" + parts.join("/");
|
|
1047
|
+
}
|
|
1048
|
+
function getElementInfo(element, config = {}) {
|
|
1049
|
+
const maxTextLength = config?.maxTextLength || 100;
|
|
1050
|
+
const maxHtmlLength = config?.maxHtmlLength || 500;
|
|
1051
|
+
const selectorDepth = config?.selectorDepth || 5;
|
|
1052
|
+
let text = "";
|
|
1053
|
+
if (element instanceof HTMLElement) {
|
|
1054
|
+
text = element.innerText || element.textContent || "";
|
|
1055
|
+
text = text.trim().substring(0, maxTextLength);
|
|
1056
|
+
if (text.length === maxTextLength) text += "...";
|
|
1057
|
+
}
|
|
1058
|
+
let html;
|
|
1059
|
+
if (config?.captureHtml !== false) {
|
|
1060
|
+
html = element.outerHTML.substring(0, maxHtmlLength);
|
|
1061
|
+
if (html.length === maxHtmlLength) html += "...";
|
|
1062
|
+
}
|
|
1063
|
+
const rect = element.getBoundingClientRect();
|
|
1064
|
+
const info = {
|
|
1065
|
+
tag: element.tagName.toLowerCase(),
|
|
1066
|
+
selector: getElementSelector(element, selectorDepth)
|
|
1067
|
+
};
|
|
1068
|
+
if (element.id) info.id = element.id;
|
|
1069
|
+
if (element.className && typeof element.className === "string") {
|
|
1070
|
+
info.className = element.className;
|
|
1071
|
+
}
|
|
1072
|
+
if (text) info.text = text;
|
|
1073
|
+
if (element instanceof HTMLAnchorElement && element.href) {
|
|
1074
|
+
info.href = element.href;
|
|
1075
|
+
}
|
|
1076
|
+
info.xpath = getElementXPath(element);
|
|
1077
|
+
info.rect = {
|
|
1078
|
+
x: Math.round(rect.x),
|
|
1079
|
+
y: Math.round(rect.y),
|
|
1080
|
+
width: Math.round(rect.width),
|
|
1081
|
+
height: Math.round(rect.height)
|
|
1082
|
+
};
|
|
1083
|
+
if (html) info.html = html;
|
|
1084
|
+
return info;
|
|
1085
|
+
}
|
|
1086
|
+
function matchesSelectors(element, selectors) {
|
|
1087
|
+
if (!selectors || selectors.length === 0) return false;
|
|
1088
|
+
return selectors.some((selector) => {
|
|
1089
|
+
try {
|
|
1090
|
+
return element.matches(selector) || element.closest(selector) !== null;
|
|
1091
|
+
} catch {
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
function captureElementVisualInfo(element) {
|
|
1097
|
+
try {
|
|
1098
|
+
if (typeof window === "undefined") return void 0;
|
|
1099
|
+
const htmlElement = element;
|
|
1100
|
+
const styles = window.getComputedStyle(htmlElement);
|
|
1101
|
+
return {
|
|
1102
|
+
backgroundColor: styles.backgroundColor,
|
|
1103
|
+
color: styles.color,
|
|
1104
|
+
fontSize: styles.fontSize,
|
|
1105
|
+
fontWeight: styles.fontWeight,
|
|
1106
|
+
borderRadius: styles.borderRadius,
|
|
1107
|
+
border: styles.border,
|
|
1108
|
+
boxShadow: styles.boxShadow,
|
|
1109
|
+
padding: styles.padding
|
|
1110
|
+
};
|
|
1111
|
+
} catch {
|
|
1112
|
+
return void 0;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
async function captureElementScreenshot(element, maxSize = 200) {
|
|
1116
|
+
try {
|
|
1117
|
+
if (typeof window === "undefined") return void 0;
|
|
1118
|
+
const html2canvas = window.html2canvas;
|
|
1119
|
+
if (!html2canvas) {
|
|
1120
|
+
try {
|
|
1121
|
+
const module2 = await import(
|
|
1122
|
+
/* webpackIgnore: true */
|
|
1123
|
+
"html2canvas"
|
|
1124
|
+
);
|
|
1125
|
+
const canvas2 = await module2.default(element, {
|
|
1126
|
+
scale: 0.5,
|
|
1127
|
+
width: maxSize,
|
|
1128
|
+
height: maxSize,
|
|
1129
|
+
useCORS: true,
|
|
1130
|
+
logging: false
|
|
1131
|
+
});
|
|
1132
|
+
return canvas2.toDataURL("image/jpeg", 0.7);
|
|
1133
|
+
} catch {
|
|
1134
|
+
console.debug("[UserFlow] html2canvas not available for screenshots");
|
|
1135
|
+
return void 0;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
const canvas = await html2canvas(element, {
|
|
1139
|
+
scale: 0.5,
|
|
1140
|
+
width: maxSize,
|
|
1141
|
+
height: maxSize,
|
|
1142
|
+
useCORS: true,
|
|
1143
|
+
logging: false
|
|
1144
|
+
});
|
|
1145
|
+
return canvas.toDataURL("image/jpeg", 0.7);
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
console.debug("[UserFlow] Failed to capture screenshot:", error);
|
|
1148
|
+
return void 0;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
function mergeConfig2(config) {
|
|
1152
|
+
const defaults = {
|
|
1153
|
+
enabled: false,
|
|
1154
|
+
samplingRate: 1,
|
|
1155
|
+
events: {
|
|
1156
|
+
clicks: true,
|
|
1157
|
+
navigation: true,
|
|
1158
|
+
formSubmit: true,
|
|
1159
|
+
inputs: false,
|
|
1160
|
+
scroll: false,
|
|
1161
|
+
ignoreSelectors: [],
|
|
1162
|
+
trackOnlySelectors: []
|
|
1163
|
+
},
|
|
1164
|
+
element: {
|
|
1165
|
+
captureVisualStyles: true,
|
|
1166
|
+
// Lightweight - enabled by default
|
|
1167
|
+
captureScreenshot: false,
|
|
1168
|
+
// Expensive - disabled by default
|
|
1169
|
+
screenshotMaxSize: 200,
|
|
1170
|
+
captureHtml: true,
|
|
1171
|
+
maxHtmlLength: 500,
|
|
1172
|
+
maxTextLength: 100,
|
|
1173
|
+
selectorDepth: 5
|
|
1174
|
+
},
|
|
1175
|
+
performance: {
|
|
1176
|
+
batchEvents: true,
|
|
1177
|
+
batchSize: 10,
|
|
1178
|
+
batchTimeout: 5e3,
|
|
1179
|
+
clickDebounce: 500
|
|
1180
|
+
},
|
|
1181
|
+
session: {
|
|
1182
|
+
endOnHidden: true,
|
|
1183
|
+
inactivityTimeout: 5 * 60 * 1e3,
|
|
1184
|
+
// 5 minutes
|
|
1185
|
+
endOnExternalNavigation: true,
|
|
1186
|
+
autoRestart: true
|
|
1187
|
+
},
|
|
1188
|
+
endpoint: "",
|
|
1189
|
+
onStart: () => {
|
|
1190
|
+
},
|
|
1191
|
+
onStop: () => {
|
|
1192
|
+
},
|
|
1193
|
+
onError: () => {
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
if (!config) return defaults;
|
|
1197
|
+
return {
|
|
1198
|
+
enabled: config.enabled ?? defaults.enabled,
|
|
1199
|
+
samplingRate: config.samplingRate ?? defaults.samplingRate,
|
|
1200
|
+
events: { ...defaults.events, ...config.events },
|
|
1201
|
+
element: { ...defaults.element, ...config.element },
|
|
1202
|
+
performance: { ...defaults.performance, ...config.performance },
|
|
1203
|
+
session: { ...defaults.session, ...config.session },
|
|
1204
|
+
endpoint: config.endpoint ?? defaults.endpoint,
|
|
1205
|
+
onStart: config.onStart ?? defaults.onStart,
|
|
1206
|
+
onStop: config.onStop ?? defaults.onStop,
|
|
1207
|
+
onError: config.onError ?? defaults.onError
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// src/user-flows/tracker.ts
|
|
1212
|
+
var UserFlowTracker = class {
|
|
1213
|
+
constructor(config) {
|
|
1214
|
+
this.state = "idle";
|
|
1215
|
+
this.sessionId = null;
|
|
1216
|
+
this.sdkSessionToken = null;
|
|
1217
|
+
this.apiUrl = "";
|
|
1218
|
+
this.sdkKey = null;
|
|
1219
|
+
this.user = null;
|
|
1220
|
+
// Tracking internals
|
|
1221
|
+
this.events = [];
|
|
1222
|
+
this.eventCount = 0;
|
|
1223
|
+
this.batchIndex = 0;
|
|
1224
|
+
this.batchesSent = 0;
|
|
1225
|
+
this.pageCount = 0;
|
|
1226
|
+
this.startTime = null;
|
|
1227
|
+
this.batchTimeout = null;
|
|
1228
|
+
this.inactivityTimeout = null;
|
|
1229
|
+
this.lastActivityTime = Date.now();
|
|
1230
|
+
this.lastClickTime = 0;
|
|
1231
|
+
this.lastClickElement = null;
|
|
1232
|
+
this.currentPath = "";
|
|
1233
|
+
this.visitedPaths = /* @__PURE__ */ new Set();
|
|
1234
|
+
// Event listeners
|
|
1235
|
+
this.clickHandler = null;
|
|
1236
|
+
this.navigationHandler = null;
|
|
1237
|
+
this.formSubmitHandler = null;
|
|
1238
|
+
this.beforeUnloadHandler = null;
|
|
1239
|
+
this.visibilityHandler = null;
|
|
1240
|
+
// Linked recording session ID (if rrweb is also recording)
|
|
1241
|
+
this.linkedRecordingSessionId = null;
|
|
1242
|
+
this.config = mergeConfig2(config);
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Initialize the tracker with SDK context
|
|
1246
|
+
*/
|
|
1247
|
+
initialize(sdkSessionToken, apiUrl, sdkKey) {
|
|
1248
|
+
this.sdkSessionToken = sdkSessionToken;
|
|
1249
|
+
this.apiUrl = apiUrl;
|
|
1250
|
+
this.sdkKey = sdkKey || null;
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Update configuration
|
|
1254
|
+
*/
|
|
1255
|
+
updateConfig(config) {
|
|
1256
|
+
this.config = mergeConfig2({ ...this.config, ...config });
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Identify the user
|
|
1260
|
+
*/
|
|
1261
|
+
identify(user) {
|
|
1262
|
+
this.user = user;
|
|
1263
|
+
if (this.state === "tracking" && this.sessionId) {
|
|
1264
|
+
this.sendIdentificationUpdate().catch((err) => {
|
|
1265
|
+
console.error("[UserFlow] Failed to send identification update:", err);
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Start tracking
|
|
1271
|
+
* @param linkedRecordingSessionId - Optional linked rrweb session ID for playback
|
|
1272
|
+
*/
|
|
1273
|
+
async start(linkedRecordingSessionId) {
|
|
1274
|
+
if (!isBrowser2()) {
|
|
1275
|
+
console.warn("[UserFlow] Cannot track outside browser environment");
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
if (!this.config.enabled) {
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
if (this.state === "tracking") {
|
|
1282
|
+
console.warn("[UserFlow] Already tracking");
|
|
1283
|
+
return this.sessionId;
|
|
1284
|
+
}
|
|
1285
|
+
if (!shouldTrackSession(this.config.samplingRate)) {
|
|
1286
|
+
console.log("[UserFlow] Session not sampled for tracking");
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
try {
|
|
1290
|
+
this.sessionId = generateSessionId2();
|
|
1291
|
+
this.linkedRecordingSessionId = linkedRecordingSessionId || null;
|
|
1292
|
+
this.events = [];
|
|
1293
|
+
this.eventCount = 0;
|
|
1294
|
+
this.batchIndex = 0;
|
|
1295
|
+
this.batchesSent = 0;
|
|
1296
|
+
this.pageCount = 1;
|
|
1297
|
+
this.startTime = /* @__PURE__ */ new Date();
|
|
1298
|
+
this.currentPath = getCurrentPath();
|
|
1299
|
+
this.visitedPaths = /* @__PURE__ */ new Set([this.currentPath]);
|
|
1300
|
+
this.state = "tracking";
|
|
1301
|
+
this.setupEventListeners();
|
|
1302
|
+
await this.sendSessionStart();
|
|
1303
|
+
this.addEvent({
|
|
1304
|
+
eventType: "navigation",
|
|
1305
|
+
eventOrder: 0,
|
|
1306
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1307
|
+
pageUrl: getCurrentUrl2(),
|
|
1308
|
+
pageTitle: getCurrentTitle(),
|
|
1309
|
+
pagePath: this.currentPath
|
|
1310
|
+
});
|
|
1311
|
+
this.config.onStart(this.sessionId);
|
|
1312
|
+
console.log("[UserFlow] Started tracking session:", this.sessionId);
|
|
1313
|
+
return this.sessionId;
|
|
1314
|
+
} catch (error) {
|
|
1315
|
+
console.error("[UserFlow] Failed to start tracking:", error);
|
|
1316
|
+
this.state = "idle";
|
|
1317
|
+
this.config.onError(error instanceof Error ? error : new Error(String(error)));
|
|
1318
|
+
return null;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Stop tracking
|
|
1323
|
+
*/
|
|
1324
|
+
async stop() {
|
|
1325
|
+
if (this.state !== "tracking" && this.state !== "paused") {
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
try {
|
|
1329
|
+
this.removeEventListeners();
|
|
1330
|
+
if (this.batchTimeout) {
|
|
1331
|
+
clearTimeout(this.batchTimeout);
|
|
1332
|
+
this.batchTimeout = null;
|
|
1333
|
+
}
|
|
1334
|
+
if (this.inactivityTimeout) {
|
|
1335
|
+
clearTimeout(this.inactivityTimeout);
|
|
1336
|
+
this.inactivityTimeout = null;
|
|
1337
|
+
}
|
|
1338
|
+
if (this.events.length > 0) {
|
|
1339
|
+
await this.sendEventBatch(true);
|
|
1340
|
+
}
|
|
1341
|
+
await this.sendSessionEnd();
|
|
1342
|
+
this.state = "stopped";
|
|
1343
|
+
const eventCount = this.eventCount;
|
|
1344
|
+
const sessionId = this.sessionId;
|
|
1345
|
+
this.sessionId = null;
|
|
1346
|
+
this.events = [];
|
|
1347
|
+
this.eventCount = 0;
|
|
1348
|
+
this.startTime = null;
|
|
1349
|
+
this.config.onStop(sessionId, eventCount);
|
|
1350
|
+
console.log("[UserFlow] Stopped tracking session:", sessionId);
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
console.error("[UserFlow] Error stopping tracking:", error);
|
|
1353
|
+
this.state = "stopped";
|
|
1354
|
+
this.config.onError(error instanceof Error ? error : new Error(String(error)));
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Pause tracking
|
|
1359
|
+
*/
|
|
1360
|
+
pause() {
|
|
1361
|
+
if (this.state === "tracking") {
|
|
1362
|
+
this.state = "paused";
|
|
1363
|
+
console.log("[UserFlow] Tracking paused");
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Resume tracking
|
|
1368
|
+
*/
|
|
1369
|
+
resume() {
|
|
1370
|
+
if (this.state === "paused") {
|
|
1371
|
+
this.state = "tracking";
|
|
1372
|
+
console.log("[UserFlow] Tracking resumed");
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Get tracker statistics
|
|
1377
|
+
*/
|
|
1378
|
+
getStats() {
|
|
1379
|
+
return {
|
|
1380
|
+
sessionId: this.sessionId,
|
|
1381
|
+
state: this.state,
|
|
1382
|
+
eventCount: this.eventCount,
|
|
1383
|
+
pageCount: this.pageCount,
|
|
1384
|
+
batchesSent: this.batchesSent,
|
|
1385
|
+
duration: this.startTime ? Date.now() - this.startTime.getTime() : 0,
|
|
1386
|
+
startTime: this.startTime
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Get tracking state
|
|
1391
|
+
*/
|
|
1392
|
+
getState() {
|
|
1393
|
+
return this.state;
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Check if currently tracking
|
|
1397
|
+
*/
|
|
1398
|
+
isTracking() {
|
|
1399
|
+
return this.state === "tracking";
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Add a custom event
|
|
1403
|
+
*/
|
|
1404
|
+
addCustomEvent(data) {
|
|
1405
|
+
if (this.state !== "tracking") return;
|
|
1406
|
+
this.addEvent({
|
|
1407
|
+
eventType: "custom",
|
|
1408
|
+
eventOrder: this.eventCount,
|
|
1409
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1410
|
+
pageUrl: getCurrentUrl2(),
|
|
1411
|
+
pageTitle: getCurrentTitle(),
|
|
1412
|
+
pagePath: getCurrentPath(),
|
|
1413
|
+
metadata: data
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
// === Private methods ===
|
|
1417
|
+
/**
|
|
1418
|
+
* Setup event listeners
|
|
1419
|
+
*/
|
|
1420
|
+
setupEventListeners() {
|
|
1421
|
+
if (!isBrowser2()) return;
|
|
1422
|
+
if (this.config.events.clicks) {
|
|
1423
|
+
this.clickHandler = (e) => this.handleClick(e);
|
|
1424
|
+
document.addEventListener("click", this.clickHandler, { capture: true, passive: true });
|
|
1425
|
+
}
|
|
1426
|
+
if (this.config.events.navigation) {
|
|
1427
|
+
this.navigationHandler = () => this.handleNavigation();
|
|
1428
|
+
window.addEventListener("popstate", this.navigationHandler);
|
|
1429
|
+
const originalPushState = history.pushState;
|
|
1430
|
+
const originalReplaceState = history.replaceState;
|
|
1431
|
+
history.pushState = (...args) => {
|
|
1432
|
+
originalPushState.apply(history, args);
|
|
1433
|
+
this.handleNavigation();
|
|
1434
|
+
};
|
|
1435
|
+
history.replaceState = (...args) => {
|
|
1436
|
+
originalReplaceState.apply(history, args);
|
|
1437
|
+
this.handleNavigation();
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
if (this.config.events.formSubmit) {
|
|
1441
|
+
this.formSubmitHandler = (e) => this.handleFormSubmit(e);
|
|
1442
|
+
document.addEventListener("submit", this.formSubmitHandler, { capture: true });
|
|
1443
|
+
}
|
|
1444
|
+
this.beforeUnloadHandler = () => {
|
|
1445
|
+
if (this.events.length > 0) {
|
|
1446
|
+
this.sendEventBatchSync(true);
|
|
1447
|
+
}
|
|
1448
|
+
this.sendSessionEndSync();
|
|
1449
|
+
};
|
|
1450
|
+
window.addEventListener("beforeunload", this.beforeUnloadHandler);
|
|
1451
|
+
this.visibilityHandler = () => {
|
|
1452
|
+
if (document.visibilityState === "hidden") {
|
|
1453
|
+
if (this.events.length > 0) {
|
|
1454
|
+
this.sendEventBatch(true).catch(console.error);
|
|
1455
|
+
}
|
|
1456
|
+
if (this.config.session.endOnHidden) {
|
|
1457
|
+
this.stop().catch(console.error);
|
|
1458
|
+
}
|
|
1459
|
+
} else if (document.visibilityState === "visible") {
|
|
1460
|
+
if (this.config.session.autoRestart && this.state === "stopped") {
|
|
1461
|
+
this.start().catch(console.error);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
document.addEventListener("visibilitychange", this.visibilityHandler);
|
|
1466
|
+
this.resetInactivityTimeout();
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Remove event listeners
|
|
1470
|
+
*/
|
|
1471
|
+
removeEventListeners() {
|
|
1472
|
+
if (!isBrowser2()) return;
|
|
1473
|
+
if (this.clickHandler) {
|
|
1474
|
+
document.removeEventListener("click", this.clickHandler, { capture: true });
|
|
1475
|
+
this.clickHandler = null;
|
|
1476
|
+
}
|
|
1477
|
+
if (this.navigationHandler) {
|
|
1478
|
+
window.removeEventListener("popstate", this.navigationHandler);
|
|
1479
|
+
this.navigationHandler = null;
|
|
1480
|
+
}
|
|
1481
|
+
if (this.formSubmitHandler) {
|
|
1482
|
+
document.removeEventListener("submit", this.formSubmitHandler, { capture: true });
|
|
1483
|
+
this.formSubmitHandler = null;
|
|
1484
|
+
}
|
|
1485
|
+
if (this.beforeUnloadHandler) {
|
|
1486
|
+
window.removeEventListener("beforeunload", this.beforeUnloadHandler);
|
|
1487
|
+
this.beforeUnloadHandler = null;
|
|
1488
|
+
}
|
|
1489
|
+
if (this.visibilityHandler) {
|
|
1490
|
+
document.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
1491
|
+
this.visibilityHandler = null;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Handle click event
|
|
1496
|
+
*/
|
|
1497
|
+
async handleClick(e) {
|
|
1498
|
+
if (this.state !== "tracking") return;
|
|
1499
|
+
const target = e.target;
|
|
1500
|
+
if (!target || !(target instanceof Element)) return;
|
|
1501
|
+
if (this.config.events.ignoreSelectors && this.config.events.ignoreSelectors.length > 0 && matchesSelectors(target, this.config.events.ignoreSelectors)) {
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
if (this.config.events.trackOnlySelectors && this.config.events.trackOnlySelectors.length > 0 && !matchesSelectors(target, this.config.events.trackOnlySelectors)) {
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
const now = Date.now();
|
|
1508
|
+
if (this.lastClickElement === target && now - this.lastClickTime < this.config.performance.clickDebounce) {
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
this.lastClickTime = now;
|
|
1512
|
+
this.lastClickElement = target;
|
|
1513
|
+
const elementInfo = getElementInfo(target, this.config.element);
|
|
1514
|
+
if (this.config.element.captureVisualStyles) {
|
|
1515
|
+
elementInfo.visualStyles = captureElementVisualInfo(target);
|
|
1516
|
+
}
|
|
1517
|
+
if (this.config.element.captureScreenshot) {
|
|
1518
|
+
try {
|
|
1519
|
+
elementInfo.screenshot = await captureElementScreenshot(
|
|
1520
|
+
target,
|
|
1521
|
+
this.config.element.screenshotMaxSize
|
|
1522
|
+
);
|
|
1523
|
+
} catch {
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
const event = {
|
|
1527
|
+
eventType: "click",
|
|
1528
|
+
eventOrder: this.eventCount,
|
|
1529
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1530
|
+
pageUrl: getCurrentUrl2(),
|
|
1531
|
+
pageTitle: getCurrentTitle(),
|
|
1532
|
+
pagePath: getCurrentPath(),
|
|
1533
|
+
element: elementInfo,
|
|
1534
|
+
clickX: Math.round(e.pageX),
|
|
1535
|
+
clickY: Math.round(e.pageY),
|
|
1536
|
+
viewportX: Math.round(e.clientX),
|
|
1537
|
+
viewportY: Math.round(e.clientY)
|
|
1538
|
+
};
|
|
1539
|
+
this.addEvent(event);
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Handle navigation event
|
|
1543
|
+
*/
|
|
1544
|
+
handleNavigation() {
|
|
1545
|
+
if (this.state !== "tracking") return;
|
|
1546
|
+
const newPath = getCurrentPath();
|
|
1547
|
+
if (newPath === this.currentPath) return;
|
|
1548
|
+
this.currentPath = newPath;
|
|
1549
|
+
if (!this.visitedPaths.has(newPath)) {
|
|
1550
|
+
this.visitedPaths.add(newPath);
|
|
1551
|
+
this.pageCount++;
|
|
1552
|
+
}
|
|
1553
|
+
const event = {
|
|
1554
|
+
eventType: "navigation",
|
|
1555
|
+
eventOrder: this.eventCount,
|
|
1556
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1557
|
+
pageUrl: getCurrentUrl2(),
|
|
1558
|
+
pageTitle: getCurrentTitle(),
|
|
1559
|
+
pagePath: newPath
|
|
1560
|
+
};
|
|
1561
|
+
this.addEvent(event);
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Handle form submit event
|
|
1565
|
+
*/
|
|
1566
|
+
handleFormSubmit(e) {
|
|
1567
|
+
if (this.state !== "tracking") return;
|
|
1568
|
+
const form = e.target;
|
|
1569
|
+
if (!form || !(form instanceof HTMLFormElement)) return;
|
|
1570
|
+
if (this.config.events.ignoreSelectors && matchesSelectors(form, this.config.events.ignoreSelectors)) {
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
const elementInfo = getElementInfo(form, this.config.element);
|
|
1574
|
+
const event = {
|
|
1575
|
+
eventType: "form_submit",
|
|
1576
|
+
eventOrder: this.eventCount,
|
|
1577
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1578
|
+
pageUrl: getCurrentUrl2(),
|
|
1579
|
+
pageTitle: getCurrentTitle(),
|
|
1580
|
+
pagePath: getCurrentPath(),
|
|
1581
|
+
element: elementInfo,
|
|
1582
|
+
metadata: {
|
|
1583
|
+
action: form.action,
|
|
1584
|
+
method: form.method,
|
|
1585
|
+
name: form.name
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
this.addEvent(event);
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Add event to queue
|
|
1592
|
+
*/
|
|
1593
|
+
addEvent(event) {
|
|
1594
|
+
this.events.push(event);
|
|
1595
|
+
this.eventCount++;
|
|
1596
|
+
this.resetInactivityTimeout();
|
|
1597
|
+
if (this.config.performance.batchEvents && this.events.length >= this.config.performance.batchSize) {
|
|
1598
|
+
this.sendEventBatch(false).catch(console.error);
|
|
1599
|
+
} else if (!this.batchTimeout) {
|
|
1600
|
+
this.batchTimeout = setTimeout(() => {
|
|
1601
|
+
this.batchTimeout = null;
|
|
1602
|
+
if (this.events.length > 0) {
|
|
1603
|
+
this.sendEventBatch(false).catch(console.error);
|
|
1604
|
+
}
|
|
1605
|
+
}, this.config.performance.batchTimeout);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Send session start to backend
|
|
1610
|
+
*/
|
|
1611
|
+
async sendSessionStart() {
|
|
1612
|
+
const endpoint = this.config.endpoint || `${this.apiUrl}/user-flows/session/start`;
|
|
1613
|
+
const device = getDeviceInfo2();
|
|
1614
|
+
const body = {
|
|
1615
|
+
sessionId: this.sessionId,
|
|
1616
|
+
sdkSessionToken: this.sdkSessionToken,
|
|
1617
|
+
startUrl: getCurrentUrl2(),
|
|
1618
|
+
startTime: this.startTime?.toISOString(),
|
|
1619
|
+
user: this.user,
|
|
1620
|
+
device,
|
|
1621
|
+
// Link to rrweb recording session if available
|
|
1622
|
+
linkedRecordingSessionId: this.linkedRecordingSessionId
|
|
1623
|
+
};
|
|
1624
|
+
const response = await fetch(endpoint, {
|
|
1625
|
+
method: "POST",
|
|
1626
|
+
headers: {
|
|
1627
|
+
"Content-Type": "application/json",
|
|
1628
|
+
...this.sdkKey ? { "X-SDK-Key": this.sdkKey } : {}
|
|
1629
|
+
},
|
|
1630
|
+
body: JSON.stringify(body)
|
|
1631
|
+
});
|
|
1632
|
+
if (!response.ok) {
|
|
1633
|
+
throw new Error(`Failed to start session: ${response.status}`);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Send event batch to backend
|
|
1638
|
+
*/
|
|
1639
|
+
async sendEventBatch(isFinal) {
|
|
1640
|
+
if (this.events.length === 0) return;
|
|
1641
|
+
const eventsToSend = [...this.events];
|
|
1642
|
+
this.events = [];
|
|
1643
|
+
const endpoint = this.config.endpoint ? `${this.config.endpoint}/events` : `${this.apiUrl}/user-flows/session/events`;
|
|
1644
|
+
const body = {
|
|
1645
|
+
sessionId: this.sessionId,
|
|
1646
|
+
events: eventsToSend,
|
|
1647
|
+
batchIndex: this.batchIndex++,
|
|
1648
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1649
|
+
isFinal
|
|
1650
|
+
};
|
|
1651
|
+
try {
|
|
1652
|
+
const response = await fetch(endpoint, {
|
|
1653
|
+
method: "POST",
|
|
1654
|
+
headers: {
|
|
1655
|
+
"Content-Type": "application/json",
|
|
1656
|
+
...this.sdkKey ? { "X-SDK-Key": this.sdkKey } : {}
|
|
1657
|
+
},
|
|
1658
|
+
body: JSON.stringify(body)
|
|
1659
|
+
});
|
|
1660
|
+
if (response.ok) {
|
|
1661
|
+
this.batchesSent++;
|
|
1662
|
+
}
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
console.error("[UserFlow] Failed to send event batch:", error);
|
|
1665
|
+
this.events = [...eventsToSend, ...this.events];
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Send event batch synchronously (for beforeunload)
|
|
1670
|
+
*/
|
|
1671
|
+
sendEventBatchSync(isFinal) {
|
|
1672
|
+
if (this.events.length === 0) return;
|
|
1673
|
+
const endpoint = this.config.endpoint ? `${this.config.endpoint}/events` : `${this.apiUrl}/user-flows/session/events`;
|
|
1674
|
+
const body = {
|
|
1675
|
+
sessionId: this.sessionId,
|
|
1676
|
+
events: this.events,
|
|
1677
|
+
batchIndex: this.batchIndex++,
|
|
1678
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1679
|
+
isFinal
|
|
1680
|
+
};
|
|
1681
|
+
if (navigator.sendBeacon) {
|
|
1682
|
+
const blob = new Blob([JSON.stringify(body)], { type: "application/json" });
|
|
1683
|
+
navigator.sendBeacon(endpoint, blob);
|
|
1684
|
+
this.events = [];
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1688
|
+
* Send session end to backend
|
|
1689
|
+
*/
|
|
1690
|
+
async sendSessionEnd() {
|
|
1691
|
+
const endpoint = this.config.endpoint ? `${this.config.endpoint}/end` : `${this.apiUrl}/user-flows/session/end`;
|
|
1692
|
+
const body = {
|
|
1693
|
+
sessionId: this.sessionId,
|
|
1694
|
+
endTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1695
|
+
eventCount: this.eventCount,
|
|
1696
|
+
status: "completed"
|
|
1697
|
+
};
|
|
1698
|
+
try {
|
|
1699
|
+
await fetch(endpoint, {
|
|
1700
|
+
method: "POST",
|
|
1701
|
+
headers: {
|
|
1702
|
+
"Content-Type": "application/json",
|
|
1703
|
+
...this.sdkKey ? { "X-SDK-Key": this.sdkKey } : {}
|
|
1704
|
+
},
|
|
1705
|
+
body: JSON.stringify(body)
|
|
1706
|
+
});
|
|
1707
|
+
} catch (error) {
|
|
1708
|
+
console.error("[UserFlow] Failed to send session end:", error);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* Send session end synchronously (for beforeunload)
|
|
1713
|
+
*/
|
|
1714
|
+
sendSessionEndSync() {
|
|
1715
|
+
const endpoint = this.config.endpoint ? `${this.config.endpoint}/end` : `${this.apiUrl}/user-flows/session/end`;
|
|
1716
|
+
const body = {
|
|
1717
|
+
sessionId: this.sessionId,
|
|
1718
|
+
endTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1719
|
+
eventCount: this.eventCount,
|
|
1720
|
+
status: "completed"
|
|
1721
|
+
};
|
|
1722
|
+
if (navigator.sendBeacon) {
|
|
1723
|
+
const blob = new Blob([JSON.stringify(body)], { type: "application/json" });
|
|
1724
|
+
navigator.sendBeacon(endpoint, blob);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Reset inactivity timeout
|
|
1729
|
+
* Called on each user interaction
|
|
1730
|
+
*/
|
|
1731
|
+
resetInactivityTimeout() {
|
|
1732
|
+
if (this.inactivityTimeout) {
|
|
1733
|
+
clearTimeout(this.inactivityTimeout);
|
|
1734
|
+
this.inactivityTimeout = null;
|
|
1735
|
+
}
|
|
1736
|
+
this.lastActivityTime = Date.now();
|
|
1737
|
+
const timeout = this.config.session.inactivityTimeout;
|
|
1738
|
+
if (timeout && timeout > 0) {
|
|
1739
|
+
this.inactivityTimeout = setTimeout(() => {
|
|
1740
|
+
if (this.state === "tracking") {
|
|
1741
|
+
console.log("[UserFlow] Session ended due to inactivity");
|
|
1742
|
+
this.stop().catch(console.error);
|
|
1743
|
+
}
|
|
1744
|
+
}, timeout);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Send identification update to backend
|
|
1749
|
+
*/
|
|
1750
|
+
async sendIdentificationUpdate() {
|
|
1751
|
+
if (!this.sessionId || !this.user) return;
|
|
1752
|
+
const endpoint = this.config.endpoint ? `${this.config.endpoint}/identify` : `${this.apiUrl}/user-flows/session/identify`;
|
|
1753
|
+
const body = {
|
|
1754
|
+
sessionId: this.sessionId,
|
|
1755
|
+
user: this.user
|
|
1756
|
+
};
|
|
1757
|
+
await fetch(endpoint, {
|
|
1758
|
+
method: "PATCH",
|
|
1759
|
+
headers: {
|
|
1760
|
+
"Content-Type": "application/json",
|
|
1761
|
+
...this.sdkKey ? { "X-SDK-Key": this.sdkKey } : {}
|
|
1762
|
+
},
|
|
1763
|
+
body: JSON.stringify(body)
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
};
|
|
1767
|
+
function createUserFlowTracker(config) {
|
|
1768
|
+
return new UserFlowTracker(config);
|
|
1769
|
+
}
|
|
1770
|
+
|
|
28
1771
|
// src/core.ts
|
|
29
1772
|
var ProduckSDK = class {
|
|
30
1773
|
constructor(config) {
|
|
@@ -32,6 +1775,12 @@ var ProduckSDK = class {
|
|
|
32
1775
|
this.eventListeners = /* @__PURE__ */ new Map();
|
|
33
1776
|
this.sessionToken = null;
|
|
34
1777
|
this.isReady = false;
|
|
1778
|
+
/** Session recorder instance (optional, isolated module) */
|
|
1779
|
+
this.recorder = null;
|
|
1780
|
+
/** Proactive behavior tracker instance (optional, isolated module) */
|
|
1781
|
+
this.proactiveTracker = null;
|
|
1782
|
+
/** User flow tracker instance (optional, isolated module) */
|
|
1783
|
+
this.userFlowTracker = null;
|
|
35
1784
|
let apiUrl = config.apiUrl;
|
|
36
1785
|
if (!apiUrl) {
|
|
37
1786
|
if (typeof window !== "undefined") {
|
|
@@ -82,7 +1831,19 @@ var ProduckSDK = class {
|
|
|
82
1831
|
this.sessionToken = session.session_token;
|
|
83
1832
|
this.isReady = true;
|
|
84
1833
|
await this.log("info", "SDK initialized successfully", { hasSessionToken: !!this.sessionToken });
|
|
1834
|
+
if (this.config.recording?.enabled) {
|
|
1835
|
+
await this.initRecording();
|
|
1836
|
+
}
|
|
1837
|
+
if (this.config.proactive?.enabled) {
|
|
1838
|
+
this.initProactive();
|
|
1839
|
+
}
|
|
1840
|
+
if (this.config.userFlows?.enabled) {
|
|
1841
|
+
await this.initUserFlowTracking();
|
|
1842
|
+
}
|
|
85
1843
|
this.emit("ready", { sessionToken: this.sessionToken });
|
|
1844
|
+
if (this.config.initialMessage) {
|
|
1845
|
+
await this.sendInitialMessage();
|
|
1846
|
+
}
|
|
86
1847
|
} catch (error) {
|
|
87
1848
|
await this.log("error", "SDK initialization failed", { error: error instanceof Error ? error.message : String(error) });
|
|
88
1849
|
this.emit("error", error);
|
|
@@ -196,9 +1957,14 @@ var ProduckSDK = class {
|
|
|
196
1957
|
const result = await sessionResponse.json();
|
|
197
1958
|
const chatMessage = {
|
|
198
1959
|
role: "assistant",
|
|
199
|
-
content: result.message?.content || result.response || ""
|
|
1960
|
+
content: result.message?.content || result.response || "",
|
|
1961
|
+
visualFlows: result.flows || [],
|
|
1962
|
+
images: result.images || []
|
|
200
1963
|
};
|
|
201
|
-
await this.log("info", "Chat response received"
|
|
1964
|
+
await this.log("info", "Chat response received", {
|
|
1965
|
+
hasFlows: (result.flows || []).length > 0,
|
|
1966
|
+
hasImages: (result.images || []).length > 0
|
|
1967
|
+
});
|
|
202
1968
|
this.emit("message", chatMessage);
|
|
203
1969
|
return chatMessage;
|
|
204
1970
|
} catch (error) {
|
|
@@ -488,10 +2254,387 @@ var ProduckSDK = class {
|
|
|
488
2254
|
getRegisteredActions() {
|
|
489
2255
|
return Array.from(this.actions.keys());
|
|
490
2256
|
}
|
|
2257
|
+
/**
|
|
2258
|
+
* Get guider ID for the current session
|
|
2259
|
+
*/
|
|
2260
|
+
getGuiderId() {
|
|
2261
|
+
return this.config.guiderId;
|
|
2262
|
+
}
|
|
2263
|
+
/**
|
|
2264
|
+
* Fetch full visual flow details by flow ID
|
|
2265
|
+
*/
|
|
2266
|
+
async getVisualFlow(flowId) {
|
|
2267
|
+
const guiderId = this.config.guiderId;
|
|
2268
|
+
if (!guiderId) {
|
|
2269
|
+
await this.log("warn", "Cannot fetch visual flow without guiderId");
|
|
2270
|
+
return null;
|
|
2271
|
+
}
|
|
2272
|
+
try {
|
|
2273
|
+
const response = await fetch(
|
|
2274
|
+
`${this.config.apiUrl}/visual-flows/chat/${guiderId}/${flowId}`
|
|
2275
|
+
);
|
|
2276
|
+
if (!response.ok) {
|
|
2277
|
+
await this.log("warn", "Visual flow not found", { flowId, status: response.status });
|
|
2278
|
+
return null;
|
|
2279
|
+
}
|
|
2280
|
+
const result = await response.json();
|
|
2281
|
+
if (!result.found || !result.flow) {
|
|
2282
|
+
return null;
|
|
2283
|
+
}
|
|
2284
|
+
return result.flow;
|
|
2285
|
+
} catch (error) {
|
|
2286
|
+
await this.log("error", "Error fetching visual flow", { flowId, error });
|
|
2287
|
+
return null;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
// ============================================================
|
|
2291
|
+
// Session Recording Methods (Isolated Module)
|
|
2292
|
+
// These methods delegate to the SessionRecorder when enabled
|
|
2293
|
+
// ============================================================
|
|
2294
|
+
/**
|
|
2295
|
+
* Initialize session recording (internal)
|
|
2296
|
+
*/
|
|
2297
|
+
async initRecording() {
|
|
2298
|
+
try {
|
|
2299
|
+
this.recorder = new SessionRecorder(this.config.recording);
|
|
2300
|
+
this.recorder.initialize(
|
|
2301
|
+
this.sessionToken,
|
|
2302
|
+
this.config.apiUrl,
|
|
2303
|
+
this.config.sdkKey
|
|
2304
|
+
);
|
|
2305
|
+
const sessionId = await this.recorder.start();
|
|
2306
|
+
if (sessionId) {
|
|
2307
|
+
await this.log("info", "Session recording started", { recordingSessionId: sessionId });
|
|
2308
|
+
}
|
|
2309
|
+
} catch (error) {
|
|
2310
|
+
await this.log("warn", "Failed to initialize session recording", {
|
|
2311
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
/**
|
|
2316
|
+
* Start session recording manually (if not auto-started)
|
|
2317
|
+
*/
|
|
2318
|
+
async startRecording() {
|
|
2319
|
+
if (!this.isReady) {
|
|
2320
|
+
console.warn("[SDK] Cannot start recording before SDK is initialized");
|
|
2321
|
+
return null;
|
|
2322
|
+
}
|
|
2323
|
+
if (!this.recorder) {
|
|
2324
|
+
this.recorder = new SessionRecorder(this.config.recording);
|
|
2325
|
+
this.recorder.initialize(
|
|
2326
|
+
this.sessionToken,
|
|
2327
|
+
this.config.apiUrl,
|
|
2328
|
+
this.config.sdkKey
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
return this.recorder.start();
|
|
2332
|
+
}
|
|
2333
|
+
/**
|
|
2334
|
+
* Stop session recording
|
|
2335
|
+
*/
|
|
2336
|
+
async stopRecording() {
|
|
2337
|
+
if (this.recorder) {
|
|
2338
|
+
await this.recorder.stop();
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Pause session recording
|
|
2343
|
+
*/
|
|
2344
|
+
pauseRecording() {
|
|
2345
|
+
this.recorder?.pause();
|
|
2346
|
+
}
|
|
2347
|
+
/**
|
|
2348
|
+
* Resume session recording
|
|
2349
|
+
*/
|
|
2350
|
+
resumeRecording() {
|
|
2351
|
+
this.recorder?.resume();
|
|
2352
|
+
}
|
|
2353
|
+
/**
|
|
2354
|
+
* Add a custom event to the recording
|
|
2355
|
+
*/
|
|
2356
|
+
addRecordingEvent(event) {
|
|
2357
|
+
this.recorder?.addCustomEvent(event);
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Get recording statistics
|
|
2361
|
+
*/
|
|
2362
|
+
getRecordingStats() {
|
|
2363
|
+
return this.recorder?.getStats() ?? null;
|
|
2364
|
+
}
|
|
2365
|
+
/**
|
|
2366
|
+
* Check if session recording is active
|
|
2367
|
+
*/
|
|
2368
|
+
isRecordingActive() {
|
|
2369
|
+
return this.recorder?.isRecording() ?? false;
|
|
2370
|
+
}
|
|
2371
|
+
/**
|
|
2372
|
+
* Get recording session ID
|
|
2373
|
+
*/
|
|
2374
|
+
getRecordingSessionId() {
|
|
2375
|
+
return this.recorder?.getSessionId() ?? null;
|
|
2376
|
+
}
|
|
2377
|
+
// ============================================================
|
|
2378
|
+
// Proactive Chat Methods (Isolated Module)
|
|
2379
|
+
// These methods manage proactive behavior tracking and messaging
|
|
2380
|
+
// ============================================================
|
|
2381
|
+
/**
|
|
2382
|
+
* Initialize proactive behavior tracking (internal)
|
|
2383
|
+
*/
|
|
2384
|
+
initProactive() {
|
|
2385
|
+
if (!this.config.proactive) return;
|
|
2386
|
+
this.proactiveTracker = new ProactiveBehaviorTracker(
|
|
2387
|
+
this.config.proactive,
|
|
2388
|
+
{
|
|
2389
|
+
onTrigger: async (context, suggestedMessage) => {
|
|
2390
|
+
this.emit("proactive", { context, message: suggestedMessage });
|
|
2391
|
+
if (this.config.onProactiveMessage) {
|
|
2392
|
+
this.config.onProactiveMessage(suggestedMessage, context);
|
|
2393
|
+
}
|
|
2394
|
+
try {
|
|
2395
|
+
await this.getProactiveAIMessage(context, suggestedMessage);
|
|
2396
|
+
} catch (error) {
|
|
2397
|
+
await this.log("warn", "Failed to get AI proactive message", { error });
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
);
|
|
2402
|
+
this.proactiveTracker.start();
|
|
2403
|
+
this.log("info", "Proactive behavior tracking initialized");
|
|
2404
|
+
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Get AI-generated proactive message based on context
|
|
2407
|
+
*/
|
|
2408
|
+
async getProactiveAIMessage(context, fallbackMessage) {
|
|
2409
|
+
try {
|
|
2410
|
+
const headers = { "Content-Type": "application/json" };
|
|
2411
|
+
if (this.config.sdkKey) {
|
|
2412
|
+
headers["X-SDK-Key"] = this.config.sdkKey;
|
|
2413
|
+
}
|
|
2414
|
+
const response = await fetch(`${this.config.apiUrl}/sdk/proactive-message`, {
|
|
2415
|
+
method: "POST",
|
|
2416
|
+
headers,
|
|
2417
|
+
body: JSON.stringify({
|
|
2418
|
+
context,
|
|
2419
|
+
fallbackMessage,
|
|
2420
|
+
sessionToken: this.sessionToken
|
|
2421
|
+
})
|
|
2422
|
+
});
|
|
2423
|
+
if (!response.ok) {
|
|
2424
|
+
return fallbackMessage;
|
|
2425
|
+
}
|
|
2426
|
+
const result = await response.json();
|
|
2427
|
+
const message = result.message || fallbackMessage;
|
|
2428
|
+
const chatMessage = {
|
|
2429
|
+
role: "assistant",
|
|
2430
|
+
content: message
|
|
2431
|
+
};
|
|
2432
|
+
this.emit("message", chatMessage);
|
|
2433
|
+
return message;
|
|
2434
|
+
} catch (error) {
|
|
2435
|
+
return fallbackMessage;
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2439
|
+
* Send initial message for landing page intros
|
|
2440
|
+
*/
|
|
2441
|
+
async sendInitialMessage() {
|
|
2442
|
+
const message = this.config.initialMessage;
|
|
2443
|
+
if (!message) return;
|
|
2444
|
+
if (this.config.streamInitialMessage) {
|
|
2445
|
+
await this.streamMessage(message);
|
|
2446
|
+
} else {
|
|
2447
|
+
const chatMessage = {
|
|
2448
|
+
role: "assistant",
|
|
2449
|
+
content: message
|
|
2450
|
+
};
|
|
2451
|
+
this.emit("message", chatMessage);
|
|
2452
|
+
if (this.config.onMessage) {
|
|
2453
|
+
this.config.onMessage(chatMessage);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Stream a message character by character for typewriter effect
|
|
2459
|
+
*/
|
|
2460
|
+
async streamMessage(message, delayMs = 30) {
|
|
2461
|
+
let currentContent = "";
|
|
2462
|
+
for (let i = 0; i < message.length; i++) {
|
|
2463
|
+
currentContent += message[i];
|
|
2464
|
+
const chatMessage = {
|
|
2465
|
+
role: "assistant",
|
|
2466
|
+
content: currentContent
|
|
2467
|
+
};
|
|
2468
|
+
this.emit("message", chatMessage);
|
|
2469
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
/**
|
|
2473
|
+
* Start proactive tracking manually
|
|
2474
|
+
*/
|
|
2475
|
+
startProactiveTracking() {
|
|
2476
|
+
if (this.proactiveTracker) {
|
|
2477
|
+
this.proactiveTracker.start();
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
/**
|
|
2481
|
+
* Stop proactive tracking
|
|
2482
|
+
*/
|
|
2483
|
+
stopProactiveTracking() {
|
|
2484
|
+
if (this.proactiveTracker) {
|
|
2485
|
+
this.proactiveTracker.stop();
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* Get proactive tracking stats
|
|
2490
|
+
*/
|
|
2491
|
+
getProactiveStats() {
|
|
2492
|
+
return this.proactiveTracker?.getStats() ?? null;
|
|
2493
|
+
}
|
|
2494
|
+
/**
|
|
2495
|
+
* Manually trigger a proactive message
|
|
2496
|
+
*/
|
|
2497
|
+
triggerProactiveMessage(message, context) {
|
|
2498
|
+
if (this.proactiveTracker) {
|
|
2499
|
+
this.proactiveTracker.triggerManual(message, context);
|
|
2500
|
+
} else {
|
|
2501
|
+
const chatMessage = {
|
|
2502
|
+
role: "assistant",
|
|
2503
|
+
content: message
|
|
2504
|
+
};
|
|
2505
|
+
this.emit("message", chatMessage);
|
|
2506
|
+
if (this.config.onMessage) {
|
|
2507
|
+
this.config.onMessage(chatMessage);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
/**
|
|
2512
|
+
* Opt out of proactive messages (respects user preference)
|
|
2513
|
+
*/
|
|
2514
|
+
optOutProactive() {
|
|
2515
|
+
this.proactiveTracker?.optOut();
|
|
2516
|
+
}
|
|
2517
|
+
/**
|
|
2518
|
+
* Opt in to proactive messages
|
|
2519
|
+
*/
|
|
2520
|
+
optInProactive() {
|
|
2521
|
+
this.proactiveTracker?.optIn();
|
|
2522
|
+
}
|
|
2523
|
+
// ============================================================
|
|
2524
|
+
// User Flow Tracking Methods (Isolated Module)
|
|
2525
|
+
// These methods manage user interaction tracking and flow analysis
|
|
2526
|
+
// ============================================================
|
|
2527
|
+
/**
|
|
2528
|
+
* Initialize user flow tracking (internal)
|
|
2529
|
+
*/
|
|
2530
|
+
async initUserFlowTracking() {
|
|
2531
|
+
try {
|
|
2532
|
+
this.userFlowTracker = new UserFlowTracker(this.config.userFlows);
|
|
2533
|
+
this.userFlowTracker.initialize(
|
|
2534
|
+
this.sessionToken,
|
|
2535
|
+
this.config.apiUrl,
|
|
2536
|
+
this.config.sdkKey
|
|
2537
|
+
);
|
|
2538
|
+
const recordingSessionId = this.recorder?.getSessionId?.() || null;
|
|
2539
|
+
const sessionId = await this.userFlowTracker.start(recordingSessionId);
|
|
2540
|
+
if (sessionId) {
|
|
2541
|
+
await this.log("info", "User flow tracking started", {
|
|
2542
|
+
flowSessionId: sessionId,
|
|
2543
|
+
linkedRecordingSession: recordingSessionId
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2546
|
+
} catch (error) {
|
|
2547
|
+
await this.log("warn", "Failed to initialize user flow tracking", {
|
|
2548
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
/**
|
|
2553
|
+
* Identify the user for flow tracking and segmentation
|
|
2554
|
+
* Call this when you know who the user is (e.g., after login)
|
|
2555
|
+
*/
|
|
2556
|
+
identify(user) {
|
|
2557
|
+
if (this.userFlowTracker) {
|
|
2558
|
+
this.userFlowTracker.identify(user);
|
|
2559
|
+
}
|
|
2560
|
+
this.log("info", "User identified", {
|
|
2561
|
+
hasEmail: !!user.email,
|
|
2562
|
+
hasName: !!user.name,
|
|
2563
|
+
hasTags: !!user.tags && Object.keys(user.tags).length > 0
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
/**
|
|
2567
|
+
* Start user flow tracking manually (if not auto-started)
|
|
2568
|
+
*/
|
|
2569
|
+
async startUserFlowTracking() {
|
|
2570
|
+
if (!this.isReady) {
|
|
2571
|
+
console.warn("[SDK] Cannot start flow tracking before SDK is initialized");
|
|
2572
|
+
return null;
|
|
2573
|
+
}
|
|
2574
|
+
if (!this.userFlowTracker) {
|
|
2575
|
+
this.userFlowTracker = new UserFlowTracker(this.config.userFlows);
|
|
2576
|
+
this.userFlowTracker.initialize(
|
|
2577
|
+
this.sessionToken,
|
|
2578
|
+
this.config.apiUrl,
|
|
2579
|
+
this.config.sdkKey
|
|
2580
|
+
);
|
|
2581
|
+
}
|
|
2582
|
+
return this.userFlowTracker.start();
|
|
2583
|
+
}
|
|
2584
|
+
/**
|
|
2585
|
+
* Stop user flow tracking
|
|
2586
|
+
*/
|
|
2587
|
+
async stopUserFlowTracking() {
|
|
2588
|
+
if (this.userFlowTracker) {
|
|
2589
|
+
await this.userFlowTracker.stop();
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
/**
|
|
2593
|
+
* Pause user flow tracking
|
|
2594
|
+
*/
|
|
2595
|
+
pauseUserFlowTracking() {
|
|
2596
|
+
this.userFlowTracker?.pause();
|
|
2597
|
+
}
|
|
2598
|
+
/**
|
|
2599
|
+
* Resume user flow tracking
|
|
2600
|
+
*/
|
|
2601
|
+
resumeUserFlowTracking() {
|
|
2602
|
+
this.userFlowTracker?.resume();
|
|
2603
|
+
}
|
|
2604
|
+
/**
|
|
2605
|
+
* Add a custom event to the user flow
|
|
2606
|
+
*/
|
|
2607
|
+
trackEvent(eventData) {
|
|
2608
|
+
this.userFlowTracker?.addCustomEvent(eventData);
|
|
2609
|
+
}
|
|
2610
|
+
/**
|
|
2611
|
+
* Get user flow tracking statistics
|
|
2612
|
+
*/
|
|
2613
|
+
getUserFlowStats() {
|
|
2614
|
+
return this.userFlowTracker?.getStats() ?? null;
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Check if user flow tracking is active
|
|
2618
|
+
*/
|
|
2619
|
+
isUserFlowTrackingActive() {
|
|
2620
|
+
return this.userFlowTracker?.isTracking() ?? false;
|
|
2621
|
+
}
|
|
491
2622
|
/**
|
|
492
2623
|
* Destroy the SDK instance
|
|
493
2624
|
*/
|
|
494
2625
|
destroy() {
|
|
2626
|
+
if (this.recorder) {
|
|
2627
|
+
this.recorder.destroy();
|
|
2628
|
+
this.recorder = null;
|
|
2629
|
+
}
|
|
2630
|
+
if (this.proactiveTracker) {
|
|
2631
|
+
this.proactiveTracker.stop();
|
|
2632
|
+
this.proactiveTracker = null;
|
|
2633
|
+
}
|
|
2634
|
+
if (this.userFlowTracker) {
|
|
2635
|
+
this.userFlowTracker.stop();
|
|
2636
|
+
this.userFlowTracker = null;
|
|
2637
|
+
}
|
|
495
2638
|
this.actions.clear();
|
|
496
2639
|
this.eventListeners.clear();
|
|
497
2640
|
this.sessionToken = null;
|
|
@@ -503,7 +2646,13 @@ function createProduck(config) {
|
|
|
503
2646
|
}
|
|
504
2647
|
// Annotate the CommonJS export names for ESM import in node:
|
|
505
2648
|
0 && (module.exports = {
|
|
2649
|
+
ProactiveBehaviorTracker,
|
|
506
2650
|
ProduckSDK,
|
|
507
|
-
|
|
2651
|
+
SessionRecorder,
|
|
2652
|
+
UserFlowTracker,
|
|
2653
|
+
createProduck,
|
|
2654
|
+
createSessionRecorder,
|
|
2655
|
+
createUserFlowTracker,
|
|
2656
|
+
defaultProactiveConfig
|
|
508
2657
|
});
|
|
509
2658
|
//# sourceMappingURL=index.js.map
|