@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.js CHANGED
@@ -96,7 +96,7 @@ function FlagsProvider({ children, client: external, payload, ...options }) {
96
96
  return /* @__PURE__ */ jsx(FlagsContext.Provider, { value, children });
97
97
  }
98
98
 
99
- // ../shared/dist/chunk-XJXXVV3C.js
99
+ // ../shared/dist/chunk-YL65O3FS.js
100
100
  function fnv1a(input) {
101
101
  let h = 2166136261;
102
102
  for (let i = 0; i < input.length; i++) {
@@ -109,18 +109,18 @@ function rolloutBucket(subject, salt) {
109
109
  return fnv1a(`${salt}:${subject}`) % 100;
110
110
  }
111
111
  var ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
112
- function randomId(size = 16) {
112
+ function randomId(size2 = 16) {
113
113
  const g = typeof globalThis !== "undefined" ? globalThis : {};
114
114
  let bytes;
115
115
  if (g.crypto && typeof g.crypto.getRandomValues === "function") {
116
- bytes = new Uint8Array(size);
116
+ bytes = new Uint8Array(size2);
117
117
  g.crypto.getRandomValues(bytes);
118
118
  } else {
119
- bytes = new Uint8Array(size);
120
- for (let i = 0; i < size; i++) bytes[i] = Math.floor(Math.random() * 256);
119
+ bytes = new Uint8Array(size2);
120
+ for (let i = 0; i < size2; i++) bytes[i] = Math.floor(Math.random() * 256);
121
121
  }
122
122
  let out = "";
123
- for (let i = 0; i < size; i++) out += ALPHABET[bytes[i] % ALPHABET.length];
123
+ for (let i = 0; i < size2; i++) out += ALPHABET[bytes[i] % ALPHABET.length];
124
124
  return out;
125
125
  }
126
126
  function getPath(obj, path) {
@@ -390,24 +390,92 @@ function evaluateRules(group, ctx) {
390
390
  const value = group.op === "and" ? results.every(Boolean) : results.some(Boolean);
391
391
  return group.negate ? !value : value;
392
392
  }
393
+ var MAX_EXCEPTION_DETAIL_BYTES = 64 * 1024;
394
+ var MAX_EXCEPTION_DETAIL_RAW_BYTES = 1024 * 1024;
395
+ var MAX_BREADCRUMBS = 100;
396
+ var MAX_EXCEPTION_CHAIN = 20;
397
+ var MAX_DETAIL_FRAMES = 250;
398
+ var MAX_DETAIL_THREADS = 100;
399
+ var MAX_DEBUG_IMAGES = 500;
400
+ var MAX_ERROR_TAGS = 50;
401
+ var size = (v) => {
402
+ try {
403
+ return JSON.stringify(v)?.length ?? 0;
404
+ } catch {
405
+ return Infinity;
406
+ }
407
+ };
408
+ function normalizeTags(tags) {
409
+ const out = {};
410
+ for (const [k, v] of Object.entries(tags ?? {})) {
411
+ if (Object.keys(out).length >= MAX_ERROR_TAGS) break;
412
+ if (!k || v === void 0 || v === null) continue;
413
+ out[k.slice(0, 32)] = String(v).slice(0, 200);
414
+ }
415
+ return out;
416
+ }
417
+ function trimExceptionDetail(detail, max = MAX_EXCEPTION_DETAIL_BYTES) {
418
+ const d = detail;
419
+ if (d.breadcrumbs?.values && d.breadcrumbs.values.length > MAX_BREADCRUMBS) d.breadcrumbs.values = d.breadcrumbs.values.slice(-MAX_BREADCRUMBS);
420
+ if (d.exception?.values && d.exception.values.length > MAX_EXCEPTION_CHAIN) d.exception.values = d.exception.values.slice(-MAX_EXCEPTION_CHAIN);
421
+ if (d.threads?.values && d.threads.values.length > MAX_DETAIL_THREADS) d.threads.values = d.threads.values.slice(0, MAX_DETAIL_THREADS);
422
+ 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);
423
+ const stacks = () => [...d.exception?.values ?? [], ...d.threads?.values ?? []].map((x) => x.stacktrace).filter((s) => !!s);
424
+ for (const s of stacks()) if (s.frames && s.frames.length > MAX_DETAIL_FRAMES) s.frames = s.frames.slice(-MAX_DETAIL_FRAMES);
425
+ if (size(d) <= max) return d;
426
+ while (d.breadcrumbs?.values.length && size(d) > max) d.breadcrumbs.values = d.breadcrumbs.values.slice(Math.ceil(d.breadcrumbs.values.length / 2));
427
+ if (size(d) <= max) return d;
428
+ for (const s of stacks())
429
+ for (const f of s.frames ?? []) {
430
+ delete f.vars;
431
+ delete f.pre_context;
432
+ delete f.post_context;
433
+ }
434
+ if (size(d) <= max) return d;
435
+ if (d.threads?.values) d.threads.values = d.threads.values.filter((t) => t.crashed || t.current);
436
+ if (size(d) <= max) return d;
437
+ if (d.extra) d.extra = { _trimmed: true };
438
+ if (d.request) delete d.request.data;
439
+ 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);
440
+ if (d.debug_meta?.images && size(d) > max) d.debug_meta.images = d.debug_meta.images.slice(0, 50);
441
+ return d;
442
+ }
443
+ var matches = (patterns, value) => patterns.some((p) => typeof p === "string" ? p !== "" && value.includes(p) : safeTest(p, value));
444
+ function safeTest(re, value) {
445
+ re.lastIndex = 0;
446
+ return re.test(value);
447
+ }
448
+ function errorFilterDrops(e, opts) {
449
+ const message = e.message ?? "";
450
+ if (opts.ignoreErrors?.length && (matches(opts.ignoreErrors, `${e.type || "Error"}: ${message}`) || matches(opts.ignoreErrors, message))) return true;
451
+ if (!e.file) return false;
452
+ if (opts.denyUrls?.length && matches(opts.denyUrls, e.file)) return true;
453
+ if (opts.allowUrls?.length && !matches(opts.allowUrls, e.file)) return true;
454
+ return false;
455
+ }
393
456
  var SENSITIVE_PROPERTY_PATTERN = /(password|passwd|secret|token|credit|card|cvv|ssn|iban)/i;
394
457
  var MAX_PROPERTY_BYTES = 8 * 1024;
395
458
  function sanitizeProperties(props) {
396
459
  if (!props) return {};
397
460
  const out = {};
398
- let size = 0;
461
+ let size2 = 0;
399
462
  for (const [k, v] of Object.entries(props)) {
400
463
  if (SENSITIVE_PROPERTY_PATTERN.test(k)) continue;
401
464
  if (typeof v === "function" || typeof v === "symbol") continue;
402
465
  const str2 = typeof v === "string" ? v : JSON.stringify(v ?? null);
403
- size += k.length + (str2?.length ?? 0);
404
- if (size > MAX_PROPERTY_BYTES) break;
466
+ size2 += k.length + (str2?.length ?? 0);
467
+ if (size2 > MAX_PROPERTY_BYTES) break;
405
468
  out[k.slice(0, 100)] = typeof v === "string" ? v.slice(0, 1e3) : v;
406
469
  }
407
470
  return out;
408
471
  }
472
+ function reactNativeBundleUrl(file) {
473
+ const base = file.replace(/[?#].*$/, "").split("/").pop() || file;
474
+ return `app:///${base}`;
475
+ }
409
476
  var SOURCEMAP_MAX_FILE_BYTES = 100 * 1024 * 1024;
410
477
  var MAX_COMPLAINT_ATTACHMENT_BYTES = 2 * 1024 * 1024;
478
+ var MAX_BUG_REPORT_SCREENSHOT_BYTES = 4 * 1024 * 1024;
411
479
  var MEDIA_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
412
480
  var MEDIA_VIDEO_MAX_BYTES = 200 * 1024 * 1024;
413
481
  var MEDIA_FILE_MAX_BYTES = 50 * 1024 * 1024;
@@ -505,6 +573,16 @@ function isSafeMediaUrl(url) {
505
573
  return false;
506
574
  }
507
575
  }
576
+ function videoThumbnailUrl(url) {
577
+ const embed = url ? toEmbedUrl(url) : null;
578
+ if (embed?.provider !== "youtube") return null;
579
+ const id = embed.src.split("/embed/")[1]?.split("?")[0];
580
+ return id ? `https://i.ytimg.com/vi/${id}/hqdefault.jpg` : null;
581
+ }
582
+ function stepVideo(step) {
583
+ if (!step.videoUrl || !isSafeMediaUrl(step.videoUrl)) return null;
584
+ return { url: step.videoUrl, poster: (step.imageUrl && isSafeMediaUrl(step.imageUrl) ? step.imageUrl : null) ?? videoThumbnailUrl(step.videoUrl) };
585
+ }
508
586
  var MAX_COLLECTION_DEPTH = 3;
509
587
  function matchesAudiences(required, member) {
510
588
  if (!required || required.length === 0) return true;
@@ -574,6 +652,13 @@ function unreadAnnouncements(items, lastSeenAt) {
574
652
  if (Number.isNaN(seen)) return items.length;
575
653
  return items.filter((a) => Date.parse(a.publishedAt) > seen).length;
576
654
  }
655
+ var ARTICLE_HREF_PREFIX = "#guidely-article:";
656
+ var ARTICLE_ID = /^[A-Za-z0-9_-]{1,64}$/;
657
+ function articleIdFromHref(href) {
658
+ if (!href || !href.startsWith(ARTICLE_HREF_PREFIX)) return null;
659
+ const id = href.slice(ARTICLE_HREF_PREFIX.length);
660
+ return ARTICLE_ID.test(id) ? id : null;
661
+ }
577
662
  var CAPTURE_SCREENSHOT_MAX_BYTES = 8 * 1024 * 1024;
578
663
  var DEFAULT_LOCALE = "en";
579
664
  function normalizeLocale(input) {
@@ -1015,7 +1100,22 @@ var KEYS = {
1015
1100
  launcher: (key) => `guidely:launcher:${key}`
1016
1101
  };
1017
1102
 
1018
- // ../sdk-core/dist/chunk-2E2YUQAG.js
1103
+ // ../sdk-core/dist/chunk-GFLVZRTS.js
1104
+ var MAX_PERSISTED_QUEUE_CHARS = 1024 * 1024;
1105
+ function fitPersisted(events) {
1106
+ let size2 = JSON.stringify(events).length;
1107
+ if (size2 <= MAX_PERSISTED_QUEUE_CHARS) return events;
1108
+ const out = events.slice();
1109
+ for (let i = 0; i < out.length && size2 > MAX_PERSISTED_QUEUE_CHARS; i++) {
1110
+ const e = out[i];
1111
+ if (!e.detail) continue;
1112
+ size2 -= JSON.stringify(e.detail).length;
1113
+ const rest = { ...e };
1114
+ delete rest.detail;
1115
+ out[i] = rest;
1116
+ }
1117
+ return out;
1118
+ }
1019
1119
  var EventQueue = class {
1020
1120
  constructor(opts) {
1021
1121
  __publicField2(this, "opts", opts);
@@ -1071,7 +1171,7 @@ var EventQueue = class {
1071
1171
  this.schedule();
1072
1172
  }
1073
1173
  persist() {
1074
- writeJson(this.opts.store, this.opts.storageKey, this.events);
1174
+ writeJson(this.opts.store, this.opts.storageKey, fitPersisted(this.events));
1075
1175
  }
1076
1176
  schedule() {
1077
1177
  if (this.timer || this.retryTimer || this.destroyed) return;
@@ -1321,6 +1421,13 @@ function dismissedAfterMerge(local, server) {
1321
1421
  if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
1322
1422
  return local?.dismissedAt ?? server.dismissedAt ?? null;
1323
1423
  }
1424
+ function environmentFromKey(key) {
1425
+ const prefix = /^pk_([a-z0-9]+)_/.exec(key)?.[1];
1426
+ if (!prefix || prefix === "dev") return "development";
1427
+ if (prefix === "prod") return "production";
1428
+ if (prefix === "stg" || prefix === "stag") return "staging";
1429
+ return prefix;
1430
+ }
1324
1431
 
1325
1432
  // src/core/client.ts
1326
1433
  import { createFlagsClient as createFlagsClient2 } from "@guidelytech/flags";
@@ -1477,6 +1584,593 @@ var TapTracker = class {
1477
1584
  }
1478
1585
  };
1479
1586
 
1587
+ // src/core/breadcrumbs.ts
1588
+ var BreadcrumbBuffer = class {
1589
+ constructor(max = MAX_BREADCRUMBS) {
1590
+ __publicField(this, "max", max);
1591
+ __publicField(this, "items", []);
1592
+ }
1593
+ /** Adds a breadcrumb; `max` (at most the buffer's own) lowers how many are kept. */
1594
+ add(crumb, max = this.max) {
1595
+ const limit = Math.min(max, this.max);
1596
+ this.items.push(crumb);
1597
+ if (this.items.length > limit) this.items.splice(0, this.items.length - limit);
1598
+ }
1599
+ /** Copies, oldest first (an event keeps the crumbs it was captured with). */
1600
+ values() {
1601
+ return this.items.map((c) => ({ ...c, ...c.data ? { data: { ...c.data } } : {} }));
1602
+ }
1603
+ get size() {
1604
+ return this.items.length;
1605
+ }
1606
+ clear() {
1607
+ this.items = [];
1608
+ }
1609
+ };
1610
+ function stripUrl(url) {
1611
+ return String(url).replace(/[?#].*$/, "").replace(/^([a-z][\w+.-]*:\/\/)[^/@]*@/i, "$1").slice(0, 500);
1612
+ }
1613
+ function describeArg(value) {
1614
+ if (typeof value === "string") return value.slice(0, 500);
1615
+ if (value instanceof Error) return `${value.name}: ${value.message}`.slice(0, 500);
1616
+ try {
1617
+ return (JSON.stringify(value) ?? String(value)).slice(0, 500);
1618
+ } catch {
1619
+ return String(value).slice(0, 500);
1620
+ }
1621
+ }
1622
+ function instrumentConsole(target, emit) {
1623
+ if (!target) return () => {
1624
+ };
1625
+ const undo = [];
1626
+ let inside = false;
1627
+ for (const [method, level] of [
1628
+ ["warn", "warning"],
1629
+ ["error", "error"]
1630
+ ]) {
1631
+ const original = target[method];
1632
+ if (typeof original !== "function") continue;
1633
+ const wrapped = function(...args) {
1634
+ const result = original.apply(this, args);
1635
+ if (!inside && !(typeof args[0] === "string" && args[0].startsWith("[Guidely]"))) {
1636
+ inside = true;
1637
+ try {
1638
+ const parts = args.slice(0, 5).map(describeArg);
1639
+ emit({ type: "debug", category: "console", level, message: parts.join(" ").slice(0, 1e3), data: { arguments: parts, logger: "console" } });
1640
+ } catch {
1641
+ } finally {
1642
+ inside = false;
1643
+ }
1644
+ }
1645
+ return result;
1646
+ };
1647
+ target[method] = wrapped;
1648
+ undo.push(() => {
1649
+ if (target[method] === wrapped) target[method] = original;
1650
+ });
1651
+ }
1652
+ return () => undo.forEach((fn) => fn());
1653
+ }
1654
+ function httpLevel(status) {
1655
+ if (!status) return "error";
1656
+ return status >= 500 ? "error" : status >= 400 ? "warning" : "info";
1657
+ }
1658
+ var XHR_INFO = "__guidelyXhr";
1659
+ function instrumentHttp(g, emit, skip, now = Date.now) {
1660
+ if (!g) return () => {
1661
+ };
1662
+ const undo = [];
1663
+ let inFetch = 0;
1664
+ const record2 = (category, method, url, start, status) => {
1665
+ try {
1666
+ const data = { method, url: stripUrl(url), ...status ? { status_code: status } : {}, duration_ms: Math.max(0, Math.round(now() - start)) };
1667
+ emit({ type: "http", category, level: httpLevel(status), data });
1668
+ } catch {
1669
+ }
1670
+ };
1671
+ const originalFetch = g.fetch;
1672
+ if (typeof originalFetch === "function") {
1673
+ const wrappedFetch = function(...args) {
1674
+ const [input, init] = args;
1675
+ let url = "";
1676
+ let method = "GET";
1677
+ try {
1678
+ const req = input;
1679
+ url = typeof req === "string" ? req : typeof req?.url === "string" ? req.url : String(input);
1680
+ method = String(init?.method ?? (req && typeof req === "object" ? req.method : void 0) ?? "GET").toUpperCase();
1681
+ } catch {
1682
+ }
1683
+ if (skip(url)) return originalFetch.apply(this, args);
1684
+ const start = now();
1685
+ let result;
1686
+ inFetch += 1;
1687
+ try {
1688
+ result = originalFetch.apply(this, args);
1689
+ } finally {
1690
+ inFetch -= 1;
1691
+ }
1692
+ if (!result || typeof result.then !== "function") return result;
1693
+ return result.then(
1694
+ (res) => {
1695
+ record2("fetch", method, url, start, typeof res?.status === "number" ? res.status : void 0);
1696
+ return res;
1697
+ },
1698
+ (err) => {
1699
+ record2("fetch", method, url, start, void 0);
1700
+ throw err;
1701
+ }
1702
+ );
1703
+ };
1704
+ g.fetch = wrappedFetch;
1705
+ undo.push(() => {
1706
+ if (g.fetch === wrappedFetch) g.fetch = originalFetch;
1707
+ });
1708
+ }
1709
+ const proto = g.XMLHttpRequest?.prototype;
1710
+ const originalOpen = proto?.open;
1711
+ const originalSend = proto?.send;
1712
+ if (proto && typeof originalOpen === "function" && typeof originalSend === "function") {
1713
+ const open = function(...args) {
1714
+ try {
1715
+ this[XHR_INFO] = { method: String(args[0] ?? "GET").toUpperCase(), url: String(args[1] ?? ""), viaFetch: inFetch > 0 };
1716
+ } catch {
1717
+ }
1718
+ return originalOpen.apply(this, args);
1719
+ };
1720
+ const send = function(...args) {
1721
+ try {
1722
+ const info = this[XHR_INFO];
1723
+ if (info && !info.viaFetch && !skip(info.url) && typeof this.addEventListener === "function") {
1724
+ info.start = now();
1725
+ this.addEventListener("readystatechange", () => {
1726
+ if (this.readyState !== 4 || info.done) return;
1727
+ info.done = true;
1728
+ record2("xhr", info.method, info.url, info.start ?? now(), typeof this.status === "number" && this.status > 0 ? this.status : void 0);
1729
+ });
1730
+ }
1731
+ } catch {
1732
+ }
1733
+ return originalSend.apply(this, args);
1734
+ };
1735
+ proto.open = open;
1736
+ proto.send = send;
1737
+ undo.push(() => {
1738
+ if (proto.open === open) proto.open = originalOpen;
1739
+ if (proto.send === send) proto.send = originalSend;
1740
+ });
1741
+ }
1742
+ return () => undo.forEach((fn) => fn());
1743
+ }
1744
+
1745
+ // src/core/crashHandlers.ts
1746
+ var FATAL_FLUSH_MS = 2e3;
1747
+ var isThenable = (v) => !!v && typeof v.then === "function";
1748
+ function installGlobalHandler(g, report, waitMs = FATAL_FLUSH_MS) {
1749
+ const eu = g.ErrorUtils;
1750
+ if (!eu || typeof eu.setGlobalHandler !== "function") return () => {
1751
+ };
1752
+ let previous;
1753
+ try {
1754
+ previous = eu.getGlobalHandler?.();
1755
+ } catch {
1756
+ previous = void 0;
1757
+ }
1758
+ let active = true;
1759
+ let handlingFatal = false;
1760
+ const handler = (error, isFatal) => {
1761
+ const fatal = !!isFatal;
1762
+ const callPrevious = () => previous?.(error, isFatal);
1763
+ if (!active || fatal && handlingFatal) return callPrevious();
1764
+ let pending;
1765
+ try {
1766
+ pending = report(error, fatal);
1767
+ } catch {
1768
+ }
1769
+ if (!fatal || g.__DEV__ || !isThenable(pending)) return callPrevious();
1770
+ handlingFatal = true;
1771
+ let done = false;
1772
+ const finish = () => {
1773
+ if (done) return;
1774
+ done = true;
1775
+ clearTimeout(timer);
1776
+ handlingFatal = false;
1777
+ try {
1778
+ callPrevious();
1779
+ } catch {
1780
+ }
1781
+ };
1782
+ const timer = setTimeout(finish, waitMs);
1783
+ pending.then(finish, finish);
1784
+ };
1785
+ try {
1786
+ eu.setGlobalHandler(handler);
1787
+ } catch {
1788
+ return () => {
1789
+ };
1790
+ }
1791
+ return () => {
1792
+ active = false;
1793
+ try {
1794
+ if (previous && eu.getGlobalHandler?.() === handler) eu.setGlobalHandler(previous);
1795
+ } catch {
1796
+ }
1797
+ };
1798
+ }
1799
+ function describeRejection(rejection) {
1800
+ if (rejection instanceof Error) return rejection.stack || `${rejection.name}: ${rejection.message}`;
1801
+ try {
1802
+ return typeof rejection === "string" ? rejection : JSON.stringify(rejection) ?? String(rejection);
1803
+ } catch {
1804
+ return String(rejection);
1805
+ }
1806
+ }
1807
+ function hermesTracker(g) {
1808
+ const h = g.HermesInternal;
1809
+ try {
1810
+ if (!h || typeof h.enablePromiseRejectionTracker !== "function" || typeof h.hasPromise === "function" && !h.hasPromise()) return null;
1811
+ } catch {
1812
+ return null;
1813
+ }
1814
+ return { enable: (options) => h.enablePromiseRejectionTracker(options) };
1815
+ }
1816
+ function installRejectionTracking(g, report, tracker) {
1817
+ let active = true;
1818
+ const warn = (message) => {
1819
+ try {
1820
+ g.console?.warn?.(message);
1821
+ } catch {
1822
+ }
1823
+ };
1824
+ const options = {
1825
+ allRejections: true,
1826
+ onUnhandled: (id, rejection) => {
1827
+ if (active) {
1828
+ try {
1829
+ report(rejection);
1830
+ } catch {
1831
+ }
1832
+ }
1833
+ if (g.__DEV__) warn(`Possible unhandled promise rejection (id: ${id}):
1834
+ ${describeRejection(rejection)}`);
1835
+ },
1836
+ onHandled: (id) => {
1837
+ if (g.__DEV__) warn(`Promise rejection handled (id: ${id})
1838
+ This means you can ignore any previous messages of the form "Possible unhandled promise rejection (id: ${id}):"`);
1839
+ }
1840
+ };
1841
+ const t = tracker ?? hermesTracker(g);
1842
+ if (t && typeof t.enable === "function") {
1843
+ try {
1844
+ t.enable(options);
1845
+ return () => {
1846
+ active = false;
1847
+ };
1848
+ } catch {
1849
+ }
1850
+ }
1851
+ if (typeof g.addEventListener === "function") {
1852
+ const listener = (event) => {
1853
+ if (active) report(event?.reason);
1854
+ };
1855
+ try {
1856
+ g.addEventListener("unhandledrejection", listener);
1857
+ return () => {
1858
+ active = false;
1859
+ g.removeEventListener?.("unhandledrejection", listener);
1860
+ };
1861
+ } catch {
1862
+ }
1863
+ }
1864
+ return () => {
1865
+ active = false;
1866
+ };
1867
+ }
1868
+ function runtimeContext(g) {
1869
+ const h = g.HermesInternal;
1870
+ if (!h) return { name: "jsc" };
1871
+ try {
1872
+ const version = h.getRuntimeProperties?.()?.["OSS Release Version"];
1873
+ return { name: "hermes", ...typeof version === "string" && version ? { version: version.slice(0, 50) } : {} };
1874
+ } catch {
1875
+ return { name: "hermes" };
1876
+ }
1877
+ }
1878
+
1879
+ // src/core/stack.ts
1880
+ var AT = /^\s*at (?:(.+?) \()?(?:address at )?(.+?):(\d+):(\d+)\)?\s*$/;
1881
+ var AT_NATIVE = /^\s*at (?:(.+?) \()?(?:native|\[native code\])\)?\s*$/;
1882
+ var JSC = /^\s*([^@]*)@(.+?):(\d+):(\d+)\s*$/;
1883
+ var JSC_BARE = /^\s*(\S+?):(\d+):(\d+)\s*$/;
1884
+ var JSC_NATIVE = /^\s*(?:([^@]*)@)?\[native code\]\s*$/;
1885
+ var BUNDLE = /\.(jsbundle|bundle|hbc)$/i;
1886
+ function normalizeBundleFile(file) {
1887
+ const f = file.trim();
1888
+ if (/^https?:\/\//i.test(f)) return f.replace(/[?#].*$/, "");
1889
+ const path = f.replace(/^file:\/\//i, "").replace(/[?#].*$/, "");
1890
+ const base = path.slice(path.lastIndexOf("/") + 1);
1891
+ return BUNDLE.test(base) ? reactNativeBundleUrl(path) : path;
1892
+ }
1893
+ function inAppFor(file) {
1894
+ if (!file) return false;
1895
+ if (/(^|\/)node_modules\//.test(file) || /(^|\/)InternalBytecode\.js$/.test(file)) return false;
1896
+ if (file.startsWith("app:///") || /^https?:\/\//i.test(file)) return void 0;
1897
+ return true;
1898
+ }
1899
+ function frame(fn, file, line, col) {
1900
+ const name = (fn || "").trim();
1901
+ const normalized = file ? normalizeBundleFile(file).slice(0, 500) : void 0;
1902
+ const inApp = inAppFor(normalized);
1903
+ return {
1904
+ ...name ? { fn: name.slice(0, 200) } : {},
1905
+ ...normalized ? { file: normalized } : {},
1906
+ ...line ? { line: Number(line) } : {},
1907
+ ...col ? { col: Number(col) } : {},
1908
+ ...inApp !== void 0 ? { inApp } : {}
1909
+ };
1910
+ }
1911
+ function parseReactNativeStack(stack, max = MAX_DETAIL_FRAMES) {
1912
+ if (!stack || typeof stack !== "string") return [];
1913
+ const frames = [];
1914
+ for (const raw of stack.split("\n")) {
1915
+ if (frames.length >= max) break;
1916
+ const line = raw.slice(0, 1e3);
1917
+ let m;
1918
+ if (m = AT_NATIVE.exec(line)) frames.push(frame(m[1], void 0));
1919
+ else if (m = AT.exec(line)) frames.push(frame(m[1], m[2], m[3], m[4]));
1920
+ else if (m = JSC_NATIVE.exec(line)) frames.push(frame(m[1], void 0));
1921
+ else if (m = JSC.exec(line)) frames.push(frame(m[1], m[2], m[3], m[4]));
1922
+ else if (m = JSC_BARE.exec(line)) frames.push(frame(void 0, m[1], m[2], m[3]));
1923
+ }
1924
+ return frames;
1925
+ }
1926
+ function toDetailFrames(frames) {
1927
+ return frames.slice().reverse().map((f) => ({
1928
+ ...f.fn ? { function: f.fn } : {},
1929
+ ...f.file ? { filename: f.file, abs_path: f.file } : {},
1930
+ ...f.line !== void 0 ? { lineno: f.line } : {},
1931
+ ...f.col !== void 0 ? { colno: f.col } : {},
1932
+ ...f.inApp !== void 0 ? { in_app: f.inApp } : {}
1933
+ }));
1934
+ }
1935
+ var MAX_FRAMES_BYTES = 6e3;
1936
+ function fitPropertyFrames(frames) {
1937
+ const out = frames.slice(0, 30);
1938
+ while (out.length > 1 && JSON.stringify(out).length > MAX_FRAMES_BYTES) out.pop();
1939
+ return out;
1940
+ }
1941
+
1942
+ // src/core/errors.ts
1943
+ var CONTEXT_KEYS = ["tags", "extra", "level", "fingerprint", "contexts", "user"];
1944
+ function splitHint(hint) {
1945
+ const ctx = {};
1946
+ const properties = {};
1947
+ for (const [k, v] of Object.entries(hint && typeof hint === "object" ? hint : {})) (CONTEXT_KEYS.includes(k) ? ctx : properties)[k] = v;
1948
+ return { ctx, properties };
1949
+ }
1950
+ var nowSeconds = () => Date.now() / 1e3;
1951
+ var ErrorScope = class {
1952
+ constructor(opts) {
1953
+ __publicField(this, "opts", opts);
1954
+ __publicField(this, "tags", {});
1955
+ __publicField(this, "extra", {});
1956
+ __publicField(this, "contexts", {});
1957
+ __publicField(this, "user", {});
1958
+ __publicField(this, "crumbs", new BreadcrumbBuffer(MAX_BREADCRUMBS));
1959
+ }
1960
+ get max() {
1961
+ return Math.max(0, Math.min(MAX_BREADCRUMBS, this.opts()?.maxBreadcrumbs ?? MAX_BREADCRUMBS));
1962
+ }
1963
+ addBreadcrumb(crumb, hint) {
1964
+ if (!crumb || typeof crumb !== "object") return;
1965
+ let c = { timestamp: nowSeconds(), ...crumb };
1966
+ const before = this.opts()?.beforeBreadcrumb;
1967
+ if (before) {
1968
+ try {
1969
+ c = before(c, hint);
1970
+ } catch {
1971
+ }
1972
+ }
1973
+ const max = this.max;
1974
+ if (!c || !max) return;
1975
+ this.crumbs.add(c, max);
1976
+ }
1977
+ breadcrumbs() {
1978
+ return this.crumbs.values();
1979
+ }
1980
+ clear() {
1981
+ this.tags = {};
1982
+ this.extra = {};
1983
+ this.contexts = {};
1984
+ this.user = {};
1985
+ this.crumbs.clear();
1986
+ }
1987
+ };
1988
+ var isError = (v) => v instanceof Error || !!v && typeof v === "object" && typeof v.message === "string" && typeof v.stack === "string";
1989
+ function describe(value) {
1990
+ if (typeof value === "string") return value;
1991
+ try {
1992
+ return JSON.stringify(value) ?? String(value);
1993
+ } catch {
1994
+ return String(value);
1995
+ }
1996
+ }
1997
+ function exceptionFromValue(error, source, handled, fallbackType = "Error") {
1998
+ const e = error;
1999
+ const ok = isError(error);
2000
+ return {
2001
+ type: (ok && e.name || fallbackType).slice(0, 200),
2002
+ message: (ok ? e.message : describe(error)).slice(0, 1e3),
2003
+ frames: ok ? fitPropertyFrames(parseReactNativeStack(e.stack)) : [],
2004
+ source,
2005
+ handled
2006
+ };
2007
+ }
2008
+ function exceptionChain(error, main, mechanism) {
2009
+ const mainFrames = isError(error) ? parseReactNativeStack(error.stack) : main.frames;
2010
+ const chain = [{ type: main.type, value: main.message, mechanism: { type: mechanism, handled: main.handled !== false, exception_id: 0 }, ...mainFrames.length ? { stacktrace: { frames: toDetailFrames(mainFrames) } } : {} }];
2011
+ const seen = /* @__PURE__ */ new Set([error]);
2012
+ let cur = isError(error) ? error.cause : void 0;
2013
+ while (cur !== void 0 && cur !== null && !seen.has(cur) && chain.length < MAX_EXCEPTION_CHAIN) {
2014
+ seen.add(cur);
2015
+ const e = cur;
2016
+ const id = chain.length;
2017
+ const frames = isError(e) ? parseReactNativeStack(e.stack, 50) : [];
2018
+ chain.push({
2019
+ type: (isError(e) && e.name || "Error").slice(0, 200),
2020
+ value: (isError(e) ? e.message : describe(cur)).slice(0, 1e3),
2021
+ mechanism: { type: "chained", source: "cause", handled: true, exception_id: id, parent_id: id - 1 },
2022
+ ...frames.length ? { stacktrace: { frames: toDetailFrames(frames) } } : {}
2023
+ });
2024
+ cur = isError(e) ? e.cause : void 0;
2025
+ }
2026
+ return chain.reverse();
2027
+ }
2028
+ var SENSITIVE_KEY = new RegExp(`${SENSITIVE_PROPERTY_PATTERN.source}|authorization|cookie|api[-_]?key`, "i");
2029
+ function normalizeValue(value, depth = 5, seen = /* @__PURE__ */ new Set()) {
2030
+ if (value === null || typeof value === "boolean") return value;
2031
+ if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
2032
+ if (typeof value === "string") return value.slice(0, 5e3);
2033
+ if (typeof value === "bigint") return value.toString();
2034
+ if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") return void 0;
2035
+ if (value instanceof Date) return Number.isFinite(value.getTime()) ? value.toISOString() : String(value);
2036
+ if (isError(value)) return { name: value.name, message: value.message.slice(0, 1e3) };
2037
+ if (typeof value !== "object" || seen.has(value)) return "[Circular]";
2038
+ if (depth <= 0) return Array.isArray(value) ? "[Array]" : "[Object]";
2039
+ seen.add(value);
2040
+ try {
2041
+ if (Array.isArray(value)) return value.slice(0, MAX_DETAIL_FRAMES).map((v) => normalizeValue(v, depth - 1, seen) ?? null);
2042
+ const out = {};
2043
+ let n = 0;
2044
+ for (const [k, v] of Object.entries(value)) {
2045
+ if (n++ >= 100) break;
2046
+ if (SENSITIVE_KEY.test(k)) out[k] = "[Filtered]";
2047
+ else {
2048
+ const nv = normalizeValue(v, depth - 1, seen);
2049
+ if (nv !== void 0) out[k] = nv;
2050
+ }
2051
+ }
2052
+ return out;
2053
+ } finally {
2054
+ seen.delete(value);
2055
+ }
2056
+ }
2057
+ var record = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
2058
+ var jsonSize = (v) => {
2059
+ try {
2060
+ return JSON.stringify(v)?.length ?? 0;
2061
+ } catch {
2062
+ return Infinity;
2063
+ }
2064
+ };
2065
+ function finalizeDetail(detail, max = MAX_EXCEPTION_DETAIL_BYTES) {
2066
+ let d = normalizeValue(detail, 12);
2067
+ if (!d || typeof d !== "object") return void 0;
2068
+ if (d.tags) d.tags = normalizeTags(Object.fromEntries(Object.entries(d.tags).filter(([k]) => !SENSITIVE_KEY.test(k))));
2069
+ d = trimExceptionDetail(d, max);
2070
+ if (jsonSize(d) <= max) return d;
2071
+ 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 } : {} };
2072
+ return jsonSize(slim) <= max ? slim : void 0;
2073
+ }
2074
+ function buildDetail(input) {
2075
+ const { scope, ctx } = input;
2076
+ const contexts = { ...input.contexts, ...scope.contexts };
2077
+ for (const [k, v] of Object.entries(record(ctx.contexts))) {
2078
+ if (v && typeof v === "object") contexts[k] = v;
2079
+ else delete contexts[k];
2080
+ }
2081
+ const user = { ...input.userId ? { id: input.userId } : {}, ...scope.user, ...record(ctx.user) };
2082
+ const message = input.props.type === "Message";
2083
+ return {
2084
+ level: input.level,
2085
+ platform: "javascript",
2086
+ timestamp: nowSeconds(),
2087
+ ...input.release ? { release: input.release } : {},
2088
+ ...input.dist ? { dist: input.dist } : {},
2089
+ ...input.environment ? { environment: input.environment } : {},
2090
+ ...message ? { message: { formatted: input.props.message } } : { exception: { values: exceptionChain(input.error, input.props, input.mechanism) } },
2091
+ breadcrumbs: { values: scope.breadcrumbs() },
2092
+ contexts,
2093
+ tags: normalizeTags({ ...scope.tags, ...record(ctx.tags) }),
2094
+ extra: { ...scope.extra, ...record(ctx.extra) },
2095
+ ...Object.keys(user).length ? { user } : {},
2096
+ ...Array.isArray(ctx.fingerprint) && ctx.fingerprint.length ? { fingerprint: ctx.fingerprint.map(String) } : {},
2097
+ sdk: input.sdk
2098
+ };
2099
+ }
2100
+ function prepareError(props, extraProperties, detail, opts, fatal, release) {
2101
+ const file = props.frames.find((f) => f.file)?.file;
2102
+ if (opts && errorFilterDrops({ type: props.type, message: props.message, file }, opts)) return null;
2103
+ if (typeof opts?.sampleRate === "number" && opts.sampleRate < 1 && Math.random() >= Math.max(0, opts.sampleRate)) return null;
2104
+ const values = detail.exception?.values ?? [];
2105
+ const thrown = values[values.length - 1];
2106
+ const mechanism = thrown?.mechanism?.type ?? props.source;
2107
+ let properties = { ...props, level: detail.level ?? "error", platform: "javascript", ...mechanism ? { mechanism } : {} };
2108
+ if (release) properties.release = release.slice(0, 100);
2109
+ if (detail.fingerprint?.length) properties.fingerprintParts = detail.fingerprint.slice(0, 20);
2110
+ for (const [k, v] of Object.entries(extraProperties)) if (!(k in properties)) properties[k] = v;
2111
+ let d = detail;
2112
+ if (opts?.beforeSend) {
2113
+ try {
2114
+ const out = opts.beforeSend({ properties, detail: d });
2115
+ if (out === null) return null;
2116
+ if (out && typeof out === "object") {
2117
+ if (out.properties && typeof out.properties === "object") properties = out.properties;
2118
+ if (out.detail && typeof out.detail === "object") d = out.detail;
2119
+ }
2120
+ } catch {
2121
+ }
2122
+ }
2123
+ const finalDetail = finalizeDetail(d);
2124
+ return { eventId: randomId(24), occurredAt: (/* @__PURE__ */ new Date()).toISOString(), properties, ...finalDetail ? { detail: finalDetail } : {}, fatal };
2125
+ }
2126
+ var ExceptionThrottle = class {
2127
+ constructor(max = 5) {
2128
+ __publicField(this, "max", max);
2129
+ __publicField(this, "seen", /* @__PURE__ */ new Map());
2130
+ }
2131
+ allow(props) {
2132
+ const frames = Array.isArray(props.frames) ? props.frames : [];
2133
+ const top = frames[0];
2134
+ const key = `${String(props.type)}|${String(props.message)}|${top ? `${top.file}:${top.line}:${top.col}` : ""}`;
2135
+ const n = this.seen.get(key) ?? 0;
2136
+ if (n >= this.max) return false;
2137
+ if (this.seen.size > 500) this.seen.clear();
2138
+ this.seen.set(key, n + 1);
2139
+ return true;
2140
+ }
2141
+ };
2142
+ function rnVersion(v) {
2143
+ if (!v) return void 0;
2144
+ if (typeof v === "string") return v;
2145
+ if (typeof v.major !== "number") return void 0;
2146
+ return `${v.major}.${v.minor ?? 0}.${v.patch ?? 0}${v.prerelease ? `-${v.prerelease}` : ""}`;
2147
+ }
2148
+ function reactNativeContexts(device, app, g) {
2149
+ const runtime = g ? runtimeContext(g) : void 0;
2150
+ const version = rnVersion(device.reactNativeVersion);
2151
+ return {
2152
+ os: { name: device.platform === "android" ? "Android" : "iOS", ...device.osVersion ? { version: device.osVersion } : {} },
2153
+ device: {
2154
+ family: device.deviceType === "tablet" ? "Tablet" : "Mobile",
2155
+ ...device.model ? { model: device.model } : {},
2156
+ ...device.manufacturer ? { manufacturer: device.manufacturer } : {},
2157
+ ...device.brand ? { brand: device.brand } : {},
2158
+ ...device.simulator !== void 0 ? { simulator: device.simulator } : {}
2159
+ },
2160
+ app: {
2161
+ ...app.appVersion ? { app_version: app.appVersion } : {},
2162
+ ...app.build ? { app_build: app.build } : {},
2163
+ ...app.inForeground !== void 0 ? { in_foreground: app.inForeground } : {}
2164
+ },
2165
+ ...runtime ? { runtime } : {},
2166
+ react_native_context: {
2167
+ ...runtime ? { js_engine: runtime.name } : {},
2168
+ ...version ? { react_native_version: version } : {},
2169
+ ...g ? { turbo_module: !!g.__turboModuleProxy || !!g.RN$Bridgeless, fabric: !!g.nativeFabricUIManager, expo: !!g.expo } : {}
2170
+ }
2171
+ };
2172
+ }
2173
+
1480
2174
  // src/core/targets.ts
1481
2175
  var TargetTimeoutError = class extends Error {
1482
2176
  constructor(ids) {
@@ -2807,38 +3501,38 @@ var SessionRecorder = class {
2807
3501
  this.capturing = true;
2808
3502
  this.captureSoon = false;
2809
3503
  const gen = this.generation;
2810
- let frame = null;
3504
+ let frame2 = null;
2811
3505
  try {
2812
- frame = await this.opts.capturer(await this.opts.captureOptions());
3506
+ frame2 = await this.opts.capturer(await this.opts.captureOptions());
2813
3507
  } catch (err) {
2814
3508
  this.opts.onError?.(err);
2815
3509
  }
2816
3510
  if (gen !== this.generation) return;
2817
3511
  this.capturing = false;
2818
- if (frame) this.onFrame(frame, this.now());
3512
+ if (frame2) this.onFrame(frame2, this.now());
2819
3513
  if (this.sid !== null && this.foreground) this.schedule(this.captureSoon ? TOUCH_SETTLE_MS : this.currentIntervalMs);
2820
3514
  }
2821
- onFrame(frame, at) {
3515
+ onFrame(frame2, at) {
2822
3516
  const id = this.sid;
2823
- if (id === null || !(frame.width > 0) || !(frame.height > 0) || !frame.base64) return;
2824
- const sizeChanged = frame.width !== this.viewportWidth || frame.height !== this.viewportHeight;
3517
+ if (id === null || !(frame2.width > 0) || !(frame2.height > 0) || !frame2.base64) return;
3518
+ const sizeChanged = frame2.width !== this.viewportWidth || frame2.height !== this.viewportHeight;
2825
3519
  const keyframe = this.needKeyframe || !this.hasKeyframe || sizeChanged;
2826
- if (!keyframe && this.lastFrame === frame.base64) {
3520
+ if (!keyframe && this.lastFrame === frame2.base64) {
2827
3521
  this.unchanged++;
2828
3522
  return;
2829
3523
  }
2830
3524
  this.unchanged = 0;
2831
- this.lastFrame = frame.base64;
2832
- this.originX = frame.originX ?? 0;
2833
- this.originY = frame.originY ?? 0;
2834
- this.viewportWidth = frame.width;
2835
- this.viewportHeight = frame.height;
2836
- const url = RrwebEvents.dataUrl(frame.base64);
3525
+ this.lastFrame = frame2.base64;
3526
+ this.originX = frame2.originX ?? 0;
3527
+ this.originY = frame2.originY ?? 0;
3528
+ this.viewportWidth = frame2.width;
3529
+ this.viewportHeight = frame2.height;
3530
+ const url = RrwebEvents.dataUrl(frame2.base64);
2837
3531
  const up = this.opts.uploader;
2838
3532
  if (!keyframe) return up.push(id, RrwebEvents.frame(at, url));
2839
3533
  this.needKeyframe = false;
2840
- up.push(id, RrwebEvents.meta(at, this.opts.href(), frame.width, frame.height));
2841
- up.push(id, RrwebEvents.fullSnapshot(at, url, frame.width, frame.height));
3534
+ up.push(id, RrwebEvents.meta(at, this.opts.href(), frame2.width, frame2.height));
3535
+ up.push(id, RrwebEvents.fullSnapshot(at, url, frame2.width, frame2.height));
2842
3536
  if (!this.hasKeyframe) {
2843
3537
  this.hasKeyframe = true;
2844
3538
  for (const [ts, payload] of this.pendingCustom) up.push(id, RrwebEvents.custom(Math.max(ts, at), "guidely", payload));
@@ -2900,7 +3594,7 @@ var PersistentStore = class {
2900
3594
 
2901
3595
  // src/core/client.ts
2902
3596
  var SDK_NAME = "@guidelytech/react-native";
2903
- var SDK_VERSION = "0.15.0";
3597
+ var SDK_VERSION = "0.17.0";
2904
3598
  var DEFAULT_API_URL = "https://guidelytech.com";
2905
3599
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
2906
3600
  var CACHED_START_MS = 1500;
@@ -2915,6 +3609,10 @@ function settlesWithin(promise, ms) {
2915
3609
  });
2916
3610
  }
2917
3611
  var appVersionKey = (key) => `guidely:app:${key}`;
3612
+ var crashKey = (key) => `guidely:crash:${key}`;
3613
+ var earlyCrashKey = (key) => `guidely:crash-early:${key}`;
3614
+ var MAX_STORED_CRASHES = 10;
3615
+ var MAX_SESSION_EXCEPTIONS = 50;
2918
3616
  var UNKNOWN_VIEW = { selector: "View", tag: "View" };
2919
3617
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
2920
3618
  var emptyState = () => ({ guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] });
@@ -2931,6 +3629,8 @@ var GuidelyClient = class {
2931
3629
  /** Survey answers, guide completions/dismissals and checklist progress, kept until the server has them. */
2932
3630
  __publicField(this, "outbox", null);
2933
3631
  __publicField(this, "config", null);
3632
+ /** The `environment` option as passed (the resolved one is in `options.environment`). */
3633
+ __publicField(this, "envOption");
2934
3634
  __publicField(this, "configFetchedAt", 0);
2935
3635
  __publicField(this, "etag", null);
2936
3636
  __publicField(this, "anonymousId", "");
@@ -2952,6 +3652,8 @@ var GuidelyClient = class {
2952
3652
  __publicField(this, "expanded", /* @__PURE__ */ new Set());
2953
3653
  __publicField(this, "surveyView", null);
2954
3654
  __publicField(this, "resourceCenterOpen", false);
3655
+ /** The article a help-article link asked the resource center to show (`seq` tells repeats apart). */
3656
+ __publicField(this, "resourceCenterArticle", null);
2955
3657
  __publicField(this, "cleanups", []);
2956
3658
  __publicField(this, "timers", /* @__PURE__ */ new Set());
2957
3659
  __publicField(this, "visibilityWatchers", /* @__PURE__ */ new Map());
@@ -2992,6 +3694,16 @@ var GuidelyClient = class {
2992
3694
  __publicField(this, "localizedRc", null);
2993
3695
  /** "What's new" last-seen time when the resource center opened (entries after it read as new). */
2994
3696
  __publicField(this, "announcementsSeenAtOpen", null);
3697
+ // Error monitoring.
3698
+ __publicField(this, "errorScope", new ErrorScope(() => this.options.errors));
3699
+ __publicField(this, "exceptionThrottle", new ExceptionThrottle());
3700
+ /** Errors captured before storage and a config were loaded (whether error tracking is on is unknown until then). */
3701
+ __publicField(this, "earlyErrors", []);
3702
+ /** Crash reports left by earlier launches, sent once a config says error tracking is on. */
3703
+ __publicField(this, "storedCrashes", []);
3704
+ /** This launch's crash reports (normally one: the app ends right after). */
3705
+ __publicField(this, "launchCrashes", []);
3706
+ __publicField(this, "lastAppState", null);
2995
3707
  __publicField(this, "customListeners", /* @__PURE__ */ new Map());
2996
3708
  __publicField(this, "eventBus", {
2997
3709
  on: (name, cb) => {
@@ -3005,9 +3717,10 @@ var GuidelyClient = class {
3005
3717
  }
3006
3718
  });
3007
3719
  if (!options || !options.projectId) throw new Error("[Guidely] `projectId` (your environment public key, e.g. pk_dev_\u2026) is required.");
3720
+ this.envOption = options.environment || void 0;
3008
3721
  this.options = {
3009
3722
  ...options,
3010
- environment: options.environment ?? (options.projectId.startsWith("pk_prod") ? "production" : options.projectId.startsWith("pk_stag") ? "staging" : "development"),
3723
+ environment: options.environment || environmentFromKey(options.projectId),
3011
3724
  apiUrl: options.apiUrl ?? DEFAULT_API_URL,
3012
3725
  analytics: options.analytics ?? true,
3013
3726
  configRefreshMs: options.configRefreshMs ?? 5 * 60 * 1e3,
@@ -3023,6 +3736,7 @@ var GuidelyClient = class {
3023
3736
  if (options.user) this.user = { ...options.user };
3024
3737
  this.locale = options.locale ? normalizeLocale(options.locale) || null : null;
3025
3738
  this.flags = this.createFlags(options.flags);
3739
+ this.installErrorMonitoring();
3026
3740
  }
3027
3741
  createFlags(opts) {
3028
3742
  const base = { sdkKey: this.options.projectId, apiUrl: this.options.apiUrl, fetch: this.options.fetch, sdk: { name: SDK_NAME, version: SDK_VERSION } };
@@ -3070,7 +3784,7 @@ var GuidelyClient = class {
3070
3784
  async bootstrap() {
3071
3785
  const key = this.options.projectId;
3072
3786
  try {
3073
- 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]);
3787
+ 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)]);
3074
3788
  if (this.destroyed) return;
3075
3789
  this.anonymousId = this.store.get(KEYS.anon) || "";
3076
3790
  this.hadIdentity = !!this.anonymousId;
@@ -3083,7 +3797,9 @@ var GuidelyClient = class {
3083
3797
  this.state = { ...emptyState(), ...readJson(this.store, KEYS.state(key), {}) };
3084
3798
  this.user = this.options.user ? { ...this.options.user } : readJson(this.store, KEYS.user(key), null);
3085
3799
  const saved = readJson(this.store, KEYS.session, null);
3086
- this.session = saved?.id && !this.sessionExpired(Date.parse(saved.lastActiveAt)) ? saved : newSession();
3800
+ this.loadStoredCrashes();
3801
+ const crashed = !!saved?.id && this.storedCrashes.some((c) => c.sessionId === saved.id);
3802
+ this.session = saved?.id && !crashed && !this.sessionExpired(Date.parse(saved.lastActiveAt)) ? saved : newSession();
3087
3803
  this.persistSession();
3088
3804
  this.lastAudiences = new Set(this.state.audienceIds);
3089
3805
  this.syncFlagAttributes();
@@ -3102,9 +3818,10 @@ var GuidelyClient = class {
3102
3818
  this.hydrated = true;
3103
3819
  const cached = readJson(this.store, KEYS.config(key), null);
3104
3820
  this.etag = this.store.get(KEYS.etag(key));
3105
- if (cached && Array.isArray(cached.guides)) this.config = forPlatform(cached, this.bridge.device.platform);
3821
+ if (cached && Array.isArray(cached.guides)) this.useConfig(forPlatform(cached, this.bridge.device.platform));
3106
3822
  this.syncMessenger();
3107
3823
  this.pending.splice(0).forEach((fn) => fn());
3824
+ this.drainErrors();
3108
3825
  const before = this.config?.configVersion;
3109
3826
  const fresh = this.fetchConfig();
3110
3827
  if (!this.config) await fresh;
@@ -3129,22 +3846,33 @@ var GuidelyClient = class {
3129
3846
  if (this.hydrated) fn();
3130
3847
  else this.pending.push(fn);
3131
3848
  }
3849
+ /** Environment guides and flags target: the `environment` option, else the one the loaded config names, else a guess from the key. */
3850
+ get environment() {
3851
+ return this.envOption ?? this.config?.environment ?? this.options.environment;
3852
+ }
3853
+ useConfig(config) {
3854
+ const env = this.environment;
3855
+ this.config = config;
3856
+ if (this.environment !== env) this.syncFlagAttributes();
3857
+ }
3132
3858
  async fetchConfig() {
3133
3859
  try {
3134
3860
  const res = await this.api.get("/config", { etag: this.etag, params: { platform: this.bridge.device.platform } });
3135
3861
  this.configFetchedAt = Date.now();
3136
3862
  if (res.status === 304) return true;
3137
3863
  if (res.ok && res.data && Array.isArray(res.data.guides)) {
3138
- this.config = forPlatform(res.data, this.bridge.device.platform);
3864
+ const config = forPlatform(res.data, this.bridge.device.platform);
3865
+ this.useConfig(config);
3139
3866
  this.etag = res.etag;
3140
3867
  writeJson(this.store, KEYS.config(this.options.projectId), res.data);
3141
3868
  if (res.etag) this.store.set(KEYS.etag(this.options.projectId), res.etag);
3142
3869
  else this.store.remove(KEYS.etag(this.options.projectId));
3143
3870
  this.syncMessenger();
3144
3871
  if (this.hydrated) this.checkLaunch();
3145
- this.emitter.emit("config", this.config);
3872
+ this.drainErrors();
3873
+ this.emitter.emit("config", config);
3146
3874
  this.syncReplay();
3147
- this.log("config loaded", this.config.configVersion, `${this.config.guides.length} guides`);
3875
+ this.log("config loaded", config.configVersion, `${config.guides.length} guides`);
3148
3876
  return true;
3149
3877
  }
3150
3878
  this.reportError(new Error(`[Guidely] config request failed (${res.status})${this.config ? "; using cached config" : ""}`));
@@ -3185,7 +3913,12 @@ var GuidelyClient = class {
3185
3913
  }
3186
3914
  /** Called by the provider with React Native `AppState` changes. */
3187
3915
  setAppState(state) {
3188
- if (this.destroyed || !this.hydrated) return;
3916
+ if (this.destroyed) return;
3917
+ if (state !== this.lastAppState) {
3918
+ this.lastAppState = state;
3919
+ this.autoBreadcrumb("appLifecycle", { type: "navigation", category: "app.lifecycle", message: state, data: { state } });
3920
+ }
3921
+ if (!this.hydrated) return;
3189
3922
  if (state === "background") {
3190
3923
  const wasForeground = this.backgroundedAt === null;
3191
3924
  this.backgroundedAt = Date.now();
@@ -3370,8 +4103,13 @@ var GuidelyClient = class {
3370
4103
  setConsent(consent) {
3371
4104
  this.consent = consent;
3372
4105
  this.whenHydrated(() => {
3373
- if (consent === "denied") this.queue?.clear();
3374
- else if (consent === "granted") void this.queue?.flush();
4106
+ if (consent === "denied") {
4107
+ this.queue?.clear();
4108
+ this.earlyErrors = [];
4109
+ this.storedCrashes = [];
4110
+ this.launchCrashes = [];
4111
+ void this.writeCrashes();
4112
+ } else if (consent === "granted") void this.queue?.flush();
3375
4113
  this.syncReplay();
3376
4114
  });
3377
4115
  }
@@ -3395,6 +4133,7 @@ var GuidelyClient = class {
3395
4133
  accountId: this.user?.account?.id || null,
3396
4134
  sessionId: this.session.id,
3397
4135
  url: this.collectUrls() && this.screenName ? this.currentUrl() : void 0,
4136
+ ...this.appInfo().appVersion ? { appVersion: this.appInfo().appVersion } : {},
3398
4137
  ...extra
3399
4138
  };
3400
4139
  if (extra.properties) event.properties = sanitizeProperties(extra.properties);
@@ -3434,6 +4173,7 @@ var GuidelyClient = class {
3434
4173
  this.whenHydrated(() => {
3435
4174
  const changed = name !== this.screenName;
3436
4175
  if (changed) {
4176
+ this.autoBreadcrumb("navigation", { type: "navigation", category: "navigation", data: { ...this.screenName ? { from: this.screenName } : {}, to: name } });
3437
4177
  this.leaveScreen();
3438
4178
  this.flushHeatmap();
3439
4179
  }
@@ -3597,7 +4337,11 @@ var GuidelyClient = class {
3597
4337
  }
3598
4338
  /** True when taps are worth observing (clicks, rage taps or heatmaps are on); the provider skips resolving views otherwise. */
3599
4339
  isTapCaptureOn() {
3600
- return this.hydrated && (this.autocaptureOn("clicks") || this.autocaptureOn("rageClicks") || this.autocaptureOn("heatmaps"));
4340
+ return this.hydrated && (this.autocaptureOn("clicks") || this.autocaptureOn("rageClicks") || this.autocaptureOn("heatmaps") || this.tapBreadcrumbsOn());
4341
+ }
4342
+ /** Taps become `ui.click` breadcrumbs while errors can be reported (even with click autocapture off). */
4343
+ tapBreadcrumbsOn() {
4344
+ return this.crumbsOn("taps") && this.errorTrackingAllowed();
3601
4345
  }
3602
4346
  /**
3603
4347
  * Records a tap observed by the provider: `click` for an interactive element, `rage_click` for
@@ -3607,8 +4351,8 @@ var GuidelyClient = class {
3607
4351
  recordTap(tap) {
3608
4352
  try {
3609
4353
  if (!this.hydrated || this.destroyed || !Number.isFinite(tap.x) || !Number.isFinite(tap.y)) return;
3610
- const on = { clicks: this.autocaptureOn("clicks"), rage: this.autocaptureOn("rageClicks"), heat: this.autocaptureOn("heatmaps") };
3611
- if (!on.clicks && !on.rage && !on.heat) return;
4354
+ const on = { clicks: this.autocaptureOn("clicks"), rage: this.autocaptureOn("rageClicks"), heat: this.autocaptureOn("heatmaps"), crumb: this.tapBreadcrumbsOn() };
4355
+ if (!on.clicks && !on.rage && !on.heat && !on.crumb) return;
3612
4356
  const t = tap.t ?? Date.now();
3613
4357
  const url = this.screenName ? this.currentUrl() : "";
3614
4358
  this.tapChain = this.tapChain.then(() => this.processTap(tap, t, url, on)).catch((err) => this.reportError(err));
@@ -3625,6 +4369,8 @@ var GuidelyClient = class {
3625
4369
  rect = target.rect;
3626
4370
  }
3627
4371
  if (this.destroyed) return;
4372
+ if (on.crumb && el) this.autoBreadcrumb("taps", { timestamp: t / 1e3, type: "user", category: "ui.click", message: el.selector, data: { selector: el.selector } });
4373
+ if (!on.clicks && !on.rage && !on.heat) return;
3628
4374
  const platform = this.bridge.device.platform;
3629
4375
  const maskAll = !!this.config?.settings?.replay?.maskAllText || !!this.options.sessionReplay?.maskAllText;
3630
4376
  const eventUrl = this.collectUrls() && url ? url : void 0;
@@ -3723,6 +4469,277 @@ var GuidelyClient = class {
3723
4469
  }
3724
4470
  }
3725
4471
  // ---------------------------------------------------------------------------
4472
+ // Error monitoring (docs/error-monitoring/sdks.md)
4473
+ // ---------------------------------------------------------------------------
4474
+ /**
4475
+ * Reports a caught error as an `exception` event (`handled: true`), grouped into an issue on the
4476
+ * Errors page. The second argument takes Sentry's capture context (`tags`, `extra`, `level`,
4477
+ * `fingerprint`, `contexts`, `user`); any other key is attached to the occurrence as a property.
4478
+ * Needs the project's error tracking on (plus analytics and consent); `errors.captureErrors: false`
4479
+ * does not stop it. Returns the event id (empty when nothing is sent).
4480
+ */
4481
+ captureException(error, hint) {
4482
+ if (this.destroyed) return "";
4483
+ try {
4484
+ const { ctx, properties } = splitHint(hint);
4485
+ const props = exceptionFromValue(error, "manual", true);
4486
+ return this.deliverError(this.prepareCapture(error, props, properties, ctx, "generic", ctx.level ?? "error", false));
4487
+ } catch (err) {
4488
+ this.reportError(err);
4489
+ return "";
4490
+ }
4491
+ }
4492
+ /** Reports a message (default level `info`) as its own issue, grouped by the message. */
4493
+ captureMessage(message, level = "info", hint) {
4494
+ if (this.destroyed) return "";
4495
+ try {
4496
+ const { ctx } = splitHint(hint);
4497
+ const props = { type: "Message", message: String(message).slice(0, 1e3), frames: [], source: "message", handled: true };
4498
+ return this.deliverError(this.prepareCapture(void 0, props, {}, ctx, "message", ctx.level ?? level, false));
4499
+ } catch (err) {
4500
+ this.reportError(err);
4501
+ return "";
4502
+ }
4503
+ }
4504
+ /** Adds a breadcrumb (sent with the next errors; the last 100 are kept). */
4505
+ addBreadcrumb(crumb, hint) {
4506
+ if (!this.destroyed) this.errorScope.addBreadcrumb(crumb, hint);
4507
+ }
4508
+ /** Tags every later error (searchable on the issue page); `null` removes the tag. */
4509
+ setTag(key, value) {
4510
+ if (value === null || value === void 0) delete this.errorScope.tags[key];
4511
+ else this.errorScope.tags[key] = String(value);
4512
+ }
4513
+ setTags(tags) {
4514
+ for (const [k, v] of Object.entries(tags ?? {})) this.setTag(k, v);
4515
+ }
4516
+ /** Arbitrary data attached to every later error. */
4517
+ setExtra(key, value) {
4518
+ if (value === void 0) delete this.errorScope.extra[key];
4519
+ else this.errorScope.extra[key] = value;
4520
+ }
4521
+ setExtras(extras) {
4522
+ for (const [k, v] of Object.entries(extras ?? {})) this.setExtra(k, v);
4523
+ }
4524
+ /** A named context (`setContext("cart", { items: 3 })`) on every later error; `null` removes it. */
4525
+ setContext(name, context) {
4526
+ if (context && typeof context === "object") this.errorScope.contexts[name] = context;
4527
+ else delete this.errorScope.contexts[name];
4528
+ }
4529
+ /**
4530
+ * Sentry's `setUser`: identifies the user by `id` (like `identify`) and keeps `email` / `username`
4531
+ * on their errors. `null` forgets the user (`reset()`).
4532
+ */
4533
+ setUser(user) {
4534
+ if (!user) {
4535
+ this.errorScope.user = {};
4536
+ return this.reset();
4537
+ }
4538
+ const { id, email, username, ...traits } = user;
4539
+ this.errorScope.user = { ...email ? { email: String(email) } : {}, ...username ? { username: String(username) } : {} };
4540
+ if (id !== void 0 && id !== null && String(id)) void this.identify(String(id), { ...traits, ...email ? { email } : {}, ...username ? { username } : {} });
4541
+ }
4542
+ crumbsOn(kind) {
4543
+ const b = this.options.errors?.breadcrumbs;
4544
+ return b !== false && !(b && b[kind] === false);
4545
+ }
4546
+ autoBreadcrumb(kind, crumb, hint) {
4547
+ if (!this.destroyed && this.crumbsOn(kind)) this.errorScope.addBreadcrumb(crumb, hint);
4548
+ }
4549
+ /** Errors may be captured: analytics on, consent not denied and, once a config says, the project's error tracking on. */
4550
+ errorsWanted() {
4551
+ if (this.destroyed || !this.options.analytics || this.consent === "denied") return false;
4552
+ const s = this.config?.settings;
4553
+ return !this.config || (s?.analyticsEnabled ?? true) && !!s?.errorTracking?.enabled;
4554
+ }
4555
+ /** Errors may be queued now (a config is known and allows them). */
4556
+ errorTrackingAllowed() {
4557
+ return !!this.config && this.errorsWanted();
4558
+ }
4559
+ /** Requests to the Guidely deployment never become breadcrumbs. */
4560
+ isGuidelyUrl(url) {
4561
+ const base = this.options.apiUrl.replace(/\/+$/, "");
4562
+ return !!base && (url === base || url.startsWith(`${base}/`));
4563
+ }
4564
+ release() {
4565
+ return this.options.release ?? this.appInfo().appVersion;
4566
+ }
4567
+ /** Builds and filters one error (`null` when dropped). */
4568
+ prepareCapture(error, props, properties, ctx, mechanism, level, fatal) {
4569
+ if (!this.errorsWanted()) return null;
4570
+ const info = this.appInfo();
4571
+ const release = this.release();
4572
+ const detail = buildDetail({
4573
+ error,
4574
+ props,
4575
+ mechanism,
4576
+ scope: this.errorScope,
4577
+ ctx,
4578
+ level,
4579
+ contexts: reactNativeContexts(this.bridge.device, { ...info, inForeground: this.backgroundedAt === null }, this.bridge.globals),
4580
+ release,
4581
+ dist: this.options.errors?.dist ?? info.build,
4582
+ environment: this.environment,
4583
+ userId: this.user?.id || null,
4584
+ sdk: { name: SDK_NAME, version: SDK_VERSION }
4585
+ });
4586
+ return prepareError(props, properties, detail, this.options.errors, fatal, release);
4587
+ }
4588
+ /** Queues a prepared error, or holds it until storage and a config are loaded. Returns its event id ("" when dropped). */
4589
+ deliverError(p) {
4590
+ if (!p) return "";
4591
+ if (!this.hydrated || !this.config) {
4592
+ if (this.earlyErrors.length >= 10) return "";
4593
+ this.earlyErrors.push(p);
4594
+ return p.eventId;
4595
+ }
4596
+ return this.sendError(p) ? p.eventId : "";
4597
+ }
4598
+ sendError(p) {
4599
+ if (!this.errorTrackingAllowed()) return false;
4600
+ if (!p.fatal && !this.exceptionThrottle.allow(p.properties)) return false;
4601
+ const count = this.session.exceptions ?? 0;
4602
+ if (!p.fatal && count >= MAX_SESSION_EXCEPTIONS) return false;
4603
+ const event = this.enqueue("exception", { eventId: p.eventId, occurredAt: p.occurredAt, properties: p.properties, ...p.detail ? { detail: p.detail } : {} });
4604
+ if (!event) return false;
4605
+ this.session.exceptions = count + 1;
4606
+ this.persistSession();
4607
+ return true;
4608
+ }
4609
+ /** Sends what waited for storage and a config: earlier launches' crash reports, then early errors. */
4610
+ drainErrors() {
4611
+ if (!this.hydrated || !this.config || this.destroyed) return;
4612
+ const stored = this.storedCrashes.splice(0);
4613
+ if (stored.length) {
4614
+ if (this.errorTrackingAllowed()) {
4615
+ for (const c of stored) this.queue?.enqueue({ ...c, anonymousId: c.anonymousId || this.anonymousId, sessionId: c.sessionId || this.session.id });
4616
+ }
4617
+ void this.writeCrashes();
4618
+ }
4619
+ this.earlyErrors.splice(0).forEach((p) => this.sendError(p));
4620
+ }
4621
+ loadStoredCrashes() {
4622
+ const key = this.options.projectId;
4623
+ const valid = (c) => !!c && typeof c === "object" && typeof c.eventId === "string" && c.name === "exception";
4624
+ const read2 = (k) => {
4625
+ const v = readJson(this.store, k, []);
4626
+ return Array.isArray(v) ? v.filter(valid) : [];
4627
+ };
4628
+ const seen = /* @__PURE__ */ new Set();
4629
+ this.storedCrashes = [...read2(crashKey(key)), ...read2(earlyCrashKey(key))].filter((c) => !seen.has(c.eventId) && !!seen.add(c.eventId)).slice(-MAX_STORED_CRASHES);
4630
+ this.store.remove(earlyCrashKey(key));
4631
+ if (this.storedCrashes.length) void this.writeCrashes();
4632
+ }
4633
+ /**
4634
+ * Writes the unsent crash reports to storage, directly (not behind other pending writes) so the
4635
+ * write starts before the app ends. Resolves when the storage has it.
4636
+ */
4637
+ writeCrashes() {
4638
+ const storage = this.options.storage;
4639
+ const key = this.options.projectId;
4640
+ const direct = (op) => {
4641
+ try {
4642
+ return Promise.resolve(op()).catch((err) => this.log("crash report storage error", err));
4643
+ } catch (err) {
4644
+ this.log("crash report storage error", err);
4645
+ return Promise.resolve();
4646
+ }
4647
+ };
4648
+ if (!this.hydrated) {
4649
+ const json2 = JSON.stringify(this.launchCrashes.slice(-MAX_STORED_CRASHES));
4650
+ return storage ? direct(() => storage.setItem(earlyCrashKey(key), json2)) : Promise.resolve();
4651
+ }
4652
+ const seen = /* @__PURE__ */ new Set();
4653
+ const all = [...this.storedCrashes, ...this.launchCrashes].filter((c) => !seen.has(c.eventId) && !!seen.add(c.eventId)).slice(-MAX_STORED_CRASHES);
4654
+ if (!all.length) {
4655
+ this.store.remove(crashKey(key));
4656
+ return storage ? direct(() => storage.removeItem(crashKey(key))) : Promise.resolve();
4657
+ }
4658
+ const json = JSON.stringify(all);
4659
+ this.store.set(crashKey(key), json);
4660
+ return storage ? direct(() => storage.setItem(crashKey(key), json)) : Promise.resolve();
4661
+ }
4662
+ /**
4663
+ * A fatal JS error: React Native ends a release app right after the global handler, so the report
4664
+ * is written to storage at once (and sent on the next launch), and sent now as well when the
4665
+ * network allows. The promise tells the handler when React Native's own handler may run.
4666
+ */
4667
+ saveCrash(p) {
4668
+ if (!this.errorsWanted()) return void 0;
4669
+ const info = this.appInfo();
4670
+ const event = {
4671
+ eventId: p.eventId,
4672
+ name: "exception",
4673
+ occurredAt: p.occurredAt,
4674
+ anonymousId: this.anonymousId,
4675
+ userId: this.user?.id || null,
4676
+ accountId: this.user?.account?.id || null,
4677
+ ...this.hydrated ? { sessionId: this.session.id } : {},
4678
+ ...this.collectUrls() && this.screenName ? { url: this.currentUrl() } : {},
4679
+ ...info.appVersion ? { appVersion: info.appVersion } : {},
4680
+ properties: sanitizeProperties(p.properties),
4681
+ ...p.detail ? { detail: p.detail } : {}
4682
+ };
4683
+ this.launchCrashes.push(event);
4684
+ const written = this.writeCrashes();
4685
+ if (!this.hydrated || !this.config || !this.sendError(p)) return written;
4686
+ const sent = this.queue?.flush() ?? Promise.resolve();
4687
+ return Promise.all([written, sent]).then(async () => {
4688
+ if (this.queue?.peek().some((e) => e.eventId === event.eventId)) return;
4689
+ this.launchCrashes = this.launchCrashes.filter((c) => c.eventId !== event.eventId);
4690
+ await this.writeCrashes();
4691
+ });
4692
+ }
4693
+ /** An uncaught JS error from `ErrorUtils` (fatal ones end the app in release builds). */
4694
+ captureGlobalError(error, fatal) {
4695
+ try {
4696
+ const props = exceptionFromValue(error, "onerror", false);
4697
+ if (props.message.startsWith("[Guidely]")) return void 0;
4698
+ const p = this.prepareCapture(error, props, {}, {}, "onerror", fatal ? "fatal" : "error", fatal);
4699
+ if (!p) return void 0;
4700
+ if (fatal) return this.saveCrash(p);
4701
+ this.deliverError(p);
4702
+ } catch (err) {
4703
+ this.log("error capture failed", err);
4704
+ }
4705
+ return void 0;
4706
+ }
4707
+ captureRejection(reason) {
4708
+ try {
4709
+ const props = exceptionFromValue(reason, "unhandledrejection", false, "UnhandledRejection");
4710
+ if (props.message.startsWith("[Guidely]")) return;
4711
+ this.deliverError(this.prepareCapture(reason, props, {}, {}, "unhandledrejection", "error", false));
4712
+ } catch (err) {
4713
+ this.log("error capture failed", err);
4714
+ }
4715
+ }
4716
+ /**
4717
+ * Guide breadcrumbs, and (with the bridge's JS globals) the global error handler, promise
4718
+ * rejection tracking and console / network breadcrumbs. Undone by `destroy()`.
4719
+ */
4720
+ installErrorMonitoring() {
4721
+ const guideCrumb = (message, guide, stepIndex) => this.autoBreadcrumb("guides", { type: "info", category: "guide", message, data: { guideId: guide.id, ...stepIndex !== void 0 ? { stepIndex } : {} } });
4722
+ this.cleanups.push(
4723
+ this.emitter.on("guide:start", (g2) => guideCrumb("Guide shown", g2)),
4724
+ this.emitter.on("guide:step", (s) => guideCrumb("Guide step", s.guide, s.stepIndex)),
4725
+ this.emitter.on("guide:complete", (g2) => guideCrumb("Guide completed", g2)),
4726
+ this.emitter.on("guide:dismiss", (g2) => guideCrumb("Guide dismissed", g2)),
4727
+ this.emitter.on("guide:skip", (g2) => guideCrumb("Guide skipped", g2))
4728
+ );
4729
+ const g = this.bridge.globals;
4730
+ if (!g) return;
4731
+ try {
4732
+ if (this.crumbsOn("console")) this.cleanups.push(instrumentConsole(g.console, (c) => this.autoBreadcrumb("console", c)));
4733
+ if (this.crumbsOn("http")) this.cleanups.push(instrumentHttp(g, (c) => this.autoBreadcrumb("http", c), (url) => this.isGuidelyUrl(url)));
4734
+ if (this.options.errors?.captureErrors !== false) {
4735
+ this.cleanups.push(installGlobalHandler(g, (error, fatal) => this.captureGlobalError(error, fatal)));
4736
+ this.cleanups.push(installRejectionTracking(g, (reason) => this.captureRejection(reason), this.options.errors?.rejectionTracker));
4737
+ }
4738
+ } catch (err) {
4739
+ this.reportError(err);
4740
+ }
4741
+ }
4742
+ // ---------------------------------------------------------------------------
3726
4743
  // Targeting + triggers
3727
4744
  // ---------------------------------------------------------------------------
3728
4745
  /** Builds the targeting context for the current moment. */
@@ -3748,7 +4765,7 @@ var GuidelyClient = class {
3748
4765
  completedChecklists: new Set(keysOf(Object.entries(this.state.checklists).filter(([, p]) => !!p.completedAt).map(([id]) => id))),
3749
4766
  audienceIds: new Set(this.state.audienceIds),
3750
4767
  now: /* @__PURE__ */ new Date(),
3751
- environment: this.options.environment,
4768
+ environment: this.environment,
3752
4769
  locale: device.locale,
3753
4770
  platform: device.platform,
3754
4771
  screen: this.screenName,
@@ -4117,6 +5134,8 @@ var GuidelyClient = class {
4117
5134
  */
4118
5135
  navigate(url) {
4119
5136
  if (!url) return;
5137
+ const articleId = articleIdFromHref(url);
5138
+ if (articleId) return this.openArticle(articleId);
4120
5139
  try {
4121
5140
  if (this.options.onNavigate?.(url) === true) return;
4122
5141
  if (/^[a-z][a-z0-9+.-]*:/i.test(url)) this.bridge.openExternal(url);
@@ -4140,7 +5159,7 @@ var GuidelyClient = class {
4140
5159
  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) });
4141
5160
  const checklists = [];
4142
5161
  this.checklists.forEach((w, id) => checklists.push({ guide: w.guide, progress: this.getChecklistProgress(id), open: w.open }));
4143
- this.ui = { steps, checklists, survey: this.surveyView, resourceCenterOpen: this.resourceCenterOpen, theme: this.themeFor(), messenger: !!this.messenger };
5162
+ this.ui = { steps, checklists, survey: this.surveyView, resourceCenterOpen: this.resourceCenterOpen, resourceCenterArticle: this.resourceCenterArticle, theme: this.themeFor(), messenger: !!this.messenger };
4144
5163
  for (const cb of Array.from(this.uiListeners)) {
4145
5164
  try {
4146
5165
  cb();
@@ -4160,6 +5179,12 @@ var GuidelyClient = class {
4160
5179
  closeStep(guideId) {
4161
5180
  this.flowFor(guideId)?.controller.dismiss();
4162
5181
  }
5182
+ /** The video on a step was tapped: records `guide_video_clicked`, then opens the video outside the app. */
5183
+ openVideo(guideId, url) {
5184
+ const v = this.flowFor(guideId)?.view;
5185
+ 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 } });
5186
+ this.navigate(url);
5187
+ }
4163
5188
  /** The dimmed backdrop behind a step was tapped. */
4164
5189
  backdropPressed(guideId) {
4165
5190
  const f = this.flowFor(guideId);
@@ -4432,10 +5457,40 @@ var GuidelyClient = class {
4432
5457
  this.emitter.emit("resource-center:open");
4433
5458
  this.notify();
4434
5459
  }
5460
+ /**
5461
+ * Opens one help article: in Scout's Help tab when the messenger is on, otherwise in the resource
5462
+ * center. Help-article links in guides and articles (`#guidely-article:<id>`) call this.
5463
+ */
5464
+ openArticle(articleId) {
5465
+ if (this.messenger) {
5466
+ this.whenHydrated(() => void this.messenger?.openArticle(articleId));
5467
+ return;
5468
+ }
5469
+ if (!this.getResourceCenter()) {
5470
+ this.log("openArticle: the resource center is not enabled or published for this environment");
5471
+ return;
5472
+ }
5473
+ this.resourceCenterArticle = { id: articleId, seq: (this.resourceCenterArticle?.seq ?? 0) + 1 };
5474
+ if (this.resourceCenterOpen) this.notify();
5475
+ else this.openResourceCenter();
5476
+ }
5477
+ /** A resource-center article with its body: from the config, or fetched when the config left it out. */
5478
+ async loadArticle(articleId) {
5479
+ const listed = this.getResourceCenter()?.articles.find((a) => a.id === articleId);
5480
+ if (listed && !listed.bodyOmitted) return listed;
5481
+ try {
5482
+ const locale = this.articleLocale();
5483
+ const res = await this.api.get(`/articles/${encodeURIComponent(articleId)}`, locale ? { params: { locale } } : {});
5484
+ return res.ok && res.data ? res.data.article : null;
5485
+ } catch {
5486
+ return null;
5487
+ }
5488
+ }
4435
5489
  closeResourceCenter() {
4436
5490
  if (this.messenger) return this.closeMessenger();
4437
5491
  if (!this.resourceCenterOpen) return;
4438
5492
  this.resourceCenterOpen = false;
5493
+ this.resourceCenterArticle = null;
4439
5494
  this.notify();
4440
5495
  }
4441
5496
  // ---------------------------------------------------------------------------
@@ -4531,7 +5586,7 @@ var GuidelyClient = class {
4531
5586
  locale: d.locale,
4532
5587
  appVersion: this.options.appVersion ?? d.appVersion,
4533
5588
  osVersion: d.osVersion,
4534
- environment: this.options.environment,
5589
+ environment: this.environment,
4535
5590
  ...this.screenName ? { screen: this.screenName, path: this.currentUrl() } : {},
4536
5591
  ...this.user?.traits ?? {},
4537
5592
  id: this.user?.id || this.anonymousId || void 0,
@@ -4656,6 +5711,35 @@ function detectAppVersion(src) {
4656
5711
  return {};
4657
5712
  }
4658
5713
  }
5714
+ function detectDeviceModel(src) {
5715
+ try {
5716
+ const expo = src.expoModules?.ExpoDevice ?? src.nativeModules.ExpoDevice;
5717
+ const info = src.nativeModules.RNDeviceInfo;
5718
+ const c = src.platform.constants ?? {};
5719
+ const android = src.platform.OS === "android";
5720
+ const model = text(constant(expo, "modelName")) ?? text(constant(info, "model")) ?? (android ? text(c.Model) : void 0);
5721
+ const manufacturer = text(constant(expo, "manufacturer")) ?? (android ? text(c.Manufacturer) : "Apple");
5722
+ const brand = text(constant(expo, "brand")) ?? text(constant(info, "brand")) ?? (android ? text(c.Brand) : void 0);
5723
+ const isDevice = constant(expo, "isDevice");
5724
+ return {
5725
+ ...model ? { model } : {},
5726
+ ...manufacturer ? { manufacturer } : {},
5727
+ ...brand ? { brand } : {},
5728
+ ...typeof isDevice === "boolean" ? { simulator: !isDevice } : {}
5729
+ };
5730
+ } catch {
5731
+ return {};
5732
+ }
5733
+ }
5734
+ function detectReactNativeVersion(src) {
5735
+ try {
5736
+ const v = src.platform.constants?.reactNativeVersion;
5737
+ if (!v || typeof v.major !== "number") return void 0;
5738
+ return { major: v.major, minor: Number(v.minor) || 0, patch: Number(v.patch) || 0, ...v.prerelease != null ? { prerelease: String(v.prerelease) } : {} };
5739
+ } catch {
5740
+ return void 0;
5741
+ }
5742
+ }
4659
5743
  function detectOsVersion(src) {
4660
5744
  try {
4661
5745
  const c = src.platform.constants ?? {};
@@ -4788,7 +5872,7 @@ function measureHost(host) {
4788
5872
  done(null);
4789
5873
  });
4790
5874
  }
4791
- function describe(f, masked, opts, withText) {
5875
+ function describe2(f, masked, opts, withText) {
4792
5876
  const props = f.memoizedProps && typeof f.memoizedProps === "object" ? f.memoizedProps : {};
4793
5877
  const host = firstHost(f);
4794
5878
  const el = { selector: idSelector(f) ?? pathSelector(f), tag: componentName(f.type) ?? "View" };
@@ -4833,8 +5917,8 @@ function resolveFiber(target, opts = {}) {
4833
5917
  const element = press ?? role;
4834
5918
  return {
4835
5919
  ignored: false,
4836
- element: element ? describe(element, masked, opts, true) : null,
4837
- hit: hitFiber ? describe(hitFiber, masked, opts, componentName(hitFiber.type) === "Text") : null
5920
+ element: element ? describe2(element, masked, opts, true) : null,
5921
+ hit: hitFiber ? describe2(hitFiber, masked, opts, componentName(hitFiber.type) === "Text") : null
4838
5922
  };
4839
5923
  } catch {
4840
5924
  return none;
@@ -4846,7 +5930,7 @@ function resolveTapTarget(event, opts = {}) {
4846
5930
 
4847
5931
  // src/ui/Overlay.tsx
4848
5932
  import { useCallback as useCallback2, useEffect as useEffect7, useRef as useRef4, useState as useState7, useSyncExternalStore as useSyncExternalStore3 } from "react";
4849
- import { Platform as Platform5, Pressable as Pressable8, StatusBar, StyleSheet as StyleSheet9, Text as Text8, useColorScheme, useWindowDimensions, View as View9 } from "react-native";
5933
+ import { Platform as Platform5, Pressable as Pressable9, StatusBar, StyleSheet as StyleSheet9, Text as Text8, useColorScheme, useWindowDimensions, View as View9 } from "react-native";
4850
5934
 
4851
5935
  // src/ui/Checklist.tsx
4852
5936
  import { Pressable as Pressable3, ScrollView as ScrollView2, StyleSheet as StyleSheet3, Text as Text3, View as View3 } from "react-native";
@@ -4967,7 +6051,7 @@ function parseHtml(html) {
4967
6051
  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;
4968
6052
  else current.runs.push(run);
4969
6053
  };
4970
- const size = (attrs, name) => {
6054
+ const size2 = (attrs, name) => {
4971
6055
  const n = Number(attr(attrs, name));
4972
6056
  return n > 0 ? n : void 0;
4973
6057
  };
@@ -5029,8 +6113,8 @@ function parseHtml(html) {
5029
6113
  }
5030
6114
  } else if (tag === "img" && !closing) {
5031
6115
  const src = attr(attrs, "src");
5032
- const w = size(attrs, "width");
5033
- const h = size(attrs, "height");
6116
+ const w = size2(attrs, "width");
6117
+ const h = size2(attrs, "height");
5034
6118
  if (isSafeMediaUrl(src)) pdf.pages.push({ src, alt: attr(attrs, "alt") ?? "", ...w && h ? { width: w, height: h } : {} });
5035
6119
  }
5036
6120
  continue;
@@ -5204,10 +6288,10 @@ function BodyPdf({ block, palette: p, onLink }) {
5204
6288
  ] }) })
5205
6289
  ] });
5206
6290
  }
5207
- function RichText({ html, palette: p, onLink, size = 15 }) {
6291
+ function RichText({ html, palette: p, onLink, size: size2 = 15 }) {
5208
6292
  const blocks = useMemo2(() => parseHtml(html), [html]);
5209
6293
  if (!blocks.length) return null;
5210
- const base = { color: p.text, fontSize: size, lineHeight: Math.round(size * 1.42), fontFamily: p.fontFamily };
6294
+ const base = { color: p.text, fontSize: size2, lineHeight: Math.round(size2 * 1.42), fontFamily: p.fontFamily };
5211
6295
  return /* @__PURE__ */ jsx3(View2, { style: styles2.wrap, children: blocks.map((b, i) => {
5212
6296
  if (b.kind === "image") return /* @__PURE__ */ jsx3(BodyImage, { block: b, palette: p }, i);
5213
6297
  if (b.kind === "video") return /* @__PURE__ */ jsx3(BodyVideo, { block: b, palette: p, onLink }, i);
@@ -5410,17 +6494,17 @@ function useReduceMotion() {
5410
6494
  }, []);
5411
6495
  return on;
5412
6496
  }
5413
- function MessengerLauncher({ store, client, palette: p, style, frame, defaultSide }) {
6497
+ function MessengerLauncher({ store, client, palette: p, style, frame: frame2, defaultSide }) {
5414
6498
  const snap = useMessenger(store);
5415
6499
  const reduceMotion = useReduceMotion();
5416
6500
  const [placement, setPlacement] = useState3(() => client.getMessengerLauncherPlacement());
5417
6501
  const pan = useRef2(new Animated.ValueXY()).current;
5418
6502
  const offset = useRef2({ x: 0, y: 0 });
5419
6503
  const dragging = useRef2(false);
5420
- const live = useRef2({ frame, defaultSide, reduceMotion });
5421
- live.current = { frame, defaultSide, reduceMotion };
5422
- const { width, height, size, defaultBottom } = frame;
5423
- const { top, bottom, left, right } = frame.insets;
6504
+ const live = useRef2({ frame: frame2, defaultSide, reduceMotion });
6505
+ live.current = { frame: frame2, defaultSide, reduceMotion };
6506
+ const { width, height, size: size2, defaultBottom } = frame2;
6507
+ const { top, bottom, left, right } = frame2.insets;
5424
6508
  useLayoutEffect(() => {
5425
6509
  if (dragging.current) return;
5426
6510
  const f = live.current.frame;
@@ -5428,7 +6512,7 @@ function MessengerLauncher({ store, client, palette: p, style, frame, defaultSid
5428
6512
  const at = placement ? placementPoint(f, placement) : anchor;
5429
6513
  offset.current = { x: at.x - anchor.x, y: at.y - anchor.y };
5430
6514
  pan.setValue(offset.current);
5431
- }, [pan, placement, defaultSide, width, height, size, defaultBottom, top, bottom, left, right]);
6515
+ }, [pan, placement, defaultSide, width, height, size2, defaultBottom, top, bottom, left, right]);
5432
6516
  const responder = useMemo3(() => {
5433
6517
  const moved = (g) => Math.hypot(g.dx, g.dy) > LAUNCHER_DRAG_THRESHOLD;
5434
6518
  const anchor = () => placementPoint(live.current.frame, { side: live.current.defaultSide, fraction: 0 });
@@ -5781,7 +6865,7 @@ var styles4 = StyleSheet4.create({
5781
6865
  import { Fragment as Fragment4, useEffect as useEffect4, useMemo as useMemo4, useState as useState4 } from "react";
5782
6866
  import { Modal as Modal3, Platform as Platform3, Pressable as Pressable5, ScrollView as ScrollView4, StyleSheet as StyleSheet5, Text as Text5, TextInput as TextInput2, View as View5 } from "react-native";
5783
6867
  import { Fragment as Fragment5, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
5784
- function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
6868
+ function ResourceCenter({ client, palette: p, open, articleRequest, topInset, bottomInset }) {
5785
6869
  const rc = client.getResourceCenter();
5786
6870
  const [query, setQuery] = useState4("");
5787
6871
  const [results, setResults] = useState4(null);
@@ -5794,6 +6878,19 @@ function ResourceCenter({ client, palette: p, open, topInset, bottomInset }) {
5794
6878
  setArticle(null);
5795
6879
  }
5796
6880
  }, [open]);
6881
+ useEffect4(() => {
6882
+ if (!open || !articleRequest) return;
6883
+ let alive = true;
6884
+ void client.loadArticle(articleRequest.id).then((a) => {
6885
+ if (!alive || !a) return;
6886
+ if (a.externalUrl) return client.navigate(a.externalUrl);
6887
+ client.articleViewed(a);
6888
+ setArticle(a);
6889
+ });
6890
+ return () => {
6891
+ alive = false;
6892
+ };
6893
+ }, [client, open, articleRequest?.seq]);
5797
6894
  useEffect4(() => {
5798
6895
  const q = query.trim();
5799
6896
  if (q.length < 2) {
@@ -5933,24 +7030,24 @@ var styles5 = StyleSheet5.create({
5933
7030
 
5934
7031
  // src/ui/StepLayer.tsx
5935
7032
  import { useEffect as useEffect5, useRef as useRef3, useState as useState5 } from "react";
5936
- import { AccessibilityInfo as AccessibilityInfo2, Animated as Animated2, Easing as Easing2, Pressable as Pressable6, StyleSheet as StyleSheet7, View as View7 } from "react-native";
7033
+ import { AccessibilityInfo as AccessibilityInfo2, Animated as Animated2, Easing as Easing2, Pressable as Pressable7, StyleSheet as StyleSheet7, View as View7 } from "react-native";
5937
7034
 
5938
7035
  // src/core/position.ts
5939
7036
  var MARGIN = 12;
5940
7037
  var ARROW_INSET = 18;
5941
7038
  var opposite = { top: "bottom", bottom: "top", left: "right", right: "left" };
5942
- function positionCard(target, size, viewport, insets, placement, offset) {
7039
+ function positionCard(target, size2, viewport, insets, placement, offset) {
5943
7040
  const [rawSide, align] = placement.split("-");
5944
7041
  const fits = (side2) => {
5945
7042
  switch (side2) {
5946
7043
  case "bottom":
5947
- return target.y + target.height + offset + size.height <= viewport.height - insets.bottom - MARGIN;
7044
+ return target.y + target.height + offset + size2.height <= viewport.height - insets.bottom - MARGIN;
5948
7045
  case "top":
5949
- return target.y - offset - size.height >= insets.top + MARGIN;
7046
+ return target.y - offset - size2.height >= insets.top + MARGIN;
5950
7047
  case "right":
5951
- return target.x + target.width + offset + size.width <= viewport.width - insets.right - MARGIN;
7048
+ return target.x + target.width + offset + size2.width <= viewport.width - insets.right - MARGIN;
5952
7049
  case "left":
5953
- return target.x - offset - size.width >= insets.left + MARGIN;
7050
+ return target.x - offset - size2.width >= insets.left + MARGIN;
5954
7051
  }
5955
7052
  };
5956
7053
  const order = rawSide === "auto" ? ["bottom", "top"] : [rawSide, opposite[rawSide], "bottom", "top"];
@@ -5959,21 +7056,21 @@ function positionCard(target, size, viewport, insets, placement, offset) {
5959
7056
  const clamp3 = (v, min, max) => Math.max(min, Math.min(max, v));
5960
7057
  if (side === "top" || side === "bottom") {
5961
7058
  const cx = target.x + target.width / 2;
5962
- let x2 = align === "start" ? target.x : align === "end" ? target.x + target.width - size.width : cx - size.width / 2;
5963
- x2 = clamp3(x2, insets.left + MARGIN, Math.max(insets.left + MARGIN, viewport.width - insets.right - MARGIN - size.width));
5964
- let y2 = side === "bottom" ? target.y + target.height + offset : target.y - offset - size.height;
5965
- y2 = clamp3(y2, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
5966
- return { x: x2, y: y2, side, arrow: clamp3(cx - x2, ARROW_INSET, size.width - ARROW_INSET) };
7059
+ let x2 = align === "start" ? target.x : align === "end" ? target.x + target.width - size2.width : cx - size2.width / 2;
7060
+ x2 = clamp3(x2, insets.left + MARGIN, Math.max(insets.left + MARGIN, viewport.width - insets.right - MARGIN - size2.width));
7061
+ let y2 = side === "bottom" ? target.y + target.height + offset : target.y - offset - size2.height;
7062
+ y2 = clamp3(y2, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size2.height));
7063
+ return { x: x2, y: y2, side, arrow: clamp3(cx - x2, ARROW_INSET, size2.width - ARROW_INSET) };
5967
7064
  }
5968
7065
  const cy = target.y + target.height / 2;
5969
- let y = align === "start" ? target.y : align === "end" ? target.y + target.height - size.height : cy - size.height / 2;
5970
- y = clamp3(y, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
5971
- const x = side === "right" ? target.x + target.width + offset : target.x - offset - size.width;
5972
- return { x, y, side, arrow: clamp3(cy - y, ARROW_INSET, size.height - ARROW_INSET) };
7066
+ let y = align === "start" ? target.y : align === "end" ? target.y + target.height - size2.height : cy - size2.height / 2;
7067
+ y = clamp3(y, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size2.height));
7068
+ const x = side === "right" ? target.x + target.width + offset : target.x - offset - size2.width;
7069
+ return { x, y, side, arrow: clamp3(cy - y, ARROW_INSET, size2.height - ARROW_INSET) };
5973
7070
  }
5974
7071
 
5975
7072
  // src/ui/StepCard.tsx
5976
- import { Image as Image2, StyleSheet as StyleSheet6, Text as Text6, View as View6 } from "react-native";
7073
+ import { Image as Image2, Pressable as Pressable6, StyleSheet as StyleSheet6, Text as Text6, View as View6 } from "react-native";
5977
7074
  import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
5978
7075
  function StepCard({ view, client, palette: p, width, style, compact }) {
5979
7076
  const { step, guide } = view;
@@ -5981,8 +7078,25 @@ function StepCard({ view, client, palette: p, width, style, compact }) {
5981
7078
  const body = step.type === "custom" ? step.customHtml || step.body : step.body;
5982
7079
  const showProgress = step.showProgress && view.total > 1;
5983
7080
  const onLink = (url) => client.navigate(url);
7081
+ const video = stepVideo(step);
5984
7082
  return /* @__PURE__ */ jsx7(Surface, { palette: p, style: [{ width }, style], children: /* @__PURE__ */ jsxs5(View6, { accessible: false, accessibilityViewIsModal: view.asModal || step.type === "modal", style: compact ? styles6.compact : styles6.inner, children: [
5985
- step.imageUrl ? (
7083
+ video ? (
7084
+ // The video opens outside the app (browser or the provider's app), like a new tab on the web.
7085
+ /* @__PURE__ */ jsxs5(
7086
+ Pressable6,
7087
+ {
7088
+ onPress: () => client.openVideo(guide.id, video.url),
7089
+ accessibilityRole: "button",
7090
+ accessibilityLabel: step.title ? `Play video: ${step.title}` : "Play video",
7091
+ accessibilityHint: "Opens the video outside the app",
7092
+ style: [styles6.video, { borderRadius: Math.max(0, p.radius - 4) }],
7093
+ children: [
7094
+ video.poster ? /* @__PURE__ */ jsx7(Image2, { source: { uri: video.poster }, style: StyleSheet6.absoluteFill, resizeMode: "cover", accessibilityIgnoresInvertColors: true, accessible: false }) : null,
7095
+ /* @__PURE__ */ jsx7(View6, { style: styles6.play, children: /* @__PURE__ */ jsx7(View6, { style: styles6.playTriangle }) })
7096
+ ]
7097
+ }
7098
+ )
7099
+ ) : step.imageUrl ? (
5986
7100
  // An empty description marks the image as decorative: screen readers skip it.
5987
7101
  /* @__PURE__ */ jsx7(
5988
7102
  Image2,
@@ -6023,6 +7137,10 @@ var styles6 = StyleSheet6.create({
6023
7137
  inner: { padding: 16, gap: 10 },
6024
7138
  compact: { paddingHorizontal: 14, paddingVertical: 10, gap: 6 },
6025
7139
  image: { width: "100%", height: 140, marginBottom: 2 },
7140
+ video: { width: "100%", aspectRatio: 16 / 9, maxHeight: 200, marginBottom: 2, overflow: "hidden", backgroundColor: "#0f172a", alignItems: "center", justifyContent: "center" },
7141
+ play: { width: 52, height: 52, borderRadius: 26, backgroundColor: "rgba(15,23,42,0.72)", alignItems: "center", justifyContent: "center", paddingLeft: 4 },
7142
+ // A CSS-border triangle: no icon font needed.
7143
+ playTriangle: { width: 0, height: 0, borderTopWidth: 10, borderBottomWidth: 10, borderLeftWidth: 16, borderTopColor: "transparent", borderBottomColor: "transparent", borderLeftColor: "#fff" },
6026
7144
  header: { flexDirection: "row", alignItems: "flex-start", gap: 8 },
6027
7145
  title: { flex: 1, fontSize: 17, fontWeight: "700", lineHeight: 22 },
6028
7146
  flex: { flex: 1 },
@@ -6082,7 +7200,7 @@ function StepLayer({ view, client, palette: p, geometry }) {
6082
7200
  if (step.type === "hotspot" && !view.expanded) return;
6083
7201
  AccessibilityInfo2.announceForAccessibility?.(announcement(view));
6084
7202
  }, [view.key, view.expanded]);
6085
- const backdrop = (children) => /* @__PURE__ */ jsx8(Pressable6, { accessible: false, style: [StyleSheet7.absoluteFill, { backgroundColor: p.overlay }], onPress: () => client.backdropPressed(view.guide.id), children });
7203
+ const backdrop = (children) => /* @__PURE__ */ jsx8(Pressable7, { accessible: false, style: [StyleSheet7.absoluteFill, { backgroundColor: p.overlay }], onPress: () => client.backdropPressed(view.guide.id), children });
6086
7204
  const anchored = !view.asModal && ["tooltip", "popover", "spotlight", "hotspot"].includes(step.type);
6087
7205
  if (!anchored && step.type === "banner") {
6088
7206
  const top = step.position.startsWith("top");
@@ -6102,9 +7220,9 @@ function StepLayer({ view, client, palette: p, geometry }) {
6102
7220
  }
6103
7221
  if (!rect) return null;
6104
7222
  if (step.type === "hotspot") {
6105
- const size = step.hotspot?.size ?? 14;
7223
+ const size2 = step.hotspot?.size ?? 14;
6106
7224
  return /* @__PURE__ */ jsxs6(View7, { pointerEvents: "box-none", style: StyleSheet7.absoluteFill, children: [
6107
- /* @__PURE__ */ jsx8(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" }),
7225
+ /* @__PURE__ */ jsx8(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" }),
6108
7226
  view.expanded ? /* @__PURE__ */ jsx8(Anchored, { view, client, palette: p, geometry, rect, width: cardWidth }) : null
6109
7227
  ] });
6110
7228
  }
@@ -6121,10 +7239,10 @@ function StepLayer({ view, client, palette: p, geometry }) {
6121
7239
  [hole.x + hole.width - r, hole.y + hole.height - r, -2 * r, -2 * r]
6122
7240
  ];
6123
7241
  return /* @__PURE__ */ jsxs6(Animated2.View, { pointerEvents: "box-none", style: [StyleSheet7.absoluteFill, { opacity }], children: [
6124
- /* @__PURE__ */ jsx8(Pressable6, { accessible: false, onPress: press, style: [styles7.abs, shade, { left: 0, right: 0, top: 0, height: Math.max(0, hole.y) }] }),
6125
- /* @__PURE__ */ jsx8(Pressable6, { accessible: false, onPress: press, style: [styles7.abs, shade, { left: 0, right: 0, top: hole.y + hole.height, bottom: 0 }] }),
6126
- /* @__PURE__ */ jsx8(Pressable6, { accessible: false, onPress: press, style: [styles7.abs, shade, { left: 0, width: Math.max(0, hole.x), top: hole.y, height: hole.height }] }),
6127
- /* @__PURE__ */ jsx8(Pressable6, { accessible: false, onPress: press, style: [styles7.abs, shade, { left: hole.x + hole.width, right: 0, top: hole.y, height: hole.height }] }),
7242
+ /* @__PURE__ */ jsx8(Pressable7, { accessible: false, onPress: press, style: [styles7.abs, shade, { left: 0, right: 0, top: 0, height: Math.max(0, hole.y) }] }),
7243
+ /* @__PURE__ */ jsx8(Pressable7, { accessible: false, onPress: press, style: [styles7.abs, shade, { left: 0, right: 0, top: hole.y + hole.height, bottom: 0 }] }),
7244
+ /* @__PURE__ */ jsx8(Pressable7, { accessible: false, onPress: press, style: [styles7.abs, shade, { left: 0, width: Math.max(0, hole.x), top: hole.y, height: hole.height }] }),
7245
+ /* @__PURE__ */ jsx8(Pressable7, { accessible: false, onPress: press, style: [styles7.abs, shade, { left: hole.x + hole.width, right: 0, top: hole.y, height: hole.height }] }),
6128
7246
  r > 0 ? corners.map(([left, top, dx, dy], i) => /* @__PURE__ */ jsx8(View7, { pointerEvents: "none", style: [styles7.abs, { left, top, width: r, height: r, overflow: "hidden" }], children: /* @__PURE__ */ jsx8(View7, { style: [styles7.abs, { left: dx, top: dy, width: 4 * r, height: 4 * r, borderRadius: 2 * r, borderWidth: r, borderColor: p.overlay }] }) }, i)) : null,
6129
7247
  /* @__PURE__ */ jsx8(View7, { pointerEvents: "none", style: [styles7.abs, { left: hole.x, top: hole.y, width: hole.width, height: hole.height, borderRadius: r, borderWidth: 2, borderColor: p.primary }] }),
6130
7248
  /* @__PURE__ */ jsx8(Anchored, { view, client, palette: p, geometry, rect: hole, width: cardWidth })
@@ -6151,7 +7269,7 @@ function Arrow({ side, at, color, border }) {
6151
7269
  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 };
6152
7270
  return /* @__PURE__ */ jsx8(View7, { pointerEvents: "none", style: [styles7.abs, styles7.arrow, place, { backgroundColor: color, borderColor: border }] });
6153
7271
  }
6154
- function Hotspot({ x, y, size, pulse, color, onPress, label }) {
7272
+ function Hotspot({ x, y, size: size2, pulse, color, onPress, label }) {
6155
7273
  const scale = useRef3(new Animated2.Value(0)).current;
6156
7274
  const [reduceMotion, setReduceMotion] = useState5(false);
6157
7275
  useEffect5(() => {
@@ -6168,15 +7286,15 @@ function Hotspot({ x, y, size, pulse, color, onPress, label }) {
6168
7286
  loop.start();
6169
7287
  return () => loop.stop();
6170
7288
  }, [pulse, reduceMotion, scale]);
6171
- return /* @__PURE__ */ jsxs6(Pressable6, { accessibilityRole: "button", accessibilityLabel: label, onPress, hitSlop: 14, style: [styles7.abs, { left: x, top: y, width: size, height: size }], children: [
7289
+ return /* @__PURE__ */ jsxs6(Pressable7, { accessibilityRole: "button", accessibilityLabel: label, onPress, hitSlop: 14, style: [styles7.abs, { left: x, top: y, width: size2, height: size2 }], children: [
6172
7290
  pulse && !reduceMotion ? /* @__PURE__ */ jsx8(
6173
7291
  Animated2.View,
6174
7292
  {
6175
7293
  pointerEvents: "none",
6176
- 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] }) }] }]
7294
+ 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] }) }] }]
6177
7295
  }
6178
7296
  ) : null,
6179
- /* @__PURE__ */ jsx8(View7, { style: { width: size, height: size, borderRadius: size / 2, backgroundColor: color, borderWidth: 2, borderColor: "#ffffff" } })
7297
+ /* @__PURE__ */ jsx8(View7, { style: { width: size2, height: size2, borderRadius: size2 / 2, backgroundColor: color, borderWidth: 2, borderColor: "#ffffff" } })
6180
7298
  ] });
6181
7299
  }
6182
7300
  var styles7 = StyleSheet7.create({
@@ -6192,7 +7310,7 @@ import {
6192
7310
  AccessibilityInfo as AccessibilityInfo3,
6193
7311
  KeyboardAvoidingView,
6194
7312
  Platform as Platform4,
6195
- Pressable as Pressable7,
7313
+ Pressable as Pressable8,
6196
7314
  ScrollView as ScrollView5,
6197
7315
  StyleSheet as StyleSheet8,
6198
7316
  Text as Text7,
@@ -6250,7 +7368,7 @@ function Survey({
6250
7368
  style: StyleSheet8.absoluteFill,
6251
7369
  children: [
6252
7370
  centered ? /* @__PURE__ */ jsx9(
6253
- Pressable7,
7371
+ Pressable8,
6254
7372
  {
6255
7373
  accessible: false,
6256
7374
  style: [StyleSheet8.absoluteFill, { backgroundColor: p.overlay }],
@@ -6347,7 +7465,7 @@ function QuestionInput({
6347
7465
  const values = Array.from({ length: max - min + 1 }, (_, i) => min + i);
6348
7466
  return /* @__PURE__ */ jsxs7(View8, { children: [
6349
7467
  /* @__PURE__ */ jsx9(View8, { style: styles8.scale, accessibilityRole: "radiogroup", accessibilityLabel: q.title, children: values.map((n) => /* @__PURE__ */ jsx9(
6350
- Pressable7,
7468
+ Pressable8,
6351
7469
  {
6352
7470
  accessibilityRole: "radio",
6353
7471
  accessibilityState: { selected: value === n },
@@ -6367,7 +7485,7 @@ function QuestionInput({
6367
7485
  case "rating": {
6368
7486
  const max = q.scaleMax ?? 5;
6369
7487
  return /* @__PURE__ */ jsx9(View8, { style: styles8.stars, accessibilityRole: "radiogroup", accessibilityLabel: q.title, children: Array.from({ length: max }, (_, i) => i + 1).map((n) => /* @__PURE__ */ jsx9(
6370
- Pressable7,
7488
+ Pressable8,
6371
7489
  {
6372
7490
  accessibilityRole: "radio",
6373
7491
  accessibilityLabel: `${n} of ${max}`,
@@ -6393,7 +7511,7 @@ function QuestionInput({
6393
7511
  [true, "\u{1F44D}", "Thumbs up"],
6394
7512
  [false, "\u{1F44E}", "Thumbs down"]
6395
7513
  ].map(([val, icon, label]) => /* @__PURE__ */ jsx9(
6396
- Pressable7,
7514
+ Pressable8,
6397
7515
  {
6398
7516
  accessibilityRole: "radio",
6399
7517
  accessibilityLabel: label,
@@ -6413,7 +7531,7 @@ function QuestionInput({
6413
7531
  return /* @__PURE__ */ jsx9(View8, { style: styles8.choices, children: q.options.map((o) => {
6414
7532
  const on = selected.has(o.id);
6415
7533
  return /* @__PURE__ */ jsxs7(
6416
- Pressable7,
7534
+ Pressable8,
6417
7535
  {
6418
7536
  accessibilityRole: multi ? "checkbox" : "radio",
6419
7537
  accessibilityState: { checked: on },
@@ -6599,7 +7717,7 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
6599
7717
  return /* @__PURE__ */ jsxs8(View9, { ref, collapsable: false, pointerEvents: "box-none", style: StyleSheet9.absoluteFill, onLayout: measureOrigin, children: [
6600
7718
  ui.checklists.map((c) => /* @__PURE__ */ jsx10(Checklist, { view: c, client, palette: palette(client.themeFor(c.guide), scheme), geometry, showLauncher: client.options.showChecklistLauncher !== false, launcherBottom: launcherOffset }, c.guide.id)),
6601
7719
  showRcLauncher ? /* @__PURE__ */ jsxs8(
6602
- Pressable8,
7720
+ Pressable9,
6603
7721
  {
6604
7722
  accessibilityRole: "button",
6605
7723
  accessibilityLabel: rcLauncherLabel(client),
@@ -6624,7 +7742,7 @@ function GuidelyOverlay({ client, insets: insetsProp, launcherOffset }) {
6624
7742
  ) : null,
6625
7743
  ui.survey ? /* @__PURE__ */ jsx10(Survey, { view: ui.survey, client, palette: palette(client.themeFor(ui.survey.guide), scheme), geometry }) : null,
6626
7744
  ui.steps.map((s) => /* @__PURE__ */ jsx10(StepLayer, { view: s, client, palette: palette(client.themeFor(s.guide), scheme, s.guide.config.appearance), geometry }, s.key)),
6627
- messenger ? /* @__PURE__ */ jsx10(MessengerSheet, { store: messenger, client, palette: basePalette, topInset: insets.top, bottomInset: insets.bottom }) : /* @__PURE__ */ jsx10(ResourceCenter, { client, palette: basePalette, open: ui.resourceCenterOpen, topInset: insets.top, bottomInset: insets.bottom })
7745
+ messenger ? /* @__PURE__ */ jsx10(MessengerSheet, { store: messenger, client, palette: basePalette, topInset: insets.top, bottomInset: insets.bottom }) : /* @__PURE__ */ jsx10(ResourceCenter, { client, palette: basePalette, open: ui.resourceCenterOpen, articleRequest: ui.resourceCenterArticle ?? null, topInset: insets.top, bottomInset: insets.bottom })
6628
7746
  ] });
6629
7747
  }
6630
7748
  var styles9 = StyleSheet9.create({
@@ -6797,12 +7915,16 @@ function defaultBridge(appVersion, appBuild) {
6797
7915
  locale: deviceLocale(),
6798
7916
  appVersion: appVersion ?? detected.appVersion,
6799
7917
  appBuild: appBuild ?? detected.appBuild,
6800
- osVersion: detectOsVersion(sources)
7918
+ osVersion: detectOsVersion(sources),
7919
+ ...detectDeviceModel(sources),
7920
+ reactNativeVersion: detectReactNativeVersion(sources)
6801
7921
  },
6802
7922
  openExternal: (url) => void Linking.openURL(url).catch(() => {
6803
7923
  }),
6804
7924
  windowSize: () => Dimensions.get("window"),
6805
- capturer: nativeCapturer()
7925
+ capturer: nativeCapturer(),
7926
+ // ErrorUtils, HermesInternal, console, fetch, XMLHttpRequest: crash capture and breadcrumbs.
7927
+ globals: globalThis
6806
7928
  };
6807
7929
  }
6808
7930
  function nativeCapturer() {
@@ -6810,6 +7932,20 @@ function nativeCapturer() {
6810
7932
  if (!mod || typeof mod.capture !== "function") return void 0;
6811
7933
  return (options) => mod.capture(options);
6812
7934
  }
7935
+ function latestErrorOptions(initial, latest) {
7936
+ if (!initial) return void 0;
7937
+ return {
7938
+ ...initial,
7939
+ beforeSend: (event) => {
7940
+ const hook = latest()?.beforeSend;
7941
+ return hook ? hook(event) : event;
7942
+ },
7943
+ beforeBreadcrumb: (crumb, hint) => {
7944
+ const hook = latest()?.beforeBreadcrumb;
7945
+ return hook ? hook(crumb, hint) : crumb;
7946
+ }
7947
+ };
7948
+ }
6813
7949
  function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPayload, flagAttributes, ...options }) {
6814
7950
  const [client, setClient] = useState8(null);
6815
7951
  const optionsRef = useRef7(options);
@@ -6840,7 +7976,8 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
6840
7976
  user: userRef.current?.id ? userRef.current : void 0,
6841
7977
  flags: latest.flags === false ? false : { ...latest.flags ?? {}, payload: payloadRef.current ?? void 0, attributes: appAttributesRef.current },
6842
7978
  onNavigate: (url) => optionsRef.current.onNavigate?.(url),
6843
- onError: (err) => optionsRef.current.onError?.(err)
7979
+ onError: (err) => optionsRef.current.onError?.(err),
7980
+ errors: latestErrorOptions(latest.errors, () => optionsRef.current.errors)
6844
7981
  },
6845
7982
  defaultBridge(latest.appVersion, latest.appBuild)
6846
7983
  );
@@ -6903,6 +8040,8 @@ var NOOP_RESULTS = {
6903
8040
  isRecordingTouches: false,
6904
8041
  getSessionId: "",
6905
8042
  getScreen: "",
8043
+ captureException: "",
8044
+ captureMessage: "",
6906
8045
  on: () => {
6907
8046
  },
6908
8047
  subscribeUi: () => () => {
@@ -7021,6 +8160,7 @@ function useActiveGuide() {
7021
8160
  }
7022
8161
  export {
7023
8162
  DEFAULT_API_URL,
8163
+ FATAL_FLUSH_MS,
7024
8164
  Feature,
7025
8165
  FeatureFlag,
7026
8166
  FeatureValue,
@@ -7045,11 +8185,15 @@ export {
7045
8185
  createFlagsClient3 as createFlagsClient,
7046
8186
  createSseParser,
7047
8187
  detectAppVersion,
8188
+ detectDeviceModel,
7048
8189
  detectOsVersion,
8190
+ detectReactNativeVersion,
7049
8191
  guidelyScreenLayout,
7050
8192
  htmlToText,
7051
8193
  noopClient,
8194
+ normalizeBundleFile,
7052
8195
  parseHtml,
8196
+ parseReactNativeStack,
7053
8197
  positionCard,
7054
8198
  targetIdsFor,
7055
8199
  useActiveGuide,