@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.js CHANGED
@@ -5,24 +5,26 @@ import {
5
5
  areMutuallyExclusive,
6
6
  countsTowardThrottle,
7
7
  dismissedAfterMerge,
8
+ environmentFromKey,
8
9
  isExclusiveGuide,
9
10
  isFrequencyEligible,
10
11
  isThrottleEligible,
11
12
  isWithinSchedule,
12
13
  pruneLaunches
13
- } from "./chunk-2E2YUQAG.js";
14
+ } from "./chunk-GFLVZRTS.js";
14
15
  import {
15
16
  Api,
16
17
  KEYS,
17
18
  __publicField,
18
19
  createStorage,
20
+ isServerFailure,
19
21
  postWithRetry,
20
22
  readJson,
21
23
  writeJson
22
24
  } from "./chunk-XML74K2P.js";
23
25
 
24
26
  // src/client.ts
25
- import { articleIdFromHref, evaluateRules, localizeGuide, localizeResourceCenter, normalizeLocale, randomId as randomId2, rolloutBucket, sanitizeProperties, supportsPlatform, visibleAnnouncements, visitorLocales } from "@guidelytech/shared/runtime";
27
+ import { articleIdFromHref, errorFilterDrops, evaluateRules, localizeGuide, localizeResourceCenter, normalizeLocale, randomId as randomId2, rolloutBucket, sanitizeProperties, supportsPlatform, visibleAnnouncements, visitorLocales } from "@guidelytech/shared/runtime";
26
28
  import {
27
29
  closestComposed as closestComposed4,
28
30
  composedTarget,
@@ -572,10 +574,10 @@ var AuthoringBridge = class {
572
574
  this.observer.observe(document.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });
573
575
  }
574
576
  scheduleSnapshot() {
575
- const now = Date.now();
576
- if (!this.snapshotTimer) this.snapshotDue = now + SNAPSHOT_MAX_WAIT_MS;
577
+ const now3 = Date.now();
578
+ if (!this.snapshotTimer) this.snapshotDue = now3 + SNAPSHOT_MAX_WAIT_MS;
577
579
  else clearTimeout(this.snapshotTimer);
578
- this.snapshotTimer = setTimeout(() => this.sendSnapshot(), Math.max(0, Math.min(SNAPSHOT_DEBOUNCE_MS, this.snapshotDue - now)));
580
+ this.snapshotTimer = setTimeout(() => this.sendSnapshot(), Math.max(0, Math.min(SNAPSHOT_DEBOUNCE_MS, this.snapshotDue - now3)));
579
581
  }
580
582
  sendSnapshot() {
581
583
  if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
@@ -880,12 +882,12 @@ function describe(value) {
880
882
  }
881
883
  }
882
884
  function exceptionFromError(error, source, handled, fallbackType = "Error") {
883
- const isError = error instanceof Error || !!error && typeof error === "object" && typeof error.message === "string" && typeof error.stack === "string";
885
+ const isError2 = error instanceof Error || !!error && typeof error === "object" && typeof error.message === "string" && typeof error.stack === "string";
884
886
  const e = error;
885
887
  return {
886
- type: isError && e.name || fallbackType,
887
- message: (isError ? e.message : describe(error)).slice(0, 1e3),
888
- frames: isError ? fitFrames(parseStack(e.stack)) : [],
888
+ type: isError2 && e.name || fallbackType,
889
+ message: (isError2 ? e.message : describe(error)).slice(0, 1e3),
890
+ frames: isError2 ? fitFrames(parseStack(e.stack)) : [],
889
891
  source,
890
892
  handled
891
893
  };
@@ -935,6 +937,222 @@ var ExceptionThrottle = class {
935
937
  }
936
938
  };
937
939
 
940
+ // src/error-monitoring.ts
941
+ import { MAX_BREADCRUMBS, MAX_EXCEPTION_CHAIN, normalizeTags, parseStack as parseStack2, trimExceptionDetail } from "@guidelytech/shared/runtime";
942
+ var CONTEXT_KEYS = ["tags", "extra", "level", "fingerprint", "contexts", "user", "mechanism"];
943
+ function splitHint(hint) {
944
+ const ctx = {};
945
+ const properties = {};
946
+ for (const [k, v] of Object.entries(hint ?? {})) (CONTEXT_KEYS.includes(k) ? ctx : properties)[k] = v;
947
+ return { ctx, properties };
948
+ }
949
+ var now = () => Date.now() / 1e3;
950
+ var ErrorScope = class {
951
+ constructor(opts) {
952
+ __publicField(this, "opts", opts);
953
+ __publicField(this, "tags", {});
954
+ __publicField(this, "extra", {});
955
+ __publicField(this, "contexts", {});
956
+ __publicField(this, "user", {});
957
+ __publicField(this, "crumbs", []);
958
+ }
959
+ get max() {
960
+ return Math.max(0, Math.min(MAX_BREADCRUMBS, this.opts()?.maxBreadcrumbs ?? MAX_BREADCRUMBS));
961
+ }
962
+ addBreadcrumb(crumb, hint) {
963
+ let c = { timestamp: now(), ...crumb };
964
+ const before = this.opts()?.beforeBreadcrumb;
965
+ if (before) {
966
+ try {
967
+ c = before(c, hint);
968
+ } catch {
969
+ }
970
+ }
971
+ if (!c || !this.max) return;
972
+ this.crumbs.push(c);
973
+ if (this.crumbs.length > this.max) this.crumbs.splice(0, this.crumbs.length - this.max);
974
+ }
975
+ breadcrumbs() {
976
+ return this.crumbs.slice();
977
+ }
978
+ clear() {
979
+ this.tags = {};
980
+ this.extra = {};
981
+ this.contexts = {};
982
+ this.user = {};
983
+ this.crumbs = [];
984
+ }
985
+ };
986
+ function toDetailFrames(frames) {
987
+ const out = [];
988
+ for (let i = frames.length - 1; i >= 0; i--) {
989
+ const f = frames[i];
990
+ 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 } : {} });
991
+ }
992
+ return out;
993
+ }
994
+ var isError = (v) => v instanceof Error || !!v && typeof v === "object" && typeof v.message === "string" && typeof v.stack === "string";
995
+ function exceptionChain(error, main, mechanism) {
996
+ const chain = [{ type: main.type, value: main.message, mechanism: { type: mechanism, handled: main.handled !== false, exception_id: 0 }, stacktrace: { frames: toDetailFrames(main.frames) } }];
997
+ const seen = /* @__PURE__ */ new Set([error]);
998
+ let cur = isError(error) ? error.cause : void 0;
999
+ while (cur !== void 0 && cur !== null && !seen.has(cur) && chain.length < MAX_EXCEPTION_CHAIN) {
1000
+ seen.add(cur);
1001
+ const e = cur;
1002
+ const id = chain.length;
1003
+ chain.push({
1004
+ type: isError(e) && e.name || "Error",
1005
+ value: (isError(e) ? e.message : String(cur)).slice(0, 1e3),
1006
+ mechanism: { type: "chained", source: "cause", handled: true, exception_id: id, parent_id: id - 1 },
1007
+ ...isError(e) ? { stacktrace: { frames: toDetailFrames(parseStack2(e.stack, 50)) } } : {}
1008
+ });
1009
+ cur = isError(e) ? e.cause : void 0;
1010
+ }
1011
+ return chain.reverse();
1012
+ }
1013
+ var VERSION_TOKEN = { chrome: "(?:Chrome|CriOS)", edge: "Edg", firefox: "(?:Firefox|FxiOS)", safari: "Version", opera: "OPR", samsung: "SamsungBrowser", electron: "Electron" };
1014
+ var OS_VERSION = /(?:Mac OS X|Windows NT|Android|CPU (?:iPhone )?OS) ([\d._]+)/;
1015
+ 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" };
1016
+ function browserContexts(ua, device) {
1017
+ const out = {};
1018
+ const token = device.browser ? VERSION_TOKEN[device.browser] : void 0;
1019
+ const v = token ? new RegExp(`${token}/([\\d.]+)`).exec(ua)?.[1] : void 0;
1020
+ if (device.browser) out.browser = { name: NAMES[device.browser] ?? device.browser, ...v ? { version: v } : {} };
1021
+ const ov = OS_VERSION.exec(ua)?.[1]?.replace(/_/g, ".");
1022
+ if (device.os) out.os = { name: NAMES[device.os] ?? device.os, ...ov ? { version: ov } : {} };
1023
+ out.device = { family: device.type === "desktop" ? "Desktop" : device.type === "tablet" ? "Tablet" : "Mobile" };
1024
+ return out;
1025
+ }
1026
+ function buildDetail(input) {
1027
+ const { scope, ctx } = input;
1028
+ const contexts = { ...input.contexts, ...scope.contexts };
1029
+ for (const [k, v] of Object.entries(ctx.contexts ?? {})) if (v) contexts[k] = v;
1030
+ const user = { ...scope.user, ...ctx.user ?? {} };
1031
+ const detail = {
1032
+ level: input.level,
1033
+ exception: { values: input.props.type === "Message" ? [] : exceptionChain(input.error, input.props, input.mechanism) },
1034
+ breadcrumbs: { values: scope.breadcrumbs() },
1035
+ contexts,
1036
+ tags: normalizeTags({ ...scope.tags, ...ctx.tags ?? {} }),
1037
+ extra: { ...scope.extra, ...ctx.extra ?? {} },
1038
+ ...Object.keys(user).length ? { user } : {},
1039
+ ...ctx.fingerprint?.length ? { fingerprint: ctx.fingerprint.map(String) } : {}
1040
+ };
1041
+ return trimExceptionDetail(detail);
1042
+ }
1043
+ var isOwnRequest = (url, apiUrl) => !!apiUrl && url.startsWith(apiUrl) && /\/api\/v1\/sdk\//.test(url);
1044
+ function breadcrumbUrl(raw) {
1045
+ try {
1046
+ const u = new URL(raw, typeof location !== "undefined" ? location.href : "http://localhost/");
1047
+ return (u.origin === "null" ? u.protocol : u.origin) + u.pathname;
1048
+ } catch {
1049
+ return raw.replace(/[?#].*$/, "");
1050
+ }
1051
+ }
1052
+ function describeArg(a) {
1053
+ if (typeof a === "string") return a;
1054
+ if (a instanceof Error) return `${a.name}: ${a.message}`;
1055
+ try {
1056
+ return JSON.stringify(a) ?? String(a);
1057
+ } catch {
1058
+ return String(a);
1059
+ }
1060
+ }
1061
+ var XHR_INFO = "__guidelyCrumb";
1062
+ function instrumentBreadcrumbs(add, opts) {
1063
+ let on = true;
1064
+ const undo = [];
1065
+ const g = globalThis;
1066
+ if (opts.console !== false && g.console) {
1067
+ const levels = opts.console === "all" ? ["log", "info", "warn", "error", "debug"] : ["warn", "error"];
1068
+ for (const level of levels) {
1069
+ const original = g.console[level];
1070
+ if (typeof original !== "function") continue;
1071
+ const wrapped = function(...args) {
1072
+ if (on) {
1073
+ try {
1074
+ const message = args.map(describeArg).join(" ").slice(0, 1e3);
1075
+ if (!message.startsWith("[Guidely]")) add({ category: "console", level: level === "warn" ? "warning" : level === "log" ? "info" : level, message, data: { logger: "console" } }, { event: args });
1076
+ } catch {
1077
+ }
1078
+ }
1079
+ return original.apply(this, args);
1080
+ };
1081
+ g.console[level] = wrapped;
1082
+ undo.push(() => {
1083
+ if (g.console[level] === wrapped) g.console[level] = original;
1084
+ });
1085
+ }
1086
+ }
1087
+ if (opts.http !== false && typeof g.fetch === "function") {
1088
+ const original = g.fetch;
1089
+ const wrapped = function(input, init) {
1090
+ const started = Date.now();
1091
+ let url = "";
1092
+ let method = "GET";
1093
+ try {
1094
+ url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1095
+ method = (init?.method ?? (typeof input === "object" && "method" in input ? input.method : "GET")).toUpperCase();
1096
+ } catch {
1097
+ }
1098
+ const p = original.call(this, input, init);
1099
+ if (on && url && !isOwnRequest(url, opts.apiUrl)) {
1100
+ const crumb = (status) => {
1101
+ try {
1102
+ 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 } });
1103
+ } catch {
1104
+ }
1105
+ };
1106
+ p.then(
1107
+ (res) => crumb(res.status),
1108
+ () => crumb(void 0)
1109
+ );
1110
+ }
1111
+ return p;
1112
+ };
1113
+ g.fetch = wrapped;
1114
+ undo.push(() => {
1115
+ if (g.fetch === wrapped) g.fetch = original;
1116
+ });
1117
+ }
1118
+ const proto = opts.http !== false ? g.XMLHttpRequest?.prototype : void 0;
1119
+ if (proto) {
1120
+ const open = proto.open;
1121
+ const send = proto.send;
1122
+ const wrappedOpen = function(method, url, ...rest) {
1123
+ try {
1124
+ this[XHR_INFO] = { method: String(method).toUpperCase(), url: String(url) };
1125
+ } catch {
1126
+ }
1127
+ return open.call(this, method, url, ...rest);
1128
+ };
1129
+ const wrappedSend = function(body) {
1130
+ const info = this[XHR_INFO];
1131
+ if (on && info && !isOwnRequest(info.url, opts.apiUrl)) {
1132
+ const started = Date.now();
1133
+ this.addEventListener("loadend", () => {
1134
+ try {
1135
+ const status = this.status || void 0;
1136
+ 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 });
1137
+ } catch {
1138
+ }
1139
+ });
1140
+ }
1141
+ return send.call(this, body);
1142
+ };
1143
+ proto.open = wrappedOpen;
1144
+ proto.send = wrappedSend;
1145
+ undo.push(() => {
1146
+ if (proto.open === wrappedOpen) proto.open = open;
1147
+ if (proto.send === wrappedSend) proto.send = send;
1148
+ });
1149
+ }
1150
+ return () => {
1151
+ on = false;
1152
+ for (const u of undo) u();
1153
+ };
1154
+ }
1155
+
938
1156
  // src/messenger-api.ts
