@guidelytech/react-native 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -23,6 +23,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
23
23
  var index_exports = {};
24
24
  __export(index_exports, {
25
25
  DEFAULT_API_URL: () => DEFAULT_API_URL,
26
+ FATAL_FLUSH_MS: () => FATAL_FLUSH_MS,
26
27
  Feature: () => Feature,
27
28
  FeatureFlag: () => FeatureFlag,
28
29
  FeatureValue: () => FeatureValue,
@@ -47,11 +48,15 @@ __export(index_exports, {
47
48
  createFlagsClient: () => import_flags6.createFlagsClient,
48
49
  createSseParser: () => createSseParser,
49
50
  detectAppVersion: () => detectAppVersion,
51
+ detectDeviceModel: () => detectDeviceModel,
50
52
  detectOsVersion: () => detectOsVersion,
53
+ detectReactNativeVersion: () => detectReactNativeVersion,
51
54
  guidelyScreenLayout: () => guidelyScreenLayout,
52
55
  htmlToText: () => htmlToText,
53
56
  noopClient: () => noopClient,
57
+ normalizeBundleFile: () => normalizeBundleFile,
54
58
  parseHtml: () => parseHtml,
59
+ parseReactNativeStack: () => parseReactNativeStack,
55
60
  positionCard: () => positionCard,
56
61
  targetIdsFor: () => targetIdsFor,
57
62
  useActiveGuide: () => useActiveGuide,
@@ -165,7 +170,7 @@ function FlagsProvider({ children, client: external, payload, ...options }) {
165
170
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FlagsContext.Provider, { value, children });
166
171
  }
167
172
 
168
- // ../shared/dist/chunk-7PWYIAJI.js
173
+ // ../shared/dist/chunk-YL65O3FS.js
169
174
  function fnv1a(input) {
170
175
  let h = 2166136261;
171
176
  for (let i = 0; i < input.length; i++) {
@@ -178,18 +183,18 @@ function rolloutBucket(subject, salt) {
178
183
  return fnv1a(`${salt}:${subject}`) % 100;
179
184
  }
180
185
  var ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
181
- function randomId(size = 16) {
186
+ function randomId(size2 = 16) {
182
187
  const g = typeof globalThis !== "undefined" ? globalThis : {};
183
188
  let bytes;
184
189
  if (g.crypto && typeof g.crypto.getRandomValues === "function") {
185
- bytes = new Uint8Array(size);
190
+ bytes = new Uint8Array(size2);
186
191
  g.crypto.getRandomValues(bytes);
187
192
  } else {
188
- bytes = new Uint8Array(size);
189
- for (let i = 0; i < size; i++) bytes[i] = Math.floor(Math.random() * 256);
193
+ bytes = new Uint8Array(size2);
194
+ for (let i = 0; i < size2; i++) bytes[i] = Math.floor(Math.random() * 256);
190
195
  }
191
196
  let out = "";
192
- for (let i = 0; i < size; i++) out += ALPHABET[bytes[i] % ALPHABET.length];
197
+ for (let i = 0; i < size2; i++) out += ALPHABET[bytes[i] % ALPHABET.length];
193
198
  return out;
194
199
  }
195
200
  function getPath(obj, path) {
@@ -459,24 +464,92 @@ function evaluateRules(group, ctx) {
459
464
  const value = group.op === "and" ? results.every(Boolean) : results.some(Boolean);
460
465
  return group.negate ? !value : value;
461
466
  }
467
+ var MAX_EXCEPTION_DETAIL_BYTES = 64 * 1024;
468
+ var MAX_EXCEPTION_DETAIL_RAW_BYTES = 1024 * 1024;
469
+ var MAX_BREADCRUMBS = 100;
470
+ var MAX_EXCEPTION_CHAIN = 20;
471
+ var MAX_DETAIL_FRAMES = 250;
472
+ var MAX_DETAIL_THREADS = 100;
473
+ var MAX_DEBUG_IMAGES = 500;
474
+ var MAX_ERROR_TAGS = 50;
475
+ var size = (v) => {
476
+ try {
477
+ return JSON.stringify(v)?.length ?? 0;
478
+ } catch {
479
+ return Infinity;
480
+ }
481
+ };
482
+ function normalizeTags(tags) {
483
+ const out = {};
484
+ for (const [k, v] of Object.entries(tags ?? {})) {
485
+ if (Object.keys(out).length >= MAX_ERROR_TAGS) break;
486
+ if (!k || v === void 0 || v === null) continue;
487
+ out[k.slice(0, 32)] = String(v).slice(0, 200);
488
+ }
489
+ return out;
490
+ }
491
+ function trimExceptionDetail(detail, max = MAX_EXCEPTION_DETAIL_BYTES) {
492
+ const d = detail;
493
+ if (d.breadcrumbs?.values && d.breadcrumbs.values.length > MAX_BREADCRUMBS) d.breadcrumbs.values = d.breadcrumbs.values.slice(-MAX_BREADCRUMBS);
494
+ if (d.exception?.values && d.exception.values.length > MAX_EXCEPTION_CHAIN) d.exception.values = d.exception.values.slice(-MAX_EXCEPTION_CHAIN);
495
+ if (d.threads?.values && d.threads.values.length > MAX_DETAIL_THREADS) d.threads.values = d.threads.values.slice(0, MAX_DETAIL_THREADS);
496
+ if (d.debug_meta?.images && d.debug_meta.images.length > MAX_DEBUG_IMAGES) d.debug_meta.images = d.debug_meta.images.slice(0, MAX_DEBUG_IMAGES);
497
+ const stacks = () => [...d.exception?.values ?? [], ...d.threads?.values ?? []].map((x) => x.stacktrace).filter((s) => !!s);
498
+ for (const s of stacks()) if (s.frames && s.frames.length > MAX_DETAIL_FRAMES) s.frames = s.frames.slice(-MAX_DETAIL_FRAMES);
499
+ if (size(d) <= max) return d;
500
+ while (d.breadcrumbs?.values.length && size(d) > max) d.breadcrumbs.values = d.breadcrumbs.values.slice(Math.ceil(d.breadcrumbs.values.length / 2));
501
+ if (size(d) <= max) return d;
502
+ for (const s of stacks())
503
+ for (const f of s.frames ?? []) {
504
+ delete f.vars;
505
+ delete f.pre_context;
506
+ delete f.post_context;
507
+ }
508
+ if (size(d) <= max) return d;
509
+ if (d.threads?.values) d.threads.values = d.threads.values.filter((t) => t.crashed || t.current);
510
+ if (size(d) <= max) return d;
511
+ if (d.extra) d.extra = { _trimmed: true };
512
+ if (d.request) delete d.request.data;
513
+ for (const s of stacks()) while (s.frames && s.frames.length > 20 && size(d) > max) s.frames.splice(Math.floor(s.frames.length / 2) - 5, 10);
514
+ if (d.debug_meta?.images && size(d) > max) d.debug_meta.images = d.debug_meta.images.slice(0, 50);
515
+ return d;
516
+ }
517
+ var matches = (patterns, value) => patterns.some((p) => typeof p === "string" ? p !== "" && value.includes(p) : safeTest(p, value));
518
+ function safeTest(re, value) {
519
+ re.lastIndex = 0;
520
+ return re.test(value);
521
+ }
522
+ function errorFilterDrops(e, opts) {
523
+ const message = e.message ?? "";
524
+ if (opts.ignoreErrors?.length && (matches(opts.ignoreErrors, `${e.type || "Error"}: ${message}`) || matches(opts.ignoreErrors, message))) return true;
525
+ if (!e.file) return false;
526
+ if (opts.denyUrls?.length && matches(opts.denyUrls, e.file)) return true;
527
+ if (opts.allowUrls?.length && !matches(opts.allowUrls, e.file)) return true;
528
+ return false;
529
+ }
462
530
  var SENSITIVE_PROPERTY_PATTERN = /(password|passwd|secret|token|credit|card|cvv|ssn|iban)/i;
463
531
  var MAX_PROPERTY_BYTES = 8 * 1024;
464
532
  function sanitizeProperties(props) {
465
533
  if (!props) return {};
466
534
  const out = {};
467
- let size = 0;
535
+ let size2 = 0;
468
536
  for (const [k, v] of Object.entries(props)) {
469
537
  if (SENSITIVE_PROPERTY_PATTERN.test(k)) continue;
470
538
  if (typeof v === "function" || typeof v === "symbol") continue;
471
539
  const str2 = typeof v === "string" ? v : JSON.stringify(v ?? null);
472
- size += k.length + (str2?.length ?? 0);
473
- if (size > MAX_PROPERTY_BYTES) break;
540
+ size2 += k.length + (str2?.length ?? 0);
541
+ if (size2 > MAX_PROPERTY_BYTES) break;
474
542
  out[k.slice(0, 100)] = typeof v === "string" ? v.slice(0, 1e3) : v;
475
543
  }
476
544
  return out;
477
545
  }
546
+ function reactNativeBundleUrl(file) {
547
+ const base = file.replace(/[?#].*$/, "").split("/").pop() || file;
548
+ return `app:///${base}`;
549
+ }
478
550
  var SOURCEMAP_MAX_FILE_BYTES = 100 * 1024 * 1024;
479
551
  var MAX_COMPLAINT_ATTACHMENT_BYTES = 2 * 1024 * 1024;
552
+ var MAX_BUG_REPORT_SCREENSHOT_BYTES = 4 * 1024 * 1024;
480
553
  var MEDIA_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
481
554
  var MEDIA_VIDEO_MAX_BYTES = 200 * 1024 * 1024;
482
555
  var MEDIA_FILE_MAX_BYTES = 50 * 1024 * 1024;
@@ -1101,7 +1174,22 @@ var KEYS = {
1101
1174
  launcher: (key) => `guidely:launcher:${key}`
1102
1175
  };
1103
1176
 
1104
- // ../sdk-core/dist/chunk-2E2YUQAG.js
1177
+ // ../sdk-core/dist/chunk-GFLVZRTS.js
1178
+ var MAX_PERSISTED_QUEUE_CHARS = 1024 * 1024;
1179
+ function fitPersisted(events) {
1180
+ let size2 = JSON.stringify(events).length;
1181
+ if (size2 <= MAX_PERSISTED_QUEUE_CHARS) return events;
1182
+ const out = events.slice();
1183
+ for (let i = 0; i < out.length && size2 > MAX_PERSISTED_QUEUE_CHARS; i++) {
1184
+ const e = out[i];
1185
+ if (!e.detail) continue;
1186
+ size2 -= JSON.stringify(e.detail).length;
1187
+ const rest = { ...e };
1188
+ delete rest.detail;
1189
+ out[i] = rest;
1190
+ }
1191
+ return out;
1192
+ }
1105
1193
  var EventQueue = class {
1106
1194
  constructor(opts) {
1107
1195
  __publicField2(this, "opts", opts);
@@ -1157,7 +1245,7 @@ var EventQueue = class {
1157
1245
  this.schedule();
1158
1246
  }
1159
1247
  persist() {
1160
- writeJson(this.opts.store, this.opts.storageKey, this.events);
1248
+ writeJson(this.opts.store, this.opts.storageKey, fitPersisted(this.events));
1161
1249
  }
1162
1250
  schedule() {
1163
1251
  if (this.timer || this.retryTimer || this.destroyed) return;
@@ -1407,6 +1495,13 @@ function dismissedAfterMerge(local, server) {
1407
1495
  if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
1408
1496
  return local?.dismissedAt ?? server.dismissedAt ?? null;
1409
1497
  }
1498
+ function environmentFromKey(key) {
1499
+ const prefix = /^pk_([a-z0-9]+)_/.exec(key)?.[1];
1500
+ if (!prefix || prefix === "dev") return "development";
1501
+ if (prefix === "prod") return "production";
1502
+ if (prefix === "stg" || prefix === "stag") return "staging";
1503
+ return prefix;
1504
+ }
1410
1505
 
1411
1506
  // src/core/client.ts
1412
1507
  var import_flags2 = require("@guidelytech/flags");
@@ -1563,6 +1658,593 @@ var TapTracker = class {
1563
1658
  }
1564
1659
  };
1565
1660
 
1661
+ // src/core/breadcrumbs.ts
1662
+ var BreadcrumbBuffer = class {
1663
+ constructor(max = MAX_BREADCRUMBS) {
1664
+ __publicField(this, "max", max);
1665
+ __publicField(this, "items", []);
1666
+ }
1667
+ /** Adds a breadcrumb; `max` (at most the buffer's own) lowers how many are kept. */
1668
+ add(crumb, max = this.max) {
1669
+ const limit = Math.min(max, this.max);
1670
+ this.items.push(crumb);
1671
+ if (this.items.length > limit) this.items.splice(0, this.items.length - limit);
1672
+ }
1673
+ /** Copies, oldest first (an event keeps the crumbs it was captured with). */
1674
+ values() {
1675
+ return this.items.map((c) => ({ ...c, ...c.data ? { data: { ...c.data } } : {} }));
1676
+ }
1677
+ get size() {
1678
+ return this.items.length;
1679
+ }
1680
+ clear() {
1681
+ this.items = [];
1682
+ }
1683
+ };
1684
+ function stripUrl(url) {
1685
+ return String(url).replace(/[?#].*$/, "").replace(/^([a-z][\w+.-]*:\/\/)[^/@]*@/i, "$1").slice(0, 500);
1686
+ }
1687
+ function describeArg(value) {
1688
+ if (typeof value === "string") return value.slice(0, 500);
1689
+ if (value instanceof Error) return `${value.name}: ${value.message}`.slice(0, 500);
1690
+ try {
1691
+ return (JSON.stringify(value) ?? String(value)).slice(0, 500);
1692
+ } catch {
1693
+ return String(value).slice(0, 500);
1694
+ }
1695
+ }
1696
+ function instrumentConsole(target, emit) {
1697
+ if (!target) return () => {
1698
+ };
1699
+ const undo = [];
1700
+ let inside = false;
1701
+ for (const [method, level] of [
1702
+ ["warn", "warning"],
1703
+ ["error", "error"]
1704
+ ]) {
1705
+ const original = target[method];
1706
+ if (typeof original !== "function") continue;
1707
+ const wrapped = function(...args) {
1708
+ const result = original.apply(this, args);
1709
+ if (!inside && !(typeof args[0] === "string" && args[0].startsWith("[Guidely]"))) {
1710
+ inside = true;
1711
+ try {
1712
+ const parts = args.slice(0, 5).map(describeArg);
1713
+ emit({ type: "debug", category: "console", level, message: parts.join(" ").slice(0, 1e3), data: { arguments: parts, logger: "console" } });
1714
+ } catch {
1715
+ } finally {
1716
+ inside = false;
1717
+ }
1718
+ }
1719
+ return result;
1720
+ };
1721
+ target[method] = wrapped;
1722
+ undo.push(() => {
1723
+ if (target[method] === wrapped) target[method] = original;
1724
+ });
1725
+ }
1726
+ return () => undo.forEach((fn) => fn());
1727
+ }
1728
+ function httpLevel(status) {
1729
+ if (!status) return "error";
1730
+ return status >= 500 ? "error" : status >= 400 ? "warning" : "info";
1731
+ }
1732
+ var XHR_INFO = "__guidelyXhr";
1733
+ function instrumentHttp(g, emit, skip, now = Date.now) {
1734
+ if (!g) return () => {
1735
+ };
1736
+ const undo = [];
1737
+ let inFetch = 0;
1738
+ const record2 = (category, method, url, start, status) => {
1739
+ try {
1740
+ const data = { method, url: stripUrl(url), ...status ? { status_code: status } : {}, duration_ms: Math.max(0, Math.round(now() - start)) };
1741
+ emit({ type: "http", category, level: httpLevel(status), data });
1742
+ } catch {
1743
+ }
1744
+ };
1745
+ const originalFetch = g.fetch;
1746
+ if (typeof originalFetch === "function") {
1747
+ const wrappedFetch = function(...args) {
1748
+ const [input, init] = args;
1749
+ let url = "";
1750
+ let method = "GET";
1751
+ try {
1752
+ const req = input;
1753
+ url = typeof req === "string" ? req : typeof req?.url === "string" ? req.url : String(input);
1754
+ method = String(init?.method ?? (req && typeof req === "object" ? req.method : void 0) ?? "GET").toUpperCase();
1755
+ } catch {
1756
+ }
1757
+ if (skip(url)) return originalFetch.apply(this, args);
1758
+ const start = now();
1759
+ let result;
1760
+ inFetch += 1;
1761
+ try {
1762
+ result = originalFetch.apply(this, args);
1763
+ } finally {
1764
+ inFetch -= 1;
1765
+ }
1766
+ if (!result || typeof result.then !== "function") return result;
1767
+ return result.then(
1768
+ (res) => {
1769
+ record2("fetch", method, url, start, typeof res?.status === "number" ? res.status : void 0);
1770
+ return res;
1771
+ },
1772
+ (err) => {
1773
+ record2("fetch", method, url, start, void 0);
1774
+ throw err;
1775
+ }
1776
+ );
1777
+ };
1778
+ g.fetch = wrappedFetch;
1779
+ undo.push(() => {
1780
+ if (g.fetch === wrappedFetch) g.fetch = originalFetch;
1781
+ });
1782
+ }
1783
+ const proto = g.XMLHttpRequest?.prototype;
1784
+ const originalOpen = proto?.open;
1785
+ const originalSend = proto?.send;
1786
+ if (proto && typeof originalOpen === "function" && typeof originalSend === "function") {
1787
+ const open = function(...args) {
1788
+ try {
1789
+ this[XHR_INFO] = { method: String(args[0] ?? "GET").toUpperCase(), url: String(args[1] ?? ""), viaFetch: inFetch > 0 };
1790
+ } catch {
1791
+ }
1792
+ return originalOpen.apply(this, args);
1793
+ };
1794
+ const send = function(...args) {
1795
+ try {
1796
+ const info = this[XHR_INFO];
1797
+ if (info && !info.viaFetch && !skip(info.url) && typeof this.addEventListener === "function") {
1798
+ info.start = now();
1799
+ this.addEventListener("readystatechange", () => {
1800
+ if (this.readyState !== 4 || info.done) return;
1801
+ info.done = true;
1802
+ record2("xhr", info.method, info.url, info.start ?? now(), typeof this.status === "number" && this.status > 0 ? this.status : void 0);
1803
+ });
1804
+ }
1805
+ } catch {
1806
+ }
1807
+ return originalSend.apply(this, args);
1808
+ };
1809
+ proto.open = open;
1810
+ proto.send = send;
1811
+ undo.push(() => {
1812
+ if (proto.open === open) proto.open = originalOpen;
1813
+ if (proto.send === send) proto.send = originalSend;
1814
+ });
1815
+ }
1816
+ return () => undo.forEach((fn) => fn());
1817
+ }
1818
+
1819
+ // src/core/crashHandlers.ts
1820
+ var FATAL_FLUSH_MS = 2e3;
1821
+ var isThenable = (v) => !!v && typeof v.then === "function";
1822
+ function installGlobalHandler(g, report, waitMs = FATAL_FLUSH_MS) {
1823
+ const eu = g.ErrorUtils;
1824
+ if (!eu || typeof eu.setGlobalHandler !== "function") return () => {
1825
+ };
1826
+ let previous;
1827
+ try {
1828
+ previous = eu.getGlobalHandler?.();
1829
+ } catch {
1830
+ previous = void 0;
1831
+ }
1832
+ let active = true;
1833
+ let handlingFatal = false;
1834
+ const handler = (error, isFatal) => {
1835
+ const fatal = !!isFatal;
1836
+ const callPrevious = () => previous?.(error, isFatal);
1837
+ if (!active || fatal && handlingFatal) return callPrevious();
1838
+ let pending;
1839
+ try {
1840
+ pending = report(error, fatal);
1841
+ } catch {
1842
+ }
1843
+ if (!fatal || g.__DEV__ || !isThenable(pending)) return callPrevious();
1844
+ handlingFatal = true;
1845
+ let done = false;
1846
+ const finish = () => {
1847
+ if (done) return;
1848
+ done = true;
1849
+ clearTimeout(timer);
1850
+ handlingFatal = false;
1851
+ try {
1852
+ callPrevious();
1853
+ } catch {
1854
+ }
1855
+ };
1856
+ const timer = setTimeout(finish, waitMs);
1857
+ pending.then(finish, finish);
1858
+ };
1859
+ try {
1860
+ eu.setGlobalHandler(handler);
1861
+ } catch {
1862
+ return () => {
1863
+ };
1864
+ }
1865
+ return () => {
1866
+ active = false;
1867
+ try {
1868
+ if (previous && eu.getGlobalHandler?.() === handler) eu.setGlobalHandler(previous);
1869
+ } catch {
1870
+ }
1871
+ };
1872
+ }
1873
+ function describeRejection(rejection) {
1874
+ if (rejection instanceof Error) return rejection.stack || `${rejection.name}: ${rejection.message}`;
1875
+ try {
1876
+ return typeof rejection === "string" ? rejection : JSON.stringify(rejection) ?? String(rejection);
1877
+ } catch {
1878
+ return String(rejection);
1879
+ }
1880
+ }
1881
+ function hermesTracker(g) {
1882
+ const h = g.HermesInternal;
1883
+ try {
1884
+ if (!h || typeof h.enablePromiseRejectionTracker !== "function" || typeof h.hasPromise === "function" && !h.hasPromise()) return null;
1885
+ } catch {
1886
+ return null;
1887
+ }
1888
+ return { enable: (options) => h.enablePromiseRejectionTracker(options) };
1889
+ }
1890
+ function installRejectionTracking(g, report, tracker) {
1891
+ let active = true;
1892
+ const warn = (message) => {
1893
+ try {
1894
+ g.console?.warn?.(message);
1895
+ } catch {
1896
+ }
1897
+ };
1898
+ const options = {
1899
+ allRejections: true,
1900
+ onUnhandled: (id, rejection) => {
1901
+ if (active) {
1902
+ try {
1903
+ report(rejection);
1904
+ } catch {
1905
+ }
1906
+ }
1907
+ if (g.__DEV__) warn(`Possible unhandled promise rejection (id: ${id}):
1908
+ ${describeRejection(rejection)}`);
1909
+ },
1910
+ onHandled: (id) => {
1911
+ if (g.__DEV__) warn(`Promise rejection handled (id: ${id})
1912
+ This means you can ignore any previous messages of the form "Possible unhandled promise rejection (id: ${id}):"`);
1913
+ }
1914
+ };
1915
+ const t = tracker ?? hermesTracker(g);
1916
+ if (t && typeof t.enable === "function") {
1917
+ try {
1918
+ t.enable(options);
1919
+ return () => {
1920
+ active = false;
1921
+ };
1922
+ } catch {
1923
+ }
1924
+ }
1925
+ if (typeof g.addEventListener === "function") {
1926
+ const listener = (event) => {
1927
+ if (active) report(event?.reason);
1928
+ };
1929
+ try {
1930
+ g.addEventListener("unhandledrejection", listener);
1931
+ return () => {
1932
+ active = false;
1933
+ g.removeEventListener?.("unhandledrejection", listener);
1934
+ };
1935
+ } catch {
1936
+ }
1937
+ }
1938
+ return () => {
1939
+ active = false;
1940
+ };
1941
+ }
1942
+ function runtimeContext(g) {
1943
+ const h = g.HermesInternal;
1944
+ if (!h) return { name: "jsc" };
1945
+ try {
1946
+ const version = h.getRuntimeProperties?.()?.["OSS Release Version"];
1947
+ return { name: "hermes", ...typeof version === "string" && version ? { version: version.slice(0, 50) } : {} };
1948
+ } catch {
1949
+ return { name: "hermes" };
1950
+ }
1951
+ }
1952
+
1953
+ // src/core/stack.ts
1954
+ var AT = /^\s*at (?:(.+?) \()?(?:address at )?(.+?):(\d+):(\d+)\)?\s*$/;
1955
+ var AT_NATIVE = /^\s*at (?:(.+?) \()?(?:native|\[native code\])\)?\s*$/;
1956
+ var JSC = /^\s*([^@]*)@(.+?):(\d+):(\d+)\s*$/;
1957
+ var JSC_BARE = /^\s*(\S+?):(\d+):(\d+)\s*$/;
1958
+ var JSC_NATIVE = /^\s*(?:([^@]*)@)?\[native code\]\s*$/;
1959
+ var BUNDLE = /\.(jsbundle|bundle|hbc)$/i;
1960
+ function normalizeBundleFile(file) {
1961
+ const f = file.trim();
1962
+ if (/^https?:\/\//i.test(f)) return f.replace(/[?#].*$/, "");
1963
+ const path = f.replace(/^file:\/\//i, "").replace(/[?#].*$/, "");
1964
+ const base = path.slice(path.lastIndexOf("/") + 1);
1965
+ return BUNDLE.test(base) ? reactNativeBundleUrl(path) : path;
1966
+ }
1967
+ function inAppFor(file) {
1968
+ if (!file) return false;
1969
+ if (/(^|\/)node_modules\//.test(file) || /(^|\/)InternalBytecode\.js$/.test(file)) return false;
1970
+ if (file.startsWith("app:///") || /^https?:\/\//i.test(file)) return void 0;
1971
+ return true;
1972
+ }
1973
+ function frame(fn, file, line, col) {
1974
+ const name = (fn || "").trim();
1975
+ const normalized = file ? normalizeBundleFile(file).slice(0, 500) : void 0;
1976
+ const inApp = inAppFor(normalized);
1977
+ return {
1978
+ ...name ? { fn: name.slice(0, 200) } : {},
1979
+ ...normalized ? { file: normalized } : {},
1980
+ ...line ? { line: Number(line) } : {},
1981
+ ...col ? { col: Number(col) } : {},
1982
+ ...inApp !== void 0 ? { inApp } : {}
1983
+ };
1984
+ }
1985
+ function parseReactNativeStack(stack, max = MAX_DETAIL_FRAMES) {
1986
+ if (!stack || typeof stack !== "string") return [];
1987
+ const frames = [];
1988
+ for (const raw of stack.split("\n")) {
1989
+ if (frames.length >= max) break;
1990
+ const line = raw.slice(0, 1e3);
1991
+ let m;
1992
+ if (m = AT_NATIVE.exec(line)) frames.push(frame(m[1], void 0));
1993
+ else if (m = AT.exec(line)) frames.push(frame(m[1], m[2], m[3], m[4]));
1994
+ else if (m = JSC_NATIVE.exec(line)) frames.push(frame(m[1], void 0));
1995
+ else if (m = JSC.exec(line)) frames.push(frame(m[1], m[2], m[3], m[4]));
1996
+ else if (m = JSC_BARE.exec(line)) frames.push(frame(void 0, m[1], m[2], m[3]));
1997
+ }
1998
+ return frames;
1999
+ }
2000
+ function toDetailFrames(frames) {
2001
+ return frames.slice().reverse().map((f) => ({
2002
+ ...f.fn ? { function: f.fn } : {},
2003
+ ...f.file ? { filename: f.file, abs_path: f.file } : {},
2004
+ ...f.line !== void 0 ? { lineno: f.line } : {},
2005
+ ...f.col !== void 0 ? { colno: f.col } : {},
2006
+ ...f.inApp !== void 0 ? { in_app: f.inApp } : {}
2007
+ }));
2008
+ }
2009
+ var MAX_FRAMES_BYTES = 6e3;
2010
+ function fitPropertyFrames(frames) {
2011
+ const out = frames.slice(0, 30);
2012
+ while (out.length > 1 && JSON.stringify(out).length > MAX_FRAMES_BYTES) out.pop();
2013
+ return out;
2014
+ }
2015
+
2016
+ // src/core/errors.ts
2017
+ var CONTEXT_KEYS = ["tags", "extra", "level", "fingerprint", "contexts", "user"];
2018
+ function splitHint(hint) {
2019
+ const ctx = {};
2020
+ const properties = {};
2021
+ for (const [k, v] of Object.entries(hint && typeof hint === "object" ? hint : {})) (CONTEXT_KEYS.includes(k) ? ctx : properties)[k] = v;
2022
+ return { ctx, properties };
2023
+ }
2024
+ var nowSeconds = () => Date.now() / 1e3;
2025
+ var ErrorScope = class {
2026
+ constructor(opts) {
2027
+ __publicField(this, "opts", opts);
2028
+ __publicField(this, "tags", {});
2029
+ __publicField(this, "extra", {});
2030
+ __publicField(this, "contexts", {});
2031
+ __publicField(this, "user", {});
2032
+ __publicField(this, "crumbs", new BreadcrumbBuffer(MAX_BREADCRUMBS));
2033
+ }
2034
+ get max() {
2035
+ return Math.max(0, Math.min(MAX_BREADCRUMBS, this.opts()?.maxBreadcrumbs ?? MAX_BREADCRUMBS));
2036
+ }
2037
+ addBreadcrumb(crumb, hint) {
2038
+ if (!crumb || typeof crumb !== "object") return;
2039
+ let c = { timestamp: nowSeconds(), ...crumb };
2040
+ const before = this.opts()?.beforeBreadcrumb;
2041
+ if (before) {
2042
+ try {
2043
+ c = before(c, hint);
2044
+ } catch {
2045
+ }
2046
+ }
2047
+ const max = this.max;
2048
+ if (!c || !max) return;
2049
+ this.crumbs.add(c, max);
2050
+ }
2051
+ breadcrumbs() {
2052
+ return this.crumbs.values();
2053
+ }
2054
+ clear() {
2055
+ this.tags = {};
2056
+ this.extra = {};
2057
+ this.contexts = {};
2058
+ this.user = {};
2059
+ this.crumbs.clear();
2060
+ }
2061
+ };
2062
+ var isError = (v) => v instanceof Error || !!v && typeof v === "object" && typeof v.message === "string" && typeof v.stack === "string";
2063
+ function describe(value) {
2064
+ if (typeof value === "string") return value;
2065
+ try {
2066
+ return JSON.stringify(value) ?? String(value);
2067
+ } catch {
2068
+ return String(value);
2069
+ }
2070
+ }
2071
+ function exceptionFromValue(error, source, handled, fallbackType = "Error") {
2072
+ const e = error;
2073
+ const ok = isError(error);
2074
+ return {
2075
+ type: (ok && e.name || fallbackType).slice(0, 200),
2076
+ message: (ok ? e.message : describe(error)).slice(0, 1e3),
2077
+ frames: ok ? fitPropertyFrames(parseReactNativeStack(e.stack)) : [],
2078
+ source,
2079
+ handled
2080
+ };
2081
+ }
2082
+ function exceptionChain(error, main, mechanism) {
2083
+ const mainFrames = isError(error) ? parseReactNativeStack(error.stack) : main.frames;
2084
+ const chain = [{ type: main.type, value: main.message, mechanism: { type: mechanism, handled: main.handled !== false, exception_id: 0 }, ...mainFrames.length ? { stacktrace: { frames: toDetailFrames(mainFrames) } } : {} }];
2085
+ const seen = /* @__PURE__ */ new Set([error]);
2086
+ let cur = isError(error) ? error.cause : void 0;
2087
+ while (cur !== void 0 && cur !== null && !seen.has(cur) && chain.length < MAX_EXCEPTION_CHAIN) {
2088
+ seen.add(cur);
2089
+ const e = cur;
2090
+ const id = chain.length;
2091
+ const frames = isError(e) ? parseReactNativeStack(e.stack, 50) : [];
2092
+ chain.push({
2093
+ type: (isError(e) && e.name || "Error").slice(0, 200),
2094
+ value: (isError(e) ? e.message : describe(cur)).slice(0, 1e3),
2095
+ mechanism: { type: "chained", source: "cause", handled: true, exception_id: id, parent_id: id - 1 },
2096
+ ...frames.length ? { stacktrace: { frames: toDetailFrames(frames) } } : {}
2097
+ });
2098
+ cur = isError(e) ? e.cause : void 0;
2099
+ }
2100
+ return chain.reverse();
2101
+ }
2102
+ var SENSITIVE_KEY = new RegExp(`${SENSITIVE_PROPERTY_PATTERN.source}|authorization|cookie|api[-_]?key`, "i");
2103
+ function normalizeValue(value, depth = 5, seen = /* @__PURE__ */ new Set()) {
2104
+ if (value === null || typeof value === "boolean") return value;
2105
+ if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
2106
+ if (typeof value === "string") return value.slice(0, 5e3);
2107
+ if (typeof value === "bigint") return value.toString();
2108
+ if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") return void 0;
2109
+ if (value instanceof Date) return Number.isFinite(value.getTime()) ? value.toISOString() : String(value);
2110
+ if (isError(value)) return { name: value.name, message: value.message.slice(0, 1e3) };
2111
+ if (typeof value !== "object" || seen.has(value)) return "[Circular]";
2112
+ if (depth <= 0) return Array.isArray(value) ? "[Array]" : "[Object]";
2113
+ seen.add(value);
2114
+ try {
2115
+ if (Array.isArray(value)) return value.slice(0, MAX_DETAIL_FRAMES).map((v) => normalizeValue(v, depth - 1, seen) ?? null);
2116
+ const out = {};
2117
+ let n = 0;
2118
+ for (const [k, v] of Object.entries(value)) {
2119
+ if (n++ >= 100) break;
2120
+ if (SENSITIVE_KEY.test(k)) out[k] = "[Filtered]";
2121
+ else {
2122
+ const nv = normalizeValue(v, depth - 1, seen);
2123
+ if (nv !== void 0) out[k] = nv;
2124
+ }
2125
+ }
2126
+ return out;
2127
+ } finally {
2128
+ seen.delete(value);
2129
+ }
2130
+ }
2131
+ var record = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
2132
+ var jsonSize = (v) => {
2133
+ try {
2134
+ return JSON.stringify(v)?.length ?? 0;
2135
+ } catch {
2136
+ return Infinity;
2137
+ }
2138
+ };
2139
+ function finalizeDetail(detail, max = MAX_EXCEPTION_DETAIL_BYTES) {
2140
+ let d = normalizeValue(detail, 12);
2141
+ if (!d || typeof d !== "object") return void 0;
2142
+ if (d.tags) d.tags = normalizeTags(Object.fromEntries(Object.entries(d.tags).filter(([k]) => !SENSITIVE_KEY.test(k))));
2143
+ d = trimExceptionDetail(d, max);
2144
+ if (jsonSize(d) <= max) return d;
2145
+ const slim = { ...d.level ? { level: d.level } : {}, ...d.exception ? { exception: d.exception } : {}, ...d.tags ? { tags: d.tags } : {}, ...d.fingerprint ? { fingerprint: d.fingerprint } : {}, ...d.release ? { release: d.release } : {}, ...d.dist ? { dist: d.dist } : {} };
2146
+ return jsonSize(slim) <= max ? slim : void 0;
2147
+ }
2148
+ function buildDetail(input) {
2149
+ const { scope, ctx } = input;
2150
+ const contexts = { ...input.contexts, ...scope.contexts };
2151
+ for (const [k, v] of Object.entries(record(ctx.contexts))) {
2152
+ if (v && typeof v === "object") contexts[k] = v;
2153
+ else delete contexts[k];
2154
+ }
2155
+ const user = { ...input.userId ? { id: input.userId } : {}, ...scope.user, ...record(ctx.user) };
2156
+ const message = input.props.type === "Message";
2157
+ return {
2158
+ level: input.level,
2159
+ platform: "javascript",
2160
+ timestamp: nowSeconds(),
2161
+ ...input.release ? { release: input.release } : {},
2162
+ ...input.dist ? { dist: input.dist } : {},
2163
+ ...input.environment ? { environment: input.environment } : {},
2164
+ ...message ? { message: { formatted: input.props.message } } : { exception: { values: exceptionChain(input.error, input.props, input.mechanism) } },
2165
+ breadcrumbs: { values: scope.breadcrumbs() },
2166
+ contexts,
2167
+ tags: normalizeTags({ ...scope.tags, ...record(ctx.tags) }),
2168
+ extra: { ...scope.extra, ...record(ctx.extra) },
2169
+ ...Object.keys(user).length ? { user } : {},
2170
+ ...Array.isArray(ctx.fingerprint) && ctx.fingerprint.length ? { fingerprint: ctx.fingerprint.map(String) } : {},
2171
+ sdk: input.sdk
2172
+ };
2173
+ }
2174
+ function prepareError(props, extraProperties, detail, opts, fatal, release) {
2175
+ const file = props.frames.find((f) => f.file)?.file;
2176
+ if (opts && errorFilterDrops({ type: props.type, message: props.message, file }, opts)) return null;
2177
+ if (typeof opts?.sampleRate === "number" && opts.sampleRate < 1 && Math.random() >= Math.max(0, opts.sampleRate)) return null;
2178
+ const values = detail.exception?.values ?? [];
2179
+ const thrown = values[values.length - 1];
2180
+ const mechanism = thrown?.mechanism?.type ?? props.source;
2181
+ let properties = { ...props, level: detail.level ?? "error", platform: "javascript", ...mechanism ? { mechanism } : {} };
2182
+ if (release) properties.release = release.slice(0, 100);
2183
+ if (detail.fingerprint?.length) properties.fingerprintParts = detail.fingerprint.slice(0, 20);
2184
+ for (const [k, v] of Object.entries(extraProperties)) if (!(k in properties)) properties[k] = v;
2185
+ let d = detail;
2186
+ if (opts?.beforeSend) {
2187
+ try {
2188
+ const out = opts.beforeSend({ properties, detail: d });
2189
+ if (out === null) return null;
2190
+ if (out && typeof out === "object") {
2191
+ if (out.properties && typeof out.properties === "object") properties = out.properties;
2192
+ if (out.detail && typeof out.detail === "object") d = out.detail;
2193
+ }
2194
+ } catch {
2195
+ }
2196
+ }
2197
+ const finalDetail = finalizeDetail(d);
2198
+ return { eventId: randomId(24), occurredAt: (/* @__PURE__ */ new Date()).toISOString(), properties, ...finalDetail ? { detail: finalDetail } : {}, fatal };
2199
+ }
2200
+ var ExceptionThrottle = class {
2201
+ constructor(max = 5) {
2202
+ __publicField(this, "max", max);
2203
+ __publicField(this, "seen", /* @__PURE__ */ new Map());
2204
+ }
2205
+ allow(props) {
2206
+ const frames = Array.isArray(props.frames) ? props.frames : [];
2207
+ const top = frames[0];
2208
+ const key = `${String(props.type)}|${String(props.message)}|${top ? `${top.file}:${top.line}:${top.col}` : ""}`;
2209
+ const n = this.seen.get(key) ?? 0;
2210
+ if (n >= this.max) return false;
2211
+ if (this.seen.size > 500) this.seen.clear();
2212
+ this.seen.set(key, n + 1);
2213
+ return true;
2214
+ }
2215
+ };
2216
+ function rnVersion(v) {
2217
+ if (!v) return void 0;
2218
+ if (typeof v === "string") return v;
2219
+ if (typeof v.major !== "number") return void 0;
2220
+ return `${v.major}.${v.minor ?? 0}.${v.patch ?? 0}${v.prerelease ? `-${v.prerelease}` : ""}`;
2221
+ }
2222
+ function reactNativeContexts(device, app, g) {
2223
+ const runtime = g ? runtimeContext(g) : void 0;
2224
+ const version = rnVersion(device.reactNativeVersion);
2225
+ return {
2226
+ os: { name: device.platform === "android" ? "Android" : "iOS", ...device.osVersion ? { version: device.osVersion } : {} },
2227
+ device: {
2228
+ family: device.deviceType === "tablet" ? "Tablet" : "Mobile",
2229
+ ...device.model ? { model: device.model } : {},
2230
+ ...device.manufacturer ? { manufacturer: device.manufacturer } : {},
2231
+ ...device.brand ? { brand: device.brand } : {},
2232
+ ...device.simulator !== void 0 ? { simulator: device.simulator } : {}
2233
+ },
2234
+ app: {
2235
+ ...app.appVersion ? { app_version: app.appVersion } : {},
2236
+ ...app.build ? { app_build: app.build } : {},
2237
+ ...app.inForeground !== void 0 ? { in_foreground: app.inForeground } : {}
2238
+ },
2239
+ ...runtime ? { runtime } : {},
2240
+ react_native_context: {
2241
+ ...runtime ? { js_engine: runtime.name } : {},
2242
+ ...version ? { react_native_version: version } : {},
2243
+ ...g ? { turbo_module: !!g.__turboModuleProxy || !!g.RN$Bridgeless, fabric: !!g.nativeFabricUIManager, expo: !!g.expo } : {}
2244
+ }
2245
+ };
2246
+ }
2247
+
1566
2248
  // src/core/targets.ts
1567
2249
  var TargetTimeoutError = class extends Error {
1568
2250
  constructor(ids) {
@@ -2893,38 +3575,38 @@ var SessionRecorder = class {
2893
3575
  this.capturing = true;
2894
3576
  this.captureSoon = false;
2895
3577
  const gen = this.generation;
2896
- let frame = null;
3578
+ let frame2 = null;
2897
3579
  try {
2898
- frame = await this.opts.capturer(await this.opts.captureOptions());
3580
+ frame2 = await this.opts.capturer(await this.opts.captureOptions());
2899
3581
  } catch (err) {
2900
3582
  this.opts.onError?.(err);
2901
3583
  }
2902
3584
  if (gen !== this.generation) return;
2903
3585
  this.capturing = false;
2904
- if (frame) this.onFrame(frame, this.now());
3586
+ if (frame2) this.onFrame(frame2, this.now());
2905
3587
  if (this.sid !== null && this.foreground) this.schedule(this.captureSoon ? TOUCH_SETTLE_MS : this.currentIntervalMs);
2906
3588
  }
2907
- onFrame(frame, at) {
3589
+ onFrame(frame2, at) {
2908
3590
  const id = this.sid;
2909
- if (id === null || !(frame.width > 0) || !(frame.height > 0) || !frame.base64) return;
2910
- const sizeChanged = frame.width !== this.viewportWidth || frame.height !== this.viewportHeight;
3591
+ if (id === null || !(frame2.width > 0) || !(frame2.height > 0) || !frame2.base64) return;
3592
+ const sizeChanged = frame2.width !== this.viewportWidth || frame2.height !== this.viewportHeight;
2911
3593
  const keyframe = this.needKeyframe || !this.hasKeyframe || sizeChanged;
2912
- if (!keyframe && this.lastFrame === frame.base64) {
3594
+ if (!keyframe && this.lastFrame === frame2.base64) {
2913
3595
  this.unchanged++;
2914
3596
  return;
2915
3597
  }
2916
3598
  this.unchanged = 0;
2917
- this.lastFrame = frame.base64;
2918
- this.originX = frame.originX ?? 0;
2919
- this.originY = frame.originY ?? 0;
2920
- this.viewportWidth = frame.width;
2921
- this.viewportHeight = frame.height;
2922
- const url = RrwebEvents.dataUrl(frame.base64);
3599
+ this.lastFrame = frame2.base64;
3600
+ this.originX = frame2.originX ?? 0;
3601
+ this.originY = frame2.originY ?? 0;
3602
+ this.viewportWidth = frame2.width;
3603
+ this.viewportHeight = frame2.height;
3604
+ const url = RrwebEvents.dataUrl(frame2.base64);
2923
3605
  const up = this.opts.uploader;
2924
3606
  if (!keyframe) return up.push(id, RrwebEvents.frame(at, url));
2925
3607
  this.needKeyframe = false;
2926
- up.push(id, RrwebEvents.meta(at, this.opts.href(), frame.width, frame.height));
2927
- up.push(id, RrwebEvents.fullSnapshot(at, url, frame.width, frame.height));
3608
+ up.push(id, RrwebEvents.meta(at, this.opts.href(), frame2.width, frame2.height));
3609
+ up.push(id, RrwebEvents.fullSnapshot(at, url, frame2.width, frame2.height));
2928
3610
  if (!this.hasKeyframe) {
2929
3611
  this.hasKeyframe = true;
2930
3612
  for (const [ts, payload] of this.pendingCustom) up.push(id, RrwebEvents.custom(Math.max(ts, at), "guidely", payload));
@@ -2986,7 +3668,7 @@ var PersistentStore = class {
2986
3668
 
2987
3669
  // src/core/client.ts
2988
3670
  var SDK_NAME = "@guidelytech/react-native";
2989
- var SDK_VERSION = "0.16.0";
3671
+ var SDK_VERSION = "0.17.0";
2990
3672
  var DEFAULT_API_URL = "https://guidelytech.com";
2991
3673
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
2992
3674
  var CACHED_START_MS = 1500;
@@ -3001,6 +3683,10 @@ function settlesWithin(promise, ms) {
3001
3683
  });
3002
3684
  }
3003
3685
  var appVersionKey = (key) => `guidely:app:${key}`;
3686
+ var crashKey = (key) => `guidely:crash:${key}`;
3687
+ var earlyCrashKey = (key) => `guidely:crash-early:${key}`;
3688
+ var MAX_STORED_CRASHES = 10;
3689
+ var MAX_SESSION_EXCEPTIONS = 50;
3004
3690
  var UNKNOWN_VIEW = { selector: "View", tag: "View" };
3005
3691
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
3006
3692
  var emptyState = () => ({ guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] });
@@ -3017,6 +3703,8 @@ var GuidelyClient = class {
3017
3703
  /** Survey answers, guide completions/dismissals and checklist progress, kept until the server has them. */
3018
3704
  __publicField(this, "outbox", null);
3019
3705
  __publicField(this, "config", null);
3706
+ /** The `environment` option as passed (the resolved one is in `options.environment`). */
3707
+ __publicField(this, "envOption");
3020
3708
  __publicField(this, "configFetchedAt", 0);
3021
3709
  __publicField(this, "etag", null);
3022
3710
  __publicField(this, "anonymousId", "");
@@ -3080,6 +3768,16 @@ var GuidelyClient = class {
3080
3768
  __publicField(this, "localizedRc", null);
3081
3769
  /** "What's new" last-seen time when the resource center opened (entries after it read as new). */
3082
3770
  __publicField(this, "announcementsSeenAtOpen", null);
3771
+ // Error monitoring.
3772
+ __publicField(this, "errorScope", new ErrorScope(() => this.options.errors));
3773
+ __publicField(this, "exceptionThrottle", new ExceptionThrottle());
3774
+ /** Errors captured before storage and a config were loaded (whether error tracking is on is unknown until then). */
3775
+ __publicField(this, "earlyErrors", []);
3776
+ /** Crash reports left by earlier launches, sent once a config says error tracking is on. */
3777
+ __publicField(this, "storedCrashes", []);
3778
+ /** This launch's crash reports (normally one: the app ends right after). */
3779
+ __publicField(this, "launchCrashes", []);
3780
+ __publicField(this, "lastAppState", null);
3083
3781
  __publicField(this, "customListeners", /* @__PURE__ */ new Map());
3084
3782
  __publicField(this, "eventBus", {
3085
3783
  on: (name, cb) => {
@@ -3093,9 +3791,10 @@ var GuidelyClient = class {
3093
3791
  }
3094
3792
  });
3095
3793
  if (!options || !options.projectId) throw new Error("[Guidely] `projectId` (your environment public key, e.g. pk_dev_\u2026) is required.");
3794
+ this.envOption = options.environment || void 0;
3096
3795
  this.options = {
3097
3796
  ...options,
3098
- environment: options.environment ?? (options.projectId.startsWith("pk_prod") ? "production" : options.projectId.startsWith("pk_stag") ? "staging" : "development"),
3797
+ environment: options.environment || environmentFromKey(options.projectId),
3099
3798
  apiUrl: options.apiUrl ?? DEFAULT_API_URL,
3100
3799
  analytics: options.analytics ?? true,
3101
3800
  configRefreshMs: options.configRefreshMs ?? 5 * 60 * 1e3,
@@ -3111,6 +3810,7 @@ var GuidelyClient = class {
3111
3810
  if (options.user) this.user = { ...options.user };
3112
3811
  this.locale = options.locale ? normalizeLocale(options.locale) || null : null;
3113
3812
  this.flags = this.createFlags(options.flags);
3813
+ this.installErrorMonitoring();
3114
3814
  }
3115
3815
  createFlags(opts) {
3116
3816
  const base = { sdkKey: this.options.projectId, apiUrl: this.options.apiUrl, fetch: this.options.fetch, sdk: { name: SDK_NAME, version: SDK_VERSION } };
@@ -3158,7 +3858,7 @@ var GuidelyClient = class {
3158
3858
  async bootstrap() {
3159
3859
  const key = this.options.projectId;
3160
3860
  try {
3161
- await this.store.hydrate([KEYS.anon, KEYS.seen, KEYS.session, KEYS.config(key), KEYS.etag(key), KEYS.state(key), KEYS.user(key), KEYS.queue(key), KEYS.outbox(key), KEYS.conversations(key), appVersionKey(key), LAUNCHER_PLACEMENT_KEY]);
3861
+ await this.store.hydrate([KEYS.anon, KEYS.seen, KEYS.session, KEYS.config(key), KEYS.etag(key), KEYS.state(key), KEYS.user(key), KEYS.queue(key), KEYS.outbox(key), KEYS.conversations(key), appVersionKey(key), LAUNCHER_PLACEMENT_KEY, crashKey(key), earlyCrashKey(key)]);
3162
3862
  if (this.destroyed) return;
3163
3863
  this.anonymousId = this.store.get(KEYS.anon) || "";
3164
3864
  this.hadIdentity = !!this.anonymousId;
@@ -3171,7 +3871,9 @@ var GuidelyClient = class {
3171
3871
  this.state = { ...emptyState(), ...readJson(this.store, KEYS.state(key), {}) };
3172
3872
  this.user = this.options.user ? { ...this.options.user } : readJson(this.store, KEYS.user(key), null);
3173
3873
  const saved = readJson(this.store, KEYS.session, null);
3174
- this.session = saved?.id && !this.sessionExpired(Date.parse(saved.lastActiveAt)) ? saved : newSession();
3874
+ this.loadStoredCrashes();
3875
+ const crashed = !!saved?.id && this.storedCrashes.some((c) => c.sessionId === saved.id);
3876
+ this.session = saved?.id && !crashed && !this.sessionExpired(Date.parse(saved.lastActiveAt)) ? saved : newSession();
3175
3877
  this.persistSession();
3176
3878
  this.lastAudiences = new Set(this.state.audienceIds);
3177
3879
  this.syncFlagAttributes();
@@ -3190,9 +3892,10 @@ var GuidelyClient = class {
3190
3892
  this.hydrated = true;
3191
3893
  const cached = readJson(this.store, KEYS.config(key), null);
3192
3894
  this.etag = this.store.get(KEYS.etag(key));
3193
- if (cached && Array.isArray(cached.guides)) this.config = forPlatform(cached, this.bridge.device.platform);
3895
+ if (cached && Array.isArray(cached.guides)) this.useConfig(forPlatform(cached, this.bridge.device.platform));
3194
3896
  this.syncMessenger();
3195
3897
  this.pending.splice(0).forEach((fn) => fn());
3898
+ this.drainErrors();
3196
3899
  const before = this.config?.configVersion;
3197
3900
  const fresh = this.fetchConfig();
3198
3901
  if (!this.config) await fresh;
@@ -3217,22 +3920,33 @@ var GuidelyClient = class {
3217
3920
  if (this.hydrated) fn();
3218
3921
  else this.pending.push(fn);
3219
3922
  }
3923
+ /** Environment guides and flags target: the `environment` option, else the one the loaded config names, else a guess from the key. */
3924
+ get environment() {
3925
+ return this.envOption ?? this.config?.environment ?? this.options.environment;
3926
+ }
3927
+ useConfig(config) {
3928
+ const env = this.environment;
3929
+ this.config = config;
3930
+ if (this.environment !== env) this.syncFlagAttributes();
3931
+ }
3220
3932
  async fetchConfig() {
3221
3933
  try {
3222
3934
  const res = await this.api.get("/config", { etag: this.etag, params: { platform: this.bridge.device.platform } });
3223
3935
  this.configFetchedAt = Date.now();
3224
3936
  if (res.status === 304) return true;
3225
3937
  if (res.ok && res.data && Array.isArray(res.data.guides)) {
3226
- this.config = forPlatform(res.data, this.bridge.device.platform);
3938
+ const config = forPlatform(res.data, this.bridge.device.platform);
3939
+ this.useConfig(config);
3227
3940
  this.etag = res.etag;
3228
3941
  writeJson(this.store, KEYS.config(this.options.projectId), res.data);
3229
3942
  if (res.etag) this.store.set(KEYS.etag(this.options.projectId), res.etag);
3230
3943
  else this.store.remove(KEYS.etag(this.options.projectId));
3231
3944
  this.syncMessenger();
3232
3945
  if (this.hydrated) this.checkLaunch();
3233
- this.emitter.emit("config", this.config);
3946
+ this.drainErrors();
3947
+ this.emitter.emit("config", config);
3234
3948
  this.syncReplay();
3235
- this.log("config loaded", this.config.configVersion, `${this.config.guides.length} guides`);
3949
+ this.log("config loaded", config.configVersion, `${config.guides.length} guides`);
3236
3950
  return true;
3237
3951
  }
3238
3952
  this.reportError(new Error(`[Guidely] config request failed (${res.status})${this.config ? "; using cached config" : ""}`));
@@ -3273,7 +3987,12 @@ var GuidelyClient = class {
3273
3987
  }
3274
3988
  /** Called by the provider with React Native `AppState` changes. */
3275
3989
  setAppState(state) {
3276
- if (this.destroyed || !this.hydrated) return;
3990
+ if (this.destroyed) return;
3991
+ if (state !== this.lastAppState) {
3992
+ this.lastAppState = state;
3993
+ this.autoBreadcrumb("appLifecycle", { type: "navigation", category: "app.lifecycle", message: state, data: { state } });
3994
+ }
3995
+ if (!this.hydrated) return;
3277
3996
  if (state === "background") {
3278
3997
  const wasForeground = this.backgroundedAt === null;
3279
3998
  this.backgroundedAt = Date.now();
@@ -3458,8 +4177,13 @@ var GuidelyClient = class {
3458
4177
  setConsent(consent) {
3459
4178
  this.consent = consent;
3460
4179
  this.whenHydrated(() => {
3461
- if (consent === "denied") this.queue?.clear();
3462
- else if (consent === "granted") void this.queue?.flush();
4180
+ if (consent === "denied") {
4181
+ this.queue?.clear();
4182
+ this.earlyErrors = [];
4183
+ this.storedCrashes = [];
4184
+ this.launchCrashes = [];
4185
+ void this.writeCrashes();
4186
+ } else if (consent === "granted") void this.queue?.flush();
3463
4187
  this.syncReplay();
3464
4188
  });
3465
4189
  }
@@ -3483,6 +4207,7 @@ var GuidelyClient = class {
3483
4207
  accountId: this.user?.account?.id || null,
3484
4208
  sessionId: this.session.id,
3485
4209
  url: this.collectUrls() && this.screenName ? this.currentUrl() : void 0,
4210
+ ...this.appInfo().appVersion ? { appVersion: this.appInfo().appVersion } : {},
3486
4211
  ...extra
3487
4212
  };
3488
4213
  if (extra.properties) event.properties = sanitizeProperties(extra.properties);
@@ -3522,6 +4247,7 @@ var GuidelyClient = class {
3522
4247
  this.whenHydrated(() => {
3523
4248
  const changed = name !== this.screenName;
3524
4249
  if (changed) {
4250
+ this.autoBreadcrumb("navigation", { type: "navigation", category: "navigation", data: { ...this.screenName ? { from: this.screenName } : {}, to: name } });
3525
4251
  this.leaveScreen();
3526
4252
  this.flushHeatmap();
3527
4253
  }
@@ -3685,7 +4411,11 @@ var GuidelyClient = class {
3685
4411
  }
3686
4412
  /** True when taps are worth observing (clicks, rage taps or heatmaps are on); the provider skips resolving views otherwise. */
3687
4413
  isTapCaptureOn() {
3688
- return this.hydrated && (this.autocaptureOn("clicks") || this.autocaptureOn("rageClicks") || this.autocaptureOn("heatmaps"));
4414
+ return this.hydrated && (this.autocaptureOn("clicks") || this.autocaptureOn("rageClicks") || this.autocaptureOn("heatmaps") || this.tapBreadcrumbsOn());
4415
+ }
4416
+ /** Taps become `ui.click` breadcrumbs while errors can be reported (even with click autocapture off). */
4417
+ tapBreadcrumbsOn() {
4418
+ return this.crumbsOn("taps") && this.errorTrackingAllowed();
3689
4419
  }
3690
4420
  /**
3691
4421
  * Records a tap observed by the provider: `click` for an interactive element, `rage_click` for
@@ -3695,8 +4425,8 @@ var GuidelyClient = class {
3695
4425
  recordTap(tap) {
3696
4426
  try {
3697
4427
  if (!this.hydrated || this.destroyed || !Number.isFinite(tap.x) || !Number.isFinite(tap.y)) return;
3698
- const on = { clicks: this.autocaptureOn("clicks"), rage: this.autocaptureOn("rageClicks"), heat: this.autocaptureOn("heatmaps") };
3699
- if (!on.clicks && !on.rage && !on.heat) return;
4428
+ const on = { clicks: this.autocaptureOn("clicks"), rage: this.autocaptureOn("rageClicks"), heat: this.autocaptureOn("heatmaps"), crumb: this.tapBreadcrumbsOn() };
4429
+ if (!on.clicks && !on.rage && !on.heat && !on.crumb) return;
3700
4430
  const t = tap.t ?? Date.now();
3701
4431
  const url = this.screenName ? this.currentUrl() : "";
3702
4432
  this.tapChain = this.tapChain.then(() => this.processTap(tap, t, url, on)).catch((err) => this.reportError(err));
@@ -3713,6 +4443,8 @@ var GuidelyClient = class {
3713
4443
  rect = target.rect;
3714
4444
  }
3715
4445
  if (this.destroyed) return;
4446
+ if (on.crumb && el) this.autoBreadcrumb("taps", { timestamp: t / 1e3, type: "user", category: "ui.click", message: el.selector, data: { selector: el.selector } });
4447
+ if (!on.clicks && !on.rage && !on.heat) return;
3716
4448
  const platform = this.bridge.device.platform;
3717
4449
  const maskAll = !!this.config?.settings?.replay?.maskAllText || !!this.options.sessionReplay?.maskAllText;
3718
4450
  const eventUrl = this.collectUrls() && url ? url : void 0;
@@ -3811,6 +4543,277 @@ var GuidelyClient = class {
3811
4543
  }
3812
4544
  }
3813
4545
  // ---------------------------------------------------------------------------
4546
+ // Error monitoring (docs/error-monitoring/sdks.md)
4547
+ // ---------------------------------------------------------------------------
4548
+ /**
4549
+ * Reports a caught error as an `exception` event (`handled: true`), grouped into an issue on the
4550
+ * Errors page. The second argument takes Sentry's capture context (`tags`, `extra`, `level`,
4551
+ * `fingerprint`, `contexts`, `user`); any other key is attached to the occurrence as a property.
4552
+ * Needs the project's error tracking on (plus analytics and consent); `errors.captureErrors: false`
4553
+ * does not stop it. Returns the event id (empty when nothing is sent).
4554
+ */
4555
+ captureException(error, hint) {
4556
+ if (this.destroyed) return "";
4557
+ try {
4558
+ const { ctx, properties } = splitHint(hint);
4559
+ const props = exceptionFromValue(error, "manual", true);
4560
+ return this.deliverError(this.prepareCapture(error, props, properties, ctx, "generic", ctx.level ?? "error", false));
4561
+ } catch (err) {
4562
+ this.reportError(err);
4563
+ return "";
4564
+ }
4565
+ }
4566
+ /** Reports a message (default level `info`) as its own issue, grouped by the message. */
4567
+ captureMessage(message, level = "info", hint) {
4568
+ if (this.destroyed) return "";
4569
+ try {
4570
+ const { ctx } = splitHint(hint);
4571
+ const props = { type: "Message", message: String(message).slice(0, 1e3), frames: [], source: "message", handled: true };
4572
+ return this.deliverError(this.prepareCapture(void 0, props, {}, ctx, "message", ctx.level ?? level, false));
4573
+ } catch (err) {
4574
+ this.reportError(err);
4575
+ return "";
4576
+ }
4577
+ }
4578
+ /** Adds a breadcrumb (sent with the next errors; the last 100 are kept). */
4579
+ addBreadcrumb(crumb, hint) {
4580
+ if (!this.destroyed) this.errorScope.addBreadcrumb(crumb, hint);
4581
+ }
4582
+ /** Tags every later error (searchable on the issue page); `null` removes the tag. */
4583
+ setTag(key, value) {
4584
+ if (value === null || value === void 0) delete this.errorScope.tags[key];
4585
+ else this.errorScope.tags[key] = String(value);
4586
+ }
4587
+ setTags(tags) {
4588
+ for (const [k, v] of Object.entries(tags ?? {})) this.setTag(k, v);
4589
+ }
4590
+ /** Arbitrary data attached to every later error. */
4591
+ setExtra(key, value) {
4592
+ if (value === void 0) delete this.errorScope.extra[key];
4593
+ else this.errorScope.extra[key] = value;
4594
+ }
4595
+ setExtras(extras) {
4596
+ for (const [k, v] of Object.entries(extras ?? {})) this.setExtra(k, v);
4597
+ }
4598
+ /** A named context (`setContext("cart", { items: 3 })`) on every later error; `null` removes it. */
4599
+ setContext(name, context) {
4600
+ if (context && typeof context === "object") this.errorScope.contexts[name] = context;
4601
+ else delete this.errorScope.contexts[name];
4602
+ }
4603
+ /**
4604
+ * Sentry's `setUser`: identifies the user by `id` (like `identify`) and keeps `email` / `username`
4605
+ * on their errors. `null` forgets the user (`reset()`).
4606
+ */
4607
+ setUser(user) {
4608
+ if (!user) {
4609
+ this.errorScope.user = {};
4610
+ return this.reset();
4611
+ }
4612
+ const { id, email, username, ...traits } = user;
4613
+ this.errorScope.user = { ...email ? { email: String(email) } : {}, ...username ? { username: String(username) } : {} };
4614
+ if (id !== void 0 && id !== null && String(id)) void this.identify(String(id), { ...traits, ...email ? { email } : {}, ...username ? { username } : {} });
4615
+ }
4616
+ crumbsOn(kind) {
4617
+ const b = this.options.errors?.breadcrumbs;
4618
+ return b !== false && !(b && b[kind] === false);
4619
+ }
4620
+ autoBreadcrumb(kind, crumb, hint) {
4621
+ if (!this.destroyed && this.crumbsOn(kind)) this.errorScope.addBreadcrumb(crumb, hint);
4622
+ }
4623
+ /** Errors may be captured: analytics on, consent not denied and, once a config says, the project's error tracking on. */
4624
+ errorsWanted() {
4625
+ if (this.destroyed || !this.options.analytics || this.consent === "denied") return false;
4626
+ const s = this.config?.settings;
4627
+ return !this.config || (s?.analyticsEnabled ?? true) && !!s?.errorTracking?.enabled;
4628
+ }
4629
+ /** Errors may be queued now (a config is known and allows them). */
4630
+ errorTrackingAllowed() {
4631
+ return !!this.config && this.errorsWanted();
4632
+ }
4633
+ /** Requests to the Guidely deployment never become breadcrumbs. */
4634
+ isGuidelyUrl(url) {
4635
+ const base = this.options.apiUrl.replace(/\/+$/, "");
4636
+ return !!base && (url === base || url.startsWith(`${base}/`));
4637
+ }
4638
+ release() {
4639
+ return this.options.release ?? this.appInfo().appVersion;
4640
+ }
4641
+ /** Builds and filters one error (`null` when dropped). */
4642
+ prepareCapture(error, props, properties, ctx, mechanism, level, fatal) {
4643
+ if (!this.errorsWanted()) return null;
4644
+ const info = this.appInfo();
4645
+ const release = this.release();
4646
+ const detail = buildDetail({
4647
+ error,
4648
+ props,
4649
+ mechanism,
4650
+ scope: this.errorScope,
4651
+ ctx,
4652
+ level,
4653
+ contexts: reactNativeContexts(this.bridge.device, { ...info, inForeground: this.backgroundedAt === null }, this.bridge.globals),
4654
+ release,
4655
+ dist: this.options.errors?.dist ?? info.build,
4656
+ environment: this.environment,
4657
+ userId: this.user?.id || null,
4658
+ sdk: { name: SDK_NAME, version: SDK_VERSION }
4659
+ });
4660
+ return prepareError(props, properties, detail, this.options.errors, fatal, release);
4661
+ }
4662
+ /** Queues a prepared error, or holds it until storage and a config are loaded. Returns its event id ("" when dropped). */
4663
+ deliverError(p) {
4664
+ if (!p) return "";
4665
+ if (!this.hydrated || !this.config) {
4666
+ if (this.earlyErrors.length >= 10) return "";
4667
+ this.earlyErrors.push(p);
4668
+ return p.eventId;
4669
+ }
4670
+ return this.sendError(p) ? p.eventId : "";
4671
+ }
4672
+ sendError(p) {
4673
+ if (!this.errorTrackingAllowed()) return false;
4674
+ if (!p.fatal && !this.exceptionThrottle.allow(p.properties)) return false;
4675
+ const count = this.session.exceptions ?? 0;
4676
+ if (!p.fatal && count >= MAX_SESSION_EXCEPTIONS) return false;
4677
+ const event = this.enqueue("exception", { eventId: p.eventId, occurredAt: p.occurredAt, properties: p.properties, ...p.detail ? { detail: p.detail } : {} });
4678
+ if (!event) return false;
4679
+ this.session.exceptions = count + 1;
4680
+ this.persistSession();
4681
+ return true;
4682
+ }
4683
+ /** Sends what waited for storage and a config: earlier launches' crash reports, then early errors. */
4684
+ drainErrors() {
4685
+ if (!this.hydrated || !this.config || this.destroyed) return;
4686
+ const stored = this.storedCrashes.splice(0);
4687
+ if (stored.length) {
4688
+ if (this.errorTrackingAllowed()) {
4689
+ for (const c of stored) this.queue?.enqueue({ ...c, anonymousId: c.anonymousId || this.anonymousId, sessionId: c.sessionId || this.session.id });
4690
+ }
4691
+ void this.writeCrashes();
4692
+ }
4693
+ this.earlyErrors.splice(0).forEach((p) => this.sendError(p));
4694
+ }
4695
+ loadStoredCrashes() {
4696
+ const key = this.options.projectId;
4697
+ const valid = (c) => !!c && typeof c === "object" && typeof c.eventId === "string" && c.name === "exception";
4698
+ const read2 = (k) => {
4699
+ const v = readJson(this.store, k, []);
4700
+ return Array.isArray(v) ? v.filter(valid) : [];
4701
+ };
4702
+ const seen = /* @__PURE__ */ new Set();
4703
+ this.storedCrashes = [...read2(crashKey(key)), ...read2(earlyCrashKey(key))].filter((c) => !seen.has(c.eventId) && !!seen.add(c.eventId)).slice(-MAX_STORED_CRASHES);
4704
+ this.store.remove(earlyCrashKey(key));
4705
+ if (this.storedCrashes.length) void this.writeCrashes();
4706
+ }
4707
+ /**
4708
+ * Writes the unsent crash reports to storage, directly (not behind other pending writes) so the
4709
+ * write starts before the app ends. Resolves when the storage has it.
4710
+ */
4711
+ writeCrashes() {
4712
+ const storage = this.options.storage;
4713
+ const key = this.options.projectId;
4714
+ const direct = (op) => {
4715
+ try {
4716
+ return Promise.resolve(op()).catch((err) => this.log("crash report storage error", err));
4717
+ } catch (err) {
4718
+ this.log("crash report storage error", err);
4719
+ return Promise.resolve();
4720
+ }
4721
+ };
4722
+ if (!this.hydrated) {
4723
+ const json2 = JSON.stringify(this.launchCrashes.slice(-MAX_STORED_CRASHES));
4724
+ return storage ? direct(() => storage.setItem(earlyCrashKey(key), json2)) : Promise.resolve();
4725
+ }
4726
+ const seen = /* @__PURE__ */ new Set();
4727
+ const all = [...this.storedCrashes, ...this.launchCrashes].filter((c) => !seen.has(c.eventId) && !!seen.add(c.eventId)).slice(-MAX_STORED_CRASHES);
4728
+ if (!all.length) {
4729
+ this.store.remove(crashKey(key));
4730
+ return storage ? direct(() => storage.removeItem(crashKey(key))) : Promise.resolve();
4731
+ }
4732
+ const json = JSON.stringify(all);
4733
+ this.store.set(crashKey(key), json);
4734
+ return storage ? direct(() => storage.setItem(crashKey(key), json)) : Promise.resolve();
4735
+ }
4736
+ /**
4737
+ * A fatal JS error: React Native ends a release app right after the global handler, so the report
4738
+ * is written to storage at once (and sent on the next launch), and sent now as well when the
4739
+ * network allows. The promise tells the handler when React Native's own handler may run.
4740
+ */
4741
+ saveCrash(p) {
4742
+ if (!this.errorsWanted()) return void 0;
4743
+ const info = this.appInfo();
4744
+ const event = {
4745
+ eventId: p.eventId,
4746
+ name: "exception",
4747
+ occurredAt: p.occurredAt,
4748
+ anonymousId: this.anonymousId,
4749
+ userId: this.user?.id || null,
4750
+ accountId: this.user?.account?.id || null,
4751
+ ...this.hydrated ? { sessionId: this.session.id } : {},
4752
+ ...this.collectUrls() && this.screenName ? { url: this.currentUrl() } : {},
4753
+ ...info.appVersion ? { appVersion: info.appVersion } : {},
4754
+ properties: sanitizeProperties(p.properties),
4755
+ ...p.detail ? { detail: p.detail } : {}
4756
+ };
4757
+ this.launchCrashes.push(event);
4758
+ const written = this.writeCrashes();
4759
+ if (!this.hydrated || !this.config || !this.sendError(p)) return written;
4760
+ const sent = this.queue?.flush() ?? Promise.resolve();
4761
+ return Promise.all([written, sent]).then(async () => {
4762
+ if (this.queue?.peek().some((e) => e.eventId === event.eventId)) return;
4763
+ this.launchCrashes = this.launchCrashes.filter((c) => c.eventId !== event.eventId);
4764
+ await this.writeCrashes();
4765
+ });
4766
+ }
4767
+ /** An uncaught JS error from `ErrorUtils` (fatal ones end the app in release builds). */
4768
+ captureGlobalError(error, fatal) {
4769
+ try {
4770
+ const props = exceptionFromValue(error, "onerror", false);
4771
+ if (props.message.startsWith("[Guidely]")) return void 0;
4772
+ const p = this.prepareCapture(error, props, {}, {}, "onerror", fatal ? "fatal" : "error", fatal);
4773
+ if (!p) return void 0;
4774
+ if (fatal) return this.saveCrash(p);
4775
+ this.deliverError(p);
4776
+ } catch (err) {
4777
+ this.log("error capture failed", err);
4778
+ }
4779
+ return void 0;
4780
+ }
4781
+ captureRejection(reason) {
4782
+ try {
4783
+ const props = exceptionFromValue(reason, "unhandledrejection", false, "UnhandledRejection");
4784
+ if (props.message.startsWith("[Guidely]")) return;
4785
+ this.deliverError(this.prepareCapture(reason, props, {}, {}, "unhandledrejection", "error", false));
4786
+ } catch (err) {
4787
+ this.log("error capture failed", err);
4788
+ }
4789
+ }
4790
+ /**
4791
+ * Guide breadcrumbs, and (with the bridge's JS globals) the global error handler, promise
4792
+ * rejection tracking and console / network breadcrumbs. Undone by `destroy()`.
4793
+ */
4794
+ installErrorMonitoring() {
4795
+ const guideCrumb = (message, guide, stepIndex) => this.autoBreadcrumb("guides", { type: "info", category: "guide", message, data: { guideId: guide.id, ...stepIndex !== void 0 ? { stepIndex } : {} } });
4796
+ this.cleanups.push(
4797
+ this.emitter.on("guide:start", (g2) => guideCrumb("Guide shown", g2)),
4798
+ this.emitter.on("guide:step", (s) => guideCrumb("Guide step", s.guide, s.stepIndex)),
4799
+ this.emitter.on("guide:complete", (g2) => guideCrumb("Guide completed", g2)),
4800
+ this.emitter.on("guide:dismiss", (g2) => guideCrumb("Guide dismissed", g2)),
4801
+ this.emitter.on("guide:skip", (g2) => guideCrumb("Guide skipped", g2))
4802
+ );
4803
+ const g = this.bridge.globals;
4804
+ if (!g) return;
4805
+ try {
4806
+ if (this.crumbsOn("console")) this.cleanups.push(instrumentConsole(g.console, (c) => this.autoBreadcrumb("console", c)));
4807
+ if (this.crumbsOn("http")) this.cleanups.push(instrumentHttp(g, (c) => this.autoBreadcrumb("http", c), (url) => this.isGuidelyUrl(url)));
4808
+ if (this.options.errors?.captureErrors !== false) {
4809
+ this.cleanups.push(installGlobalHandler(g, (error, fatal) => this.captureGlobalError(error, fatal)));
4810
+ this.cleanups.push(installRejectionTracking(g, (reason) => this.captureRejection(reason), this.options.errors?.rejectionTracker));
4811
+ }
4812
+ } catch (err) {
4813
+ this.reportError(err);
4814
+ }
4815
+ }
4816
+ // ---------------------------------------------------------------------------
3814
4817
  // Targeting + triggers
3815
4818
  // ---------------------------------------------------------------------------
3816
4819
  /** Builds the targeting context for the current moment. */
@@ -3836,7 +4839,7 @@ var GuidelyClient = class {
3836
4839
  completedChecklists: new Set(keysOf(Object.entries(this.state.checklists).filter(([, p]) => !!p.completedAt).map(([id]) => id))),
3837
4840
  audienceIds: new Set(this.state.audienceIds),
3838
4841
  now: /* @__PURE__ */ new Date(),
3839
- environment: this.options.environment,
4842
+ environment: this.environment,
3840
4843
  locale: device.locale,
3841
4844
  platform: device.platform,
3842
4845
  screen: this.screenName,
@@ -4657,7 +5660,7 @@ var GuidelyClient = class {
4657
5660
  locale: d.locale,
4658
5661
  appVersion: this.options.appVersion ?? d.appVersion,
4659
5662
  osVersion: d.osVersion,
4660
- environment: this.options.environment,
5663
+ environment: this.environment,
4661
5664
  ...this.screenName ? { screen: this.screenName, path: this.currentUrl() } : {},
4662
5665
  ...this.user?.traits ?? {},
4663
5666
  id: this.user?.id || this.anonymousId || void 0,
@@ -4782,6 +5785,35 @@ function detectAppVersion(src) {
4782
5785
  return {};
4783
5786
  }
4784
5787
  }
5788
+ function detectDeviceModel(src) {
5789
+ try {
5790
+ const expo = src.expoModules?.ExpoDevice ?? src.nativeModules.ExpoDevice;
5791
+ const info = src.nativeModules.RNDeviceInfo;
5792
+ const c = src.platform.constants ?? {};
5793
+ const android = src.platform.OS === "android";
5794
+ const model = text(constant(expo, "modelName")) ?? text(constant(info, "model")) ?? (android ? text(c.Model) : void 0);
5795
+ const manufacturer = text(constant(expo, "manufacturer")) ?? (android ? text(c.Manufacturer) : "Apple");
5796
+ const brand = text(constant(expo, "brand")) ?? text(constant(info, "brand")) ?? (android ? text(c.Brand) : void 0);
5797
+ const isDevice = constant(expo, "isDevice");
5798
+ return {
5799
+ ...model ? { model } : {},
5800
+ ...manufacturer ? { manufacturer } : {},
5801
+ ...brand ? { brand } : {},
5802
+ ...typeof isDevice === "boolean" ? { simulator: !isDevice } : {}
5803
+ };
5804
+ } catch {
5805
+ return {};
5806
+ }
5807
+ }
5808
+ function detectReactNativeVersion(src) {
5809
+ try {
5810
+ const v = src.platform.constants?.reactNativeVersion;
5811
+ if (!v || typeof v.major !== "number") return void 0;
5812
+ return { major: v.major, minor: Number(v.minor) || 0, patch: Number(v.patch) || 0, ...v.prerelease != null ? { prerelease: String(v.prerelease) } : {} };
5813
+ } catch {
5814
+ return void 0;
5815
+ }
5816
+ }
4785
5817
  function detectOsVersion(src) {
4786
5818
  try {
4787
5819
  const c = src.platform.constants ?? {};
@@ -4914,7 +5946,7 @@ function measureHost(host) {
4914
5946
  done(null);
4915
5947
  });
4916
5948
  }
4917
- function describe(f, masked, opts, withText) {
5949
+ function describe2(f, masked, opts, withText) {
4918
5950
  const props = f.memoizedProps && typeof f.memoizedProps === "object" ? f.memoizedProps : {};
4919
5951
  const host = firstHost(f);
4920
5952
  const el = { selector: idSelector(f) ?? pathSelector(f), tag: componentName(f.type) ?? "View" };
@@ -4959,8 +5991,8 @@ function resolveFiber(target, opts = {}) {
4959
5991
  const element = press ?? role;
4960
5992
  return {
4961
5993
  ignored: false,
4962
- element: element ? describe(element, masked, opts, true) : null,
4963
- hit: hitFiber ? describe(hitFiber, masked, opts, componentName(hitFiber.type) === "Text") : null
5994
+ element: element ? describe2(element, masked, opts, true) : null,
5995
+ hit: hitFiber ? describe2(hitFiber, masked, opts, componentName(hitFiber.type) === "Text") : null
4964
5996
  };
4965
5997
  } catch {
4966
5998
  return none;
@@ -5093,7 +6125,7 @@ function parseHtml(html) {
5093
6125
  if (last && last.bold === run.bold && last.italic === run.italic && last.underline === run.underline && last.code === run.code && last.href === run.href) last.text += text2;
5094
6126
  else current.runs.push(run);
5095
6127
  };
5096
- const size = (attrs, name) => {
6128
+ const size2 = (attrs, name) => {
5097
6129
  const n = Number(attr(attrs, name));
5098
6130
  return n > 0 ? n : void 0;
5099
6131
  };
@@ -5155,8 +6187,8 @@ function parseHtml(html) {
5155
6187
  }
5156
6188
  } else if (tag === "img" && !closing) {
5157
6189
  const src = attr(attrs, "src");
5158
- const w = size(attrs, "width");
5159
- const h = size(attrs, "height");
6190
+ const w = size2(attrs, "width");
6191
+ const h = size2(attrs, "height");
5160
6192
  if (isSafeMediaUrl(src)) pdf.pages.push({ src, alt: attr(attrs, "alt") ?? "", ...w && h ? { width: w, height: h } : {} });
5161
6193
  }
5162
6194
  continue;
@@ -5330,10 +6362,10 @@ function BodyPdf({ block, palette: p, onLink }) {
5330
6362
  ] }) })
5331
6363
  ] });
5332
6364
  }
5333
- function RichText({ html, palette: p, onLink, size = 15 }) {
6365
+ function RichText({ html, palette: p, onLink, size: size2 = 15 }) {
5334
6366
  const blocks = (0, import_react2.useMemo)(() => parseHtml(html), [html]);
5335
6367
  if (!blocks.length) return null;
5336
- const base = { color: p.text, fontSize: size, lineHeight: Math.round(size * 1.42), fontFamily: p.fontFamily };
6368
+ const base = { color: p.text, fontSize: size2, lineHeight: Math.round(size2 * 1.42), fontFamily: p.fontFamily };
5337
6369
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_native3.View, { style: styles2.wrap, children: blocks.map((b, i) => {
5338
6370
  if (b.kind === "image") return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(BodyImage, { block: b, palette: p }, i);
5339
6371
  if (b.kind === "video") return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(BodyVideo, { block: b, palette: p, onLink }, i);
@@ -5536,17 +6568,17 @@ function useReduceMotion() {
5536
6568
  }, []);
5537
6569
  return on;
5538
6570
  }
5539
- function MessengerLauncher({ store, client, palette: p, style, frame, defaultSide }) {
6571
+ function MessengerLauncher({ store, client, palette: p, style, frame: frame2, defaultSide }) {
5540
6572
  const snap = useMessenger(store);
5541
6573
  const reduceMotion = useReduceMotion();
5542
6574
  const [placement, setPlacement] = (0, import_react3.useState)(() => client.getMessengerLauncherPlacement());
5543
6575
  const pan = (0, import_react3.useRef)(new import_react_native5.Animated.ValueXY()).current;
5544
6576
  const offset = (0, import_react3.useRef)({ x: 0, y: 0 });
5545
6577
  const dragging = (0, import_react3.useRef)(false);
5546
- const live = (0, import_react3.useRef)({ frame, defaultSide, reduceMotion });
5547
- live.current = { frame, defaultSide, reduceMotion };
5548
- const { width, height, size, defaultBottom } = frame;
5549
- const { top, bottom, left, right } = frame.insets;
6578
+ const live = (0, import_react3.useRef)({ frame: frame2, defaultSide, reduceMotion });
6579
+ live.current = { frame: frame2, defaultSide, reduceMotion };
6580
+ const { width, height, size: size2, defaultBottom } = frame2;
6581
+ const { top, bottom, left, right } = frame2.insets;
5550
6582
  (0, import_react3.useLayoutEffect)(() => {
5551
6583
  if (dragging.current) return;
5552
6584
  const f = live.current.frame;
@@ -5554,7 +6586,7 @@ function MessengerLauncher({ store, client, palette: p, style, frame, defaultSid
5554
6586
  const at = placement ? placementPoint(f, placement) : anchor;
5555
6587
  offset.current = { x: at.x - anchor.x, y: at.y - anchor.y };
5556
6588
  pan.setValue(offset.current);
5557
- }, [pan, placement, defaultSide, width, height, size, defaultBottom, top, bottom, left, right]);
6589
+ }, [pan, placement, defaultSide, width, height, size2, defaultBottom, top, bottom, left, right]);
5558
6590
  const responder = (0, import_react3.useMemo)(() => {
5559
6591
  const moved = (g) => Math.hypot(g.dx, g.dy) > LAUNCHER_DRAG_THRESHOLD;
5560
6592
  const anchor = () => placementPoint(live.current.frame, { side: live.current.defaultSide, fraction: 0 });
@@ -6078,18 +7110,18 @@ var import_react_native8 = require("react-native");
6078
7110
  var MARGIN = 12;
6079
7111
  var ARROW_INSET = 18;
6080
7112
  var opposite = { top: "bottom", bottom: "top", left: "right", right: "left" };
6081
- function positionCard(target, size, viewport, insets, placement, offset) {
7113
+ function positionCard(target, size2, viewport, insets, placement, offset) {
6082
7114
  const [rawSide, align] = placement.split("-");
6083
7115
  const fits = (side2) => {
6084
7116
  switch (side2) {
6085
7117
  case "bottom":
6086
- return target.y + target.height + offset + size.height <= viewport.height - insets.bottom - MARGIN;
7118
+ return target.y + target.height + offset + size2.height <= viewport.height - insets.bottom - MARGIN;
6087
7119
  case "top":
6088
- return target.y - offset - size.height >= insets.top + MARGIN;
7120
+ return target.y - offset - size2.height >= insets.top + MARGIN;
6089
7121
  case "right":
6090
- return target.x + target.width + offset + size.width <= viewport.width - insets.right - MARGIN;
7122
+ return target.x + target.width + offset + size2.width <= viewport.width - insets.right - MARGIN;
6091
7123
  case "left":
6092
- return target.x - offset - size.width >= insets.left + MARGIN;
7124
+ return target.x - offset - size2.width >= insets.left + MARGIN;
6093
7125
  }
6094
7126
  };
6095
7127
  const order = rawSide === "auto" ? ["bottom", "top"] : [rawSide, opposite[rawSide], "bottom", "top"];
@@ -6098,17 +7130,17 @@ function positionCard(target, size, viewport, insets, placement, offset) {
6098
7130
  const clamp3 = (v, min, max) => Math.max(min, Math.min(max, v));
6099
7131
  if (side === "top" || side === "bottom") {
6100
7132
  const cx = target.x + target.width / 2;
6101
- let x2 = align === "start" ? target.x : align === "end" ? target.x + target.width - size.width : cx - size.width / 2;
6102
- x2 = clamp3(x2, insets.left + MARGIN, Math.max(insets.left + MARGIN, viewport.width - insets.right - MARGIN - size.width));
6103
- let y2 = side === "bottom" ? target.y + target.height + offset : target.y - offset - size.height;
6104
- y2 = clamp3(y2, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
6105
- return { x: x2, y: y2, side, arrow: clamp3(cx - x2, ARROW_INSET, size.width - ARROW_INSET) };
7133
+ let x2 = align === "start" ? target.x : align === "end" ? target.x + target.width - size2.width : cx - size2.width / 2;
7134
+ x2 = clamp3(x2, insets.left + MARGIN, Math.max(insets.left + MARGIN, viewport.width - insets.right - MARGIN - size2.width));
7135
+ let y2 = side === "bottom" ? target.y + target.height + offset : target.y - offset - size2.height;
7136
+ y2 = clamp3(y2, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size2.height));
7137
+ return { x: x2, y: y2, side, arrow: clamp3(cx - x2, ARROW_INSET, size2.width - ARROW_INSET) };
6106
7138
  }
6107
7139
  const cy = target.y + target.height / 2;
6108
- let y = align === "start" ? target.y : align === "end" ? target.y + target.height - size.height : cy - size.height / 2;
6109
- y = clamp3(y, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
6110
- const x = side === "right" ? target.x + target.width + offset : target.x - offset - size.width;
6111
- return { x, y, side, arrow: clamp3(cy - y, ARROW_INSET, size.height - ARROW_INSET) };
7140
+ let y = align === "start" ? target.y : align === "end" ? target.y + target.height - size2.height : cy - size2.height / 2;
7141
+ y = clamp3(y, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size2.height));
7142
+ const x = side === "right" ? target.x + target.width + offset : target.x - offset - size2.width;
7143
+ return { x, y, side, arrow: clamp3(cy - y, ARROW_INSET, size2.height - ARROW_INSET) };
6112
7144
  }
6113
7145
 
6114
7146
  // src/ui/StepCard.tsx
@@ -6262,9 +7294,9 @@ function StepLayer({ view, client, palette: p, geometry }) {
6262
7294
  }
6263
7295
  if (!rect) return null;
6264
7296
  if (step.type === "hotspot") {
6265
- const size = step.hotspot?.size ?? 14;
7297
+ const size2 = step.hotspot?.size ?? 14;
6266
7298
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.View, { pointerEvents: "box-none", style: import_react_native8.StyleSheet.absoluteFill, children: [
6267
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Hotspot, { x: rect.x + rect.width - size / 2, y: rect.y - size / 2, size, pulse: step.hotspot?.pulse ?? true, color: p.primary, onPress: () => client.toggleHotspot(view.guide.id), label: step.title || "Show tip" }),
7299
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Hotspot, { x: rect.x + rect.width - size2 / 2, y: rect.y - size2 / 2, size: size2, pulse: step.hotspot?.pulse ?? true, color: p.primary, onPress: () => client.toggleHotspot(view.guide.id), label: step.title || "Show tip" }),
6268
7300
  view.expanded ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Anchored, { view, client, palette: p, geometry, rect, width: cardWidth }) : null
6269
7301
  ] });
6270
7302
  }
@@ -6311,7 +7343,7 @@ function Arrow({ side, at, color, border }) {
6311
7343
  const place = side === "bottom" ? { top: -half, left: at - half } : side === "top" ? { bottom: -half, left: at - half } : side === "right" ? { left: -half, top: at - half } : { right: -half, top: at - half };
6312
7344
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.View, { pointerEvents: "none", style: [styles7.abs, styles7.arrow, place, { backgroundColor: color, borderColor: border }] });
6313
7345
  }
6314
- function Hotspot({ x, y, size, pulse, color, onPress, label }) {
7346
+ function Hotspot({ x, y, size: size2, pulse, color, onPress, label }) {
6315
7347
  const scale = (0, import_react5.useRef)(new import_react_native8.Animated.Value(0)).current;
6316
7348
  const [reduceMotion, setReduceMotion] = (0, import_react5.useState)(false);
6317
7349
  (0, import_react5.useEffect)(() => {
@@ -6328,15 +7360,15 @@ function Hotspot({ x, y, size, pulse, color, onPress, label }) {
6328
7360
  loop.start();
6329
7361
  return () => loop.stop();
6330
7362
  }, [pulse, reduceMotion, scale]);
6331
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.Pressable, { accessibilityRole: "button", accessibilityLabel: label, onPress, hitSlop: 14, style: [styles7.abs, { left: x, top: y, width: size, height: size }], children: [
7363
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react_native8.Pressable, { accessibilityRole: "button", accessibilityLabel: label, onPress, hitSlop: 14, style: [styles7.abs, { left: x, top: y, width: size2, height: size2 }], children: [
6332
7364
  pulse && !reduceMotion ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
6333
7365
  import_react_native8.Animated.View,
6334
7366
  {
6335
7367
  pointerEvents: "none",
6336
- style: [styles7.abs, { width: size, height: size, borderRadius: size / 2, backgroundColor: color, opacity: scale.interpolate({ inputRange: [0, 1], outputRange: [0.55, 0] }), transform: [{ scale: scale.interpolate({ inputRange: [0, 1], outputRange: [1, 2.6] }) }] }]
7368
+ style: [styles7.abs, { width: size2, height: size2, borderRadius: size2 / 2, backgroundColor: color, opacity: scale.interpolate({ inputRange: [0, 1], outputRange: [0.55, 0] }), transform: [{ scale: scale.interpolate({ inputRange: [0, 1], outputRange: [1, 2.6] }) }] }]
6337
7369
  }
6338
7370
  ) : null,
6339
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.View, { style: { width: size, height: size, borderRadius: size / 2, backgroundColor: color, borderWidth: 2, borderColor: "#ffffff" } })
7371
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_react_native8.View, { style: { width: size2, height: size2, borderRadius: size2 / 2, backgroundColor: color, borderWidth: 2, borderColor: "#ffffff" } })
6340
7372
  ] });
6341
7373
  }
6342
7374
  var styles7 = import_react_native8.StyleSheet.create({
@@ -6947,12 +7979,16 @@ function defaultBridge(appVersion, appBuild) {
6947
7979
  locale: deviceLocale(),
6948
7980
  appVersion: appVersion ?? detected.appVersion,
6949
7981
  appBuild: appBuild ?? detected.appBuild,
6950
- osVersion: detectOsVersion(sources)
7982
+ osVersion: detectOsVersion(sources),
7983
+ ...detectDeviceModel(sources),
7984
+ reactNativeVersion: detectReactNativeVersion(sources)
6951
7985
  },
6952
7986
  openExternal: (url) => void import_react_native13.Linking.openURL(url).catch(() => {
6953
7987
  }),
6954
7988
  windowSize: () => import_react_native13.Dimensions.get("window"),
6955
- capturer: nativeCapturer()
7989
+ capturer: nativeCapturer(),
7990
+ // ErrorUtils, HermesInternal, console, fetch, XMLHttpRequest: crash capture and breadcrumbs.
7991
+ globals: globalThis
6956
7992
  };
6957
7993
  }
6958
7994
  function nativeCapturer() {
@@ -6960,6 +7996,20 @@ function nativeCapturer() {
6960
7996
  if (!mod || typeof mod.capture !== "function") return void 0;
6961
7997
  return (options) => mod.capture(options);
6962
7998
  }
7999
+ function latestErrorOptions(initial, latest) {
8000
+ if (!initial) return void 0;
8001
+ return {
8002
+ ...initial,
8003
+ beforeSend: (event) => {
8004
+ const hook = latest()?.beforeSend;
8005
+ return hook ? hook(event) : event;
8006
+ },
8007
+ beforeBreadcrumb: (crumb, hint) => {
8008
+ const hook = latest()?.beforeBreadcrumb;
8009
+ return hook ? hook(crumb, hint) : crumb;
8010
+ }
8011
+ };
8012
+ }
6963
8013
  function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPayload, flagAttributes, ...options }) {
6964
8014
  const [client, setClient] = (0, import_react10.useState)(null);
6965
8015
  const optionsRef = (0, import_react10.useRef)(options);
@@ -6990,7 +8040,8 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
6990
8040
  user: userRef.current?.id ? userRef.current : void 0,
6991
8041
  flags: latest.flags === false ? false : { ...latest.flags ?? {}, payload: payloadRef.current ?? void 0, attributes: appAttributesRef.current },
6992
8042
  onNavigate: (url) => optionsRef.current.onNavigate?.(url),
6993
- onError: (err) => optionsRef.current.onError?.(err)
8043
+ onError: (err) => optionsRef.current.onError?.(err),
8044
+ errors: latestErrorOptions(latest.errors, () => optionsRef.current.errors)
6994
8045
  },
6995
8046
  defaultBridge(latest.appVersion, latest.appBuild)
6996
8047
  );
@@ -7053,6 +8104,8 @@ var NOOP_RESULTS = {
7053
8104
  isRecordingTouches: false,
7054
8105
  getSessionId: "",
7055
8106
  getScreen: "",
8107
+ captureException: "",
8108
+ captureMessage: "",
7056
8109
  on: () => {
7057
8110
  },
7058
8111
  subscribeUi: () => () => {
@@ -7172,6 +8225,7 @@ function useActiveGuide() {
7172
8225
  // Annotate the CommonJS export names for ESM import in node:
7173
8226
  0 && (module.exports = {
7174
8227
  DEFAULT_API_URL,
8228
+ FATAL_FLUSH_MS,
7175
8229
  Feature,
7176
8230
  FeatureFlag,
7177
8231
  FeatureValue,
@@ -7196,11 +8250,15 @@ function useActiveGuide() {
7196
8250
  createFlagsClient,
7197
8251
  createSseParser,
7198
8252
  detectAppVersion,
8253
+ detectDeviceModel,
7199
8254
  detectOsVersion,
8255
+ detectReactNativeVersion,
7200
8256
  guidelyScreenLayout,
7201
8257
  htmlToText,
7202
8258
  noopClient,
8259
+ normalizeBundleFile,
7203
8260
  parseHtml,
8261
+ parseReactNativeStack,
7204
8262
  positionCard,
7205
8263
  targetIdsFor,
7206
8264
  useActiveGuide,