@prodact.ai/sdk 0.0.2 → 0.0.5

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