@guidelytech/sdk-core 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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 { 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";
@@ -1098,7 +1316,7 @@ function createMessengerTransport(opts) {
1098
1316
  }
1099
1317
 
1100
1318
  // src/messenger-visitor.ts
1101
- import { firstNameFromTraits } from "@guidelytech/shared/runtime";
1319
+ import { firstNameFromTraits, videoThumbnailUrl } from "@guidelytech/shared/runtime";
1102
1320
  import { urlMatches } from "@guidelytech/guide-runtime";
1103
1321
  var PAGE_GUIDE_KINDS = /* @__PURE__ */ new Set(["flow", "tooltip", "modal"]);
1104
1322
  var CATCH_ALL = /* @__PURE__ */ new Set(["", "*", "**", "/*", "/**", "/"]);
@@ -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,14 +1370,14 @@ 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,
1159
1377
  key: g.key,
1160
1378
  title: step.title || g.name,
1161
1379
  body: plainText(step.body ?? ""),
1162
- imageUrl: step.imageUrl || null,
1380
+ imageUrl: step.imageUrl || videoThumbnailUrl(step.videoUrl) || null,
1163
1381
  publishedAt: g.publishedAt,
1164
1382
  // Read once opened in the News tab, or once the announcement itself was shown.
1165
1383
  unread: !seen.has(g.id) && !(state.guides[g.id]?.impressions ?? 0)
@@ -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.15.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);
@@ -1681,6 +2534,15 @@ var GuidelyClient = class _GuidelyClient {
1681
2534
  const onClick = (e) => this.handleDocumentClick(e);
1682
2535
  doc.addEventListener("click", onClick, true);
1683
2536
  this.cleanups.push(() => doc.removeEventListener("click", onClick, true));
2537
+ const onArticleLink = (e) => {
2538
+ if (e.defaultPrevented) return;
2539
+ const articleId = articleIdFromHref(composedTarget(e)?.closest?.("a[href]")?.getAttribute("href"));
2540
+ if (!articleId) return;
2541
+ e.preventDefault();
2542
+ this.openArticle(articleId);
2543
+ };
2544
+ doc.addEventListener("click", onArticleLink);
2545
+ this.cleanups.push(() => doc.removeEventListener("click", onArticleLink));
1684
2546
  const onVisibility = () => {
1685
2547
  const hidden = doc.visibilityState === "hidden";
1686
2548
  this.pageTracker.setHidden(hidden);
@@ -1702,9 +2564,9 @@ var GuidelyClient = class _GuidelyClient {
1702
2564
  };
1703
2565
  let lastScroll = 0;
1704
2566
  const onScroll = () => {
1705
- const now = Date.now();
1706
- if (now - lastScroll < 250) return;
1707
- lastScroll = now;
2567
+ const now3 = Date.now();
2568
+ if (now3 - lastScroll < 250) return;
2569
+ lastScroll = now3;
1708
2570
  this.pageTracker.measureScroll();
1709
2571
  this.touchSession();
1710
2572
  };
@@ -1762,14 +2624,22 @@ var GuidelyClient = class _GuidelyClient {
1762
2624
  /** Uncaught errors + unhandled rejections; attached before the config loads so boot errors are kept. */
1763
2625
  wireErrorListeners() {
1764
2626
  const win = window;
1765
- const onError = (e) => this.captureGlobalException(errorEventToException(e));
1766
- 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);
1767
2629
  win.addEventListener("error", onError);
1768
2630
  win.addEventListener("unhandledrejection", onRejection);
1769
2631
  this.cleanups.push(() => {
1770
2632
  win.removeEventListener("error", onError);
1771
2633
  win.removeEventListener("unhandledrejection", onRejection);
1772
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 });
1773
2643
  }
1774
2644
  /** Re-fetches the published config and re-evaluates triggers. */
1775
2645
  async refresh() {
@@ -1817,6 +2687,7 @@ var GuidelyClient = class _GuidelyClient {
1817
2687
  this.queue.destroy();
1818
2688
  this.outbox.destroy();
1819
2689
  this.agentTurns.destroy();
2690
+ this.tracer?.destroy();
1820
2691
  this.flags.destroy();
1821
2692
  this.emitter.clear();
1822
2693
  destroyHost();
@@ -1946,6 +2817,11 @@ var GuidelyClient = class _GuidelyClient {
1946
2817
  analyticsEnabled() {
1947
2818
  return this.options.analytics && this.consent === "granted" && (this.config?.settings?.analyticsEnabled ?? true);
1948
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
+ }
1949
2825
  collectUrls() {
1950
2826
  return this.config?.settings?.collectUrls ?? true;
1951
2827
  }
@@ -1961,12 +2837,14 @@ var GuidelyClient = class _GuidelyClient {
1961
2837
  accountId: this.user?.account?.id || null,
1962
2838
  sessionId: this.session.id,
1963
2839
  url: this.collectUrls() ? this.pageHref() : void 0,
2840
+ ...this.appVersion() ? { appVersion: this.appVersion() } : {},
1964
2841
  ...extra
1965
2842
  };
1966
2843
  if (extra.properties) event.properties = sanitizeProperties(extra.properties);
1967
2844
  const locale = extra.guideId ? this.guideLocales.get(extra.guideId) : void 0;
1968
2845
  if (locale) event.properties = { ...event.properties ?? {}, locale };
1969
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 } : {} } });
1970
2848
  this.replay?.addCustomEvent("guidely", { name, customName: event.customName, guideId: event.guideId });
1971
2849
  this.emitter.emit("track", event);
1972
2850
  return event;
@@ -2020,6 +2898,7 @@ var GuidelyClient = class _GuidelyClient {
2020
2898
  handleNavigation(url, explicit = false) {
2021
2899
  if (this.destroyed) return;
2022
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) } : {} });
2023
2902
  this.lastUrl = url;
