@guidelytech/react-native 0.15.0 → 0.17.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
@@ -23,6 +23,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
23
23
  var index_exports = {};
24
24
  __export(index_exports, {
25
25
  DEFAULT_API_URL: () => DEFAULT_API_URL,
26
+ FATAL_FLUSH_MS: () => FATAL_FLUSH_MS,
26
27
  Feature: () => Feature,
27
28
  FeatureFlag: () => FeatureFlag,
28
29
  FeatureValue: () => FeatureValue,
@@ -47,11 +48,15 @@ __export(index_exports, {
47
48
  createFlagsClient: () => import_flags6.createFlagsClient,
48
49
  createSseParser: () => createSseParser,
49
50
  detectAppVersion: () => detectAppVersion,
51
+ detectDeviceModel: () => detectDeviceModel,
50
52
  detectOsVersion: () => detectOsVersion,
53
+ detectReactNativeVersion: () => detectReactNativeVersion,
51
54
  guidelyScreenLayout: () => guidelyScreenLayout,
52
55
  htmlToText: () => htmlToText,
53
56
  noopClient: () => noopClient,
57
+ normalizeBundleFile: () => normalizeBundleFile,
54
58
  parseHtml: () => parseHtml,
59
+ parseReactNativeStack: () => parseReactNativeStack,
55
60
  positionCard: () => positionCard,
56
61
  targetIdsFor: () => targetIdsFor,
57
62
  useActiveGuide: () => useActiveGuide,
@@ -165,7 +170,7 @@ function FlagsProvider({ children, client: external, payload, ...options }) {
165
170
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FlagsContext.Provider, { value, children });
166
171
  }
167
172
 
168
- // ../shared/dist/chunk-XJXXVV3C.js
173
+ // ../shared/dist/chunk-YL65O3FS.js
169
174
  function fnv1a(input) {
170
175
  let h = 2166136261;
171
176
  for (let i = 0; i < input.length; i++) {
@@ -178,18 +183,18 @@ function rolloutBucket(subject, salt) {
178
183
  return fnv1a(`${salt}:${subject}`) % 100;
179
184
  }
180
185
  var ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
181
- function randomId(size = 16) {
186
+ function randomId(size2 = 16) {
182
187
  const g = typeof globalThis !== "undefined" ? globalThis : {};
183
188
  let bytes;
184
189
  if (g.crypto && typeof g.crypto.getRandomValues === "function") {
185
- bytes = new Uint8Array(size);
190
+ bytes = new Uint8Array(size2);
186
191
  g.crypto.getRandomValues(bytes);
187
192
  } else {
188
- bytes = new Uint8Array(size);
189
- for (let i = 0; i < size; i++) bytes[i] = Math.floor(Math.random() * 256);
193
+ bytes = new Uint8Array(size2);
194
+ for (let i = 0; i < size2; i++) bytes[i] = Math.floor(Math.random() * 256);
190
195
  }
191
196
  let out = "";
192
- for (let i = 0; i < size; i++) out += ALPHABET[bytes[i] % ALPHABET.length];
197
+ for (let i = 0; i < size2; i++) out += ALPHABET[bytes[i] % ALPHABET.length];
193
198
  return out;
194
199
  }
195
200
  function getPath(obj, path) {
@@ -459,24 +464,92 @@ function evaluateRules(group, ctx) {
459
464
  const value = group.op === "and" ? results.every(Boolean) : results.some(Boolean);
460
465
  return group.negate ? !value : value;
461
466
  }
467
+ var MAX_EXCEPTION_DETAIL_BYTES = 64 * 1024;
468
+ var MAX_EXCEPTION_DETAIL_RAW_BYTES = 1024 * 1024;
469
+ var MAX_BREADCRUMBS = 100;
470
+ var MAX_EXCEPTION_CHAIN = 20;
471
+ var MAX_DETAIL_FRAMES = 250;
472
+ var MAX_DETAIL_THREADS = 100;
473
+ var MAX_DEBUG_IMAGES = 500;
474
+ var MAX_ERROR_TAGS = 50;
475
+ var size = (v) => {
476
+ try {
477
+ return JSON.stringify(v)?.length ?? 0;
478
+ } catch {
479
+ return Infinity;
480
+ }
481
+ };
482
+ function normalizeTags(tags) {
483
+ const out = {};
484
+ for (const [k, v] of Object.entries(tags ?? {})) {
485
+ if (Object.keys(out).length >= MAX_ERROR_TAGS) break;
486
+ if (!k || v === void 0 || v === null) continue;
487
+ out[k.slice(0, 32)] = String(v).slice(0, 200);
488
+ }
489
+ return out;
490
+ }
491
+ function trimExceptionDetail(detail, max = MAX_EXCEPTION_DETAIL_BYTES) {
492
+ const d = detail;
493
+ if (d.breadcrumbs?.values && d.breadcrumbs.values.length > MAX_BREADCRUMBS) d.breadcrumbs.values = d.breadcrumbs.values.slice(-MAX_BREADCRUMBS);
494
+ if (d.exception?.values && d.exception.values.length > MAX_EXCEPTION_CHAIN) d.exception.values = d.exception.values.slice(-MAX_EXCEPTION_CHAIN);
495
+ if (d.threads?.values && d.threads.values.length > MAX_DETAIL_THREADS) d.threads.values = d.threads.values.slice(0, MAX_DETAIL_THREADS);
496
+ if (d.debug_meta?.images && d.debug_meta.images.length > MAX_DEBUG_IMAGES) d.debug_meta.images = d.debug_meta.images.slice(0, MAX_DEBUG_IMAGES);
497
+ const stacks = () => [...d.exception?.values ?? [], ...d.threads?.values ?? []].map((x) => x.stacktrace).filter((s) => !!s);
498
+ for (const s of stacks()) if (s.frames && s.frames.length > MAX_DETAIL_FRAMES) s.frames = s.frames.slice(-MAX_DETAIL_FRAMES);
499
+ if (size(d) <= max) return d;
500
+ while (d.breadcrumbs?.values.length && size(d) > max) d.breadcrumbs.values = d.breadcrumbs.values.slice(Math.ceil(d.breadcrumbs.values.length / 2));
501
+ if (size(d) <= max) return d;
502
+ for (const s of stacks())
503
+ for (const f of s.frames ?? []) {
504
+ delete f.vars;
505
+ delete f.pre_context;
506
+ delete f.post_context;
507
+ }
508
+ if (size(d) <= max) return d;
509
+ if (d.threads?.values) d.threads.values = d.threads.values.filter((t) => t.crashed || t.current);
510
+ if (size(d) <= max) return d;
511
+ if (d.extra) d.extra = { _trimmed: true };
512
+ if (d.request) delete d.request.data;
513
+ for (const s of stacks()) while (s.frames && s.frames.length > 20 && size(d) > max) s.frames.splice(Math.floor(s.frames.length / 2) - 5, 10);
514
+ if (d.debug_meta?.images && size(d) > max) d.debug_meta.images = d.debug_meta.images.slice(0, 50);
515
+ return d;
516
+ }
517
+ var matches = (patterns, value) => patterns.some((p) => typeof p === "string" ? p !== "" && value.includes(p) : safeTest(p, value));
518
+ function safeTest(re, value) {
519
+ re.lastIndex = 0;
520
+ return re.test(value);
521
+ }
522
+ function errorFilterDrops(e, opts) {
523
+ const message = e.message ?? "";
524
+ if (opts.ignoreErrors?.length && (matches(opts.ignoreErrors, `${e.type || "Error"}: ${message}`) || matches(opts.ignoreErrors, message))) return true;
525
+ if (!e.file) return false;
526
+ if (opts.denyUrls?.length && matches(opts.denyUrls, e.file)) return true;
527
+ if (opts.allowUrls?.length && !matches(opts.allowUrls, e.file)) return true;
528
+ return false;
529
+ }
462
530
  var SENSITIVE_PROPERTY_PATTERN = /(password|passwd|secret|token|credit|card|cvv|ssn|iban)/i;
463
531
  var MAX_PROPERTY_BYTES = 8 * 1024;
464
532
  function sanitizeProperties(props) {
465
533
  if (!props) return {};
466
534
  const out = {};
467
- let size = 0;
535
+ let size2 = 0;
468
536
  for (const [k, v] of Object.entries(props)) {
469
537
  if (SENSITIVE_PROPERTY_PATTERN.test(k)) continue;
470
538
  if (typeof v === "function" || typeof v === "symbol") continue;
471
539
  const str2 = typeof v === "string" ? v : JSON.stringify(v ?? null);
472
- size += k.length + (str2?.length ?? 0);
473
- if (size > MAX_PROPERTY_BYTES) break;
540
+ size2 += k.length + (str2?.length ?? 0);
541
+ if (size2 > MAX_PROPERTY_BYTES) break;
474
542
  out[k.slice(0, 100)] = typeof v === "string" ? v.slice(0, 1e3) : v;
475
543
  }
476
544
  return out;
477
545
  }
546
+ function reactNativeBundleUrl(file) {
547
+ const base = file.replace(/[?#].*$/, "").split("/").pop() || file;
548
+ return `app:///${base}`;
549
+ }
478
550
  var SOURCEMAP_MAX_FILE_BYTES = 100 * 1024 * 1024;
479
551
  var MAX_COMPLAINT_ATTACHMENT_BYTES = 2 * 1024 * 1024;
552
+ var MAX_BUG_REPORT_SCREENSHOT_BYTES = 4 * 1024 * 1024;
480
553
  var MEDIA_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
481
554
  var MEDIA_VIDEO_MAX_BYTES = 200 * 1024 * 1024;
482
555
  var MEDIA_FILE_MAX_BYTES = 50 * 1024 * 1024;
@@ -574,6 +647,16 @@ function isSafeMediaUrl(url) {
574
647
  return false;
575
648
  }
576
649
  }
650
+ function videoThumbnailUrl(url) {
651
+ const embed = url ? toEmbedUrl(url) : null;
652
+ if (embed?.provider !== "youtube") return null;
653
+ const id = embed.src.split("/embed/")[1]?.split("?")[0];
654
+ return id ? `https://i.ytimg.com/vi/${id}/hqdefault.jpg` : null;
655
+ }
656
+ function stepVideo(step) {
657
+ if (!step.videoUrl || !isSafeMediaUrl(step.videoUrl)) return null;
658
+ return { url: step.videoUrl, poster: (step.imageUrl && isSafeMediaUrl(step.imageUrl) ? step.imageUrl : null) ?? videoThumbnailUrl(step.videoUrl) };
659
+ }
577
660
  var MAX_COLLECTION_DEPTH = 3;
578
661
  function matchesAudiences(required, member) {
579
662
  if (!required || required.length === 0) return true;
@@ -643,6 +726,13 @@ function unreadAnnouncements(items, lastSeenAt) {
643
726
  if (Number.isNaN(seen)) return items.length;
644
727
  return items.filter((a) => Date.parse(a.publishedAt) > seen).length;
645
728
  }
729
+ var ARTICLE_HREF_PREFIX = "#guidely-article:";
730
+ var ARTICLE_ID = /^[A-Za-z0-9_-]{1,64}$/;
731
+ function articleIdFromHref(href) {
732
+ if (!href || !href.startsWith(ARTICLE_HREF_PREFIX)) return null;
733
+ const id = href.slice(ARTICLE_HREF_PREFIX.length);
734
+ return ARTICLE_ID.test(id) ? id : null;
735
+ }
646
736
  var CAPTURE_SCREENSHOT_MAX_BYTES = 8 * 1024 * 1024;
647
737
  var DEFAULT_LOCALE = "en";
648
738
  function normalizeLocale(input) {
@@ -1084,7 +1174,22 @@ var KEYS = {
1084
1174
  launcher: (key) => `guidely:launcher:${key}`
1085
1175
  };
1086
1176
 
1087
- // ../sdk-core/dist/chunk-2E2YUQAG.js
1177
+ // ../sdk-core/dist/chunk-GFLVZRTS.js
1178
+ var MAX_PERSISTED_QUEUE_CHARS = 1024 * 1024;
1179
+ function fitPersisted(events) {
1180
+ let size2 = JSON.stringify(events).length;
1181
+ if (size2 <= MAX_PERSISTED_QUEUE_CHARS) return events;
1182
+ const out = events.slice();
1183
+ for (let i = 0; i < out.length && size2 > MAX_PERSISTED_QUEUE_CHARS; i++) {
1184
+ const e = out[i];
1185
+ if (!e.detail) continue;
1186
+ size2 -= JSON.stringify(e.detail).length;
1187
+ const rest = { ...e };
1188
+ delete rest.detail;
1189
+ out[i] = rest;
1190
+ }
1191
+ return out;
1192
+ }
1088
1193
  var EventQueue = class {
1089
1194
  constructor(opts) {
1090
1195
  __publicField2(this, "opts", opts);
@@ -1140,7 +1245,7 @@ var EventQueue = class {
1140
1245
  this.schedule();
1141
1246
  }
1142
1247
  persist() {
1143
- writeJson(this.opts.store, this.opts.storageKey, this.events);
1248
+ writeJson(this.opts.store, this.opts.storageKey, fitPersisted(this.events));
1144
1249
  }
1145
1250
  schedule() {
1146
1251
  if (this.timer || this.retryTimer || this.destroyed) return;
@@ -1390,6 +1495,13 @@ function dismissedAfterMerge(local, server) {
1390
1495
  if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
1391
1496
  return local?.dismissedAt ?? server.dismissedAt ?? null;
1392
1497
  }
1498
+ function environmentFromKey(key) {
1499
+ const prefix = /^pk_([a-z0-9]+)_/.exec(key)?.[1];
1500
+ if (!prefix || prefix === "dev") return "development";
1501
+ if (prefix === "prod") return "production";
1502
+ if (prefix === "stg" || prefix === "stag") return "staging";
1503
+ return prefix;
1504
+ }
1393
1505
 
1394
1506
  // src/core/client.ts
1395
1507
  var import_flags2 = require("@guidelytech/flags");
@@ -1546,6 +1658,593 @@ var TapTracker = class {
1546
1658
  }
1547
1659
  };
1548
1660
 
1661
+ // src/core/breadcrumbs.ts
1662
+ var BreadcrumbBuffer = class {
1663
+ constructor(max = MAX_BREADCRUMBS) {
1664
+ __publicField(this, "max", max);
1665
+ __publicField(this, "items", []);
1666
+ }
1667
+ /** Adds a breadcrumb; `max` (at most the buffer's own) lowers how many are kept. */
1668
+ add(crumb, max = this.max) {
1669
+ const limit = Math.min(max, this.max);
1670
+ this.items.push(crumb);
1671
+ if (this.items.length > limit) this.items.splice(0, this.items.length - limit);
1672
+ }
1673
+ /** Copies, oldest first (an event keeps the crumbs it was captured with). */
1674
+ values() {
1675
+ return this.items.map((c) => ({ ...c, ...c.data ? { data: { ...c.data } } : {} }));
1676
+ }
1677
+ get size() {
1678
+ return this.items.length;
1679
+ }
1680
+ clear() {
1681
+ this.items = [];
1682
+ }
1683
+ };
1684
+ function stripUrl(url) {
1685
+ return String(url).replace(/[?#].*$/, "").replace(/^([a-z][\w+.-]*:\/\/)[^/@]*@/i, "$1").slice(0, 500);
1686
+ }
1687
+ function describeArg(value) {
1688
+ if (typeof value === "string") return value.slice(0, 500);
1689
+ if (value instanceof Error) return `${value.name}: ${value.message}`.slice(0, 500);
1690
+ try {
1691
+ return (JSON.stringify(value) ?? String(value)).slice(0, 500);
1692
+ } catch {
1693
+ return String(value).slice(0, 500);
1694
+ }
1695
+ }
1696
+ function instrumentConsole(target, emit) {
1697
+ if (!target) return () => {
1698
+ };
1699
+ const undo = [];
1700
+ let inside = false;
1701
+ for (const [method, level] of [
1702
+ ["warn", "warning"],
1703
+ ["error", "error"]
1704
+ ]) {
1705
+ const original = target[method];
1706
+ if (typeof original !== "function") continue;
1707
+ const wrapped = function(...args) {
1708
+ const result = original.apply(this, args);
1709
+ if (!inside && !(typeof args[0] === "string" && args[0].startsWith("[Guidely]"))) {
1710
+ inside = true;
1711
+ try {
1712
+ const parts = args.slice(0, 5).map(describeArg);
1713
+ emit({ type: "debug", category: "console", level, message: parts.join(" ").slice(0, 1e3), data: { arguments: parts, logger: "console" } });
1714
+ } catch {
1715
+ } finally {
1716
+ inside = false;
1717
+ }
1718
+ }
1719
+ return result;
1720
+ };
1721
+ target[method] = wrapped;
1722
+ undo.push(() => {
1723
+ if (target[method] === wrapped) target[method] = original;
1724
+ });
1725
+ }
1726
+ return () => undo.forEach((fn) => fn());
1727
+ }
1728
+ function httpLevel(status) {
1729
+ if (!status) return "error";
1730
+ return status >= 500 ? "error" : status >= 400 ? "warning" : "info";
1731
+ }
1732
+ var XHR_INFO = "__guidelyXhr";
1733
+ function instrumentHttp(g, emit, skip, now = Date.now) {
1734
+ if (!g) return () => {
1735
+ };
1736
+ const undo = [];
1737
+ let inFetch = 0;
1738
+ const record2 = (category, method, url, start, status) => {
1739
+ try {
1740
+ const data = { method, url: stripUrl(url), ...status ? { status_code: status } : {}, duration_ms: Math.max(0, Math.round(now() - start)) };
1741
+ emit({ type: "http", category, level: httpLevel(status), data });
1742
+ } catch {
1743
+ }
1744
+ };
1745
+ const originalFetch = g.fetch;
1746
+ if (typeof originalFetch === "function") {
1747
+ const wrappedFetch = function(...args) {
1748
+ const [input, init] = args;
1749
+ let url = "";
1750
+ let method = "GET";
1751
+ try {
1752
+ const req = input;
1753
+ url = typeof req === "string" ? req : typeof req?.url === "string" ? req.url : String(input);
1754
+ method = String(init?.method ?? (req && typeof req === "object" ? req.method : void 0) ?? "GET").toUpperCase();
1755
+ } catch {
1756
+ }
1757
+ if (skip(url)) return originalFetch.apply(this, args);
1758
+ const start = now();
1759
+ let result;
1760
+ inFetch += 1;
1761
+ try {
1762
+ result = originalFetch.apply(this, args);
1763
+ } finally {
1764
+ inFetch -= 1;
1765
+ }
1766
+ if (!result || typeof result.then !== "function") return result;
1767
+ return result.then(
1768
+ (res) => {
1769
+ record2("fetch", method, url, start, typeof res?.status === "number" ? res.status : void 0);
1770
+ return res;
1771
+ },
1772
+ (err) => {
1773
+ record2("fetch", method, url, start, void 0);
1774
+ throw err;
1775
+ }
1776
+ );
1777
+ };
1778
+ g.fetch = wrappedFetch;
1779
+ undo.push(() => {
1780
+ if (g.fetch === wrappedFetch) g.fetch = originalFetch;
1781
+ });
1782
+ }
1783
+ const proto = g.XMLHttpRequest?.prototype;
1784
+ const originalOpen = proto?.open;
1785
+ const originalSend = proto?.send;
1786
+ if (proto && typeof originalOpen === "function" && typeof originalSend === "function") {
1787
+ const open = function(...args) {
1788
+ try {
1789
+ this[XHR_INFO] = { method: String(args[0] ?? "GET").toUpperCase(), url: String(args[1] ?? ""), viaFetch: inFetch > 0 };
1790
+ } catch {
1791
+ }
1792
+ return originalOpen.apply(this, args);
1793
+ };
1794
+ const send = function(...args) {
1795
+ try {
1796
+ const info = this[XHR_INFO];
1797
+ if (info && !info.viaFetch && !skip(info.url) && typeof this.addEventListener === "function") {
1798
+ info.start = now();
1799
+ this.addEventListener("readystatechange", () => {
1800
+ if (this.readyState !== 4 || info.done) return;
1801
+ info.done = true;
1802
+ record2("xhr", info.method, info.url, info.start ?? now(), typeof this.status === "number" && this.status > 0 ? this.status : void 0);
1803
+ });
1804
+ }
1805
+ } catch {
1806
+ }
1807
+ return originalSend.apply(this, args);
1808
+ };
1809
+ proto.open = open;
1810
+ proto.send = send;
1811
+ undo.push(() => {
1812
+ if (proto.open === open) proto.open = originalOpen;
1813
+ if (proto.send === send) proto.send = originalSend;
1814
+ });
1815
+ }
1816
+ return () => undo.forEach((fn) => fn());
1817
+ }
1818
+
1819
+ // src/core/crashHandlers.ts
1820
+ var FATAL_FLUSH_MS = 2e3;
1821
+ var isThenable = (v) => !!v && typeof v.then === "function";
1822
+ function installGlobalHandler(g, report, waitMs = FATAL_FLUSH_MS) {
1823
+ const eu = g.ErrorUtils;
1824
+ if (!eu || typeof eu.setGlobalHandler !== "function") return () => {
1825
+ };
1826
+ let previous;
1827
+ try {
1828
+ previous = eu.getGlobalHandler?.();
1829
+ } catch {
1830
+ previous = void 0;
1831
+ }
1832
+ let active = true;
1833
+ let handlingFatal = false;
1834
+ const handler = (error, isFatal) => {
1835
+ const fatal = !!isFatal;
1836
+ const callPrevious = () => previous?.(error, isFatal);
1837
+ if (!active || fatal && handlingFatal) return callPrevious();
1838
+ let pending;
1839
+ try {
1840
+ pending = report(error, fatal);
1841
+ } catch {
1842
+ }
1843
+ if (!fatal || g.__DEV__ || !isThenable(pending)) return callPrevious();
1844
+ handlingFatal = true;
1845
+ let done = false;
1846
+ const finish = () => {
1847
+ if (done) return;
1848
+ done = true;
1849
+ clearTimeout(timer);
1850
+ handlingFatal = false;
1851
+ try {
1852
+ callPrevious();
1853
+ } catch {
1854
+ }
1855
+ };
1856
+ const timer = setTimeout(finish, waitMs);
1857
+ pending.then(finish, finish);
1858
+ };
1859
+ try {
1860
+ eu.setGlobalHandler(handler);
1861
+ } catch {
1862
+ return () => {
1863
+ };
1864
+ }
1865
+ return () => {
1866
+ active = false;
1867
+ try {
1868
+ if (previous && eu.getGlobalHandler?.() === handler) eu.setGlobalHandler(previous);
1869
+ } catch {
1870
+ }
1871
+ };
1872
+ }
1873
+ function describeRejection(rejection) {
1874
+ if (rejection instanceof Error) return rejection.stack || `${rejection.name}: ${rejection.message}`;
1875
+ try {
1876
+ return typeof rejection === "string" ? rejection : JSON.stringify(rejection) ?? String(rejection);
1877
+ } catch {
1878
+ return String(rejection);
1879
+ }
1880
+ }
1881
+ function hermesTracker(g) {
1882
+ const h = g.HermesInternal;
1883
+ try {
1884
+ if (!h || typeof h.enablePromiseRejectionTracker !== "function" || typeof h.hasPromise === "function" && !h.hasPromise()) return null;
1885
+ } catch {
1886
+ return null;
1887
+ }
1888
+ return { enable: (options) => h.enablePromiseRejectionTracker(options) };
1889
+ }
1890
+ function installRejectionTracking(g, report, tracker) {
1891
+ let active = true;
1892
+ const warn = (message) => {
1893
+ try {
1894
+ g.console?.warn?.(message);
1895
+ } catch {
1896
+ }
1897
+ };
1898
+ const options = {
1899
+ allRejections: true,
1900
+ onUnhandled: (id, rejection) => {
1901
+ if (active) {
1902
+ try {
1903
+ report(rejection);
1904
+ } catch {
1905
+ }
1906
+ }
1907
+ if (g.__DEV__) warn(`Possible unhandled promise rejection (id: ${id}):
1908
+ ${describeRejection(rejection)}`);
1909
+ },
1910
+ onHandled: (id) => {
1911
+ if (g.__DEV__) warn(`Promise rejection handled (id: ${id})
1912
+ This means you can ignore any previous messages of the form "Possible unhandled promise rejection (id: ${id}):"`);
1913
+ }
1914
+ };
1915
+ const t = tracker ?? hermesTracker(g);
1916
+ if (t && typeof t.enable === "function") {
1917
+ try {
1918
+ t.enable(options);
1919
+ return () => {
1920
+ active = false;
1921
+ };
1922
+ } catch {
1923
+ }
1924
+ }
1925
+ if (typeof g.addEventListener === "function") {
1926
+ const listener = (event) => {
1927
+ if (active) report(event?.reason);
1928
+ };
1929
+ try {
1930
+ g.addEventListener("unhandledrejection", listener);
1931
+ return () => {
1932
+ active = false;
1933
+ g.removeEventListener?.("unhandledrejection", listener);
1934
+ };
1935
+ } catch {
1936
+ }
1937
+ }
1938
+ return () => {
1939
+ active = false;
1940
+ };
1941
+ }
1942
+ function runtimeContext(g) {
1943
+ const h = g.HermesInternal;
1944
+ if (!h) return { name: "jsc" };
1945
+ try {
1946
+ const version = h.getRuntimeProperties?.()?.["OSS Release Version"];
1947
+ return { name: "hermes", ...typeof version === "string" && version ? { version: version.slice(0, 50) } : {} };
1948
+ } catch {
1949
+ return { name: "hermes" };
1950
+ }
1951
+ }
1952
+
1953
+ // src/core/stack.ts
1954
+ var AT = /^\s*at (?:(.+?) \()?(?:address at )?(.+?):(\d+):(\d+)\)?\s*$/;
1955
+ var AT_NATIVE = /^\s*at (?:(.+?) \()?(?:native|\[native code\])\)?\s*$/;
1956
+ var JSC = /^\s*([^@]*)@(.+?):(\d+):(\d+)\s*$/;
1957
+ var JSC_BARE = /^\s*(\S+?):(\d+):(\d+)\s*$/;
1958
+ var JSC_NATIVE = /^\s*(?:([^@]*)@)?\[native code\]\s*$/;
1959
+ var BUNDLE = /\.(jsbundle|bundle|hbc)$/i;
1960
+ function normalizeBundleFile(file) {
1961
+ const f = file.trim();
1962
+ if (/^https?:\/\//i.test(f)) return f.replace(/[?#].*$/, "");
1963
+ const path = f.replace(/^file:\/\//i, "").replace(/[?#].*$/, "");
1964
+ const base = path.slice(path.lastIndexOf("/") + 1);
1965
+ return BUNDLE.test(base) ? reactNativeBundleUrl(path) : path;
1966
+ }
1967
+ function inAppFor(file) {
1968
+ if (!file) return false;
1969
+ if (/(^|\/)node_modules\//.test(file) || /(^|\/)InternalBytecode\.js$/.test(file)) return false;
1970
+ if (file.startsWith("app:///") || /^https?:\/\//i.test(file)) return void 0;
1971
+ return true;
1972
+ }
1973
+ function frame(fn, file, line, col) {
1974
+ const name = (fn || "").trim();
1975
+ const normalized = file ? normalizeBundleFile(file).slice(0, 500) : void 0;
1976
+ const inApp = inAppFor(normalized);
1977
+ return {
1978
+ ...name ? { fn: name.slice(0, 200) } : {},
1979
+ ...normalized ? { file: normalized } : {},
1980
+ ...line ? { line: Number(line) } : {},
1981
+ ...col ? { col: Number(col) } : {},
1982
+ ...inApp !== void 0 ? { inApp } : {}
1983
+ };
1984
+ }
1985
+ function parseReactNativeStack(stack, max = MAX_DETAIL_FRAMES) {
1986
+ if (!stack || typeof stack !== "string") return [];
1987
+ const frames = [];
1988
+ for (const raw of stack.split("\n")) {
1989
+ if (frames.length >= max) break;
1990
+ const line = raw.slice(0, 1e3);
1991
+ let m;
1992
+ if (m = AT_NATIVE.exec(line)) frames.push(frame(m[1], void 0));
1993
+ else if (m = AT.exec(line)) frames.push(frame(m[1], m[2], m[3], m[4]));
1994
+ else if (m = JSC_NATIVE.exec(line)) frames.push(frame(m[1], void 0));
1995
+ else if (m = JSC.exec(line)) frames.push(frame(m[1], m[2], m[3], m[4]));
1996
+ else if (m = JSC_BARE.exec(line)) frames.push(frame(void 0, m[1], m[2], m[3]));
1997
+ }
1998
+ return frames;
1999
+ }
2000
+ function toDetailFrames(frames) {
2001
+ return frames.slice().reverse().map((f) => ({
2002
+ ...f.fn ? { function: f.fn } : {},
2003
+ ...f.file ? { filename: f.file, abs_path: f.file } : {},
2004
+ ...f.line !== void 0 ? { lineno: f.line } : {},
2005
+ ...f.col !== void 0 ? { colno: f.col } : {},
2006
+ ...f.inApp !== void 0 ? { in_app: f.inApp } : {}
2007
+ }));
2008
+ }
2009
+ var MAX_FRAMES_BYTES = 6e3;
2010
+ function fitPropertyFrames(frames) {
2011
+ const out = frames.slice(0, 30);
2012
+ while (out.length > 1 && JSON.stringify(out).length > MAX_FRAMES_BYTES) out.pop();
2013
+ return out;
2014
+ }
2015
+
2016
+ // src/core/errors.ts
2017
+ var CONTEXT_KEYS = ["tags", "extra", "level", "fingerprint", "contexts", "user"];
2018
+ function splitHint(hint) {
2019
+ const ctx = {};
2020
+ const properties = {};
2021
+ for (const [k, v] of Object.entries(hint && typeof hint === "object" ? hint : {})) (CONTEXT_KEYS.includes(k) ? ctx : properties)[k] = v;
2022
+ return { ctx, properties };
2023
+ }
2024
+ var nowSeconds = () => Date.now() / 1e3;
2025
+ var ErrorScope = class {
2026
+ constructor(opts) {
2027
+ __publicField(this, "opts", opts);
2028
+ __publicField(this, "tags", {});
2029
+ __publicField(this, "extra", {});
2030
+ __publicField(this, "contexts", {});
2031
+ __publicField(this, "user", {});
2032
+ __publicField(this, "crumbs", new BreadcrumbBuffer(MAX_BREADCRUMBS));
2033
+ }
2034
+ get max() {
2035
+ return Math.max(0, Math.min(MAX_BREADCRUMBS, this.opts()?.maxBreadcrumbs ?? MAX_BREADCRUMBS));
2036
+ }
2037
+ addBreadcrumb(crumb, hint) {
2038
+ if (!crumb || typeof crumb !== "object") return;
2039
+ let c = { timestamp: nowSeconds(), ...crumb };
2040
+ const before = this.opts()?.beforeBreadcrumb;
2041
+ if (before) {
2042
+ try {
2043
+ c = before(c, hint);
2044
+ } catch {
2045
+ }
2046
+ }
2047
+ const max = this.max;
2048
+ if (!c || !max) return;
2049
+ this.crumbs.add(c, max);
2050
+ }
2051
+ breadcrumbs() {
2052
+ return this.crumbs.values();
2053
+ }
2054
+ clear() {
2055
+ this.tags = {};
2056
+ this.extra = {};
2057
+ this.contexts = {};
2058
+ this.user = {};
2059
+ this.crumbs.clear();
2060
+ }
2061
+ };
2062
+ var isError = (v) => v instanceof Error || !!v && typeof v === "object" && typeof v.message === "string" && typeof v.stack === "string";
2063
+ function describe(value) {
2064
+ if (typeof value === "string") return value;
2065
+ try {
2066
+ return JSON.stringify(value) ?? String(value);
2067
+ } catch {
2068
+ return String(value);
2069
+ }
2070
+ }
2071
+ function exceptionFromValue(error, source, handled, fallbackType = "Error") {
2072
+ const e = error;
2073
+ const ok = isError(error);
2074
+ return {
2075
+ type: (ok && e.name || fallbackType).slice(0, 200),
2076
+ message: (ok ? e.message : describe(error)).slice(0, 1e3),
2077
+ frames: ok ? fitPropertyFrames(parseReactNativeStack(e.stack)) : [],
2078
+ source,
2079
+ handled
2080
+ };
2081
+ }
2082
+ function exceptionChain(error, main, mechanism) {
2083
+ const mainFrames = isError(error) ? parseReactNativeStack(error.stack) : main.frames;
2084
+ const chain = [{ type: main.type, value: main.message, mechanism: { type: mechanism, handled: main.handled !== false, exception_id: 0 }, ...mainFrames.length ? { stacktrace: { frames: toDetailFrames(mainFrames) } } : {} }];
2085
+ const seen = /* @__PURE__ */ new Set([error]);
2086
+ let cur = isError(error) ? error.cause : void 0;
2087
+ while (cur !== void 0 && cur !== null && !seen.has(cur) && chain.length < MAX_EXCEPTION_CHAIN) {
2088
+ seen.add(cur);
2089
+ const e = cur;
2090
+ const id = chain.length;
2091
+ const frames = isError(e) ? parseReactNativeStack(e.stack, 50) : [];
2092
+ chain.push({
2093
+ type: (isError(e) && e.name || "Error").slice(0, 200),
2094
+ value: (isError(e) ? e.message : describe(cur)).slice(0, 1e3),
2095
+ mechanism: { type: "chained", source: "cause", handled: true, exception_id: id, parent_id: id - 1 },
2096
+ ...frames.length ? { stacktrace: { frames: toDetailFrames(frames) } } : {}
2097
+ });
2098
+ cur = isError(e) ? e.cause : void 0;
2099
+ }
2100
+ return chain.reverse();
2101
+ }
2102
+ var SENSITIVE_KEY = new RegExp(`${SENSITIVE_PROPERTY_PATTERN.source}|authorization|cookie|api[-_]?key`, "i");
2103
+ function normalizeValue(value, depth = 5, seen = /* @__PURE__ */ new Set()) {
2104
+ if (value === null || typeof value === "boolean") return value;
2105
+ if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
2106
+ if (typeof value === "string") return value.slice(0, 5e3);
2107
+ if (typeof value === "bigint") return value.toString();
2108
+ if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") return void 0;
2109
+ if (value instanceof Date) return Number.isFinite(value.getTime()) ? value.toISOString() : String(value);
2110
+ if (isError(value)) return { name: value.name, message: value.message.slice(0, 1e3) };
2111
+ if (typeof value !== "object" || seen.has(value)) return "[Circular]";
2112
+ if (depth <= 0) return Array.isArray(value) ? "[Array]" : "[Object]";
2113
+ seen.add(value);
2114
+ try {
2115
+ if (Array.isArray(value)) return value.slice(0, MAX_DETAIL_FRAMES).map((v) => normalizeValue(v, depth - 1, seen) ?? null);
2116
+ const out = {};
2117
+ let n = 0;
2118
+ for (const [k, v] of Object.entries(value)) {
2119
+ if (n++ >= 100) break;
2120
+ if (SENSITIVE_KEY.test(k)) out[k] = "[Filtered]";
2121
+ else {
2122
+ const nv = normalizeValue(v, depth - 1, seen);
2123
+ if (nv !== void 0) out[k] = nv;
2124
+ }
2125
+ }
2126
+ return out;
2127
+ } finally {
2128
+ seen.delete(value);
2129
+ }
2130
+ }
2131
+ var record = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
2132
+ var jsonSize = (v) => {
2133
+ try {
2134
+ return JSON.stringify(v)?.length ?? 0;
2135
+ } catch {
2136
+ return Infinity;
2137
+ }
2138
+ };
2139
+ function finalizeDetail(detail, max = MAX_EXCEPTION_DETAIL_BYTES) {
2140
+ let d = normalizeValue(detail, 12);
2141
+ if (!d || typeof d !== "object") return void 0;
2142
+ if (d.tags) d.tags = normalizeTags(Object.fromEntries(Object.entries(d.tags).filter(([k]) => !SENSITIVE_KEY.test(k))));
2143
+ d = trimExceptionDetail(d, max);
2144
+ if (jsonSize(d) <= max) return d;
2145
+ const slim = { ...d.level ? { level: d.level } : {}, ...d.exception ? { exception: d.exception } : {}, ...d.tags ? { tags: d.tags } : {}, ...d.fingerprint ? { fingerprint: d.fingerprint } : {}, ...d.release ? { release: d.release } : {}, ...d.dist ? { dist: d.dist } : {} };
2146
+ return jsonSize(slim) <= max ? slim : void 0;
2147
+ }
2148
+ function buildDetail(input) {
2149
+ const { scope, ctx } = input;
2150
+ const contexts = { ...input.contexts, ...scope.contexts };
2151
+ for (const [k, v] of Object.entries(record(ctx.contexts))) {
2152
+ if (v && typeof v === "object") contexts[k] = v;
2153
+ else delete contexts[k];
2154
+ }
2155
+ const user = { ...input.userId ? { id: input.userId } : {}, ...scope.user, ...record(ctx.user) };
2156
+ const message = input.props.type === "Message";
2157
+ return {
2158
+ level: input.level,
2159
+ platform: "javascript",
2160
+ timestamp: nowSeconds(),
2161
+ ...input.release ? { release: input.release } : {},
2162
+ ...input.dist ? { dist: input.dist } : {},
2163
+ ...input.environment ? { environment: input.environment } : {},
2164
+ ...message ? { message: { formatted: input.props.message } } : { exception: { values: exceptionChain(input.error, input.props, input.mechanism) } },
2165
+ breadcrumbs: { values: scope.breadcrumbs() },
2166
+ contexts,
2167
+ tags: normalizeTags({ ...scope.tags, ...record(ctx.tags) }),
2168
+ extra: { ...scope.extra, ...record(ctx.extra) },
2169
+ ...Object.keys(user).length ? { user } : {},
2170
+ ...Array.isArray(ctx.fingerprint) && ctx.fingerprint.length ? { fingerprint: ctx.fingerprint.map(String) } : {},
2171
+ sdk: input.sdk
2172
+ };
2173
+ }
2174
+ function prepareError(props, extraProperties, detail, opts, fatal, release) {
2175
+ const file = props.frames.find((f) => f.file)?.file;
2176
+ if (opts && errorFilterDrops({ type: props.type, message: props.message, file }, opts)) return null;
2177
+ if (typeof opts?.sampleRate === "number" && opts.sampleRate < 1 && Math.random() >= Math.max(0, opts.sampleRate)) return null;
2178
+ const values = detail.exception?.values ?? [];
2179
+ const thrown = values[values.length - 1];
2180
+ const mechanism = thrown?.mechanism?.type ?? props.source;
2181
+ let properties = { ...props, level: detail.level ?? "error", platform: "javascript", ...mechanism ? { mechanism } : {} };
2182
+ if (release) properties.release = release.slice(0, 100);
2183
+ if (detail.fingerprint?.length) properties.fingerprintParts = detail.fingerprint.slice(0, 20);
2184
+ for (const [k, v] of Object.entries(extraProperties)) if (!(k in properties)) properties[k] = v;
2185
+ let d = detail;
2186
+ if (opts?.beforeSend) {
2187
+ try {
2188
+ const out = opts.beforeSend({ properties, detail: d });
2189
+ if (out === null) return null;
2190
+ if (out && typeof out === "object") {
2191
+ if (out.properties && typeof out.properties === "object") properties = out.properties;
2192
+ if (out.detail && typeof out.detail === "object") d = out.detail;
2193
+ }
2194
+ } catch {
2195
+ }
2196
+ }
2197
+ const finalDetail = finalizeDetail(d);
2198
+ return { eventId: randomId(24), occurredAt: (/* @__PURE__ */ new Date()).toISOString(), properties, ...finalDetail ? { detail: finalDetail } : {}, fatal };
2199
+ }
2200
+ var ExceptionThrottle = class {
2201
+ constructor(max = 5) {
2202
+ __publicField(this, "max", max);
2203
+ __publicField(this, "seen", /* @__PURE__ */ new Map());
2204
+ }
2205
+ allow(props) {
2206
+ const frames = Array.isArray(props.frames) ? props.frames : [];
2207
+ const top = frames[0];
2208
+ const key = `${String(props.type)}|${String(props.message)}|${top ? `${top.file}:${top.line}:${top.col}` : ""}`;
2209
+ const n = this.seen.get(key) ?? 0;
2210
+ if (n >= this.max) return false;
2211
+ if (this.seen.size > 500) this.seen.clear();
2212
+ this.seen.set(key, n + 1);
2213
+ return true;
2214
+ }
2215
+ };
2216
+ function rnVersion(v) {
2217
+ if (!v) return void 0;
2218
+ if (typeof v === "string") return v;
2219
+ if (typeof v.major !== "number") return void 0;
2220
+ return `${v.major}.${v.minor ?? 0}.${v.patch ?? 0}${v.prerelease ? `-${v.prerelease}` : ""}`;
2221
+ }
2222
+ function reactNativeContexts(device, app, g) {
2223
+ const runtime = g ? runtimeContext(g) : void 0;
2224
+ const version = rnVersion(device.reactNativeVersion);
2225
+ return {
2226
+ os: { name: device.platform === "android" ? "Android" : "iOS", ...device.osVersion ? { version: device.osVersion } : {} },
2227
+ device: {
2228
+ family: device.deviceType === "tablet" ? "Tablet" : "Mobile",
2229
+ ...device.model ? { model: device.model } : {},
2230
+ ...device.manufacturer ? { manufacturer: device.manufacturer } : {},
2231
+ ...device.brand ? { brand: device.brand } : {},
2232
+ ...device.simulator !== void 0 ? { simulator: device.simulator } : {}
2233
+ },
2234
+ app: {
2235
+ ...app.appVersion ? { app_version: app.appVersion } : {},
2236
+ ...app.build ? { app_build: app.build } : {},
2237
+ ...app.inForeground !== void 0 ? { in_foreground: app.inForeground } : {}
2238
+ },
2239
+ ...runtime ? { runtime } : {},
2240
+ react_native_context: {
2241
+ ...runtime ? { js_engine: runtime.name } : {},
2242
+ ...version ? { react_native_version: version } : {},
2243
+ ...g ? { turbo_module: !!g.__turboModuleProxy || !!g.RN$Bridgeless, fabric: !!g.nativeFabricUIManager, expo: !!g.expo } : {}
2244
+ }
2245
+ };
2246
+ }
2247
+
1549
2248
  // src/core/targets.ts
1550
2249
  var TargetTimeoutError = class extends Error {
1551
2250
  constructor(ids) {
@@ -2876,38 +3575,38 @@ var SessionRecorder = class {
2876
3575
  this.capturing = true;
2877
3576
  this.captureSoon = false;
2878
3577
  const gen = this.generation;
2879
- let frame = null;
3578
+ let frame2 = null;
2880
3579
  try {
2881
- frame = await this.opts.capturer(await this.opts.captureOptions());
3580
+ frame2 = await this.opts.capturer(await this.opts.captureOptions());
2882
3581
  } catch (err) {
2883
3582
  this.opts.onError?.(err);
2884
3583
  }
2885
3584
  if (gen !== this.generation) return;
2886
3585
  this.capturing = false;
2887
- if (frame) this.onFrame(frame, this.now());
3586
+ if (frame2) this.onFrame(frame2, this.now());
2888
3587
  if (this.sid !== null && this.foreground) this.schedule(this.captureSoon ? TOUCH_SETTLE_MS : this.currentIntervalMs);
2889
3588
  }
2890
- onFrame(frame, at) {
3589
+ onFrame(frame2, at) {
2891
3590
  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;
3591
+ if (id === null || !(frame2.width > 0) || !(frame2.height > 0) || !frame2.base64) return;
3592
+ const sizeChanged = frame2.width !== this.viewportWidth || frame2.height !== this.viewportHeight;
2894
3593
  const keyframe = this.needKeyframe || !this.hasKeyframe || sizeChanged;
2895
- if (!keyframe && this.lastFrame === frame.base64) {
3594
+ if (!keyframe && this.lastFrame === frame2.base64) {
2896
3595
  this.unchanged++;
2897
3596
  return;
2898
3597
  }
2899
3598
  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);
3599
+ this.lastFrame = frame2.base64;
3600
+ this.originX = frame2.originX ?? 0;
3601
+ this.originY = frame2.originY ?? 0;
3602
+ this.viewportWidth = frame2.width;
3603
+ this.viewportHeight = frame2.height;
3604
+ const url = RrwebEvents.dataUrl(frame2.base64);
2906
3605
  const up = this.opts.uploader;
2907
3606
  if (!keyframe) return up.push(id, RrwebEvents.frame(at, url));
2908
3607
  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));
3608
+ up.push(id, RrwebEvents.meta(at, this.opts.href(), frame2.width, frame2.height));
3609
+ up.push(id, RrwebEvents.fullSnapshot(at, url, frame2.width, frame2.height));
2911
3610
  if (!this.hasKeyframe) {
2912
3611
  this.hasKeyframe = true;
2913
3612
  for (const [ts, payload] of this.pendingCustom) up.push(id, RrwebEvents.custom(Math.max(ts, at), "guidely", payload));
@@ -2969,7 +3668,7 @@ var PersistentStore = class {
2969
3668
 
2970
3669
  // src/core/client.ts
2971
3670
  var SDK_NAME = "@guidelytech/react-native";
2972
- var SDK_VERSION = "0.15.0";
3671
+ var SDK_VERSION = "0.17.0";
2973
3672
  var DEFAULT_API_URL = "https://guidelytech.com";
2974
3673
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
2975
3674
  var CACHED_START_MS = 1500;
@@ -2984,6 +3683,10 @@ function settlesWithin(promise, ms) {
2984
3683
  });
2985
3684
  }
2986
3685
  var appVersionKey = (key) => `guidely:app:${key}`;
3686
+ var crashKey = (key) => `guidely:crash:${key}`;
3687
+ var earlyCrashKey = (key) => `guidely:crash-early:${key}`;
3688
+ var MAX_STORED_CRASHES = 10;
3689
+ var MAX_SESSION_EXCEPTIONS = 50;
2987
3690
  var UNKNOWN_VIEW = { selector: "View", tag: "View" };
2988
3691
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
2989
3692
  var emptyState = () => ({ guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] });
@@ -3000,6 +3703,8 @@ var GuidelyClient = class {
3000
3703
  /** Survey answers, guide completions/dismissals and checklist progress, kept until the server has them. */
3001
3704
  __publicField(this, "outbox", null);
3002
3705
  __publicField(this, "config", null);
3706
+ /** The `environment` option as passed (the resolved one is in `options.environment`). */
3707
+ __publicField(this, "envOption");
3003
3708
  __publicField(this, "configFetchedAt", 0);
3004
3709
  __publicField(this, "etag", null);
3005
3710
  __publicField(this, "anonymousId", "");
@@ -3021,6 +3726,8 @@ var GuidelyClient = class {
3021
3726
  __publicField(this, "expanded", /* @__PURE__ */ new Set());
3022
3727
  __publicField(this, "surveyView", null);
3023
3728
  __publicField(this, "resourceCenterOpen", false);
3729
+ /** The article a help-article link asked the resource center to show (`seq` tells repeats apart). */
3730
+ __publicField(this, "resourceCenterArticle", null);
3024
3731
  __publicField(this, "cleanups", []);
3025
3732
  __publicField(this, "timers", /* @__PURE__ */ new Set());
3026
3733
  __publicField(this, "visibilityWatchers", /* @__PURE__ */ new Map());
@@ -3061,6 +3768,16 @@ var GuidelyClient = class {
3061
3768
  __publicField(this, "localizedRc", null);
3062
3769
  /** "What's new" last-seen time when the resource center opened (entries after it read as new). */
3063
3770
  __publicField(this, "announcementsSeenAtOpen", null);
3771
+ // Error monitoring.
3772
+ __publicField(this, "errorScope", new ErrorScope(() => this.options.errors));
3773
+ __publicField(this, "exceptionThrottle", new ExceptionThrottle());
3774
+ /** Errors captured before storage and a config were loaded (whether error tracking is on is unknown until then). */
3775
+ __publicField(this, "earlyErrors", []);
3776
+ /** Crash reports left by earlier launches, sent once a config says error tracking is on. */
3777
+ __publicField(this, "storedCrashes", []);
3778
+ /** This launch's crash reports (normally one: the app ends right after). */
3779
+ __publicField(this, "launchCrashes", []);
3780
+ __publicField(this, "lastAppState", null);
3064
3781
  __publicField(this, "customListeners", /* @__PURE__ */ new Map());
3065
3782
  __publicField(this, "eventBus", {
3066
3783
  on: (name, cb) => {
@@ -3074,9 +3791,10 @@ var GuidelyClient = class {
3074
3791
  }
3075
3792
  });
3076
3793
  if (!options || !options.projectId) throw new Error("[Guidely] `projectId` (your environment public key, e.g. pk_dev_\u2026) is required.");
3794
+ this.envOption = options.environment || void 0;
3077
3795
  this.options = {
3078
3796
  ...options,
3079
- environment: options.environment ?? (options.projectId.startsWith("pk_prod") ? "production" : options.projectId.startsWith("pk_stag") ? "staging" : "development"),
3797
+ environment: options.environment || environmentFromKey(options.projectId),
3080
3798
  apiUrl: options.apiUrl ?? DEFAULT_API_URL,
3081
3799
  analytics: options.analytics ?? true,
3082
3800
  configRefreshMs: options.configRefreshMs ?? 5 * 60 * 1e3,
@@ -3092,6 +3810,7 @@ var GuidelyClient = class {
3092
3810
  if (options.user) this.user = { ...options.user };
3093
3811
  this.locale = options.locale ? normalizeLocale(options.locale) || null : null;
3094
3812
  this.flags = this.createFlags(options.flags);
3813
+ this.installErrorMonitoring();
3095
3814
  }
3096
3815
  createFlags(opts) {
3097
3816
  const base = { sdkKey: this.options.projectId, apiUrl: this.options.apiUrl, fetch: this.options.fetch, sdk: { name: SDK_NAME, version: SDK_VERSION } };
@@ -3139,7 +3858,7 @@ var GuidelyClient = class {
3139
3858
  async bootstrap() {
3140
3859
  const key = this.options.projectId;
3141
3860
  try {
3142
- await this.store.hydrate([KEYS.anon, KEYS.seen, KEYS.session, KEYS.config(key), KEYS.etag(key), KEYS.state(key), KEYS.user(key), KEYS.queue(key), KEYS.outbox(key), KEYS.conversations(key), appVersionKey(key), LAUNCHER_PLACEMENT_KEY]);
3861
+ await this.store.hydrate([KEYS.anon, KEYS.seen, KEYS.session, KEYS.config(key), KEYS.etag(key), KEYS.state(key), KEYS.user(key), KEYS.queue(key), KEYS.outbox(key), KEYS.conversations(key), appVersionKey(key), LAUNCHER_PLACEMENT_KEY, crashKey(key), earlyCrashKey(key)]);
3143
3862
  if (this.destroyed) return;
3144
3863
  this.anonymousId = this.store.get(KEYS.anon) || "";
3145
3864
  this.hadIdentity = !!this.anonymousId;
@@ -3152,7 +3871,9 @@ var GuidelyClient = class {
3152
3871
  this.state = { ...emptyState(), ...readJson(this.store, KEYS.state(key), {}) };
3153
3872
  this.user = this.options.user ? { ...this.options.user } : readJson(this.store, KEYS.user(key), null);
3154
3873
  const saved = readJson(this.store, KEYS.session, null);
3155
- this.session = saved?.id && !this.sessionExpired(Date.parse(saved.lastActiveAt)) ? saved : newSession();
3874
+ this.loadStoredCrashes();
3875
+ const crashed = !!saved?.id && this.storedCrashes.some((c) => c.sessionId === saved.id);
3876
+ this.session = saved?.id && !crashed && !this.sessionExpired(Date.parse(saved.lastActiveAt)) ? saved : newSession();
3156
3877
  this.persistSession();
3157
3878
  this.lastAudiences = new Set(this.state.audienceIds);
3158
3879
  this.syncFlagAttributes();
@@ -3171,9 +3892,10 @@ var GuidelyClient = class {
3171
3892
  this.hydrated = true;
3172
3893
  const cached = readJson(this.store, KEYS.config(key), null);
3173
3894
  this.etag = this.store.get(KEYS.etag(key));
3174
- if (cached && Array.isArray(cached.guides)) this.config = forPlatform(cached, this.bridge.device.platform);
3895
+ if (cached && Array.isArray(cached.guides)) this.useConfig(forPlatform(cached, this.bridge.device.platform));
3175
3896
  this.syncMessenger();
3176
3897
  this.pending.splice(0).forEach((fn) => fn());
3898
+ this.drainErrors();
3177
3899
  const before = this.config?.configVersion;
3178
3900
  const fresh = this.fetchConfig();
3179
3901
  if (!this.config) await fresh;
@@ -3198,22 +3920,33 @@ var GuidelyClient = class {
3198
3920
  if (this.hydrated) fn();
3199
3921
  else this.pending.push(fn);
3200
3922
  }
3923
+ /** Environment guides and flags target: the `environment` option, else the one the loaded config names, else a guess from the key. */
3924
+ get environment() {
3925
+ return this.envOption ?? this.config?.environment ?? this.options.environment;
3926
+ }
3927
+ useConfig(config) {
3928
+ const env = this.environment;
3929
+ this.config = config;
3930
+ if (this.environment !== env) this.syncFlagAttributes();
3931
+ }
3201
3932
  async fetchConfig() {
3202
3933
  try {
3203
3934
  const res = await this.api.get("/config", { etag: this.etag, params: { platform: this.bridge.device.platform } });
3204
3935
  this.configFetchedAt = Date.now();
3205
3936
  if (res.status === 304) return true;
3206
3937
  if (res.ok && res.data && Array.isArray(res.data.guides)) {
3207
- this.config = forPlatform(res.data, this.bridge.device.platform);
3938
+ const config = forPlatform(res.data, this.bridge.device.platform);
3939
+ this.useConfig(config);
3208
3940
  this.etag = res.etag;
3209
3941
  writeJson(this.store, KEYS.config(this.options.projectId), res.data);
3210
3942
  if (res.etag) this.store.set(KEYS.etag(this.options.projectId), res.etag);
3211
3943
  else this.store.remove(KEYS.etag(this.options.projectId));
3212
3944
  this.syncMessenger();
3213
3945
  if (this.hydrated) this.checkLaunch();
3214
- this.emitter.emit("config", this.config);
3946
+ this.drainErrors();
3947
+ this.emitter.emit("config", config);
3215
3948
  this.syncReplay();
3216
- this.log("config loaded", this.config.configVersion, `${this.config.guides.length} guides`);
3949
+ this.log("config loaded", config.configVersion, `${config.guides.length} guides`);
3217
3950
  return true;
3218
3951
  }
3219
3952
  this.reportError(new Error(`[Guidely] config request failed (${res.status})${this.config ? "; using cached config" : ""}`));
@@ -3254,7 +3987,12 @@ var GuidelyClient = class {
3254
3987
  }
3255
3988
  /** Called by the provider with React Native `AppState` changes. */
3256
3989
  setAppState(state) {
3257
- if (this.destroyed || !this.hydrated) return;
3990
+ if (this.destroyed) return;
3991
+ if (state !== this.lastAppState) {
3992
+ this.lastAppState = state;
3993
+ this.autoBreadcrumb("appLifecycle", { type: "navigation", category: "app.lifecycle", message: state, data: { state } });
3994
+ }
3995
+ if (!this.hydrated) return;
3258
3996
  if (state === "background") {
3259
3997
  const wasForeground = this.backgroundedAt === null;
3260
3998
  this.backgroundedAt = Date.now();
@@ -3439,8 +4177,13 @@ var GuidelyClient = class {
3439
4177
  setConsent(consent) {
3440
4178
  this.consent = consent;
3441
4179
  this.whenHydrated(() => {
3442
- if (consent === "denied") this.queue?.clear();
3443
- else if (consent === "granted") void this.queue?.flush();
4180
+ if (consent === "denied") {
4181
+ this.queue?.clear();
4182
+ this.earlyErrors = [];
4183
+ this.storedCrashes = [];
4184
+ this.launchCrashes = [];
4185
+ void this.writeCrashes();
4186
+ } else if (consent === "granted") void this.queue?.flush();
3444
4187
  this.syncReplay();
3445
4188
  });
3446
4189
  }
@@ -3464,6 +4207,7 @@ var GuidelyClient = class {
3464
4207
  accountId: this.user?.account?.id || null,
3465
4208
  sessionId: this.session.id,
3466
4209
  url: this.collectUrls() && this.screenName ? this.currentUrl() : void 0,
4210
+ ...this.appInfo().appVersion ? { appVersion: this.appInfo().appVersion } : {},
3467
4211
  ...extra
3468
4212
  };
3469
4213
  if (extra.properties) event.properties = sanitizeProperties(extra.properties);
@@ -3503,6 +4247,7 @@ var GuidelyClient = class {
3503
4247
  this.whenHydrated(() => {
3504
4248
  const changed = name !== this.screenName;
3505
4249
  if (changed) {
4250
+ this.autoBreadcrumb("navigation", { type: "navigation", category: "navigation", data: { ...this.screenName ? { from: this.screenName } : {}, to: name } });
3506
4251
  this.leaveScreen();
3507
4252
  this.flushHeatmap();
3508
4253
  }
@@ -3666,7 +4411,11 @@ var GuidelyClient = class {
3666
4411
  }
3667
4412
  /** True when taps are worth observing (clicks, rage taps or heatmaps are on); the provider skips resolving views otherwise. */
3668
4413
  isTapCaptureOn() {
3669
- return this.hydrated && (this.autocaptureOn("clicks") || this.autocaptureOn("rageClicks") || this.autocaptureOn("heatmaps"));
4414
+ return this.hydrated && (this.autocaptureOn("clicks") || this.autocaptureOn("rageClicks") || this.autocaptureOn("heatmaps") || this.tapBreadcrumbsOn());
4415
+ }
4416
+ /** Taps become `ui.click` breadcrumbs while errors can be reported (even with click autocapture off). */
4417
+ tapBreadcrumbsOn() {
4418
+ return this.crumbsOn("taps") && this.errorTrackingAllowed();
3670
4419
  }
3671
4420
  /**
3672
4421
  * Records a tap observed by the provider: `click` for an interactive element, `rage_click` for
@@ -3676,8 +4425,8 @@ var GuidelyClient = class {
3676
4425
  recordTap(tap) {
3677
4426
  try {
3678
4427
  if (!this.hydrated || this.destroyed || !Number.isFinite(tap.x) || !Number.isFinite(tap.y)) return;
3679
- const on = { clicks: this.autocaptureOn("clicks"), rage: this.autocaptureOn("rageClicks"), heat: this.autocaptureOn("heatmaps") };
3680
- if (!on.clicks && !on.rage && !on.heat) return;
4428
+ const on = { clicks: this.autocaptureOn("clicks"), rage: this.autocaptureOn("rageClicks"), heat: this.autocaptureOn("heatmaps"), crumb: this.tapBreadcrumbsOn() };
4429
+ if (!on.clicks && !on.rage && !on.heat && !on.crumb) return;
3681
4430
  const t = tap.t ?? Date.now();
3682
4431
  const url = this.screenName ? this.currentUrl() : "";
3683
4432
  this.tapChain = this.tapChain.then(() => this.processTap(tap, t, url, on)).catch((err) => this.reportError(err));
@@ -3694,6 +4443,8 @@ var GuidelyClient = class {
3694
4443
  rect = target.rect;
3695
4444
  }
3696
4445
  if (this.destroyed) return;
4446
+ if (on.crumb && el) this.autoBreadcrumb("taps", { timestamp: t / 1e3, type: "user", category: "ui.click", message: el.selector, data: { selector: el.selector } });
4447
+ if (!on.clicks && !on.rage && !on.heat) return;
3697
4448
  const platform = this.bridge.device.platform;
3698
4449
  const maskAll = !!this.config?.settings?.replay?.maskAllText || !!this.options.sessionReplay?.maskAllText;
3699
4450
  const eventUrl = this.collectUrls() && url ? url : void 0;
@@ -3792,6 +4543,277 @@ var GuidelyClient = class {
3792
4543
  }
3793
4544
  }
3794
4545
  // ---------------------------------------------------------------------------
4546
+ // Error monitoring (docs/error-monitoring/sdks.md)
4547
+ // ---------------------------------------------------------------------------
4548
+ /**
4549
+ * Reports a caught error as an `exception` event (`handled: true`), grouped into an issue on the
4550
+ * Errors page. The second argument takes Sentry's capture context (`tags`, `extra`, `level`,
4551
+ * `fingerprint`, `contexts`, `user`); any other key is attached to the occurrence as a property.
4552
+ * Needs the project's error tracking on (plus analytics and consent); `errors.captureErrors: false`
4553
+ * does not stop it. Returns the event id (empty when nothing is sent).
4554
+ */
4555
+ captureException(error, hint) {
4556
+ if (this.destroyed) return "";
4557
+ try {
4558
+ const { ctx, properties } = splitHint(hint);
4559
+ const props = exceptionFromValue(error, "manual", true);
4560
+ return this.deliverError(this.prepareCapture(error, props, properties, ctx, "generic", ctx.level ?? "error", false));
4561
+ } catch (err) {
4562
+ this.reportError(err);
4563
+ return "";
4564
+ }
4565
+ }
4566
+ /** Reports a message (default level `info`) as its own issue, grouped by the message. */
4567
+ captureMessage(message, level = "info", hint) {
4568
+ if (this.destroyed) return "";
4569
+ try {
4570
+ const { ctx } = splitHint(hint);
4571
+ const props = { type: "Message", message: String(message).slice(0, 1e3), frames: [], source: "message", handled: true };
4572
+ return this.deliverError(this.prepareCapture(void 0, props, {}, ctx, "message", ctx.level ?? level, false));
4573
+ } catch (err) {
4574
+ this.reportError(err);
4575
+ return "";
4576
+ }
4577
+ }
4578
+ /** Adds a breadcrumb (sent with the next errors; the last 100 are kept). */
4579
+ addBreadcrumb(crumb, hint) {
4580
+ if (!this.destroyed) this.errorScope.addBreadcrumb(crumb, hint);
4581
+ }
4582
+ /** Tags every later error (searchable on the issue page); `null` removes the tag. */
4583
+ setTag(key, value) {
4584
+ if (value === null || value === void 0) delete this.errorScope.tags[key];
4585
+ else this.errorScope.tags[key] = String(value);
4586
+ }
4587
+ setTags(tags) {
4588
+ for (const [k, v] of Object.entries(tags ?? {})) this.setTag(k, v);
4589
+ }
4590
+ /** Arbitrary data attached to every later error. */
4591
+ setExtra(key, value) {
4592
+ if (value === void 0) delete this.errorScope.extra[key];
4593
+ else this.errorScope.extra[key] = value;
4594
+ }
4595
+ setExtras(extras) {
4596
+ for (const [k, v] of Object.entries(extras ?? {})) this.setExtra(k, v);
4597
+ }
4598
+ /** A named context (`setContext("cart", { items: 3 })`) on every later error; `null` removes it. */
4599
+ setContext(name, context) {
4600
+ if (context && typeof context === "object") this.errorScope.contexts[name] = context;
4601
+ else delete this.errorScope.contexts[name];
4602
+ }
4603
+ /**
4604
+ * Sentry's `setUser`: identifies the user by `id` (like `identify`) and keeps `email` / `username`
4605
+ * on their errors. `null` forgets the user (`reset()`).
4606
+ */
4607
+ setUser(user) {
4608
+ if (!user) {
4609
+ this.errorScope.user = {};
4610
+ return this.reset();
4611
+ }
4612
+ const { id, email, username, ...traits } = user;
4613
+ this.errorScope.user = { ...email ? { email: String(email) } : {}, ...username ? { username: String(username) } : {} };
4614
+ if (id !== void 0 && id !== null && String(id)) void this.identify(String(id), { ...traits, ...email ? { email } : {}, ...username ? { username } : {} });
4615
+ }
4616
+ crumbsOn(kind) {
4617
+ const b = this.options.errors?.breadcrumbs;
4618
+ return b !== false && !(b && b[kind] === false);
4619
+ }
4620
+ autoBreadcrumb(kind, crumb, hint) {
4621
+ if (!this.destroyed && this.crumbsOn(kind)) this.errorScope.addBreadcrumb(crumb, hint);
4622
+ }
4623
+ /** Errors may be captured: analytics on, consent not denied and, once a config says, the project's error tracking on. */
4624
+ errorsWanted() {
4625
+ if (this.destroyed || !this.options.analytics || this.consent === "denied") return false;
4626
+ const s = this.config?.settings;
4627
+ return !this.config || (s?.analyticsEnabled ?? true) && !!s?.errorTracking?.enabled;
4628
+ }
4629
+ /** Errors may be queued now (a config is known and allows them). */
4630
+ errorTrackingAllowed() {
4631
+ return !!this.config && this.errorsWanted();
4632
+ }
4633
+ /** Requests to the Guidely deployment never become breadcrumbs. */
4634
+ isGuidelyUrl(url) {
4635
+ const base = this.options.apiUrl.replace(/\/+$/, "");
4636
+ return !!base && (url === base || url.startsWith(`${base}/`));
4637
+ }
4638
+ release() {
4639
+ return this.options.release ?? this.appInfo().appVersion;
4640
+ }
4641
+ /** Builds and filters one error (`null` when dropped). */
4642
+ prepareCapture(error, props, properties, ctx, mechanism, level, fatal) {
4643
+ if (!this.errorsWanted()) return null;
4644
+ const info = this.appInfo();
4645
+ const release = this.release();
4646
+ const detail = buildDetail({
4647
+ error,
4648
+ props,
4649
+ mechanism,
4650
+ scope: this.errorScope,
4651
+ ctx,
4652
+ level,
4653
+ contexts: reactNativeContexts(this.bridge.device, { ...info, inForeground: this.backgroundedAt === null }, this.bridge.globals),
4654
+ release,
4655
+ dist: this.options.errors?.dist ?? info.build,
4656
+ environment: this.environment,
4657
+ userId: this.user?.id || null,
4658
+ sdk: { name: SDK_NAME, version: SDK_VERSION }
4659
+ });
4660
+ return prepareError(props, properties, detail, this.options.errors, fatal, release);
4661
+ }
4662
+ /** Queues a prepared error, or holds it until storage and a config are loaded. Returns its event id ("" when dropped). */
4663
+ deliverError(p) {
4664
+ if (!p) return "";
4665
+ if (!this.hydrated || !this.config) {
4666
+ if (this.earlyErrors.length >= 10) return "";
4667
+ this.earlyErrors.push(p);
4668
+ return p.eventId;
4669
+ }
4670
+ return this.sendError(p) ? p.eventId : "";
4671
+ }
4672
+ sendError(p) {
4673
+ if (!this.errorTrackingAllowed()) return false;
4674
+ if (!p.fatal && !this.exceptionThrottle.allow(p.properties)) return false;
4675
+ const count = this.session.exceptions ?? 0;
4676
+ if (!p.fatal && count >= MAX_SESSION_EXCEPTIONS) return false;
4677
+ const event = this.enqueue("exception", { eventId: p.eventId, occurredAt: p.occurredAt, properties: p.properties, ...p.detail ? { detail: p.detail } : {} });
4678
+ if (!event) return false;
4679
+ this.session.exceptions = count + 1;
4680
+ this.persistSession();
4681
+ return true;
4682
+ }
4683
+ /** Sends what waited for storage and a config: earlier launches' crash reports, then early errors. */
4684
+ drainErrors() {
4685
+ if (!this.hydrated || !this.config || this.destroyed) return;
4686
+ const stored = this.storedCrashes.splice(0);
4687
+ if (stored.length) {
4688
+ if (this.errorTrackingAllowed()) {
4689
+ for (const c of stored) this.queue?.enqueue({ ...c, anonymousId: c.anonymousId || this.anonymousId, sessionId: c.sessionId || this.session.id });
4690
+ }
4691
+ void this.writeCrashes();
4692
+ }
4693
+ this.earlyErrors.splice(0).forEach((p) => this.sendError(p));
4694
+ }
4695
+ loadStoredCrashes() {
4696
+ const key = this.options.projectId;
4697
+ const valid = (c) => !!c && typeof c === "object" && typeof c.eventId === "string" && c.name === "exception";
4698
+ const read2 = (k) => {
4699
+ const v = readJson(this.store, k, []);
4700
+ return Array.isArray(v) ? v.filter(valid) : [];
4701
+ };
4702
+ const seen = /* @__PURE__ */ new Set();
4703
+ this.storedCrashes = [...read2(crashKey(key)), ...read2(earlyCrashKey(key))].filter((c) => !seen.has(c.eventId) && !!seen.add(c.eventId)).slice(-MAX_STORED_CRASHES);
4704
+ this.store.remove(earlyCrashKey(key));
4705
+ if (this.storedCrashes.length) void this.writeCrashes();
4706
+ }
4707
+ /**
4708
+ * Writes the unsent crash reports to storage, directly (not behind other pending writes) so the
4709
+ * write starts before the app ends. Resolves when the storage has it.
4710
+ */
4711
+ writeCrashes() {
4712
+ const storage = this.options.storage;
4713
+ const key = this.options.projectId;
4714
+ const direct = (op) => {
4715
+ try {
4716
+ return Promise.resolve(op()).catch((err) => this.log("crash report storage error", err));
4717
+ } catch (err) {
4718
+ this.log("crash report storage error", err);
4719
+ return Promise.resolve();
4720
+ }
4721
+ };
4722
+ if (!this.hydrated) {
4723
+ const json2 = JSON.stringify(this.launchCrashes.slice(-MAX_STORED_CRASHES));
4724
+ return storage ? direct(() => storage.setItem(earlyCrashKey(key), json2)) : Promise.resolve();
4725
+ }
4726
+ const seen = /* @__PURE__ */ new Set();
4727
+ const all = [...this.storedCrashes, ...this.launchCrashes].filter((c) => !seen.has(c.eventId) && !!seen.add(c.eventId)).slice(-MAX_STORED_CRASHES);
4728
+ if (!all.length) {
4729
+ this.store.remove(crashKey(key));
4730
+ return storage ? direct(() => storage.removeItem(crashKey(key))) : Promise.resolve();
4731
+ }
4732
+ const json = JSON.stringify(all);
4733
+ this.store.set(crashKey(key), json);
4734
+ return storage ? direct(() => storage.setItem(crashKey(key), json)) : Promise.resolve();
4735
+ }
4736
+ /**
4737
+ * A fatal JS error: React Native ends a release app right after the global handler, so the report
4738
+ * is written to storage at once (and sent on the next launch), and sent now as well when the
4739
+ * network allows. The promise tells the handler when React Native's own handler may run.
4740
+ */
4741
+ saveCrash(p) {
4742
+ if (!this.errorsWanted()) return void 0;
4743
+ const info = this.appInfo();
4744
+ const event = {
4745
+ eventId: p.eventId,
4746
+ name: "exception",
4747
+ occurredAt: p.occurredAt,
4748
+ anonymousId: this.anonymousId,
4749
+ userId: this.user?.id || null,
4750
+ accountId: this.user?.account?.id || null,
4751
+ ...this.hydrated ? { sessionId: this.session.id } : {},
4752
+ ...this.collectUrls() && this.screenName ? { url: this.currentUrl() } : {},
4753
+ ...info.appVersion ? { appVersion: info.appVersion } : {},
4754
+ properties: sanitizeProperties(p.properties),
4755
+ ...p.detail ? { detail: p.detail } : {}
4756
+ };
4757
+ this.launchCrashes.push(event);
4758
+ const written = this.writeCrashes();
4759
+ if (!this.hydrated || !this.config || !this.sendError(p)) return written;
4760
+ const sent = this.queue?.flush() ?? Promise.resolve();
4761
+ return Promise.all([written, sent]).then(async () => {
4762
+ if (this.queue?.peek().some((e) => e.eventId === event.eventId)) return;
4763
+ this.launchCrashes = this.launchCrashes.filter((c) => c.eventId !== event.eventId);
4764
+ await this.writeCrashes();
4765
+ });
4766
+ }
4767
+ /** An uncaught JS error from `ErrorUtils` (fatal ones end the app in release builds). */
4768
+ captureGlobalError(error, fatal) {
4769
+ try {
4770
+ const props = exceptionFromValue(error, "onerror", false);
4771
+ if (props.message.startsWith("[Guidely]")) return void 0;
4772
+ const p = this.prepareCapture(error, props, {}, {}, "onerror", fatal ? "fatal" : "error", fatal);
4773
+ if (!p) return void 0;
4774
+ if (fatal) return this.saveCrash(p);
4775
+ this.deliverError(p);
4776
+ } catch (err) {
4777
+ this.log("error capture failed", err);
4778
+ }
4779
+ return void 0;
4780
+ }
4781
+ captureRejection(reason) {
4782
+ try {
4783
+ const props = exceptionFromValue(reason, "unhandledrejection", false, "UnhandledRejection");
4784
+ if (props.message.startsWith("[Guidely]")) return;
4785
+ this.deliverError(this.prepareCapture(reason, props, {}, {}, "unhandledrejection", "error", false));
4786
+ } catch (err) {
4787
+ this.log("error capture failed", err);
4788
+ }
4789
+ }
4790
+ /**
4791
+ * Guide breadcrumbs, and (with the bridge's JS globals) the global error handler, promise
4792
+ * rejection tracking and console / network breadcrumbs. Undone by `destroy()`.
4793
+ */
4794
+ installErrorMonitoring() {
4795
+ const guideCrumb = (message, guide, stepIndex) => this.autoBreadcrumb("guides", { type: "info", category: "guide", message, data: { guideId: guide.id, ...stepIndex !== void 0 ? { stepIndex } : {} } });
4796
+ this.cleanups.push(
4797
+ this.emitter.on("guide:start", (g2) => guideCrumb("Guide shown", g2)),
4798
+ this.emitter.on("guide:step", (s) => guideCrumb("Guide step", s.guide, s.stepIndex)),
4799
+ this.emitter.on("guide:complete", (g2) => guideCrumb("Guide completed", g2)),
4800
+ this.emitter.on("guide:dismiss", (g2) => guideCrumb("Guide dismissed", g2)),
4801
+ this.emitter.on("guide:skip", (g2) => guideCrumb("Guide skipped", g2))
4802
+ );
4803
+ const g = this.bridge.globals;
4804
+ if (!g) return;
4805
+ try {
4806
+ if (this.crumbsOn("console")) this.cleanups.push(instrumentConsole(g.console, (c) => this.autoBreadcrumb("console", c)));
4807
+ if (this.crumbsOn("http")) this.cleanups.push(instrumentHttp(g, (c) => this.autoBreadcrumb("http", c), (url) => this.isGuidelyUrl(url)));
4808
+ if (this.options.errors?.captureErrors !== false) {
4809
+ this.cleanups.push(installGlobalHandler(g, (error, fatal) => this.captureGlobalError(error, fatal)));
4810
+ this.cleanups.push(installRejectionTracking(g, (reason) => this.captureRejection(reason), this.options.errors?.rejectionTracker));
4811
+ }
4812
+ } catch (err) {
4813
+ this.reportError(err);
4814
+ }
4815
+ }
4816
+ // ---------------------------------------------------------------------------
3795
4817
  // Targeting + triggers
3796
4818
  // ---------------------------------------------------------------------------
3797
4819
  /** Builds the targeting context for the current moment. */
@@ -3817,7 +4839,7 @@ var GuidelyClient = class {
3817
4839
  completedChecklists: new Set(keysOf(Object.entries(this.state.checklists).filter(([, p]) => !!p.completedAt).map(([id]) => id))),
3818
4840
  audienceIds: new Set(this.state.audienceIds),
3819
4841
  now: /* @__PURE__ */ new Date(),
3820
- environment: this.options.environment,
4842
+ environment: this.environment,
3821
4843
  locale: device.locale,
3822
4844
  platform: device.platform,
3823
4845
  screen: this.screenName,
@@ -4186,6 +5208,8 @@ var GuidelyClient = class {
4186
5208
  */
4187
5209
  navigate(url) {
4188
5210
  if (!url) return;
5211
+ const articleId = articleIdFromHref(url);
5212
+ if (articleId) return this.openArticle(articleId);
4189
5213
  try {
4190
5214
  if (this.options.onNavigate?.(url) === true) return;
4191
5215
  if (/^[a-z][a-z0-9+.-]*:/i.test(url)) this.bridge.openExternal(url);
@@ -4209,7 +5233,7 @@ var GuidelyClient = class {
4209
5233
  for (const f of [...this.flows.values(), ...this.active ? [this.active] : []]) if (f.view) steps.push({ ...f.view, expanded: this.expanded.has(f.guide.id) });
4210
5234
  const checklists = [];
4211
5235
  this.checklists.forEach((w, id) => checklists.push({ guide: w.guide, progress: this.getChecklistProgress(id), open: w.open }));
4212
- this.ui = { steps, checklists, survey: this.surveyView, resourceCenterOpen: this.resourceCenterOpen, theme: this.themeFor(), messenger: !!this.messenger };
5236
+ this.ui = { steps, checklists, survey: this.surveyView, resourceCenterOpen: this.resourceCenterOpen, resourceCenterArticle: this.resourceCenterArticle, theme: this.themeFor(), messenger: !!this.messenger };
4213
5237
  for (const cb of Array.from(this.uiListeners)) {
4214
5238
  try {
4215
5239
  cb();
@@ -4229,6 +5253,12 @@ var GuidelyClient = class {
4229
5253
  closeStep(guideId) {
4230
5254
  this.flowFor(guideId)?.controller.dismiss();
4231
5255
  }
5256
+ /** The video on a step was tapped: records `guide_video_clicked`, then opens the video outside the app. */
5257
+ openVideo(guideId, url) {
5258
+ const v = this.flowFor(guideId)?.view;
5259
+ if (v) this.enqueue("guide_video_clicked", { guideId: v.guide.id, guideVersion: v.guide.version, stepId: v.step.id, stepIndex: v.index, properties: { videoUrl: url } });
5260
+ this.navigate(url);
5261
+ }
4232
5262
  /** The dimmed backdrop behind a step was tapped. */
4233
5263
  backdropPressed(guideId) {
4234
5264
  const f = this.flowFor(guideId);
@@ -4501,10 +5531,40 @@ var GuidelyClient = class {
4501
5531
  this.emitter.emit("resource-center:open");
4502
5532
  this.notify();
4503
5533
  }
5534
+ /**
5535
+ * Opens one help article: in Scout's Help tab when the messenger is on, otherwise in the resource
5536
+ * center. Help-article links in guides and articles (`#guidely-article:<id>`) call this.
5537
+ */
5538
+ openArticle(articleId) {
5539
+ if (this.messenger) {
5540
+ this.whenHydrated(() => void this.messenger?.openArticle(articleId));
5541
+ return;
5542
+ }
5543
+ if (!this.getResourceCenter()) {
5544
+ this.log("openArticle: the resource center is not enabled or published for this environment");
5545
+ return;
5546
+ }
5547
+ this.resourceCenterArticle = { id: articleId, seq: (this.resourceCenterArticle?.seq ?? 0) + 1 };
5548
+ if (this.resourceCenterOpen) this.notify();
5549
+ else this.openResourceCenter();
5550
+ }
5551
+ /** A resource-center article with its body: from the config, or fetched when the config left it out. */
5552
+ async loadArticle(articleId) {
5553
+ const listed = this.getResourceCenter()?.articles.find((a) => a.id === articleId);
5554
+ if (listed && !listed.bodyOmitted) return listed;
5555
+ try {
5556
+ const locale = this.articleLocale();
5557
+ const res = await this.api.get(`/articles/${encodeURIComponent(articleId)}`, locale ? { params: { locale } } : {});
5558
+ return res.ok && res.data ? res.data.article : null;
5559
+ } catch {
5560
+ return null;
5561
+ }
5562
+ }
4504
5563
  closeResourceCenter() {
4505
5564
  if (this.messenger) return this.closeMessenger();
4506
5565
  if (!this.resourceCenterOpen) return;
4507
5566
  this.resourceCenterOpen = false;
5567
+ this.resourceCenterArticle = null;
4508
5568
  this.notify();
4509
5569
  }
4510
5570
  // ---------------------------------------------------------------------------
@@ -4600,7 +5660,7 @@ var GuidelyClient = class {
4600
5660
  locale: d.locale,
4601
5661
  appVersion: this.options.appVersion ?? d.appVersion,
4602
5662
  osVersion: d.osVersion,
4603
- environment: this.options.environment,
5663
+ environment: this.environment,
4604
5664
  ...this.screenName ? { screen: this.screenName, path: this.currentUrl() } : {},
4605
5665
  ...this.user?.traits ?? {},
4606
5666
  id: this.user?.id || this.anonymousId || void 0,
@@ -4725,6 +5785,35 @@ function detectAppVersion(src) {
4725
5785
  return {};
4726
5786
  }
4727
5787
  }
5788
+ function detectDeviceModel(src) {
5789
+ try {
5790
+ const expo = src.expoModules?.ExpoDevice ?? src.nativeModules.ExpoDevice;
5791
+ const info = src.nativeModules.RNDeviceInfo;
5792
+ const c = src.platform.constants ?? {};
5793
+ const android = src.platform.OS === "android";
5794
+ const model = text(constant(expo, "modelName")) ?? text(constant(info, "model")) ?? (android ? text(c.Model) : void 0);
5795
+ const manufacturer = text(constant(expo, "manufacturer")) ?? (android ? text(c.Manufacturer) : "Apple");
5796
+ const brand = text(constant(expo, "brand")) ?? text(constant(info, "brand")) ?? (android ? text(c.Brand) : void 0);
5797
+ const isDevice = constant(expo, "isDevice");
5798
+ return {
5799
+ ...model ? { model } : {},
5800
+ ...manufacturer ? { manufacturer } : {},
5801
+ ...brand ? { brand } : {},
5802
+ ...typeof isDevice === "boolean" ? { simulator: !isDevice } : {}
5803
+ };
5804
+ } catch {
5805
+ return {};
5806
+ }
5807
+ }
5808
+ function detectReactNativeVersion(src) {
5809
+ try {
5810
+ const v = src.platform.constants?.reactNativeVersion;
5811
+ if (!v || typeof v.major !== "number") return void 0;
5812
+ return { major: v.major, minor: Number(v.minor) || 0, patch: Number(v.patch) || 0, ...v.prerelease != null ? { prerelease: String(v.prerelease) } : {} };
5813
+ } catch {
5814
+ return void 0;
5815
+ }
5816
+ }
4728
5817
  function detectOsVersion(src) {
4729
5818
  try {
4730
5819
  const c = src.platform.constants ?? {};
@@ -4857,7 +5946,7 @@ function measureHost(host) {
4857
5946
  done(null);
4858
5947
  });
4859
5948
  }
4860
- function describe(f, masked, opts, withText) {
5949
+ function describe2(f, masked, opts, withText) {
4861
5950
  const props = f.memoizedProps && typeof f.memoizedProps === "object" ? f.memoizedProps : {};
4862
5951
  const host = firstHost(f);
4863
5952
  const el = { selector: idSelector(f) ?? pathSelector(f), tag: componentName(f.type) ?? "View" };
@@ -4902,8 +5991,8 @@ function resolveFiber(target, opts = {}) {
4902
5991
  const element = press ?? role;
4903
5992
  return {
4904
5993
  ignored: false,
4905
- element: element ? describe(element, masked, opts, true) : null,
4906
- hit: hitFiber ? describe(hitFiber, masked, opts, componentName(hitFiber.type) === "Text") : null
5994
+ element: element ? describe2(element, masked, opts, true) : null,
5995
+ hit: hitFiber ? describe2(hitFiber, masked, opts, componentName(hitFiber.type) === "Text") : null
4907
5996
  };
4908
5997
  } catch {
4909
5998
  return none;
@@ -5036,7 +6125,7 @@ function parseHtml(html) {
5036
6125
  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;
5037
6126
  else current.runs.push(run);
5038
6127
  };
5039
- const size = (attrs, name) => {
6128
+ const size2 = (attrs, name) => {
5040
6129
  const n = Number(attr(attrs, name));
5041
6130
  return n > 0 ? n : void 0;
5042
6131
  };
@@ -5098,8 +6187,8 @@ function parseHtml(html) {
5098
6187
  }
5099
6188
  } else if (tag === "img" && !closing) {
5100
6189
  const src = attr(attrs, "src");
5101
- const w = size(attrs, "width");
5102
- const h = size(attrs, "height");
6190
+ const w = size2(attrs, "width");
6191
+ const h = size2(attrs, "height");
5103
6192
  if (isSafeMediaUrl(src)) pdf.pages.push({ src, alt: attr(attrs, "alt") ?? "", ...w && h ? { width: w, height: h } : {} });
5104
6193
  }
5105
6194
  continue;
@@ -5273,10 +6362,10 @@ function BodyPdf({ block, palette: p, onLink }) {
5273
6362
  ] }) })
5274
6363
  ] });
5275
6364
  }
5276
- function RichText({ html, palette: p, onLink, size = 15 }) {
6365
+ function RichText({ html, palette: p, onLink, size: size2 = 15 }) {
5277
6366
  const blocks = (0, import_react2.useMemo)(() => parseHtml(html), [html]);
5278
6367
  if (!blocks.length) return null;
5279
- const base = { color: p.text, fontSize: size, lineHeight: Math.round(size * 1.42), fontFamily: p.fontFamily };
6368
+ const base = { color: p.text, fontSize: size2, lineHeight: Math.round(size2 * 1.42), fontFamily: p.fontFamily };
5280
6369
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.View, { style: styles2.wrap, children: blocks.map((b, i) => {
5281
6370
  if (b.kind === "image") return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(BodyImage, { block: b, palette: p }, i);
5282
6371
  if (b.kind === "video") return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(BodyVideo, { block: b, palette: p, onLink }, i);
@@ -5479,17 +6568,17 @@ function useReduceMotion() {
5479
6568
  }, []);
5480
6569
  return on;
5481
6570
  }
5482
- function MessengerLauncher({ store, client, palette: p, style, frame, defaultSide }) {
6571
+ function MessengerLauncher({ store, client, palette: p, style, frame: frame2, defaultSide }) {
5483
6572
  const snap = useMessenger(store);
5484
6573
  const reduceMotion = useReduceMotion();
5485
6574
  const [placement, setPlacement] = (0, import_react3.useState)(() => client.getMessengerLauncherPlacement());
5486
6575
  const pan = (0, import_react3.useRef)(new import_react_native5.Animated.ValueXY()).current;
5487
6576
  const offset = (0, import_react3.useRef)({ x: 0, y: 0 });
5488
6577
  const dragging = (0, import_react3.useRef)(false);
5489
- const live = (0, import_react3.useRef)({ frame, defaultSide, reduceMotion });
5490
- live.current = { frame, defaultSide, reduceMotion };
5491
- const { width, height, size, defaultBottom } = frame;
5492
- const { top, bottom, left, right } = frame.insets;
6578
+ const live = (0, import_react3.useRef)({ frame: frame2, defaultSide, reduceMotion });
6579
+ live.current = { frame: frame2, defaultSide, reduceMotion };
6580
+ const { width, height, size: size2, defaultBottom } = frame2;
6581
+ const { top, bottom, left, right } = frame2.insets;
5493
6582
  (0, import_react3.useLayoutEffect)(() => {
5494
6583
  if (dragging.current) return;
5495
6584
  const f = live.current.frame;
@@ -5497,7 +6586,7 @@ function MessengerLauncher({ store, client, palette: p, style, frame, defaultSid
5497
6586
  const at = placement ? placementPoint(f, placement) : anchor;
5498
6587
  offset.current = { x: at.x - anchor.x, y: at.y - anchor.y };
5499
6588
  pan.setValue(offset.current);
5500
- }, [pan, placement, defaultSide, width, height, size, defaultBottom, top, bottom, left, right]);
6589
+ }, [pan, placement, defaultSide, width, height, size2, defaultBottom, top, bottom, left, right]);
5501
6590
  const responder = (0, import_react3.useMemo)(() => {
5502
6591
  const moved = (g) => Math.hypot(g.dx, g.dy) > LAUNCHER_DRAG_THRESHOLD;
5503
6592
  const anchor = () => placementPoint(live.current.frame, { side: live.current.defaultSide, fraction: 0 });
@@ -5850,7 +6939,7 @@ var styles4 = import_react_native5.StyleSheet.create({
5850
6939
  var import_react4 = require("react");
5851
6940
  var import_react_native6 = require("react-native");
5852
6941
  var import_jsx_runtime6 = require("react/jsx-runtime");
5853
- function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
6942
+ function ResourceCenter({ client, palette: p, open, articleRequest, topInset, bottomInset }) {
5854
6943
  const rc = client.getResourceCenter();
5855
6944
  const [query, setQuery] = (0, import_react4.useState)("");
5856
6945
  const [results, setResults] = (0, import_react4.useState)(null);
@@ -5863,6 +6952,19 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
5863
6952
  setArticle(null);
5864
6953
  }
5865
6954
  }, [open]);
6955
+ (0, import_react4.useEffect)(() => {
6956
+ if (!open || !articleRequest) return;
6957
+ let alive = true;
6958
+ void client.loadArticle(articleRequest.id).then((a) => {
6959
+ if (!alive || !a) return;
6960
+ if (a.externalUrl) return client.navigate(a.externalUrl);
6961
+ client.articleViewed(a);
6962
+ setArticle(a);
6963
+ });
6964
+ return () => {
6965
+ alive = false;
6966
+ };
6967
+ }, [client, open, articleRequest?.seq]);
5866
6968
  (0, import_react4.useEffect)(() => {
5867
6969
  const q = query.trim();
5868
6970
  if (q.length < 2) {
@@ -6008,18 +7110,18 @@ var import_react_native8 = require("react-native");
6008
7110
  var MARGIN = 12;
6009
7111
  var ARROW_INSET = 18;
6010
7112
  var opposite = { top: "bottom", bottom: "top", left: "right", right: "left" };
6011
- function positionCard(target, size, viewport, insets, placement, offset) {
7113
+ function positionCard(target, size2, viewport, insets, placement, offset) {
6012
7114
  const [rawSide, align] = placement.split("-");
6013
7115
  const fits = (side2) => {
6014
7116
  switch (side2) {
6015
7117
  case "bottom":
6016
- return target.y + target.height + offset + size.height <= viewport.height - insets.bottom - MARGIN;
7118
+ return target.y + target.height + offset + size2.height <= viewport.height - insets.bottom - MARGIN;
6017
7119
  case "top":
6018
- return target.y - offset - size.height >= insets.top + MARGIN;
7120
+ return target.y - offset - size2.height >= insets.top + MARGIN;
6019
7121
  case "right":
6020
- return target.x + target.width + offset + size.width <= viewport.width - insets.right - MARGIN;
7122
+ return target.x + target.width + offset + size2.width <= viewport.width - insets.right - MARGIN;
6021
7123
  case "left":
6022
- return target.x - offset - size.width >= insets.left + MARGIN;
7124
+ return target.x - offset - size2.width >= insets.left + MARGIN;
6023
7125
  }
6024
7126
  };
6025
7127
  const order = rawSide === "auto" ? ["bottom", "top"] : [rawSide, opposite[rawSide], "bottom", "top"];
@@ -6028,23 +7130,23 @@ function positionCard(target, size, viewport, insets, placement, offset) {
6028
7130
  const clamp3 = (v, min, max) => Math.max(min, Math.min(max, v));
6029
7131
  if (side === "top" || side === "bottom") {
6030
7132
  const cx = target.x + target.width / 2;
6031
- let x2 = align === "start" ? target.x : align === "end" ? target.x + target.width - size.width : cx - size.width / 2;
6032
- x2 = clamp3(x2, insets.left + MARGIN, Math.max(insets.left + MARGIN, viewport.width - insets.right - MARGIN - size.width));
6033
- let y2 = side === "bottom" ? target.y + target.height + offset : target.y - offset - size.height;
6034
- y2 = clamp3(y2, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
6035
- return { x: x2, y: y2, side, arrow: clamp3(cx - x2, ARROW_INSET, size.width - ARROW_INSET) };
7133
+ let x2 = align === "start" ? target.x : align === "end" ? target.x + target.width - size2.width : cx - size2.width / 2;
7134
+ x2 = clamp3(x2, insets.left + MARGIN, Math.max(insets.left + MARGIN, viewport.width - insets.right - MARGIN - size2.width));
7135
+ let y2 = side === "bottom" ? target.y + target.height + offset : target.y - offset - size2.height;
7136
+ y2 = clamp3(y2, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size2.height));
7137
+ return { x: x2, y: y2, side, arrow: clamp3(cx - x2, ARROW_INSET, size2.width - ARROW_INSET) };
6036
7138
  }
6037
7139
  const cy = target.y + target.height / 2;
6038
- let y = align === "start" ? target.y : align === "end" ? target.y + target.height - size.height : cy - size.height / 2;
6039
- y = clamp3(y, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
6040
- const x = side === "right" ? target.x + target.width + offset : target.x - offset - size.width;
6041
- return { x, y, side, arrow: clamp3(cy - y, ARROW_INSET, size.height - ARROW_INSET) };
7140
+ let y = align === "start" ? target.y : align === "end" ? target.y + target.height - size2.height : cy - size2.height / 2;
7141
+ y = clamp3(y, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size2.height));
7142
+ const x = side === "right" ? target.x + target.width + offset : target.x - offset - size2.width;
7143
+ return { x, y, side, arrow: clamp3(cy - y, ARROW_INSET, size2.height - ARROW_INSET) };
6042
7144
  }
6043
7145
 
6044
7146
  // src/ui/StepCard.tsx
6045
7147
  var import_react_native7 = require("react-native");
6046
7148
  var import_jsx_runtime7 = (
6047
- // An empty description marks the image as decorative: screen readers skip it.
7149
+ // The video opens outside the app (browser or the provider's app), like a new tab on the web.
6048
7150
  require("react/jsx-runtime")
6049
7151
  );
6050
7152
  function StepCard({ view, client, palette: p, width, style, compact }) {
@@ -6053,16 +7155,33 @@ function StepCard({ view, client, palette: p, width, style, compact }) {
6053
7155
  const body = step.type === "custom" ? step.customHtml || step.body : step.body;
6054
7156
  const showProgress = step.showProgress && view.total > 1;
6055
7157
  const onLink = (url) => client.navigate(url);
7158
+ const video = stepVideo(step);
6056
7159
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Surface, { palette: p, style: [{ width }, style], children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { accessible: false, accessibilityViewIsModal: view.asModal || step.type === "modal", style: compact ? styles6.compact : styles6.inner, children: [
6057
- step.imageUrl ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
6058
- import_react_native7.Image,
7160
+ video ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
7161
+ import_react_native7.Pressable,
6059
7162
  {
6060
- source: { uri: step.imageUrl },
6061
- style: [styles6.image, { borderRadius: Math.max(0, p.radius - 4) }],
6062
- resizeMode: "cover",
6063
- accessibilityIgnoresInvertColors: true,
6064
- ...step.imageAlt?.trim() ? { accessible: true, accessibilityRole: "image", accessibilityLabel: step.imageAlt.trim() } : { accessible: false, importantForAccessibility: "no" }
7163
+ onPress: () => client.openVideo(guide.id, video.url),
7164
+ accessibilityRole: "button",
7165
+ accessibilityLabel: step.title ? `Play video: ${step.title}` : "Play video",
7166
+ accessibilityHint: "Opens the video outside the app",
7167
+ style: [styles6.video, { borderRadius: Math.max(0, p.radius - 4) }],
7168
+ children: [
7169
+ video.poster ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Image, { source: { uri: video.poster }, style: import_react_native7.StyleSheet.absoluteFill, resizeMode: "cover", accessibilityIgnoresInvertColors: true, accessible: false }) : null,
7170
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: styles6.play, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.View, { style: styles6.playTriangle }) })
7171
+ ]
6065
7172
  }
7173
+ ) : step.imageUrl ? (
7174
+ // An empty description marks the image as decorative: screen readers skip it.
7175
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
7176
+ import_react_native7.Image,
7177
+ {
7178
+ source: { uri: step.imageUrl },
7179
+ style: [styles6.image, { borderRadius: Math.max(0, p.radius - 4) }],
7180
+ resizeMode: "cover",
7181
+ accessibilityIgnoresInvertColors: true,
7182
+ ...step.imageAlt?.trim() ? { accessible: true, accessibilityRole: "image", accessibilityLabel: step.imageAlt.trim() } : { accessible: false, importantForAccessibility: "no" }
7183
+ }
7184
+ )
6066
7185
  ) : null,
6067
7186
  step.title ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react_native7.View, { style: styles6.header, children: [
6068
7187
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native7.Text, { accessibilityRole: "header", style: [styles6.title, { color: p.text, fontFamily: p.fontFamily }], children: step.title }),
@@ -6092,6 +7211,10 @@ var styles6 = import_react_native7.StyleSheet.create({
6092
7211
  inner: { padding: 16, gap: 10 },
6093
7212
  compact: { paddingHorizontal: 14, paddingVertical: 10, gap: 6 },
6094
7213
  image: { width: "100%", height: 140, marginBottom: 2 },
7214
+ video: { width: "100%", aspectRatio: 16 / 9, maxHeight: 200, marginBottom: 2, overflow: "hidden", backgroundColor: "#0f172a", alignItems: "center", justifyContent: "center" },
7215
+ play: { width: 52, height: 52, borderRadius: 26, backgroundColor: "rgba(15,23,42,0.72)", alignItems: "center", justifyContent: "center", paddingLeft: 4 },
7216
+ // A CSS-border triangle: no icon font needed.
7217
+ playTriangle: { width: 0, height: 0, borderTopWidth: 10, borderBottomWidth: 10, borderLeftWidth: 16, borderTopColor: "transparent", borderBottomColor: "transparent", borderLeftColor: "#fff" },
6095
7218
  header: { flexDirection: "row", alignItems: "flex-start", gap: 8 },
6096
7219
  title: { flex: 1, fontSize: 17, fontWeight: "700", lineHeight: 22 },
6097
7220
  flex: { flex: 1 },
@@ -6171,9 +7294,9 @@ function StepLayer({ view, client, palette: p, geometry }) {
6171
7294
  }
6172
7295
  if (!rect) return null;
6173
7296
  if (step.type === "hotspot") {
6174
- const size = step.hotspot?.size ?? 14;
7297
+ const size2 = step.hotspot?.size ?? 14;
6175
7298
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { pointerEvents: "box-none", style: import_react_native8.StyleSheet.absoluteFill, children: [
6176
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Hotspot, { x: rect.x + rect.width - size / 2, y: rect.y - size / 2, size, pulse: step.hotspot?.pulse ?? true, color: p.primary, onPress: () => client.toggleHotspot(view.guide.id), label: step.title || "Show tip" }),
7299
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Hotspot, { x: rect.x + rect.width - size2 / 2, y: rect.y - size2 / 2, size: size2, pulse: step.hotspot?.pulse ?? true, color: p.primary, onPress: () => client.toggleHotspot(view.guide.id), label: step.title || "Show tip" }),
6177
7300
  view.expanded ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Anchored, { view, client, palette: p, geometry, rect, width: cardWidth }) : null
6178
7301
  ] });
6179
7302
  }
@@ -6220,7 +7343,7 @@ function Arrow({ side, at, color, border }) {
6220
7343
  const place = side === "bottom" ? { top: -half, left: at - half } : side === "top" ? { bottom: -half, left: at - half } : side === "right" ? { left: -half, top: at - half } : { right: -half, top: at - half };
6221
7344
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.View, { pointerEvents: "none", style: [styles7.abs, styles7.arrow, place, { backgroundColor: color, borderColor: border }] });
6222
7345
  }
6223
- function Hotspot({ x, y, size, pulse, color, onPress, label }) {
7346
+ function Hotspot({ x, y, size: size2, pulse, color, onPress, label }) {
6224
7347
  const scale = (0, import_react5.useRef)(new import_react_native8.Animated.Value(0)).current;
6225
7348
  const [reduceMotion, setReduceMotion] = (0, import_react5.useState)(false);
6226
7349
  (0, import_react5.useEffect)(() => {
@@ -6237,15 +7360,15 @@ function Hotspot({ x, y, size, pulse, color, onPress, label }) {
6237
7360
  loop.start();
6238
7361
  return () => loop.stop();
6239
7362
  }, [pulse, reduceMotion, scale]);
6240
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.Pressable, { accessibilityRole: "button", accessibilityLabel: label, onPress, hitSlop: 14, style: [styles7.abs, { left: x, top: y, width: size, height: size }], children: [
7363
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.Pressable, { accessibilityRole: "button", accessibilityLabel: label, onPress, hitSlop: 14, style: [styles7.abs, { left: x, top: y, width: size2, height: size2 }], children: [
6241
7364
  pulse && !reduceMotion ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
6242
7365
  import_react_native8.Animated.View,
6243
7366
  {
6244
7367
  pointerEvents: "none",
6245
- style: [styles7.abs, { width: size, height: size, borderRadius: size / 2, backgroundColor: color, opacity: scale.interpolate({ inputRange: [0, 1], outputRange: [0.55, 0] }), transform: [{ scale: scale.interpolate({ inputRange: [0, 1], outputRange: [1, 2.6] }) }] }]
7368
+ style: [styles7.abs, { width: size2, height: size2, borderRadius: size2 / 2, backgroundColor: color, opacity: scale.interpolate({ inputRange: [0, 1], outputRange: [0.55, 0] }), transform: [{ scale: scale.interpolate({ inputRange: [0, 1], outputRange: [1, 2.6] }) }] }]
6246
7369
  }
6247
7370
  ) : null,
6248
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.View, { style: { width: size, height: size, borderRadius: size / 2, backgroundColor: color, borderWidth: 2, borderColor: "#ffffff" } })
7371
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.View, { style: { width: size2, height: size2, borderRadius: size2 / 2, backgroundColor: color, borderWidth: 2, borderColor: "#ffffff" } })
6249
7372
  ] });
6250
7373
  }
6251
7374
  var styles7 = import_react_native8.StyleSheet.create({
@@ -6683,7 +7806,7 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
6683
7806
  ) : null,
6684
7807
  ui.survey ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Survey, { view: ui.survey, client, palette: palette(client.themeFor(ui.survey.guide), scheme), geometry }) : null,
6685
7808
  ui.steps.map((s) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(StepLayer, { view: s, client, palette: palette(client.themeFor(s.guide), scheme, s.guide.config.appearance), geometry }, s.key)),
6686
- messenger ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MessengerSheet, { store: messenger, client, palette: basePalette, topInset: insets.top, bottomInset: insets.bottom }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ResourceCenter, { client, palette: basePalette, open: ui.resourceCenterOpen, topInset: insets.top, bottomInset: insets.bottom })
7809
+ messenger ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MessengerSheet, { store: messenger, client, palette: basePalette, topInset: insets.top, bottomInset: insets.bottom }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ResourceCenter, { client, palette: basePalette, open: ui.resourceCenterOpen, articleRequest: ui.resourceCenterArticle ?? null, topInset: insets.top, bottomInset: insets.bottom })
6687
7810
  ] });
6688
7811
  }
6689
7812
  var styles9 = import_react_native10.StyleSheet.create({
@@ -6856,12 +7979,16 @@ function defaultBridge(appVersion, appBuild) {
6856
7979
  locale: deviceLocale(),
6857
7980
  appVersion: appVersion ?? detected.appVersion,
6858
7981
  appBuild: appBuild ?? detected.appBuild,
6859
- osVersion: detectOsVersion(sources)
7982
+ osVersion: detectOsVersion(sources),
7983
+ ...detectDeviceModel(sources),
7984
+ reactNativeVersion: detectReactNativeVersion(sources)
6860
7985
  },
6861
7986
  openExternal: (url) => void import_react_native13.Linking.openURL(url).catch(() => {
6862
7987
  }),
6863
7988
  windowSize: () => import_react_native13.Dimensions.get("window"),
6864
- capturer: nativeCapturer()
7989
+ capturer: nativeCapturer(),
7990
+ // ErrorUtils, HermesInternal, console, fetch, XMLHttpRequest: crash capture and breadcrumbs.
7991
+ globals: globalThis
6865
7992
  };
6866
7993
  }
6867
7994
  function nativeCapturer() {
@@ -6869,6 +7996,20 @@ function nativeCapturer() {
6869
7996
  if (!mod || typeof mod.capture !== "function") return void 0;
6870
7997
  return (options) => mod.capture(options);
6871
7998
  }
7999
+ function latestErrorOptions(initial, latest) {
8000
+ if (!initial) return void 0;
8001
+ return {
8002
+ ...initial,
8003
+ beforeSend: (event) => {
8004
+ const hook = latest()?.beforeSend;
8005
+ return hook ? hook(event) : event;
8006
+ },
8007
+ beforeBreadcrumb: (crumb, hint) => {
8008
+ const hook = latest()?.beforeBreadcrumb;
8009
+ return hook ? hook(crumb, hint) : crumb;
8010
+ }
8011
+ };
8012
+ }
6872
8013
  function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPayload, flagAttributes, ...options }) {
6873
8014
  const [client, setClient] = (0, import_react10.useState)(null);
6874
8015
  const optionsRef = (0, import_react10.useRef)(options);
@@ -6899,7 +8040,8 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
6899
8040
  user: userRef.current?.id ? userRef.current : void 0,
6900
8041
  flags: latest.flags === false ? false : { ...latest.flags ?? {}, payload: payloadRef.current ?? void 0, attributes: appAttributesRef.current },
6901
8042
  onNavigate: (url) => optionsRef.current.onNavigate?.(url),
6902
- onError: (err) => optionsRef.current.onError?.(err)
8043
+ onError: (err) => optionsRef.current.onError?.(err),
8044
+ errors: latestErrorOptions(latest.errors, () => optionsRef.current.errors)
6903
8045
  },
6904
8046
  defaultBridge(latest.appVersion, latest.appBuild)
6905
8047
  );
@@ -6962,6 +8104,8 @@ var NOOP_RESULTS = {
6962
8104
  isRecordingTouches: false,
6963
8105
  getSessionId: "",
6964
8106
  getScreen: "",
8107
+ captureException: "",
8108
+ captureMessage: "",
6965
8109
  on: () => {
6966
8110
  },
6967
8111
  subscribeUi: () => () => {
@@ -7081,6 +8225,7 @@ function useActiveGuide() {
7081
8225
  // Annotate the CommonJS export names for ESM import in node:
7082
8226
  0 && (module.exports = {
7083
8227
  DEFAULT_API_URL,
8228
+ FATAL_FLUSH_MS,
7084
8229
  Feature,
7085
8230
  FeatureFlag,
7086
8231
  FeatureValue,
@@ -7105,11 +8250,15 @@ function useActiveGuide() {
7105
8250
  createFlagsClient,
7106
8251
  createSseParser,
7107
8252
  detectAppVersion,
8253
+ detectDeviceModel,
7108
8254
  detectOsVersion,
8255
+ detectReactNativeVersion,
7109
8256
  guidelyScreenLayout,
7110
8257
  htmlToText,
7111
8258
  noopClient,
8259
+ normalizeBundleFile,
7112
8260
  parseHtml,
8261
+ parseReactNativeStack,
7113
8262
  positionCard,
7114
8263
  targetIdsFor,
7115
8264
  useActiveGuide,