939
1157
  var MAX_STORED = 50;
940
1158
  var TOKEN_HEADER = "X-Guidely-Conversation";
@@ -1123,7 +1341,7 @@ function plainText(html, max = 280) {
1123
1341
  return text.length > max ? `${text.slice(0, max - 1).trimEnd()}\u2026` : text;
1124
1342
  }
1125
1343
  function buildMessengerVisitor(input) {
1126
- const now = input.now ?? Date.now();
1344
+ const now3 = input.now ?? Date.now();
1127
1345
  const { guides, state } = input;
1128
1346
  const done = (g) => !!state.guides[g.id]?.completedAt;
1129
1347
  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 }));
@@ -1133,7 +1351,7 @@ function buildMessengerVisitor(input) {
1133
1351
  const s = state.guides[g.id];
1134
1352
  if (g.kind !== "flow" || !s?.lastStepId || s.completedAt || g.config.steps.length < 2) continue;
1135
1353
  const at = Date.parse(s.lastShownAt ?? "");
1136
- if (!Number.isFinite(at) || now - at > RESUME_WITHIN_MS || at <= resumeAt) continue;
1354
+ if (!Number.isFinite(at) || now3 - at > RESUME_WITHIN_MS || at <= resumeAt) continue;
1137
1355
  const stepIndex = g.config.steps.findIndex((st) => st.id === s.lastStepId);
1138
1356
  if (stepIndex < 1 || !isWithinSchedule(g)) continue;
1139
1357
  resumeGuide = { id: g.id, name: g.name, stepId: s.lastStepId, stepIndex, stepCount: g.config.steps.length };
@@ -1152,7 +1370,7 @@ function buildMessengerVisitor(input) {
1152
1370
  break;
1153
1371
  }
1154
1372
  const seen = new Set(input.newsSeen);
1155
- 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) => {
1373
+ 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) => {
1156
1374
  const step = g.config.steps[0];
1157
1375
  return {
1158
1376
  id: g.id,
@@ -1282,19 +1500,19 @@ var PageTracker = class {
1282
1500
  __publicField(this, "maxScroll", 0);
1283
1501
  }
1284
1502
  /** Starts measuring a new page. */
1285
- start(url, now = Date.now()) {
1503
+ start(url, now3 = Date.now()) {
1286
1504
  this.url = url;
1287
1505
  this.visibleMs = 0;
1288
- this.visibleSince = typeof document !== "undefined" && document.visibilityState === "hidden" ? null : now;
1506
+ this.visibleSince = typeof document !== "undefined" && document.visibilityState === "hidden" ? null : now3;
1289
1507
  this.maxScroll = 0;
1290
1508
  this.measureScroll();
1291
1509
  }
1292
1510
  /** Pauses / resumes the visible-time clock. */
1293
- setHidden(hidden, now = Date.now()) {
1511
+ setHidden(hidden, now3 = Date.now()) {
1294
1512
  if (hidden && this.visibleSince !== null) {
1295
- this.visibleMs += Math.max(0, now - this.visibleSince);
1513
+ this.visibleMs += Math.max(0, now3 - this.visibleSince);
1296
1514
  this.visibleSince = null;
1297
- } else if (!hidden && this.visibleSince === null) this.visibleSince = now;
1515
+ } else if (!hidden && this.visibleSince === null) this.visibleSince = now3;
1298
1516
  }
1299
1517
  measureScroll() {
1300
1518
  try {
@@ -1303,9 +1521,9 @@ var PageTracker = class {
1303
1521
  }
1304
1522
  }
1305
1523
  /** Final numbers for the current page. */
1306
- leave(now = Date.now()) {
1524
+ leave(now3 = Date.now()) {
1307
1525
  this.measureScroll();
1308
- const running = this.visibleSince !== null ? Math.max(0, now - this.visibleSince) : 0;
1526
+ const running = this.visibleSince !== null ? Math.max(0, now3 - this.visibleSince) : 0;
1309
1527
  return { url: this.url, durationMs: Math.round(this.visibleMs + running), scrollDepth: this.maxScroll };
1310
1528
  }
1311
1529
  };
@@ -1413,17 +1631,622 @@ function openPrototypeOverlay(testUrl, opts) {
1413
1631
  // src/load-recorder.ts
1414
1632
  var loadRecorder = async () => (await import("./recorder.js")).recorder;
1415
1633
 
1634
+ // src/trace-propagation.ts
1635
+ function hex(bytes) {
1636
+ const a = new Uint8Array(bytes);
1637
+ try {
1638
+ crypto.getRandomValues(a);
1639
+ } catch {
1640
+ for (let i = 0; i < bytes; i++) a[i] = Math.floor(Math.random() * 256);
1641
+ }
1642
+ let out = "";
1643
+ for (const b of a) out += (b < 16 ? "0" : "") + b.toString(16);
1644
+ return out;
1645
+ }
1646
+ function stripUrl(url) {
1647
+ return url.split(/[?#]/)[0];
1648
+ }
1649
+ function resolveUrl(url) {
1650
+ try {
1651
+ return new URL(url, location.href).href;
1652
+ } catch {
1653
+ return url;
1654
+ }
1655
+ }
1656
+ function shouldPropagate(url, targets) {
1657
+ const abs = resolveUrl(url);
1658
+ if (!targets) {
1659
+ try {
1660
+ return new URL(abs).origin === location.origin;
1661
+ } catch {
1662
+ return false;
1663
+ }
1664
+ }
1665
+ return targets.some((t) => {
1666
+ if (typeof t === "string") return url.includes(t) || abs.includes(t);
1667
+ t.lastIndex = 0;
1668
+ if (t.test(url)) return true;
1669
+ t.lastIndex = 0;
1670
+ return t.test(abs);
1671
+ });
1672
+ }
1673
+ function mergeBaggage(existing, ours) {
1674
+ const kept = (existing || "").split(",").map((s) => s.trim()).filter((s) => s && !s.startsWith("sentry-"));
1675
+ return [...kept, ours].join(",");
1676
+ }
1677
+ function traceHeaders(dsc, spanId) {
1678
+ const flag = dsc.sampled ? "1" : "0";
1679
+ const e = encodeURIComponent;
1680
+ 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(",");
1681
+ return { "sentry-trace": `${dsc.traceId}-${spanId}-${flag}`, traceparent: `00-${dsc.traceId}-${spanId}-0${flag}`, baggage };
1682
+ }
1683
+ function httpSpanStatus(code) {
1684
+ if (code >= 100 && code < 400) return "ok";
1685
+ 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";
1686
+ if (code >= 500 && code < 600) return { 501: "unimplemented", 503: "unavailable", 504: "deadline_exceeded" }[code] ?? "internal_error";
1687
+ return "unknown_error";
1688
+ }
1689
+
1690
+ // src/web-vitals.ts
1691
+ function observe(type, cb, extra = {}) {
1692
+ try {
1693
+ if (typeof PerformanceObserver === "undefined" || !PerformanceObserver.supportedEntryTypes?.includes(type)) return null;
1694
+ const po = new PerformanceObserver((list) => cb(list.getEntries()));
1695
+ po.observe({ type, buffered: true, ...extra });
1696
+ return { take: () => cb(po.takeRecords()), stop: () => po.disconnect() };
1697
+ } catch {
1698
+ return null;
1699
+ }
1700
+ }
1701
+ var newCls = () => ({ value: 0, session: 0, first: 0, last: 0 });
1702
+ function addLayoutShift(s, e) {
1703
+ if (e.hadRecentInput) return;
1704
+ if (s.session && e.startTime - s.last < 1e3 && e.startTime - s.first < 5e3) s.session += e.value;
1705
+ else {
1706
+ s.session = e.value;
1707
+ s.first = e.startTime;
1708
+ }
1709
+ s.last = e.startTime;
1710
+ if (s.session > s.value) s.value = s.session;
1711
+ }
1712
+ var newInp = () => ({ longest: [], ids: /* @__PURE__ */ new Set() });
1713
+ function addInteraction(s, e) {
1714
+ const id = e.interactionId;
1715
+ if (!id) return;
1716
+ s.ids.add(id);
1717
+ const known = s.longest.find((i) => i.id === id);
1718
+ if (known) known.latency = Math.max(known.latency, e.duration);
1719
+ else if (s.longest.length < 10 || e.duration > s.longest[s.longest.length - 1].latency) s.longest.push({ id, latency: e.duration });
1720
+ else return;
1721
+ s.longest.sort((a, b) => b.latency - a.latency);
1722
+ s.longest.splice(10);
1723
+ }
1724
+ function inpValue(s, count = s.ids.size) {
1725
+ if (!s.longest.length) return void 0;
1726
+ return s.longest[Math.min(s.longest.length - 1, Math.floor(count / 50))].latency;
1727
+ }
1728
+ var ms = (value) => ({ value, unit: "millisecond" });
1729
+ var WebVitals = class {
1730
+ constructor(nav) {
1731
+ __publicField(this, "lcp");
1732
+ __publicField(this, "fcp");
1733
+ __publicField(this, "ttfb");
1734
+ __publicField(this, "cls", newCls());
1735
+ __publicField(this, "inp", newInp());
1736
+ __publicField(this, "observed", []);
1737
+ __publicField(this, "lcpFinal", false);
1738
+ __publicField(this, "hiddenAt");
1739
+ __publicField(this, "offs", []);
1740
+ const activation = nav?.activationStart || 0;
1741
+ const rel = (t) => Math.max(0, t - activation);
1742
+ this.hiddenAt = document.visibilityState === "hidden" ? 0 : Infinity;
1743
+ if (nav && nav.responseStart > 0) this.ttfb = rel(nav.responseStart);
1744
+ const add = (o) => o && this.observed.push(o);
1745
+ add(
1746
+ observe("paint", (es) => {
1747
+ for (const e of es) if (e.name === "first-contentful-paint" && e.startTime < this.hiddenAt) this.fcp = rel(e.startTime);
1748
+ })
1749
+ );
1750
+ add(
1751
+ observe("largest-contentful-paint", (es) => {
1752
+ const e = es[es.length - 1];
1753
+ if (e && !this.lcpFinal && e.startTime < this.hiddenAt) this.lcp = rel(e.startTime);
1754
+ })
1755
+ );
1756
+ add(observe("layout-shift", (es) => es.forEach((e) => addLayoutShift(this.cls, e))));
1757
+ const onEvent = (es) => es.forEach((e) => addInteraction(this.inp, e));
1758
+ add(observe("event", onEvent, { durationThreshold: 40 }));
1759
+ add(observe("first-input", onEvent));
1760
+ const freeze = () => {
1761
+ this.lcpFinal = true;
1762
+ this.offs.splice(0).forEach((f) => f());
1763
+ };
1764
+ for (const type of ["keydown", "pointerdown"]) {
1765
+ addEventListener(type, freeze, { once: true, capture: true });
1766
+ this.offs.push(() => removeEventListener(type, freeze, { capture: true }));
1767
+ }
1768
+ }
1769
+ /** The page was hidden: deliver pending entries and freeze LCP. */
1770
+ hidden() {
1771
+ this.observed.forEach((o) => o.take());
1772
+ this.lcpFinal = true;
1773
+ if (this.hiddenAt === Infinity) this.hiddenAt = performance.now();
1774
+ }
1775
+ /** Known at load: LCP (so far), FCP and TTFB. */
1776
+ pageload() {
1777
+ const m = {};
1778
+ if (this.lcp !== void 0) m.lcp = ms(this.lcp);
1779
+ if (this.fcp !== void 0) m.fcp = ms(this.fcp);
1780
+ if (this.ttfb !== void 0) m.ttfb = ms(this.ttfb);
1781
+ return m;
1782
+ }
1783
+ /** Known only when the page is hidden: CLS, and INP once there was an interaction. */
1784
+ late() {
1785
+ const count = performance.interactionCount;
1786
+ const inp = inpValue(this.inp, count || void 0);
1787
+ return { cls: { value: this.cls.value, unit: "" }, ...inp !== void 0 ? { inp: ms(inp) } : {} };
1788
+ }
1789
+ stop() {
1790
+ this.observed.splice(0).forEach((o) => o.stop());
1791
+ this.offs.splice(0).forEach((f) => f());
1792
+ }
1793
+ };
1794
+
1795
+ // src/tracing.ts
1796
+ var TRACES_SDK_NAME = "guidely.javascript";
1797
+ var MAX_SPANS = 1e3;
1798
+ var MAX_QUEUE = 20;
1799
+ var BATCH = 10;
1800
+ var now2 = () => performance.now();
1801
+ function entries(type) {
1802
+ try {
1803
+ return performance.getEntriesByType(type);
1804
+ } catch {
1805
+ return [];
1806
+ }
1807
+ }
1808
+ function pathOf2(href) {
1809
+ try {
1810
+ return new URL(href).pathname || "/";
1811
+ } catch {
1812
+ return stripUrl(href) || "/";
1813
+ }
1814
+ }
1815
+ function browserContext() {
1816
+ const ua = navigator.userAgent || "";
1817
+ const name = detectDevice(ua, 1024).browser;
1818
+ if (!name) return void 0;
1819
+ const token = { edge: "Edg", opera: "OPR", samsung: "SamsungBrowser", chrome: "(?:Chrome|CriOS)", firefox: "(?:Firefox|FxiOS)", safari: "Version", electron: "Electron" }[name];
1820
+ const version = token ? new RegExp(`${token}/([\\d.]+)`).exec(ua)?.[1] : void 0;
1821
+ return { name: name[0].toUpperCase() + name.slice(1), ...version ? { version } : {} };
1822
+ }
1823
+ function startTracer(config) {
1824
+ if (typeof performance === "undefined" || typeof performance.now !== "function" || typeof window === "undefined") return null;
1825
+ return new Tracer(config);
1826
+ }
1827
+ var Tracer = class {
1828
+ constructor(o) {
1829
+ __publicField(this, "o", o);
1830
+ /** null until the project setting is known; then whether tracing is on for this page load. */
1831
+ __publicField(this, "enabled", null);
1832
+ __publicField(this, "sampled", false);
1833
+ __publicField(this, "rate", 0);
1834
+ __publicField(this, "txn", null);
1835
+ __publicField(this, "pageload");
1836
+ __publicField(this, "origin");
1837
+ __publicField(this, "vitals");
1838
+ __publicField(this, "longTasks", []);
1839
+ /** Finished before sampling was decided. */
1840
+ __publicField(this, "held", []);
1841
+ __publicField(this, "out", []);
1842
+ __publicField(this, "flushTimer", null);
1843
+ __publicField(this, "cleanups", []);
1844
+ __publicField(this, "stopped", false);
1845
+ __publicField(this, "vitalsSent", false);
1846
+ __publicField(this, "apiPrefix");
1847
+ __publicField(this, "idleMs");
1848
+ __publicField(this, "finalMs");
1849
+ this.idleMs = o.idleMs ?? 1e3;
1850
+ this.finalMs = o.finalMs ?? 3e4;
1851
+ this.origin = performance.timeOrigin || Date.now() - now2();
1852
+ this.apiPrefix = `${resolveUrl(o.apiUrl).replace(/\/+$/, "")}/api/v1/sdk/`;
1853
+ this.vitals = new WebVitals(entries("navigation")[0]);
1854
+ const lt = observe("longtask", (es) => {
1855
+ for (const e of es) if (this.longTasks.length < 500) this.longTasks.push(e);
1856
+ });
1857
+ if (lt) this.cleanups.push(lt.stop);
1858
+ this.instrumentFetch();
1859
+ this.instrumentXhr();
1860
+ this.cleanups.push(watchNavigation((href) => this.navigate(href), 0));
1861
+ const onVisibility = () => document.visibilityState === "hidden" && this.onHide();
1862
+ const onPageHide = () => this.onHide();
1863
+ document.addEventListener("visibilitychange", onVisibility);
1864
+ addEventListener("pagehide", onPageHide);
1865
+ this.cleanups.push(() => {
1866
+ document.removeEventListener("visibilitychange", onVisibility);
1867
+ removeEventListener("pagehide", onPageHide);
1868
+ });
1869
+ const t = this.pageload = this.begin("pageload", 0);
1870
+ const onLoad = () => setTimeout(() => {
1871
+ t.loaded = true;
1872
+ this.scheduleIdle(t);
1873
+ }, 0);
1874
+ if (document.readyState === "complete") onLoad();
1875
+ else {
1876
+ addEventListener("load", onLoad, { once: true });
1877
+ this.cleanups.push(() => removeEventListener("load", onLoad));
1878
+ }
1879
+ }
1880
+ /** Applies the project's `performance` setting; sampling is decided the first time it is on. */
1881
+ configure(settings) {
1882
+ if (this.stopped) return;
1883
+ if (!settings?.enabled) {
1884
+ this.enabled = false;
1885
+ this.held = [];
1886
+ this.out = [];
1887
+ return this.stop();
1888
+ }
1889
+ if (this.enabled) return;
1890
+ this.enabled = true;
1891
+ const sdkRate = this.o.tracesSampleRate ?? 1;
1892
+ this.rate = Math.max(0, Math.min((settings.sampleRate ?? 100) / 100, Number.isFinite(sdkRate) ? sdkRate : 1, 1));
1893
+ this.sampled = Math.random() < this.rate;
1894
+ if (!this.sampled) this.vitals.stop();
1895
+ for (const e of this.held.splice(0)) if (this.sampled) this.enqueue(e);
1896
+ }
1897
+ /**
1898
+ * The trace an error happening now belongs to (the open navigation, else the page load), so error
1899
+ * events link to it. Null when this page load isn't traced.
1900
+ */
1901
+ traceContext() {
1902
+ if (!this.enabled || !this.sampled || this.stopped) return null;
1903
+ const t = this.txn ?? this.pageload;
1904
+ return { traceId: t.traceId, spanId: t.spanId };
1905
+ }
1906
+ /** Stops tracing and sends what is queued. */
1907
+ destroy() {
1908
+ this.flush(true);
1909
+ this.stop();
1910
+ }
1911
+ stop() {
1912
+ if (this.stopped) return;
1913
+ this.stopped = true;
1914
+ if (this.flushTimer) clearTimeout(this.flushTimer);
1915
+ for (const t of [this.txn, this.pageload]) {
1916
+ clearTimeout(t?.idle);
1917
+ clearTimeout(t?.final);
1918
+ }
1919
+ this.vitals.stop();
1920
+ this.cleanups.splice(0).forEach((f) => f());
1921
+ }
1922
+ ts(rel) {
1923
+ return (this.origin + rel) / 1e3;
1924
+ }
1925
+ begin(op, start, href) {
1926
+ const url = this.o.pageHref(href);
1927
+ const t = { op, name: pathOf2(url), url, traceId: hex(16), spanId: hex(8), start, last: start, inflight: 0, spans: [], loaded: op !== "pageload" };
1928
+ t.final = setTimeout(() => this.finish(t), Math.max(0, this.finalMs - (now2() - start)));
1929
+ this.txn = t;
1930
+ return t;
1931
+ }
1932
+ scheduleIdle(t) {
1933
+ clearTimeout(t.idle);
1934
+ if (t.done || t.inflight > 0 || !t.loaded) return;
1935
+ t.idle = setTimeout(() => this.finish(t), this.idleMs);
1936
+ }
1937
+ navigate(href) {
1938
+ if (this.stopped || pathOf2(this.o.pageHref(href)) === this.txn?.name) return;
1939
+ if (this.txn && !this.txn.done) this.finish(this.txn);
1940
+ this.scheduleIdle(this.begin("navigation", now2(), href));
1941
+ }
1942
+ onHide() {
1943
+ if (this.stopped) return;
1944
+ this.vitals.hidden();
1945
+ if (this.txn && !this.txn.done) this.finish(this.txn, now2());
1946
+ if (this.enabled && this.sampled && !this.vitalsSent) {
1947
+ this.vitalsSent = true;
1948
+ const at = this.ts(now2());
1949
+ const p = this.pageload;
1950
+ const e = this.event(p, "ui.webvitals", at, at, [], this.vitals.late());
1951
+ e.contexts.trace.span_id = hex(8);
1952
+ e.contexts.trace.parent_span_id = p.spanId;
1953
+ this.enqueue(e);
1954
+ }
1955
+ this.flush(true);
1956
+ }
1957
+ /** Ends a transaction: adds the spans read from performance entries and queues (or holds) it. */
1958
+ finish(t, endAt) {
1959
+ if (t.done) return;
1960
+ t.done = true;
1961
+ clearTimeout(t.idle);
1962
+ clearTimeout(t.final);
1963
+ if (this.stopped || this.enabled === false || this.enabled && !this.sampled) return;
1964
+ const until = now2();
1965
+ const spans = t.spans;
1966
+ const add = (op, description, start, end2, origin, data) => {
1967
+ if (spans.length >= MAX_SPANS || !(end2 >= start) || start < t.start || start > until) return;
1968
+ 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 } : {} });
1969
+ };
1970
+ if (t.op === "pageload") {
1971
+ const n = entries("navigation")[0];
1972
+ if (n) {
1973
+ const timing = (name, s, e) => s > 0 && e > 0 && add("browser", name, s, e, "auto.ui.browser.metrics");
1974
+ timing("redirect", n.redirectStart, n.redirectEnd);
1975
+ timing("cache", n.fetchStart, n.domainLookupStart);
1976
+ timing("DNS", n.domainLookupStart, n.domainLookupEnd);
1977
+ timing("connect", n.connectStart, n.connectEnd);
1978
+ timing("TLS/SSL", n.secureConnectionStart, n.connectEnd);
1979
+ timing("request", n.requestStart, n.responseEnd);
1980
+ timing("response", n.responseStart, n.responseEnd);
1981
+ timing("domContentLoadedEvent", n.domContentLoadedEventStart, n.domContentLoadedEventEnd);
1982
+ timing("loadEvent", n.loadEventStart, n.loadEventEnd);
1983
+ }
1984
+ }
1985
+ for (const e of entries("resource")) {
1986
+ const it = e.initiatorType;
1987
+ if (it === "fetch" || it === "xmlhttprequest" || this.own(e.name)) continue;
1988
+ const kind = it === "script" || it === "css" || it === "link" ? it : it === "img" || it === "image" ? "img" : "other";
1989
+ const data = { "http.response_content_length": e.encodedBodySize, "http.decoded_response_content_length": e.decodedBodySize, "http.response_transfer_size": e.transferSize };
1990
+ if (e.renderBlockingStatus) data["resource.render_blocking_status"] = e.renderBlockingStatus;
1991
+ add(`resource.${kind}`, stripUrl(e.name), e.startTime, e.responseEnd || e.startTime + e.duration, "auto.resource.browser.metrics", data);
1992
+ }
1993
+ for (const e of this.longTasks) add("ui.long-task", "Main UI thread blocked", e.startTime, e.startTime + e.duration, "auto.ui.browser.metrics");
1994
+ for (const e of entries("mark")) add("mark", e.name, e.startTime, e.startTime, "auto.ui.browser.metrics");
1995
+ for (const e of entries("measure")) add("measure", e.name, e.startTime, e.startTime + e.duration, "auto.ui.browser.metrics");
1996
+ const end = endAt !== void 0 ? this.ts(endAt) : spans.reduce((m2, s) => Math.max(m2, s.timestamp), this.ts(t.last));
1997
+ const m = t.op === "pageload" ? this.vitals.pageload() : {};
1998
+ const event = this.event(t, t.op, this.ts(t.start), end, spans, Object.keys(m).length ? m : void 0);
1999
+ if (this.enabled) this.enqueue(event);
2000
+ else if (this.held.length < MAX_QUEUE) this.held.push(event);
2001
+ }
2002
+ event(t, op, start, end, spans, measurements) {
2003
+ const c = this.o.context();
2004
+ const url = c.collectUrls ? stripUrl(t.url) : void 0;
2005
+ const browser = browserContext();
2006
+ return {
2007
+ type: "transaction",
2008
+ event_id: hex(16),
2009
+ platform: "javascript",
2010
+ transaction: t.name,
2011
+ transaction_info: { source: "url" },
2012
+ start_timestamp: start,
2013
+ timestamp: end,
2014
+ ...c.release ? { release: c.release } : {},
2015
+ environment: c.environment,
2016
+ contexts: {
2017
+ trace: { trace_id: t.traceId, span_id: t.spanId, op, status: "ok", origin: `auto.${op}.guidely` },
2018
+ ...browser ? { browser } : {},
2019
+ guidely: { anonymousId: c.anonymousId, sessionId: c.sessionId, ...url ? { url } : {} }
2020
+ },
2021
+ spans,
2022
+ ...measurements ? { measurements } : {},
2023
+ ...url ? { request: { url } } : {},
2024
+ ...c.userId ? { user: { id: c.userId } } : {}
2025
+ };
2026
+ }
2027
+ // --- requests ---------------------------------------------------------------------------------
2028
+ /** A Guidely API request (never traced, never given headers). */
2029
+ own(url) {
2030
+ return resolveUrl(url).startsWith(this.apiPrefix);
2031
+ }
2032
+ startHttp(method, url, type) {
2033
+ if (this.stopped || this.enabled === false || this.own(url)) return null;
2034
+ const abs = resolveUrl(url);
2035
+ const t = this.txn && !this.txn.done ? this.txn : null;
2036
+ const spanId = hex(8);
2037
+ let span = null;
2038
+ if (t && (this.enabled === null || this.sampled)) {
2039
+ const u = stripUrl(abs);
2040
+ 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 } };
2041
+ t.inflight += 1;
2042
+ clearTimeout(t.idle);
2043
+ }
2044
+ return { txn: t, span, spanId, url };
2045
+ }
2046
+ endHttp(live, status, length) {
2047
+ const { txn: t, span } = live;
2048
+ if (!t || !span) return;
2049
+ t.inflight = Math.max(0, t.inflight - 1);
2050
+ t.last = now2();
2051
+ span.timestamp = this.ts(t.last);
2052
+ span.status = status ? httpSpanStatus(status) : "unknown_error";
2053
+ if (status) span.data["http.response.status_code"] = status;
2054
+ if (length && Number(length) >= 0) span.data["http.response_content_length"] = Number(length);
2055
+ if (!t.done && t.spans.length < MAX_SPANS) t.spans.push(span);
2056
+ this.scheduleIdle(t);
2057
+ }
2058
+ /** Trace headers for a request, or null when it must not get any (tracing undecided / off, or not a target). */
2059
+ headers(live, baggage) {
2060
+ if (!this.enabled || this.stopped || !shouldPropagate(live.url, this.o.tracePropagationTargets)) return null;
2061
+ const c = this.o.context();
2062
+ 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);
2063
+ return { ...h, baggage: mergeBaggage(baggage, h.baggage) };
2064
+ }
2065
+ instrumentFetch() {
2066
+ const win = window;
2067
+ const orig = win.fetch;
2068
+ if (typeof orig !== "function") return;
2069
+ const patched = (input, init) => {
2070
+ let live = null;
2071
+ try {
2072
+ const isReq = typeof Request !== "undefined" && input instanceof Request;
2073
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
2074
+ live = this.startHttp(String(init?.method || (isReq ? input.method : "GET")).toUpperCase(), url, "fetch");
2075
+ if (live && typeof Headers !== "undefined") {
2076
+ const headers = new Headers(init?.headers ?? (isReq ? input.headers : void 0));
2077
+ const add = headers.has("sentry-trace") ? null : this.headers(live, headers.get("baggage"));
2078
+ if (add) {
2079
+ for (const [k, v] of Object.entries(add)) if (k !== "traceparent" || !headers.has(k)) headers.set(k, v);
2080
+ init = { ...init, headers };
2081
+ }
2082
+ }
2083
+ } catch {
2084
+ }
2085
+ const p = orig.call(win, input, init);
2086
+ if (live?.span) {
2087
+ const l = live;
2088
+ p.then(
2089
+ (res) => this.endHttp(l, res.status, res.headers?.get?.("content-length")),
2090
+ () => this.endHttp(l, 0)
2091
+ );
2092
+ }
2093
+ return p;
2094
+ };
2095
+ win.fetch = patched;
2096
+ this.cleanups.push(() => {
2097
+ if (win.fetch === patched) win.fetch = orig;
2098
+ });
2099
+ }
2100
+ instrumentXhr() {
2101
+ const proto = typeof XMLHttpRequest !== "undefined" ? XMLHttpRequest.prototype : null;
2102
+ if (!proto) return;
2103
+ const { open, send, setRequestHeader } = proto;
2104
+ const self = { startHttp: this.startHttp.bind(this), headers: this.headers.bind(this), endHttp: this.endHttp.bind(this) };
2105
+ const meta = /* @__PURE__ */ new WeakMap();
2106
+ const pOpen = function(method, url, ...rest) {
2107
+ meta.set(this, { method: String(method).toUpperCase(), url: String(url), headers: {} });
2108
+ return open.call(this, method, url, ...rest);
2109
+ };
2110
+ const pSetHeader = function(name, value) {
2111
+ const m = meta.get(this);
2112
+ if (m) m.headers[String(name).toLowerCase()] = String(value);
2113
+ return setRequestHeader.call(this, name, value);
2114
+ };
2115
+ const pSend = function(body) {
2116
+ try {
2117
+ const m = meta.get(this);
2118
+ const live = m ? self.startHttp(m.method, m.url, "xhr") : null;
2119
+ if (live && m && !m.headers["sentry-trace"]) {
2120
+ const existing = m.headers.baggage ?? null;
2121
+ const add = self.headers(live, null);
2122
+ if (add) {
2123
+ setRequestHeader.call(this, "sentry-trace", add["sentry-trace"]);
2124
+ if (!m.headers.traceparent) setRequestHeader.call(this, "traceparent", add.traceparent);
2125
+ if (!existing?.includes("sentry-")) setRequestHeader.call(this, "baggage", add.baggage);
2126
+ }
2127
+ }
2128
+ if (live?.span) this.addEventListener("loadend", () => self.endHttp(live, this.status, safeHeader(this, "content-length")), { once: true });
2129
+ } catch {
2130
+ }
2131
+ return send.call(this, body);
2132
+ };
2133
+ proto.open = pOpen;
2134
+ proto.setRequestHeader = pSetHeader;
2135
+ proto.send = pSend;
2136
+ this.cleanups.push(() => {
2137
+ if (proto.open === pOpen) proto.open = open;
2138
+ if (proto.setRequestHeader === pSetHeader) proto.setRequestHeader = setRequestHeader;
2139
+ if (proto.send === pSend) proto.send = send;
2140
+ });
2141
+ }
2142
+ // --- sending ----------------------------------------------------------------------------------
2143
+ enqueue(e) {
2144
+ this.out.push(e);
2145
+ if (this.out.length > MAX_QUEUE) this.out.shift();
2146
+ this.flushTimer ?? (this.flushTimer = setTimeout(() => this.flush(), this.o.flushMs ?? 1e3));
2147
+ }
2148
+ /** Sends queued transactions (`hide`: with `sendBeacon` / keepalive, the page is going away). */
2149
+ flush(hide = false) {
2150
+ if (this.flushTimer) clearTimeout(this.flushTimer);
2151
+ this.flushTimer = null;
2152
+ if (!this.out.length) return;
2153
+ const allowed = this.o.allowed();
2154
+ if (allowed === false) {
2155
+ this.out = [];
2156
+ return;
2157
+ }
2158
+ if (allowed === null) return;
2159
+ const { api } = this.o;
2160
+ if (!hide && !api.health.canSend()) {
2161
+ this.flushTimer = setTimeout(() => this.flush(), api.health.waitMs());
2162
+ return;
2163
+ }
2164
+ while (this.out.length) {
2165
+ const batch = this.out.splice(0, BATCH);
2166
+ const body = { sdk: { name: TRACES_SDK_NAME, version: this.o.sdkVersion }, transactions: batch };
2167
+ if (hide && api.beacon("/traces", body)) continue;
2168
+ api.post("/traces", body, { keepalive: hide }).then(
2169
+ (res) => isServerFailure(res.status) && this.retry(batch),
2170
+ () => this.retry(batch)
2171
+ );
2172
+ }
2173
+ }
2174
+ retry(batch) {
2175
+ if (this.stopped) return;
2176
+ this.out.unshift(...batch);
2177
+ this.out.splice(MAX_QUEUE);
2178
+ this.flushTimer ?? (this.flushTimer = setTimeout(() => this.flush(), Math.max(5e3, this.o.api.health.waitMs())));
2179
+ }
2180
+ };
2181
+ function safeHeader(xhr, name) {
2182
+ try {
2183
+ return xhr.getResponseHeader(name);
2184
+ } catch {
2185
+ return null;
2186
+ }
2187
+ }
2188
+
2189
+ // src/bug-report.ts
2190
+ import { MAX_BUG_REPORT_LENGTH, MAX_BUG_REPORT_SCREENSHOT_BYTES } from "@guidelytech/shared/runtime";
2191
+ async function encodeScreenshot(s) {
2192
+ if (!s) return null;
2193
+ if (!(typeof Blob !== "undefined" && s instanceof Blob)) {
2194
+ const encoded = s;
2195
+ const data = encoded.data.replace(/^data:[^,]*,/, "");
2196
+ if (data.length * 0.75 > MAX_BUG_REPORT_SCREENSHOT_BYTES) throw new Error("[Guidely] captureFeedback: screenshots can be up to 4 MB");
2197
+ return { fileName: encoded.fileName || "screenshot.png", data };
2198
+ }
2199
+ if (s.size > MAX_BUG_REPORT_SCREENSHOT_BYTES) throw new Error("[Guidely] captureFeedback: screenshots can be up to 4 MB");
2200
+ const dataUrl = await new Promise((resolve, reject) => {
2201
+ const r = new FileReader();
2202
+ r.onload = () => resolve(String(r.result));
2203
+ r.onerror = () => reject(new Error("[Guidely] captureFeedback: the screenshot couldn't be read"));
2204
+ r.readAsDataURL(s);
2205
+ });
2206
+ const name = s.name || (s.type === "image/jpeg" ? "screenshot.jpg" : "screenshot.png");
2207
+ return { fileName: name, data: dataUrl.slice(dataUrl.indexOf(",") + 1) };
2208
+ }
2209
+ function bugReportBody(input, ctx, source, screenshot) {
2210
+ const tags = {};
2211
+ 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);
2212
+ const email = input.email?.trim();
2213
+ const name = input.name?.trim();
2214
+ const associated = input.associatedEventId ?? ctx.lastExceptionId;
2215
+ return {
2216
+ anonymousId: ctx.anonymousId,
2217
+ userId: ctx.userId,
2218
+ sessionId: ctx.sessionId,
2219
+ message: String(input.message ?? "").trim().slice(0, MAX_BUG_REPORT_LENGTH),
2220
+ ...email ? { email } : {},
2221
+ ...name ? { name } : {},
2222
+ ...associated ? { associatedEventId: associated } : {},
2223
+ ...ctx.url ? { url: ctx.url } : {},
2224
+ ...ctx.appVersion ? { appVersion: ctx.appVersion } : {},
2225
+ source,
2226
+ ...Object.keys(tags).length ? { tags } : {},
2227
+ sdk: ctx.sdk,
2228
+ ...screenshot ? { screenshot } : {}
2229
+ };
2230
+ }
2231
+ async function sendBugReport(api, body) {
2232
+ const res = await api.request("POST", "/bug-reports", { body, timeoutMs: body.screenshot ? 6e4 : 2e4 });
2233
+ if (res.ok && res.data && "id" in res.data) return res.data;
2234
+ if (res.status === 429) throw new Error("You've sent several reports just now. Please try again later.");
2235
+ const message = res.data && "error" in res.data ? res.data.error?.message : null;
2236
+ throw new Error(res.status === 400 && message ? message : "Your report couldn't be sent. Please try again.");
2237
+ }
2238
+
1416
2239
  // src/client.ts