2024
2903
  if (changed) this.syncFlagAttributes();
2025
2904
  if (!explicit && changed) {
@@ -2048,6 +2927,7 @@ var GuidelyClient = class _GuidelyClient {
2048
2927
  handleDocumentClick(e) {
2049
2928
  const target = composedTarget(e);
2050
2929
  if (!target || typeof target.closest !== "function") return;
2930
+ if (!isIgnoredElement(target)) this.autoBreadcrumb("clicks", { category: "ui.click", message: elementSelector(interactiveElement(target) ?? target) }, e);
2051
2931
  this.autocaptureClick(e, target);
2052
2932
  this.completeChecklistTasksBy("click", target);
2053
2933
  if (this.isReady) this.evaluate("click", { target });
@@ -2057,17 +2937,17 @@ var GuidelyClient = class _GuidelyClient {
2057
2937
  // ---------------------------------------------------------------------------
2058
2938
  /** Records activity; starts a new session after `sessionTimeoutMinutes` of inactivity or 24h. */
2059
2939
  touchSession() {
2060
- const now = Date.now();
2940
+ const now3 = Date.now();
2061
2941
  const s = this.session;
2062
2942
  const timeout = Math.max(1, this.config?.settings?.sessionTimeoutMinutes ?? 30) * 6e4;
2063
- const last = s.lastActivityAt ?? now;
2064
- if (now - last > timeout || now - (Date.parse(s.startedAt) || now) > DAY_MS) return this.rotateSession(now);
2065
- if (now - (s.lastActivityAt ?? 0) < 1e3) return;
2066
- 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;
2067
2947
  this.persistSession();
2068
2948
  }
2069
- rotateSession(now) {
2070
- this.session = newSession(now);
2949
+ rotateSession(now3) {
2950
+ this.session = newSession(now3);
2071
2951
  this.persistSession();
2072
2952
  this.log("session rotated", this.session.id);
2073
2953
  if (this.replay?.activeSessionId) this.replay.stop();
@@ -2099,7 +2979,7 @@ var GuidelyClient = class _GuidelyClient {
2099
2979
  const device = detectDevice(navigator.userAgent, window.innerWidth);
2100
2980
  const props = { deviceType: device.type, browser: device.browser, os: device.os, screenWidth: window.screen?.width, viewportWidth: window.innerWidth };
2101
2981
  if (collect) {
2102
- props.path = pathOf2(url);
2982
+ props.path = pathOf3(url);
2103
2983
  props.title = (document.title || "").slice(0, 200);
2104
2984
  const ref = !this.session.pageViews ? externalReferrer() : void 0;
2105
2985
  if (ref) props.referrer = ref;
@@ -2118,7 +2998,7 @@ var GuidelyClient = class _GuidelyClient {
2118
2998
  if (!this.pageLeft && this.autocaptureOn("pageLeave")) {
2119
2999
  const info = t.leave();
2120
3000
  const collect = this.collectUrls();
2121
- 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 } });
2122
3002
  }
2123
3003
  if (nextUrl !== null) {
2124
3004
  t.start(nextUrl);
@@ -2162,42 +3042,120 @@ var GuidelyClient = class _GuidelyClient {
2162
3042
  if (!batch || !this.captureAllowed()) return;
2163
3043
  const collect = this.collectUrls();
2164
3044
  const de = document.documentElement;
2165
- 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 };
2166
3046
  if (!collect) delete properties.path;
2167
3047
  this.enqueue("heatmap", { url: collect ? batch.url : void 0, properties });
2168
3048
  }
2169
- captureGlobalException(props) {
3049
+ captureGlobalException(props, detail, error) {
2170
3050
  if (!props || isGuidelyException(props) || this.options.captureErrors === false) return;
3051
+ const d = detail ?? this.errorDetail(error, props, props.source === "unhandledrejection" ? "onunhandledrejection" : "onerror", {}, "error");
2171
3052
  if (!this.config) {
2172
- if (this.earlyExceptions.length < 10) this.earlyExceptions.push([props, void 0]);
3053
+ if (this.earlyExceptions.length < 10) this.earlyExceptions.push([props, void 0, d]);
2173
3054
  return;
2174
3055
  }
2175
- this.sendException(props);
3056
+ this.sendException(props, void 0, d);
2176
3057
  }
2177
3058
  /**
2178
3059
  * Reports a caught error as an `exception` event (`source: "manual"`, `handled: true`), grouped
2179
- * into an issue on the Errors page. Extra properties are attached to the occurrence. Needs the
2180
- * 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).
2181
3064
  */
2182
- captureException(error, properties) {
2183
- if (this.destroyed) return;
2184
- 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) {
2185
3078
  if (!this.config) {
2186
- if (this.earlyExceptions.length < 10) this.earlyExceptions.push([props, properties]);
2187
- return;
3079
+ if (this.earlyExceptions.length < 10) this.earlyExceptions.push([props, properties, detail]);
3080
+ return "";
2188
3081
  }
2189
- this.sendException(props, properties);
3082
+ return this.sendException(props, properties, detail);
3083
+ }
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);
2190
3100
  }
2191
- sendException(props, extra) {
2192
- if (!this.config?.settings?.errorTracking?.enabled || !this.captureAllowed() || !this.exceptionThrottle.allow(props)) return;
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 "";
2193
3134
  const count = this.session.exceptions ?? 0;
2194
- if (count >= MAX_SESSION_EXCEPTIONS) return;
2195
- 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 } : {} };
2196
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 });
2197
3141
  for (const [k, v] of Object.entries(extra ?? {})) if (!(k in properties)) properties[k] = v;
2198
- 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;
2199
3156
  this.session.exceptions = count + 1;
2200
3157
  this.persistSession();
3158
+ return event.eventId;
2201
3159
  }
2202
3160
  maskSelector() {
2203
3161
  return validSelectors([...DEFAULT_MASK_SELECTORS, ...this.config?.settings?.maskSelectors || [], ...this.options.maskSelectors || []]).join(",");
@@ -2231,9 +3189,9 @@ var GuidelyClient = class _GuidelyClient {
2231
3189
  this.trackPageView(this.pageHref());
2232
3190
  }
2233
3191
  if (this.config) {
2234
- for (const [props, extra] of this.earlyExceptions.splice(0)) {
2235
- if (props.handled) this.sendException(props, extra);
2236
- 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);
2237
3195
  }
2238
3196
  }
2239
3197
  this.syncReplay();
@@ -2266,7 +3224,8 @@ var GuidelyClient = class _GuidelyClient {
2266
3224
  url: this.collectUrls() ? this.pageHref() : void 0,
2267
3225
  device: { type: device.type, browser: device.browser, os: device.os },
2268
3226
  viewport: { width: window.innerWidth, height: window.innerHeight },
2269
- sdk: { name: SDK_NAME, version: SDK_VERSION }
3227
+ sdk: { name: SDK_NAME, version: SDK_VERSION },
3228
+ ...this.appVersion() ? { appVersion: this.appVersion() } : {}
2270
3229
  };
2271
3230
  },
