@guidelytech/sdk-core 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
@@ -51,6 +51,7 @@ __export(index_exports, {
51
51
  createStorage: () => createStorage,
52
52
  default: () => index_default,
53
53
  detectDevice: () => detectDevice,
54
+ environmentFromKey: () => environmentFromKey,
54
55
  isAppUrl: () => isAppUrl,
55
56
  isExclusiveGuide: () => isExclusiveGuide,
56
57
  isFrequencyEligible: () => isFrequencyEligible,
@@ -64,7 +65,7 @@ __export(index_exports, {
64
65
  module.exports = __toCommonJS(index_exports);
65
66
 
66
67
  // src/client.ts
67
- var import_runtime18 = require("@guidelytech/shared/runtime");
68
+ var import_runtime23 = require("@guidelytech/shared/runtime");
68
69
  var import_guide_runtime5 = require("@guidelytech/guide-runtime");
69
70
  var import_guide_runtime6 = require("@guidelytech/guide-runtime");
70
71
  var import_flags = require("@guidelytech/flags");
@@ -103,14 +104,14 @@ var ServerHealth = class {
103
104
  /** The request failed or the server is overloaded. `retryAfterMs` comes from a `Retry-After` header. */
104
105
  failure(retryAfterMs) {
105
106
  this.failures += 1;
106
- const now = this.now();
107
+ const now3 = this.now();
107
108
  if (retryAfterMs != null && retryAfterMs > 0) {
108
- this.openUntil = Math.max(this.openUntil, now + Math.min(retryAfterMs, MAX_RETRY_AFTER_MS));
109
+ this.openUntil = Math.max(this.openUntil, now3 + Math.min(retryAfterMs, MAX_RETRY_AFTER_MS));
109
110
  return;
110
111
  }
111
112
  if (this.failures < this.threshold) return;
112
113
  const cooldown = Math.min(this.maxMs, this.baseMs * 2 ** (this.failures - this.threshold));
113
- this.openUntil = Math.max(this.openUntil, now + cooldown * (0.8 + this.random() * 0.4));
114
+ this.openUntil = Math.max(this.openUntil, now3 + cooldown * (0.8 + this.random() * 0.4));
114
115
  }
115
116
  /** Milliseconds background traffic should wait before trying again (0: send now). */
116
117
  waitMs() {
@@ -146,12 +147,12 @@ var ServerHealth = class {
146
147
  }
147
148
  }
148
149
  };
149
- function parseRetryAfter(value, now = Date.now()) {
150
+ function parseRetryAfter(value, now3 = Date.now()) {
150
151
  if (!value) return null;
151
152
  const trimmed = value.trim();
152
153
  if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
153
154
  const at = Date.parse(trimmed);
154
- return Number.isNaN(at) ? null : Math.max(0, at - now);
155
+ return Number.isNaN(at) ? null : Math.max(0, at - now3);
155
156
  }
156
157
  function isServerFailure(status) {
157
158
  return status === 429 || status >= 500;
@@ -243,10 +244,10 @@ var Api = class {
243
244
  if (!f) throw new Error("fetch is not available");
244
245
  const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
245
246
  let timer = null;
246
- const arm = (ms) => {
247
+ const arm = (ms2) => {
247
248
  if (!ac) return;
248
249
  if (timer) clearTimeout(timer);
249
- timer = setTimeout(() => ac.abort(), ms);
250
+ timer = setTimeout(() => ac.abort(), ms2);
250
251
  };
251
252
  arm(STREAM_RESPONSE_TIMEOUT_MS);
252
253
  try {
@@ -293,7 +294,7 @@ var Api = class {
293
294
  }
294
295
  }
295
296
  };
296
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
297
+ var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
297
298
  function backoffMs(attempt, baseMs = 1e3, maxMs = 3e4) {
298
299
  return Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt));
299
300
  }
@@ -467,13 +468,13 @@ var Outbox = class {
467
468
  persist() {
468
469
  writeJson(this.opts.store, this.opts.storageKey, this.items);
469
470
  }
470
- retryIn(ms) {
471
+ retryIn(ms2) {
471
472
  if (this.destroyed) return;
472
473
  if (this.timer) clearTimeout(this.timer);
473
474
  this.timer = setTimeout(() => {
474
475
  this.timer = null;
475
476
  void this.flush();
476
- }, ms);
477
+ }, ms2);
477
478
  }
478
479
  /** Sends queued requests in order, one at a time. */
479
480
  async flush() {
@@ -1046,10 +1047,10 @@ var AuthoringBridge = class {
1046
1047
  this.observer.observe(document.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });
1047
1048
  }
1048
1049
  scheduleSnapshot() {
1049
- const now = Date.now();
1050
- if (!this.snapshotTimer) this.snapshotDue = now + SNAPSHOT_MAX_WAIT_MS;
1050
+ const now3 = Date.now();
1051
+ if (!this.snapshotTimer) this.snapshotDue = now3 + SNAPSHOT_MAX_WAIT_MS;
1051
1052
  else clearTimeout(this.snapshotTimer);
1052
- this.snapshotTimer = setTimeout(() => this.sendSnapshot(), Math.max(0, Math.min(SNAPSHOT_DEBOUNCE_MS, this.snapshotDue - now)));
1053
+ this.snapshotTimer = setTimeout(() => this.sendSnapshot(), Math.max(0, Math.min(SNAPSHOT_DEBOUNCE_MS, this.snapshotDue - now3)));
1053
1054
  }
1054
1055
  sendSnapshot() {
1055
1056
  if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
@@ -1354,12 +1355,12 @@ function describe(value) {
1354
1355
  }
1355
1356
  }
1356
1357
  function exceptionFromError(error, source, handled, fallbackType = "Error") {
1357
- const isError = error instanceof Error || !!error && typeof error === "object" && typeof error.message === "string" && typeof error.stack === "string";
1358
+ const isError2 = error instanceof Error || !!error && typeof error === "object" && typeof error.message === "string" && typeof error.stack === "string";
1358
1359
  const e = error;
1359
1360
  return {
1360
- type: isError && e.name || fallbackType,
1361
- message: (isError ? e.message : describe(error)).slice(0, 1e3),
1362
- frames: isError ? fitFrames((0, import_runtime11.parseStack)(e.stack)) : [],
1361
+ type: isError2 && e.name || fallbackType,
1362
+ message: (isError2 ? e.message : describe(error)).slice(0, 1e3),
1363
+ frames: isError2 ? fitFrames((0, import_runtime11.parseStack)(e.stack)) : [],
1363
1364
  source,
1364
1365
  handled
1365
1366
  };
@@ -1409,6 +1410,222 @@ var ExceptionThrottle = class {
1409
1410
  }
1410
1411
  };
1411
1412
 
1413
+ // src/error-monitoring.ts
1414
+ var import_runtime13 = require("@guidelytech/shared/runtime");
1415
+ var CONTEXT_KEYS = ["tags", "extra", "level", "fingerprint", "contexts", "user", "mechanism"];
1416
+ function splitHint(hint) {
1417
+ const ctx = {};
1418
+ const properties = {};
1419
+ for (const [k, v] of Object.entries(hint ?? {})) (CONTEXT_KEYS.includes(k) ? ctx : properties)[k] = v;
1420
+ return { ctx, properties };
1421
+ }
1422
+ var now = () => Date.now() / 1e3;
1423
+ var ErrorScope = class {
1424
+ constructor(opts) {
1425
+ __publicField(this, "opts", opts);
1426
+ __publicField(this, "tags", {});
1427
+ __publicField(this, "extra", {});
1428
+ __publicField(this, "contexts", {});
1429
+ __publicField(this, "user", {});
1430
+ __publicField(this, "crumbs", []);
1431
+ }
1432
+ get max() {
1433
+ return Math.max(0, Math.min(import_runtime13.MAX_BREADCRUMBS, this.opts()?.maxBreadcrumbs ?? import_runtime13.MAX_BREADCRUMBS));
1434
+ }
1435
+ addBreadcrumb(crumb, hint) {
1436
+ let c = { timestamp: now(), ...crumb };
1437
+ const before = this.opts()?.beforeBreadcrumb;
1438
+ if (before) {
1439
+ try {
1440
+ c = before(c, hint);
1441
+ } catch {
1442
+ }
1443
+ }
1444
+ if (!c || !this.max) return;
1445
+ this.crumbs.push(c);
1446
+ if (this.crumbs.length > this.max) this.crumbs.splice(0, this.crumbs.length - this.max);
1447
+ }
1448
+ breadcrumbs() {
1449
+ return this.crumbs.slice();
1450
+ }
1451
+ clear() {
1452
+ this.tags = {};
1453
+ this.extra = {};
1454
+ this.contexts = {};
1455
+ this.user = {};
1456
+ this.crumbs = [];
1457
+ }
1458
+ };
1459
+ function toDetailFrames(frames) {
1460
+ const out = [];
1461
+ for (let i = frames.length - 1; i >= 0; i--) {
1462
+ const f = frames[i];
1463
+ out.push({ ...f.fn ? { function: f.fn } : {}, ...f.file ? { abs_path: f.file, filename: f.file } : {}, ...f.line ? { lineno: f.line } : {}, ...f.col ? { colno: f.col } : {}, ...typeof f.inApp === "boolean" ? { in_app: f.inApp } : {} });
1464
+ }
1465
+ return out;
1466
+ }
1467
+ var isError = (v) => v instanceof Error || !!v && typeof v === "object" && typeof v.message === "string" && typeof v.stack === "string";
1468
+ function exceptionChain(error, main, mechanism) {
1469
+ const chain = [{ type: main.type, value: main.message, mechanism: { type: mechanism, handled: main.handled !== false, exception_id: 0 }, stacktrace: { frames: toDetailFrames(main.frames) } }];
1470
+ const seen = /* @__PURE__ */ new Set([error]);
1471
+ let cur = isError(error) ? error.cause : void 0;
1472
+ while (cur !== void 0 && cur !== null && !seen.has(cur) && chain.length < import_runtime13.MAX_EXCEPTION_CHAIN) {
1473
+ seen.add(cur);
1474
+ const e = cur;
1475
+ const id = chain.length;
1476
+ chain.push({
1477
+ type: isError(e) && e.name || "Error",
1478
+ value: (isError(e) ? e.message : String(cur)).slice(0, 1e3),
1479
+ mechanism: { type: "chained", source: "cause", handled: true, exception_id: id, parent_id: id - 1 },
1480
+ ...isError(e) ? { stacktrace: { frames: toDetailFrames((0, import_runtime13.parseStack)(e.stack, 50)) } } : {}
1481
+ });
1482
+ cur = isError(e) ? e.cause : void 0;
1483
+ }
1484
+ return chain.reverse();
1485
+ }
1486
+ var VERSION_TOKEN = { chrome: "(?:Chrome|CriOS)", edge: "Edg", firefox: "(?:Firefox|FxiOS)", safari: "Version", opera: "OPR", samsung: "SamsungBrowser", electron: "Electron" };
1487
+ var OS_VERSION = /(?:Mac OS X|Windows NT|Android|CPU (?:iPhone )?OS) ([\d._]+)/;
1488
+ var NAMES = { chrome: "Chrome", edge: "Edge", firefox: "Firefox", safari: "Safari", opera: "Opera", samsung: "Samsung Internet", electron: "Electron", ie: "Internet Explorer", macos: "macOS", windows: "Windows", ios: "iOS", android: "Android", linux: "Linux", chromeos: "Chrome OS" };
1489
+ function browserContexts(ua, device) {
1490
+ const out = {};
1491
+ const token = device.browser ? VERSION_TOKEN[device.browser] : void 0;
1492
+ const v = token ? new RegExp(`${token}/([\\d.]+)`).exec(ua)?.[1] : void 0;
1493
+ if (device.browser) out.browser = { name: NAMES[device.browser] ?? device.browser, ...v ? { version: v } : {} };
1494
+ const ov = OS_VERSION.exec(ua)?.[1]?.replace(/_/g, ".");
1495
+ if (device.os) out.os = { name: NAMES[device.os] ?? device.os, ...ov ? { version: ov } : {} };
1496
+ out.device = { family: device.type === "desktop" ? "Desktop" : device.type === "tablet" ? "Tablet" : "Mobile" };
1497
+ return out;
1498
+ }
1499
+ function buildDetail(input) {
1500
+ const { scope, ctx } = input;
1501
+ const contexts = { ...input.contexts, ...scope.contexts };
1502
+ for (const [k, v] of Object.entries(ctx.contexts ?? {})) if (v) contexts[k] = v;
1503
+ const user = { ...scope.user, ...ctx.user ?? {} };
1504
+ const detail = {
1505
+ level: input.level,
1506
+ exception: { values: input.props.type === "Message" ? [] : exceptionChain(input.error, input.props, input.mechanism) },
1507
+ breadcrumbs: { values: scope.breadcrumbs() },
1508
+ contexts,
1509
+ tags: (0, import_runtime13.normalizeTags)({ ...scope.tags, ...ctx.tags ?? {} }),
1510
+ extra: { ...scope.extra, ...ctx.extra ?? {} },
1511
+ ...Object.keys(user).length ? { user } : {},
1512
+ ...ctx.fingerprint?.length ? { fingerprint: ctx.fingerprint.map(String) } : {}
1513
+ };
1514
+ return (0, import_runtime13.trimExceptionDetail)(detail);
1515
+ }
1516
+ var isOwnRequest = (url, apiUrl) => !!apiUrl && url.startsWith(apiUrl) && /\/api\/v1\/sdk\//.test(url);
1517
+ function breadcrumbUrl(raw) {
1518
+ try {
1519
+ const u = new URL(raw, typeof location !== "undefined" ? location.href : "http://localhost/");
1520
+ return (u.origin === "null" ? u.protocol : u.origin) + u.pathname;
1521
+ } catch {
1522
+ return raw.replace(/[?#].*$/, "");
1523
+ }
1524
+ }
1525
+ function describeArg(a) {
1526
+ if (typeof a === "string") return a;
1527
+ if (a instanceof Error) return `${a.name}: ${a.message}`;
1528
+ try {
1529
+ return JSON.stringify(a) ?? String(a);
1530
+ } catch {
1531
+ return String(a);
1532
+ }
1533
+ }
1534
+ var XHR_INFO = "__guidelyCrumb";
1535
+ function instrumentBreadcrumbs(add, opts) {
1536
+ let on = true;
1537
+ const undo = [];
1538
+ const g = globalThis;
1539
+ if (opts.console !== false && g.console) {
1540
+ const levels = opts.console === "all" ? ["log", "info", "warn", "error", "debug"] : ["warn", "error"];
1541
+ for (const level of levels) {
1542
+ const original = g.console[level];
1543
+ if (typeof original !== "function") continue;
1544
+ const wrapped = function(...args) {
1545
+ if (on) {
1546
+ try {
1547
+ const message = args.map(describeArg).join(" ").slice(0, 1e3);
1548
+ if (!message.startsWith("[Guidely]")) add({ category: "console", level: level === "warn" ? "warning" : level === "log" ? "info" : level, message, data: { logger: "console" } }, { event: args });
1549
+ } catch {
1550
+ }
1551
+ }
1552
+ return original.apply(this, args);
1553
+ };
1554
+ g.console[level] = wrapped;
1555
+ undo.push(() => {
1556
+ if (g.console[level] === wrapped) g.console[level] = original;
1557
+ });
1558
+ }
1559
+ }
1560
+ if (opts.http !== false && typeof g.fetch === "function") {
1561
+ const original = g.fetch;
1562
+ const wrapped = function(input, init) {
1563
+ const started = Date.now();
1564
+ let url = "";
1565
+ let method = "GET";
1566
+ try {
1567
+ url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1568
+ method = (init?.method ?? (typeof input === "object" && "method" in input ? input.method : "GET")).toUpperCase();
1569
+ } catch {
1570
+ }
1571
+ const p = original.call(this, input, init);
1572
+ if (on && url && !isOwnRequest(url, opts.apiUrl)) {
1573
+ const crumb = (status) => {
1574
+ try {
1575
+ add({ type: "http", category: "fetch", level: status === void 0 || status >= 500 ? "error" : status >= 400 ? "warning" : "info", data: { method, url: breadcrumbUrl(url), ...status !== void 0 ? { status_code: status } : {}, duration_ms: Date.now() - started } });
1576
+ } catch {
1577
+ }
1578
+ };
1579
+ p.then(
1580
+ (res) => crumb(res.status),
1581
+ () => crumb(void 0)
1582
+ );
1583
+ }
1584
+ return p;
1585
+ };
1586
+ g.fetch = wrapped;
1587
+ undo.push(() => {
1588
+ if (g.fetch === wrapped) g.fetch = original;
1589
+ });
1590
+ }
1591
+ const proto = opts.http !== false ? g.XMLHttpRequest?.prototype : void 0;
1592
+ if (proto) {
1593
+ const open = proto.open;
1594
+ const send = proto.send;
1595
+ const wrappedOpen = function(method, url, ...rest) {
1596
+ try {
1597
+ this[XHR_INFO] = { method: String(method).toUpperCase(), url: String(url) };
1598
+ } catch {
1599
+ }
1600
+ return open.call(this, method, url, ...rest);
1601
+ };
1602
+ const wrappedSend = function(body) {
1603
+ const info = this[XHR_INFO];
1604
+ if (on && info && !isOwnRequest(info.url, opts.apiUrl)) {
1605
+ const started = Date.now();
1606
+ this.addEventListener("loadend", () => {
1607
+ try {
1608
+ const status = this.status || void 0;
1609
+ add({ type: "http", category: "xhr", level: !status || status >= 500 ? "error" : status >= 400 ? "warning" : "info", data: { method: info.method, url: breadcrumbUrl(info.url), ...status ? { status_code: status } : {}, duration_ms: Date.now() - started } }, { event: this });
1610
+ } catch {
1611
+ }
1612
+ });
1613
+ }
1614
+ return send.call(this, body);
1615
+ };
1616
+ proto.open = wrappedOpen;
1617
+ proto.send = wrappedSend;
1618
+ undo.push(() => {
1619
+ if (proto.open === wrappedOpen) proto.open = open;
1620
+ if (proto.send === wrappedSend) proto.send = send;
1621
+ });
1622
+ }
1623
+ return () => {
1624
+ on = false;
1625
+ for (const u of undo) u();
1626
+ };
1627
+ }
1628
+
1412
1629
  // src/emitter.ts
1413
1630
  var Emitter = class {
1414
1631
  constructor(onError) {
@@ -1445,13 +1662,13 @@ var Emitter = class {
1445
1662
 
1446
1663
  // src/frequency.ts
1447
1664
  var HOUR = 3600 * 1e3;
1448
- function isFrequencyEligible(freq, state, shownThisSession, now = Date.now()) {
1665
+ function isFrequencyEligible(freq, state, shownThisSession, now3 = Date.now()) {
1449
1666
  const s = state ?? { impressions: 0 };
1450
1667
  if (freq.maxImpressions != null && s.impressions >= freq.maxImpressions) return false;
1451
1668
  if (s.completedAt && freq.mode !== "every_time") return false;
1452
1669
  if (s.dismissedAt) {
1453
1670
  if (!freq.reshowAfterDismiss) return false;
1454
- if (freq.cooldownHours > 0 && now - Date.parse(s.dismissedAt) < freq.cooldownHours * HOUR) return false;
1671
+ if (freq.cooldownHours > 0 && now3 - Date.parse(s.dismissedAt) < freq.cooldownHours * HOUR) return false;
1455
1672
  }
1456
1673
  switch (freq.mode) {
1457
1674
  case "once":
@@ -1461,7 +1678,7 @@ function isFrequencyEligible(freq, state, shownThisSession, now = Date.now()) {
1461
1678
  case "interval": {
1462
1679
  if (!s.lastShownAt) return true;
1463
1680
  const hours = freq.intervalHours ?? 24;
1464
- return now - Date.parse(s.lastShownAt) >= hours * HOUR;
1681
+ return now3 - Date.parse(s.lastShownAt) >= hours * HOUR;
1465
1682
  }
1466
1683
  case "every_time":
1467
1684
  default:
@@ -1479,11 +1696,11 @@ function areMutuallyExclusive(a, b) {
1479
1696
  const lb = b.config.frequency.mutuallyExclusiveWith;
1480
1697
  return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);
1481
1698
  }
1482
- function isWithinSchedule(guide, now = Date.now()) {
1699
+ function isWithinSchedule(guide, now3 = Date.now()) {
1483
1700
  for (const w of [guide.config.schedule, guide.config.survey]) {
1484
1701
  if (!w) continue;
1485
- if (w.startAt && now < Date.parse(w.startAt)) return false;
1486
- if (w.endAt && now > Date.parse(w.endAt)) return false;
1702
+ if (w.startAt && now3 < Date.parse(w.startAt)) return false;
1703
+ if (w.endAt && now3 > Date.parse(w.endAt)) return false;
1487
1704
  }
1488
1705
  return true;
1489
1706
  }
@@ -1491,19 +1708,19 @@ var MINUTE = 60 * 1e3;
1491
1708
  function countsTowardThrottle(guide) {
1492
1709
  return isExclusiveGuide(guide) && !guide.config.frequency.ignoreThrottling;
1493
1710
  }
1494
- function isThrottleEligible(settings, launches, now = Date.now()) {
1711
+ function isThrottleEligible(settings, launches, now3 = Date.now()) {
1495
1712
  if (!settings || !settings.enabled) return true;
1496
1713
  const times = launches.map((l) => Date.parse(l)).filter((t) => !Number.isNaN(t));
1497
1714
  const windowMs = Math.max(1, settings.windowHours) * HOUR;
1498
- if (times.filter((t) => now - t < windowMs).length >= Math.max(1, settings.maxGuides)) return false;
1499
- if (settings.minGapMinutes > 0 && times.some((t) => now - t < settings.minGapMinutes * MINUTE)) return false;
1715
+ if (times.filter((t) => now3 - t < windowMs).length >= Math.max(1, settings.maxGuides)) return false;
1716
+ if (settings.minGapMinutes > 0 && times.some((t) => now3 - t < settings.minGapMinutes * MINUTE)) return false;
1500
1717
  return true;
1501
1718
  }
1502
- function pruneLaunches(launches, settings, now = Date.now()) {
1719
+ function pruneLaunches(launches, settings, now3 = Date.now()) {
1503
1720
  const keepMs = Math.max(settings?.windowHours ?? 24, 1) * HOUR + (settings?.minGapMinutes ?? 0) * MINUTE;
1504
1721
  return launches.filter((l) => {
1505
1722
  const t = Date.parse(l);
1506
- return !Number.isNaN(t) && now - t < keepMs;
1723
+ return !Number.isNaN(t) && now3 - t < keepMs;
1507
1724
  }).slice(-100);
1508
1725
  }
1509
1726
  function dismissedAfterMerge(local, server) {
@@ -1674,7 +1891,7 @@ function createMessengerTransport(opts) {
1674
1891
  }
1675
1892
 
1676
1893
  // src/messenger-visitor.ts
1677
- var import_runtime15 = require("@guidelytech/shared/runtime");
1894
+ var import_runtime17 = require("@guidelytech/shared/runtime");
1678
1895
  var import_guide_runtime4 = require("@guidelytech/guide-runtime");
1679
1896
  var PAGE_GUIDE_KINDS = /* @__PURE__ */ new Set(["flow", "tooltip", "modal"]);
1680
1897
  var CATCH_ALL = /* @__PURE__ */ new Set(["", "*", "**", "/*", "/**", "/"]);
@@ -1699,7 +1916,7 @@ function plainText(html, max = 280) {
1699
1916
  return text.length > max ? `${text.slice(0, max - 1).trimEnd()}\u2026` : text;
1700
1917
  }
1701
1918
  function buildMessengerVisitor(input) {
1702
- const now = input.now ?? Date.now();
1919
+ const now3 = input.now ?? Date.now();
1703
1920
  const { guides, state } = input;
1704
1921
  const done = (g) => !!state.guides[g.id]?.completedAt;
1705
1922
  const pageGuides = guides.filter((g) => PAGE_GUIDE_KINDS.has(g.kind) && g.config.steps.length > 0 && !done(g) && targetsPage(g, input.url) && input.isTargeted(g)).slice(0, 3).map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind }));
@@ -1709,7 +1926,7 @@ function buildMessengerVisitor(input) {
1709
1926
  const s = state.guides[g.id];
1710
1927
  if (g.kind !== "flow" || !s?.lastStepId || s.completedAt || g.config.steps.length < 2) continue;
1711
1928
  const at = Date.parse(s.lastShownAt ?? "");
1712
- if (!Number.isFinite(at) || now - at > RESUME_WITHIN_MS || at <= resumeAt) continue;
1929
+ if (!Number.isFinite(at) || now3 - at > RESUME_WITHIN_MS || at <= resumeAt) continue;
1713
1930
  const stepIndex = g.config.steps.findIndex((st) => st.id === s.lastStepId);
1714
1931
  if (stepIndex < 1 || !isWithinSchedule(g)) continue;
1715
1932
  resumeGuide = { id: g.id, name: g.name, stepId: s.lastStepId, stepIndex, stepCount: g.config.steps.length };
@@ -1728,20 +1945,20 @@ function buildMessengerVisitor(input) {
1728
1945
  break;
1729
1946
  }
1730
1947
  const seen = new Set(input.newsSeen);
1731
- const news = guides.filter((g) => g.kind === "announcement" && g.config.steps.length > 0 && now - Date.parse(g.publishedAt) <= NEWS_WITHIN_MS && input.isAudienceTargeted(g)).sort((a, b) => b.publishedAt.localeCompare(a.publishedAt)).slice(0, MAX_NEWS).map((g) => {
1948
+ const news = guides.filter((g) => g.kind === "announcement" && g.config.steps.length > 0 && now3 - Date.parse(g.publishedAt) <= NEWS_WITHIN_MS && input.isAudienceTargeted(g)).sort((a, b) => b.publishedAt.localeCompare(a.publishedAt)).slice(0, MAX_NEWS).map((g) => {
1732
1949
  const step = g.config.steps[0];
1733
1950
  return {
1734
1951
  id: g.id,
1735
1952
  key: g.key,
1736
1953
  title: step.title || g.name,
1737
1954
  body: plainText(step.body ?? ""),
1738
- imageUrl: step.imageUrl || (0, import_runtime15.videoThumbnailUrl)(step.videoUrl) || null,
1955
+ imageUrl: step.imageUrl || (0, import_runtime17.videoThumbnailUrl)(step.videoUrl) || null,
1739
1956
  publishedAt: g.publishedAt,
1740
1957
  // Read once opened in the News tab, or once the announcement itself was shown.
1741
1958
  unread: !seen.has(g.id) && !(state.guides[g.id]?.impressions ?? 0)
1742
1959
  };
1743
1960
  });
1744
- return { firstName: (0, import_runtime15.firstNameFromTraits)(input.traits), path: pathOf(input.url), pageGuides, resumeGuide, checklist, news };
1961
+ return { firstName: (0, import_runtime17.firstNameFromTraits)(input.traits), path: pathOf(input.url), pageGuides, resumeGuide, checklist, news };
1745
1962
  }
1746
1963
 
1747
1964
  // src/navigation.ts
@@ -1858,19 +2075,19 @@ var PageTracker = class {
1858
2075
  __publicField(this, "maxScroll", 0);
1859
2076
  }
1860
2077
  /** Starts measuring a new page. */
1861
- start(url, now = Date.now()) {
2078
+ start(url, now3 = Date.now()) {
1862
2079
  this.url = url;
1863
2080
  this.visibleMs = 0;
1864
- this.visibleSince = typeof document !== "undefined" && document.visibilityState === "hidden" ? null : now;
2081
+ this.visibleSince = typeof document !== "undefined" && document.visibilityState === "hidden" ? null : now3;
1865
2082
  this.maxScroll = 0;
1866
2083
  this.measureScroll();
1867
2084
  }
1868
2085
  /** Pauses / resumes the visible-time clock. */
1869
- setHidden(hidden, now = Date.now()) {
2086
+ setHidden(hidden, now3 = Date.now()) {
1870
2087
  if (hidden && this.visibleSince !== null) {
1871
- this.visibleMs += Math.max(0, now - this.visibleSince);
2088
+ this.visibleMs += Math.max(0, now3 - this.visibleSince);
1872
2089
  this.visibleSince = null;
1873
- } else if (!hidden && this.visibleSince === null) this.visibleSince = now;
2090
+ } else if (!hidden && this.visibleSince === null) this.visibleSince = now3;
1874
2091
  }
1875
2092
  measureScroll() {
1876
2093
  try {
@@ -1879,9 +2096,9 @@ var PageTracker = class {
1879
2096
  }
1880
2097
  }
1881
2098
  /** Final numbers for the current page. */
1882
- leave(now = Date.now()) {
2099
+ leave(now3 = Date.now()) {
1883
2100
  this.measureScroll();
1884
- const running = this.visibleSince !== null ? Math.max(0, now - this.visibleSince) : 0;
2101
+ const running = this.visibleSince !== null ? Math.max(0, now3 - this.visibleSince) : 0;
1885
2102
  return { url: this.url, durationMs: Math.round(this.visibleMs + running), scrollDepth: this.maxScroll };
1886
2103
  }
1887
2104
  };
@@ -1987,6 +2204,21 @@ function openPrototypeOverlay(testUrl, opts) {
1987
2204
  }
1988
2205
 
1989
2206
  // src/queue.ts
2207
+ var MAX_PERSISTED_QUEUE_CHARS = 1024 * 1024;
2208
+ function fitPersisted(events) {
2209
+ let size = JSON.stringify(events).length;
2210
+ if (size <= MAX_PERSISTED_QUEUE_CHARS) return events;
2211
+ const out = events.slice();
2212
+ for (let i = 0; i < out.length && size > MAX_PERSISTED_QUEUE_CHARS; i++) {
2213
+ const e = out[i];
2214
+ if (!e.detail) continue;
2215
+ size -= JSON.stringify(e.detail).length;
2216
+ const rest = { ...e };
2217
+ delete rest.detail;
2218
+ out[i] = rest;
2219
+ }
2220
+ return out;
2221
+ }
1990
2222
  var EventQueue = class {
1991
2223
  constructor(opts) {
1992
2224
  __publicField(this, "opts", opts);
@@ -2019,12 +2251,12 @@ var EventQueue = class {
2019
2251
  this.attempt = 0;
2020
2252
  void this.flush();
2021
2253
  }
2022
- retryIn(ms) {
2254
+ retryIn(ms2) {
2023
2255
  if (this.retryTimer || this.destroyed) return;
2024
2256
  this.retryTimer = setTimeout(() => {
2025
2257
  this.retryTimer = null;
2026
2258
  void this.flush();
2027
- }, ms);
2259
+ }, ms2);
2028
2260
  }
2029
2261
  get size() {
2030
2262
  return this.events.length;
@@ -2042,7 +2274,7 @@ var EventQueue = class {
2042
2274
  this.schedule();
2043
2275
  }
2044
2276
  persist() {
2045
- writeJson(this.opts.store, this.opts.storageKey, this.events);
2277
+ writeJson(this.opts.store, this.opts.storageKey, fitPersisted(this.events));
2046
2278
  }
2047
2279
  schedule() {
2048
2280
  if (this.timer || this.retryTimer || this.destroyed) return;
@@ -2116,17 +2348,631 @@ var EventQueue = class {
2116
2348
  // src/load-recorder.ts
2117
2349
  var loadRecorder = async () => (await import("./recorder.cjs")).recorder;
2118
2350
 
2351
+ // src/environment.ts
2352
+ function environmentFromKey(key) {
2353
+ const prefix = /^pk_([a-z0-9]+)_/.exec(key)?.[1];
2354
+ if (!prefix || prefix === "dev") return "development";
2355
+ if (prefix === "prod") return "production";
2356
+ if (prefix === "stg" || prefix === "stag") return "staging";
2357
+ return prefix;
2358
+ }
2359
+
2360
+ // src/trace-propagation.ts
2361
+ function hex(bytes) {
2362
+ const a = new Uint8Array(bytes);
2363
+ try {
2364
+ crypto.getRandomValues(a);
2365
+ } catch {
2366
+ for (let i = 0; i < bytes; i++) a[i] = Math.floor(Math.random() * 256);
2367
+ }
2368
+ let out = "";
2369
+ for (const b of a) out += (b < 16 ? "0" : "") + b.toString(16);
2370
+ return out;
2371
+ }
2372
+ function stripUrl(url) {
2373
+ return url.split(/[?#]/)[0];
2374
+ }
2375
+ function resolveUrl(url) {
2376
+ try {
2377
+ return new URL(url, location.href).href;
2378
+ } catch {
2379
+ return url;
2380
+ }
2381
+ }
2382
+ function shouldPropagate(url, targets) {
2383
+ const abs = resolveUrl(url);
2384
+ if (!targets) {
2385
+ try {
2386
+ return new URL(abs).origin === location.origin;
2387
+ } catch {
2388
+ return false;
2389
+ }
2390
+ }
2391
+ return targets.some((t) => {
2392
+ if (typeof t === "string") return url.includes(t) || abs.includes(t);
2393
+ t.lastIndex = 0;
2394
+ if (t.test(url)) return true;
2395
+ t.lastIndex = 0;
2396
+ return t.test(abs);
2397
+ });
2398
+ }
2399
+ function mergeBaggage(existing, ours) {
2400
+ const kept = (existing || "").split(",").map((s) => s.trim()).filter((s) => s && !s.startsWith("sentry-"));
2401
+ return [...kept, ours].join(",");
2402
+ }
2403
+ function traceHeaders(dsc, spanId) {
2404
+ const flag = dsc.sampled ? "1" : "0";
2405
+ const e = encodeURIComponent;
2406
+ const baggage = [`sentry-trace_id=${dsc.traceId}`, `sentry-public_key=${e(dsc.publicKey)}`, dsc.release ? `sentry-release=${e(dsc.release)}` : "", dsc.environment ? `sentry-environment=${e(dsc.environment)}` : "", `sentry-sampled=${dsc.sampled}`, `sentry-sample_rate=${dsc.sampleRate}`].filter(Boolean).join(",");
2407
+ return { "sentry-trace": `${dsc.traceId}-${spanId}-${flag}`, traceparent: `00-${dsc.traceId}-${spanId}-0${flag}`, baggage };
2408
+ }
2409
+ function httpSpanStatus(code) {
2410
+ if (code >= 100 && code < 400) return "ok";
2411
+ if (code >= 400 && code < 500) return { 401: "unauthenticated", 403: "permission_denied", 404: "not_found", 409: "already_exists", 413: "failed_precondition", 429: "resource_exhausted", 499: "cancelled" }[code] ?? "invalid_argument";
2412
+ if (code >= 500 && code < 600) return { 501: "unimplemented", 503: "unavailable", 504: "deadline_exceeded" }[code] ?? "internal_error";
2413
+ return "unknown_error";
2414
+ }
2415
+
2416
+ // src/web-vitals.ts
2417
+ function observe(type, cb, extra = {}) {
2418
+ try {
2419
+ if (typeof PerformanceObserver === "undefined" || !PerformanceObserver.supportedEntryTypes?.includes(type)) return null;
2420
+ const po = new PerformanceObserver((list) => cb(list.getEntries()));
2421
+ po.observe({ type, buffered: true, ...extra });
2422
+ return { take: () => cb(po.takeRecords()), stop: () => po.disconnect() };
2423
+ } catch {
2424
+ return null;
2425
+ }
2426
+ }
2427
+ var newCls = () => ({ value: 0, session: 0, first: 0, last: 0 });
2428
+ function addLayoutShift(s, e) {
2429
+ if (e.hadRecentInput) return;
2430
+ if (s.session && e.startTime - s.last < 1e3 && e.startTime - s.first < 5e3) s.session += e.value;
2431
+ else {
2432
+ s.session = e.value;
2433
+ s.first = e.startTime;
2434
+ }
2435
+ s.last = e.startTime;
2436
+ if (s.session > s.value) s.value = s.session;
2437
+ }
2438
+ var newInp = () => ({ longest: [], ids: /* @__PURE__ */ new Set() });
2439
+ function addInteraction(s, e) {
2440
+ const id = e.interactionId;
2441
+ if (!id) return;
2442
+ s.ids.add(id);
2443
+ const known = s.longest.find((i) => i.id === id);
2444
+ if (known) known.latency = Math.max(known.latency, e.duration);
2445
+ else if (s.longest.length < 10 || e.duration > s.longest[s.longest.length - 1].latency) s.longest.push({ id, latency: e.duration });
2446
+ else return;
2447
+ s.longest.sort((a, b) => b.latency - a.latency);
2448
+ s.longest.splice(10);
2449
+ }
2450
+ function inpValue(s, count = s.ids.size) {
2451
+ if (!s.longest.length) return void 0;
2452
+ return s.longest[Math.min(s.longest.length - 1, Math.floor(count / 50))].latency;
2453
+ }
2454
+ var ms = (value) => ({ value, unit: "millisecond" });
2455
+ var WebVitals = class {
2456
+ constructor(nav) {
2457
+ __publicField(this, "lcp");
2458
+ __publicField(this, "fcp");
2459
+ __publicField(this, "ttfb");
2460
+ __publicField(this, "cls", newCls());
2461
+ __publicField(this, "inp", newInp());
2462
+ __publicField(this, "observed", []);
2463
+ __publicField(this, "lcpFinal", false);
2464
+ __publicField(this, "hiddenAt");
2465
+ __publicField(this, "offs", []);
2466
+ const activation = nav?.activationStart || 0;
2467
+ const rel = (t) => Math.max(0, t - activation);
2468
+ this.hiddenAt = document.visibilityState === "hidden" ? 0 : Infinity;
2469
+ if (nav && nav.responseStart > 0) this.ttfb = rel(nav.responseStart);
2470
+ const add = (o) => o && this.observed.push(o);
2471
+ add(
2472
+ observe("paint", (es) => {
2473
+ for (const e of es) if (e.name === "first-contentful-paint" && e.startTime < this.hiddenAt) this.fcp = rel(e.startTime);
2474
+ })
2475
+ );
2476
+ add(
2477
+ observe("largest-contentful-paint", (es) => {
2478
+ const e = es[es.length - 1];
2479
+ if (e && !this.lcpFinal && e.startTime < this.hiddenAt) this.lcp = rel(e.startTime);
2480
+ })
2481
+ );
2482
+ add(observe("layout-shift", (es) => es.forEach((e) => addLayoutShift(this.cls, e))));
2483
+ const onEvent = (es) => es.forEach((e) => addInteraction(this.inp, e));
2484
+ add(observe("event", onEvent, { durationThreshold: 40 }));
2485
+ add(observe("first-input", onEvent));
2486
+ const freeze = () => {
2487
+ this.lcpFinal = true;
2488
+ this.offs.splice(0).forEach((f) => f());
2489
+ };
2490
+ for (const type of ["keydown", "pointerdown"]) {
2491
+ addEventListener(type, freeze, { once: true, capture: true });
2492
+ this.offs.push(() => removeEventListener(type, freeze, { capture: true }));
2493
+ }
2494
+ }
2495
+ /** The page was hidden: deliver pending entries and freeze LCP. */
2496
+ hidden() {
2497
+ this.observed.forEach((o) => o.take());
2498
+ this.lcpFinal = true;
2499
+ if (this.hiddenAt === Infinity) this.hiddenAt = performance.now();
2500
+ }
2501
+ /** Known at load: LCP (so far), FCP and TTFB. */
2502
+ pageload() {
2503
+ const m = {};
2504
+ if (this.lcp !== void 0) m.lcp = ms(this.lcp);
2505
+ if (this.fcp !== void 0) m.fcp = ms(this.fcp);
2506
+ if (this.ttfb !== void 0) m.ttfb = ms(this.ttfb);
2507
+ return m;
2508
+ }
2509
+ /** Known only when the page is hidden: CLS, and INP once there was an interaction. */
2510
+ late() {
2511
+ const count = performance.interactionCount;
2512
+ const inp = inpValue(this.inp, count || void 0);
2513
+ return { cls: { value: this.cls.value, unit: "" }, ...inp !== void 0 ? { inp: ms(inp) } : {} };
2514
+ }
2515
+ stop() {
2516
+ this.observed.splice(0).forEach((o) => o.stop());
2517
+ this.offs.splice(0).forEach((f) => f());
2518
+ }
2519
+ };
2520
+
2521
+ // src/tracing.ts
2522
+ var TRACES_SDK_NAME = "guidely.javascript";
2523
+ var MAX_SPANS = 1e3;
2524
+ var MAX_QUEUE = 20;
2525
+ var BATCH = 10;
2526
+ var now2 = () => performance.now();
2527
+ function entries(type) {
2528
+ try {
2529
+ return performance.getEntriesByType(type);
2530
+ } catch {
2531
+ return [];
2532
+ }
2533
+ }
2534
+ function pathOf2(href) {
2535
+ try {
2536
+ return new URL(href).pathname || "/";
2537
+ } catch {
2538
+ return stripUrl(href) || "/";
2539
+ }
2540
+ }
2541
+ function browserContext() {
2542
+ const ua = navigator.userAgent || "";
2543
+ const name = detectDevice(ua, 1024).browser;
2544
+ if (!name) return void 0;
2545
+ const token = { edge: "Edg", opera: "OPR", samsung: "SamsungBrowser", chrome: "(?:Chrome|CriOS)", firefox: "(?:Firefox|FxiOS)", safari: "Version", electron: "Electron" }[name];
2546
+ const version = token ? new RegExp(`${token}/([\\d.]+)`).exec(ua)?.[1] : void 0;
2547
+ return { name: name[0].toUpperCase() + name.slice(1), ...version ? { version } : {} };
2548
+ }
2549
+ function startTracer(config) {
2550
+ if (typeof performance === "undefined" || typeof performance.now !== "function" || typeof window === "undefined") return null;
2551
+ return new Tracer(config);
2552
+ }
2553
+ var Tracer = class {
2554
+ constructor(o) {
2555
+ __publicField(this, "o", o);
2556
+ /** null until the project setting is known; then whether tracing is on for this page load. */
2557
+ __publicField(this, "enabled", null);
2558
+ __publicField(this, "sampled", false);
2559
+ __publicField(this, "rate", 0);
2560
+ __publicField(this, "txn", null);
2561
+ __publicField(this, "pageload");
2562
+ __publicField(this, "origin");
2563
+ __publicField(this, "vitals");
2564
+ __publicField(this, "longTasks", []);
2565
+ /** Finished before sampling was decided. */
2566
+ __publicField(this, "held", []);
2567
+ __publicField(this, "out", []);
2568
+ __publicField(this, "flushTimer", null);
2569
+ __publicField(this, "cleanups", []);
2570
+ __publicField(this, "stopped", false);
2571
+ __publicField(this, "vitalsSent", false);
2572
+ __publicField(this, "apiPrefix");
2573
+ __publicField(this, "idleMs");
2574
+ __publicField(this, "finalMs");
2575
+ this.idleMs = o.idleMs ?? 1e3;
2576
+ this.finalMs = o.finalMs ?? 3e4;
2577
+ this.origin = performance.timeOrigin || Date.now() - now2();
2578
+ this.apiPrefix = `${resolveUrl(o.apiUrl).replace(/\/+$/, "")}/api/v1/sdk/`;
2579
+ this.vitals = new WebVitals(entries("navigation")[0]);
2580
+ const lt = observe("longtask", (es) => {
2581
+ for (const e of es) if (this.longTasks.length < 500) this.longTasks.push(e);
2582
+ });
2583
+ if (lt) this.cleanups.push(lt.stop);
2584
+ this.instrumentFetch();
2585
+ this.instrumentXhr();
2586
+ this.cleanups.push(watchNavigation((href) => this.navigate(href), 0));
2587
+ const onVisibility = () => document.visibilityState === "hidden" && this.onHide();
2588
+ const onPageHide = () => this.onHide();
2589
+ document.addEventListener("visibilitychange", onVisibility);
2590
+ addEventListener("pagehide", onPageHide);
2591
+ this.cleanups.push(() => {
2592
+ document.removeEventListener("visibilitychange", onVisibility);
2593
+ removeEventListener("pagehide", onPageHide);
2594
+ });
2595
+ const t = this.pageload = this.begin("pageload", 0);
2596
+ const onLoad = () => setTimeout(() => {
2597
+ t.loaded = true;
2598
+ this.scheduleIdle(t);
2599
+ }, 0);
2600
+ if (document.readyState === "complete") onLoad();
2601
+ else {
2602
+ addEventListener("load", onLoad, { once: true });
2603
+ this.cleanups.push(() => removeEventListener("load", onLoad));
2604
+ }
2605
+ }
2606
+ /** Applies the project's `performance` setting; sampling is decided the first time it is on. */
2607
+ configure(settings) {
2608
+ if (this.stopped) return;
2609
+ if (!settings?.enabled) {
2610
+ this.enabled = false;
2611
+ this.held = [];
2612
+ this.out = [];
2613
+ return this.stop();
2614
+ }
2615
+ if (this.enabled) return;
2616
+ this.enabled = true;
2617
+ const sdkRate = this.o.tracesSampleRate ?? 1;
2618
+ this.rate = Math.max(0, Math.min((settings.sampleRate ?? 100) / 100, Number.isFinite(sdkRate) ? sdkRate : 1, 1));
2619
+ this.sampled = Math.random() < this.rate;
2620
+ if (!this.sampled) this.vitals.stop();
2621
+ for (const e of this.held.splice(0)) if (this.sampled) this.enqueue(e);
2622
+ }
2623
+ /**
2624
+ * The trace an error happening now belongs to (the open navigation, else the page load), so error
2625
+ * events link to it. Null when this page load isn't traced.
2626
+ */
2627
+ traceContext() {
2628
+ if (!this.enabled || !this.sampled || this.stopped) return null;
2629
+ const t = this.txn ?? this.pageload;
2630
+ return { traceId: t.traceId, spanId: t.spanId };
2631
+ }
2632
+ /** Stops tracing and sends what is queued. */
2633
+ destroy() {
2634
+ this.flush(true);
2635
+ this.stop();
2636
+ }
2637
+ stop() {
2638
+ if (this.stopped) return;
2639
+ this.stopped = true;
2640
+ if (this.flushTimer) clearTimeout(this.flushTimer);
2641
+ for (const t of [this.txn, this.pageload]) {
2642
+ clearTimeout(t?.idle);
2643
+ clearTimeout(t?.final);
2644
+ }
2645
+ this.vitals.stop();
2646
+ this.cleanups.splice(0).forEach((f) => f());
2647
+ }
2648
+ ts(rel) {
2649
+ return (this.origin + rel) / 1e3;
2650
+ }
2651
+ begin(op, start, href) {
2652
+ const url = this.o.pageHref(href);
2653
+ const t = { op, name: pathOf2(url), url, traceId: hex(16), spanId: hex(8), start, last: start, inflight: 0, spans: [], loaded: op !== "pageload" };
2654
+ t.final = setTimeout(() => this.finish(t), Math.max(0, this.finalMs - (now2() - start)));
2655
+ this.txn = t;
2656
+ return t;
2657
+ }
2658
+ scheduleIdle(t) {
2659
+ clearTimeout(t.idle);
2660
+ if (t.done || t.inflight > 0 || !t.loaded) return;
2661
+ t.idle = setTimeout(() => this.finish(t), this.idleMs);
2662
+ }
2663
+ navigate(href) {
2664
+ if (this.stopped || pathOf2(this.o.pageHref(href)) === this.txn?.name) return;
2665
+ if (this.txn && !this.txn.done) this.finish(this.txn);
2666
+ this.scheduleIdle(this.begin("navigation", now2(), href));
2667
+ }
2668
+ onHide() {
2669
+ if (this.stopped) return;
2670
+ this.vitals.hidden();
2671
+ if (this.txn && !this.txn.done) this.finish(this.txn, now2());
2672
+ if (this.enabled && this.sampled && !this.vitalsSent) {
2673
+ this.vitalsSent = true;
2674
+ const at = this.ts(now2());
2675
+ const p = this.pageload;
2676
+ const e = this.event(p, "ui.webvitals", at, at, [], this.vitals.late());
2677
+ e.contexts.trace.span_id = hex(8);
2678
+ e.contexts.trace.parent_span_id = p.spanId;
2679
+ this.enqueue(e);
2680
+ }
2681
+ this.flush(true);
2682
+ }
2683
+ /** Ends a transaction: adds the spans read from performance entries and queues (or holds) it. */
2684
+ finish(t, endAt) {
2685
+ if (t.done) return;
2686
+ t.done = true;
2687
+ clearTimeout(t.idle);
2688
+ clearTimeout(t.final);
2689
+ if (this.stopped || this.enabled === false || this.enabled && !this.sampled) return;
2690
+ const until = now2();
2691
+ const spans = t.spans;
2692
+ const add = (op, description, start, end2, origin, data) => {
2693
+ if (spans.length >= MAX_SPANS || !(end2 >= start) || start < t.start || start > until) return;
2694
+ spans.push({ span_id: hex(8), parent_span_id: t.spanId, trace_id: t.traceId, op, description, start_timestamp: this.ts(start), timestamp: this.ts(end2), origin, ...data ? { data } : {} });
2695
+ };
2696
+ if (t.op === "pageload") {
2697
+ const n = entries("navigation")[0];
2698
+ if (n) {
2699
+ const timing = (name, s, e) => s > 0 && e > 0 && add("browser", name, s, e, "auto.ui.browser.metrics");
2700
+ timing("redirect", n.redirectStart, n.redirectEnd);
2701
+ timing("cache", n.fetchStart, n.domainLookupStart);
2702
+ timing("DNS", n.domainLookupStart, n.domainLookupEnd);
2703
+ timing("connect", n.connectStart, n.connectEnd);
2704
+ timing("TLS/SSL", n.secureConnectionStart, n.connectEnd);
2705
+ timing("request", n.requestStart, n.responseEnd);
2706
+ timing("response", n.responseStart, n.responseEnd);
2707
+ timing("domContentLoadedEvent", n.domContentLoadedEventStart, n.domContentLoadedEventEnd);
2708
+ timing("loadEvent", n.loadEventStart, n.loadEventEnd);
2709
+ }
2710
+ }
2711
+ for (const e of entries("resource")) {
2712
+ const it = e.initiatorType;
2713
+ if (it === "fetch" || it === "xmlhttprequest" || this.own(e.name)) continue;
2714
+ const kind = it === "script" || it === "css" || it === "link" ? it : it === "img" || it === "image" ? "img" : "other";
2715
+ const data = { "http.response_content_length": e.encodedBodySize, "http.decoded_response_content_length": e.decodedBodySize, "http.response_transfer_size": e.transferSize };
2716
+ if (e.renderBlockingStatus) data["resource.render_blocking_status"] = e.renderBlockingStatus;
2717
+ add(`resource.${kind}`, stripUrl(e.name), e.startTime, e.responseEnd || e.startTime + e.duration, "auto.resource.browser.metrics", data);
2718
+ }
2719
+ for (const e of this.longTasks) add("ui.long-task", "Main UI thread blocked", e.startTime, e.startTime + e.duration, "auto.ui.browser.metrics");
2720
+ for (const e of entries("mark")) add("mark", e.name, e.startTime, e.startTime, "auto.ui.browser.metrics");
2721
+ for (const e of entries("measure")) add("measure", e.name, e.startTime, e.startTime + e.duration, "auto.ui.browser.metrics");
2722
+ const end = endAt !== void 0 ? this.ts(endAt) : spans.reduce((m2, s) => Math.max(m2, s.timestamp), this.ts(t.last));
2723
+ const m = t.op === "pageload" ? this.vitals.pageload() : {};
2724
+ const event = this.event(t, t.op, this.ts(t.start), end, spans, Object.keys(m).length ? m : void 0);
2725
+ if (this.enabled) this.enqueue(event);
2726
+ else if (this.held.length < MAX_QUEUE) this.held.push(event);
2727
+ }
2728
+ event(t, op, start, end, spans, measurements) {
2729
+ const c = this.o.context();
2730
+ const url = c.collectUrls ? stripUrl(t.url) : void 0;
2731
+ const browser = browserContext();
2732
+ return {
2733
+ type: "transaction",
2734
+ event_id: hex(16),
2735
+ platform: "javascript",
2736
+ transaction: t.name,
2737
+ transaction_info: { source: "url" },
2738
+ start_timestamp: start,
2739
+ timestamp: end,
2740
+ ...c.release ? { release: c.release } : {},
2741
+ environment: c.environment,
2742
+ contexts: {
2743
+ trace: { trace_id: t.traceId, span_id: t.spanId, op, status: "ok", origin: `auto.${op}.guidely` },
2744
+ ...browser ? { browser } : {},
2745
+ guidely: { anonymousId: c.anonymousId, sessionId: c.sessionId, ...url ? { url } : {} }
2746
+ },
2747
+ spans,
2748
+ ...measurements ? { measurements } : {},
2749
+ ...url ? { request: { url } } : {},
2750
+ ...c.userId ? { user: { id: c.userId } } : {}
2751
+ };
2752
+ }
2753
+ // --- requests ---------------------------------------------------------------------------------
2754
+ /** A Guidely API request (never traced, never given headers). */
2755
+ own(url) {
2756
+ return resolveUrl(url).startsWith(this.apiPrefix);
2757
+ }
2758
+ startHttp(method, url, type) {
2759
+ if (this.stopped || this.enabled === false || this.own(url)) return null;
2760
+ const abs = resolveUrl(url);
2761
+ const t = this.txn && !this.txn.done ? this.txn : null;
2762
+ const spanId = hex(8);
2763
+ let span = null;
2764
+ if (t && (this.enabled === null || this.sampled)) {
2765
+ const u = stripUrl(abs);
2766
+ span = { span_id: spanId, parent_span_id: t.spanId, trace_id: t.traceId, op: "http.client", description: `${method} ${u}`, start_timestamp: this.ts(now2()), timestamp: 0, origin: "auto.http.browser", data: { "http.method": method, url: u, type } };
2767
+ t.inflight += 1;
2768
+ clearTimeout(t.idle);
2769
+ }
2770
+ return { txn: t, span, spanId, url };
2771
+ }
2772
+ endHttp(live, status, length) {
2773
+ const { txn: t, span } = live;
2774
+ if (!t || !span) return;
2775
+ t.inflight = Math.max(0, t.inflight - 1);
2776
+ t.last = now2();
2777
+ span.timestamp = this.ts(t.last);
2778
+ span.status = status ? httpSpanStatus(status) : "unknown_error";
2779
+ if (status) span.data["http.response.status_code"] = status;
2780
+ if (length && Number(length) >= 0) span.data["http.response_content_length"] = Number(length);
2781
+ if (!t.done && t.spans.length < MAX_SPANS) t.spans.push(span);
2782
+ this.scheduleIdle(t);
2783
+ }
2784
+ /** Trace headers for a request, or null when it must not get any (tracing undecided / off, or not a target). */
2785
+ headers(live, baggage) {
2786
+ if (!this.enabled || this.stopped || !shouldPropagate(live.url, this.o.tracePropagationTargets)) return null;
2787
+ const c = this.o.context();
2788
+ const h = traceHeaders({ traceId: (live.txn ?? this.txn ?? this.pageload).traceId, publicKey: this.o.publicKey, release: c.release, environment: c.environment, sampled: this.sampled, sampleRate: this.rate }, live.spanId);
2789
+ return { ...h, baggage: mergeBaggage(baggage, h.baggage) };
2790
+ }
2791
+ instrumentFetch() {
2792
+ const win = window;
2793
+ const orig = win.fetch;
2794
+ if (typeof orig !== "function") return;
2795
+ const patched = (input, init) => {
2796
+ let live = null;
2797
+ try {
2798
+ const isReq = typeof Request !== "undefined" && input instanceof Request;
2799
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
2800
+ live = this.startHttp(String(init?.method || (isReq ? input.method : "GET")).toUpperCase(), url, "fetch");
2801
+ if (live && typeof Headers !== "undefined") {
2802
+ const headers = new Headers(init?.headers ?? (isReq ? input.headers : void 0));
2803
+ const add = headers.has("sentry-trace") ? null : this.headers(live, headers.get("baggage"));
2804
+ if (add) {
2805
+ for (const [k, v] of Object.entries(add)) if (k !== "traceparent" || !headers.has(k)) headers.set(k, v);
2806
+ init = { ...init, headers };
2807
+ }
2808
+ }
2809
+ } catch {
2810
+ }
2811
+ const p = orig.call(win, input, init);
2812
+ if (live?.span) {
2813
+ const l = live;
2814
+ p.then(
2815
+ (res) => this.endHttp(l, res.status, res.headers?.get?.("content-length")),
2816
+ () => this.endHttp(l, 0)
2817
+ );
2818
+ }
2819
+ return p;
2820
+ };
2821
+ win.fetch = patched;
2822
+ this.cleanups.push(() => {
2823
+ if (win.fetch === patched) win.fetch = orig;
2824
+ });
2825
+ }
2826
+ instrumentXhr() {
2827
+ const proto = typeof XMLHttpRequest !== "undefined" ? XMLHttpRequest.prototype : null;
2828
+ if (!proto) return;
2829
+ const { open, send, setRequestHeader } = proto;
2830
+ const self = { startHttp: this.startHttp.bind(this), headers: this.headers.bind(this), endHttp: this.endHttp.bind(this) };
2831
+ const meta = /* @__PURE__ */ new WeakMap();
2832
+ const pOpen = function(method, url, ...rest) {
2833
+ meta.set(this, { method: String(method).toUpperCase(), url: String(url), headers: {} });
2834
+ return open.call(this, method, url, ...rest);
2835
+ };
2836
+ const pSetHeader = function(name, value) {
2837
+ const m = meta.get(this);
2838
+ if (m) m.headers[String(name).toLowerCase()] = String(value);
2839
+ return setRequestHeader.call(this, name, value);
2840
+ };
2841
+ const pSend = function(body) {
2842
+ try {
2843
+ const m = meta.get(this);
2844
+ const live = m ? self.startHttp(m.method, m.url, "xhr") : null;
2845
+ if (live && m && !m.headers["sentry-trace"]) {
2846
+ const existing = m.headers.baggage ?? null;
2847
+ const add = self.headers(live, null);
2848
+ if (add) {
2849
+ setRequestHeader.call(this, "sentry-trace", add["sentry-trace"]);
2850
+ if (!m.headers.traceparent) setRequestHeader.call(this, "traceparent", add.traceparent);
2851
+ if (!existing?.includes("sentry-")) setRequestHeader.call(this, "baggage", add.baggage);
2852
+ }
2853
+ }
2854
+ if (live?.span) this.addEventListener("loadend", () => self.endHttp(live, this.status, safeHeader(this, "content-length")), { once: true });
2855
+ } catch {
2856
+ }
2857
+ return send.call(this, body);
2858
+ };
2859
+ proto.open = pOpen;
2860
+ proto.setRequestHeader = pSetHeader;
2861
+ proto.send = pSend;
2862
+ this.cleanups.push(() => {
2863
+ if (proto.open === pOpen) proto.open = open;
2864
+ if (proto.setRequestHeader === pSetHeader) proto.setRequestHeader = setRequestHeader;
2865
+ if (proto.send === pSend) proto.send = send;
2866
+ });
2867
+ }
2868
+ // --- sending ----------------------------------------------------------------------------------
2869
+ enqueue(e) {
2870
+ this.out.push(e);
2871
+ if (this.out.length > MAX_QUEUE) this.out.shift();
2872
+ this.flushTimer ?? (this.flushTimer = setTimeout(() => this.flush(), this.o.flushMs ?? 1e3));
2873
+ }
2874
+ /** Sends queued transactions (`hide`: with `sendBeacon` / keepalive, the page is going away). */
2875
+ flush(hide = false) {
2876
+ if (this.flushTimer) clearTimeout(this.flushTimer);
2877
+ this.flushTimer = null;
2878
+ if (!this.out.length) return;
2879
+ const allowed = this.o.allowed();
2880
+ if (allowed === false) {
2881
+ this.out = [];
2882
+ return;
2883
+ }
2884
+ if (allowed === null) return;
2885
+ const { api } = this.o;
2886
+ if (!hide && !api.health.canSend()) {
2887
+ this.flushTimer = setTimeout(() => this.flush(), api.health.waitMs());
2888
+ return;
2889
+ }
2890
+ while (this.out.length) {
2891
+ const batch = this.out.splice(0, BATCH);
2892
+ const body = { sdk: { name: TRACES_SDK_NAME, version: this.o.sdkVersion }, transactions: batch };
2893
+ if (hide && api.beacon("/traces", body)) continue;
2894
+ api.post("/traces", body, { keepalive: hide }).then(
2895
+ (res) => isServerFailure(res.status) && this.retry(batch),
2896
+ () => this.retry(batch)
2897
+ );
2898
+ }
2899
+ }
2900
+ retry(batch) {
2901
+ if (this.stopped) return;
2902
+ this.out.unshift(...batch);
2903
+ this.out.splice(MAX_QUEUE);
2904
+ this.flushTimer ?? (this.flushTimer = setTimeout(() => this.flush(), Math.max(5e3, this.o.api.health.waitMs())));
2905
+ }
2906
+ };
2907
+ function safeHeader(xhr, name) {
2908
+ try {
2909
+ return xhr.getResponseHeader(name);
2910
+ } catch {
2911
+ return null;
2912
+ }
2913
+ }
2914
+
2915
+ // src/bug-report.ts
2916
+ var import_runtime22 = require("@guidelytech/shared/runtime");
2917
+ async function encodeScreenshot(s) {
2918
+ if (!s) return null;
2919
+ if (!(typeof Blob !== "undefined" && s instanceof Blob)) {
2920
+ const encoded = s;
2921
+ const data = encoded.data.replace(/^data:[^,]*,/, "");
2922
+ if (data.length * 0.75 > import_runtime22.MAX_BUG_REPORT_SCREENSHOT_BYTES) throw new Error("[Guidely] captureFeedback: screenshots can be up to 4 MB");
2923
+ return { fileName: encoded.fileName || "screenshot.png", data };
2924
+ }
2925
+ if (s.size > import_runtime22.MAX_BUG_REPORT_SCREENSHOT_BYTES) throw new Error("[Guidely] captureFeedback: screenshots can be up to 4 MB");
2926
+ const dataUrl = await new Promise((resolve, reject) => {
2927
+ const r = new FileReader();
2928
+ r.onload = () => resolve(String(r.result));
2929
+ r.onerror = () => reject(new Error("[Guidely] captureFeedback: the screenshot couldn't be read"));
2930
+ r.readAsDataURL(s);
2931
+ });
2932
+ const name = s.name || (s.type === "image/jpeg" ? "screenshot.jpg" : "screenshot.png");
2933
+ return { fileName: name, data: dataUrl.slice(dataUrl.indexOf(",") + 1) };
2934
+ }
2935
+ function bugReportBody(input, ctx, source, screenshot) {
2936
+ const tags = {};
2937
+ for (const [k, v] of Object.entries(input.tags ?? {}).slice(0, 20)) if (k && v !== void 0 && v !== null) tags[k.slice(0, 64)] = String(v).slice(0, 200);
2938
+ const email = input.email?.trim();
2939
+ const name = input.name?.trim();
2940
+ const associated = input.associatedEventId ?? ctx.lastExceptionId;
2941
+ return {
2942
+ anonymousId: ctx.anonymousId,
2943
+ userId: ctx.userId,
2944
+ sessionId: ctx.sessionId,
2945
+ message: String(input.message ?? "").trim().slice(0, import_runtime22.MAX_BUG_REPORT_LENGTH),
2946
+ ...email ? { email } : {},
2947
+ ...name ? { name } : {},
2948
+ ...associated ? { associatedEventId: associated } : {},
2949
+ ...ctx.url ? { url: ctx.url } : {},
2950
+ ...ctx.appVersion ? { appVersion: ctx.appVersion } : {},
2951
+ source,
2952
+ ...Object.keys(tags).length ? { tags } : {},
2953
+ sdk: ctx.sdk,
2954
+ ...screenshot ? { screenshot } : {}
2955
+ };
2956
+ }
2957
+ async function sendBugReport(api, body) {
2958
+ const res = await api.request("POST", "/bug-reports", { body, timeoutMs: body.screenshot ? 6e4 : 2e4 });
2959
+ if (res.ok && res.data && "id" in res.data) return res.data;
2960
+ if (res.status === 429) throw new Error("You've sent several reports just now. Please try again later.");
2961
+ const message = res.data && "error" in res.data ? res.data.error?.message : null;
2962
+ throw new Error(res.status === 400 && message ? message : "Your report couldn't be sent. Please try again.");
2963
+ }
2964
+
2119
2965
  // src/client.ts
2120
2966
  var SDK_NAME = "@guidelytech/sdk-core";
2121
- var SDK_VERSION = "0.16.0";
2967
+ var SDK_VERSION = "0.17.0";
2122
2968
  var DEFAULT_API_URL = "https://api.guidely.app";
2123
2969
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
2124
2970
  var DAY_MS = 24 * 60 * 60 * 1e3;
2125
2971
  var MAX_SESSION_EXCEPTIONS = 100;
2126
2972
  var CACHED_START_MS = 1500;
2127
- function settlesWithin(promise, ms) {
2973
+ function settlesWithin(promise, ms2) {
2128
2974
  return new Promise((resolve) => {
2129
- const timer = setTimeout(() => resolve(false), ms);
2975
+ const timer = setTimeout(() => resolve(false), ms2);
2130
2976
  const done = () => {
2131
2977
  clearTimeout(timer);
2132
2978
  resolve(true);
@@ -2143,8 +2989,8 @@ function nowIso() {
2143
2989
  function emptyState() {
2144
2990
  return { guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] };
2145
2991
  }
2146
- function newSession(now = Date.now()) {
2147
- return { id: `s_${(0, import_runtime18.randomId)(16)}`, startedAt: new Date(now).toISOString(), shownGuides: [], events: [], lastActivityAt: now };
2992
+ function newSession(now3 = Date.now()) {
2993
+ return { id: `s_${(0, import_runtime23.randomId)(16)}`, startedAt: new Date(now3).toISOString(), shownGuides: [], events: [], lastActivityAt: now3 };
2148
2994
  }
2149
2995
  var GuidelyClient = class _GuidelyClient {
2150
2996
  constructor(options) {
@@ -2159,6 +3005,8 @@ var GuidelyClient = class _GuidelyClient {
2159
3005
  /** Agent Analytics turns (agentConversation), in their own outbox. */
2160
3006
  __publicField(this, "agentTurns");
2161
3007
  __publicField(this, "config", null);
3008
+ /** The `environment` option as passed (the resolved one is in `options.environment`). */
3009
+ __publicField(this, "envOption");
2162
3010
  __publicField(this, "etag", null);
2163
3011
  __publicField(this, "anonymousId");
2164
3012
  __publicField(this, "user", null);
@@ -2200,6 +3048,11 @@ var GuidelyClient = class _GuidelyClient {
2200
3048
  __publicField(this, "exceptionThrottle", new ExceptionThrottle());
2201
3049
  /** Exceptions seen before the config was loaded (reported once it is known whether the server accepts them). */
2202
3050
  __publicField(this, "earlyExceptions", []);
3051
+ /** The last exception this page reported, which bug reports link to by default. */
3052
+ __publicField(this, "lastExceptionId", null);
3053
+ __publicField(this, "bugReportDialog", null);
3054
+ /** Tags, extra, contexts and breadcrumbs sent with every error (`setTag`, `addBreadcrumb`…). */
3055
+ __publicField(this, "errorScope", new ErrorScope(() => this.options.errors));
2203
3056
  __publicField(this, "initialPageViewSent", false);
2204
3057
  __publicField(this, "pageViewUrl", null);
2205
3058
  __publicField(this, "pageLeft", false);
@@ -2211,6 +3064,8 @@ var GuidelyClient = class _GuidelyClient {
2211
3064
  __publicField(this, "locale", null);
2212
3065
  /** Language each shown guide was rendered in; added to its events as `locale`. */
2213
3066
  __publicField(this, "guideLocales", /* @__PURE__ */ new Map());
3067
+ /** Performance tracing (`tracing` option + the project's `performance` setting); see tracing.ts. */
3068
+ __publicField(this, "tracer");
2214
3069
  __publicField(this, "customListeners", /* @__PURE__ */ new Map());
2215
3070
  __publicField(this, "eventBus", {
2216
3071
  on: (name, cb) => {
@@ -2227,9 +3082,10 @@ var GuidelyClient = class _GuidelyClient {
2227
3082
  });
2228
3083
  if (!isBrowser()) throw new Error("[Guidely] createGuidely() must be called in the browser. Create the client inside useEffect / onMount, or use the framework adapters.");
2229
3084
  if (!options || !options.projectId) throw new Error("[Guidely] `projectId` (your environment public key, e.g. pk_dev_\u2026) is required.");
3085
+ this.envOption = options.environment || void 0;
2230
3086
  this.options = {
2231
3087
  ...options,
2232
- environment: options.environment ?? (options.projectId.startsWith("pk_prod") ? "production" : options.projectId.startsWith("pk_stag") ? "staging" : "development"),
3088
+ environment: options.environment || environmentFromKey(options.projectId),
2233
3089
  apiUrl: options.apiUrl ?? DEFAULT_API_URL,
2234
3090
  analytics: options.analytics ?? true,
2235
3091
  consent: options.consent ?? "granted",
@@ -2245,7 +3101,7 @@ var GuidelyClient = class _GuidelyClient {
2245
3101
  this.sessionStore = this.options.storage === "memory" ? this.store : createStorage("session");
2246
3102
  this.anonymousId = this.store.get(KEYS.anon) || "";
2247
3103
  if (!this.anonymousId) {
2248
- this.anonymousId = `anon_${(0, import_runtime18.randomId)(20)}`;
3104
+ this.anonymousId = `anon_${(0, import_runtime23.randomId)(20)}`;
2249
3105
  this.store.set(KEYS.anon, this.anonymousId);
2250
3106
  }
2251
3107
  this.firstVisit = !this.store.get(KEYS.seen);
@@ -2273,6 +3129,16 @@ var GuidelyClient = class _GuidelyClient {
2273
3129
  this.agentTurns = new AgentTurns({ api: this.api, store: this.store, storageKey: KEYS.agentOutbox(this.options.projectId), health: this.api.health, sdk: { name: SDK_NAME, version: SDK_VERSION } });
2274
3130
  if (options.user) this.user = { ...options.user };
2275
3131
  this.flags = this.createFlags(options.flags);
3132
+ this.tracer = options.tracing === false ? null : startTracer({
3133
+ ...typeof options.tracing === "object" ? options.tracing : {},
3134
+ api: this.api,
3135
+ publicKey: this.options.projectId,
3136
+ apiUrl: this.options.apiUrl,
3137
+ sdkVersion: SDK_VERSION,
3138
+ pageHref: (href) => this.pageHref(href),
3139
+ context: () => ({ anonymousId: this.anonymousId, sessionId: this.session.id, userId: this.user?.id || null, release: this.options.release || this.appVersion(), environment: this.environment, collectUrls: this.collectUrls() }),
3140
+ allowed: () => this.consent === "pending" ? null : this.analyticsEnabled() && !this.editing()
3141
+ });
2276
3142
  }
2277
3143
  createFlags(opts) {
2278
3144
  const base = { sdkKey: this.options.projectId, apiUrl: this.options.apiUrl, fetch: this.options.fetch, sdk: { name: SDK_NAME, version: SDK_VERSION } };
@@ -2327,7 +3193,7 @@ var GuidelyClient = class _GuidelyClient {
2327
3193
  try {
2328
3194
  const cached = readJson(this.store, KEYS.config(this.options.projectId), null);
2329
3195
  this.etag = this.store.get(KEYS.etag(this.options.projectId));
2330
- if (cached && cached.guides) this.config = webOnly(cached);
3196
+ if (cached && cached.guides) this.useConfig(webOnly(cached));
2331
3197
  const before = this.config?.configVersion;
2332
3198
  const fresh = this.fetchConfig();
2333
3199
  if (!this.config) await fresh;
@@ -2356,12 +3222,22 @@ var GuidelyClient = class _GuidelyClient {
2356
3222
  this.resolveReady?.();
2357
3223
  }
2358
3224
  }
3225
+ /** Environment guides and flags target: the `environment` option, else the one the loaded config names, else a guess from the key. */
3226
+ get environment() {
3227
+ return this.envOption ?? this.config?.environment ?? this.options.environment;
3228
+ }
3229
+ useConfig(config) {
3230
+ const env = this.environment;
3231
+ this.config = config;
3232
+ if (this.environment !== env) this.syncFlagAttributes();
3233
+ this.tracer?.configure(config.settings?.performance);
3234
+ }
2359
3235
  async fetchConfig() {
2360
3236
  try {
2361
3237
  const res = await this.api.get("/config", { etag: this.etag, params: { platform: "web" } });
2362
3238
  if (res.status === 304) return true;
2363
3239
  if (res.ok && res.data && Array.isArray(res.data.guides)) {
2364
- this.config = webOnly(res.data);
3240
+ this.useConfig(webOnly(res.data));
2365
3241
  this.etag = res.etag;
2366
3242
  writeJson(this.store, KEYS.config(this.options.projectId), res.data);
2367
3243
  if (res.etag) this.store.set(KEYS.etag(this.options.projectId), res.etag);
@@ -2386,7 +3262,7 @@ var GuidelyClient = class _GuidelyClient {
2386
3262
  this.cleanups.push(() => doc.removeEventListener("click", onClick, true));
2387
3263
  const onArticleLink = (e) => {
2388
3264
  if (e.defaultPrevented) return;
2389
- const articleId = (0, import_runtime18.articleIdFromHref)((0, import_guide_runtime5.composedTarget)(e)?.closest?.("a[href]")?.getAttribute("href"));
3265
+ const articleId = (0, import_runtime23.articleIdFromHref)((0, import_guide_runtime5.composedTarget)(e)?.closest?.("a[href]")?.getAttribute("href"));
2390
3266
  if (!articleId) return;
2391
3267
  e.preventDefault();
2392
3268
  this.openArticle(articleId);
@@ -2414,9 +3290,9 @@ var GuidelyClient = class _GuidelyClient {
2414
3290
  };
2415
3291
  let lastScroll = 0;
2416
3292
  const onScroll = () => {
2417
- const now = Date.now();
2418
- if (now - lastScroll < 250) return;
2419
- lastScroll = now;
3293
+ const now3 = Date.now();
3294
+ if (now3 - lastScroll < 250) return;
3295
+ lastScroll = now3;
2420
3296
  this.pageTracker.measureScroll();
2421
3297
  this.touchSession();
2422
3298
  };
@@ -2474,14 +3350,22 @@ var GuidelyClient = class _GuidelyClient {
2474
3350
  /** Uncaught errors + unhandled rejections; attached before the config loads so boot errors are kept. */
2475
3351
  wireErrorListeners() {
2476
3352
  const win = window;
2477
- const onError = (e) => this.captureGlobalException(errorEventToException(e));
2478
- const onRejection = (e) => this.captureGlobalException(rejectionToException(e));
3353
+ const onError = (e) => this.captureGlobalException(errorEventToException(e), void 0, e.error);
3354
+ const onRejection = (e) => this.captureGlobalException(rejectionToException(e), void 0, e.reason);
2479
3355
  win.addEventListener("error", onError);
2480
3356
  win.addEventListener("unhandledrejection", onRejection);
2481
3357
  this.cleanups.push(() => {
2482
3358
  win.removeEventListener("error", onError);
2483
3359
  win.removeEventListener("unhandledrejection", onRejection);
2484
3360
  });
3361
+ const crumbs = this.options.errors?.breadcrumbs;
3362
+ if (crumbs !== false) this.cleanups.push(instrumentBreadcrumbs((c, hint) => this.errorScope.addBreadcrumb(c, hint), { console: crumbs?.console, http: crumbs?.http, apiUrl: this.options.apiUrl }));
3363
+ }
3364
+ /** An automatic breadcrumb, unless the `errors.breadcrumbs` option turns its kind off. */
3365
+ autoBreadcrumb(kind, crumb, hint) {
3366
+ const o = this.options.errors?.breadcrumbs;
3367
+ if (o === false || o && o[kind] === false) return;
3368
+ this.errorScope.addBreadcrumb(crumb, hint === void 0 ? void 0 : { event: hint });
2485
3369
  }
2486
3370
  /** Re-fetches the published config and re-evaluates triggers. */
2487
3371
  async refresh() {
@@ -2529,6 +3413,7 @@ var GuidelyClient = class _GuidelyClient {
2529
3413
  this.queue.destroy();
2530
3414
  this.outbox.destroy();
2531
3415
  this.agentTurns.destroy();
3416
+ this.tracer?.destroy();
2532
3417
  this.flags.destroy();
2533
3418
  this.emitter.clear();
2534
3419
  (0, import_guide_runtime5.destroyHost)();
@@ -2544,7 +3429,7 @@ var GuidelyClient = class _GuidelyClient {
2544
3429
  this.user = { id: String(userId), traits: { ...this.user?.id === String(userId) ? this.user.traits : {}, ...traits || {} }, account: account ?? (this.user?.id === String(userId) ? this.user.account : void 0) };
2545
3430
  writeJson(this.store, KEYS.user(this.options.projectId), this.user);
2546
3431
  this.syncFlagAttributes();
2547
- this.enqueue("identify", { properties: { traits: (0, import_runtime18.sanitizeProperties)(this.user.traits) } });
3432
+ this.enqueue("identify", { properties: { traits: (0, import_runtime23.sanitizeProperties)(this.user.traits) } });
2548
3433
  await this.syncIdentify();
2549
3434
  this.emitter.emit("identify", { userId: this.user.id, anonymousId: this.anonymousId });
2550
3435
  if (this.isReady) this.evaluate("identify");
@@ -2620,7 +3505,7 @@ var GuidelyClient = class _GuidelyClient {
2620
3505
  this.stopGuide();
2621
3506
  this.user = null;
2622
3507
  this.store.remove(KEYS.user(this.options.projectId));
2623
- this.anonymousId = `anon_${(0, import_runtime18.randomId)(20)}`;
3508
+ this.anonymousId = `anon_${(0, import_runtime23.randomId)(20)}`;
2624
3509
  this.store.set(KEYS.anon, this.anonymousId);
2625
3510
  this.state = emptyState();
2626
3511
  this.persistState();
@@ -2658,6 +3543,11 @@ var GuidelyClient = class _GuidelyClient {
2658
3543
  analyticsEnabled() {
2659
3544
  return this.options.analytics && this.consent === "granted" && (this.config?.settings?.analyticsEnabled ?? true);
2660
3545
  }
3546
+ /** The host app's version (`appVersion` option), sent with every event and recording. */
3547
+ appVersion() {
3548
+ const v = this.options.appVersion;
3549
+ return v === void 0 || v === null || String(v).trim() === "" ? void 0 : String(v).trim().slice(0, 200);
3550
+ }
2661
3551
  collectUrls() {
2662
3552
  return this.config?.settings?.collectUrls ?? true;
2663
3553
  }
@@ -2665,7 +3555,7 @@ var GuidelyClient = class _GuidelyClient {
2665
3555
  if (this.consent === "denied" || !this.options.analytics) return null;
2666
3556
  if (name !== "page_leave" && name !== "heatmap") this.touchSession();
2667
3557
  const event = {
2668
- eventId: (0, import_runtime18.randomId)(24),
3558
+ eventId: (0, import_runtime23.randomId)(24),
2669
3559
  name,
2670
3560
  occurredAt: nowIso(),
2671
3561
  anonymousId: this.anonymousId,
@@ -2673,12 +3563,14 @@ var GuidelyClient = class _GuidelyClient {
2673
3563
  accountId: this.user?.account?.id || null,
2674
3564
  sessionId: this.session.id,
2675
3565
  url: this.collectUrls() ? this.pageHref() : void 0,
3566
+ ...this.appVersion() ? { appVersion: this.appVersion() } : {},
2676
3567
  ...extra
2677
3568
  };
2678
- if (extra.properties) event.properties = (0, import_runtime18.sanitizeProperties)(extra.properties);
3569
+ if (extra.properties) event.properties = (0, import_runtime23.sanitizeProperties)(extra.properties);
2679
3570
  const locale = extra.guideId ? this.guideLocales.get(extra.guideId) : void 0;
2680
3571
  if (locale) event.properties = { ...event.properties ?? {}, locale };
2681
3572
  this.queue.enqueue(event);
3573
+ if (name.startsWith("guide_") || name.startsWith("step_")) this.autoBreadcrumb("guides", { category: "guide", message: name, data: { guideId: event.guideId, ...event.stepIndex !== void 0 ? { stepIndex: event.stepIndex } : {} } });
2682
3574
  this.replay?.addCustomEvent("guidely", { name, customName: event.customName, guideId: event.guideId });
2683
3575
  this.emitter.emit("track", event);
2684
3576
  return event;
@@ -2732,6 +3624,7 @@ var GuidelyClient = class _GuidelyClient {
2732
3624
  handleNavigation(url, explicit = false) {
2733
3625
  if (this.destroyed) return;
2734
3626
  const changed = url !== this.lastUrl;
3627
+ if (changed && this.lastUrl) this.autoBreadcrumb("navigation", { category: "navigation", data: this.collectUrls() ? { from: breadcrumbUrl(this.lastUrl), to: breadcrumbUrl(url) } : {} });
2735
3628
  this.lastUrl = url;
2736
3629
  if (changed) this.syncFlagAttributes();
2737
3630
  if (!explicit && changed) {
@@ -2760,6 +3653,7 @@ var GuidelyClient = class _GuidelyClient {
2760
3653
  handleDocumentClick(e) {
2761
3654
  const target = (0, import_guide_runtime5.composedTarget)(e);
2762
3655
  if (!target || typeof target.closest !== "function") return;
3656
+ if (!isIgnoredElement(target)) this.autoBreadcrumb("clicks", { category: "ui.click", message: elementSelector(interactiveElement(target) ?? target) }, e);
2763
3657
  this.autocaptureClick(e, target);
2764
3658
  this.completeChecklistTasksBy("click", target);
2765
3659
  if (this.isReady) this.evaluate("click", { target });
@@ -2769,17 +3663,17 @@ var GuidelyClient = class _GuidelyClient {
2769
3663
  // ---------------------------------------------------------------------------
2770
3664
  /** Records activity; starts a new session after `sessionTimeoutMinutes` of inactivity or 24h. */
2771
3665
  touchSession() {
2772
- const now = Date.now();
3666
+ const now3 = Date.now();
2773
3667
  const s = this.session;
2774
3668
  const timeout = Math.max(1, this.config?.settings?.sessionTimeoutMinutes ?? 30) * 6e4;
2775
- const last = s.lastActivityAt ?? now;
2776
- if (now - last > timeout || now - (Date.parse(s.startedAt) || now) > DAY_MS) return this.rotateSession(now);
2777
- if (now - (s.lastActivityAt ?? 0) < 1e3) return;
2778
- s.lastActivityAt = now;
3669
+ const last = s.lastActivityAt ?? now3;
3670
+ if (now3 - last > timeout || now3 - (Date.parse(s.startedAt) || now3) > DAY_MS) return this.rotateSession(now3);
3671
+ if (now3 - (s.lastActivityAt ?? 0) < 1e3) return;
3672
+ s.lastActivityAt = now3;
2779
3673
  this.persistSession();
2780
3674
  }
2781
- rotateSession(now) {
2782
- this.session = newSession(now);
3675
+ rotateSession(now3) {
3676
+ this.session = newSession(now3);
2783
3677
  this.persistSession();
2784
3678
  this.log("session rotated", this.session.id);
2785
3679
  if (this.replay?.activeSessionId) this.replay.stop();
@@ -2811,7 +3705,7 @@ var GuidelyClient = class _GuidelyClient {
2811
3705
  const device = detectDevice(navigator.userAgent, window.innerWidth);
2812
3706
  const props = { deviceType: device.type, browser: device.browser, os: device.os, screenWidth: window.screen?.width, viewportWidth: window.innerWidth };
2813
3707
  if (collect) {
2814
- props.path = pathOf2(url);
3708
+ props.path = pathOf3(url);
2815
3709
  props.title = (document.title || "").slice(0, 200);
2816
3710
  const ref = !this.session.pageViews ? externalReferrer() : void 0;
2817
3711
  if (ref) props.referrer = ref;
@@ -2830,7 +3724,7 @@ var GuidelyClient = class _GuidelyClient {
2830
3724
  if (!this.pageLeft && this.autocaptureOn("pageLeave")) {
2831
3725
  const info = t.leave();
2832
3726
  const collect = this.collectUrls();
2833
- this.enqueue("page_leave", { url: collect ? info.url : void 0, properties: { ...collect ? { path: pathOf2(info.url) } : {}, durationMs: info.durationMs, scrollDepth: info.scrollDepth } });
3727
+ this.enqueue("page_leave", { url: collect ? info.url : void 0, properties: { ...collect ? { path: pathOf3(info.url) } : {}, durationMs: info.durationMs, scrollDepth: info.scrollDepth } });
2834
3728
  }
2835
3729
  if (nextUrl !== null) {
2836
3730
  t.start(nextUrl);
@@ -2874,42 +3768,120 @@ var GuidelyClient = class _GuidelyClient {
2874
3768
  if (!batch || !this.captureAllowed()) return;
2875
3769
  const collect = this.collectUrls();
2876
3770
  const de = document.documentElement;
2877
- const properties = { path: collect ? pathOf2(batch.url) : void 0, viewportWidth: window.innerWidth, viewportHeight: window.innerHeight, pageHeight: Math.max(de.scrollHeight, document.body?.scrollHeight || 0), points: batch.points };
3771
+ const properties = { path: collect ? pathOf3(batch.url) : void 0, viewportWidth: window.innerWidth, viewportHeight: window.innerHeight, pageHeight: Math.max(de.scrollHeight, document.body?.scrollHeight || 0), points: batch.points };
2878
3772
  if (!collect) delete properties.path;
2879
3773
  this.enqueue("heatmap", { url: collect ? batch.url : void 0, properties });
2880
3774
  }
2881
- captureGlobalException(props) {
3775
+ captureGlobalException(props, detail, error) {
2882
3776
  if (!props || isGuidelyException(props) || this.options.captureErrors === false) return;
3777
+ const d = detail ?? this.errorDetail(error, props, props.source === "unhandledrejection" ? "onunhandledrejection" : "onerror", {}, "error");
2883
3778
  if (!this.config) {
2884
- if (this.earlyExceptions.length < 10) this.earlyExceptions.push([props, void 0]);
3779
+ if (this.earlyExceptions.length < 10) this.earlyExceptions.push([props, void 0, d]);
2885
3780
  return;
2886
3781
  }
2887
- this.sendException(props);
3782
+ this.sendException(props, void 0, d);
2888
3783
  }
2889
3784
  /**
2890
3785
  * Reports a caught error as an `exception` event (`source: "manual"`, `handled: true`), grouped
2891
- * into an issue on the Errors page. Extra properties are attached to the occurrence. Needs the
2892
- * project's error tracking on (plus analytics + consent); `captureErrors: false` does not stop it.
3786
+ * into an issue on the Errors page. The second argument takes Sentry's capture context (`tags`,
3787
+ * `extra`, `level`, `fingerprint`, `contexts`, `user`); any other key is attached to the occurrence
3788
+ * as a property. Needs the project's error tracking on (plus analytics + consent);
3789
+ * `captureErrors: false` does not stop it. Returns the event id (empty when nothing is sent).
2893
3790
  */
2894
- captureException(error, properties) {
2895
- if (this.destroyed) return;
2896
- const props = exceptionFromError(error, "manual", true);
3791
+ captureException(error, hint) {
3792
+ if (this.destroyed) return "";
3793
+ const { ctx, properties } = splitHint(hint);
3794
+ const props = exceptionFromError(error, "manual", ctx.mechanism?.handled ?? true);
3795
+ return this.captureWith(props, properties, this.errorDetail(error, props, ctx.mechanism?.type ?? "generic", ctx, ctx.level ?? "error"));
3796
+ }
3797
+ /** Reports a message (default level `info`) as its own issue, grouped by the message. */
3798
+ captureMessage(message, level = "info", hint) {
3799
+ if (this.destroyed) return "";
3800
+ const props = { type: "Message", message: String(message).slice(0, 1e3), frames: [], source: "message", handled: true };
3801
+ return this.captureWith(props, {}, this.errorDetail(void 0, props, "message", hint ?? {}, hint?.level ?? level));
3802
+ }
3803
+ captureWith(props, properties, detail) {
2897
3804
  if (!this.config) {
2898
- if (this.earlyExceptions.length < 10) this.earlyExceptions.push([props, properties]);
2899
- return;
3805
+ if (this.earlyExceptions.length < 10) this.earlyExceptions.push([props, properties, detail]);
3806
+ return "";
2900
3807
  }
2901
- this.sendException(props, properties);
3808
+ return this.sendException(props, properties, detail);
3809
+ }
3810
+ /** The `detail` of an error: scope, breadcrumbs, browser contexts and the `cause` chain. */
3811
+ errorDetail(error, props, mechanism, ctx, level) {
3812
+ const device = detectDevice(navigator.userAgent, window.innerWidth);
3813
+ return buildDetail({ error, props, mechanism, scope: this.errorScope, ctx, level, contexts: browserContexts(navigator.userAgent, device) });
3814
+ }
3815
+ /** Adds a breadcrumb (sent with the next errors; the last 100 are kept). */
3816
+ addBreadcrumb(crumb) {
3817
+ if (!this.destroyed) this.errorScope.addBreadcrumb(crumb);
3818
+ }
3819
+ /** Tags every later error (searchable on the issue page). */
3820
+ setTag(key, value) {
3821
+ if (value === null || value === void 0) delete this.errorScope.tags[key];
3822
+ else this.errorScope.tags[key] = String(value);
3823
+ }
3824
+ setTags(tags) {
3825
+ for (const [k, v] of Object.entries(tags)) this.setTag(k, v);
3826
+ }
3827
+ /** Arbitrary data attached to every later error. */
3828
+ setExtra(key, value) {
3829
+ if (value === void 0) delete this.errorScope.extra[key];
3830
+ else this.errorScope.extra[key] = value;
2902
3831
  }
2903
- sendException(props, extra) {
2904
- if (!this.config?.settings?.errorTracking?.enabled || !this.captureAllowed() || !this.exceptionThrottle.allow(props)) return;
3832
+ setExtras(extras) {
3833
+ for (const [k, v] of Object.entries(extras)) this.setExtra(k, v);
3834
+ }
3835
+ /** A named context (`setContext("cart", { items: 3 })`) on every later error; null removes it. */
3836
+ setContext(name, context) {
3837
+ if (context) this.errorScope.contexts[name] = context;
3838
+ else delete this.errorScope.contexts[name];
3839
+ }
3840
+ /**
3841
+ * Sentry's `setUser`: identifies the user by `id` (like `identify`) and keeps `email` / `username`
3842
+ * on their errors. `null` forgets the user (`reset()`).
3843
+ */
3844
+ setUser(user) {
3845
+ if (!user) {
3846
+ this.errorScope.user = {};
3847
+ return this.reset();
3848
+ }
3849
+ const { id, email, username, ...traits } = user;
3850
+ this.errorScope.user = { ...email ? { email: String(email) } : {}, ...username ? { username: String(username) } : {} };
3851
+ if (id !== void 0 && id !== null && String(id)) void this.identify(String(id), { ...traits, ...email ? { email } : {}, ...username ? { username } : {} });
3852
+ }
3853
+ sendException(props, extra, detail) {
3854
+ if (!this.config?.settings?.errorTracking?.enabled || !this.captureAllowed()) return "";
3855
+ const o = this.options.errors;
3856
+ const file = props.frames.find((f) => f.file)?.file;
3857
+ if (o && (0, import_runtime23.errorFilterDrops)({ type: props.type, message: props.message, file }, o)) return "";
3858
+ if (o?.sampleRate !== void 0 && o.sampleRate < 1 && Math.random() >= Math.max(0, o.sampleRate)) return "";
3859
+ if (!this.exceptionThrottle.allow(props)) return "";
2905
3860
  const count = this.session.exceptions ?? 0;
2906
- if (count >= MAX_SESSION_EXCEPTIONS) return;
2907
- const properties = { ...props };
3861
+ if (count >= MAX_SESSION_EXCEPTIONS) return "";
3862
+ const mechanism = detail?.exception?.values.at(-1)?.mechanism?.type;
3863
+ let properties = { ...props, level: detail?.level ?? "error", platform: "javascript", ...mechanism ? { mechanism } : {} };
2908
3864
  if (this.options.release) properties.release = String(this.options.release).slice(0, 100);
3865
+ const trace = this.tracer?.traceContext();
3866
+ if (trace) Object.assign(properties, { traceId: trace.traceId, spanId: trace.spanId });
2909
3867
  for (const [k, v] of Object.entries(extra ?? {})) if (!(k in properties)) properties[k] = v;
2910
- this.enqueue("exception", { properties });
3868
+ let d = detail;
3869
+ if (o?.beforeSend) {
3870
+ try {
3871
+ const out = o.beforeSend({ properties, detail: d ?? {} });
3872
+ if (!out) return "";
3873
+ properties = out.properties;
3874
+ d = out.detail;
3875
+ } catch (err) {
3876
+ this.reportError(err);
3877
+ }
3878
+ }
3879
+ const event = this.enqueue("exception", { properties, ...d ? { detail: d } : {} });
3880
+ if (!event) return "";
3881
+ this.lastExceptionId = event.eventId;
2911
3882
  this.session.exceptions = count + 1;
2912
3883
  this.persistSession();
3884
+ return event.eventId;
2913
3885
  }
2914
3886
  maskSelector() {
2915
3887
  return validSelectors([...DEFAULT_MASK_SELECTORS, ...this.config?.settings?.maskSelectors || [], ...this.options.maskSelectors || []]).join(",");
@@ -2943,9 +3915,9 @@ var GuidelyClient = class _GuidelyClient {
2943
3915
  this.trackPageView(this.pageHref());
2944
3916
  }
2945
3917
  if (this.config) {
2946
- for (const [props, extra] of this.earlyExceptions.splice(0)) {
2947
- if (props.handled) this.sendException(props, extra);
2948
- else this.captureGlobalException(props);
3918
+ for (const [props, extra, detail] of this.earlyExceptions.splice(0)) {
3919
+ if (props.handled) this.sendException(props, extra, detail);
3920
+ else this.captureGlobalException(props, detail);
2949
3921
  }
2950
3922
  }
2951
3923
  this.syncReplay();
@@ -2953,7 +3925,7 @@ var GuidelyClient = class _GuidelyClient {
2953
3925
  syncReplay() {
2954
3926
  const cfg = this.isReady && this.captureAllowed() ? this.replayConfig() : null;
2955
3927
  if (cfg && this.session.replay === void 0) {
2956
- this.session.replay = (0, import_runtime18.rolloutBucket)(this.session.id, "replay") < cfg.sampleRate;
3928
+ this.session.replay = (0, import_runtime23.rolloutBucket)(this.session.id, "replay") < cfg.sampleRate;
2957
3929
  this.persistSession();
2958
3930
  }
2959
3931
  if (!cfg || !this.session.replay) {
@@ -2978,7 +3950,8 @@ var GuidelyClient = class _GuidelyClient {
2978
3950
  url: this.collectUrls() ? this.pageHref() : void 0,
2979
3951
  device: { type: device.type, browser: device.browser, os: device.os },
2980
3952
  viewport: { width: window.innerWidth, height: window.innerHeight },
2981
- sdk: { name: SDK_NAME, version: SDK_VERSION }
3953
+ sdk: { name: SDK_NAME, version: SDK_VERSION },
3954
+ ...this.appVersion() ? { appVersion: this.appVersion() } : {}
2982
3955
  };
2983
3956
  },
2984
3957
  onDisabled: (sessionId) => {
@@ -3052,7 +4025,7 @@ var GuidelyClient = class _GuidelyClient {
3052
4025
  seenGuides: keysOf(Object.keys(guides).filter((id) => (guides[id]?.impressions ?? 0) > 0)),
3053
4026
  completedChecklists: keysOf(completedChecklists),
3054
4027
  audienceIds: this.state.audienceIds,
3055
- environment: this.options.environment,
4028
+ environment: this.environment,
3056
4029
  platform: "web",
3057
4030
  ...this.options.appVersion ? { appVersion: this.options.appVersion } : {}
3058
4031
  });
@@ -3064,7 +4037,7 @@ var GuidelyClient = class _GuidelyClient {
3064
4037
  if (!isWithinSchedule(guide)) return false;
3065
4038
  if (guide.kind === "survey" && (this.state.surveysSubmitted.includes(guide.id) || this.state.surveysSubmitted.includes(guide.key))) return false;
3066
4039
  ctx.rolloutSalt = guide.id;
3067
- return (0, import_runtime18.evaluateRules)(c.targeting, ctx);
4040
+ return (0, import_runtime23.evaluateRules)(c.targeting, ctx);
3068
4041
  }
3069
4042
  /** Guide throttling: may this guide launch automatically now? */
3070
4043
  throttleAllows(guide) {
@@ -3088,14 +4061,14 @@ var GuidelyClient = class _GuidelyClient {
3088
4061
  /** The visitor's language candidates, most preferred first. */
3089
4062
  locales() {
3090
4063
  const nav = typeof navigator !== "undefined" ? [...navigator.languages ?? [], navigator.language] : [];
3091
- return (0, import_runtime18.visitorLocales)(this.locale ?? this.options.locale, this.user?.traits, nav);
4064
+ return (0, import_runtime23.visitorLocales)(this.locale ?? this.options.locale, this.user?.traits, nav);
3092
4065
  }
3093
4066
  defaultLocale() {
3094
4067
  return this.config?.settings.localization?.defaultLocale || "en";
3095
4068
  }
3096
4069
  /** The guide in the visitor's language (remembered so its events carry `locale`). */
3097
4070
  localize(guide) {
3098
- const localized = (0, import_runtime18.localizeGuide)(guide, this.locales(), this.defaultLocale());
4071
+ const localized = (0, import_runtime23.localizeGuide)(guide, this.locales(), this.defaultLocale());
3099
4072
  this.guideLocales.set(guide.id, localized.locale);
3100
4073
  return localized;
3101
4074
  }
@@ -3104,7 +4077,7 @@ var GuidelyClient = class _GuidelyClient {
3104
4077
  * Guides already on screen keep their language; the resource center re-renders.
3105
4078
  */
3106
4079
  setLocale(locale) {
3107
- this.locale = locale ? (0, import_runtime18.normalizeLocale)(locale) || null : null;
4080
+ this.locale = locale ? (0, import_runtime23.normalizeLocale)(locale) || null : null;
3108
4081
  if (this.resourceCenter) {
3109
4082
  this.resourceCenter.destroy();
3110
4083
  this.resourceCenterScope?.destroy();
@@ -3215,17 +4188,17 @@ var GuidelyClient = class _GuidelyClient {
3215
4188
  this.evaluate("visible", { guideId: guide.id });
3216
4189
  };
3217
4190
  if (typeof IntersectionObserver === "undefined") return fire();
3218
- io = new IntersectionObserver((entries) => {
3219
- if (entries.some((e) => e.isIntersecting)) fire();
4191
+ io = new IntersectionObserver((entries2) => {
4192
+ if (entries2.some((e) => e.isIntersecting)) fire();
3220
4193
  });
3221
4194
  io.observe(el);
3222
4195
  }).catch(() => stop());
3223
4196
  }
3224
- setTimer(fn, ms) {
4197
+ setTimer(fn, ms2) {
3225
4198
  const t = setTimeout(() => {
3226
4199
  this.timers.delete(t);
3227
4200
  if (!this.destroyed) fn();
3228
- }, ms);
4201
+ }, ms2);
3229
4202
  this.timers.add(t);
3230
4203
  }
3231
4204
  /** Decides whether a guide can show now, otherwise queues it. */
@@ -3432,7 +4405,7 @@ var GuidelyClient = class _GuidelyClient {
3432
4405
  title: guide.name,
3433
4406
  onDismiss: () => finish(this.state.surveysSubmitted.includes(guide.id) ? "submitted" : "dismissed"),
3434
4407
  onSubmit: (answers) => {
3435
- const responseId = (0, import_runtime18.randomId)(24);
4408
+ const responseId = (0, import_runtime23.randomId)(24);
3436
4409
  this.state.surveysSubmitted.push(guide.id);
3437
4410
  this.persistState();
3438
4411
  this.recordEnd(guide, "completed");
@@ -3479,8 +4452,8 @@ var GuidelyClient = class _GuidelyClient {
3479
4452
  this.resourceCenterScope = scope;
3480
4453
  this.resourceCenter = (0, import_guide_runtime5.renderResourceCenter)({
3481
4454
  scope: scope.el,
3482
- config: (0, import_runtime18.localizeResourceCenter)(rc, this.locales(), this.defaultLocale()),
3483
- evaluate: (rules) => (0, import_runtime18.evaluateRules)(rules, this.getContext()),
4455
+ config: (0, import_runtime23.localizeResourceCenter)(rc, this.locales(), this.defaultLocale()),
4456
+ evaluate: (rules) => (0, import_runtime23.evaluateRules)(rules, this.getContext()),
3484
4457
  announcementsSeenAt: () => this.state.announcementsSeenAt ?? null,
3485
4458
  onAnnouncementsSeen: () => {
3486
4459
  this.state.announcementsSeenAt = nowIso();
@@ -3542,8 +4515,8 @@ var GuidelyClient = class _GuidelyClient {
3542
4515
  const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
3543
4516
  writeJson(this.store, KEYS.newsSeen(this.options.projectId), Array.from(/* @__PURE__ */ new Set([...ids, ...Array.isArray(seen) ? seen : []])).slice(0, 200));
3544
4517
  },
3545
- resourceCenter: this.config.resourceCenter ? (0, import_runtime18.localizeResourceCenter)(this.config.resourceCenter, this.locales(), this.defaultLocale()) : null,
3546
- evaluate: (rules) => (0, import_runtime18.evaluateRules)(rules, this.getContext()),
4518
+ resourceCenter: this.config.resourceCenter ? (0, import_runtime23.localizeResourceCenter)(this.config.resourceCenter, this.locales(), this.defaultLocale()) : null,
4519
+ evaluate: (rules) => (0, import_runtime23.evaluateRules)(rules, this.getContext()),
3547
4520
  audienceIds: () => this.state.audienceIds,
3548
4521
  guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
3549
4522
  onStartGuide: (idOrKey) => this.startGuide(idOrKey),
@@ -3578,12 +4551,12 @@ var GuidelyClient = class _GuidelyClient {
3578
4551
  isAudienceTargeted: (g) => {
3579
4552
  if (!isWithinSchedule(g)) return false;
3580
4553
  ctx.rolloutSalt = g.id;
3581
- return (0, import_runtime18.evaluateRules)(g.config.targeting, ctx);
4554
+ return (0, import_runtime23.evaluateRules)(g.config.targeting, ctx);
3582
4555
  },
3583
4556
  newsSeen: Array.isArray(seen) ? seen : []
3584
4557
  });
3585
4558
  const rc = this.config?.resourceCenter;
3586
- const rcNews = rc?.announcements?.length ? (0, import_runtime18.visibleAnnouncements)((0, import_runtime18.localizeResourceCenter)(rc, this.locales(), this.defaultLocale()).announcements, this.state.audienceIds).map((a) => ({
4559
+ const rcNews = rc?.announcements?.length ? (0, import_runtime23.visibleAnnouncements)((0, import_runtime23.localizeResourceCenter)(rc, this.locales(), this.defaultLocale()).announcements, this.state.audienceIds).map((a) => ({
3587
4560
  id: `rc_${a.id}`,
3588
4561
  key: a.guideId ?? "",
3589
4562
  title: a.title,
@@ -3607,7 +4580,7 @@ var GuidelyClient = class _GuidelyClient {
3607
4580
  rcLocale() {
3608
4581
  const rc = this.config?.resourceCenter;
3609
4582
  if (!rc) return null;
3610
- const locale = (0, import_runtime18.localizeResourceCenter)(rc, this.locales(), this.defaultLocale()).locale;
4583
+ const locale = (0, import_runtime23.localizeResourceCenter)(rc, this.locales(), this.defaultLocale()).locale;
3611
4584
  return locale.toLowerCase() === this.defaultLocale().toLowerCase() ? null : locale;
3612
4585
  }
3613
4586
  async loadArticle(id) {
@@ -3830,6 +4803,49 @@ var GuidelyClient = class _GuidelyClient {
3830
4803
  if (!this.config?.messenger?.enabled || !this.config.messenger.ideas) return;
3831
4804
  void this.loadMessenger().then((m) => m?.openIdeas(requestId));
3832
4805
  }
4806
+ /**
4807
+ * Sends a bug report to Monitor → User feedback (Sentry's `captureFeedback`). It is linked to the visitor,
4808
+ * this session's recording and, unless `associatedEventId` says otherwise, the last error this page
4809
+ * reported. Resolves with the report's id; rejects when it can't be sent.
4810
+ */
4811
+ async captureFeedback(input, source = "api") {
4812
+ if (this.destroyed) throw new Error("[Guidely] captureFeedback: the SDK was destroyed");
4813
+ if (!input || !String(input.message ?? "").trim()) throw new Error("[Guidely] captureFeedback: message is required");
4814
+ const screenshot = await encodeScreenshot(input.screenshot);
4815
+ if (this.lastExceptionId) await this.queue.flush().catch(() => {
4816
+ });
4817
+ const body = bugReportBody(
4818
+ input,
4819
+ { anonymousId: this.anonymousId, userId: this.user?.id || null, sessionId: this.session.id, url: this.collectUrls() ? this.pageHref() : void 0, appVersion: this.appVersion(), lastExceptionId: this.lastExceptionId, sdk: { name: SDK_NAME, version: SDK_VERSION } },
4820
+ source,
4821
+ screenshot
4822
+ );
4823
+ return sendBugReport(this.api, body);
4824
+ }
4825
+ /** Opens the "Report a bug" dialog (message, name, email, a screenshot), which sends with captureFeedback. */
4826
+ showBugReport(options = {}) {
4827
+ if (this.destroyed || typeof document === "undefined" || this.bugReportDialog) return;
4828
+ void import("@guidelytech/guide-runtime/bug-report").then(({ renderBugReport }) => {
4829
+ if (this.destroyed || this.bugReportDialog) return;
4830
+ const scope = (0, import_guide_runtime6.createScope)(this.theme());
4831
+ const traits = this.user?.traits ?? {};
4832
+ const str = (v) => typeof v === "string" ? v : void 0;
4833
+ this.bugReportDialog = renderBugReport({
4834
+ scope: scope.el,
4835
+ title: options.title,
4836
+ message: options.message,
4837
+ name: options.name ?? str(traits.name),
4838
+ email: options.email ?? str(traits.email),
4839
+ onSubmit: async (i) => {
4840
+ await this.captureFeedback({ message: i.message, name: i.name || null, email: i.email || null, screenshot: i.screenshot }, "widget");
4841
+ },
4842
+ onClose: () => {
4843
+ this.bugReportDialog = null;
4844
+ scope.destroy();
4845
+ }
4846
+ });
4847
+ }).catch((err) => this.reportError(err));
4848
+ }
3833
4849
  /** Opens a new messenger conversation, optionally pre-filling the message. */
3834
4850
  showNewMessage(text) {
3835
4851
  if (!this.config?.messenger?.enabled) return;
@@ -3860,12 +4876,12 @@ var GuidelyClient = class _GuidelyClient {
3860
4876
  const account = this.user?.account;
3861
4877
  return {
3862
4878
  url: this.pageHref(),
3863
- path: pathOf2(this.pageHref()),
4879
+ path: pathOf3(this.pageHref()),
3864
4880
  deviceType: device.type,
3865
4881
  browser: device.browser,
3866
4882
  os: device.os,
3867
4883
  locale: navigator.language || void 0,
3868
- environment: this.options.environment,
4884
+ environment: this.environment,
3869
4885
  platform: "web",
3870
4886
  ...this.options.appVersion ? { appVersion: this.options.appVersion } : {},
3871
4887
  ...this.user?.traits ?? {},
@@ -4040,9 +5056,9 @@ var GuidelyClient = class _GuidelyClient {
4040
5056
  };
4041
5057
  var current2 = null;
4042
5058
  function webOnly(config) {
4043
- return config.guides.every((g) => (0, import_runtime18.supportsPlatform)(g.config, "web")) ? config : { ...config, guides: config.guides.filter((g) => (0, import_runtime18.supportsPlatform)(g.config, "web")) };
5059
+ return config.guides.every((g) => (0, import_runtime23.supportsPlatform)(g.config, "web")) ? config : { ...config, guides: config.guides.filter((g) => (0, import_runtime23.supportsPlatform)(g.config, "web")) };
4044
5060
  }
4045
- function pathOf2(url) {
5061
+ function pathOf3(url) {
4046
5062
  try {
4047
5063
  return new URL(url, "http://localhost").pathname;
4048
5064
  } catch {
@@ -4093,6 +5109,7 @@ var index_default = Guidely;
4093
5109
  createGuidely,
4094
5110
  createStorage,
4095
5111
  detectDevice,
5112
+ environmentFromKey,
4096
5113
  isAppUrl,
4097
5114
  isExclusiveGuide,
4098
5115
  isFrequencyEligible,