1417
2240
  var SDK_NAME = "@guidelytech/sdk-core";
1418
- var SDK_VERSION = "0.16.0";
2241
+ var SDK_VERSION = "0.17.0";
1419
2242
  var DEFAULT_API_URL = "https://api.guidely.app";
1420
2243
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
1421
2244
  var DAY_MS = 24 * 60 * 60 * 1e3;
1422
2245
  var MAX_SESSION_EXCEPTIONS = 100;
1423
2246
  var CACHED_START_MS = 1500;
1424
- function settlesWithin(promise, ms) {
2247
+ function settlesWithin(promise, ms2) {
1425
2248
  return new Promise((resolve) => {
1426
- const timer = setTimeout(() => resolve(false), ms);
2249
+ const timer = setTimeout(() => resolve(false), ms2);
1427
2250
  const done = () => {
1428
2251
  clearTimeout(timer);
1429
2252
  resolve(true);
@@ -1440,8 +2263,8 @@ function nowIso() {
1440
2263
  function emptyState() {
1441
2264
  return { guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] };
1442
2265
  }
1443
- function newSession(now = Date.now()) {
1444
- return { id: `s_${randomId2(16)}`, startedAt: new Date(now).toISOString(), shownGuides: [], events: [], lastActivityAt: now };
2266
+ function newSession(now3 = Date.now()) {
2267
+ return { id: `s_${randomId2(16)}`, startedAt: new Date(now3).toISOString(), shownGuides: [], events: [], lastActivityAt: now3 };
1445
2268
  }
1446
2269
  var GuidelyClient = class _GuidelyClient {
1447
2270
  constructor(options) {
@@ -1456,6 +2279,8 @@ var GuidelyClient = class _GuidelyClient {
1456
2279
  /** Agent Analytics turns (agentConversation), in their own outbox. */
1457
2280
  __publicField(this, "agentTurns");
1458
2281
  __publicField(this, "config", null);
2282
+ /** The `environment` option as passed (the resolved one is in `options.environment`). */
2283
+ __publicField(this, "envOption");
1459
2284
  __publicField(this, "etag", null);
1460
2285
  __publicField(this, "anonymousId");
1461
2286
  __publicField(this, "user", null);
@@ -1497,6 +2322,11 @@ var GuidelyClient = class _GuidelyClient {
1497
2322
  __publicField(this, "exceptionThrottle", new ExceptionThrottle());
1498
2323
  /** Exceptions seen before the config was loaded (reported once it is known whether the server accepts them). */
1499
2324
  __publicField(this, "earlyExceptions", []);
2325
+ /** The last exception this page reported, which bug reports link to by default. */
2326
+ __publicField(this, "lastExceptionId", null);
2327
+ __publicField(this, "bugReportDialog", null);
2328
+ /** Tags, extra, contexts and breadcrumbs sent with every error (`setTag`, `addBreadcrumb`…). */
2329
+ __publicField(this, "errorScope", new ErrorScope(() => this.options.errors));
1500
2330
  __publicField(this, "initialPageViewSent", false);
1501
2331
  __publicField(this, "pageViewUrl", null);
1502
2332
  __publicField(this, "pageLeft", false);
@@ -1508,6 +2338,8 @@ var GuidelyClient = class _GuidelyClient {
1508
2338
  __publicField(this, "locale", null);
1509
2339
  /** Language each shown guide was rendered in; added to its events as `locale`. */
1510
2340
  __publicField(this, "guideLocales", /* @__PURE__ */ new Map());
2341
+ /** Performance tracing (`tracing` option + the project's `performance` setting); see tracing.ts. */
2342
+ __publicField(this, "tracer");
1511
2343
  __publicField(this, "customListeners", /* @__PURE__ */ new Map());
1512
2344
  __publicField(this, "eventBus", {
1513
2345
  on: (name, cb) => {
@@ -1524,9 +2356,10 @@ var GuidelyClient = class _GuidelyClient {
1524
2356
  });
1525
2357
  if (!isBrowser()) throw new Error("[Guidely] createGuidely() must be called in the browser. Create the client inside useEffect / onMount, or use the framework adapters.");
1526
2358
  if (!options || !options.projectId) throw new Error("[Guidely] `projectId` (your environment public key, e.g. pk_dev_\u2026) is required.");
2359
+ this.envOption = options.environment || void 0;
1527
2360
  this.options = {
1528
2361
  ...options,
1529
- environment: options.environment ?? (options.projectId.startsWith("pk_prod") ? "production" : options.projectId.startsWith("pk_stag") ? "staging" : "development"),
2362
+ environment: options.environment || environmentFromKey(options.projectId),
1530
2363
  apiUrl: options.apiUrl ?? DEFAULT_API_URL,
1531
2364
  analytics: options.analytics ?? true,
1532
2365
  consent: options.consent ?? "granted",
@@ -1570,6 +2403,16 @@ var GuidelyClient = class _GuidelyClient {
1570
2403
  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 } });
1571
2404
  if (options.user) this.user = { ...options.user };
1572
2405
  this.flags = this.createFlags(options.flags);
2406
+ this.tracer = options.tracing === false ? null : startTracer({
2407
+ ...typeof options.tracing === "object" ? options.tracing : {},
2408
+ api: this.api,
2409
+ publicKey: this.options.projectId,
2410
+ apiUrl: this.options.apiUrl,
2411
+ sdkVersion: SDK_VERSION,
2412
+ pageHref: (href) => this.pageHref(href),
2413
+ 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() }),
2414
+ allowed: () => this.consent === "pending" ? null : this.analyticsEnabled() && !this.editing()
2415
+ });
1573
2416
  }
1574
2417
  createFlags(opts) {
1575
2418
  const base = { sdkKey: this.options.projectId, apiUrl: this.options.apiUrl, fetch: this.options.fetch, sdk: { name: SDK_NAME, version: SDK_VERSION } };
@@ -1624,7 +2467,7 @@ var GuidelyClient = class _GuidelyClient {
1624
2467
  try {
1625
2468
  const cached = readJson(this.store, KEYS.config(this.options.projectId), null);
1626
2469
  this.etag = this.store.get(KEYS.etag(this.options.projectId));
1627
- if (cached && cached.guides) this.config = webOnly(cached);
2470
+ if (cached && cached.guides) this.useConfig(webOnly(cached));
1628
2471
  const before = this.config?.configVersion;
1629
2472
  const fresh = this.fetchConfig();
1630
2473
  if (!this.config) await fresh;
@@ -1653,12 +2496,22 @@ var GuidelyClient = class _GuidelyClient {
1653
2496
  this.resolveReady?.();
1654
2497
  }
1655
2498
  }
2499
+ /** Environment guides and flags target: the `environment` option, else the one the loaded config names, else a guess from the key. */
2500
+ get environment() {
2501
+ return this.envOption ?? this.config?.environment ?? this.options.environment;
2502
+ }
2503
+ useConfig(config) {
2504
+ const env = this.environment;
2505
+ this.config = config;
2506
+ if (this.environment !== env) this.syncFlagAttributes();
2507
+ this.tracer?.configure(config.settings?.performance);
2508
+ }
1656
2509
  async fetchConfig() {
1657
2510
  try {
1658
2511
  const res = await this.api.get("/config", { etag: this.etag, params: { platform: "web" } });
1659
2512
  if (res.status === 304) return true;
1660
2513
  if (res.ok && res.data && Array.isArray(res.data.guides)) {
1661
- this.config = webOnly(res.data);
2514
+ this.useConfig(webOnly(res.data));
1662
2515
  this.etag = res.etag;
1663
2516
  writeJson(this.store, KEYS.config(this.options.projectId), res.data);
1664
2517
  if (res.etag) this.store.set(KEYS.etag(this.options.projectId), res.etag);
@@ -1711,9 +2564,9 @@ var GuidelyClient = class _GuidelyClient {
1711
2564
  };
1712
2565
  let lastScroll = 0;
1713
2566
  const onScroll = () => {
1714
- const now = Date.now();
1715
- if (now - lastScroll < 250) return;
1716
- lastScroll = now;
2567
+ const now3 = Date.now();
2568
+ if (now3 - lastScroll < 250) return;
2569
+ lastScroll = now3;
1717
2570
  this.pageTracker.measureScroll();
1718
2571
  this.touchSession();
1719
2572
  };
@@ -1771,14 +2624,22 @@ var GuidelyClient = class _GuidelyClient {
1771
2624
  /** Uncaught errors + unhandled rejections; attached before the config loads so boot errors are kept. */
1772
2625
  wireErrorListeners() {
1773
2626
  const win = window;
1774
- const onError = (e) => this.captureGlobalException(errorEventToException(e));
1775
- const onRejection = (e) => this.captureGlobalException(rejectionToException(e));
2627
+ const onError = (e) => this.captureGlobalException(errorEventToException(e), void 0, e.error);
2628
+ const onRejection = (e) => this.captureGlobalException(rejectionToException(e), void 0, e.reason);
1776
2629
  win.addEventListener("error", onError);
1777
2630
  win.addEventListener("unhandledrejection", onRejection);
1778
2631
  this.cleanups.push(() => {
1779
2632
  win.removeEventListener("error", onError);
1780
2633
  win.removeEventListener("unhandledrejection", onRejection);
1781
2634
  });
2635
+ const crumbs = this.options.errors?.breadcrumbs;
2636
+ if (crumbs !== false) this.cleanups.push(instrumentBreadcrumbs((c, hint) => this.errorScope.addBreadcrumb(c, hint), { console: crumbs?.console, http: crumbs?.http, apiUrl: this.options.apiUrl }));
2637
+ }
2638
+ /** An automatic breadcrumb, unless the `errors.breadcrumbs` option turns its kind off. */
2639
+ autoBreadcrumb(kind, crumb, hint) {
2640
+ const o = this.options.errors?.breadcrumbs;
2641
+ if (o === false || o && o[kind] === false) return;
2642
+ this.errorScope.addBreadcrumb(crumb, hint === void 0 ? void 0 : { event: hint });
1782
2643
  }
1783
2644
  /** Re-fetches the published config and re-evaluates triggers. */
1784
2645
  async refresh() {
@@ -1826,6 +2687,7 @@ var GuidelyClient = class _GuidelyClient {
1826
2687
  this.queue.destroy();
1827
2688
  this.outbox.destroy();
1828
2689
  this.agentTurns.destroy();
2690
+ this.tracer?.destroy();
1829
2691
  this.flags.destroy();
1830
2692
  this.emitter.clear();
1831
2693
  destroyHost();
@@ -1955,6 +2817,11 @@ var GuidelyClient = class _GuidelyClient {
1955
2817
  analyticsEnabled() {
1956
2818
  return this.options.analytics && this.consent === "granted" && (this.config?.settings?.analyticsEnabled ?? true);
1957
2819
  }
2820
+ /** The host app's version (`appVersion` option), sent with every event and recording. */
2821
+ appVersion() {
2822
+ const v = this.options.appVersion;
2823
+ return v === void 0 || v === null || String(v).trim() === "" ? void 0 : String(v).trim().slice(0, 200);
2824
+ }
1958
2825
  collectUrls() {
1959
2826
  return this.config?.settings?.collectUrls ?? true;
1960
2827
  }
@@ -1970,12 +2837,14 @@ var GuidelyClient = class _GuidelyClient {
1970
2837
  accountId: this.user?.account?.id || null,
1971
2838
  sessionId: this.session.id,
1972
2839
  url: this.collectUrls() ? this.pageHref() : void 0,
2840
+ ...this.appVersion() ? { appVersion: this.appVersion() } : {},
1973
2841
  ...extra
1974
2842
  };
1975
2843
  if (extra.properties) event.properties = sanitizeProperties(extra.properties);
1976
2844
  const locale = extra.guideId ? this.guideLocales.get(extra.guideId) : void 0;
1977
2845
  if (locale) event.properties = { ...event.properties ?? {}, locale };
1978
2846
  this.queue.enqueue(event);
2847
+ 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 } : {} } });
1979
2848
  this.replay?.addCustomEvent("guidely", { name, customName: event.customName, guideId: event.guideId });
1980
2849
  this.emitter.emit("track", event);
1981
2850
  return event;
@@ -2029,6 +2898,7 @@ var GuidelyClient = class _GuidelyClient {
2029
2898
  handleNavigation(url, explicit = false) {
2030
2899
  if (this.destroyed) return;
2031
2900
  const changed = url !== this.lastUrl;
2901
+ if (changed && this.lastUrl) this.autoBreadcrumb("navigation", { category: "navigation", data: this.collectUrls() ? { from: breadcrumbUrl(this.lastUrl), to: breadcrumbUrl(url) } : {} });
2032
2902
  this.lastUrl = url;
2033
2903
  if (changed) this.syncFlagAttributes();
2034
2904
  if (!explicit && changed) {
@@ -2057,6 +2927,7 @@ var GuidelyClient = class _GuidelyClient {
2057
2927
  handleDocumentClick(e) {
2058
2928
  const target = composedTarget(e);
2059
2929
  if (!target || typeof target.closest !== "function") return;
2930
+ if (!isIgnoredElement(target)) this.autoBreadcrumb("clicks", { category: "ui.click", message: elementSelector(interactiveElement(target) ?? target) }, e);
2060
2931
  this.autocaptureClick(e, target);
2061
2932
  this.completeChecklistTasksBy("click", target);
2062
2933
  if (this.isReady) this.evaluate("click", { target });
@@ -2066,17 +2937,17 @@ var GuidelyClient = class _GuidelyClient {
2066
2937
  // ---------------------------------------------------------------------------
2067
2938
  /** Records activity; starts a new session after `sessionTimeoutMinutes` of inactivity or 24h. */
2068
2939
  touchSession() {
2069
- const now = Date.now();
2940
+ const now3 = Date.now();
2070
2941
  const s = this.session;
2071
2942
  const timeout = Math.max(1, this.config?.settings?.sessionTimeoutMinutes ?? 30) * 6e4;
2072
- const last = s.lastActivityAt ?? now;
2073
- if (now - last > timeout || now - (Date.parse(s.startedAt) || now) > DAY_MS) return this.rotateSession(now);
2074
- if (now - (s.lastActivityAt ?? 0) < 1e3) return;
2075
- s.lastActivityAt = now;
2943
+ const last = s.lastActivityAt ?? now3;
2944
+ if (now3 - last > timeout || now3 - (Date.parse(s.startedAt) || now3) > DAY_MS) return this.rotateSession(now3);
2945
+ if (now3 - (s.lastActivityAt ?? 0) < 1e3) return;
2946
+ s.lastActivityAt = now3;
2076
2947
  this.persistSession();
2077
2948
  }
2078
- rotateSession(now) {
2079
- this.session = newSession(now);
2949
+ rotateSession(now3) {
2950
+ this.session = newSession(now3);
2080
2951
  this.persistSession();
2081
2952
  this.log("session rotated", this.session.id);
2082
2953
  if (this.replay?.activeSessionId) this.replay.stop();
@@ -2108,7 +2979,7 @@ var GuidelyClient = class _GuidelyClient {
2108
2979
  const device = detectDevice(navigator.userAgent, window.innerWidth);
2109
2980
  const props = { deviceType: device.type, browser: device.browser, os: device.os, screenWidth: window.screen?.width, viewportWidth: window.innerWidth };
2110
2981
  if (collect) {
2111
- props.path = pathOf2(url);
2982
+ props.path = pathOf3(url);
2112
2983
  props.title = (document.title || "").slice(0, 200);
2113
2984
  const ref = !this.session.pageViews ? externalReferrer() : void 0;
2114
2985
  if (ref) props.referrer = ref;
@@ -2127,7 +2998,7 @@ var GuidelyClient = class _GuidelyClient {
2127
2998
  if (!this.pageLeft && this.autocaptureOn("pageLeave")) {
2128
2999
  const info = t.leave();
2129
3000
  const collect = this.collectUrls();
2130
- this.enqueue("page_leave", { url: collect ? info.url : void 0, properties: { ...collect ? { path: pathOf2(info.url) } : {}, durationMs: info.durationMs, scrollDepth: info.scrollDepth } });
3001
+ this.enqueue("page_leave", { url: collect ? info.url : void 0, properties: { ...collect ? { path: pathOf3(info.url) } : {}, durationMs: info.durationMs, scrollDepth: info.scrollDepth } });
2131
3002
  }
2132
3003
  if (nextUrl !== null) {
2133
3004
  t.start(nextUrl);
@@ -2171,42 +3042,120 @@ var GuidelyClient = class _GuidelyClient {
2171
3042
  if (!batch || !this.captureAllowed()) return;
2172
3043
  const collect = this.collectUrls();
2173
3044
  const de = document.documentElement;
2174
- 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 };
3045
+ 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 };
2175
3046
  if (!collect) delete properties.path;
2176
3047
  this.enqueue("heatmap", { url: collect ? batch.url : void 0, properties });
2177
3048
  }
2178
- captureGlobalException(props) {
3049
+ captureGlobalException(props, detail, error) {
2179
3050
  if (!props || isGuidelyException(props) || this.options.captureErrors === false) return;
3051
+ const d = detail ?? this.errorDetail(error, props, props.source === "unhandledrejection" ? "onunhandledrejection" : "onerror", {}, "error");
2180
3052
  if (!this.config) {
2181
- if (this.earlyExceptions.length < 10) this.earlyExceptions.push([props, void 0]);
3053
+ if (this.earlyExceptions.length < 10) this.earlyExceptions.push([props, void 0, d]);
2182
3054
  return;
2183
3055
  }
2184
- this.sendException(props);
3056
+ this.sendException(props, void 0, d);
2185
3057
  }
2186
3058
  /**
2187
3059
  * Reports a caught error as an `exception` event (`source: "manual"`, `handled: true`), grouped
2188
- * into an issue on the Errors page. Extra properties are attached to the occurrence. Needs the
2189
- * project's error tracking on (plus analytics + consent); `captureErrors: false` does not stop it.
3060
+ * into an issue on the Errors page. The second argument takes Sentry's capture context (`tags`,
3061
+ * `extra`, `level`, `fingerprint`, `contexts`, `user`); any other key is attached to the occurrence
3062
+ * as a property. Needs the project's error tracking on (plus analytics + consent);
3063
+ * `captureErrors: false` does not stop it. Returns the event id (empty when nothing is sent).
2190
3064
  */
2191
- captureException(error, properties) {
2192
- if (this.destroyed) return;
2193
- const props = exceptionFromError(error, "manual", true);
3065
+ captureException(error, hint) {
3066
+ if (this.destroyed) return "";
3067
+ const { ctx, properties } = splitHint(hint);
3068
+ const props = exceptionFromError(error, "manual", ctx.mechanism?.handled ?? true);
3069
+ return this.captureWith(props, properties, this.errorDetail(error, props, ctx.mechanism?.type ?? "generic", ctx, ctx.level ?? "error"));
3070
+ }
3071
+ /** Reports a message (default level `info`) as its own issue, grouped by the message. */
3072
+ captureMessage(message, level = "info", hint) {
3073
+ if (this.destroyed) return "";
3074
+ const props = { type: "Message", message: String(message).slice(0, 1e3), frames: [], source: "message", handled: true };
3075
+ return this.captureWith(props, {}, this.errorDetail(void 0, props, "message", hint ?? {}, hint?.level ?? level));
3076
+ }
3077
+ captureWith(props, properties, detail) {
2194
3078
  if (!this.config) {
2195
- if (this.earlyExceptions.length < 10) this.earlyExceptions.push([props, properties]);
2196
- return;
3079
+ if (this.earlyExceptions.length < 10) this.earlyExceptions.push([props, properties, detail]);
3080
+ return "";
2197
3081
  }
2198
- this.sendException(props, properties);
3082
+ return this.sendException(props, properties, detail);
2199
3083
  }
2200
- sendException(props, extra) {
2201
- if (!this.config?.settings?.errorTracking?.enabled || !this.captureAllowed() || !this.exceptionThrottle.allow(props)) return;
3084
+ /** The `detail` of an error: scope, breadcrumbs, browser contexts and the `cause` chain. */
3085
+ errorDetail(error, props, mechanism, ctx, level) {
3086
+ const device = detectDevice(navigator.userAgent, window.innerWidth);
3087
+ return buildDetail({ error, props, mechanism, scope: this.errorScope, ctx, level, contexts: browserContexts(navigator.userAgent, device) });
3088
+ }
3089
+ /** Adds a breadcrumb (sent with the next errors; the last 100 are kept). */
3090
+ addBreadcrumb(crumb) {
3091
+ if (!this.destroyed) this.errorScope.addBreadcrumb(crumb);
3092
+ }
3093
+ /** Tags every later error (searchable on the issue page). */
3094
+ setTag(key, value) {
3095
+ if (value === null || value === void 0) delete this.errorScope.tags[key];
3096
+ else this.errorScope.tags[key] = String(value);
3097
+ }
3098
+ setTags(tags) {
3099
+ for (const [k, v] of Object.entries(tags)) this.setTag(k, v);
3100
+ }
3101
+ /** Arbitrary data attached to every later error. */
3102
+ setExtra(key, value) {
3103
+ if (value === void 0) delete this.errorScope.extra[key];
3104
+ else this.errorScope.extra[key] = value;
3105
+ }
3106
+ setExtras(extras) {
3107
+ for (const [k, v] of Object.entries(extras)) this.setExtra(k, v);
3108
+ }
3109
+ /** A named context (`setContext("cart", { items: 3 })`) on every later error; null removes it. */
3110
+ setContext(name, context) {
3111
+ if (context) this.errorScope.contexts[name] = context;
3112
+ else delete this.errorScope.contexts[name];
3113
+ }
3114
+ /**
3115
+ * Sentry's `setUser`: identifies the user by `id` (like `identify`) and keeps `email` / `username`
3116
+ * on their errors. `null` forgets the user (`reset()`).
3117
+ */
3118
+ setUser(user) {
3119
+ if (!user) {
3120
+ this.errorScope.user = {};
3121
+ return this.reset();
3122
+ }
3123
+ const { id, email, username, ...traits } = user;
3124
+ this.errorScope.user = { ...email ? { email: String(email) } : {}, ...username ? { username: String(username) } : {} };
3125
+ if (id !== void 0 && id !== null && String(id)) void this.identify(String(id), { ...traits, ...email ? { email } : {}, ...username ? { username } : {} });
3126
+ }
3127
+ sendException(props, extra, detail) {
3128
+ if (!this.config?.settings?.errorTracking?.enabled || !this.captureAllowed()) return "";
3129
+ const o = this.options.errors;
3130
+ const file = props.frames.find((f) => f.file)?.file;
3131
+ if (o && errorFilterDrops({ type: props.type, message: props.message, file }, o)) return "";
3132
+ if (o?.sampleRate !== void 0 && o.sampleRate < 1 && Math.random() >= Math.max(0, o.sampleRate)) return "";
3133
+ if (!this.exceptionThrottle.allow(props)) return "";
2202
3134
  const count = this.session.exceptions ?? 0;
2203
- if (count >= MAX_SESSION_EXCEPTIONS) return;
2204
- const properties = { ...props };
3135
+ if (count >= MAX_SESSION_EXCEPTIONS) return "";
3136
+ const mechanism = detail?.exception?.values.at(-1)?.mechanism?.type;
3137
+ let properties = { ...props, level: detail?.level ?? "error", platform: "javascript", ...mechanism ? { mechanism } : {} };
2205
3138
  if (this.options.release) properties.release = String(this.options.release).slice(0, 100);
3139
+ const trace = this.tracer?.traceContext();
3140
+ if (trace) Object.assign(properties, { traceId: trace.traceId, spanId: trace.spanId });
2206
3141
  for (const [k, v] of Object.entries(extra ?? {})) if (!(k in properties)) properties[k] = v;
2207
- this.enqueue("exception", { properties });
3142
+ let d = detail;
3143
+ if (o?.beforeSend) {
3144
+ try {
3145
+ const out = o.beforeSend({ properties, detail: d ?? {} });
3146
+ if (!out) return "";
3147
+ properties = out.properties;
3148
+ d = out.detail;
3149
+ } catch (err) {
3150
+ this.reportError(err);
3151
+ }
3152
+ }
3153
+ const event = this.enqueue("exception", { properties, ...d ? { detail: d } : {} });
3154
+ if (!event) return "";
3155
+ this.lastExceptionId = event.eventId;
2208
3156
  this.session.exceptions = count + 1;
2209
3157
  this.persistSession();
3158
+ return event.eventId;
2210
3159
  }
2211
3160
  maskSelector() {
2212
3161
  return validSelectors([...DEFAULT_MASK_SELECTORS, ...this.config?.settings?.maskSelectors || [], ...this.options.maskSelectors || []]).join(",");
@@ -2240,9 +3189,9 @@ var GuidelyClient = class _GuidelyClient {
2240
3189
  this.trackPageView(this.pageHref());
2241
3190
  }
2242
3191
  if (this.config) {
2243
- for (const [props, extra] of this.earlyExceptions.splice(0)) {
2244
- if (props.handled) this.sendException(props, extra);
2245
- else this.captureGlobalException(props);
3192
+ for (const [props, extra, detail] of this.earlyExceptions.splice(0)) {
3193
+ if (props.handled) this.sendException(props, extra, detail);
3194
+ else this.captureGlobalException(props, detail);
2246
3195
  }
2247
3196
  }
2248
3197
  this.syncReplay();
@@ -2275,7 +3224,8 @@ var GuidelyClient = class _GuidelyClient {
2275
3224
  url: this.collectUrls() ? this.pageHref() : void 0,
2276
3225
  device: { type: device.type, browser: device.browser, os: device.os },
2277
3226
  viewport: { width: window.innerWidth, height: window.innerHeight },
2278
- sdk: { name: SDK_NAME, version: SDK_VERSION }
3227
+ sdk: { name: SDK_NAME, version: SDK_VERSION },
3228
+ ...this.appVersion() ? { appVersion: this.appVersion() } : {}
2279
3229
  };
2280
3230
  },
2281
3231
  onDisabled: (sessionId) => {
@@ -2349,7 +3299,7 @@ var GuidelyClient = class _GuidelyClient {
2349
3299
  seenGuides: keysOf(Object.keys(guides).filter((id) => (guides[id]?.impressions ?? 0) > 0)),
2350
3300
  completedChecklists: keysOf(completedChecklists),
2351
3301
  audienceIds: this.state.audienceIds,
2352
- environment: this.options.environment,
3302
+ environment: this.environment,
2353
3303
  platform: "web",
2354
3304
  ...this.options.appVersion ? { appVersion: this.options.appVersion } : {}
2355
3305
  });
@@ -2512,17 +3462,17 @@ var GuidelyClient = class _GuidelyClient {
2512
3462
  this.evaluate("visible", { guideId: guide.id });
2513
3463
  };
2514
3464
  if (typeof IntersectionObserver === "undefined") return fire();
2515
- io = new IntersectionObserver((entries) => {
2516
- if (entries.some((e) => e.isIntersecting)) fire();
3465
+ io = new IntersectionObserver((entries2) => {
3466
+ if (entries2.some((e) => e.isIntersecting)) fire();
2517
3467
  });
2518
3468
  io.observe(el);
2519
3469
  }).catch(() => stop());
2520
3470
  }
2521
- setTimer(fn, ms) {
3471
+ setTimer(fn, ms2) {
2522
3472
  const t = setTimeout(() => {
2523
3473
  this.timers.delete(t);
2524
3474
  if (!this.destroyed) fn();
2525
- }, ms);
3475
+ }, ms2);
2526
3476
  this.timers.add(t);
2527
3477
  }
2528
3478
  /** Decides whether a guide can show now, otherwise queues it. */
@@ -3127,6 +4077,49 @@ var GuidelyClient = class _GuidelyClient {
3127
4077
  if (!this.config?.messenger?.enabled || !this.config.messenger.ideas) return;
3128
4078
  void this.loadMessenger().then((m) => m?.openIdeas(requestId));
3129
4079
  }
4080
+ /**
4081
+ * Sends a bug report to Monitor → User feedback (Sentry's `captureFeedback`). It is linked to the visitor,
4082
+ * this session's recording and, unless `associatedEventId` says otherwise, the last error this page
4083
+ * reported. Resolves with the report's id; rejects when it can't be sent.
4084
+ */
4085
+ async captureFeedback(input, source = "api") {
4086
+ if (this.destroyed) throw new Error("[Guidely] captureFeedback: the SDK was destroyed");
4087
+ if (!input || !String(input.message ?? "").trim()) throw new Error("[Guidely] captureFeedback: message is required");
4088
+ const screenshot = await encodeScreenshot(input.screenshot);
4089
+ if (this.lastExceptionId) await this.queue.flush().catch(() => {
4090
+ });
4091
+ const body = bugReportBody(
4092
+ input,
4093
+ { 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 } },
4094
+ source,
4095
+ screenshot
4096
+ );
4097
+ return sendBugReport(this.api, body);
4098
+ }
4099
+ /** Opens the "Report a bug" dialog (message, name, email, a screenshot), which sends with captureFeedback. */
4100
+ showBugReport(options = {}) {
4101
+ if (this.destroyed || typeof document === "undefined" || this.bugReportDialog) return;
4102
+ void import("@guidelytech/guide-runtime/bug-report").then(({ renderBugReport }) => {
4103
+ if (this.destroyed || this.bugReportDialog) return;
4104
+ const scope = createScope(this.theme());
4105
+ const traits = this.user?.traits ?? {};
4106
+ const str = (v) => typeof v === "string" ? v : void 0;
4107
+ this.bugReportDialog = renderBugReport({
4108
+ scope: scope.el,
4109
+ title: options.title,
4110
+ message: options.message,
4111
+ name: options.name ?? str(traits.name),
4112
+ email: options.email ?? str(traits.email),
4113
+ onSubmit: async (i) => {
4114
+ await this.captureFeedback({ message: i.message, name: i.name || null, email: i.email || null, screenshot: i.screenshot }, "widget");
4115
+ },
4116
+ onClose: () => {
4117
+ this.bugReportDialog = null;
4118
+ scope.destroy();
4119
+ }
4120
+ });
4121
+ }).catch((err) => this.reportError(err));
4122
+ }
3130
4123
  /** Opens a new messenger conversation, optionally pre-filling the message. */
3131
4124
  showNewMessage(text) {
3132
4125
  if (!this.config?.messenger?.enabled) return;
@@ -3157,12 +4150,12 @@ var GuidelyClient = class _GuidelyClient {
3157
4150
  const account = this.user?.account;
3158
4151
  return {
3159
4152
  url: this.pageHref(),
3160
- path: pathOf2(this.pageHref()),
4153
+ path: pathOf3(this.pageHref()),
3161
4154
  deviceType: device.type,
3162
4155
  browser: device.browser,
3163
4156
  os: device.os,
3164
4157
  locale: navigator.language || void 0,
3165
- environment: this.options.environment,
4158
+ environment: this.environment,
3166
4159
  platform: "web",
3167
4160
  ...this.options.appVersion ? { appVersion: this.options.appVersion } : {},
3168
4161
  ...this.user?.traits ?? {},
@@ -3339,7 +4332,7 @@ var current2 = null;
3339
4332
  function webOnly(config) {
3340
4333
  return config.guides.every((g) => supportsPlatform(g.config, "web")) ? config : { ...config, guides: config.guides.filter((g) => supportsPlatform(g.config, "web")) };
3341
4334
  }
3342
- function pathOf2(url) {
4335
+ function pathOf3(url) {
3343
4336
  try {
3344
4337
  return new URL(url, "http://localhost").pathname;
3345
4338
  } catch {
@@ -3390,6 +4383,7 @@ export {
3390
4383
  createStorage,
3391
4384
  index_default as default,
3392
4385
  detectDevice,
4386
+ environmentFromKey,
3393
4387
  isAppUrl,
3394
4388
  isExclusiveGuide,
3395
4389
  isFrequencyEligible,