2272
3231
  onDisabled: (sessionId) => {
@@ -2340,7 +3299,7 @@ var GuidelyClient = class _GuidelyClient {
2340
3299
  seenGuides: keysOf(Object.keys(guides).filter((id) => (guides[id]?.impressions ?? 0) > 0)),
2341
3300
  completedChecklists: keysOf(completedChecklists),
2342
3301
  audienceIds: this.state.audienceIds,
2343
- environment: this.options.environment,
3302
+ environment: this.environment,
2344
3303
  platform: "web",
2345
3304
  ...this.options.appVersion ? { appVersion: this.options.appVersion } : {}
2346
3305
  });
@@ -2503,17 +3462,17 @@ var GuidelyClient = class _GuidelyClient {
2503
3462
  this.evaluate("visible", { guideId: guide.id });
2504
3463
  };
2505
3464
  if (typeof IntersectionObserver === "undefined") return fire();
2506
- io = new IntersectionObserver((entries) => {
2507
- if (entries.some((e) => e.isIntersecting)) fire();
3465
+ io = new IntersectionObserver((entries2) => {
3466
+ if (entries2.some((e) => e.isIntersecting)) fire();
2508
3467
  });
2509
3468
  io.observe(el);
2510
3469
  }).catch(() => stop());
2511
3470
  }
