@guidelytech/react-native 0.12.0 → 0.14.0

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.cjs CHANGED
@@ -37,12 +37,17 @@ __export(index_exports, {
37
37
  InMemoryStickyBucketStore: () => import_flags6.InMemoryStickyBucketStore,
38
38
  Messenger: () => Messenger,
39
39
  RageClickDetector: () => RageClickDetector,
40
+ ReplayUploader: () => ReplayUploader,
41
+ RrwebEvents: () => RrwebEvents,
40
42
  SDK_NAME: () => SDK_NAME,
41
43
  SDK_VERSION: () => SDK_VERSION,
44
+ SessionRecorder: () => SessionRecorder,
42
45
  TapTracker: () => TapTracker,
43
46
  TargetRegistry: () => TargetRegistry,
44
47
  createFlagsClient: () => import_flags6.createFlagsClient,
45
48
  createSseParser: () => createSseParser,
49
+ detectAppVersion: () => detectAppVersion,
50
+ detectOsVersion: () => detectOsVersion,
46
51
  guidelyScreenLayout: () => guidelyScreenLayout,
47
52
  htmlToText: () => htmlToText,
48
53
  noopClient: () => noopClient,
@@ -1442,14 +1447,14 @@ function quoteValue(value) {
1442
1447
  function attrSelector(name, value) {
1443
1448
  return `[${name}=${quoteValue(value)}]`.slice(0, 300);
1444
1449
  }
1445
- function cleanText(text) {
1446
- const t = (text ?? "").replace(/\s+/g, " ").trim();
1450
+ function cleanText(text2) {
1451
+ const t = (text2 ?? "").replace(/\s+/g, " ").trim();
1447
1452
  return t ? t.slice(0, 100) : void 0;
1448
1453
  }
1449
1454
  function tapProperties(el, x, y, platform, maskAll) {
1450
1455
  const props = { selector: el.selector.slice(0, 300), tag: el.tag || "View", x: Math.round(x) || 0, y: Math.round(y) || 0, platform };
1451
- const text = maskAll ? void 0 : cleanText(el.text);
1452
- if (text) props.text = text;
1456
+ const text2 = maskAll ? void 0 : cleanText(el.text);
1457
+ if (text2) props.text = text2;
1453
1458
  return props;
1454
1459
  }
1455
1460
  var r2 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 100) / 100;
@@ -2070,10 +2075,10 @@ function xhrStreamPost() {
2070
2075
  const type = xhr.getResponseHeader("content-type") || "";
2071
2076
  if (!type.includes("text/event-stream")) return;
2072
2077
  streamed = true;
2073
- const text = xhr.responseText || "";
2074
- if (text.length > seen) {
2075
- onChunk(text.slice(seen));
2076
- seen = text.length;
2078
+ const text2 = xhr.responseText || "";
2079
+ if (text2.length > seen) {
2080
+ onChunk(text2.slice(seen));
2081
+ seen = text2.length;
2077
2082
  }
2078
2083
  };
2079
2084
  xhr.open("POST", url);
@@ -2148,14 +2153,14 @@ function fetchStreamPost(fetchImpl) {
2148
2153
  if (rest) onChunk(rest);
2149
2154
  return { status: res.status, json: null, streamed: true };
2150
2155
  }
2151
- const text = await race(res.text());
2156
+ const text2 = await race(res.text());
2152
2157
  if (sse) {
2153
- onChunk(text);
2158
+ onChunk(text2);
2154
2159
  return { status: res.status, json: null, streamed: true };
2155
2160
  }
2156
2161
  let json = null;
2157
2162
  try {
2158
- json = JSON.parse(text || "null");
2163
+ json = JSON.parse(text2 || "null");
2159
2164
  } catch {
2160
2165
  json = null;
2161
2166
  }
@@ -2268,10 +2273,10 @@ var Messenger = class {
2268
2273
  if (v.kind === "tab" && v.tab === "messages") void this.refreshConversations();
2269
2274
  }
2270
2275
  /** Opens a new thread (optionally pre-filling the composer). */
2271
- showNewMessage(text = "") {
2276
+ showNewMessage(text2 = "") {
2272
2277
  this.stopThreadPolling();
2273
2278
  if (!this.snapshot.open) this.deps.onOpen();
2274
- this.set({ open: true, view: { kind: "thread", conversationId: null }, thread: { conversation: null, messages: [], streaming: {}, sending: false, error: null, draft: text } });
2279
+ this.set({ open: true, view: { kind: "thread", conversationId: null }, thread: { conversation: null, messages: [], streaming: {}, sending: false, error: null, draft: text2 } });
2275
2280
  }
2276
2281
  async openConversation(id) {
2277
2282
  const known = this.snapshot.conversations.find((c) => c.id === id) ?? null;
@@ -2315,8 +2320,8 @@ var Messenger = class {
2315
2320
  }
2316
2321
  }
2317
2322
  /** Sends the composer text: starts a conversation or posts to the open one. Streams Scout's reply. */
2318
- async send(text) {
2319
- const body = text.trim().slice(0, 4e3);
2323
+ async send(text2) {
2324
+ const body = text2.trim().slice(0, 4e3);
2320
2325
  const thread = this.snapshot.thread;
2321
2326
  if (!body || !thread || thread.sending) return;
2322
2327
  this.setThread({ sending: true, error: null, draft: "" });
@@ -2492,6 +2497,425 @@ var Messenger = class {
2492
2497
  }
2493
2498
  };
2494
2499
 
2500
+ // src/core/replay.ts
2501
+ var RrwebEvents = {
2502
+ IMG_ID: 8,
2503
+ TOUCH_START: 7,
2504
+ TOUCH_END: 9,
2505
+ CLICK: 2,
2506
+ CSS: "html,body{margin:0;padding:0;width:100%;height:100%;overflow:hidden;background:#000}img{display:block;width:100%;height:100%;object-fit:contain}",
2507
+ dataUrl(base64) {
2508
+ return `data:image/jpeg;base64,${base64}`;
2509
+ },
2510
+ meta(timestamp, href, width, height) {
2511
+ return JSON.stringify({ type: 4, data: { href, width, height }, timestamp });
2512
+ },
2513
+ fullSnapshot(timestamp, dataUrl, width, height) {
2514
+ const style = { type: 2, id: 5, tagName: "style", attributes: {}, childNodes: [{ type: 3, id: 6, textContent: RrwebEvents.CSS, isStyle: true }] };
2515
+ const img = { type: 2, id: 8, tagName: "img", attributes: { src: dataUrl, width, height, alt: "" }, childNodes: [] };
2516
+ const html = { type: 2, id: 3, tagName: "html", attributes: {}, childNodes: [{ type: 2, id: 4, tagName: "head", attributes: {}, childNodes: [style] }, { type: 2, id: 7, tagName: "body", attributes: {}, childNodes: [img] }] };
2517
+ const node = { type: 0, id: 1, childNodes: [{ type: 1, id: 2, name: "html", publicId: "", systemId: "" }, html] };
2518
+ return JSON.stringify({ type: 2, data: { node, initialOffset: { left: 0, top: 0 } }, timestamp });
2519
+ },
2520
+ frame(timestamp, dataUrl) {
2521
+ return JSON.stringify({ type: 3, data: { source: 0, texts: [], attributes: [{ id: 8, attributes: { src: dataUrl } }], removes: [], adds: [] }, timestamp });
2522
+ },
2523
+ /** MouseInteraction (source 2): 7 TouchStart, 9 TouchEnd, 2 Click. */
2524
+ interaction(timestamp, type, x, y) {
2525
+ return JSON.stringify({ type: 3, data: { source: 2, type, id: 8, x: round1(x), y: round1(y) }, timestamp });
2526
+ },
2527
+ touchMove(timestamp, x, y) {
2528
+ return JSON.stringify({ type: 3, data: { source: 6, positions: [{ x: round1(x), y: round1(y), id: 8, timeOffset: 0 }] }, timestamp });
2529
+ },
2530
+ custom(timestamp, tag, payload) {
2531
+ return JSON.stringify({ type: 5, data: { tag, payload }, timestamp });
2532
+ }
2533
+ };
2534
+ var round1 = (v) => Math.round(v * 10) / 10;
2535
+ var UPLOAD_TIMEOUT_MS = 3e4;
2536
+ var ReplayUploader = class {
2537
+ constructor(opts) {
2538
+ __publicField(this, "opts", opts);
2539
+ __publicField(this, "windowId", randomId(16));
2540
+ __publicField(this, "buffer", null);
2541
+ __publicField(this, "queue", []);
2542
+ __publicField(this, "queuedBytes", 0);
2543
+ __publicField(this, "seqs", /* @__PURE__ */ new Map());
2544
+ __publicField(this, "inflight", null);
2545
+ __publicField(this, "flushTimer", null);
2546
+ __publicField(this, "retryTimer", null);
2547
+ __publicField(this, "attempt", 0);
2548
+ __publicField(this, "destroyed", false);
2549
+ __publicField(this, "offRecover");
2550
+ this.offRecover = opts.health?.onRecover?.(() => {
2551
+ this.clearRetry();
2552
+ this.attempt = 0;
2553
+ this.flush();
2554
+ }) ?? null;
2555
+ }
2556
+ get pendingChunks() {
2557
+ return this.queue.length;
2558
+ }
2559
+ get bufferedEvents() {
2560
+ return this.buffer?.events.length ?? 0;
2561
+ }
2562
+ push(sessionId, json) {
2563
+ if (this.destroyed) return;
2564
+ if (this.buffer && this.buffer.sessionId !== sessionId) this.seal();
2565
+ const chunk = this.buffer ?? (this.buffer = { sessionId, meta: this.opts.meta(), events: [], bytes: 0 });
2566
+ chunk.events.push(json);
2567
+ chunk.bytes += json.length;
2568
+ if (chunk.bytes > (this.opts.maxBufferBytes ?? 1e6)) this.flush();
2569
+ else if (!this.flushTimer) {
2570
+ this.flushTimer = setTimeout(() => {
2571
+ this.flushTimer = null;
2572
+ this.flush();
2573
+ }, this.opts.flushIntervalMs ?? 1e4);
2574
+ }
2575
+ }
2576
+ /** Seals the buffer and uploads the backlog (one chunk at a time). */
2577
+ flush() {
2578
+ if (this.destroyed) return;
2579
+ this.seal();
2580
+ if (this.retryTimer || this.inflight || !this.queue.length) return;
2581
+ void this.sendNext();
2582
+ }
2583
+ /** Drops everything buffered or queued (consent withdrawn, server disabled replay). */
2584
+ discard() {
2585
+ this.buffer = null;
2586
+ this.queue = [];
2587
+ this.queuedBytes = 0;
2588
+ this.clearFlushTimer();
2589
+ this.clearRetry();
2590
+ this.attempt = 0;
2591
+ }
2592
+ /** Uploads what is left once; no retries afterwards. */
2593
+ destroy() {
2594
+ this.flush();
2595
+ this.destroyed = true;
2596
+ this.offRecover?.();
2597
+ this.clearFlushTimer();
2598
+ this.clearRetry();
2599
+ }
2600
+ clearFlushTimer() {
2601
+ if (this.flushTimer) clearTimeout(this.flushTimer);
2602
+ this.flushTimer = null;
2603
+ }
2604
+ clearRetry() {
2605
+ if (this.retryTimer) clearTimeout(this.retryTimer);
2606
+ this.retryTimer = null;
2607
+ }
2608
+ scheduleRetry(ms) {
2609
+ this.retryTimer = setTimeout(() => {
2610
+ this.retryTimer = null;
2611
+ this.flush();
2612
+ }, ms);
2613
+ }
2614
+ seal() {
2615
+ this.clearFlushTimer();
2616
+ const chunk = this.buffer;
2617
+ this.buffer = null;
2618
+ if (!chunk || !chunk.events.length) return;
2619
+ this.queue.push(chunk);
2620
+ this.queuedBytes += chunk.bytes;
2621
+ const max = this.opts.maxQueuedBytes ?? 5e6;
2622
+ while (this.queuedBytes > max && this.queue.length > 1) {
2623
+ const i = this.inflight && this.queue[0] === this.inflight ? 1 : 0;
2624
+ const [dropped] = this.queue.splice(i, 1);
2625
+ if (!dropped) break;
2626
+ this.queuedBytes -= dropped.bytes;
2627
+ this.opts.log?.(`replay backlog full: dropped a chunk of ${dropped.events.length} events`);
2628
+ }
2629
+ }
2630
+ nextSeq(chunk) {
2631
+ if (chunk.seq === void 0) {
2632
+ const seq = this.seqs.get(chunk.sessionId) ?? 0;
2633
+ this.seqs.set(chunk.sessionId, seq + 1);
2634
+ chunk.seq = seq;
2635
+ }
2636
+ return chunk.seq;
2637
+ }
2638
+ body(chunk) {
2639
+ const head = JSON.stringify({ sessionId: chunk.sessionId, windowId: this.windowId, seq: this.nextSeq(chunk), ...chunk.meta });
2640
+ return `${head.slice(0, -1)},"events":[${chunk.events.join(",")}]}`;
2641
+ }
2642
+ async sendNext() {
2643
+ const chunk = this.queue[0];
2644
+ if (!chunk) return;
2645
+ const wait = this.destroyed ? 0 : this.opts.health?.waitMs() ?? 0;
2646
+ if (wait > 0) return this.scheduleRetry(wait);
2647
+ this.inflight = chunk;
2648
+ const result = await this.upload(this.body(chunk));
2649
+ this.onResult(chunk, result);
2650
+ }
2651
+ async upload(json) {
2652
+ const f = this.opts.fetch();
2653
+ if (!f) return "drop";
2654
+ const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
2655
+ const timer = ac ? setTimeout(() => ac.abort(), UPLOAD_TIMEOUT_MS) : null;
2656
+ try {
2657
+ let res;
2658
+ try {
2659
+ res = await f(this.opts.url(), { method: "POST", body: json, headers: { "Content-Type": "application/json" }, signal: ac?.signal });
2660
+ } catch {
2661
+ this.opts.health?.failure();
2662
+ return "retry";
2663
+ }
2664
+ if (res.status === 429 || res.status >= 500) this.opts.health?.failure();
2665
+ else this.opts.health?.success();
2666
+ if (res.ok) {
2667
+ const data = await res.json().catch(() => null);
2668
+ return data?.disabled ? "disabled" : "ok";
2669
+ }
2670
+ return res.status >= 400 && res.status < 500 && res.status !== 429 ? "drop" : "retry";
2671
+ } finally {
2672
+ if (timer) clearTimeout(timer);
2673
+ }
2674
+ }
2675
+ onResult(chunk, result) {
2676
+ if (this.inflight === chunk) this.inflight = null;
2677
+ if (result === "retry" && this.queue.includes(chunk)) {
2678
+ if (!this.destroyed) {
2679
+ const max = this.opts.maxAttempts ?? 5;
2680
+ this.attempt = Math.min(this.attempt + 1, max);
2681
+ const delay = this.attempt >= max ? this.opts.slowRetryMs ?? 6e4 : backoffMs(this.attempt - 1, 1e3, 8e3);
2682
+ return this.scheduleRetry(Math.max(delay, this.opts.health?.waitMs() ?? 0));
2683
+ }
2684
+ this.opts.log?.("replay upload failed after the recorder stopped; dropping the chunk");
2685
+ }
2686
+ this.attempt = 0;
2687
+ this.remove(chunk);
2688
+ if (result === "disabled") {
2689
+ for (const c of this.queue.slice()) if (c.sessionId === chunk.sessionId) this.remove(c);
2690
+ if (this.buffer?.sessionId === chunk.sessionId) this.buffer = null;
2691
+ this.opts.onDisabled(chunk.sessionId);
2692
+ }
2693
+ if (this.queue.length && !this.retryTimer && !this.inflight) void this.sendNext();
2694
+ }
2695
+ remove(chunk) {
2696
+ const i = this.queue.indexOf(chunk);
2697
+ if (i >= 0) {
2698
+ this.queue.splice(i, 1);
2699
+ this.queuedBytes -= chunk.bytes;
2700
+ }
2701
+ }
2702
+ };
2703
+ var TOUCH_SETTLE_MS = 200;
2704
+ var IDLE_FRAMES = 3;
2705
+ var MAX_IDLE_INTERVAL_MS = 5e3;
2706
+ var SessionRecorder = class {
2707
+ constructor(opts) {
2708
+ __publicField(this, "opts", opts);
2709
+ __publicField(this, "intervalMs");
2710
+ __publicField(this, "now");
2711
+ __publicField(this, "sid", null);
2712
+ __publicField(this, "foreground", true);
2713
+ __publicField(this, "generation", 0);
2714
+ __publicField(this, "timer", null);
2715
+ __publicField(this, "timerDue", 0);
2716
+ __publicField(this, "capturing", false);
2717
+ __publicField(this, "needKeyframe", true);
2718
+ __publicField(this, "hasKeyframe", false);
2719
+ __publicField(this, "lastFrame", null);
2720
+ __publicField(this, "unchanged", 0);
2721
+ __publicField(this, "captureSoon", false);
2722
+ __publicField(this, "pendingCustom", []);
2723
+ __publicField(this, "touchDown", false);
2724
+ __publicField(this, "downX", 0);
2725
+ __publicField(this, "downY", 0);
2726
+ __publicField(this, "downAt", 0);
2727
+ __publicField(this, "maxDistance", 0);
2728
+ __publicField(this, "lastMoveAt", 0);
2729
+ __publicField(this, "originX", 0);
2730
+ __publicField(this, "originY", 0);
2731
+ /** Viewport of the last frame (points), for chunk metadata. */
2732
+ __publicField(this, "viewportWidth", 0);
2733
+ __publicField(this, "viewportHeight", 0);
2734
+ this.intervalMs = Math.max(500, opts.intervalMs);
2735
+ this.now = opts.now ?? Date.now;
2736
+ }
2737
+ get sessionId() {
2738
+ return this.sid;
2739
+ }
2740
+ get isRecording() {
2741
+ return this.sid !== null;
2742
+ }
2743
+ get currentIntervalMs() {
2744
+ return this.unchanged >= IDLE_FRAMES ? Math.max(this.intervalMs, Math.min(4 * this.intervalMs, MAX_IDLE_INTERVAL_MS)) : this.intervalMs;
2745
+ }
2746
+ /** Starts (or keeps) recording `sessionId`; another session stops the current one first (uploading it). */
2747
+ start(sessionId) {
2748
+ if (this.sid === sessionId) return;
2749
+ if (this.sid !== null) this.stop(false);
2750
+ this.sid = sessionId;
2751
+ this.generation++;
2752
+ this.needKeyframe = true;
2753
+ this.hasKeyframe = false;
2754
+ this.lastFrame = null;
2755
+ this.unchanged = 0;
2756
+ this.captureSoon = false;
2757
+ this.pendingCustom = [];
2758
+ this.touchDown = false;
2759
+ if (this.foreground) this.schedule(0);
2760
+ }
2761
+ /** Stops recording; buffered events are uploaded unless `discard`. */
2762
+ stop(discard) {
2763
+ if (this.sid === null) return;
2764
+ this.sid = null;
2765
+ this.generation++;
2766
+ this.clearTimer();
2767
+ this.capturing = false;
2768
+ this.pendingCustom = [];
2769
+ this.touchDown = false;
2770
+ if (discard) this.opts.uploader.discard();
2771
+ else this.opts.uploader.flush();
2772
+ }
2773
+ /** App backgrounded: no captures; uploads what is buffered. */
2774
+ pause() {
2775
+ this.foreground = false;
2776
+ this.clearTimer();
2777
+ this.generation++;
2778
+ this.capturing = false;
2779
+ this.touchDown = false;
2780
+ this.opts.uploader.flush();
2781
+ }
2782
+ /** App foregrounded: resume with a keyframe. */
2783
+ resume() {
2784
+ if (this.foreground) return;
2785
+ this.foreground = true;
2786
+ this.needKeyframe = true;
2787
+ this.unchanged = 0;
2788
+ if (this.sid !== null) this.schedule(0);
2789
+ }
2790
+ /** A new screen: the next frame is a keyframe with the new href. */
2791
+ onScreen() {
2792
+ if (this.sid === null) return;
2793
+ this.needKeyframe = true;
2794
+ this.activity();
2795
+ }
2796
+ activity() {
2797
+ const wasIdle = this.unchanged >= IDLE_FRAMES;
2798
+ this.unchanged = 0;
2799
+ if (wasIdle && this.sid !== null && this.foreground && !this.capturing && !this.touchDown) this.schedule(this.intervalMs, true);
2800
+ }
2801
+ /** Adds the `guidely` marker for an analytics event. */
2802
+ addGuidelyEvent(name, customName, guideId) {
2803
+ const id = this.sid;
2804
+ if (id === null) return;
2805
+ const payload = { name };
2806
+ if (customName) payload.customName = customName;
2807
+ if (guideId) payload.guideId = guideId;
2808
+ this.activity();
2809
+ const now = this.now();
2810
+ if (!this.hasKeyframe) {
2811
+ if (this.pendingCustom.length < 50) this.pendingCustom.push([now, payload]);
2812
+ return;
2813
+ }
2814
+ this.opts.uploader.push(id, RrwebEvents.custom(now, "guidely", payload));
2815
+ }
2816
+ /** A primary-pointer touch at (x, y) points from the React root view. */
2817
+ touch(phase, rootX, rootY) {
2818
+ const id = this.sid;
2819
+ if (id === null || !this.foreground) return;
2820
+ const x = rootX + this.originX;
2821
+ const y = rootY + this.originY;
2822
+ const now = this.now();
2823
+ const emit = this.hasKeyframe;
2824
+ const push = (json) => emit && this.opts.uploader.push(id, json);
2825
+ switch (phase) {
2826
+ case "down":
2827
+ this.touchDown = true;
2828
+ this.unchanged = 0;
2829
+ this.downX = x;
2830
+ this.downY = y;
2831
+ this.downAt = now;
2832
+ this.maxDistance = 0;
2833
+ this.lastMoveAt = now;
2834
+ push(RrwebEvents.interaction(now, RrwebEvents.TOUCH_START, x, y));
2835
+ return;
2836
+ case "move":
2837
+ if (!this.touchDown) return;
2838
+ this.maxDistance = Math.max(this.maxDistance, Math.hypot(x - this.downX, y - this.downY));
2839
+ if (now - this.lastMoveAt < 50) return;
2840
+ this.lastMoveAt = now;
2841
+ push(RrwebEvents.touchMove(now, x, y));
2842
+ return;
2843
+ default:
2844
+ if (!this.touchDown) return;
2845
+ this.touchDown = false;
2846
+ this.unchanged = 0;
2847
+ this.maxDistance = Math.max(this.maxDistance, Math.hypot(x - this.downX, y - this.downY));
2848
+ push(RrwebEvents.interaction(now, RrwebEvents.TOUCH_END, x, y));
2849
+ if (phase === "up" && this.maxDistance <= 10 && now - this.downAt < 1e3) push(RrwebEvents.interaction(now, RrwebEvents.CLICK, x, y));
2850
+ if (this.capturing) this.captureSoon = true;
2851
+ else this.schedule(TOUCH_SETTLE_MS, true);
2852
+ }
2853
+ }
2854
+ clearTimer() {
2855
+ if (this.timer) clearTimeout(this.timer);
2856
+ this.timer = null;
2857
+ }
2858
+ /** Schedules the next capture `delay` ms from now, or keeps an earlier one when `ifSooner`. */
2859
+ schedule(delay, ifSooner = false) {
2860
+ const due = this.now() + delay;
2861
+ if (ifSooner && this.timer && this.timerDue <= due) return;
2862
+ this.clearTimer();
2863
+ this.timerDue = due;
2864
+ const gen = this.generation;
2865
+ this.timer = setTimeout(() => {
2866
+ this.timer = null;
2867
+ if (gen === this.generation) void this.tick();
2868
+ }, delay);
2869
+ }
2870
+ async tick() {
2871
+ if (this.sid === null || !this.foreground || this.capturing) return;
2872
+ if (this.touchDown) {
2873
+ this.schedule(this.intervalMs);
2874
+ return;
2875
+ }
2876
+ this.capturing = true;
2877
+ this.captureSoon = false;
2878
+ const gen = this.generation;
2879
+ let frame = null;
2880
+ try {
2881
+ frame = await this.opts.capturer(await this.opts.captureOptions());
2882
+ } catch (err) {
2883
+ this.opts.onError?.(err);
2884
+ }
2885
+ if (gen !== this.generation) return;
2886
+ this.capturing = false;
2887
+ if (frame) this.onFrame(frame, this.now());
2888
+ if (this.sid !== null && this.foreground) this.schedule(this.captureSoon ? TOUCH_SETTLE_MS : this.currentIntervalMs);
2889
+ }
2890
+ onFrame(frame, at) {
2891
+ const id = this.sid;
2892
+ if (id === null || !(frame.width > 0) || !(frame.height > 0) || !frame.base64) return;
2893
+ const sizeChanged = frame.width !== this.viewportWidth || frame.height !== this.viewportHeight;
2894
+ const keyframe = this.needKeyframe || !this.hasKeyframe || sizeChanged;
2895
+ if (!keyframe && this.lastFrame === frame.base64) {
2896
+ this.unchanged++;
2897
+ return;
2898
+ }
2899
+ this.unchanged = 0;
2900
+ this.lastFrame = frame.base64;
2901
+ this.originX = frame.originX ?? 0;
2902
+ this.originY = frame.originY ?? 0;
2903
+ this.viewportWidth = frame.width;
2904
+ this.viewportHeight = frame.height;
2905
+ const url = RrwebEvents.dataUrl(frame.base64);
2906
+ const up = this.opts.uploader;
2907
+ if (!keyframe) return up.push(id, RrwebEvents.frame(at, url));
2908
+ this.needKeyframe = false;
2909
+ up.push(id, RrwebEvents.meta(at, this.opts.href(), frame.width, frame.height));
2910
+ up.push(id, RrwebEvents.fullSnapshot(at, url, frame.width, frame.height));
2911
+ if (!this.hasKeyframe) {
2912
+ this.hasKeyframe = true;
2913
+ for (const [ts, payload] of this.pendingCustom) up.push(id, RrwebEvents.custom(Math.max(ts, at), "guidely", payload));
2914
+ this.pendingCustom = [];
2915
+ }
2916
+ }
2917
+ };
2918
+
2495
2919
  // src/core/storage.ts
2496
2920
  var PersistentStore = class {
2497
2921
  constructor(backing, onError = () => {
@@ -2545,7 +2969,7 @@ var PersistentStore = class {
2545
2969
 
2546
2970
  // src/core/client.ts
2547
2971
  var SDK_NAME = "@guidelytech/react-native";
2548
- var SDK_VERSION = "0.12.0";
2972
+ var SDK_VERSION = "0.14.0";
2549
2973
  var DEFAULT_API_URL = "https://guidelytech.com";
2550
2974
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
2551
2975
  var CACHED_START_MS = 1500;
@@ -2606,6 +3030,10 @@ var GuidelyClient = class {
2606
3030
  __publicField(this, "lastAudiences", /* @__PURE__ */ new Set());
2607
3031
  __publicField(this, "backgroundedAt", null);
2608
3032
  __publicField(this, "messenger", null);
3033
+ __publicField(this, "replayUploader", null);
3034
+ __publicField(this, "recorder", null);
3035
+ /** Replay decision for the current session (sampled once; forced by start/stopSessionRecording). */
3036
+ __publicField(this, "replayDecision", null);
2609
3037
  __publicField(this, "ui", { steps: [], checklists: [], survey: null, resourceCenterOpen: false, theme: {}, messenger: false });
2610
3038
  __publicField(this, "uiListeners", /* @__PURE__ */ new Set());
2611
3039
  /** Feature flags for this environment: `guidely.flags.isEnabled("new-checkout")`. */
@@ -2757,6 +3185,7 @@ var GuidelyClient = class {
2757
3185
  if (this.destroyed) return;
2758
3186
  this.isReady = true;
2759
3187
  if (this.config) this.emitter.emit("ready", this.config);
3188
+ this.syncReplay();
2760
3189
  this.notify();
2761
3190
  this.evaluate("ready");
2762
3191
  } catch (err) {
@@ -2783,6 +3212,7 @@ var GuidelyClient = class {
2783
3212
  this.syncMessenger();
2784
3213
  if (this.hydrated) this.checkLaunch();
2785
3214
  this.emitter.emit("config", this.config);
3215
+ this.syncReplay();
2786
3216
  this.log("config loaded", this.config.configVersion, `${this.config.guides.length} guides`);
2787
3217
  return true;
2788
3218
  }
@@ -2835,6 +3265,7 @@ var GuidelyClient = class {
2835
3265
  void this.outbox?.flush();
2836
3266
  void this.store.flush();
2837
3267
  void this.flags.flush();
3268
+ this.recorder?.pause();
2838
3269
  this.messenger?.setForeground(false);
2839
3270
  } else if (state === "active" && this.backgroundedAt !== null) {
2840
3271
  this.messenger?.setForeground(true);
@@ -2844,7 +3275,10 @@ var GuidelyClient = class {
2844
3275
  this.session = newSession();
2845
3276
  this.persistSession();
2846
3277
  this.firedDelays.clear();
3278
+ this.recorder?.stop(false);
2847
3279
  }
3280
+ this.recorder?.resume();
3281
+ this.syncReplay();
2848
3282
  this.cameToForeground();
2849
3283
  void this.flags.refresh();
2850
3284
  if (Date.now() - this.configFetchedAt > this.options.configRefreshMs && this.api.health.canSend()) void this.refresh();
@@ -2867,6 +3301,9 @@ var GuidelyClient = class {
2867
3301
  this.queued = [];
2868
3302
  this.messenger?.destroy();
2869
3303
  this.messenger = null;
3304
+ this.recorder?.stop(!this.analyticsEnabled());
3305
+ this.recorder = null;
3306
+ this.replayUploader?.destroy();
2870
3307
  this.visibilityWatchers.forEach((stop) => stop());
2871
3308
  this.visibilityWatchers.clear();
2872
3309
  this.timers.forEach((t) => clearTimeout(t));
@@ -2981,6 +3418,7 @@ var GuidelyClient = class {
2981
3418
  this.store.set(KEYS.anon, this.anonymousId);
2982
3419
  this.state = emptyState();
2983
3420
  this.persistState();
3421
+ this.recorder?.stop(false);
2984
3422
  this.session = newSession();
2985
3423
  this.persistSession();
2986
3424
  this.lastAudiences = /* @__PURE__ */ new Set();
@@ -2988,6 +3426,7 @@ var GuidelyClient = class {
2988
3426
  this.firstVisit = true;
2989
3427
  this.exposures.clear();
2990
3428
  this.syncFlagAttributes();
3429
+ this.syncReplay();
2991
3430
  this.emitter.emit("identify", { userId: null, anonymousId: this.anonymousId });
2992
3431
  this.notify();
2993
3432
  if (this.isReady) this.evaluate("identify");
@@ -3002,6 +3441,7 @@ var GuidelyClient = class {
3002
3441
  this.whenHydrated(() => {
3003
3442
  if (consent === "denied") this.queue?.clear();
3004
3443
  else if (consent === "granted") void this.queue?.flush();
3444
+ this.syncReplay();
3005
3445
  });
3006
3446
  }
3007
3447
  analyticsEnabled() {
@@ -3030,6 +3470,7 @@ var GuidelyClient = class {
3030
3470
  const locale = extra.guideId ? this.guideLocales.get(extra.guideId) : void 0;
3031
3471
  if (locale) event.properties = { ...event.properties ?? {}, locale };
3032
3472
  this.queue.enqueue(event);
3473
+ this.recorder?.addGuidelyEvent(name, event.customName, event.guideId);
3033
3474
  this.emitter.emit("track", event);
3034
3475
  return event;
3035
3476
  }
@@ -3067,6 +3508,7 @@ var GuidelyClient = class {
3067
3508
  }
3068
3509
  this.screenName = name;
3069
3510
  this.targets.setScreen(name);
3511
+ if (changed) this.recorder?.onScreen();
3070
3512
  if (changed) this.syncFlagAttributes();
3071
3513
  const url = this.currentUrl();
3072
3514
  if (changed) {
@@ -3101,6 +3543,112 @@ var GuidelyClient = class {
3101
3543
  if (this.isReady) this.evaluate("click", { targetId });
3102
3544
  }
3103
3545
  // ---------------------------------------------------------------------------
3546
+ // Session replay
3547
+ // ---------------------------------------------------------------------------
3548
+ replaySettings() {
3549
+ const r = this.config?.settings?.replay;
3550
+ if (!r?.enabled || !this.options.sessionReplay?.enabled || !this.bridge.capturer) return null;
3551
+ return r;
3552
+ }
3553
+ /** Reconciles recording with config, consent, readiness, the session and its sample decision. Idempotent. */
3554
+ syncReplay() {
3555
+ if (this.destroyed || !this.hydrated) return;
3556
+ const r = this.isReady && this.analyticsEnabled() ? this.replaySettings() : null;
3557
+ const id = this.session.id;
3558
+ if (r && this.replayDecision?.sessionId !== id) {
3559
+ this.replayDecision = { sessionId: id, record: rolloutBucket(id, "replay") < (r.sampleRate ?? 100) };
3560
+ this.log(`replay ${this.replayDecision.record ? "sampled" : "not sampled"} for session ${id}`);
3561
+ }
3562
+ if (!r || !this.replayDecision?.record) {
3563
+ if (this.recorder?.isRecording) this.recorder.stop(!this.analyticsEnabled());
3564
+ return;
3565
+ }
3566
+ const rec = this.recorder ?? this.createRecorder();
3567
+ if (rec && rec.sessionId !== id) {
3568
+ this.log(`recording session ${id}`);
3569
+ rec.start(id);
3570
+ }
3571
+ }
3572
+ createRecorder() {
3573
+ const capturer = this.bridge.capturer;
3574
+ if (!capturer) return null;
3575
+ const uploader = this.replayUploader ?? (this.replayUploader = new ReplayUploader({
3576
+ url: () => this.api.url("/replay"),
3577
+ fetch: () => this.options.fetch ?? (typeof fetch === "function" ? fetch : null),
3578
+ health: this.api.health,
3579
+ meta: () => {
3580
+ const rec2 = this.recorder;
3581
+ return {
3582
+ anonymousId: this.anonymousId,
3583
+ userId: this.user?.id || null,
3584
+ ...this.collectUrls() && this.screenName ? { url: this.currentUrl() } : {},
3585
+ device: { type: this.bridge.device.deviceType, os: this.bridge.device.platform },
3586
+ ...rec2 && rec2.viewportWidth > 0 && rec2.viewportHeight > 0 ? { viewport: { width: rec2.viewportWidth, height: rec2.viewportHeight } } : {},
3587
+ sdk: { name: SDK_NAME, version: SDK_VERSION }
3588
+ };
3589
+ },
3590
+ onDisabled: (sid) => {
3591
+ if (sid !== this.session.id) return;
3592
+ this.log(`replay disabled by the server for session ${sid}`);
3593
+ this.replayDecision = { sessionId: sid, record: false };
3594
+ this.recorder?.stop(true);
3595
+ },
3596
+ log: (m) => this.log(m)
3597
+ }));
3598
+ const opts = this.options.sessionReplay ?? {};
3599
+ const rec = new SessionRecorder({
3600
+ capturer,
3601
+ uploader,
3602
+ intervalMs: opts.captureIntervalMs ?? 1e3,
3603
+ captureOptions: () => this.replayCaptureOptions(),
3604
+ href: () => this.currentUrl(),
3605
+ onError: (err) => this.reportError(err)
3606
+ });
3607
+ if (this.backgroundedAt !== null) rec.pause();
3608
+ this.recorder = rec;
3609
+ return rec;
3610
+ }
3611
+ async replayCaptureOptions() {
3612
+ const r = this.config?.settings?.replay;
3613
+ const opts = this.options.sessionReplay ?? {};
3614
+ const ids = new Set((r?.blockSelectors ?? []).map((sel) => guidelyIdFromSelector(sel)).filter((id) => !!id));
3615
+ const rects = await Promise.all(Array.from(ids, (id) => this.targets.measure(id)));
3616
+ return { maskAllText: !!opts.maskAllText || !!r?.maskAllText, maskAllImages: !!opts.maskAllImages, blockRects: rects.filter((rect) => !!rect) };
3617
+ }
3618
+ /**
3619
+ * Records this session even when it was not sampled (still needs `sessionReplay.enabled`, replay
3620
+ * enabled for the project, analytics consent and the native replay module). Sticks until the session ends.
3621
+ */
3622
+ startSessionRecording() {
3623
+ this.whenHydrated(() => {
3624
+ this.replayDecision = { sessionId: this.session.id, record: true };
3625
+ this.syncReplay();
3626
+ });
3627
+ }
3628
+ /** Stops recording for the rest of this session; what was recorded is uploaded. */
3629
+ stopSessionRecording() {
3630
+ this.whenHydrated(() => {
3631
+ this.replayDecision = { sessionId: this.session.id, record: false };
3632
+ this.recorder?.stop(false);
3633
+ });
3634
+ }
3635
+ /** True while this session is being recorded. */
3636
+ isRecording() {
3637
+ return !!this.recorder?.isRecording;
3638
+ }
3639
+ /** The current analytics session id (also the replay id). */
3640
+ getSessionId() {
3641
+ return this.session.id;
3642
+ }
3643
+ /** True when touches should be passed to {@link recordTouch}. */
3644
+ isRecordingTouches() {
3645
+ return !!this.recorder?.isRecording;
3646
+ }
3647
+ /** A primary-pointer touch on the app, in window points (the provider's touch observer calls this). */
3648
+ recordTouch(phase, x, y) {
3649
+ this.recorder?.touch(phase, x, y);
3650
+ }
3651
+ // ---------------------------------------------------------------------------
3104
3652
  // Product analytics (autocapture)
3105
3653
  // ---------------------------------------------------------------------------
3106
3654
  captureAllowed() {
@@ -3147,7 +3695,7 @@ var GuidelyClient = class {
3147
3695
  }
3148
3696
  if (this.destroyed) return;
3149
3697
  const platform = this.bridge.device.platform;
3150
- const maskAll = !!this.config?.settings?.replay?.maskAllText;
3698
+ const maskAll = !!this.config?.settings?.replay?.maskAllText || !!this.options.sessionReplay?.maskAllText;
3151
3699
  const eventUrl = this.collectUrls() && url ? url : void 0;
3152
3700
  if (on.clicks && el) this.enqueue("click", { url: eventUrl, properties: { ...tapProperties(el, tap.x, tap.y, platform, maskAll) } });
3153
3701
  const count = this.rage.add(tap.x, tap.y, t);
@@ -3193,12 +3741,12 @@ var GuidelyClient = class {
3193
3741
  /** `{ appVersion, build }` of this launch, without unknown keys. */
3194
3742
  appInfo() {
3195
3743
  const appVersion = this.options.appVersion ?? this.bridge.device.appVersion;
3196
- const build = this.options.appBuild;
3744
+ const build = this.options.appBuild ?? (this.options.appVersion ? void 0 : this.bridge.device.appBuild);
3197
3745
  return { ...appVersion ? { appVersion: String(appVersion) } : {}, ...build ? { build: String(build) } : {} };
3198
3746
  }
3199
3747
  /**
3200
3748
  * Once per launch, as soon as a config is known: `app_install` / `app_update` from the stored
3201
- * version (when the app passes `appVersion` and `storage`), then `app_open` with `coldStart: true`.
3749
+ * version (when the app version is known and the app passes `storage`), then `app_open` with `coldStart: true`.
3202
3750
  */
3203
3751
  checkLaunch() {
3204
3752
  if (this.launchChecked || !this.config || this.destroyed || !this.hydrated) return;
@@ -4015,8 +4563,8 @@ var GuidelyClient = class {
4015
4563
  this.messenger?.close();
4016
4564
  }
4017
4565
  /** Opens a new messenger conversation, optionally pre-filling the message. */
4018
- showNewMessage(text) {
4019
- this.whenHydrated(() => this.messenger?.showNewMessage(text));
4566
+ showNewMessage(text2) {
4567
+ this.whenHydrated(() => this.messenger?.showNewMessage(text2));
4020
4568
  }
4021
4569
  // ---------------------------------------------------------------------------
4022
4570
  // Feature flags
@@ -4040,7 +4588,7 @@ var GuidelyClient = class {
4040
4588
  /** Device facts on every screen view, read by the Pages devices / browsers breakdowns. */
4041
4589
  deviceProperties() {
4042
4590
  const d = this.bridge.device;
4043
- return { platform: d.platform, deviceType: d.deviceType, os: d.platform };
4591
+ return { platform: d.platform, deviceType: d.deviceType, os: d.platform, ...d.osVersion ? { osVersion: d.osVersion } : {} };
4044
4592
  }
4045
4593
  /** Attributes flags are evaluated with: identity, traits, account, audiences, screen and device. */
4046
4594
  flagContext() {
@@ -4051,6 +4599,7 @@ var GuidelyClient = class {
4051
4599
  deviceType: d.deviceType,
4052
4600
  locale: d.locale,
4053
4601
  appVersion: this.options.appVersion ?? d.appVersion,
4602
+ osVersion: d.osVersion,
4054
4603
  environment: this.options.environment,
4055
4604
  ...this.screenName ? { screen: this.screenName, path: this.currentUrl() } : {},
4056
4605
  ...this.user?.traits ?? {},
@@ -4141,6 +4690,51 @@ function withoutCode(guide) {
4141
4690
  return { ...guide, config: { ...guide.config, steps: guide.config.steps.map(({ code: _code, ...step }) => step) } };
4142
4691
  }
4143
4692
 
4693
+ // src/core/device.ts
4694
+ function text(value) {
4695
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
4696
+ if (typeof value !== "string") return void 0;
4697
+ const v = value.trim();
4698
+ return v ? v.slice(0, 64) : void 0;
4699
+ }
4700
+ function constant(mod, key) {
4701
+ if (!mod || typeof mod !== "object" && typeof mod !== "function") return void 0;
4702
+ const m = mod;
4703
+ try {
4704
+ const fromGetter = typeof m.getConstants === "function" ? m.getConstants()?.[key] : void 0;
4705
+ return fromGetter ?? m[key];
4706
+ } catch {
4707
+ return void 0;
4708
+ }
4709
+ }
4710
+ function read(mod, versionKey, buildKey) {
4711
+ const appVersion = text(constant(mod, versionKey));
4712
+ if (!appVersion) return null;
4713
+ const appBuild = text(constant(mod, buildKey));
4714
+ return { appVersion, ...appBuild ? { appBuild } : {} };
4715
+ }
4716
+ function inExpoGo(src) {
4717
+ const c = src.expoModules?.ExponentConstants ?? src.nativeModules.ExponentConstants;
4718
+ return constant(c, "executionEnvironment") === "storeClient" || constant(c, "appOwnership") === "expo";
4719
+ }
4720
+ function detectAppVersion(src) {
4721
+ try {
4722
+ const expo = inExpoGo(src) ? null : src.expoModules?.ExpoApplication ?? src.nativeModules.ExpoApplication;
4723
+ return read(src.nativeModules.GuidelyReplay, "appVersion", "appBuild") ?? read(expo, "nativeApplicationVersion", "nativeBuildVersion") ?? read(src.nativeModules.RNDeviceInfo, "appVersion", "buildNumber") ?? {};
4724
+ } catch {
4725
+ return {};
4726
+ }
4727
+ }
4728
+ function detectOsVersion(src) {
4729
+ try {
4730
+ const c = src.platform.constants ?? {};
4731
+ if (src.platform.OS === "android") return text(c.Release) ?? text(src.platform.Version);
4732
+ return text(c.osVersion) ?? text(src.platform.Version);
4733
+ } catch {
4734
+ return void 0;
4735
+ }
4736
+ }
4737
+
4144
4738
  // src/ui/Autocapture.tsx
4145
4739
  var import_react8 = require("react");
4146
4740
  var import_react_native11 = require("react-native");
@@ -4272,8 +4866,8 @@ function describe(f, masked, opts, withText) {
4272
4866
  if (!sub.masked) {
4273
4867
  const hostProps = host?.memoizedProps && typeof host.memoizedProps === "object" ? host.memoizedProps : {};
4274
4868
  const label = props.accessibilityLabel ?? props["aria-label"] ?? hostProps.accessibilityLabel ?? hostProps["aria-label"];
4275
- const text = cleanText(sub.text) ?? (typeof label === "string" ? cleanText(label) : void 0);
4276
- if (text) el.text = text;
4869
+ const text2 = cleanText(sub.text) ?? (typeof label === "string" ? cleanText(label) : void 0);
4870
+ if (text2) el.text = text2;
4277
4871
  }
4278
4872
  }
4279
4873
  if (host) el.measure = () => measureHost(host);
@@ -4429,17 +5023,17 @@ function parseHtml(html) {
4429
5023
  flush();
4430
5024
  current = { kind, level, marker, runs: [] };
4431
5025
  };
4432
- const push = (text) => {
4433
- if (!text) return;
5026
+ const push = (text2) => {
5027
+ if (!text2) return;
4434
5028
  if (!current) current = { kind: "paragraph", level: 1, runs: [] };
4435
- const run = { text };
5029
+ const run = { text: text2 };
4436
5030
  if (style.bold) run.bold = true;
4437
5031
  if (style.italic) run.italic = true;
4438
5032
  if (style.underline) run.underline = true;
4439
5033
  if (style.code) run.code = true;
4440
5034
  if (hrefs.length) run.href = hrefs[hrefs.length - 1];
4441
5035
  const last = current.runs[current.runs.length - 1];
4442
- if (last && last.bold === run.bold && last.italic === run.italic && last.underline === run.underline && last.code === run.code && last.href === run.href) last.text += text;
5036
+ if (last && last.bold === run.bold && last.italic === run.italic && last.underline === run.underline && last.code === run.code && last.href === run.href) last.text += text2;
4443
5037
  else current.runs.push(run);
4444
5038
  };
4445
5039
  const size = (attrs, name) => {
@@ -4455,18 +5049,18 @@ function parseHtml(html) {
4455
5049
  const re = /<!--[\s\S]*?-->|<\/?([a-zA-Z][a-zA-Z0-9]*)\b([^>]*)>|([^<]+|<)/g;
4456
5050
  let m;
4457
5051
  while (m = re.exec(html || "")) {
4458
- const [token, rawTag, attrs = "", text] = m;
4459
- if (text !== void 0) {
5052
+ const [token, rawTag, attrs = "", text2] = m;
5053
+ if (text2 !== void 0) {
4460
5054
  if (skipping) continue;
4461
5055
  if (pdf) {
4462
- if (pdf.inLink) pdf.name += decodeEntities(text);
5056
+ if (pdf.inLink) pdf.name += decodeEntities(text2);
4463
5057
  continue;
4464
5058
  }
4465
5059
  if (fileLink) {
4466
- fileLink.name += decodeEntities(text);
5060
+ fileLink.name += decodeEntities(text2);
4467
5061
  continue;
4468
5062
  }
4469
- push(decodeEntities(text.replace(/\s+/g, " ")));
5063
+ push(decodeEntities(text2.replace(/\s+/g, " ")));
4470
5064
  continue;
4471
5065
  }
4472
5066
  if (!rawTag) continue;
@@ -5040,13 +5634,13 @@ function Conversations({ store, palette: p }) {
5040
5634
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(GButton, { label: "New conversation", palette: p, onPress: () => store.showNewMessage(), style: styles4.fullWidth })
5041
5635
  ] });
5042
5636
  }
5043
- function Bubble({ m, text, name, palette: p, onArticle, onGuide }) {
5637
+ function Bubble({ m, text: text2, name, palette: p, onArticle, onGuide }) {
5044
5638
  if (m.author === "system")
5045
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { accessibilityRole: "text", style: [styles4.system, { color: p.muted }], children: text });
5639
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { accessibilityRole: "text", style: [styles4.system, { color: p.muted }], children: text2 });
5046
5640
  const mine = m.author === "user";
5047
5641
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.View, { style: [styles4.bubbleWrap, mine ? styles4.mine : styles4.theirs], children: [
5048
5642
  !mine ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.author, { color: p.muted }], children: m.author === "agent" ? m.authorName || "Team" : name }) : null,
5049
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.View, { style: [styles4.bubble, { borderRadius: Math.max(12, p.radius) }, mine ? { backgroundColor: p.primary } : { backgroundColor: p.surface }], children: m.status === "pending" && !text ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TypingDots, { color: p.muted }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.bubbleText, { color: mine ? p.onPrimary : p.text }], children: text }) }),
5643
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.View, { style: [styles4.bubble, { borderRadius: Math.max(12, p.radius) }, mine ? { backgroundColor: p.primary } : { backgroundColor: p.surface }], children: m.status === "pending" && !text2 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TypingDots, { color: p.muted }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Text, { style: [styles4.bubbleText, { color: mine ? p.onPrimary : p.text }], children: text2 }) }),
5050
5644
  m.citations.length || m.suggestedGuides.length ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.View, { style: styles4.chips, children: [
5051
5645
  m.citations.map((c) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react_native5.Pressable, { accessibilityRole: "link", accessibilityLabel: `Article: ${c.title}`, onPress: () => onArticle(c.articleId), style: [styles4.chip, { borderColor: p.border }], children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_react_native5.Text, { style: [styles4.chipText, { color: p.text }], children: [
5052
5646
  "\u{1F4C4} ",
@@ -5065,7 +5659,7 @@ function TypingDots({ color }) {
5065
5659
  function Thread({ store, palette: p, bottomInset }) {
5066
5660
  const snap = useMessenger(store);
5067
5661
  const t = snap.thread;
5068
- const [text, setText] = (0, import_react3.useState)(t?.draft ?? "");
5662
+ const [text2, setText] = (0, import_react3.useState)(t?.draft ?? "");
5069
5663
  const scroll = (0, import_react3.useRef)(null);
5070
5664
  const convo = t?.conversation ?? null;
5071
5665
  (0, import_react3.useEffect)(() => {
@@ -5074,7 +5668,7 @@ function Thread({ store, palette: p, bottomInset }) {
5074
5668
  const streamingIds = (0, import_react3.useMemo)(() => Object.keys(t?.streaming ?? {}).filter((id) => !t?.messages.some((m) => m.id === id)), [t]);
5075
5669
  if (!t) return null;
5076
5670
  const send = () => {
5077
- const value = text;
5671
+ const value = text2;
5078
5672
  setText("");
5079
5673
  void store.send(value);
5080
5674
  };
@@ -5096,7 +5690,7 @@ function Thread({ store, palette: p, bottomInset }) {
5096
5690
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
5097
5691
  import_react_native5.TextInput,
5098
5692
  {
5099
- value: text,
5693
+ value: text2,
5100
5694
  onChangeText: (v) => setText(v.slice(0, 4e3)),
5101
5695
  placeholder: convo ? "Reply\u2026" : "Write a message\u2026",
5102
5696
  placeholderTextColor: p.muted,
@@ -5105,7 +5699,7 @@ function Thread({ store, palette: p, bottomInset }) {
5105
5699
  style: [styles4.input, { color: p.text, backgroundColor: p.surface, borderRadius: Math.min(18, p.radius + 6) }]
5106
5700
  }
5107
5701
  ),
5108
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(GButton, { label: "Send", palette: p, onPress: send, disabled: !text.trim() || t.sending })
5702
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(GButton, { label: "Send", palette: p, onPress: send, disabled: !text2.trim() || t.sending })
5109
5703
  ] })
5110
5704
  ] });
5111
5705
  }
@@ -6009,15 +6603,15 @@ function palette(theme, systemScheme, explicit) {
6009
6603
  const wantsDark = theme.dark === "dark" || theme.dark === "auto" && systemScheme === "dark";
6010
6604
  const dark = wantsDark && !explicit?.backgroundColor;
6011
6605
  const background = dark ? DARK.background : theme.backgroundColor ?? "#ffffff";
6012
- const text = dark ? DARK.text : theme.textColor ?? "#111827";
6606
+ const text2 = dark ? DARK.text : theme.textColor ?? "#111827";
6013
6607
  return {
6014
6608
  primary,
6015
6609
  onPrimary: luminance(primary) > 0.45 ? "#111827" : "#ffffff",
6016
6610
  background,
6017
- text,
6018
- muted: dark ? DARK.muted : mix(text, 0.65),
6019
- border: dark ? DARK.border : mix(text, 0.12),
6020
- surface: dark ? DARK.surface : mix(text, 0.05),
6611
+ text: text2,
6612
+ muted: dark ? DARK.muted : mix(text2, 0.65),
6613
+ border: dark ? DARK.border : mix(text2, 0.12),
6614
+ surface: dark ? DARK.surface : mix(text2, 0.05),
6021
6615
  radius: theme.borderRadius ?? 12,
6022
6616
  overlay: `rgba(15, 23, 42, ${theme.overlayOpacity ?? 0.55})`,
6023
6617
  fontFamily: nativeFont(theme.fontFamily)
@@ -6106,8 +6700,9 @@ function rcLauncherLabel(client) {
6106
6700
 
6107
6701
  // src/ui/Autocapture.tsx
6108
6702
  var import_jsx_runtime11 = require("react/jsx-runtime");
6703
+ var GUIDELY_MASK_NATIVE_ID = "guidely-mask";
6109
6704
  function GuidelyMask({ children, ...rest }) {
6110
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native11.View, { ...rest, children });
6705
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_react_native11.View, { collapsable: false, ...rest, nativeID: GUIDELY_MASK_NATIVE_ID, children });
6111
6706
  }
6112
6707
  var RESOLVE = {
6113
6708
  isIgnored: (type) => type === GuidelyOverlay,
@@ -6118,6 +6713,18 @@ function useTapObserver(client) {
6118
6713
  clientRef.current = client;
6119
6714
  return (0, import_react8.useMemo)(() => {
6120
6715
  const tracker = new TapTracker();
6716
+ let replayPointer = null;
6717
+ const replay = (phase, e) => {
6718
+ const c = clientRef.current;
6719
+ const n = e.nativeEvent;
6720
+ if (!c?.isRecordingTouches()) return void (replayPointer = null);
6721
+ if (phase === "down") {
6722
+ if (replayPointer !== null) return;
6723
+ replayPointer = n.identifier;
6724
+ } else if (n.identifier !== replayPointer) return;
6725
+ if (phase === "up" || phase === "cancel") replayPointer = null;
6726
+ c.recordTouch(phase, n.pageX, n.pageY);
6727
+ };
6121
6728
  const guard = (fn) => (e) => {
6122
6729
  try {
6123
6730
  fn(e);
@@ -6127,12 +6734,17 @@ function useTapObserver(client) {
6127
6734
  };
6128
6735
  return {
6129
6736
  onTouchStartCapture: guard((e) => {
6737
+ replay("down", e);
6130
6738
  const n = e.nativeEvent;
6131
6739
  if (!clientRef.current?.isTapCaptureOn()) return tracker.cancel();
6132
6740
  tracker.begin(n.pageX, n.pageY, Date.now(), Array.isArray(n.touches) ? n.touches.length : 1, n.identifier);
6133
6741
  }),
6134
- onTouchMoveCapture: guard((e) => tracker.move(e.nativeEvent.pageX, e.nativeEvent.pageY, e.nativeEvent.identifier)),
6742
+ onTouchMoveCapture: guard((e) => {
6743
+ replay("move", e);
6744
+ tracker.move(e.nativeEvent.pageX, e.nativeEvent.pageY, e.nativeEvent.identifier);
6745
+ }),
6135
6746
  onTouchEndCapture: guard((e) => {
6747
+ replay("up", e);
6136
6748
  const n = e.nativeEvent;
6137
6749
  const tap = tracker.end(n.pageX, n.pageY, Date.now(), n.identifier);
6138
6750
  const c = clientRef.current;
@@ -6140,7 +6752,10 @@ function useTapObserver(client) {
6140
6752
  const r = resolveTapTarget(e, RESOLVE);
6141
6753
  if (!r.ignored) c.recordTap({ x: tap.x, y: tap.y, t: Date.now(), element: r.element, hit: r.hit });
6142
6754
  }),
6143
- onTouchCancelCapture: guard(() => tracker.cancel())
6755
+ onTouchCancelCapture: guard((e) => {
6756
+ replay("cancel", e);
6757
+ tracker.cancel();
6758
+ })
6144
6759
  };
6145
6760
  }, []);
6146
6761
  }
@@ -6223,20 +6838,37 @@ function deviceLocale() {
6223
6838
  return void 0;
6224
6839
  }
6225
6840
  }
6226
- function defaultBridge(appVersion) {
6841
+ function nativeSources() {
6842
+ return {
6843
+ platform: import_react_native13.Platform,
6844
+ nativeModules: import_react_native13.NativeModules,
6845
+ expoModules: globalThis.expo?.modules
6846
+ };
6847
+ }
6848
+ function defaultBridge(appVersion, appBuild) {
6227
6849
  const { width, height } = import_react_native13.Dimensions.get("window");
6850
+ const sources = nativeSources();
6851
+ const detected = appVersion ? {} : detectAppVersion(sources);
6228
6852
  return {
6229
6853
  device: {
6230
6854
  platform: import_react_native13.Platform.OS === "android" ? "android" : "ios",
6231
6855
  deviceType: import_react_native13.Platform.OS === "ios" && import_react_native13.Platform.isPad || Math.min(width, height) >= 600 ? "tablet" : "mobile",
6232
6856
  locale: deviceLocale(),
6233
- appVersion
6857
+ appVersion: appVersion ?? detected.appVersion,
6858
+ appBuild: appBuild ?? detected.appBuild,
6859
+ osVersion: detectOsVersion(sources)
6234
6860
  },
6235
6861
  openExternal: (url) => void import_react_native13.Linking.openURL(url).catch(() => {
6236
6862
  }),
6237
- windowSize: () => import_react_native13.Dimensions.get("window")
6863
+ windowSize: () => import_react_native13.Dimensions.get("window"),
6864
+ capturer: nativeCapturer()
6238
6865
  };
6239
6866
  }
6867
+ function nativeCapturer() {
6868
+ const mod = import_react_native13.NativeModules.GuidelyReplay;
6869
+ if (!mod || typeof mod.capture !== "function") return void 0;
6870
+ return (options) => mod.capture(options);
6871
+ }
6240
6872
  function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPayload, flagAttributes, ...options }) {
6241
6873
  const [client, setClient] = (0, import_react10.useState)(null);
6242
6874
  const optionsRef = (0, import_react10.useRef)(options);
@@ -6269,7 +6901,7 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
6269
6901
  onNavigate: (url) => optionsRef.current.onNavigate?.(url),
6270
6902
  onError: (err) => optionsRef.current.onError?.(err)
6271
6903
  },
6272
- defaultBridge(latest.appVersion)
6904
+ defaultBridge(latest.appVersion, latest.appBuild)
6273
6905
  );
6274
6906
  } catch (err) {
6275
6907
  optionsRef.current.onError?.(err instanceof Error ? err : new Error(String(err)));
@@ -6306,7 +6938,7 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
6306
6938
  const flags = (0, import_react10.useMemo)(() => ({ client: client?.flags ?? null, bootstrap }), [client, bootstrap]);
6307
6939
  const tapObserver = useTapObserver(client);
6308
6940
  return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(GuidelyContext.Provider, { value: client, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(FlagsContext.Provider, { value: flags, children: [
6309
- options.autocapture === false ? children : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native13.View, { style: styles10.root, ...tapObserver, children }),
6941
+ options.autocapture === false && !options.sessionReplay?.enabled ? children : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_react_native13.View, { style: styles10.root, ...tapObserver, children }),
6310
6942
  client ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(GuidelyOverlay, { client, insets, launcherOffset }) : null
6311
6943
  ] }) });
6312
6944
  }
@@ -6326,6 +6958,9 @@ var NOOP_RESULTS = {
6326
6958
  getMessenger: null,
6327
6959
  isGuideCompleted: false,
6328
6960
  isClientReady: false,
6961
+ isRecording: false,
6962
+ isRecordingTouches: false,
6963
+ getSessionId: "",
6329
6964
  getScreen: "",
6330
6965
  on: () => {
6331
6966
  },
@@ -6460,12 +7095,17 @@ function useActiveGuide() {
6460
7095
  InMemoryStickyBucketStore,
6461
7096
  Messenger,
6462
7097
  RageClickDetector,
7098
+ ReplayUploader,
7099
+ RrwebEvents,
6463
7100
  SDK_NAME,
6464
7101
  SDK_VERSION,
7102
+ SessionRecorder,
6465
7103
  TapTracker,
6466
7104
  TargetRegistry,
6467
7105
  createFlagsClient,
6468
7106
  createSseParser,
7107
+ detectAppVersion,
7108
+ detectOsVersion,
6469
7109
  guidelyScreenLayout,
6470
7110
  htmlToText,
6471
7111
  noopClient,