@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.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
- createProduck: () => createProduck
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
- createProduck
2651
+ SessionRecorder,
2652
+ UserFlowTracker,
2653
+ createProduck,
2654
+ createSessionRecorder,
2655
+ createUserFlowTracker,
2656
+ defaultProactiveConfig
508
2657
  });
509
2658
  //# sourceMappingURL=index.js.map