@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.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-7PWYIAJI.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;
@@ -1032,7 +1100,22 @@ var KEYS = {
1032
1100
  launcher: (key) => `guidely:launcher:${key}`
1033
1101
  };
1034
1102
 
1035
- // ../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
+ }
1036
1119
  var EventQueue = class {
1037
1120
  constructor(opts) {
1038
1121
  __publicField2(this, "opts", opts);
@@ -1088,7 +1171,7 @@ var EventQueue = class {
1088
1171
  this.schedule();
1089
1172
  }
1090
1173
  persist() {
1091
- writeJson(this.opts.store, this.opts.storageKey, this.events);
1174
+ writeJson(this.opts.store, this.opts.storageKey, fitPersisted(this.events));
1092
1175
  }
1093
1176
  schedule() {
1094
1177
  if (this.timer || this.retryTimer || this.destroyed) return;
@@ -1338,6 +1421,13 @@ function dismissedAfterMerge(local, server) {
1338
1421
  if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
1339
1422
  return local?.dismissedAt ?? server.dismissedAt ?? null;
1340
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
+ }
1341
1431
 
1342
1432
  // src/core/client.ts
1343
1433
  import { createFlagsClient as createFlagsClient2 } from "@guidelytech/flags";
@@ -1494,6 +1584,593 @@ var TapTracker = class {
1494
1584
  }
1495
1585
  };
1496
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
+
1497
2174
  // src/core/targets.ts
1498
2175
  var TargetTimeoutError = class extends Error {
1499
2176
  constructor(ids) {
@@ -2824,38 +3501,38 @@ var SessionRecorder = class {
2824
3501
  this.capturing = true;
2825
3502
  this.captureSoon = false;
2826
3503
  const gen = this.generation;
2827
- let frame = null;
3504
+ let frame2 = null;
2828
3505
  try {
2829
- frame = await this.opts.capturer(await this.opts.captureOptions());
3506
+ frame2 = await this.opts.capturer(await this.opts.captureOptions());
2830
3507
  } catch (err) {
2831
3508
  this.opts.onError?.(err);
2832
3509
  }
2833
3510
  if (gen !== this.generation) return;
2834
3511
  this.capturing = false;
2835
- if (frame) this.onFrame(frame, this.now());
3512
+ if (frame2) this.onFrame(frame2, this.now());
2836
3513
  if (this.sid !== null && this.foreground) this.schedule(this.captureSoon ? TOUCH_SETTLE_MS : this.currentIntervalMs);
2837
3514
  }
2838
- onFrame(frame, at) {
3515
+ onFrame(frame2, at) {
2839
3516
  const id = this.sid;
2840
- if (id === null || !(frame.width > 0) || !(frame.height > 0) || !frame.base64) return;
2841
- 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;
2842
3519
  const keyframe = this.needKeyframe || !this.hasKeyframe || sizeChanged;
2843
- if (!keyframe && this.lastFrame === frame.base64) {
3520
+ if (!keyframe && this.lastFrame === frame2.base64) {
2844
3521
  this.unchanged++;
2845
3522
  return;
2846
3523
  }
2847
3524
  this.unchanged = 0;
2848
- this.lastFrame = frame.base64;
2849
- this.originX = frame.originX ?? 0;
2850
- this.originY = frame.originY ?? 0;
2851
- this.viewportWidth = frame.width;
2852
- this.viewportHeight = frame.height;
2853
- 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);
2854
3531
  const up = this.opts.uploader;
2855
3532
  if (!keyframe) return up.push(id, RrwebEvents.frame(at, url));
2856
3533
  this.needKeyframe = false;
2857
- up.push(id, RrwebEvents.meta(at, this.opts.href(), frame.width, frame.height));
2858
- 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));
2859
3536
  if (!this.hasKeyframe) {
2860
3537
  this.hasKeyframe = true;
2861
3538
  for (const [ts, payload] of this.pendingCustom) up.push(id, RrwebEvents.custom(Math.max(ts, at), "guidely", payload));
@@ -2917,7 +3594,7 @@ var PersistentStore = class {
2917
3594
 
2918
3595
  // src/core/client.ts
2919
3596
  var SDK_NAME = "@guidelytech/react-native";
2920
- var SDK_VERSION = "0.16.0";
3597
+ var SDK_VERSION = "0.17.0";
2921
3598
  var DEFAULT_API_URL = "https://guidelytech.com";
2922
3599
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
2923
3600
  var CACHED_START_MS = 1500;
@@ -2932,6 +3609,10 @@ function settlesWithin(promise, ms) {
2932
3609
  });
2933
3610
  }
2934
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;
2935
3616
  var UNKNOWN_VIEW = { selector: "View", tag: "View" };
2936
3617
  var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
2937
3618
  var emptyState = () => ({ guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] });
@@ -2948,6 +3629,8 @@ var GuidelyClient = class {
2948
3629
  /** Survey answers, guide completions/dismissals and checklist progress, kept until the server has them. */
2949
3630
  __publicField(this, "outbox", null);
2950
3631
  __publicField(this, "config", null);
3632
+ /** The `environment` option as passed (the resolved one is in `options.environment`). */
3633
+ __publicField(this, "envOption");
2951
3634
  __publicField(this, "configFetchedAt", 0);
2952
3635
  __publicField(this, "etag", null);
2953
3636
  __publicField(this, "anonymousId", "");
@@ -3011,6 +3694,16 @@ var GuidelyClient = class {
3011
3694
  __publicField(this, "localizedRc", null);
3012
3695
  /** "What's new" last-seen time when the resource center opened (entries after it read as new). */
3013
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);
3014
3707
  __publicField(this, "customListeners", /* @__PURE__ */ new Map());
3015
3708
  __publicField(this, "eventBus", {
3016
3709
  on: (name, cb) => {
@@ -3024,9 +3717,10 @@ var GuidelyClient = class {
3024
3717
  }
3025
3718
  });
3026
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;
3027
3721
  this.options = {
3028
3722
  ...options,
3029
- environment: options.environment ?? (options.projectId.startsWith("pk_prod") ? "production" : options.projectId.startsWith("pk_stag") ? "staging" : "development"),
3723
+ environment: options.environment || environmentFromKey(options.projectId),
3030
3724
  apiUrl: options.apiUrl ?? DEFAULT_API_URL,
3031
3725
  analytics: options.analytics ?? true,
3032
3726
  configRefreshMs: options.configRefreshMs ?? 5 * 60 * 1e3,
@@ -3042,6 +3736,7 @@ var GuidelyClient = class {
3042
3736
  if (options.user) this.user = { ...options.user };
3043
3737
  this.locale = options.locale ? normalizeLocale(options.locale) || null : null;
3044
3738
  this.flags = this.createFlags(options.flags);
3739
+ this.installErrorMonitoring();
3045
3740
  }
3046
3741
  createFlags(opts) {
3047
3742
  const base = { sdkKey: this.options.projectId, apiUrl: this.options.apiUrl, fetch: this.options.fetch, sdk: { name: SDK_NAME, version: SDK_VERSION } };
@@ -3089,7 +3784,7 @@ var GuidelyClient = class {
3089
3784
  async bootstrap() {
3090
3785
  const key = this.options.projectId;
3091
3786
  try {
3092
- 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)]);
3093
3788
  if (this.destroyed) return;
3094
3789
  this.anonymousId = this.store.get(KEYS.anon) || "";
3095
3790
  this.hadIdentity = !!this.anonymousId;
@@ -3102,7 +3797,9 @@ var GuidelyClient = class {
3102
3797
  this.state = { ...emptyState(), ...readJson(this.store, KEYS.state(key), {}) };
3103
3798
  this.user = this.options.user ? { ...this.options.user } : readJson(this.store, KEYS.user(key), null);
3104
3799
  const saved = readJson(this.store, KEYS.session, null);
3105
- 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();
3106
3803
  this.persistSession();
3107
3804
  this.lastAudiences = new Set(this.state.audienceIds);
3108
3805
  this.syncFlagAttributes();
@@ -3121,9 +3818,10 @@ var GuidelyClient = class {
3121
3818
  this.hydrated = true;
3122
3819
  const cached = readJson(this.store, KEYS.config(key), null);
3123
3820
  this.etag = this.store.get(KEYS.etag(key));
3124
- 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));
3125
3822
  this.syncMessenger();
3126
3823
  this.pending.splice(0).forEach((fn) => fn());
3824
+ this.drainErrors();
3127
3825
  const before = this.config?.configVersion;
3128
3826
  const fresh = this.fetchConfig();
3129
3827
  if (!this.config) await fresh;
@@ -3148,22 +3846,33 @@ var GuidelyClient = class {
3148
3846
  if (this.hydrated) fn();
3149
3847
  else this.pending.push(fn);
3150
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
+ }
3151
3858
  async fetchConfig() {
3152
3859
  try {
3153
3860
  const res = await this.api.get("/config", { etag: this.etag, params: { platform: this.bridge.device.platform } });
3154
3861
  this.configFetchedAt = Date.now();
3155
3862
  if (res.status === 304) return true;
3156
3863
  if (res.ok && res.data && Array.isArray(res.data.guides)) {
3157
- this.config = forPlatform(res.data, this.bridge.device.platform);
3864
+ const config = forPlatform(res.data, this.bridge.device.platform);
3865
+ this.useConfig(config);
3158
3866
  this.etag = res.etag;
3159
3867
  writeJson(this.store, KEYS.config(this.options.projectId), res.data);
3160
3868
  if (res.etag) this.store.set(KEYS.etag(this.options.projectId), res.etag);
3161
3869
  else this.store.remove(KEYS.etag(this.options.projectId));
3162
3870
  this.syncMessenger();
3163
3871
  if (this.hydrated) this.checkLaunch();
3164
- this.emitter.emit("config", this.config);
3872
+ this.drainErrors();
3873
+ this.emitter.emit("config", config);
3165
3874
  this.syncReplay();
3166
- this.log("config loaded", this.config.configVersion, `${this.config.guides.length} guides`);
3875
+ this.log("config loaded", config.configVersion, `${config.guides.length} guides`);
3167
3876
  return true;
3168
3877
  }
3169
3878
  this.reportError(new Error(`[Guidely] config request failed (${res.status})${this.config ? "; using cached config" : ""}`));
@@ -3204,7 +3913,12 @@ var GuidelyClient = class {
3204
3913
  }
3205
3914
  /** Called by the provider with React Native `AppState` changes. */
3206
3915
  setAppState(state) {
3207
- 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;
3208
3922
  if (state === "background") {
3209
3923
  const wasForeground = this.backgroundedAt === null;
3210
3924
  this.backgroundedAt = Date.now();
@@ -3389,8 +4103,13 @@ var GuidelyClient = class {
3389
4103
  setConsent(consent) {
3390
4104
  this.consent = consent;
3391
4105
  this.whenHydrated(() => {
3392
- if (consent === "denied") this.queue?.clear();
3393
- 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();
3394
4113
  this.syncReplay();
3395
4114
  });
3396
4115
  }
@@ -3414,6 +4133,7 @@ var GuidelyClient = class {
3414
4133
  accountId: this.user?.account?.id || null,
3415
4134
  sessionId: this.session.id,
3416
4135
  url: this.collectUrls() && this.screenName ? this.currentUrl() : void 0,
4136
+ ...this.appInfo().appVersion ? { appVersion: this.appInfo().appVersion } : {},
3417
4137
  ...extra
3418
4138
  };
3419
4139
  if (extra.properties) event.properties = sanitizeProperties(extra.properties);
@@ -3453,6 +4173,7 @@ var GuidelyClient = class {
3453
4173
  this.whenHydrated(() => {
3454
4174
  const changed = name !== this.screenName;
3455
4175
  if (changed) {
4176
+ this.autoBreadcrumb("navigation", { type: "navigation", category: "navigation", data: { ...this.screenName ? { from: this.screenName } : {}, to: name } });
3456
4177
  this.leaveScreen();
3457
4178
  this.flushHeatmap();
3458
4179
  }
@@ -3616,7 +4337,11 @@ var GuidelyClient = class {
3616
4337
  }
3617
4338
  /** True when taps are worth observing (clicks, rage taps or heatmaps are on); the provider skips resolving views otherwise. */
3618
4339
  isTapCaptureOn() {
3619
- 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();
3620
4345
  }
3621
4346
  /**
3622
4347
  * Records a tap observed by the provider: `click` for an interactive element, `rage_click` for
@@ -3626,8 +4351,8 @@ var GuidelyClient = class {
3626
4351
  recordTap(tap) {
3627
4352
  try {
3628
4353
  if (!this.hydrated || this.destroyed || !Number.isFinite(tap.x) || !Number.isFinite(tap.y)) return;
3629
- const on = { clicks: this.autocaptureOn("clicks"), rage: this.autocaptureOn("rageClicks"), heat: this.autocaptureOn("heatmaps") };
3630
- 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;
3631
4356
  const t = tap.t ?? Date.now();
3632
4357
  const url = this.screenName ? this.currentUrl() : "";
3633
4358
  this.tapChain = this.tapChain.then(() => this.processTap(tap, t, url, on)).catch((err) => this.reportError(err));
@@ -3644,6 +4369,8 @@ var GuidelyClient = class {
3644
4369
  rect = target.rect;
3645
4370
  }
3646
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;
3647
4374
  const platform = this.bridge.device.platform;
3648
4375
  const maskAll = !!this.config?.settings?.replay?.maskAllText || !!this.options.sessionReplay?.maskAllText;
3649
4376
  const eventUrl = this.collectUrls() && url ? url : void 0;
@@ -3742,6 +4469,277 @@ var GuidelyClient = class {
3742
4469
  }
3743
4470
  }
3744
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
+ // ---------------------------------------------------------------------------
3745
4743
  // Targeting + triggers
3746
4744
  // ---------------------------------------------------------------------------
3747
4745
  /** Builds the targeting context for the current moment. */
@@ -3767,7 +4765,7 @@ var GuidelyClient = class {
3767
4765
  completedChecklists: new Set(keysOf(Object.entries(this.state.checklists).filter(([, p]) => !!p.completedAt).map(([id]) => id))),
3768
4766
  audienceIds: new Set(this.state.audienceIds),
3769
4767
  now: /* @__PURE__ */ new Date(),
3770
- environment: this.options.environment,
4768
+ environment: this.environment,
3771
4769
  locale: device.locale,
3772
4770
  platform: device.platform,
3773
4771
  screen: this.screenName,
@@ -4588,7 +5586,7 @@ var GuidelyClient = class {
4588
5586
  locale: d.locale,
4589
5587
  appVersion: this.options.appVersion ?? d.appVersion,
4590
5588
  osVersion: d.osVersion,
4591
- environment: this.options.environment,
5589
+ environment: this.environment,
4592
5590
  ...this.screenName ? { screen: this.screenName, path: this.currentUrl() } : {},
4593
5591
  ...this.user?.traits ?? {},
4594
5592
  id: this.user?.id || this.anonymousId || void 0,
@@ -4713,6 +5711,35 @@ function detectAppVersion(src) {
4713
5711
  return {};
4714
5712
  }
4715
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
+ }
4716
5743
  function detectOsVersion(src) {
4717
5744
  try {
4718
5745
  const c = src.platform.constants ?? {};
@@ -4845,7 +5872,7 @@ function measureHost(host) {
4845
5872
  done(null);
4846
5873
  });
4847
5874
  }
4848
- function describe(f, masked, opts, withText) {
5875
+ function describe2(f, masked, opts, withText) {
4849
5876
  const props = f.memoizedProps && typeof f.memoizedProps === "object" ? f.memoizedProps : {};
4850
5877
  const host = firstHost(f);
4851
5878
  const el = { selector: idSelector(f) ?? pathSelector(f), tag: componentName(f.type) ?? "View" };
@@ -4890,8 +5917,8 @@ function resolveFiber(target, opts = {}) {
4890
5917
  const element = press ?? role;
4891
5918
  return {
4892
5919
  ignored: false,
4893
- element: element ? describe(element, masked, opts, true) : null,
4894
- 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
4895
5922
  };
4896
5923
  } catch {
4897
5924
  return none;
@@ -5024,7 +6051,7 @@ function parseHtml(html) {
5024
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;
5025
6052
  else current.runs.push(run);
5026
6053
  };
5027
- const size = (attrs, name) => {
6054
+ const size2 = (attrs, name) => {
5028
6055
  const n = Number(attr(attrs, name));
5029
6056
  return n > 0 ? n : void 0;
5030
6057
  };
@@ -5086,8 +6113,8 @@ function parseHtml(html) {
5086
6113
  }
5087
6114
  } else if (tag === "img" && !closing) {
5088
6115
  const src = attr(attrs, "src");
5089
- const w = size(attrs, "width");
5090
- const h = size(attrs, "height");
6116
+ const w = size2(attrs, "width");
6117
+ const h = size2(attrs, "height");
5091
6118
  if (isSafeMediaUrl(src)) pdf.pages.push({ src, alt: attr(attrs, "alt") ?? "", ...w && h ? { width: w, height: h } : {} });
5092
6119
  }
5093
6120
  continue;
@@ -5261,10 +6288,10 @@ function BodyPdf({ block, palette: p, onLink }) {
5261
6288
  ] }) })
5262
6289
  ] });
5263
6290
  }
5264
- function RichText({ html, palette: p, onLink, size = 15 }) {
6291
+ function RichText({ html, palette: p, onLink, size: size2 = 15 }) {
5265
6292
  const blocks = useMemo2(() => parseHtml(html), [html]);
5266
6293
  if (!blocks.length) return null;
5267
- 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 };
5268
6295
  return /* @__PURE__ */ jsx3(View2, { style: styles2.wrap, children: blocks.map((b, i) => {
5269
6296
  if (b.kind === "image") return /* @__PURE__ */ jsx3(BodyImage, { block: b, palette: p }, i);
5270
6297
  if (b.kind === "video") return /* @__PURE__ */ jsx3(BodyVideo, { block: b, palette: p, onLink }, i);
@@ -5467,17 +6494,17 @@ function useReduceMotion() {
5467
6494
  }, []);
5468
6495
  return on;
5469
6496
  }
5470
- function MessengerLauncher({ store, client, palette: p, style, frame, defaultSide }) {
6497
+ function MessengerLauncher({ store, client, palette: p, style, frame: frame2, defaultSide }) {
5471
6498
  const snap = useMessenger(store);
5472
6499
  const reduceMotion = useReduceMotion();
5473
6500
  const [placement, setPlacement] = useState3(() => client.getMessengerLauncherPlacement());
5474
6501
  const pan = useRef2(new Animated.ValueXY()).current;
5475
6502
  const offset = useRef2({ x: 0, y: 0 });
5476
6503
  const dragging = useRef2(false);
5477
- const live = useRef2({ frame, defaultSide, reduceMotion });
5478
- live.current = { frame, defaultSide, reduceMotion };
5479
- const { width, height, size, defaultBottom } = frame;
5480
- 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;
5481
6508
  useLayoutEffect(() => {
5482
6509
  if (dragging.current) return;
5483
6510
  const f = live.current.frame;
@@ -5485,7 +6512,7 @@ function MessengerLauncher({ store, client, palette: p, style, frame, defaultSid
5485
6512
  const at = placement ? placementPoint(f, placement) : anchor;
5486
6513
  offset.current = { x: at.x - anchor.x, y: at.y - anchor.y };
5487
6514
  pan.setValue(offset.current);
5488
- }, [pan, placement, defaultSide, width, height, size, defaultBottom, top, bottom, left, right]);
6515
+ }, [pan, placement, defaultSide, width, height, size2, defaultBottom, top, bottom, left, right]);
5489
6516
  const responder = useMemo3(() => {
5490
6517
  const moved = (g) => Math.hypot(g.dx, g.dy) > LAUNCHER_DRAG_THRESHOLD;
5491
6518
  const anchor = () => placementPoint(live.current.frame, { side: live.current.defaultSide, fraction: 0 });
@@ -6009,18 +7036,18 @@ import { AccessibilityInfo as AccessibilityInfo2, Animated as Animated2, Easing
6009
7036
  var MARGIN = 12;
6010
7037
  var ARROW_INSET = 18;
6011
7038
  var opposite = { top: "bottom", bottom: "top", left: "right", right: "left" };
6012
- function positionCard(target, size, viewport, insets, placement, offset) {
7039
+ function positionCard(target, size2, viewport, insets, placement, offset) {
6013
7040
  const [rawSide, align] = placement.split("-");
6014
7041
  const fits = (side2) => {
6015
7042
  switch (side2) {
6016
7043
  case "bottom":
6017
- 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;
6018
7045
  case "top":
6019
- return target.y - offset - size.height >= insets.top + MARGIN;
7046
+ return target.y - offset - size2.height >= insets.top + MARGIN;
6020
7047
  case "right":
6021
- 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;
6022
7049
  case "left":
6023
- return target.x - offset - size.width >= insets.left + MARGIN;
7050
+ return target.x - offset - size2.width >= insets.left + MARGIN;
6024
7051
  }
6025
7052
  };
6026
7053
  const order = rawSide === "auto" ? ["bottom", "top"] : [rawSide, opposite[rawSide], "bottom", "top"];
@@ -6029,17 +7056,17 @@ function positionCard(target, size, viewport, insets, placement, offset) {
6029
7056
  const clamp3 = (v, min, max) => Math.max(min, Math.min(max, v));
6030
7057
  if (side === "top" || side === "bottom") {
6031
7058
  const cx = target.x + target.width / 2;
6032
- let x2 = align === "start" ? target.x : align === "end" ? target.x + target.width - size.width : cx - size.width / 2;
6033
- x2 = clamp3(x2, insets.left + MARGIN, Math.max(insets.left + MARGIN, viewport.width - insets.right - MARGIN - size.width));
6034
- let y2 = side === "bottom" ? target.y + target.height + offset : target.y - offset - size.height;
6035
- y2 = clamp3(y2, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
6036
- 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) };
6037
7064
  }
6038
7065
  const cy = target.y + target.height / 2;
6039
- let y = align === "start" ? target.y : align === "end" ? target.y + target.height - size.height : cy - size.height / 2;
6040
- y = clamp3(y, insets.top + MARGIN, Math.max(insets.top + MARGIN, viewport.height - insets.bottom - MARGIN - size.height));
6041
- const x = side === "right" ? target.x + target.width + offset : target.x - offset - size.width;
6042
- 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) };
6043
7070
  }
6044
7071
 
6045
7072
  // src/ui/StepCard.tsx
@@ -6193,9 +7220,9 @@ function StepLayer({ view, client, palette: p, geometry }) {
6193
7220
  }
6194
7221
  if (!rect) return null;
6195
7222
  if (step.type === "hotspot") {
6196
- const size = step.hotspot?.size ?? 14;
7223
+ const size2 = step.hotspot?.size ?? 14;
6197
7224
  return /* @__PURE__ */ jsxs6(View7, { pointerEvents: "box-none", style: StyleSheet7.absoluteFill, children: [
6198
- /* @__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" }),
6199
7226
  view.expanded ? /* @__PURE__ */ jsx8(Anchored, { view, client, palette: p, geometry, rect, width: cardWidth }) : null
6200
7227
  ] });
6201
7228
  }
@@ -6242,7 +7269,7 @@ function Arrow({ side, at, color, border }) {
6242
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 };
6243
7270
  return /* @__PURE__ */ jsx8(View7, { pointerEvents: "none", style: [styles7.abs, styles7.arrow, place, { backgroundColor: color, borderColor: border }] });
6244
7271
  }
6245
- function Hotspot({ x, y, size, pulse, color, onPress, label }) {
7272
+ function Hotspot({ x, y, size: size2, pulse, color, onPress, label }) {
6246
7273
  const scale = useRef3(new Animated2.Value(0)).current;
6247
7274
  const [reduceMotion, setReduceMotion] = useState5(false);
6248
7275
  useEffect5(() => {
@@ -6259,15 +7286,15 @@ function Hotspot({ x, y, size, pulse, color, onPress, label }) {
6259
7286
  loop.start();
6260
7287
  return () => loop.stop();
6261
7288
  }, [pulse, reduceMotion, scale]);
6262
- return /* @__PURE__ */ jsxs6(Pressable7, { 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: [
6263
7290
  pulse && !reduceMotion ? /* @__PURE__ */ jsx8(
6264
7291
  Animated2.View,
6265
7292
  {
6266
7293
  pointerEvents: "none",
6267
- 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] }) }] }]
6268
7295
  }
6269
7296
  ) : null,
6270
- /* @__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" } })
6271
7298
  ] });
6272
7299
  }
6273
7300
  var styles7 = StyleSheet7.create({
@@ -6888,12 +7915,16 @@ function defaultBridge(appVersion, appBuild) {
6888
7915
  locale: deviceLocale(),
6889
7916
  appVersion: appVersion ?? detected.appVersion,
6890
7917
  appBuild: appBuild ?? detected.appBuild,
6891
- osVersion: detectOsVersion(sources)
7918
+ osVersion: detectOsVersion(sources),
7919
+ ...detectDeviceModel(sources),
7920
+ reactNativeVersion: detectReactNativeVersion(sources)
6892
7921
  },
6893
7922
  openExternal: (url) => void Linking.openURL(url).catch(() => {
6894
7923
  }),
6895
7924
  windowSize: () => Dimensions.get("window"),
6896
- capturer: nativeCapturer()
7925
+ capturer: nativeCapturer(),
7926
+ // ErrorUtils, HermesInternal, console, fetch, XMLHttpRequest: crash capture and breadcrumbs.
7927
+ globals: globalThis
6897
7928
  };
6898
7929
  }
6899
7930
  function nativeCapturer() {
@@ -6901,6 +7932,20 @@ function nativeCapturer() {
6901
7932
  if (!mod || typeof mod.capture !== "function") return void 0;
6902
7933
  return (options) => mod.capture(options);
6903
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
+ }
6904
7949
  function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPayload, flagAttributes, ...options }) {
6905
7950
  const [client, setClient] = useState8(null);
6906
7951
  const optionsRef = useRef7(options);
@@ -6931,7 +7976,8 @@ function GuidelyProvider({ children, user, insets, launcherOffset = 72, flagsPay
6931
7976
  user: userRef.current?.id ? userRef.current : void 0,
6932
7977
  flags: latest.flags === false ? false : { ...latest.flags ?? {}, payload: payloadRef.current ?? void 0, attributes: appAttributesRef.current },
6933
7978
  onNavigate: (url) => optionsRef.current.onNavigate?.(url),
6934
- onError: (err) => optionsRef.current.onError?.(err)
7979
+ onError: (err) => optionsRef.current.onError?.(err),
7980
+ errors: latestErrorOptions(latest.errors, () => optionsRef.current.errors)
6935
7981
  },
6936
7982
  defaultBridge(latest.appVersion, latest.appBuild)
6937
7983
  );
@@ -6994,6 +8040,8 @@ var NOOP_RESULTS = {
6994
8040
  isRecordingTouches: false,
6995
8041
  getSessionId: "",
6996
8042
  getScreen: "",
8043
+ captureException: "",
8044
+ captureMessage: "",
6997
8045
  on: () => {
6998
8046
  },
6999
8047
  subscribeUi: () => () => {
@@ -7112,6 +8160,7 @@ function useActiveGuide() {
7112
8160
  }
7113
8161
  export {
7114
8162
  DEFAULT_API_URL,
8163
+ FATAL_FLUSH_MS,
7115
8164
  Feature,
7116
8165
  FeatureFlag,
7117
8166
  FeatureValue,
@@ -7136,11 +8185,15 @@ export {
7136
8185
  createFlagsClient3 as createFlagsClient,
7137
8186
  createSseParser,
7138
8187
  detectAppVersion,
8188
+ detectDeviceModel,
7139
8189
  detectOsVersion,
8190
+ detectReactNativeVersion,
7140
8191
  guidelyScreenLayout,
7141
8192
  htmlToText,
7142
8193
  noopClient,
8194
+ normalizeBundleFile,
7143
8195
  parseHtml,
8196
+ parseReactNativeStack,
7144
8197
  positionCard,
7145
8198
  targetIdsFor,
7146
8199
  useActiveGuide,