2512
- setTimer(fn, ms) {
3471
+ setTimer(fn, ms2) {
2513
3472
  const t = setTimeout(() => {
2514
3473
  this.timers.delete(t);
2515
3474
  if (!this.destroyed) fn();
2516
- }, ms);
3475
+ }, ms2);
2517
3476
  this.timers.add(t);
2518
3477
  }
2519
3478
  /** Decides whether a guide can show now, otherwise queues it. */
@@ -2654,6 +3613,7 @@ var GuidelyClient = class _GuidelyClient {
2654
3613
  if (action === "start_guide" && value) this.setTimer(() => this.startGuide(value), 0);
2655
3614
  else if (action === "event" && value) this.track(value);
2656
3615
  else if (action === "open_resource_center") this.openResourceCenter();
3616
+ else if (action === "open_article" && value) this.openArticle(value);
2657
3617
  },
2658
3618
  onEnd: (reason) => {
2659
3619
  if (exclusive) {
@@ -3064,6 +4024,19 @@ var GuidelyClient = class _GuidelyClient {
3064
4024
  if (!this.resourceCenter) this.renderPersistentWidgets();
3065
4025
  this.resourceCenter?.open();
3066
4026
  }
4027
+ /**
4028
+ * Opens one help article: in Scout's Help tab when the messenger is on, otherwise in the resource
4029
+ * center. Help-article links in guides (`#guidely-article:<id>`) call this.
4030
+ */
4031
+ openArticle(articleId) {
4032
+ if (this.config?.messenger?.enabled) {
4033
+ void this.loadMessenger().then((m) => m?.openArticle(articleId));
4034
+ return;
4035
+ }
4036
+ if (!this.resourceCenter) this.renderPersistentWidgets();
4037
+ if (this.resourceCenter) this.resourceCenter.openArticle(articleId);
4038
+ else this.log("openArticle: the resource center is not enabled or published for this environment");
4039
+ }
3067
4040
  closeResourceCenter() {
3068
4041
  if (this.messenger) return this.closeMessenger();
3069
4042
  this.resourceCenter?.close();
@@ -3104,6 +4077,49 @@ var GuidelyClient = class _GuidelyClient {
3104
4077
  if (!this.config?.messenger?.enabled || !this.config.messenger.ideas) return;
3105
4078
  void this.loadMessenger().then((m) => m?.openIdeas(requestId));
3106
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
+ }
3107
4123
  /** Opens a new messenger conversation, optionally pre-filling the message. */
3108
4124
  showNewMessage(text) {
3109
4125
  if (!this.config?.messenger?.enabled) return;
@@ -3134,12 +4150,12 @@ var GuidelyClient = class _GuidelyClient {
3134
4150
  const account = this.user?.account;
3135
4151
  return {
3136
4152
  url: this.pageHref(),
3137
- path: pathOf2(this.pageHref()),
4153
+ path: pathOf3(this.pageHref()),
3138
4154
  deviceType: device.type,
3139
4155
  browser: device.browser,
3140
4156
  os: device.os,
3141
4157
  locale: navigator.language || void 0,
3142
- environment: this.options.environment,
4158
+ environment: this.environment,
3143
4159
  platform: "web",
3144
4160
  ...this.options.appVersion ? { appVersion: this.options.appVersion } : {},
3145
4161
  ...this.user?.traits ?? {},
@@ -3316,7 +4332,7 @@ var current2 = null;
3316
4332
  function webOnly(config) {
3317
4333
  return config.guides.every((g) => supportsPlatform(g.config, "web")) ? config : { ...config, guides: config.guides.filter((g) => supportsPlatform(g.config, "web")) };
3318
4334
  }
3319
- function pathOf2(url) {
4335
+ function pathOf3(url) {
3320
4336
  try {
3321
4337
  return new URL(url, "http://localhost").pathname;
3322
4338
  } catch {
@@ -3367,6 +4383,7 @@ export {
3367
4383
  createStorage,
3368
4384
  index_default as default,
3369
4385
  detectDevice,
4386
+ environmentFromKey,
3370
4387
  isAppUrl,
3371
4388
  isExclusiveGuide,
3372
4389
  isFrequencyEligible,