@guidelytech/sdk-core 0.10.0 → 0.12.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/{capture-DVJyQ2JZ.d.cts → capture-4EgpjK6Z.d.cts} +2 -2
- package/dist/{capture-369s4YYT.d.ts → capture-C-4R56G0.d.ts} +2 -2
- package/dist/capture-CgN69Lxq.d.cts +100 -0
- package/dist/capture-Csz721-A.d.ts +100 -0
- package/dist/capture.cjs +5 -1
- package/dist/capture.cjs.map +1 -1
- package/dist/capture.d.cts +3 -3
- package/dist/capture.d.ts +3 -3
- package/dist/capture.js +1 -1
- package/dist/{chunk-GDJB466J.js → chunk-2E2YUQAG.js} +29 -6
- package/dist/chunk-2E2YUQAG.js.map +1 -0
- package/dist/{chunk-C4WWSF67.js → chunk-JUSM7J5N.js} +3 -1
- package/dist/{chunk-C4WWSF67.js.map → chunk-JUSM7J5N.js.map} +1 -1
- package/dist/chunk-XML74K2P.js +378 -0
- package/dist/chunk-XML74K2P.js.map +1 -0
- package/dist/chunk-ZAF2KRJA.js +334 -0
- package/dist/chunk-ZAF2KRJA.js.map +1 -0
- package/dist/headless.cjs +35 -5
- package/dist/headless.cjs.map +1 -1
- package/dist/headless.d.cts +6 -5
- package/dist/headless.d.ts +6 -5
- package/dist/headless.js +9 -3
- package/dist/health-DjZ2gwYa.d.cts +92 -0
- package/dist/health-DjZ2gwYa.d.ts +92 -0
- package/dist/{health-DFHUstWd.d.cts → health-sbP9JwTE.d.cts} +2 -0
- package/dist/{health-DFHUstWd.d.ts → health-sbP9JwTE.d.ts} +2 -0
- package/dist/{http-DJd8jRhK.d.ts → http-BXys2d-j.d.ts} +1 -1
- package/dist/{http-D0dCcpLN.d.cts → http-Cb6KYYFv.d.cts} +1 -1
- package/dist/http-D047Ngxk.d.ts +61 -0
- package/dist/http-zOI8d2c2.d.cts +61 -0
- package/dist/index.cjs +1638 -1047
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -9
- package/dist/index.d.ts +77 -9
- package/dist/index.js +763 -202
- package/dist/index.js.map +1 -1
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts +1 -1
- package/dist/recorder.d.ts +1 -1
- package/dist/recorder.js +1 -1
- package/dist/{types-B5ecgHUV.d.cts → types-BDlebXrm.d.ts} +59 -5
- package/dist/types-Bkw8ErmO.d.cts +314 -0
- package/dist/{types-BtPovimB.d.ts → types-Dy_uPu6S.d.ts} +59 -5
- package/dist/types-Mtlmlj5C.d.cts +314 -0
- package/package.json +4 -4
- package/dist/chunk-GDJB466J.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -33,165 +33,370 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
33
33
|
var index_exports = {};
|
|
34
34
|
__export(index_exports, {
|
|
35
35
|
AUTHORING_PARAM: () => AUTHORING_PARAM,
|
|
36
|
-
CAPTURE_PARAM: () =>
|
|
36
|
+
CAPTURE_PARAM: () => import_runtime9.CAPTURE_PARAM,
|
|
37
37
|
DEFAULT_API_URL: () => DEFAULT_API_URL,
|
|
38
38
|
EventQueue: () => EventQueue,
|
|
39
39
|
Guidely: () => Guidely,
|
|
40
40
|
GuidelyClient: () => GuidelyClient,
|
|
41
41
|
HEARTBEAT_MS: () => HEARTBEAT_MS,
|
|
42
42
|
KEYS: () => KEYS,
|
|
43
|
+
PROTOTYPE_CLOSE_MESSAGE: () => PROTOTYPE_CLOSE_MESSAGE,
|
|
43
44
|
SDK_NAME: () => SDK_NAME,
|
|
44
45
|
SDK_VERSION: () => SDK_VERSION,
|
|
45
46
|
areMutuallyExclusive: () => areMutuallyExclusive,
|
|
46
47
|
buildContext: () => buildContext,
|
|
47
48
|
captureSnapshot: () => captureSnapshot,
|
|
49
|
+
countsTowardThrottle: () => countsTowardThrottle,
|
|
48
50
|
createGuidely: () => createGuidely,
|
|
49
51
|
createStorage: () => createStorage,
|
|
50
52
|
default: () => index_default,
|
|
51
53
|
detectDevice: () => detectDevice,
|
|
54
|
+
isAppUrl: () => isAppUrl,
|
|
52
55
|
isExclusiveGuide: () => isExclusiveGuide,
|
|
53
56
|
isFrequencyEligible: () => isFrequencyEligible,
|
|
57
|
+
isThrottleEligible: () => isThrottleEligible,
|
|
54
58
|
isWithinSchedule: () => isWithinSchedule,
|
|
59
|
+
matchPrototypeTestUrl: () => matchPrototypeTestUrl,
|
|
60
|
+
normalizeAppUrl: () => normalizeAppUrl,
|
|
61
|
+
pruneLaunches: () => pruneLaunches,
|
|
55
62
|
watchNavigation: () => watchNavigation
|
|
56
63
|
});
|
|
57
64
|
module.exports = __toCommonJS(index_exports);
|
|
58
65
|
|
|
59
66
|
// src/client.ts
|
|
60
|
-
var
|
|
61
|
-
var import_guide_runtime4 = require("@guidelytech/guide-runtime");
|
|
67
|
+
var import_runtime18 = require("@guidelytech/shared/runtime");
|
|
62
68
|
var import_guide_runtime5 = require("@guidelytech/guide-runtime");
|
|
69
|
+
var import_guide_runtime6 = require("@guidelytech/guide-runtime");
|
|
63
70
|
var import_flags = require("@guidelytech/flags");
|
|
64
71
|
|
|
65
|
-
// src/
|
|
66
|
-
var
|
|
72
|
+
// src/agent-turns.ts
|
|
73
|
+
var import_runtime4 = require("@guidelytech/shared/runtime");
|
|
67
74
|
|
|
68
|
-
// src/
|
|
69
|
-
var
|
|
70
|
-
var
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
// src/health.ts
|
|
76
|
+
var MAX_RETRY_AFTER_MS = 60 * 60 * 1e3;
|
|
77
|
+
var OFFLINE_WAIT_MS = 3e4;
|
|
78
|
+
var ServerHealth = class {
|
|
79
|
+
constructor(opts = {}) {
|
|
80
|
+
__publicField(this, "failures", 0);
|
|
81
|
+
__publicField(this, "openUntil", 0);
|
|
82
|
+
__publicField(this, "lastOkAt", 0);
|
|
83
|
+
__publicField(this, "listeners", /* @__PURE__ */ new Set());
|
|
84
|
+
__publicField(this, "threshold");
|
|
85
|
+
__publicField(this, "baseMs");
|
|
86
|
+
__publicField(this, "maxMs");
|
|
87
|
+
__publicField(this, "now");
|
|
88
|
+
__publicField(this, "random");
|
|
89
|
+
this.threshold = opts.threshold ?? 3;
|
|
90
|
+
this.baseMs = opts.baseMs ?? 5e3;
|
|
91
|
+
this.maxMs = opts.maxMs ?? 5 * 60 * 1e3;
|
|
92
|
+
this.now = opts.now ?? Date.now;
|
|
93
|
+
this.random = opts.random ?? Math.random;
|
|
77
94
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
95
|
+
/** The server answered (2xx, 3xx, or a 4xx other than 429): it is up. */
|
|
96
|
+
success() {
|
|
97
|
+
const wasDown = this.failures > 0 || this.openUntil > 0;
|
|
98
|
+
this.failures = 0;
|
|
99
|
+
this.openUntil = 0;
|
|
100
|
+
this.lastOkAt = this.now();
|
|
101
|
+
if (wasDown) this.notify();
|
|
83
102
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const css = rulesText(origStyles[i]?.sheet);
|
|
92
|
-
if (css !== null && css.length > (el.textContent ?? "").length) el.textContent = css;
|
|
93
|
-
});
|
|
94
|
-
const origFrames = doc.documentElement.querySelectorAll("iframe, canvas, video, embed, object");
|
|
95
|
-
clone.querySelectorAll("iframe, canvas, video, embed, object").forEach((el, i) => {
|
|
96
|
-
const r = origFrames[i]?.getBoundingClientRect();
|
|
97
|
-
const box = doc.createElement("div");
|
|
98
|
-
box.setAttribute("style", `${el.getAttribute("style") ?? ""};width:${r?.width ?? 0}px;height:${r?.height ?? 0}px;background:#e2e8f0;border-radius:4px`);
|
|
99
|
-
if (el.getAttribute("class")) box.setAttribute("class", el.getAttribute("class"));
|
|
100
|
-
el.replaceWith(box);
|
|
101
|
-
});
|
|
102
|
-
clone.querySelectorAll(STRIP).forEach((el) => el.remove());
|
|
103
|
-
for (const sel of maskSelectors) {
|
|
104
|
-
try {
|
|
105
|
-
clone.querySelectorAll(sel).forEach(maskText);
|
|
106
|
-
} catch {
|
|
103
|
+
/** The request failed or the server is overloaded. `retryAfterMs` comes from a `Retry-After` header. */
|
|
104
|
+
failure(retryAfterMs) {
|
|
105
|
+
this.failures += 1;
|
|
106
|
+
const now = this.now();
|
|
107
|
+
if (retryAfterMs != null && retryAfterMs > 0) {
|
|
108
|
+
this.openUntil = Math.max(this.openUntil, now + Math.min(retryAfterMs, MAX_RETRY_AFTER_MS));
|
|
109
|
+
return;
|
|
107
110
|
}
|
|
111
|
+
if (this.failures < this.threshold) return;
|
|
112
|
+
const cooldown = Math.min(this.maxMs, this.baseMs * 2 ** (this.failures - this.threshold));
|
|
113
|
+
this.openUntil = Math.max(this.openUntil, now + cooldown * (0.8 + this.random() * 0.4));
|
|
108
114
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
if (el instanceof HTMLInputElement) {
|
|
115
|
-
el.removeAttribute("value");
|
|
116
|
-
el.removeAttribute("checked");
|
|
117
|
-
} else if (el instanceof HTMLTextAreaElement) {
|
|
118
|
-
el.textContent = "";
|
|
119
|
-
}
|
|
120
|
-
el.removeAttribute("autofocus");
|
|
121
|
-
});
|
|
122
|
-
const adopted = Array.from(doc.adoptedStyleSheets ?? []).map(rulesText).filter(Boolean).join("\n");
|
|
123
|
-
let head = clone.querySelector("head");
|
|
124
|
-
if (!head) {
|
|
125
|
-
head = doc.createElement("head");
|
|
126
|
-
clone.prepend(head);
|
|
115
|
+
/** Milliseconds background traffic should wait before trying again (0: send now). */
|
|
116
|
+
waitMs() {
|
|
117
|
+
if (typeof navigator !== "undefined" && navigator.onLine === false) return OFFLINE_WAIT_MS;
|
|
118
|
+
return Math.max(0, this.openUntil - this.now());
|
|
127
119
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
style.textContent = adopted;
|
|
131
|
-
head.append(style);
|
|
120
|
+
canSend() {
|
|
121
|
+
return this.waitMs() === 0;
|
|
132
122
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
123
|
+
/**
|
|
124
|
+
* The last request got an answer. Unload beacons can't report back, so what they carry is only
|
|
125
|
+
* forgotten while this holds; otherwise it stays queued and is sent again (the server drops duplicates).
|
|
126
|
+
*/
|
|
127
|
+
get reachable() {
|
|
128
|
+
return this.failures === 0 && this.lastOkAt > 0;
|
|
129
|
+
}
|
|
130
|
+
/** The browser came back online: end the cooldown and let queued traffic try straight away. */
|
|
131
|
+
online() {
|
|
132
|
+
this.openUntil = 0;
|
|
133
|
+
this.notify();
|
|
134
|
+
}
|
|
135
|
+
/** Called when the server answers again after failures, or the browser comes back online. */
|
|
136
|
+
onRecover(cb) {
|
|
137
|
+
this.listeners.add(cb);
|
|
138
|
+
return () => this.listeners.delete(cb);
|
|
139
|
+
}
|
|
140
|
+
notify() {
|
|
141
|
+
for (const cb of Array.from(this.listeners)) {
|
|
142
|
+
try {
|
|
143
|
+
cb();
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
150
|
+
if (!value) return null;
|
|
151
|
+
const trimmed = value.trim();
|
|
152
|
+
if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
|
|
153
|
+
const at = Date.parse(trimmed);
|
|
154
|
+
return Number.isNaN(at) ? null : Math.max(0, at - now);
|
|
142
155
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
function memoryStore() {
|
|
146
|
-
const map = /* @__PURE__ */ new Map();
|
|
147
|
-
return { get: (k) => map.get(k) ?? null, set: (k, v) => void map.set(k, v), remove: (k) => void map.delete(k) };
|
|
156
|
+
function isServerFailure(status) {
|
|
157
|
+
return status === 429 || status >= 500;
|
|
148
158
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
159
|
+
|
|
160
|
+
// src/http.ts
|
|
161
|
+
var STREAM_RESPONSE_TIMEOUT_MS = 3e4;
|
|
162
|
+
var STREAM_IDLE_TIMEOUT_MS = 6e4;
|
|
163
|
+
var Api = class {
|
|
164
|
+
constructor(baseUrl, key, fetchImpl, health) {
|
|
165
|
+
__publicField(this, "baseUrl", baseUrl);
|
|
166
|
+
__publicField(this, "key", key);
|
|
167
|
+
__publicField(this, "fetchImpl", fetchImpl);
|
|
168
|
+
/** Reachability of the API, fed by every answer (see {@link ServerHealth}). */
|
|
169
|
+
__publicField(this, "health");
|
|
170
|
+
this.health = health ?? new ServerHealth();
|
|
171
|
+
}
|
|
172
|
+
/** Feeds an answer (or a failed request, `null`) into {@link health}. */
|
|
173
|
+
observe(res) {
|
|
174
|
+
if (!res) this.health.failure();
|
|
175
|
+
else if (isServerFailure(res.status)) this.health.failure(parseRetryAfter(res.headers?.get?.("retry-after")));
|
|
176
|
+
else this.health.success();
|
|
177
|
+
}
|
|
178
|
+
url(path, params = {}) {
|
|
179
|
+
const query = Object.entries({ key: this.key, ...params }).filter((e) => e[1] != null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
180
|
+
return `${this.baseUrl.replace(/\/$/, "")}/api/v1/sdk${path}?${query}`;
|
|
181
|
+
}
|
|
182
|
+
get fetcher() {
|
|
183
|
+
if (this.fetchImpl) return this.fetchImpl;
|
|
184
|
+
return typeof fetch === "function" ? fetch.bind(globalThis) : null;
|
|
185
|
+
}
|
|
186
|
+
async request(method, path, options = {}) {
|
|
187
|
+
const f = this.fetcher;
|
|
188
|
+
if (!f) throw new Error("fetch is not available");
|
|
189
|
+
const headers = { ...options.headers, "X-Guidely-Key": this.key, Accept: "application/json" };
|
|
190
|
+
const form = typeof FormData !== "undefined" && options.body instanceof FormData;
|
|
191
|
+
if (options.body !== void 0 && !form) headers["Content-Type"] = "application/json";
|
|
192
|
+
if (options.etag) headers["If-None-Match"] = options.etag;
|
|
193
|
+
const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
194
|
+
const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 1e4) : null;
|
|
195
|
+
try {
|
|
196
|
+
let res;
|
|
197
|
+
try {
|
|
198
|
+
res = await f(this.url(path, options.params), {
|
|
199
|
+
method,
|
|
200
|
+
headers,
|
|
201
|
+
body: options.body === void 0 ? void 0 : form ? options.body : JSON.stringify(options.body),
|
|
202
|
+
keepalive: options.keepalive,
|
|
203
|
+
signal: ac?.signal,
|
|
204
|
+
credentials: "omit",
|
|
205
|
+
mode: "cors"
|
|
206
|
+
});
|
|
207
|
+
} catch (err) {
|
|
208
|
+
this.observe(null);
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
211
|
+
this.observe(res);
|
|
212
|
+
let data = null;
|
|
213
|
+
if (res.status !== 304 && res.status !== 204) {
|
|
172
214
|
try {
|
|
173
|
-
|
|
215
|
+
data = await res.json();
|
|
174
216
|
} catch {
|
|
217
|
+
data = null;
|
|
175
218
|
}
|
|
176
219
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
220
|
+
return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.("etag") ?? null };
|
|
221
|
+
} finally {
|
|
222
|
+
if (timer) clearTimeout(timer);
|
|
223
|
+
}
|
|
180
224
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (kind === "memory") return memoryStore();
|
|
184
|
-
return webStore(kind === "local" ? "localStorage" : "sessionStorage") ?? memoryStore();
|
|
185
|
-
}
|
|
186
|
-
function readJson(store, key, fallback) {
|
|
187
|
-
const raw = store.get(key);
|
|
188
|
-
if (raw == null) return fallback;
|
|
189
|
-
try {
|
|
190
|
-
return JSON.parse(raw);
|
|
191
|
-
} catch {
|
|
192
|
-
return fallback;
|
|
225
|
+
get(path, options) {
|
|
226
|
+
return this.request("GET", path, options);
|
|
193
227
|
}
|
|
194
|
-
|
|
228
|
+
post(path, body, options) {
|
|
229
|
+
return this.request("POST", path, { body, ...options });
|
|
230
|
+
}
|
|
231
|
+
/** Multipart POST (file uploads). No Content-Type header: the browser adds it with the boundary. */
|
|
232
|
+
postForm(path, form, options) {
|
|
233
|
+
return this.request("POST", path, { body: form, ...options });
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
|
|
237
|
+
* returned instead. Scout replies can take a while, so there is no overall timeout: the response
|
|
238
|
+
* must start within 30 s and then never go quiet for more than 60 s (the server sends keepalives),
|
|
239
|
+
* otherwise the request is aborted and rejects.
|
|
240
|
+
*/
|
|
241
|
+
async stream(path, body, onEvent, headers = {}) {
|
|
242
|
+
const f = this.fetcher;
|
|
243
|
+
if (!f) throw new Error("fetch is not available");
|
|
244
|
+
const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
245
|
+
let timer = null;
|
|
246
|
+
const arm = (ms) => {
|
|
247
|
+
if (!ac) return;
|
|
248
|
+
if (timer) clearTimeout(timer);
|
|
249
|
+
timer = setTimeout(() => ac.abort(), ms);
|
|
250
|
+
};
|
|
251
|
+
arm(STREAM_RESPONSE_TIMEOUT_MS);
|
|
252
|
+
try {
|
|
253
|
+
let res;
|
|
254
|
+
try {
|
|
255
|
+
res = await f(this.url(path), {
|
|
256
|
+
method: "POST",
|
|
257
|
+
headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
|
|
258
|
+
body: JSON.stringify(body),
|
|
259
|
+
signal: ac?.signal,
|
|
260
|
+
credentials: "omit",
|
|
261
|
+
mode: "cors"
|
|
262
|
+
});
|
|
263
|
+
} catch (err) {
|
|
264
|
+
this.observe(null);
|
|
265
|
+
throw err;
|
|
266
|
+
}
|
|
267
|
+
this.observe(res);
|
|
268
|
+
const type = res.headers?.get?.("content-type") ?? "";
|
|
269
|
+
if (!type.includes("text/event-stream") || !res.body) {
|
|
270
|
+
let data = null;
|
|
271
|
+
try {
|
|
272
|
+
data = await res.json();
|
|
273
|
+
} catch {
|
|
274
|
+
data = null;
|
|
275
|
+
}
|
|
276
|
+
return { status: res.status, ok: res.ok, data, etag: null };
|
|
277
|
+
}
|
|
278
|
+
arm(STREAM_IDLE_TIMEOUT_MS);
|
|
279
|
+
await readEventStream(res.body, onEvent, () => arm(STREAM_IDLE_TIMEOUT_MS));
|
|
280
|
+
return { status: res.status, ok: res.ok, data: null, etag: null };
|
|
281
|
+
} finally {
|
|
282
|
+
if (timer) clearTimeout(timer);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */
|
|
286
|
+
beacon(path, body) {
|
|
287
|
+
try {
|
|
288
|
+
if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") return false;
|
|
289
|
+
const blob = new Blob([JSON.stringify(body)], { type: "text/plain;charset=UTF-8" });
|
|
290
|
+
return navigator.sendBeacon(this.url(path), blob);
|
|
291
|
+
} catch {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
297
|
+
function backoffMs(attempt, baseMs = 1e3, maxMs = 3e4) {
|
|
298
|
+
return Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt));
|
|
299
|
+
}
|
|
300
|
+
async function withRetry(send, attempts = 3, baseMs = 500) {
|
|
301
|
+
for (let i = 0; i < attempts; i++) {
|
|
302
|
+
try {
|
|
303
|
+
const res = await send();
|
|
304
|
+
if (res.ok || res.status >= 400 && res.status < 500 && res.status !== 429) return res;
|
|
305
|
+
} catch {
|
|
306
|
+
}
|
|
307
|
+
if (i < attempts - 1) await sleep(backoffMs(i, baseMs, 8e3));
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
|
|
312
|
+
return withRetry(() => api.post(path, body), attempts, baseMs);
|
|
313
|
+
}
|
|
314
|
+
async function readEventStream(body, onEvent, onChunk) {
|
|
315
|
+
const reader = body.getReader();
|
|
316
|
+
const decoder = new TextDecoder();
|
|
317
|
+
let buffer = "";
|
|
318
|
+
const dispatch = (raw) => {
|
|
319
|
+
let event = "message";
|
|
320
|
+
const data = [];
|
|
321
|
+
for (const line of raw.split("\n")) {
|
|
322
|
+
if (!line || line.startsWith(":")) continue;
|
|
323
|
+
const i = line.indexOf(":");
|
|
324
|
+
const field = i < 0 ? line : line.slice(0, i);
|
|
325
|
+
const value = i < 0 ? "" : line.slice(i + 1).replace(/^ /, "");
|
|
326
|
+
if (field === "event") event = value;
|
|
327
|
+
else if (field === "data") data.push(value);
|
|
328
|
+
}
|
|
329
|
+
if (!data.length) return;
|
|
330
|
+
try {
|
|
331
|
+
onEvent(event, JSON.parse(data.join("\n")));
|
|
332
|
+
} catch {
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
for (; ; ) {
|
|
336
|
+
const { done, value } = await reader.read();
|
|
337
|
+
if (value) onChunk?.();
|
|
338
|
+
if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
|
|
339
|
+
let sep;
|
|
340
|
+
while ((sep = buffer.indexOf("\n\n")) >= 0) {
|
|
341
|
+
dispatch(buffer.slice(0, sep));
|
|
342
|
+
buffer = buffer.slice(sep + 2);
|
|
343
|
+
}
|
|
344
|
+
if (done) break;
|
|
345
|
+
}
|
|
346
|
+
if (buffer.trim()) dispatch(buffer);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/storage.ts
|
|
350
|
+
function memoryStore() {
|
|
351
|
+
const map = /* @__PURE__ */ new Map();
|
|
352
|
+
return { get: (k) => map.get(k) ?? null, set: (k, v) => void map.set(k, v), remove: (k) => void map.delete(k) };
|
|
353
|
+
}
|
|
354
|
+
function webStore(area) {
|
|
355
|
+
try {
|
|
356
|
+
if (typeof window === "undefined") return null;
|
|
357
|
+
const s = window[area];
|
|
358
|
+
if (!s) return null;
|
|
359
|
+
const probe = "__guidely_probe__";
|
|
360
|
+
s.setItem(probe, "1");
|
|
361
|
+
s.removeItem(probe);
|
|
362
|
+
return {
|
|
363
|
+
get: (k) => {
|
|
364
|
+
try {
|
|
365
|
+
return s.getItem(k);
|
|
366
|
+
} catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
set: (k, v) => {
|
|
371
|
+
try {
|
|
372
|
+
s.setItem(k, v);
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
remove: (k) => {
|
|
377
|
+
try {
|
|
378
|
+
s.removeItem(k);
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
} catch {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function createStorage(kind = "local") {
|
|
388
|
+
if (kind === "memory") return memoryStore();
|
|
389
|
+
return webStore(kind === "local" ? "localStorage" : "sessionStorage") ?? memoryStore();
|
|
390
|
+
}
|
|
391
|
+
function readJson(store, key, fallback) {
|
|
392
|
+
const raw = store.get(key);
|
|
393
|
+
if (raw == null) return fallback;
|
|
394
|
+
try {
|
|
395
|
+
return JSON.parse(raw);
|
|
396
|
+
} catch {
|
|
397
|
+
return fallback;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
195
400
|
function writeJson(store, key, value) {
|
|
196
401
|
try {
|
|
197
402
|
store.set(key, JSON.stringify(value));
|
|
@@ -208,6 +413,8 @@ var KEYS = {
|
|
|
208
413
|
queue: (key) => `guidely:queue:${key}`,
|
|
209
414
|
/** Survey answers and guide/checklist progress not yet accepted by the server. */
|
|
210
415
|
outbox: (key) => `guidely:outbox:${key}`,
|
|
416
|
+
/** Agent Analytics turns not yet accepted by the server. */
|
|
417
|
+
agentOutbox: (key) => `guidely:agents:${key}`,
|
|
211
418
|
session: "guidely:session",
|
|
212
419
|
active: "guidely:active",
|
|
213
420
|
authoring: "guidely:authoring",
|
|
@@ -217,285 +424,261 @@ var KEYS = {
|
|
|
217
424
|
/** Messenger conversations this browser can read: [{ id, token }]. */
|
|
218
425
|
conversations: (key) => `guidely:conversations:${key}`,
|
|
219
426
|
/** Announcement ids opened in the messenger's News tab. */
|
|
220
|
-
newsSeen: (key) => `guidely:news:${key}
|
|
427
|
+
newsSeen: (key) => `guidely:news:${key}`,
|
|
428
|
+
/** Where the visitor dragged the messenger launcher ({ side, y }). */
|
|
429
|
+
launcher: (key) => `guidely:launcher:${key}`
|
|
221
430
|
};
|
|
222
431
|
|
|
223
|
-
// src/
|
|
224
|
-
|
|
225
|
-
const m = await import("@guidelytech/shared");
|
|
226
|
-
return { StepSchema: m.StepSchema, GuideConfigSchema: m.GuideConfigSchema };
|
|
227
|
-
}
|
|
228
|
-
var AUTHORING_PARAM = "guidely_authoring";
|
|
229
|
-
var HEARTBEAT_MS = 15e3;
|
|
230
|
-
var SNAPSHOT_DEBOUNCE_MS = 800;
|
|
231
|
-
var SNAPSHOT_MAX_WAIT_MS = 4e3;
|
|
232
|
-
var AuthoringBridge = class {
|
|
432
|
+
// src/outbox.ts
|
|
433
|
+
var Outbox = class {
|
|
233
434
|
constructor(opts) {
|
|
234
435
|
__publicField(this, "opts", opts);
|
|
235
|
-
__publicField(this, "
|
|
236
|
-
__publicField(this, "
|
|
237
|
-
__publicField(this, "
|
|
238
|
-
__publicField(this, "
|
|
239
|
-
__publicField(this, "
|
|
240
|
-
__publicField(this, "
|
|
241
|
-
__publicField(this, "
|
|
242
|
-
__publicField(this, "
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
436
|
+
__publicField(this, "items");
|
|
437
|
+
__publicField(this, "sending", false);
|
|
438
|
+
__publicField(this, "attempt", 0);
|
|
439
|
+
__publicField(this, "timer", null);
|
|
440
|
+
__publicField(this, "destroyed", false);
|
|
441
|
+
__publicField(this, "offRecover");
|
|
442
|
+
__publicField(this, "maxItems");
|
|
443
|
+
__publicField(this, "maxAgeMs");
|
|
444
|
+
this.maxItems = opts.maxItems ?? 50;
|
|
445
|
+
this.maxAgeMs = opts.maxAgeMs ?? 7 * 24 * 60 * 60 * 1e3;
|
|
446
|
+
const stored = readJson(opts.store, opts.storageKey, []);
|
|
447
|
+
const cutoff = Date.now() - this.maxAgeMs;
|
|
448
|
+
this.items = Array.isArray(stored) ? stored.filter((i) => i && typeof i.path === "string" && typeof i.key === "string" && i.createdAt > cutoff).slice(-this.maxItems) : [];
|
|
449
|
+
this.offRecover = opts.health?.onRecover(() => {
|
|
450
|
+
this.attempt = 0;
|
|
451
|
+
this.retryIn(0);
|
|
452
|
+
}) ?? null;
|
|
453
|
+
if (this.items.length) this.retryIn(0);
|
|
246
454
|
}
|
|
247
|
-
|
|
248
|
-
return this.
|
|
455
|
+
get size() {
|
|
456
|
+
return this.items.length;
|
|
249
457
|
}
|
|
250
|
-
/**
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
url.searchParams.delete(AUTHORING_PARAM);
|
|
259
|
-
window.history.replaceState(window.history.state, "", url.toString());
|
|
260
|
-
}
|
|
261
|
-
} catch {
|
|
262
|
-
token = null;
|
|
263
|
-
}
|
|
264
|
-
if (!token) {
|
|
265
|
-
const marker = readJson(this.opts.sessionStore, KEYS.authoring, null);
|
|
266
|
-
token = marker?.token ?? null;
|
|
267
|
-
}
|
|
268
|
-
if (!token) return false;
|
|
269
|
-
this.token = token;
|
|
270
|
-
writeJson(this.opts.sessionStore, KEYS.authoring, { token });
|
|
271
|
-
void this.start();
|
|
272
|
-
return true;
|
|
458
|
+
/** Queues `POST path` and sends it now (or as soon as the API is reachable). */
|
|
459
|
+
add(key, path, body) {
|
|
460
|
+
if (this.destroyed) return;
|
|
461
|
+
this.items = this.items.filter((i) => i.key !== key);
|
|
462
|
+
this.items.push({ key, path, body, createdAt: Date.now() });
|
|
463
|
+
if (this.items.length > this.maxItems) this.items.splice(0, this.items.length - this.maxItems);
|
|
464
|
+
this.persist();
|
|
465
|
+
void this.flush();
|
|
273
466
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
try {
|
|
277
|
-
const res = await this.opts.api.get("/authoring/session", { params: { token: this.token } });
|
|
278
|
-
if (!res.ok || !res.data) {
|
|
279
|
-
this.opts.sessionStore.remove(KEYS.authoring);
|
|
280
|
-
this.opts.onError(new Error(`[Guidely] authoring session invalid or expired (${res.status})`));
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
this.info = res.data;
|
|
284
|
-
if (Date.parse(this.info.expiresAt) < Date.now()) {
|
|
285
|
-
this.opts.sessionStore.remove(KEYS.authoring);
|
|
286
|
-
this.opts.onError(new Error("[Guidely] authoring session expired"));
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
this.active = true;
|
|
290
|
-
this.opts.onActive(true);
|
|
291
|
-
this.session = (0, import_guide_runtime.startAuthoring)({
|
|
292
|
-
dashboardOrigin: this.info.dashboardOrigin,
|
|
293
|
-
onPick: (pick) => this.sendPick(pick),
|
|
294
|
-
onExit: () => this.stop()
|
|
295
|
-
});
|
|
296
|
-
this.heartbeat = setInterval(() => {
|
|
297
|
-
void this.opts.api.post("/authoring/session/heartbeat", {}, { params: { token: this.token ?? void 0 } }).catch(() => {
|
|
298
|
-
});
|
|
299
|
-
}, HEARTBEAT_MS);
|
|
300
|
-
this.post({ type: "guidely:authoring-ready", sessionId: this.info.id, url: window.location.href });
|
|
301
|
-
this.watchPage();
|
|
302
|
-
} catch (err) {
|
|
303
|
-
this.opts.onError(err);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
masked(el) {
|
|
307
|
-
return this.opts.maskSelectors.some((sel) => {
|
|
308
|
-
try {
|
|
309
|
-
return !!(0, import_guide_runtime.closestComposed)(el, sel);
|
|
310
|
-
} catch {
|
|
311
|
-
return false;
|
|
312
|
-
}
|
|
313
|
-
});
|
|
467
|
+
persist() {
|
|
468
|
+
writeJson(this.opts.store, this.opts.storageKey, this.items);
|
|
314
469
|
}
|
|
315
|
-
|
|
316
|
-
if (
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
470
|
+
retryIn(ms) {
|
|
471
|
+
if (this.destroyed) return;
|
|
472
|
+
if (this.timer) clearTimeout(this.timer);
|
|
473
|
+
this.timer = setTimeout(() => {
|
|
474
|
+
this.timer = null;
|
|
475
|
+
void this.flush();
|
|
476
|
+
}, ms);
|
|
322
477
|
}
|
|
323
|
-
/** Sends
|
|
324
|
-
|
|
325
|
-
this.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
this.
|
|
478
|
+
/** Sends queued requests in order, one at a time. */
|
|
479
|
+
async flush() {
|
|
480
|
+
if (this.destroyed || this.sending || !this.items.length) return;
|
|
481
|
+
const wait = this.opts.health?.waitMs() ?? 0;
|
|
482
|
+
if (wait > 0) return this.retryIn(wait);
|
|
483
|
+
this.sending = true;
|
|
484
|
+
try {
|
|
485
|
+
while (this.items.length && !this.destroyed) {
|
|
486
|
+
const item = this.items[0];
|
|
487
|
+
let retry = false;
|
|
488
|
+
try {
|
|
489
|
+
const res = await this.opts.api.post(item.path, item.body);
|
|
490
|
+
retry = !res.ok && (res.status === 429 || res.status >= 500);
|
|
491
|
+
} catch (err) {
|
|
492
|
+
retry = true;
|
|
493
|
+
this.opts.onError?.(err);
|
|
494
|
+
}
|
|
495
|
+
if (retry) {
|
|
496
|
+
this.attempt += 1;
|
|
497
|
+
this.retryIn(Math.max(backoffMs(this.attempt - 1, 1e3, 6e4), this.opts.health?.waitMs() ?? 0));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
this.attempt = 0;
|
|
501
|
+
this.items = this.items.filter((i) => i !== item);
|
|
502
|
+
this.persist();
|
|
503
|
+
}
|
|
504
|
+
} finally {
|
|
505
|
+
this.sending = false;
|
|
506
|
+
}
|
|
329
507
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
this.
|
|
508
|
+
destroy() {
|
|
509
|
+
this.destroyed = true;
|
|
510
|
+
this.offRecover?.();
|
|
511
|
+
if (this.timer) clearTimeout(this.timer);
|
|
512
|
+
this.timer = null;
|
|
335
513
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// src/agent-turns.ts
|
|
517
|
+
var PENDING_MAX = 500;
|
|
518
|
+
function toolCalls(calls) {
|
|
519
|
+
if (!Array.isArray(calls)) return void 0;
|
|
520
|
+
return calls.filter((c) => !!c && typeof c.name === "string" && !!c.name).slice(0, import_runtime4.AGENT_TOOL_CALLS_PER_TURN).map((c) => ({
|
|
521
|
+
name: c.name.slice(0, 100),
|
|
522
|
+
...c.status === "ok" || c.status === "error" ? { status: c.status } : {},
|
|
523
|
+
...typeof c.latencyMs === "number" && Number.isFinite(c.latencyMs) && c.latencyMs >= 0 ? { latencyMs: Math.round(c.latencyMs) } : {}
|
|
524
|
+
}));
|
|
525
|
+
}
|
|
526
|
+
function prepareAgentTurn(turn, identity, redact, onError) {
|
|
527
|
+
if (!turn || typeof turn !== "object" || !turn.conversationId || !turn.agentId || !turn.role) return null;
|
|
528
|
+
let out = {
|
|
529
|
+
...turn,
|
|
530
|
+
type: "agent.conversation",
|
|
531
|
+
turnId: turn.turnId || `t_${(0, import_runtime4.randomId)(20)}`,
|
|
532
|
+
occurredAt: turn.occurredAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
533
|
+
anonymousId: turn.anonymousId ?? identity.anonymousId,
|
|
534
|
+
userId: turn.userId !== void 0 ? turn.userId : identity.userId,
|
|
535
|
+
accountId: turn.accountId !== void 0 ? turn.accountId : identity.accountId,
|
|
536
|
+
sessionId: turn.sessionId ?? identity.sessionId
|
|
537
|
+
};
|
|
538
|
+
if (redact) {
|
|
340
539
|
try {
|
|
341
|
-
const
|
|
342
|
-
if (!
|
|
343
|
-
|
|
344
|
-
this.post({ type: "guidely:snapshot", sessionId: this.info.id, snapshot });
|
|
540
|
+
const r = redact({ ...out, toolCalls: out.toolCalls ? out.toolCalls.map((c) => ({ ...c })) : void 0, metadata: out.metadata ? { ...out.metadata } : void 0 });
|
|
541
|
+
if (!r) return null;
|
|
542
|
+
out = { ...r, type: "agent.conversation", conversationId: out.conversationId, agentId: out.agentId, turnId: out.turnId, role: out.role };
|
|
345
543
|
} catch (err) {
|
|
346
|
-
|
|
544
|
+
onError?.(err);
|
|
545
|
+
return null;
|
|
347
546
|
}
|
|
348
547
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
548
|
+
if (typeof out.text === "string" && out.text.length > import_runtime4.AGENT_TURN_MAX_TEXT) out.text = out.text.slice(0, import_runtime4.AGENT_TURN_MAX_TEXT);
|
|
549
|
+
if (out.text !== void 0 && typeof out.text !== "string") out.text = String(out.text);
|
|
550
|
+
const calls = toolCalls(out.toolCalls);
|
|
551
|
+
if (calls) out.toolCalls = calls;
|
|
552
|
+
else delete out.toolCalls;
|
|
553
|
+
return out;
|
|
554
|
+
}
|
|
555
|
+
var AgentTurns = class {
|
|
556
|
+
constructor(opts) {
|
|
557
|
+
__publicField(this, "opts", opts);
|
|
558
|
+
__publicField(this, "outbox");
|
|
559
|
+
__publicField(this, "pending", []);
|
|
560
|
+
this.outbox = new Outbox({ api: opts.api, store: opts.store, storageKey: opts.storageKey, health: opts.health, maxItems: 20, maxAgeMs: 24 * 60 * 60 * 1e3 });
|
|
561
|
+
}
|
|
562
|
+
get queued() {
|
|
563
|
+
return this.outbox.size + this.pending.length;
|
|
564
|
+
}
|
|
565
|
+
/** Queues turns to send now, or (`hold`) until consent is granted. */
|
|
566
|
+
add(turns, hold = false) {
|
|
567
|
+
if (!turns.length) return;
|
|
568
|
+
if (hold) {
|
|
569
|
+
this.pending.push(...turns);
|
|
570
|
+
if (this.pending.length > PENDING_MAX) this.pending.splice(0, this.pending.length - PENDING_MAX);
|
|
571
|
+
return;
|
|
354
572
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
this.preview = null;
|
|
359
|
-
this.previewGuide?.destroy();
|
|
360
|
-
this.previewGuide = null;
|
|
361
|
-
this.session?.setPicking(true);
|
|
362
|
-
}
|
|
363
|
-
/** Handles `message` events; ignores anything not from `dashboardOrigin`. */
|
|
364
|
-
async handleMessage(e) {
|
|
365
|
-
if (!this.active || !this.info) return;
|
|
366
|
-
if (e.origin !== this.info.dashboardOrigin) return;
|
|
367
|
-
const msg = e.data;
|
|
368
|
-
if (!msg || typeof msg !== "object" || typeof msg.type !== "string") return;
|
|
369
|
-
try {
|
|
370
|
-
switch (msg.type) {
|
|
371
|
-
case "guidely:preview-step": {
|
|
372
|
-
this.stopPreview();
|
|
373
|
-
const { StepSchema, GuideConfigSchema } = await loadSchemas();
|
|
374
|
-
const step = StepSchema.parse(msg.step);
|
|
375
|
-
const appearance = msg.appearance ? GuideConfigSchema.shape.appearance.parse(msg.appearance) : void 0;
|
|
376
|
-
this.session?.setPicking(false);
|
|
377
|
-
this.session?.setStatus(`Previewing step \u201C${step.title || step.id}\u201D \u2014 close it to continue picking`);
|
|
378
|
-
this.preview = (0, import_guide_runtime.previewStep)(step, { ...this.opts.theme(), ...msg.theme || {} }, {
|
|
379
|
-
appearance,
|
|
380
|
-
onEnd: () => {
|
|
381
|
-
this.preview = null;
|
|
382
|
-
this.session?.setPicking(true);
|
|
383
|
-
this.session?.setStatus("Guidely authoring \u2014 hover an element and click to select \xB7 Esc to cancel");
|
|
384
|
-
this.post({ type: "guidely:preview-ended", sessionId: this.info.id, reason: "step" });
|
|
385
|
-
}
|
|
386
|
-
});
|
|
387
|
-
break;
|
|
388
|
-
}
|
|
389
|
-
case "guidely:preview-guide": {
|
|
390
|
-
this.stopPreview();
|
|
391
|
-
const g = msg.guide;
|
|
392
|
-
const guide = { id: g.id || "preview", key: g.key || "preview", name: g.name || "Preview", kind: g.kind || "flow", version: g.version ?? 0, config: (await loadSchemas()).GuideConfigSchema.parse(g.config) };
|
|
393
|
-
this.session?.setPicking(false);
|
|
394
|
-
this.session?.setStatus(`Previewing \u201C${guide.name}\u201D`);
|
|
395
|
-
this.previewGuide = (0, import_guide_runtime.runGuide)(guide, {
|
|
396
|
-
theme: this.opts.theme(),
|
|
397
|
-
ctx: this.opts.ctx,
|
|
398
|
-
onNavigate: (url, newTab) => newTab ? window.open(url, "_blank", "noopener") : window.location.assign(url),
|
|
399
|
-
onEnd: (reason) => {
|
|
400
|
-
this.previewGuide = null;
|
|
401
|
-
this.session?.setPicking(true);
|
|
402
|
-
this.post({ type: "guidely:preview-ended", sessionId: this.info.id, reason });
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
this.previewGuide.start();
|
|
406
|
-
break;
|
|
407
|
-
}
|
|
408
|
-
case "guidely:highlight": {
|
|
409
|
-
const result = (0, import_guide_runtime.highlightSelectors)(Array.isArray(msg.selectors) ? msg.selectors : [], { durationMs: 3e3 });
|
|
410
|
-
this.post({ type: "guidely:highlight-result", sessionId: this.info.id, counts: result.counts, resolved: !!result.resolved });
|
|
411
|
-
break;
|
|
412
|
-
}
|
|
413
|
-
case "guidely:stop-preview":
|
|
414
|
-
this.stopPreview();
|
|
415
|
-
break;
|
|
416
|
-
case "guidely:request-snapshot":
|
|
417
|
-
this.lastSnapshot = "";
|
|
418
|
-
this.sendSnapshot();
|
|
419
|
-
break;
|
|
420
|
-
case "guidely:ping":
|
|
421
|
-
this.post({ type: "guidely:pong", sessionId: this.info.id, url: window.location.href });
|
|
422
|
-
break;
|
|
423
|
-
case "guidely:exit":
|
|
424
|
-
this.stop();
|
|
425
|
-
break;
|
|
426
|
-
default:
|
|
427
|
-
break;
|
|
428
|
-
}
|
|
429
|
-
} catch (err) {
|
|
430
|
-
this.opts.onError(err);
|
|
573
|
+
for (let i = 0; i < turns.length; i += import_runtime4.AGENT_TURNS_PER_BATCH) {
|
|
574
|
+
const batch = turns.slice(i, i + import_runtime4.AGENT_TURNS_PER_BATCH);
|
|
575
|
+
this.outbox.add(`agent:${batch[0].turnId}`, "/agent-events", { turns: batch, sdk: this.opts.sdk });
|
|
431
576
|
}
|
|
432
577
|
}
|
|
433
|
-
/**
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
this.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
this.
|
|
440
|
-
|
|
441
|
-
this.snapshotTimer = null;
|
|
442
|
-
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
443
|
-
this.heartbeat = null;
|
|
444
|
-
this.session?.stop();
|
|
445
|
-
this.session = null;
|
|
446
|
-
this.opts.sessionStore.remove(KEYS.authoring);
|
|
447
|
-
if (this.info) this.post({ type: "guidely:exit", sessionId: this.info.id });
|
|
448
|
-
this.opts.onActive(false);
|
|
578
|
+
/** Consent changed: granted sends what was held, anything else drops it. */
|
|
579
|
+
consentChanged(granted) {
|
|
580
|
+
const held = this.pending.splice(0);
|
|
581
|
+
if (granted) this.add(held);
|
|
582
|
+
}
|
|
583
|
+
destroy() {
|
|
584
|
+
this.pending = [];
|
|
585
|
+
this.outbox.destroy();
|
|
449
586
|
}
|
|
450
587
|
};
|
|
451
588
|
|
|
452
|
-
// src/
|
|
453
|
-
var
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
589
|
+
// src/authoring.ts
|
|
590
|
+
var import_guide_runtime3 = require("@guidelytech/guide-runtime");
|
|
591
|
+
|
|
592
|
+
// src/snapshot.ts
|
|
593
|
+
var MAX_SNAPSHOT_BYTES = 6 * 1024 * 1024;
|
|
594
|
+
var STRIP = "script, noscript, template, #guidely-root, [data-guidely], meta[http-equiv], link[rel~='preload'], link[rel~='modulepreload'], link[rel~='prefetch'], link[rel~='manifest'], link[rel~='serviceworker']";
|
|
595
|
+
function rulesText(sheet) {
|
|
596
|
+
if (!sheet) return null;
|
|
458
597
|
try {
|
|
459
|
-
|
|
460
|
-
token = url.searchParams.get(import_runtime2.CAPTURE_PARAM);
|
|
461
|
-
authoringLink = url.searchParams.has(AUTHORING_PARAM);
|
|
462
|
-
if (token) {
|
|
463
|
-
url.searchParams.delete(import_runtime2.CAPTURE_PARAM);
|
|
464
|
-
window.history.replaceState(window.history.state, "", url.toString());
|
|
465
|
-
}
|
|
598
|
+
return Array.from(sheet.cssRules, (r) => r.cssText).join("\n");
|
|
466
599
|
} catch {
|
|
467
|
-
token = null;
|
|
468
|
-
}
|
|
469
|
-
const saved = readJson(sessionStore, KEYS.capture, null);
|
|
470
|
-
let marker = null;
|
|
471
|
-
if (token) marker = saved?.token === token ? saved : { token };
|
|
472
|
-
else if (saved?.token && !authoringLink) marker = saved;
|
|
473
|
-
if (!marker) {
|
|
474
|
-
if (saved) {
|
|
475
|
-
sessionStore.remove(KEYS.capture);
|
|
476
|
-
sessionStore.remove(KEYS.capturePending);
|
|
477
|
-
}
|
|
478
600
|
return null;
|
|
479
601
|
}
|
|
480
|
-
if (token && saved?.token !== token) sessionStore.remove(KEYS.capturePending);
|
|
481
|
-
writeJson(sessionStore, KEYS.capture, marker);
|
|
482
|
-
sessionStore.remove(KEYS.authoring);
|
|
483
|
-
return marker;
|
|
484
602
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
603
|
+
function maskText(root) {
|
|
604
|
+
const walker = root.ownerDocument.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
605
|
+
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
|
606
|
+
n.textContent = (n.textContent ?? "").replace(/\S/g, "\u2022");
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function captureSnapshot(doc, maskSelectors, maxBytes = MAX_SNAPSHOT_BYTES) {
|
|
610
|
+
const win = doc.defaultView;
|
|
611
|
+
if (!win || !doc.documentElement) return null;
|
|
612
|
+
const clone = doc.documentElement.cloneNode(true);
|
|
613
|
+
const origStyles = doc.documentElement.querySelectorAll("style");
|
|
614
|
+
clone.querySelectorAll("style").forEach((el, i) => {
|
|
615
|
+
const css = rulesText(origStyles[i]?.sheet);
|
|
616
|
+
if (css !== null && css.length > (el.textContent ?? "").length) el.textContent = css;
|
|
617
|
+
});
|
|
618
|
+
const origFrames = doc.documentElement.querySelectorAll("iframe, canvas, video, embed, object");
|
|
619
|
+
clone.querySelectorAll("iframe, canvas, video, embed, object").forEach((el, i) => {
|
|
620
|
+
const r = origFrames[i]?.getBoundingClientRect();
|
|
621
|
+
const box = doc.createElement("div");
|
|
622
|
+
box.setAttribute("style", `${el.getAttribute("style") ?? ""};width:${r?.width ?? 0}px;height:${r?.height ?? 0}px;background:#e2e8f0;border-radius:4px`);
|
|
623
|
+
if (el.getAttribute("class")) box.setAttribute("class", el.getAttribute("class"));
|
|
624
|
+
el.replaceWith(box);
|
|
625
|
+
});
|
|
626
|
+
clone.querySelectorAll(STRIP).forEach((el) => el.remove());
|
|
627
|
+
for (const sel of maskSelectors) {
|
|
628
|
+
try {
|
|
629
|
+
clone.querySelectorAll(sel).forEach(maskText);
|
|
630
|
+
} catch {
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
clone.querySelectorAll("*").forEach((el) => {
|
|
634
|
+
for (const attr2 of Array.from(el.attributes)) {
|
|
635
|
+
const name = attr2.name.toLowerCase();
|
|
636
|
+
if (name.startsWith("on") || (name === "href" || name === "src" || name === "action") && /^\s*javascript:/i.test(attr2.value)) el.removeAttribute(attr2.name);
|
|
637
|
+
}
|
|
638
|
+
if (el instanceof HTMLInputElement) {
|
|
639
|
+
el.removeAttribute("value");
|
|
640
|
+
el.removeAttribute("checked");
|
|
641
|
+
} else if (el instanceof HTMLTextAreaElement) {
|
|
642
|
+
el.textContent = "";
|
|
643
|
+
}
|
|
644
|
+
el.removeAttribute("autofocus");
|
|
645
|
+
});
|
|
646
|
+
const adopted = Array.from(doc.adoptedStyleSheets ?? []).map(rulesText).filter(Boolean).join("\n");
|
|
647
|
+
let head = clone.querySelector("head");
|
|
648
|
+
if (!head) {
|
|
649
|
+
head = doc.createElement("head");
|
|
650
|
+
clone.prepend(head);
|
|
651
|
+
}
|
|
652
|
+
if (adopted) {
|
|
653
|
+
const style = doc.createElement("style");
|
|
654
|
+
style.textContent = adopted;
|
|
655
|
+
head.append(style);
|
|
656
|
+
}
|
|
657
|
+
clone.querySelectorAll("base").forEach((el) => el.remove());
|
|
658
|
+
const base = doc.createElement("base");
|
|
659
|
+
base.setAttribute("href", doc.baseURI || win.location.href);
|
|
660
|
+
base.setAttribute("target", "_blank");
|
|
661
|
+
head.prepend(base);
|
|
662
|
+
const html = `<!DOCTYPE html>
|
|
663
|
+
${clone.outerHTML}`;
|
|
664
|
+
if (html.length > maxBytes) return null;
|
|
665
|
+
return { html, url: win.location.href, title: doc.title, width: win.innerWidth, height: win.innerHeight, capturedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// src/tagging.ts
|
|
669
|
+
var import_runtime7 = require("@guidelytech/shared/runtime");
|
|
670
|
+
var import_guide_runtime2 = require("@guidelytech/guide-runtime");
|
|
671
|
+
|
|
672
|
+
// src/autocapture.ts
|
|
673
|
+
var import_guide_runtime = require("@guidelytech/guide-runtime");
|
|
674
|
+
var INTERACTIVE_SELECTOR = "a,button,input[type=button],input[type=submit],input[type=reset],input[type=checkbox],input[type=radio],select,summary,label,[role=button],[role=link],[role=tab],[role=menuitem],[role=checkbox],[role=switch],[role=option],[onclick],[data-guidely-track]";
|
|
675
|
+
var DEFAULT_MASK_SELECTORS = ["[data-guidely-mask]", ".guidely-mask"];
|
|
676
|
+
var PREFERRED_ATTRS = ["data-testid", "data-test-id", "data-test", "data-cy", "data-qa", "data-guidely-track", "data-tour"];
|
|
677
|
+
var NOISY_DATA_ATTR = /^data-(state|v-|react|radix|headlessui|svelte|orientation|highlighted|focus|key|index|side|align|disabled|selected|active|open|guidely-(mask|ignore|block))/i;
|
|
678
|
+
function validSelectors(list) {
|
|
679
|
+
const out = [];
|
|
680
|
+
const probe = typeof document !== "undefined" ? document.createDocumentFragment() : null;
|
|
681
|
+
for (const raw of list) {
|
|
499
682
|
const s = (raw || "").trim();
|
|
500
683
|
if (!s || out.includes(s)) continue;
|
|
501
684
|
try {
|
|
@@ -507,11 +690,11 @@ function validSelectors(list) {
|
|
|
507
690
|
return out;
|
|
508
691
|
}
|
|
509
692
|
function isIgnoredElement(el) {
|
|
510
|
-
return (0,
|
|
693
|
+
return (0, import_guide_runtime.isGuidelyElement)(el) || !!(0, import_guide_runtime.closestComposed)(el, "[data-guidely-ignore]");
|
|
511
694
|
}
|
|
512
695
|
function interactiveElement(target) {
|
|
513
696
|
try {
|
|
514
|
-
return (0,
|
|
697
|
+
return (0, import_guide_runtime.closestComposed)(target, INTERACTIVE_SELECTOR);
|
|
515
698
|
} catch {
|
|
516
699
|
return null;
|
|
517
700
|
}
|
|
@@ -523,12 +706,12 @@ function esc(value) {
|
|
|
523
706
|
var attr = (tag, name, value) => value ? `${tag}[${name}="${value.replace(/["\\]/g, "\\$&")}"]` : `${tag}[${name}]`;
|
|
524
707
|
function elementSelector(el) {
|
|
525
708
|
const own = ownSelector(el);
|
|
526
|
-
const chain = (0,
|
|
527
|
-
return (chain ? chain +
|
|
709
|
+
const chain = (0, import_guide_runtime.shadowHostChain)(el);
|
|
710
|
+
return (chain ? chain + import_guide_runtime.SHADOW_SEPARATOR + own : own).slice(0, 300);
|
|
528
711
|
}
|
|
529
712
|
function ownSelector(el) {
|
|
530
713
|
const tag = el.tagName.toLowerCase();
|
|
531
|
-
if (el.id && (0,
|
|
714
|
+
if (el.id && (0, import_guide_runtime.isStableId)(el.id)) return `#${esc(el.id)}`;
|
|
532
715
|
for (const name of PREFERRED_ATTRS) if (el.hasAttribute(name)) return attr(tag, name, el.getAttribute(name) || "");
|
|
533
716
|
for (const a of Array.from(el.attributes)) {
|
|
534
717
|
if (a.name.startsWith("data-") && !NOISY_DATA_ATTR.test(a.name) && a.value && a.value.length <= 60) return attr(tag, a.name, a.value);
|
|
@@ -540,7 +723,7 @@ function ownSelector(el) {
|
|
|
540
723
|
while (cur && parts.length < 4) {
|
|
541
724
|
const t = cur.tagName.toLowerCase();
|
|
542
725
|
if (t === "html" || t === "body") break;
|
|
543
|
-
if (cur !== el && cur.id && (0,
|
|
726
|
+
if (cur !== el && cur.id && (0, import_guide_runtime.isStableId)(cur.id)) {
|
|
544
727
|
parts.unshift(`#${esc(cur.id)}`);
|
|
545
728
|
break;
|
|
546
729
|
}
|
|
@@ -555,7 +738,7 @@ function ownSelector(el) {
|
|
|
555
738
|
function matches(el, selector, deep) {
|
|
556
739
|
if (!selector) return false;
|
|
557
740
|
try {
|
|
558
|
-
return !!(0,
|
|
741
|
+
return !!(0, import_guide_runtime.closestComposed)(el, selector) || deep && !!el.querySelector(selector);
|
|
559
742
|
} catch {
|
|
560
743
|
return false;
|
|
561
744
|
}
|
|
@@ -573,7 +756,7 @@ function elementText(el, mask, maskAll) {
|
|
|
573
756
|
return text ? text.slice(0, 100) : void 0;
|
|
574
757
|
}
|
|
575
758
|
function linkHref(el) {
|
|
576
|
-
const a = (0,
|
|
759
|
+
const a = (0, import_guide_runtime.closestComposed)(el, "a[href]");
|
|
577
760
|
if (!a) return void 0;
|
|
578
761
|
try {
|
|
579
762
|
const u = new URL(a.getAttribute("href") || "", location.href);
|
|
@@ -619,564 +802,713 @@ var RageClickDetector = class {
|
|
|
619
802
|
}
|
|
620
803
|
};
|
|
621
804
|
|
|
622
|
-
// src/
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
else if (/Firefox\/|FxiOS/i.test(u)) browser = "firefox";
|
|
636
|
-
else if (/Safari\//i.test(u) && /Version\//i.test(u)) browser = "safari";
|
|
637
|
-
else if (/MSIE|Trident/i.test(u)) browser = "ie";
|
|
638
|
-
let os;
|
|
639
|
-
if (/Windows/i.test(u)) os = "windows";
|
|
640
|
-
else if (/iPhone|iPad|iPod/i.test(u)) os = "ios";
|
|
641
|
-
else if (/Mac OS X|Macintosh/i.test(u)) os = "macos";
|
|
642
|
-
else if (/Android/i.test(u)) os = "android";
|
|
643
|
-
else if (/CrOS/i.test(u)) os = "chromeos";
|
|
644
|
-
else if (/Linux/i.test(u)) os = "linux";
|
|
645
|
-
return { type, browser, os };
|
|
805
|
+
// src/tagging.ts
|
|
806
|
+
var TEXT_ATTRS = /* @__PURE__ */ new Set(["aria-label", "title", "alt"]);
|
|
807
|
+
var SAFE_ATTRS = ["role", "name", "type", "aria-label", "title", "alt", "for"];
|
|
808
|
+
var STATE_CLASS = /^(is-|has-)?(active|hover|hovered|focus|focused|focus-visible|selected|open|opened|closed|disabled|visible|hidden|show|shown|collapsed|expanded|checked|pressed|loading|current)$/i;
|
|
809
|
+
function nthOfType(el) {
|
|
810
|
+
const parent = el.parentElement;
|
|
811
|
+
if (!parent) return void 0;
|
|
812
|
+
let n = 0;
|
|
813
|
+
for (const c of Array.from(parent.children)) {
|
|
814
|
+
if (c.tagName === el.tagName) n++;
|
|
815
|
+
if (c === el) return n;
|
|
816
|
+
}
|
|
817
|
+
return void 0;
|
|
646
818
|
}
|
|
647
|
-
function
|
|
648
|
-
|
|
649
|
-
|
|
819
|
+
function nodeOf(el, masked, featureAttribute) {
|
|
820
|
+
const node = { t: el.tagName.toLowerCase().slice(0, import_runtime7.SNAPSHOT_LIMITS.tag) };
|
|
821
|
+
if (el.id && (0, import_guide_runtime2.isStableId)(el.id) && el.id.length <= import_runtime7.SNAPSHOT_LIMITS.id) node.id = el.id;
|
|
822
|
+
const classes = Array.from(el.classList).filter((c) => (0, import_guide_runtime2.isStableClass)(c) && !STATE_CLASS.test(c) && c.length <= import_runtime7.SNAPSHOT_LIMITS.className);
|
|
823
|
+
if (classes.length) node.c = classes.slice(0, import_runtime7.SNAPSHOT_LIMITS.classes);
|
|
824
|
+
const a = {};
|
|
825
|
+
const put = (name, value) => {
|
|
826
|
+
if (value === null || Object.keys(a).length >= import_runtime7.SNAPSHOT_LIMITS.attrs || name in a) return;
|
|
827
|
+
if (masked && TEXT_ATTRS.has(name)) return;
|
|
828
|
+
a[name] = value.slice(0, import_runtime7.SNAPSHOT_LIMITS.attrValue);
|
|
829
|
+
};
|
|
830
|
+
if (featureAttribute && el.hasAttribute(featureAttribute)) put(featureAttribute, el.getAttribute(featureAttribute));
|
|
831
|
+
for (const attr2 of Array.from(el.attributes)) {
|
|
832
|
+
if (attr2.name.startsWith("data-") && !import_runtime7.NOISY_DATA_ATTR.test(attr2.name) && attr2.value.length <= import_runtime7.SNAPSHOT_LIMITS.attrValue) put(attr2.name, attr2.value);
|
|
833
|
+
}
|
|
834
|
+
for (const name of SAFE_ATTRS) if (el.hasAttribute(name)) put(name, el.getAttribute(name));
|
|
835
|
+
if (Object.keys(a).length) node.a = a;
|
|
836
|
+
const n = nthOfType(el);
|
|
837
|
+
if (n) node.n = n;
|
|
838
|
+
return node;
|
|
839
|
+
}
|
|
840
|
+
function elementSnapshot(el, opts) {
|
|
841
|
+
const masked = opts.maskAll || isMasked(el, opts.mask);
|
|
842
|
+
const snap = nodeOf(el, masked, opts.featureAttribute);
|
|
843
|
+
const p = [];
|
|
844
|
+
for (let cur = el.parentElement; cur && p.length < import_runtime7.SNAPSHOT_LIMITS.ancestors; cur = cur.parentElement) {
|
|
845
|
+
if (cur.tagName === "BODY" || cur.tagName === "HTML") break;
|
|
846
|
+
p.push(nodeOf(cur, masked, opts.featureAttribute));
|
|
847
|
+
}
|
|
848
|
+
if (p.length) snap.p = p;
|
|
849
|
+
const h = [];
|
|
850
|
+
let root = el.getRootNode?.();
|
|
851
|
+
while (root && root.nodeType === 11 && root.host && h.length < import_runtime7.SNAPSHOT_LIMITS.hosts) {
|
|
852
|
+
h.push(nodeOf(root.host, masked, opts.featureAttribute));
|
|
853
|
+
root = root.host.getRootNode?.();
|
|
854
|
+
}
|
|
855
|
+
if (h.length) snap.h = h;
|
|
856
|
+
return snap;
|
|
857
|
+
}
|
|
858
|
+
function isMasked(el, mask) {
|
|
859
|
+
if (!mask) return false;
|
|
650
860
|
try {
|
|
651
|
-
|
|
652
|
-
path = u.pathname;
|
|
653
|
-
u.searchParams.forEach((v, k) => {
|
|
654
|
-
query[k] = v;
|
|
655
|
-
});
|
|
861
|
+
return !!(0, import_guide_runtime2.closestComposed)(el, mask);
|
|
656
862
|
} catch {
|
|
657
|
-
|
|
863
|
+
return false;
|
|
658
864
|
}
|
|
659
|
-
const device = input.device ?? (typeof navigator !== "undefined" ? detectDevice(navigator.userAgent, typeof window !== "undefined" ? window.innerWidth : 0) : { type: "desktop" });
|
|
660
|
-
return {
|
|
661
|
-
anonymousId: input.anonymousId,
|
|
662
|
-
userId: input.userId ?? null,
|
|
663
|
-
userAttributes: input.userAttributes,
|
|
664
|
-
accountAttributes: input.accountAttributes,
|
|
665
|
-
url: input.url,
|
|
666
|
-
path,
|
|
667
|
-
query,
|
|
668
|
-
device,
|
|
669
|
-
firstVisit: input.firstVisit,
|
|
670
|
-
eventsSeen: new Set(input.eventsSeen),
|
|
671
|
-
completedGuides: new Set(input.completedGuides),
|
|
672
|
-
dismissedGuides: new Set(input.dismissedGuides),
|
|
673
|
-
seenGuides: new Set(input.seenGuides),
|
|
674
|
-
completedChecklists: new Set(input.completedChecklists),
|
|
675
|
-
audienceIds: new Set(input.audienceIds),
|
|
676
|
-
now: input.now ?? /* @__PURE__ */ new Date(),
|
|
677
|
-
environment: input.environment,
|
|
678
|
-
locale: input.locale ?? (typeof navigator !== "undefined" ? navigator.language : void 0),
|
|
679
|
-
platform: input.platform ?? "web",
|
|
680
|
-
...input.screen !== void 0 ? { screen: input.screen } : {},
|
|
681
|
-
...input.appVersion !== void 0 ? { appVersion: input.appVersion } : {}
|
|
682
|
-
};
|
|
683
865
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const s = elementSelector(el).slice(0, 120);
|
|
693
|
-
if (s) p.s = s;
|
|
694
|
-
const rect = el.getBoundingClientRect();
|
|
695
|
-
if (rect.width > 0 && rect.height > 0) {
|
|
696
|
-
p.rx = r2((e.clientX - rect.left) / rect.width);
|
|
697
|
-
p.ry = r2((e.clientY - rect.top) / rect.height);
|
|
866
|
+
function featureAttributeOf(target, attribute) {
|
|
867
|
+
if (!attribute) return null;
|
|
868
|
+
try {
|
|
869
|
+
const el = (0, import_guide_runtime2.closestComposed)(target, `[${attribute}]`);
|
|
870
|
+
const value = el?.getAttribute(attribute)?.replace(/\s+/g, " ").trim();
|
|
871
|
+
return el && value ? { el, value: value.slice(0, 100) } : null;
|
|
872
|
+
} catch {
|
|
873
|
+
return null;
|
|
698
874
|
}
|
|
699
|
-
return p;
|
|
700
|
-
}
|
|
701
|
-
var selectors = /* @__PURE__ */ new WeakMap();
|
|
702
|
-
var r1 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 10) / 10;
|
|
703
|
-
function movementBin(el, e) {
|
|
704
|
-
const p = { k: "m", x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
|
|
705
|
-
let s = /^(HTML|BODY)$/.test(el.tagName) ? "" : selectors.get(el);
|
|
706
|
-
if (s === void 0) selectors.set(el, s = elementSelector(el).slice(0, 120));
|
|
707
|
-
const rect = s ? el.getBoundingClientRect() : null;
|
|
708
|
-
if (!rect || !(rect.width > 0 && rect.height > 0)) return [`${p.x >> 4},${p.y >> 4}`, p];
|
|
709
|
-
p.s = s;
|
|
710
|
-
p.rx = r1((e.clientX - rect.left) / rect.width);
|
|
711
|
-
p.ry = r1((e.clientY - rect.top) / rect.height);
|
|
712
|
-
return [`${s} ${p.rx},${p.ry}`, p];
|
|
713
875
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
722
|
-
/** Adds a click (`burst` > 0 marks it and the burst's previous clicks as rage); true when the batch is full. */
|
|
723
|
-
add(url, point, burst) {
|
|
724
|
-
this.url = url;
|
|
725
|
-
for (let i = Math.max(0, this.points.length - burst + 1); i < this.points.length; i++) this.points[i].k = "r";
|
|
726
|
-
this.points.push(point);
|
|
727
|
-
this.bytes += JSON.stringify(point).length + 1;
|
|
728
|
-
return this.full();
|
|
729
|
-
}
|
|
730
|
-
/** Adds a movement sample to its bin (weight +1); true when the batch is full. */
|
|
731
|
-
move(url, [key, point]) {
|
|
732
|
-
this.url = url;
|
|
733
|
-
const bin = this.bins.get(key);
|
|
734
|
-
if (bin) bin.w = (bin.w ?? 1) + 1;
|
|
735
|
-
else {
|
|
736
|
-
this.bins.set(key, point);
|
|
737
|
-
this.bytes += JSON.stringify(point).length + 8;
|
|
876
|
+
function readProperty(def, base, opts) {
|
|
877
|
+
let el = base;
|
|
878
|
+
if (def.selector) {
|
|
879
|
+
try {
|
|
880
|
+
el = (0, import_guide_runtime2.querySelectorStrategy)({ kind: "css", value: def.selector }, document)[0] ?? null;
|
|
881
|
+
} catch {
|
|
882
|
+
el = null;
|
|
738
883
|
}
|
|
739
|
-
return this.full();
|
|
740
|
-
}
|
|
741
|
-
full() {
|
|
742
|
-
return this.points.length >= HEATMAP_MAX_POINTS || this.bins.size > HEATMAP_MAX_BINS || this.bytes >= HEATMAP_MAX_BYTES;
|
|
743
884
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
return
|
|
885
|
+
if (!el) return void 0;
|
|
886
|
+
if (def.source === "attribute") {
|
|
887
|
+
const name = (def.attribute ?? "").toLowerCase();
|
|
888
|
+
if (!name || name === "value" || name.startsWith("on")) return void 0;
|
|
889
|
+
if (opts.maskAll || isMasked(el, opts.mask)) return void 0;
|
|
890
|
+
const v = el.getAttribute(name);
|
|
891
|
+
return v ? v.replace(/\s+/g, " ").trim().slice(0, import_runtime7.TAG_PROPERTY_LIMITS.value) || void 0 : void 0;
|
|
751
892
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
// src/errors.ts
|
|
755
|
-
var import_runtime5 = require("@guidelytech/shared/runtime");
|
|
756
|
-
var MAX_FRAMES_BYTES = 6e3;
|
|
757
|
-
function fitFrames(frames) {
|
|
758
|
-
const out = frames.slice(0, 30);
|
|
759
|
-
while (out.length > 1 && JSON.stringify(out).length > MAX_FRAMES_BYTES) out.pop();
|
|
760
|
-
return out;
|
|
893
|
+
return elementText(el, opts.mask, opts.maskAll);
|
|
761
894
|
}
|
|
762
|
-
function
|
|
763
|
-
if (
|
|
895
|
+
function clickPropertyValues(config, el, click, opts) {
|
|
896
|
+
if (!config?.properties.length) return void 0;
|
|
897
|
+
const out = {};
|
|
898
|
+
const css = (selector) => {
|
|
899
|
+
try {
|
|
900
|
+
return !!(0, import_guide_runtime2.closestComposed)(el, selector);
|
|
901
|
+
} catch {
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
const here = { url: location.href, path: location.pathname, title: document.title };
|
|
906
|
+
for (const tag of config.properties) {
|
|
907
|
+
if (tag.kind !== "feature" || !tag.featureRules) continue;
|
|
908
|
+
const rules = tag.featureRules;
|
|
909
|
+
const matches2 = (r) => (0, import_runtime7.featureRuleMatchesClick)(r, click, css) && (!r.page?.pattern || (0, import_runtime7.pageRuleMatches)({ type: "url", value: r.page.pattern }, here));
|
|
910
|
+
if (!rules.some((r) => !r.exclude && matches2(r)) || rules.some((r) => r.exclude && matches2(r))) continue;
|
|
911
|
+
for (const def of tag.properties) {
|
|
912
|
+
const v = readProperty(def, el, opts);
|
|
913
|
+
if (v) out[def.id] = v;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return Object.keys(out).length ? out : void 0;
|
|
917
|
+
}
|
|
918
|
+
function pagePropertyValues(config, url, opts) {
|
|
919
|
+
if (!config?.properties.length) return void 0;
|
|
920
|
+
const out = {};
|
|
921
|
+
let path = "/";
|
|
764
922
|
try {
|
|
765
|
-
|
|
923
|
+
path = new URL(url, location.href).pathname;
|
|
766
924
|
} catch {
|
|
767
|
-
return String(value);
|
|
768
925
|
}
|
|
926
|
+
const ctx = { url, path, title: document.title };
|
|
927
|
+
for (const tag of config.properties) {
|
|
928
|
+
if (tag.kind !== "page" || !tag.pageRules || !(0, import_runtime7.pageRulesMatch)(tag.pageRules, ctx)) continue;
|
|
929
|
+
for (const def of tag.properties) {
|
|
930
|
+
if (!def.selector) continue;
|
|
931
|
+
const v = readProperty(def, null, opts);
|
|
932
|
+
if (v) out[def.id] = v;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return Object.keys(out).length ? out : void 0;
|
|
769
936
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
message: (isError ? e.message : describe(error)).slice(0, 1e3),
|
|
776
|
-
frames: isError ? fitFrames((0, import_runtime5.parseStack)(e.stack)) : [],
|
|
777
|
-
source,
|
|
778
|
-
handled
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
function errorEventToException(e) {
|
|
782
|
-
const target = e.target;
|
|
783
|
-
if (target && typeof Element !== "undefined" && target instanceof Element) return null;
|
|
784
|
-
if (e.error != null) return exceptionFromError(e.error, "onerror", false);
|
|
785
|
-
const message = e.message || "";
|
|
786
|
-
if (!message || /^script error\.?$/i.test(message.trim())) return null;
|
|
787
|
-
const m = /^(?:Uncaught )?([A-Z]\w*(?:Error|Exception)):\s*([\s\S]*)$/.exec(message);
|
|
788
|
-
return {
|
|
789
|
-
type: m?.[1] ?? "Error",
|
|
790
|
-
message: (m?.[2] ?? message).slice(0, 1e3),
|
|
791
|
-
frames: e.filename ? [{ file: e.filename.slice(0, 500), line: e.lineno || void 0, col: e.colno || void 0 }] : [],
|
|
792
|
-
source: "onerror",
|
|
793
|
-
handled: false
|
|
794
|
-
};
|
|
937
|
+
|
|
938
|
+
// src/authoring.ts
|
|
939
|
+
async function loadSchemas() {
|
|
940
|
+
const m = await import("@guidelytech/shared");
|
|
941
|
+
return { StepSchema: m.StepSchema, GuideConfigSchema: m.GuideConfigSchema };
|
|
795
942
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
var
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
943
|
+
var AUTHORING_PARAM = "guidely_authoring";
|
|
944
|
+
var HEARTBEAT_MS = 15e3;
|
|
945
|
+
var SNAPSHOT_DEBOUNCE_MS = 800;
|
|
946
|
+
var SNAPSHOT_MAX_WAIT_MS = 4e3;
|
|
947
|
+
var AuthoringBridge = class {
|
|
948
|
+
constructor(opts) {
|
|
949
|
+
__publicField(this, "opts", opts);
|
|
950
|
+
__publicField(this, "token", null);
|
|
951
|
+
__publicField(this, "info", null);
|
|
952
|
+
__publicField(this, "session", null);
|
|
953
|
+
__publicField(this, "heartbeat", null);
|
|
954
|
+
__publicField(this, "preview", null);
|
|
955
|
+
__publicField(this, "previewGuide", null);
|
|
956
|
+
__publicField(this, "active", false);
|
|
957
|
+
__publicField(this, "observer", null);
|
|
958
|
+
__publicField(this, "snapshotTimer", null);
|
|
959
|
+
__publicField(this, "snapshotDue", 0);
|
|
960
|
+
__publicField(this, "lastSnapshot", "");
|
|
808
961
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
constructor(max = 5) {
|
|
812
|
-
__publicField(this, "max", max);
|
|
813
|
-
__publicField(this, "seen", /* @__PURE__ */ new Map());
|
|
962
|
+
isActive() {
|
|
963
|
+
return this.active;
|
|
814
964
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
965
|
+
/** Returns true when a token was found (in the URL or the session marker) and verification started. */
|
|
966
|
+
detect() {
|
|
967
|
+
if (typeof window === "undefined") return false;
|
|
968
|
+
let token = null;
|
|
969
|
+
try {
|
|
970
|
+
const url = new URL(window.location.href);
|
|
971
|
+
token = url.searchParams.get(AUTHORING_PARAM);
|
|
972
|
+
if (token) {
|
|
973
|
+
url.searchParams.delete(AUTHORING_PARAM);
|
|
974
|
+
window.history.replaceState(window.history.state, "", url.toString());
|
|
975
|
+
}
|
|
976
|
+
} catch {
|
|
977
|
+
token = null;
|
|
978
|
+
}
|
|
979
|
+
if (!token) {
|
|
980
|
+
const marker = readJson(this.opts.sessionStore, KEYS.authoring, null);
|
|
981
|
+
token = marker?.token ?? null;
|
|
982
|
+
}
|
|
983
|
+
if (!token) return false;
|
|
984
|
+
this.token = token;
|
|
985
|
+
writeJson(this.opts.sessionStore, KEYS.authoring, { token });
|
|
986
|
+
void this.start();
|
|
822
987
|
return true;
|
|
823
988
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
989
|
+
async start() {
|
|
990
|
+
if (!this.token) return;
|
|
991
|
+
try {
|
|
992
|
+
const res = await this.opts.api.get("/authoring/session", { params: { token: this.token } });
|
|
993
|
+
if (!res.ok || !res.data) {
|
|
994
|
+
this.opts.sessionStore.remove(KEYS.authoring);
|
|
995
|
+
this.opts.onError(new Error(`[Guidely] authoring session invalid or expired (${res.status})`));
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
this.info = res.data;
|
|
999
|
+
if (Date.parse(this.info.expiresAt) < Date.now()) {
|
|
1000
|
+
this.opts.sessionStore.remove(KEYS.authoring);
|
|
1001
|
+
this.opts.onError(new Error("[Guidely] authoring session expired"));
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
this.active = true;
|
|
1005
|
+
this.opts.onActive(true);
|
|
1006
|
+
this.session = (0, import_guide_runtime3.startAuthoring)({
|
|
1007
|
+
dashboardOrigin: this.info.dashboardOrigin,
|
|
1008
|
+
// Tag mode (Pages & Features): picks become feature rules; the page itself is tagged from the dashboard.
|
|
1009
|
+
message: this.info.purpose === "tag" ? "Guidely tagging \u2014 click an element to tag it as a feature \xB7 tag the page from the dashboard \xB7 Esc to finish" : void 0,
|
|
1010
|
+
onPick: (pick) => this.sendPick(pick),
|
|
1011
|
+
onExit: () => this.stop()
|
|
1012
|
+
});
|
|
1013
|
+
this.heartbeat = setInterval(() => {
|
|
1014
|
+
void this.opts.api.post("/authoring/session/heartbeat", {}, { params: { token: this.token ?? void 0 } }).catch(() => {
|
|
1015
|
+
});
|
|
1016
|
+
}, HEARTBEAT_MS);
|
|
1017
|
+
this.post({ type: "guidely:authoring-ready", sessionId: this.info.id, url: window.location.href });
|
|
1018
|
+
this.watchPage();
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
this.opts.onError(err);
|
|
837
1021
|
}
|
|
838
|
-
set.add(cb);
|
|
839
|
-
return () => this.off(event, cb);
|
|
840
1022
|
}
|
|
841
|
-
|
|
842
|
-
this.
|
|
843
|
-
}
|
|
844
|
-
emit(event, ...args) {
|
|
845
|
-
const set = this.listeners.get(event);
|
|
846
|
-
if (!set) return;
|
|
847
|
-
for (const cb of Array.from(set)) {
|
|
1023
|
+
masked(el) {
|
|
1024
|
+
return this.opts.maskSelectors.some((sel) => {
|
|
848
1025
|
try {
|
|
849
|
-
|
|
850
|
-
} catch
|
|
851
|
-
|
|
1026
|
+
return !!(0, import_guide_runtime3.closestComposed)(el, sel);
|
|
1027
|
+
} catch {
|
|
1028
|
+
return false;
|
|
852
1029
|
}
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
clear() {
|
|
856
|
-
this.listeners.clear();
|
|
857
|
-
}
|
|
858
|
-
};
|
|
859
|
-
|
|
860
|
-
// src/frequency.ts
|
|
861
|
-
var HOUR = 3600 * 1e3;
|
|
862
|
-
function isFrequencyEligible(freq, state, shownThisSession, now = Date.now()) {
|
|
863
|
-
const s = state ?? { impressions: 0 };
|
|
864
|
-
if (freq.maxImpressions != null && s.impressions >= freq.maxImpressions) return false;
|
|
865
|
-
if (s.completedAt && freq.mode !== "every_time") return false;
|
|
866
|
-
if (s.dismissedAt) {
|
|
867
|
-
if (!freq.reshowAfterDismiss) return false;
|
|
868
|
-
if (freq.cooldownHours > 0 && now - Date.parse(s.dismissedAt) < freq.cooldownHours * HOUR) return false;
|
|
1030
|
+
});
|
|
869
1031
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
return now - Date.parse(s.lastShownAt) >= hours * HOUR;
|
|
879
|
-
}
|
|
880
|
-
case "every_time":
|
|
881
|
-
default:
|
|
882
|
-
return true;
|
|
1032
|
+
sendPick(raw) {
|
|
1033
|
+
if (!this.info || !this.token) return;
|
|
1034
|
+
const { element, ...rest } = raw;
|
|
1035
|
+
const pick = this.masked(element) ? { ...rest, textSnapshot: "", selectors: rest.selectors.filter((s) => s.kind !== "text") } : rest;
|
|
1036
|
+
pick.url = window.location.href;
|
|
1037
|
+
if (this.info.purpose === "tag") pick.elementSnapshot = elementSnapshot(element, { mask: this.opts.maskSelectors.join(","), maskAll: false, featureAttribute: null });
|
|
1038
|
+
this.post({ type: "guidely:pick", sessionId: this.info.id, pick });
|
|
1039
|
+
void this.opts.api.post("/authoring/session/picks", pick, { params: { token: this.token } }).catch((err) => this.opts.onError(err));
|
|
883
1040
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
}
|
|
891
|
-
function areMutuallyExclusive(a, b) {
|
|
892
|
-
const la = a.config.frequency.mutuallyExclusiveWith;
|
|
893
|
-
const lb = b.config.frequency.mutuallyExclusiveWith;
|
|
894
|
-
return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);
|
|
895
|
-
}
|
|
896
|
-
function isWithinSchedule(guide, now = Date.now()) {
|
|
897
|
-
const s = guide.config.survey;
|
|
898
|
-
if (!s) return true;
|
|
899
|
-
if (s.startAt && now < Date.parse(s.startAt)) return false;
|
|
900
|
-
if (s.endAt && now > Date.parse(s.endAt)) return false;
|
|
901
|
-
return true;
|
|
902
|
-
}
|
|
903
|
-
function dismissedAfterMerge(local, server) {
|
|
904
|
-
if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
|
|
905
|
-
return local?.dismissedAt ?? server.dismissedAt ?? null;
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// src/health.ts
|
|
909
|
-
var MAX_RETRY_AFTER_MS = 60 * 60 * 1e3;
|
|
910
|
-
var OFFLINE_WAIT_MS = 3e4;
|
|
911
|
-
var ServerHealth = class {
|
|
912
|
-
constructor(opts = {}) {
|
|
913
|
-
__publicField(this, "failures", 0);
|
|
914
|
-
__publicField(this, "openUntil", 0);
|
|
915
|
-
__publicField(this, "lastOkAt", 0);
|
|
916
|
-
__publicField(this, "listeners", /* @__PURE__ */ new Set());
|
|
917
|
-
__publicField(this, "threshold");
|
|
918
|
-
__publicField(this, "baseMs");
|
|
919
|
-
__publicField(this, "maxMs");
|
|
920
|
-
__publicField(this, "now");
|
|
921
|
-
__publicField(this, "random");
|
|
922
|
-
this.threshold = opts.threshold ?? 3;
|
|
923
|
-
this.baseMs = opts.baseMs ?? 5e3;
|
|
924
|
-
this.maxMs = opts.maxMs ?? 5 * 60 * 1e3;
|
|
925
|
-
this.now = opts.now ?? Date.now;
|
|
926
|
-
this.random = opts.random ?? Math.random;
|
|
1041
|
+
/** Sends a snapshot now and again whenever the page settles after DOM changes or navigation. */
|
|
1042
|
+
watchPage() {
|
|
1043
|
+
this.scheduleSnapshot();
|
|
1044
|
+
if (typeof MutationObserver === "undefined") return;
|
|
1045
|
+
this.observer = new MutationObserver(() => this.scheduleSnapshot());
|
|
1046
|
+
this.observer.observe(document.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
927
1047
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
this.
|
|
932
|
-
this.
|
|
933
|
-
this.lastOkAt = this.now();
|
|
934
|
-
if (wasDown) this.notify();
|
|
1048
|
+
scheduleSnapshot() {
|
|
1049
|
+
const now = Date.now();
|
|
1050
|
+
if (!this.snapshotTimer) this.snapshotDue = now + SNAPSHOT_MAX_WAIT_MS;
|
|
1051
|
+
else clearTimeout(this.snapshotTimer);
|
|
1052
|
+
this.snapshotTimer = setTimeout(() => this.sendSnapshot(), Math.max(0, Math.min(SNAPSHOT_DEBOUNCE_MS, this.snapshotDue - now)));
|
|
935
1053
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
this.
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
return;
|
|
1054
|
+
sendSnapshot() {
|
|
1055
|
+
if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
|
|
1056
|
+
this.snapshotTimer = null;
|
|
1057
|
+
if (!this.info || !this.active) return;
|
|
1058
|
+
try {
|
|
1059
|
+
const snapshot = captureSnapshot(document, this.opts.maskSelectors);
|
|
1060
|
+
if (!snapshot || snapshot.html === this.lastSnapshot) return;
|
|
1061
|
+
this.lastSnapshot = snapshot.html;
|
|
1062
|
+
this.post({ type: "guidely:snapshot", sessionId: this.info.id, snapshot });
|
|
1063
|
+
} catch (err) {
|
|
1064
|
+
this.opts.onError(err);
|
|
943
1065
|
}
|
|
944
|
-
if (this.failures < this.threshold) return;
|
|
945
|
-
const cooldown = Math.min(this.maxMs, this.baseMs * 2 ** (this.failures - this.threshold));
|
|
946
|
-
this.openUntil = Math.max(this.openUntil, now + cooldown * (0.8 + this.random() * 0.4));
|
|
947
1066
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1067
|
+
post(message) {
|
|
1068
|
+
if (!this.info) return;
|
|
1069
|
+
try {
|
|
1070
|
+
window.opener?.postMessage(message, this.info.dashboardOrigin);
|
|
1071
|
+
} catch {
|
|
1072
|
+
}
|
|
952
1073
|
}
|
|
953
|
-
|
|
954
|
-
|
|
1074
|
+
stopPreview() {
|
|
1075
|
+
this.preview?.destroy();
|
|
1076
|
+
this.preview = null;
|
|
1077
|
+
this.previewGuide?.destroy();
|
|
1078
|
+
this.previewGuide = null;
|
|
1079
|
+
this.session?.setPicking(true);
|
|
955
1080
|
}
|
|
956
|
-
/**
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1081
|
+
/** Handles `message` events; ignores anything not from `dashboardOrigin`. */
|
|
1082
|
+
async handleMessage(e) {
|
|
1083
|
+
if (!this.active || !this.info) return;
|
|
1084
|
+
if (e.origin !== this.info.dashboardOrigin) return;
|
|
1085
|
+
const msg = e.data;
|
|
1086
|
+
if (!msg || typeof msg !== "object" || typeof msg.type !== "string") return;
|
|
1087
|
+
try {
|
|
1088
|
+
switch (msg.type) {
|
|
1089
|
+
case "guidely:preview-step": {
|
|
1090
|
+
this.stopPreview();
|
|
1091
|
+
const { StepSchema, GuideConfigSchema } = await loadSchemas();
|
|
1092
|
+
const step = StepSchema.parse(msg.step);
|
|
1093
|
+
const appearance = msg.appearance ? GuideConfigSchema.shape.appearance.parse(msg.appearance) : void 0;
|
|
1094
|
+
this.session?.setPicking(false);
|
|
1095
|
+
this.session?.setStatus(`Previewing step \u201C${step.title || step.id}\u201D \u2014 close it to continue picking`);
|
|
1096
|
+
this.preview = (0, import_guide_runtime3.previewStep)(step, { ...this.opts.theme(), ...msg.theme || {} }, {
|
|
1097
|
+
appearance,
|
|
1098
|
+
onEnd: () => {
|
|
1099
|
+
this.preview = null;
|
|
1100
|
+
this.session?.setPicking(true);
|
|
1101
|
+
this.session?.setStatus("Guidely authoring \u2014 hover an element and click to select \xB7 Esc to cancel");
|
|
1102
|
+
this.post({ type: "guidely:preview-ended", sessionId: this.info.id, reason: "step" });
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
case "guidely:preview-guide": {
|
|
1108
|
+
this.stopPreview();
|
|
1109
|
+
const g = msg.guide;
|
|
1110
|
+
const guide = { id: g.id || "preview", key: g.key || "preview", name: g.name || "Preview", kind: g.kind || "flow", version: g.version ?? 0, config: (await loadSchemas()).GuideConfigSchema.parse(g.config) };
|
|
1111
|
+
this.session?.setPicking(false);
|
|
1112
|
+
this.session?.setStatus(`Previewing \u201C${guide.name}\u201D`);
|
|
1113
|
+
this.previewGuide = (0, import_guide_runtime3.runGuide)(guide, {
|
|
1114
|
+
theme: this.opts.theme(),
|
|
1115
|
+
ctx: this.opts.ctx,
|
|
1116
|
+
onNavigate: (url, newTab) => newTab ? window.open(url, "_blank", "noopener") : window.location.assign(url),
|
|
1117
|
+
onEnd: (reason) => {
|
|
1118
|
+
this.previewGuide = null;
|
|
1119
|
+
this.session?.setPicking(true);
|
|
1120
|
+
this.post({ type: "guidely:preview-ended", sessionId: this.info.id, reason });
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
this.previewGuide.start();
|
|
1124
|
+
break;
|
|
1125
|
+
}
|
|
1126
|
+
case "guidely:highlight": {
|
|
1127
|
+
const result = (0, import_guide_runtime3.highlightSelectors)(Array.isArray(msg.selectors) ? msg.selectors : [], { durationMs: 3e3 });
|
|
1128
|
+
this.post({ type: "guidely:highlight-result", sessionId: this.info.id, counts: result.counts, resolved: !!result.resolved });
|
|
1129
|
+
break;
|
|
1130
|
+
}
|
|
1131
|
+
case "guidely:stop-preview":
|
|
1132
|
+
this.stopPreview();
|
|
1133
|
+
break;
|
|
1134
|
+
case "guidely:request-snapshot":
|
|
1135
|
+
this.lastSnapshot = "";
|
|
1136
|
+
this.sendSnapshot();
|
|
1137
|
+
break;
|
|
1138
|
+
case "guidely:ping":
|
|
1139
|
+
this.post({ type: "guidely:pong", sessionId: this.info.id, url: window.location.href });
|
|
1140
|
+
break;
|
|
1141
|
+
case "guidely:exit":
|
|
1142
|
+
this.stop();
|
|
1143
|
+
break;
|
|
1144
|
+
default:
|
|
1145
|
+
break;
|
|
1146
|
+
}
|
|
1147
|
+
} catch (err) {
|
|
1148
|
+
this.opts.onError(err);
|
|
1149
|
+
}
|
|
962
1150
|
}
|
|
963
|
-
/**
|
|
964
|
-
|
|
965
|
-
this.
|
|
966
|
-
this.
|
|
1151
|
+
/** Leaves authoring mode and clears the session marker. */
|
|
1152
|
+
stop() {
|
|
1153
|
+
if (!this.active) return;
|
|
1154
|
+
this.active = false;
|
|
1155
|
+
this.stopPreview();
|
|
1156
|
+
this.observer?.disconnect();
|
|
1157
|
+
this.observer = null;
|
|
1158
|
+
if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
|
|
1159
|
+
this.snapshotTimer = null;
|
|
1160
|
+
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
1161
|
+
this.heartbeat = null;
|
|
1162
|
+
this.session?.stop();
|
|
1163
|
+
this.session = null;
|
|
1164
|
+
this.opts.sessionStore.remove(KEYS.authoring);
|
|
1165
|
+
if (this.info) this.post({ type: "guidely:exit", sessionId: this.info.id });
|
|
1166
|
+
this.opts.onActive(false);
|
|
967
1167
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
// src/capture-token.ts
|
|
1171
|
+
var import_runtime9 = require("@guidelytech/shared/runtime");
|
|
1172
|
+
function takeCaptureToken(sessionStore) {
|
|
1173
|
+
if (typeof window === "undefined") return null;
|
|
1174
|
+
let token = null;
|
|
1175
|
+
let authoringLink = false;
|
|
1176
|
+
try {
|
|
1177
|
+
const url = new URL(window.location.href);
|
|
1178
|
+
token = url.searchParams.get(import_runtime9.CAPTURE_PARAM);
|
|
1179
|
+
authoringLink = url.searchParams.has(AUTHORING_PARAM);
|
|
1180
|
+
if (token) {
|
|
1181
|
+
url.searchParams.delete(import_runtime9.CAPTURE_PARAM);
|
|
1182
|
+
window.history.replaceState(window.history.state, "", url.toString());
|
|
1183
|
+
}
|
|
1184
|
+
} catch {
|
|
1185
|
+
token = null;
|
|
972
1186
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1187
|
+
const saved = readJson(sessionStore, KEYS.capture, null);
|
|
1188
|
+
let marker = null;
|
|
1189
|
+
if (token) marker = saved?.token === token ? saved : { token };
|
|
1190
|
+
else if (saved?.token && !authoringLink) marker = saved;
|
|
1191
|
+
if (!marker) {
|
|
1192
|
+
if (saved) {
|
|
1193
|
+
sessionStore.remove(KEYS.capture);
|
|
1194
|
+
sessionStore.remove(KEYS.capturePending);
|
|
979
1195
|
}
|
|
1196
|
+
return null;
|
|
980
1197
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
|
|
986
|
-
const at = Date.parse(trimmed);
|
|
987
|
-
return Number.isNaN(at) ? null : Math.max(0, at - now);
|
|
988
|
-
}
|
|
989
|
-
function isServerFailure(status) {
|
|
990
|
-
return status === 429 || status >= 500;
|
|
1198
|
+
if (token && saved?.token !== token) sessionStore.remove(KEYS.capturePending);
|
|
1199
|
+
writeJson(sessionStore, KEYS.capture, marker);
|
|
1200
|
+
sessionStore.remove(KEYS.authoring);
|
|
1201
|
+
return marker;
|
|
991
1202
|
}
|
|
992
1203
|
|
|
993
|
-
// src/
|
|
994
|
-
var
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1204
|
+
// src/load-capture.ts
|
|
1205
|
+
var loadCapture = async () => (await import("./capture.cjs")).capture;
|
|
1206
|
+
|
|
1207
|
+
// src/context.ts
|
|
1208
|
+
function detectDevice(ua, width) {
|
|
1209
|
+
const u = ua || "";
|
|
1210
|
+
let type = "desktop";
|
|
1211
|
+
if (/iPad|Tablet|PlayBook|Silk/i.test(u) || /Android/i.test(u) && !/Mobile/i.test(u)) type = "tablet";
|
|
1212
|
+
else if (/Mobi|iPhone|iPod|Android.*Mobile|Windows Phone/i.test(u)) type = "mobile";
|
|
1213
|
+
else if (width > 0 && width < 768) type = "mobile";
|
|
1214
|
+
else if (width > 0 && width < 1024) type = "tablet";
|
|
1215
|
+
let browser;
|
|
1216
|
+
if (/Electron\//i.test(u)) browser = "electron";
|
|
1217
|
+
else if (/Edg\//i.test(u)) browser = "edge";
|
|
1218
|
+
else if (/OPR\/|Opera/i.test(u)) browser = "opera";
|
|
1219
|
+
else if (/SamsungBrowser/i.test(u)) browser = "samsung";
|
|
1220
|
+
else if (/Chrome\/|CriOS/i.test(u)) browser = "chrome";
|
|
1221
|
+
else if (/Firefox\/|FxiOS/i.test(u)) browser = "firefox";
|
|
1222
|
+
else if (/Safari\//i.test(u) && /Version\//i.test(u)) browser = "safari";
|
|
1223
|
+
else if (/MSIE|Trident/i.test(u)) browser = "ie";
|
|
1224
|
+
let os;
|
|
1225
|
+
if (/Windows/i.test(u)) os = "windows";
|
|
1226
|
+
else if (/iPhone|iPad|iPod/i.test(u)) os = "ios";
|
|
1227
|
+
else if (/Mac OS X|Macintosh/i.test(u)) os = "macos";
|
|
1228
|
+
else if (/Android/i.test(u)) os = "android";
|
|
1229
|
+
else if (/CrOS/i.test(u)) os = "chromeos";
|
|
1230
|
+
else if (/Linux/i.test(u)) os = "linux";
|
|
1231
|
+
return { type, browser, os };
|
|
1232
|
+
}
|
|
1233
|
+
function buildContext(input) {
|
|
1234
|
+
let path = "";
|
|
1235
|
+
const query = {};
|
|
1236
|
+
try {
|
|
1237
|
+
const u = new URL(input.url, "http://localhost");
|
|
1238
|
+
path = u.pathname;
|
|
1239
|
+
u.searchParams.forEach((v, k) => {
|
|
1240
|
+
query[k] = v;
|
|
1241
|
+
});
|
|
1242
|
+
} catch {
|
|
1243
|
+
path = input.url;
|
|
1004
1244
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1245
|
+
const device = input.device ?? (typeof navigator !== "undefined" ? detectDevice(navigator.userAgent, typeof window !== "undefined" ? window.innerWidth : 0) : { type: "desktop" });
|
|
1246
|
+
return {
|
|
1247
|
+
anonymousId: input.anonymousId,
|
|
1248
|
+
userId: input.userId ?? null,
|
|
1249
|
+
userAttributes: input.userAttributes,
|
|
1250
|
+
accountAttributes: input.accountAttributes,
|
|
1251
|
+
url: input.url,
|
|
1252
|
+
path,
|
|
1253
|
+
query,
|
|
1254
|
+
device,
|
|
1255
|
+
firstVisit: input.firstVisit,
|
|
1256
|
+
eventsSeen: new Set(input.eventsSeen),
|
|
1257
|
+
completedGuides: new Set(input.completedGuides),
|
|
1258
|
+
dismissedGuides: new Set(input.dismissedGuides),
|
|
1259
|
+
seenGuides: new Set(input.seenGuides),
|
|
1260
|
+
completedChecklists: new Set(input.completedChecklists),
|
|
1261
|
+
audienceIds: new Set(input.audienceIds),
|
|
1262
|
+
now: input.now ?? /* @__PURE__ */ new Date(),
|
|
1263
|
+
environment: input.environment,
|
|
1264
|
+
locale: input.locale ?? (typeof navigator !== "undefined" ? navigator.language : void 0),
|
|
1265
|
+
platform: input.platform ?? "web",
|
|
1266
|
+
...input.screen !== void 0 ? { screen: input.screen } : {},
|
|
1267
|
+
...input.appVersion !== void 0 ? { appVersion: input.appVersion } : {}
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// src/heatmap.ts
|
|
1272
|
+
var HEATMAP_MAX_POINTS = 40;
|
|
1273
|
+
var HEATMAP_MAX_BYTES = 6e3;
|
|
1274
|
+
var HEATMAP_MAX_BINS = 60;
|
|
1275
|
+
var r2 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 100) / 100;
|
|
1276
|
+
function heatmapPoint(el, e, rage) {
|
|
1277
|
+
const p = { k: rage ? "r" : "c", x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
|
|
1278
|
+
const s = elementSelector(el).slice(0, 120);
|
|
1279
|
+
if (s) p.s = s;
|
|
1280
|
+
const rect = el.getBoundingClientRect();
|
|
1281
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
1282
|
+
p.rx = r2((e.clientX - rect.left) / rect.width);
|
|
1283
|
+
p.ry = r2((e.clientY - rect.top) / rect.height);
|
|
1010
1284
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1285
|
+
return p;
|
|
1286
|
+
}
|
|
1287
|
+
var selectors = /* @__PURE__ */ new WeakMap();
|
|
1288
|
+
var r1 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 10) / 10;
|
|
1289
|
+
function movementBin(el, e) {
|
|
1290
|
+
const p = { k: "m", x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
|
|
1291
|
+
let s = /^(HTML|BODY)$/.test(el.tagName) ? "" : selectors.get(el);
|
|
1292
|
+
if (s === void 0) selectors.set(el, s = elementSelector(el).slice(0, 120));
|
|
1293
|
+
const rect = s ? el.getBoundingClientRect() : null;
|
|
1294
|
+
if (!rect || !(rect.width > 0 && rect.height > 0)) return [`${p.x >> 4},${p.y >> 4}`, p];
|
|
1295
|
+
p.s = s;
|
|
1296
|
+
p.rx = r1((e.clientX - rect.left) / rect.width);
|
|
1297
|
+
p.ry = r1((e.clientY - rect.top) / rect.height);
|
|
1298
|
+
return [`${s} ${p.rx},${p.ry}`, p];
|
|
1299
|
+
}
|
|
1300
|
+
var HeatmapBuffer = class {
|
|
1301
|
+
constructor() {
|
|
1302
|
+
/** The page the buffered points belong to. */
|
|
1303
|
+
__publicField(this, "url", "");
|
|
1304
|
+
__publicField(this, "points", []);
|
|
1305
|
+
__publicField(this, "bins", /* @__PURE__ */ new Map());
|
|
1306
|
+
__publicField(this, "bytes", 0);
|
|
1014
1307
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1308
|
+
/** Adds a click (`burst` > 0 marks it and the burst's previous clicks as rage); true when the batch is full. */
|
|
1309
|
+
add(url, point, burst) {
|
|
1310
|
+
this.url = url;
|
|
1311
|
+
for (let i = Math.max(0, this.points.length - burst + 1); i < this.points.length; i++) this.points[i].k = "r";
|
|
1312
|
+
this.points.push(point);
|
|
1313
|
+
this.bytes += JSON.stringify(point).length + 1;
|
|
1314
|
+
return this.full();
|
|
1018
1315
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 1e4) : null;
|
|
1028
|
-
try {
|
|
1029
|
-
let res;
|
|
1030
|
-
try {
|
|
1031
|
-
res = await f(this.url(path, options.params), {
|
|
1032
|
-
method,
|
|
1033
|
-
headers,
|
|
1034
|
-
body: options.body === void 0 ? void 0 : form ? options.body : JSON.stringify(options.body),
|
|
1035
|
-
keepalive: options.keepalive,
|
|
1036
|
-
signal: ac?.signal,
|
|
1037
|
-
credentials: "omit",
|
|
1038
|
-
mode: "cors"
|
|
1039
|
-
});
|
|
1040
|
-
} catch (err) {
|
|
1041
|
-
this.observe(null);
|
|
1042
|
-
throw err;
|
|
1043
|
-
}
|
|
1044
|
-
this.observe(res);
|
|
1045
|
-
let data = null;
|
|
1046
|
-
if (res.status !== 304 && res.status !== 204) {
|
|
1047
|
-
try {
|
|
1048
|
-
data = await res.json();
|
|
1049
|
-
} catch {
|
|
1050
|
-
data = null;
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
return { status: res.status, ok: res.ok, data, etag: res.headers?.get?.("etag") ?? null };
|
|
1054
|
-
} finally {
|
|
1055
|
-
if (timer) clearTimeout(timer);
|
|
1316
|
+
/** Adds a movement sample to its bin (weight +1); true when the batch is full. */
|
|
1317
|
+
move(url, [key, point]) {
|
|
1318
|
+
this.url = url;
|
|
1319
|
+
const bin = this.bins.get(key);
|
|
1320
|
+
if (bin) bin.w = (bin.w ?? 1) + 1;
|
|
1321
|
+
else {
|
|
1322
|
+
this.bins.set(key, point);
|
|
1323
|
+
this.bytes += JSON.stringify(point).length + 8;
|
|
1056
1324
|
}
|
|
1325
|
+
return this.full();
|
|
1057
1326
|
}
|
|
1058
|
-
|
|
1059
|
-
return this.
|
|
1327
|
+
full() {
|
|
1328
|
+
return this.points.length >= HEATMAP_MAX_POINTS || this.bins.size > HEATMAP_MAX_BINS || this.bytes >= HEATMAP_MAX_BYTES;
|
|
1060
1329
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1330
|
+
/** Empties the buffer, returning what was in it (clicks first, then movement bins). */
|
|
1331
|
+
take() {
|
|
1332
|
+
const points = [...this.points, ...this.bins.values()];
|
|
1333
|
+
this.points = [];
|
|
1334
|
+
this.bins = /* @__PURE__ */ new Map();
|
|
1335
|
+
this.bytes = 0;
|
|
1336
|
+
return points.length ? { url: this.url, points } : null;
|
|
1063
1337
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1338
|
+
};
|
|
1339
|
+
|
|
1340
|
+
// src/errors.ts
|
|
1341
|
+
var import_runtime11 = require("@guidelytech/shared/runtime");
|
|
1342
|
+
var MAX_FRAMES_BYTES = 6e3;
|
|
1343
|
+
function fitFrames(frames) {
|
|
1344
|
+
const out = frames.slice(0, 30);
|
|
1345
|
+
while (out.length > 1 && JSON.stringify(out).length > MAX_FRAMES_BYTES) out.pop();
|
|
1346
|
+
return out;
|
|
1347
|
+
}
|
|
1348
|
+
function describe(value) {
|
|
1349
|
+
if (typeof value === "string") return value;
|
|
1350
|
+
try {
|
|
1351
|
+
return JSON.stringify(value) ?? String(value);
|
|
1352
|
+
} catch {
|
|
1353
|
+
return String(value);
|
|
1067
1354
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1355
|
+
}
|
|
1356
|
+
function exceptionFromError(error, source, handled, fallbackType = "Error") {
|
|
1357
|
+
const isError = error instanceof Error || !!error && typeof error === "object" && typeof error.message === "string" && typeof error.stack === "string";
|
|
1358
|
+
const e = error;
|
|
1359
|
+
return {
|
|
1360
|
+
type: isError && e.name || fallbackType,
|
|
1361
|
+
message: (isError ? e.message : describe(error)).slice(0, 1e3),
|
|
1362
|
+
frames: isError ? fitFrames((0, import_runtime11.parseStack)(e.stack)) : [],
|
|
1363
|
+
source,
|
|
1364
|
+
handled
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
function errorEventToException(e) {
|
|
1368
|
+
const target = e.target;
|
|
1369
|
+
if (target && typeof Element !== "undefined" && target instanceof Element) return null;
|
|
1370
|
+
if (e.error != null) return exceptionFromError(e.error, "onerror", false);
|
|
1371
|
+
const message = e.message || "";
|
|
1372
|
+
if (!message || /^script error\.?$/i.test(message.trim())) return null;
|
|
1373
|
+
const m = /^(?:Uncaught )?([A-Z]\w*(?:Error|Exception)):\s*([\s\S]*)$/.exec(message);
|
|
1374
|
+
return {
|
|
1375
|
+
type: m?.[1] ?? "Error",
|
|
1376
|
+
message: (m?.[2] ?? message).slice(0, 1e3),
|
|
1377
|
+
frames: e.filename ? [{ file: e.filename.slice(0, 500), line: e.lineno || void 0, col: e.colno || void 0 }] : [],
|
|
1378
|
+
source: "onerror",
|
|
1379
|
+
handled: false
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
function rejectionToException(e) {
|
|
1383
|
+
return exceptionFromError(e.reason, "unhandledrejection", false, "UnhandledRejection");
|
|
1384
|
+
}
|
|
1385
|
+
var GUIDELY_SCRIPT = /\/guidely(-recorder)?(\.min)?\.js$/i;
|
|
1386
|
+
function isGuidelyException(props) {
|
|
1387
|
+
if (props.message.startsWith("[Guidely]")) return true;
|
|
1388
|
+
const file = props.frames[0]?.file;
|
|
1389
|
+
if (!file) return false;
|
|
1390
|
+
try {
|
|
1391
|
+
return GUIDELY_SCRIPT.test(new URL(file).pathname);
|
|
1392
|
+
} catch {
|
|
1393
|
+
return GUIDELY_SCRIPT.test(file.replace(/[?#].*$/, ""));
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
var ExceptionThrottle = class {
|
|
1397
|
+
constructor(max = 5) {
|
|
1398
|
+
__publicField(this, "max", max);
|
|
1399
|
+
__publicField(this, "seen", /* @__PURE__ */ new Map());
|
|
1400
|
+
}
|
|
1401
|
+
allow(props) {
|
|
1402
|
+
const top = props.frames[0];
|
|
1403
|
+
const key = `${props.type}|${props.message}|${top ? `${top.file}:${top.line}:${top.col}` : ""}`;
|
|
1404
|
+
const n = this.seen.get(key) ?? 0;
|
|
1405
|
+
if (n >= this.max) return false;
|
|
1406
|
+
if (this.seen.size > 500) this.seen.clear();
|
|
1407
|
+
this.seen.set(key, n + 1);
|
|
1408
|
+
return true;
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
|
|
1412
|
+
// src/emitter.ts
|
|
1413
|
+
var Emitter = class {
|
|
1414
|
+
constructor(onError) {
|
|
1415
|
+
__publicField(this, "onError", onError);
|
|
1416
|
+
__publicField(this, "listeners", /* @__PURE__ */ new Map());
|
|
1417
|
+
}
|
|
1418
|
+
on(event, cb) {
|
|
1419
|
+
let set = this.listeners.get(event);
|
|
1420
|
+
if (!set) {
|
|
1421
|
+
set = /* @__PURE__ */ new Set();
|
|
1422
|
+
this.listeners.set(event, set);
|
|
1116
1423
|
}
|
|
1424
|
+
set.add(cb);
|
|
1425
|
+
return () => this.off(event, cb);
|
|
1117
1426
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1427
|
+
off(event, cb) {
|
|
1428
|
+
this.listeners.get(event)?.delete(cb);
|
|
1429
|
+
}
|
|
1430
|
+
emit(event, ...args) {
|
|
1431
|
+
const set = this.listeners.get(event);
|
|
1432
|
+
if (!set) return;
|
|
1433
|
+
for (const cb of Array.from(set)) {
|
|
1434
|
+
try {
|
|
1435
|
+
cb(...args);
|
|
1436
|
+
} catch (err) {
|
|
1437
|
+
this.onError?.(err);
|
|
1438
|
+
}
|
|
1126
1439
|
}
|
|
1127
1440
|
}
|
|
1441
|
+
clear() {
|
|
1442
|
+
this.listeners.clear();
|
|
1443
|
+
}
|
|
1128
1444
|
};
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1445
|
+
|
|
1446
|
+
// src/frequency.ts
|
|
1447
|
+
var HOUR = 3600 * 1e3;
|
|
1448
|
+
function isFrequencyEligible(freq, state, shownThisSession, now = Date.now()) {
|
|
1449
|
+
const s = state ?? { impressions: 0 };
|
|
1450
|
+
if (freq.maxImpressions != null && s.impressions >= freq.maxImpressions) return false;
|
|
1451
|
+
if (s.completedAt && freq.mode !== "every_time") return false;
|
|
1452
|
+
if (s.dismissedAt) {
|
|
1453
|
+
if (!freq.reshowAfterDismiss) return false;
|
|
1454
|
+
if (freq.cooldownHours > 0 && now - Date.parse(s.dismissedAt) < freq.cooldownHours * HOUR) return false;
|
|
1455
|
+
}
|
|
1456
|
+
switch (freq.mode) {
|
|
1457
|
+
case "once":
|
|
1458
|
+
return s.impressions === 0 || !!s.dismissedAt && freq.reshowAfterDismiss;
|
|
1459
|
+
case "once_per_session":
|
|
1460
|
+
return !shownThisSession;
|
|
1461
|
+
case "interval": {
|
|
1462
|
+
if (!s.lastShownAt) return true;
|
|
1463
|
+
const hours = freq.intervalHours ?? 24;
|
|
1464
|
+
return now - Date.parse(s.lastShownAt) >= hours * HOUR;
|
|
1139
1465
|
}
|
|
1140
|
-
|
|
1466
|
+
case "every_time":
|
|
1467
|
+
default:
|
|
1468
|
+
return true;
|
|
1141
1469
|
}
|
|
1142
|
-
return null;
|
|
1143
1470
|
}
|
|
1144
|
-
function
|
|
1145
|
-
|
|
1471
|
+
function isExclusiveGuide(guide) {
|
|
1472
|
+
if (guide.kind === "banner" || guide.kind === "checklist") return false;
|
|
1473
|
+
const steps = guide.config.steps;
|
|
1474
|
+
if (steps.length > 0 && steps.every((s) => s.type === "hotspot" || s.type === "banner")) return false;
|
|
1475
|
+
return true;
|
|
1146
1476
|
}
|
|
1147
|
-
|
|
1148
|
-
const
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
const field = i < 0 ? line : line.slice(0, i);
|
|
1158
|
-
const value = i < 0 ? "" : line.slice(i + 1).replace(/^ /, "");
|
|
1159
|
-
if (field === "event") event = value;
|
|
1160
|
-
else if (field === "data") data.push(value);
|
|
1161
|
-
}
|
|
1162
|
-
if (!data.length) return;
|
|
1163
|
-
try {
|
|
1164
|
-
onEvent(event, JSON.parse(data.join("\n")));
|
|
1165
|
-
} catch {
|
|
1166
|
-
}
|
|
1167
|
-
};
|
|
1168
|
-
for (; ; ) {
|
|
1169
|
-
const { done, value } = await reader.read();
|
|
1170
|
-
if (value) onChunk?.();
|
|
1171
|
-
if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
|
|
1172
|
-
let sep;
|
|
1173
|
-
while ((sep = buffer.indexOf("\n\n")) >= 0) {
|
|
1174
|
-
dispatch(buffer.slice(0, sep));
|
|
1175
|
-
buffer = buffer.slice(sep + 2);
|
|
1176
|
-
}
|
|
1177
|
-
if (done) break;
|
|
1477
|
+
function areMutuallyExclusive(a, b) {
|
|
1478
|
+
const la = a.config.frequency.mutuallyExclusiveWith;
|
|
1479
|
+
const lb = b.config.frequency.mutuallyExclusiveWith;
|
|
1480
|
+
return la.includes(b.id) || la.includes(b.key) || lb.includes(a.id) || lb.includes(a.key);
|
|
1481
|
+
}
|
|
1482
|
+
function isWithinSchedule(guide, now = Date.now()) {
|
|
1483
|
+
for (const w of [guide.config.schedule, guide.config.survey]) {
|
|
1484
|
+
if (!w) continue;
|
|
1485
|
+
if (w.startAt && now < Date.parse(w.startAt)) return false;
|
|
1486
|
+
if (w.endAt && now > Date.parse(w.endAt)) return false;
|
|
1178
1487
|
}
|
|
1179
|
-
|
|
1488
|
+
return true;
|
|
1489
|
+
}
|
|
1490
|
+
var MINUTE = 60 * 1e3;
|
|
1491
|
+
function countsTowardThrottle(guide) {
|
|
1492
|
+
return isExclusiveGuide(guide) && !guide.config.frequency.ignoreThrottling;
|
|
1493
|
+
}
|
|
1494
|
+
function isThrottleEligible(settings, launches, now = Date.now()) {
|
|
1495
|
+
if (!settings || !settings.enabled) return true;
|
|
1496
|
+
const times = launches.map((l) => Date.parse(l)).filter((t) => !Number.isNaN(t));
|
|
1497
|
+
const windowMs = Math.max(1, settings.windowHours) * HOUR;
|
|
1498
|
+
if (times.filter((t) => now - t < windowMs).length >= Math.max(1, settings.maxGuides)) return false;
|
|
1499
|
+
if (settings.minGapMinutes > 0 && times.some((t) => now - t < settings.minGapMinutes * MINUTE)) return false;
|
|
1500
|
+
return true;
|
|
1501
|
+
}
|
|
1502
|
+
function pruneLaunches(launches, settings, now = Date.now()) {
|
|
1503
|
+
const keepMs = Math.max(settings?.windowHours ?? 24, 1) * HOUR + (settings?.minGapMinutes ?? 0) * MINUTE;
|
|
1504
|
+
return launches.filter((l) => {
|
|
1505
|
+
const t = Date.parse(l);
|
|
1506
|
+
return !Number.isNaN(t) && now - t < keepMs;
|
|
1507
|
+
}).slice(-100);
|
|
1508
|
+
}
|
|
1509
|
+
function dismissedAfterMerge(local, server) {
|
|
1510
|
+
if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
|
|
1511
|
+
return local?.dismissedAt ?? server.dismissedAt ?? null;
|
|
1180
1512
|
}
|
|
1181
1513
|
|
|
1182
1514
|
// src/messenger-api.ts
|
|
@@ -1250,6 +1582,14 @@ function createMessengerTransport(opts) {
|
|
|
1250
1582
|
h.onDone(d.messages.at(-1) ?? null, d.conversation);
|
|
1251
1583
|
} else if (!finished) h.onError("The reply was interrupted");
|
|
1252
1584
|
}
|
|
1585
|
+
async function feedback(path, body, fallback) {
|
|
1586
|
+
const res = await api.post(`/feedback${path}`, { ...body, anonymousId: opts.anonymousId(), userId: opts.userId() });
|
|
1587
|
+
if (!res.ok || !res.data) {
|
|
1588
|
+
if (res.status === 429) throw new HttpError(429, "You're doing that a lot. Please try again in a minute.");
|
|
1589
|
+
throw new HttpError(res.status, serverMessage(res.data) ?? fallback);
|
|
1590
|
+
}
|
|
1591
|
+
return res.data;
|
|
1592
|
+
}
|
|
1253
1593
|
return {
|
|
1254
1594
|
async listConversations() {
|
|
1255
1595
|
const tokens = storedConversations(store, projectId).map((c) => c.token);
|
|
@@ -1286,7 +1626,8 @@ function createMessengerTransport(opts) {
|
|
|
1286
1626
|
return res.data;
|
|
1287
1627
|
},
|
|
1288
1628
|
async getArticle(id) {
|
|
1289
|
-
const
|
|
1629
|
+
const locale = opts.locale?.();
|
|
1630
|
+
const res = await api.get(`/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : void 0);
|
|
1290
1631
|
return res.ok && res.data ? res.data.article : null;
|
|
1291
1632
|
},
|
|
1292
1633
|
async fileComplaint(input) {
|
|
@@ -1316,13 +1657,25 @@ function createMessengerTransport(opts) {
|
|
|
1316
1657
|
async getHowTo(id) {
|
|
1317
1658
|
const res = await api.get(`/how-tos/${encodeURIComponent(id)}`);
|
|
1318
1659
|
return res.ok && res.data ? res.data.howTo : null;
|
|
1319
|
-
}
|
|
1660
|
+
},
|
|
1661
|
+
// Ideas (feature requests). The visitor's ids go in every body; the server decides what they may do.
|
|
1662
|
+
listIdeas: (query) => feedback("/requests/query", query, "Ideas couldn't be loaded."),
|
|
1663
|
+
getIdea: (id) => feedback(`/requests/${encodeURIComponent(id)}/view`, {}, "This idea couldn't be loaded."),
|
|
1664
|
+
submitIdea: (input) => feedback("/requests", { ...input, pageUrl: opts.pageUrl() }, "Your idea couldn't be sent. Please try again."),
|
|
1665
|
+
voteIdea: (id, vote) => feedback(`/requests/${encodeURIComponent(id)}/vote`, vote, "Your vote couldn't be saved."),
|
|
1666
|
+
unvoteIdea: (id) => feedback(`/requests/${encodeURIComponent(id)}/unvote`, {}, "Your vote couldn't be removed."),
|
|
1667
|
+
followIdea: (id, follow) => feedback(`/requests/${encodeURIComponent(id)}/follow`, { follow }, "That didn't work. Please try again."),
|
|
1668
|
+
commentIdea: (id, body) => feedback(`/requests/${encodeURIComponent(id)}/comments`, { body }, "Your comment couldn't be posted."),
|
|
1669
|
+
similarIdeas: async (title) => (await feedback("/similar", { title }, "Similar ideas couldn't be loaded.")).requests ?? [],
|
|
1670
|
+
ideaUpdates: () => feedback("/updates", {}, "Updates couldn't be loaded."),
|
|
1671
|
+
markIdeaUpdatesRead: (ids) => feedback("/updates/read", ids ? { ids } : {}, "Updates couldn't be marked read."),
|
|
1672
|
+
ideaPortalLink: async () => (await feedback("/portal-link", {}, "The feedback portal couldn't be opened.")).url
|
|
1320
1673
|
};
|
|
1321
1674
|
}
|
|
1322
1675
|
|
|
1323
1676
|
// src/messenger-visitor.ts
|
|
1324
|
-
var
|
|
1325
|
-
var
|
|
1677
|
+
var import_runtime15 = require("@guidelytech/shared/runtime");
|
|
1678
|
+
var import_guide_runtime4 = require("@guidelytech/guide-runtime");
|
|
1326
1679
|
var PAGE_GUIDE_KINDS = /* @__PURE__ */ new Set(["flow", "tooltip", "modal"]);
|
|
1327
1680
|
var CATCH_ALL = /* @__PURE__ */ new Set(["", "*", "**", "/*", "/**", "/"]);
|
|
1328
1681
|
var RESUME_WITHIN_MS = 14 * 864e5;
|
|
@@ -1339,7 +1692,7 @@ function targetsPage(guide, url) {
|
|
|
1339
1692
|
const c = guide.config;
|
|
1340
1693
|
const checks = [...c.urlPatterns, ...c.trigger.urlPattern ? [c.trigger.urlPattern] : [], ...c.trigger.type === "url_match" && c.trigger.value ? [c.trigger.value] : [], ...c.steps[0]?.url ? [c.steps[0].url] : []];
|
|
1341
1694
|
const specific = checks.filter((p) => !CATCH_ALL.has(p.trim()));
|
|
1342
|
-
return specific.length > 0 && checks.every((p) => (0,
|
|
1695
|
+
return specific.length > 0 && checks.every((p) => (0, import_guide_runtime4.urlMatches)(p, url));
|
|
1343
1696
|
}
|
|
1344
1697
|
function plainText(html, max = 280) {
|
|
1345
1698
|
const text = html.replace(/<(br|\/p|\/div|\/li|\/h[1-6])\s*\/?>/gi, " ").replace(/<[^>]*>/g, "").replace(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&").replace(/\s+/g, " ").trim();
|
|
@@ -1388,7 +1741,7 @@ function buildMessengerVisitor(input) {
|
|
|
1388
1741
|
unread: !seen.has(g.id) && !(state.guides[g.id]?.impressions ?? 0)
|
|
1389
1742
|
};
|
|
1390
1743
|
});
|
|
1391
|
-
return { firstName: (0,
|
|
1744
|
+
return { firstName: (0, import_runtime15.firstNameFromTraits)(input.traits), path: pathOf(input.url), pageGuides, resumeGuide, checklist, news };
|
|
1392
1745
|
}
|
|
1393
1746
|
|
|
1394
1747
|
// src/navigation.ts
|
|
@@ -1446,6 +1799,48 @@ function watchNavigation(listener, debounceMs = 50) {
|
|
|
1446
1799
|
};
|
|
1447
1800
|
}
|
|
1448
1801
|
|
|
1802
|
+
// src/page-url.ts
|
|
1803
|
+
var WEB_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "about", "data", "blob", "javascript"]);
|
|
1804
|
+
function appScheme(href) {
|
|
1805
|
+
let u;
|
|
1806
|
+
try {
|
|
1807
|
+
u = new URL(href);
|
|
1808
|
+
} catch {
|
|
1809
|
+
return null;
|
|
1810
|
+
}
|
|
1811
|
+
const scheme = u.protocol.replace(/:$/, "").toLowerCase();
|
|
1812
|
+
return WEB_SCHEMES.has(scheme) ? null : { u, scheme };
|
|
1813
|
+
}
|
|
1814
|
+
function normalizeAppUrl(href) {
|
|
1815
|
+
const app = appScheme(href);
|
|
1816
|
+
if (!app) return href;
|
|
1817
|
+
const { u, scheme } = app;
|
|
1818
|
+
const host = scheme === "file" ? "app" : u.host || "app";
|
|
1819
|
+
const hash = /^#!?(\/.*)$/.exec(u.hash);
|
|
1820
|
+
if (hash) {
|
|
1821
|
+
const route = hash[1];
|
|
1822
|
+
return `${scheme}://${host}${route.startsWith("/") ? route : `/${route}`}`;
|
|
1823
|
+
}
|
|
1824
|
+
let path = safeDecode(u.pathname);
|
|
1825
|
+
if (scheme === "file" || /\.html?$/i.test(path)) {
|
|
1826
|
+
const file = path.split("/").filter(Boolean).pop() ?? "";
|
|
1827
|
+
const name = file.replace(/\.html?$/i, "");
|
|
1828
|
+
path = !name || name.toLowerCase() === "index" ? "/" : `/${name}`;
|
|
1829
|
+
}
|
|
1830
|
+
if (!path.startsWith("/")) path = `/${path}`;
|
|
1831
|
+
return `${scheme}://${host}${path}${u.search}`;
|
|
1832
|
+
}
|
|
1833
|
+
function safeDecode(s) {
|
|
1834
|
+
try {
|
|
1835
|
+
return decodeURI(s);
|
|
1836
|
+
} catch {
|
|
1837
|
+
return s;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
function isAppUrl(href) {
|
|
1841
|
+
return appScheme(href) !== null;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1449
1844
|
// src/page-tracking.ts
|
|
1450
1845
|
function scrollDepth() {
|
|
1451
1846
|
const de = document.documentElement;
|
|
@@ -1491,89 +1886,105 @@ var PageTracker = class {
|
|
|
1491
1886
|
}
|
|
1492
1887
|
};
|
|
1493
1888
|
|
|
1494
|
-
// src/
|
|
1495
|
-
var
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
__publicField(this, "maxAgeMs");
|
|
1506
|
-
this.maxItems = opts.maxItems ?? 50;
|
|
1507
|
-
this.maxAgeMs = opts.maxAgeMs ?? 7 * 24 * 60 * 60 * 1e3;
|
|
1508
|
-
const stored = readJson(opts.store, opts.storageKey, []);
|
|
1509
|
-
const cutoff = Date.now() - this.maxAgeMs;
|
|
1510
|
-
this.items = Array.isArray(stored) ? stored.filter((i) => i && typeof i.path === "string" && typeof i.key === "string" && i.createdAt > cutoff).slice(-this.maxItems) : [];
|
|
1511
|
-
this.offRecover = opts.health?.onRecover(() => {
|
|
1512
|
-
this.attempt = 0;
|
|
1513
|
-
this.retryIn(0);
|
|
1514
|
-
}) ?? null;
|
|
1515
|
-
if (this.items.length) this.retryIn(0);
|
|
1516
|
-
}
|
|
1517
|
-
get size() {
|
|
1518
|
-
return this.items.length;
|
|
1519
|
-
}
|
|
1520
|
-
/** Queues `POST path` and sends it now (or as soon as the API is reachable). */
|
|
1521
|
-
add(key, path, body) {
|
|
1522
|
-
if (this.destroyed) return;
|
|
1523
|
-
this.items = this.items.filter((i) => i.key !== key);
|
|
1524
|
-
this.items.push({ key, path, body, createdAt: Date.now() });
|
|
1525
|
-
if (this.items.length > this.maxItems) this.items.splice(0, this.items.length - this.maxItems);
|
|
1526
|
-
this.persist();
|
|
1527
|
-
void this.flush();
|
|
1528
|
-
}
|
|
1529
|
-
persist() {
|
|
1530
|
-
writeJson(this.opts.store, this.opts.storageKey, this.items);
|
|
1889
|
+
// src/prototype-overlay.ts
|
|
1890
|
+
var PROTOTYPE_CLOSE_MESSAGE = "guidely:prototype:close";
|
|
1891
|
+
var TOKEN = /^[A-Za-z0-9_-]{16,64}$/;
|
|
1892
|
+
var OVERLAY_ATTR = "data-guidely-prototype-overlay";
|
|
1893
|
+
function matchPrototypeTestUrl(urlOrToken, apiUrl) {
|
|
1894
|
+
const value = (urlOrToken || "").trim();
|
|
1895
|
+
let api;
|
|
1896
|
+
try {
|
|
1897
|
+
api = new URL(apiUrl);
|
|
1898
|
+
} catch {
|
|
1899
|
+
return null;
|
|
1531
1900
|
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
}, ms);
|
|
1901
|
+
if (TOKEN.test(value)) return new URL(`/validate/${value}`, api.origin);
|
|
1902
|
+
let url;
|
|
1903
|
+
try {
|
|
1904
|
+
url = new URL(value, typeof location !== "undefined" ? location.href : api.origin);
|
|
1905
|
+
} catch {
|
|
1906
|
+
return null;
|
|
1539
1907
|
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1908
|
+
if (url.origin !== api.origin) return null;
|
|
1909
|
+
const m = /^\/validate\/([^/]+)\/?$/.exec(url.pathname);
|
|
1910
|
+
if (!m || !TOKEN.test(m[1])) return null;
|
|
1911
|
+
return new URL(`/validate/${m[1]}`, api.origin);
|
|
1912
|
+
}
|
|
1913
|
+
var current = null;
|
|
1914
|
+
function openPrototypeOverlay(testUrl, opts) {
|
|
1915
|
+
current?.close();
|
|
1916
|
+
const doc = document;
|
|
1917
|
+
const previous = doc.activeElement;
|
|
1918
|
+
const src = new URL(testUrl.href);
|
|
1919
|
+
const hash = new URLSearchParams();
|
|
1920
|
+
hash.set("key", opts.key);
|
|
1921
|
+
if (opts.anonymousId) hash.set("anon", opts.anonymousId);
|
|
1922
|
+
if (opts.userId) hash.set("uid", opts.userId);
|
|
1923
|
+
src.hash = hash.toString();
|
|
1924
|
+
const root = doc.createElement("div");
|
|
1925
|
+
root.setAttribute(OVERLAY_ATTR, "");
|
|
1926
|
+
root.setAttribute("role", "dialog");
|
|
1927
|
+
root.setAttribute("aria-modal", "true");
|
|
1928
|
+
root.setAttribute("aria-label", "Prototype test");
|
|
1929
|
+
root.style.cssText = `position:fixed;inset:0;z-index:${opts.zIndex ?? 2147483646};background:rgba(15,23,42,.55);display:flex;align-items:stretch;justify-content:center;padding:0;margin:0;`;
|
|
1930
|
+
const frame = doc.createElement("iframe");
|
|
1931
|
+
frame.src = src.toString();
|
|
1932
|
+
frame.title = "Prototype test";
|
|
1933
|
+
frame.setAttribute("allow", "fullscreen");
|
|
1934
|
+
frame.setAttribute("referrerpolicy", "no-referrer");
|
|
1935
|
+
frame.style.cssText = "border:0;width:100%;height:100%;background:#fff;display:block;";
|
|
1936
|
+
const close = doc.createElement("button");
|
|
1937
|
+
close.type = "button";
|
|
1938
|
+
close.setAttribute("aria-label", "Close prototype test");
|
|
1939
|
+
close.textContent = "\xD7";
|
|
1940
|
+
close.style.cssText = "position:absolute;top:10px;right:10px;width:36px;height:36px;border-radius:999px;border:0;background:rgba(15,23,42,.75);color:#fff;font:600 22px/36px system-ui,sans-serif;text-align:center;cursor:pointer;padding:0;z-index:1;";
|
|
1941
|
+
root.append(frame, close);
|
|
1942
|
+
const overflow = doc.documentElement.style.overflow;
|
|
1943
|
+
doc.documentElement.style.overflow = "hidden";
|
|
1944
|
+
let closed = false;
|
|
1945
|
+
const shut = () => {
|
|
1946
|
+
if (closed) return;
|
|
1947
|
+
closed = true;
|
|
1948
|
+
window.removeEventListener("message", onMessage);
|
|
1949
|
+
doc.removeEventListener("keydown", onKey, true);
|
|
1950
|
+
doc.removeEventListener("focusin", onFocus, true);
|
|
1951
|
+
root.remove();
|
|
1952
|
+
doc.documentElement.style.overflow = overflow;
|
|
1953
|
+
if (current === overlay) current = null;
|
|
1546
1954
|
try {
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
let retry = false;
|
|
1550
|
-
try {
|
|
1551
|
-
const res = await this.opts.api.post(item.path, item.body);
|
|
1552
|
-
retry = !res.ok && (res.status === 429 || res.status >= 500);
|
|
1553
|
-
} catch (err) {
|
|
1554
|
-
retry = true;
|
|
1555
|
-
this.opts.onError?.(err);
|
|
1556
|
-
}
|
|
1557
|
-
if (retry) {
|
|
1558
|
-
this.attempt += 1;
|
|
1559
|
-
this.retryIn(Math.max(backoffMs(this.attempt - 1, 1e3, 6e4), this.opts.health?.waitMs() ?? 0));
|
|
1560
|
-
return;
|
|
1561
|
-
}
|
|
1562
|
-
this.attempt = 0;
|
|
1563
|
-
this.items = this.items.filter((i) => i !== item);
|
|
1564
|
-
this.persist();
|
|
1565
|
-
}
|
|
1566
|
-
} finally {
|
|
1567
|
-
this.sending = false;
|
|
1955
|
+
previous?.focus?.();
|
|
1956
|
+
} catch {
|
|
1568
1957
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1958
|
+
opts.onClose?.();
|
|
1959
|
+
};
|
|
1960
|
+
const onMessage = (e) => {
|
|
1961
|
+
if (e.origin !== testUrl.origin || e.source !== frame.contentWindow) return;
|
|
1962
|
+
const data = e.data;
|
|
1963
|
+
if (data && typeof data === "object" && data.type === PROTOTYPE_CLOSE_MESSAGE) shut();
|
|
1964
|
+
};
|
|
1965
|
+
const onKey = (e) => {
|
|
1966
|
+
if (e.key === "Escape") {
|
|
1967
|
+
e.preventDefault();
|
|
1968
|
+
e.stopPropagation();
|
|
1969
|
+
shut();
|
|
1970
|
+
} else if (e.key === "Tab") {
|
|
1971
|
+
e.preventDefault();
|
|
1972
|
+
(doc.activeElement === close ? frame : close).focus();
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
1975
|
+
const onFocus = (e) => {
|
|
1976
|
+
if (!root.contains(e.target)) close.focus();
|
|
1977
|
+
};
|
|
1978
|
+
close.addEventListener("click", shut);
|
|
1979
|
+
window.addEventListener("message", onMessage);
|
|
1980
|
+
doc.addEventListener("keydown", onKey, true);
|
|
1981
|
+
doc.addEventListener("focusin", onFocus, true);
|
|
1982
|
+
doc.body.appendChild(root);
|
|
1983
|
+
frame.focus();
|
|
1984
|
+
const overlay = { element: root, close: shut };
|
|
1985
|
+
current = overlay;
|
|
1986
|
+
return overlay;
|
|
1987
|
+
}
|
|
1577
1988
|
|
|
1578
1989
|
// src/queue.ts
|
|
1579
1990
|
var EventQueue = class {
|
|
@@ -1707,7 +2118,7 @@ var loadRecorder = async () => (await import("./recorder.cjs")).recorder;
|
|
|
1707
2118
|
|
|
1708
2119
|
// src/client.ts
|
|
1709
2120
|
var SDK_NAME = "@guidelytech/sdk-core";
|
|
1710
|
-
var SDK_VERSION = "0.
|
|
2121
|
+
var SDK_VERSION = "0.12.0";
|
|
1711
2122
|
var DEFAULT_API_URL = "https://api.guidely.app";
|
|
1712
2123
|
var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
|
|
1713
2124
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -1733,7 +2144,7 @@ function emptyState() {
|
|
|
1733
2144
|
return { guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] };
|
|
1734
2145
|
}
|
|
1735
2146
|
function newSession(now = Date.now()) {
|
|
1736
|
-
return { id: `s_${(0,
|
|
2147
|
+
return { id: `s_${(0, import_runtime18.randomId)(16)}`, startedAt: new Date(now).toISOString(), shownGuides: [], events: [], lastActivityAt: now };
|
|
1737
2148
|
}
|
|
1738
2149
|
var GuidelyClient = class _GuidelyClient {
|
|
1739
2150
|
constructor(options) {
|
|
@@ -1745,6 +2156,8 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1745
2156
|
__publicField(this, "queue");
|
|
1746
2157
|
/** Survey answers and guide/checklist progress, kept until the server accepts them. */
|
|
1747
2158
|
__publicField(this, "outbox");
|
|
2159
|
+
/** Agent Analytics turns (agentConversation), in their own outbox. */
|
|
2160
|
+
__publicField(this, "agentTurns");
|
|
1748
2161
|
__publicField(this, "config", null);
|
|
1749
2162
|
__publicField(this, "etag", null);
|
|
1750
2163
|
__publicField(this, "anonymousId");
|
|
@@ -1794,6 +2207,10 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1794
2207
|
__publicField(this, "flags");
|
|
1795
2208
|
/** Experiments already reported this session (`experiment_viewed` is sent once per session). */
|
|
1796
2209
|
__publicField(this, "exposures", /* @__PURE__ */ new Set());
|
|
2210
|
+
/** Language set with `setLocale()`. */
|
|
2211
|
+
__publicField(this, "locale", null);
|
|
2212
|
+
/** Language each shown guide was rendered in; added to its events as `locale`. */
|
|
2213
|
+
__publicField(this, "guideLocales", /* @__PURE__ */ new Map());
|
|
1797
2214
|
__publicField(this, "customListeners", /* @__PURE__ */ new Map());
|
|
1798
2215
|
__publicField(this, "eventBus", {
|
|
1799
2216
|
on: (name, cb) => {
|
|
@@ -1828,7 +2245,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1828
2245
|
this.sessionStore = this.options.storage === "memory" ? this.store : createStorage("session");
|
|
1829
2246
|
this.anonymousId = this.store.get(KEYS.anon) || "";
|
|
1830
2247
|
if (!this.anonymousId) {
|
|
1831
|
-
this.anonymousId = `anon_${(0,
|
|
2248
|
+
this.anonymousId = `anon_${(0, import_runtime18.randomId)(20)}`;
|
|
1832
2249
|
this.store.set(KEYS.anon, this.anonymousId);
|
|
1833
2250
|
}
|
|
1834
2251
|
this.firstVisit = !this.store.get(KEYS.seen);
|
|
@@ -1838,7 +2255,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1838
2255
|
const session = readJson(this.sessionStore, KEYS.session, null);
|
|
1839
2256
|
this.session = session && session.id ? session : newSession();
|
|
1840
2257
|
this.persistSession();
|
|
1841
|
-
this.lastUrl =
|
|
2258
|
+
this.lastUrl = this.pageHref();
|
|
1842
2259
|
this.lastAudiences = new Set(this.state.audienceIds);
|
|
1843
2260
|
this.queue = new EventQueue({
|
|
1844
2261
|
store: this.store,
|
|
@@ -1853,6 +2270,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1853
2270
|
onError: (err) => this.reportError(err)
|
|
1854
2271
|
});
|
|
1855
2272
|
this.outbox = new Outbox({ api: this.api, store: this.store, storageKey: KEYS.outbox(this.options.projectId), health: this.api.health });
|
|
2273
|
+
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 } });
|
|
1856
2274
|
if (options.user) this.user = { ...options.user };
|
|
1857
2275
|
this.flags = this.createFlags(options.flags);
|
|
1858
2276
|
}
|
|
@@ -1929,7 +2347,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1929
2347
|
this.resumeActiveGuide();
|
|
1930
2348
|
this.evaluate("ready");
|
|
1931
2349
|
if (!this.options.disableAuthoring && !capturing) this.maybeStartAuthoring();
|
|
1932
|
-
this.pageTracker.start(
|
|
2350
|
+
this.pageTracker.start(this.pageHref());
|
|
1933
2351
|
this.touchSession();
|
|
1934
2352
|
this.syncCapture();
|
|
1935
2353
|
} catch (err) {
|
|
@@ -1962,7 +2380,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1962
2380
|
wireListeners() {
|
|
1963
2381
|
const doc = document;
|
|
1964
2382
|
const win = window;
|
|
1965
|
-
this.cleanups.push(watchNavigation((url) => this.handleNavigation(url)));
|
|
2383
|
+
this.cleanups.push(watchNavigation((url) => this.handleNavigation(this.pageHref(url))));
|
|
1966
2384
|
const onClick = (e) => this.handleDocumentClick(e);
|
|
1967
2385
|
doc.addEventListener("click", onClick, true);
|
|
1968
2386
|
this.cleanups.push(() => doc.removeEventListener("click", onClick, true));
|
|
@@ -1983,7 +2401,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1983
2401
|
const onPageShow = (e) => {
|
|
1984
2402
|
if (!e.persisted || !this.pageLeft) return;
|
|
1985
2403
|
this.pageLeft = false;
|
|
1986
|
-
this.pageTracker.start(
|
|
2404
|
+
this.pageTracker.start(this.pageHref());
|
|
1987
2405
|
};
|
|
1988
2406
|
let lastScroll = 0;
|
|
1989
2407
|
const onScroll = () => {
|
|
@@ -2032,7 +2450,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2032
2450
|
this.cleanups.push(() => clearInterval(refresh));
|
|
2033
2451
|
const poll = setInterval(() => {
|
|
2034
2452
|
if (!this.isReady || !this.config || this.editing() || doc.visibilityState === "hidden") return;
|
|
2035
|
-
const url =
|
|
2453
|
+
const url = this.pageHref();
|
|
2036
2454
|
const ctx = this.getContext();
|
|
2037
2455
|
for (const guide of this.config.guides) {
|
|
2038
2456
|
if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
|
|
@@ -2101,10 +2519,11 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2101
2519
|
this.queue.flushSync();
|
|
2102
2520
|
this.queue.destroy();
|
|
2103
2521
|
this.outbox.destroy();
|
|
2522
|
+
this.agentTurns.destroy();
|
|
2104
2523
|
this.flags.destroy();
|
|
2105
2524
|
this.emitter.clear();
|
|
2106
|
-
(0,
|
|
2107
|
-
if (
|
|
2525
|
+
(0, import_guide_runtime5.destroyHost)();
|
|
2526
|
+
if (current2 === this) current2 = null;
|
|
2108
2527
|
}
|
|
2109
2528
|
// ---------------------------------------------------------------------------
|
|
2110
2529
|
// Identity
|
|
@@ -2116,13 +2535,13 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2116
2535
|
this.user = { id: String(userId), traits: { ...this.user?.id === String(userId) ? this.user.traits : {}, ...traits || {} }, account: account ?? (this.user?.id === String(userId) ? this.user.account : void 0) };
|
|
2117
2536
|
writeJson(this.store, KEYS.user(this.options.projectId), this.user);
|
|
2118
2537
|
this.syncFlagAttributes();
|
|
2119
|
-
this.enqueue("identify", { properties: { traits: (0,
|
|
2538
|
+
this.enqueue("identify", { properties: { traits: (0, import_runtime18.sanitizeProperties)(this.user.traits) } });
|
|
2120
2539
|
await this.syncIdentify();
|
|
2121
2540
|
this.emitter.emit("identify", { userId: this.user.id, anonymousId: this.anonymousId });
|
|
2122
2541
|
if (this.isReady) this.evaluate("identify");
|
|
2123
2542
|
}
|
|
2124
2543
|
async syncIdentify() {
|
|
2125
|
-
const body = { anonymousId: this.anonymousId, userId: this.user?.id ?? null, traits: this.user?.traits, account: this.user?.account ? { id: this.user.account.id, traits: this.user.account.traits } : null, sessionId: this.session.id, url: this.collectUrls() ?
|
|
2544
|
+
const body = { anonymousId: this.anonymousId, userId: this.user?.id ?? null, traits: this.user?.traits, account: this.user?.account ? { id: this.user.account.id, traits: this.user.account.traits } : null, sessionId: this.session.id, url: this.collectUrls() ? this.pageHref() : void 0 };
|
|
2126
2545
|
const res = await postWithRetry(this.api, "/identify", body, 2);
|
|
2127
2546
|
if (res?.ok && res.data) this.mergeServerState(res.data);
|
|
2128
2547
|
}
|
|
@@ -2153,7 +2572,10 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2153
2572
|
checklists,
|
|
2154
2573
|
surveysSubmitted: Array.from(/* @__PURE__ */ new Set([...this.state.surveysSubmitted, ...data.surveysSubmitted || []])),
|
|
2155
2574
|
audienceIds: data.audienceIds || [],
|
|
2156
|
-
firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt
|
|
2575
|
+
firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt,
|
|
2576
|
+
// Device-local only (the server doesn't hold them).
|
|
2577
|
+
...this.state.throttle ? { throttle: this.state.throttle } : {},
|
|
2578
|
+
...this.state.announcementsSeenAt ? { announcementsSeenAt: this.state.announcementsSeenAt } : {}
|
|
2157
2579
|
};
|
|
2158
2580
|
if (data.firstSeenAt && Date.now() - Date.parse(data.firstSeenAt) > 6e4) this.firstVisit = false;
|
|
2159
2581
|
this.persistState();
|
|
@@ -2189,7 +2611,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2189
2611
|
this.stopGuide();
|
|
2190
2612
|
this.user = null;
|
|
2191
2613
|
this.store.remove(KEYS.user(this.options.projectId));
|
|
2192
|
-
this.anonymousId = `anon_${(0,
|
|
2614
|
+
this.anonymousId = `anon_${(0, import_runtime18.randomId)(20)}`;
|
|
2193
2615
|
this.store.set(KEYS.anon, this.anonymousId);
|
|
2194
2616
|
this.state = emptyState();
|
|
2195
2617
|
this.persistState();
|
|
@@ -2221,6 +2643,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2221
2643
|
if (consent === "denied") this.queue.clear();
|
|
2222
2644
|
else if (consent === "granted") void this.queue.flush();
|
|
2223
2645
|
if (consent !== "granted") this.replay?.stop({ discard: true });
|
|
2646
|
+
if (consent !== "pending") this.agentTurns.consentChanged(consent === "granted");
|
|
2224
2647
|
this.syncCapture();
|
|
2225
2648
|
}
|
|
2226
2649
|
analyticsEnabled() {
|
|
@@ -2233,17 +2656,19 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2233
2656
|
if (this.consent === "denied" || !this.options.analytics) return null;
|
|
2234
2657
|
if (name !== "page_leave" && name !== "heatmap") this.touchSession();
|
|
2235
2658
|
const event = {
|
|
2236
|
-
eventId: (0,
|
|
2659
|
+
eventId: (0, import_runtime18.randomId)(24),
|
|
2237
2660
|
name,
|
|
2238
2661
|
occurredAt: nowIso(),
|
|
2239
2662
|
anonymousId: this.anonymousId,
|
|
2240
2663
|
userId: this.user?.id || null,
|
|
2241
2664
|
accountId: this.user?.account?.id || null,
|
|
2242
2665
|
sessionId: this.session.id,
|
|
2243
|
-
url: this.collectUrls() ?
|
|
2666
|
+
url: this.collectUrls() ? this.pageHref() : void 0,
|
|
2244
2667
|
...extra
|
|
2245
2668
|
};
|
|
2246
|
-
if (extra.properties) event.properties = (0,
|
|
2669
|
+
if (extra.properties) event.properties = (0, import_runtime18.sanitizeProperties)(extra.properties);
|
|
2670
|
+
const locale = extra.guideId ? this.guideLocales.get(extra.guideId) : void 0;
|
|
2671
|
+
if (locale) event.properties = { ...event.properties ?? {}, locale };
|
|
2247
2672
|
this.queue.enqueue(event);
|
|
2248
2673
|
this.replay?.addCustomEvent("guidely", { name, customName: event.customName, guideId: event.guideId });
|
|
2249
2674
|
this.emitter.emit("track", event);
|
|
@@ -2267,9 +2692,30 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2267
2692
|
this.completeChecklistTasksBy("event", name);
|
|
2268
2693
|
if (this.isReady) this.evaluate("event", { eventName: name });
|
|
2269
2694
|
}
|
|
2695
|
+
/**
|
|
2696
|
+
* Agent Analytics: records one or more turns of a conversation with your own AI agent or chatbot
|
|
2697
|
+
* (`{ conversationId, agentId, role, text, toolCalls?, latencyMs?, outcome?, … }`). The visitor's
|
|
2698
|
+
* anonymous id, user id, account id and session id are filled in, `agents.redact` from the init
|
|
2699
|
+
* options runs on each turn, and turns are retried until Guidely accepts them. Returns false when
|
|
2700
|
+
* nothing was queued (analytics off or consent denied, or every turn was invalid or dropped).
|
|
2701
|
+
*/
|
|
2702
|
+
agentConversation(turns) {
|
|
2703
|
+
if (this.destroyed || !this.options.analytics || this.consent === "denied" || this.config?.settings?.analyticsEnabled === false) return false;
|
|
2704
|
+
const identity = { anonymousId: this.anonymousId, userId: this.user?.id || null, accountId: this.user?.account?.id || null, sessionId: this.session.id };
|
|
2705
|
+
const prepared = (Array.isArray(turns) ? turns : [turns]).map((t) => prepareAgentTurn(t, identity, this.options.agents?.redact, (err) => this.reportError(err))).filter((t) => !!t);
|
|
2706
|
+
if (!prepared.length) return false;
|
|
2707
|
+
this.touchSession();
|
|
2708
|
+
this.agentTurns.add(prepared, this.consent === "pending");
|
|
2709
|
+
return true;
|
|
2710
|
+
}
|
|
2711
|
+
/** Alias of {@link agentConversation}. */
|
|
2712
|
+
trackAgentTurn(turns) {
|
|
2713
|
+
return this.agentConversation(turns);
|
|
2714
|
+
}
|
|
2270
2715
|
/** Records a page view and re-evaluates triggers. Called automatically on SPA navigation. */
|
|
2271
|
-
page(
|
|
2716
|
+
page(href) {
|
|
2272
2717
|
if (this.destroyed) return;
|
|
2718
|
+
const url = this.pageHref(href);
|
|
2273
2719
|
this.leavePage(url);
|
|
2274
2720
|
this.trackPageView(url);
|
|
2275
2721
|
this.handleNavigation(url, true);
|
|
@@ -2289,13 +2735,13 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2289
2735
|
const st = this.active.controller.getState();
|
|
2290
2736
|
if (st.status === "running" && !this.active.manual) {
|
|
2291
2737
|
const g = this.active.guide;
|
|
2292
|
-
if (g.config.urlPatterns.length && !g.config.urlPatterns.some((p) => (0,
|
|
2738
|
+
if (g.config.urlPatterns.length && !g.config.urlPatterns.some((p) => (0, import_guide_runtime5.urlMatches)(p, url))) this.active.controller.destroy();
|
|
2293
2739
|
}
|
|
2294
2740
|
}
|
|
2295
2741
|
this.widgets.forEach((w, id) => {
|
|
2296
2742
|
if (!w.controller || w.manual) return;
|
|
2297
2743
|
const g = this.findGuide(id);
|
|
2298
|
-
if (g?.config.urlPatterns.length && !g.config.urlPatterns.some((p) => (0,
|
|
2744
|
+
if (g?.config.urlPatterns.length && !g.config.urlPatterns.some((p) => (0, import_guide_runtime5.urlMatches)(p, url))) w.controller.destroy();
|
|
2299
2745
|
});
|
|
2300
2746
|
this.firedDelays.clear();
|
|
2301
2747
|
if (this.isReady) this.evaluate("navigation");
|
|
@@ -2303,7 +2749,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2303
2749
|
if (changed) void this.queue.flush();
|
|
2304
2750
|
}
|
|
2305
2751
|
handleDocumentClick(e) {
|
|
2306
|
-
const target = (0,
|
|
2752
|
+
const target = (0, import_guide_runtime5.composedTarget)(e);
|
|
2307
2753
|
if (!target || typeof target.closest !== "function") return;
|
|
2308
2754
|
this.autocaptureClick(e, target);
|
|
2309
2755
|
this.completeChecklistTasksBy("click", target);
|
|
@@ -2361,6 +2807,8 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2361
2807
|
const ref = !this.session.pageViews ? externalReferrer() : void 0;
|
|
2362
2808
|
if (ref) props.referrer = ref;
|
|
2363
2809
|
}
|
|
2810
|
+
const ep = this.captureAllowed() ? pagePropertyValues(this.config?.tagging, url, { mask: this.maskSelector(), maskAll: this.maskAllText() }) : void 0;
|
|
2811
|
+
if (ep) props.ep = ep;
|
|
2364
2812
|
this.session.pageViews = (this.session.pageViews ?? 0) + 1;
|
|
2365
2813
|
this.persistSession();
|
|
2366
2814
|
this.enqueue("page_view", { url: collect ? url : void 0, properties: props });
|
|
@@ -2386,21 +2834,28 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2386
2834
|
const heat = this.autocaptureOn("heatmaps");
|
|
2387
2835
|
if (!clicks && !rage && !heat || isIgnoredElement(target)) return;
|
|
2388
2836
|
this.touchSession();
|
|
2389
|
-
const
|
|
2837
|
+
const featureAttribute = this.config?.tagging?.featureAttribute ?? null;
|
|
2838
|
+
const feature = featureAttributeOf(target, featureAttribute);
|
|
2839
|
+
const el = interactiveElement(target) ?? feature?.el ?? null;
|
|
2390
2840
|
const opts = { mask: this.maskSelector(), maskAll: this.maskAllText(), collectUrls: this.collectUrls() };
|
|
2391
|
-
|
|
2841
|
+
const clickProps = (on) => {
|
|
2842
|
+
const p = describeClick(on, e, opts);
|
|
2843
|
+
const ep = clickPropertyValues(this.config?.tagging, on, p, opts);
|
|
2844
|
+
return { ...p, el: elementSnapshot(on, { ...opts, featureAttribute }), ...feature ? { fa: feature.value } : {}, ...ep ? { ep } : {} };
|
|
2845
|
+
};
|
|
2846
|
+
if (clicks && el) this.enqueue("click", { properties: clickProps(el) });
|
|
2392
2847
|
const count = this.rage.add(e.pageX ?? 0, e.pageY ?? 0, Date.now());
|
|
2393
|
-
if (rage && count) this.enqueue("rage_click", { properties: { ...
|
|
2848
|
+
if (rage && count) this.enqueue("rage_click", { properties: { ...clickProps(el ?? target), count } });
|
|
2394
2849
|
if (!heat) return;
|
|
2395
|
-
const url =
|
|
2850
|
+
const url = this.pageHref();
|
|
2396
2851
|
if (this.heatmap.url !== url) this.flushHeatmap();
|
|
2397
2852
|
if (this.heatmap.add(url, heatmapPoint(el ?? target, e, this.rage.burst > 0), this.rage.burst)) this.flushHeatmap();
|
|
2398
2853
|
}
|
|
2399
2854
|
/** Bins one pointer sample into the heatmap buffer. Not session activity. */
|
|
2400
2855
|
sampleMove(e) {
|
|
2401
|
-
const target = (0,
|
|
2856
|
+
const target = (0, import_guide_runtime5.composedTarget)(e);
|
|
2402
2857
|
if (document.visibilityState === "hidden" || !target?.closest || !this.autocaptureOn("mouseMovement") || isIgnoredElement(target)) return;
|
|
2403
|
-
const url =
|
|
2858
|
+
const url = this.pageHref();
|
|
2404
2859
|
if (this.heatmap.url !== url) this.flushHeatmap();
|
|
2405
2860
|
if (this.heatmap.move(url, movementBin(interactiveElement(target) ?? target, e))) this.flushHeatmap();
|
|
2406
2861
|
}
|
|
@@ -2476,7 +2931,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2476
2931
|
if (!this.isReady || this.destroyed) return;
|
|
2477
2932
|
if (this.config && !this.initialPageViewSent && this.config.settings?.autocapture) {
|
|
2478
2933
|
this.initialPageViewSent = true;
|
|
2479
|
-
this.trackPageView(
|
|
2934
|
+
this.trackPageView(this.pageHref());
|
|
2480
2935
|
}
|
|
2481
2936
|
if (this.config) {
|
|
2482
2937
|
for (const [props, extra] of this.earlyExceptions.splice(0)) {
|
|
@@ -2489,7 +2944,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2489
2944
|
syncReplay() {
|
|
2490
2945
|
const cfg = this.isReady && this.captureAllowed() ? this.replayConfig() : null;
|
|
2491
2946
|
if (cfg && this.session.replay === void 0) {
|
|
2492
|
-
this.session.replay = (0,
|
|
2947
|
+
this.session.replay = (0, import_runtime18.rolloutBucket)(this.session.id, "replay") < cfg.sampleRate;
|
|
2493
2948
|
this.persistSession();
|
|
2494
2949
|
}
|
|
2495
2950
|
if (!cfg || !this.session.replay) {
|
|
@@ -2511,7 +2966,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2511
2966
|
return {
|
|
2512
2967
|
anonymousId: this.anonymousId,
|
|
2513
2968
|
userId: this.user?.id || null,
|
|
2514
|
-
url: this.collectUrls() ?
|
|
2969
|
+
url: this.collectUrls() ? this.pageHref() : void 0,
|
|
2515
2970
|
device: { type: device.type, browser: device.browser, os: device.os },
|
|
2516
2971
|
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
2517
2972
|
sdk: { name: SDK_NAME, version: SDK_VERSION }
|
|
@@ -2562,6 +3017,14 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2562
3017
|
// ---------------------------------------------------------------------------
|
|
2563
3018
|
// Targeting + triggers
|
|
2564
3019
|
// ---------------------------------------------------------------------------
|
|
3020
|
+
/** The page URL Guidely works with: `options.pageUrl`, else app-relative for `file:` / custom protocols (`normalizeAppUrl`). */
|
|
3021
|
+
pageHref(href = location.href) {
|
|
3022
|
+
try {
|
|
3023
|
+
return this.options.pageUrl ? this.options.pageUrl(href) || href : normalizeAppUrl(href);
|
|
3024
|
+
} catch {
|
|
3025
|
+
return href;
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
2565
3028
|
/** Builds the targeting context for the current moment. */
|
|
2566
3029
|
getContext() {
|
|
2567
3030
|
const guides = this.state.guides;
|
|
@@ -2572,7 +3035,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2572
3035
|
userId: this.user?.id || null,
|
|
2573
3036
|
userAttributes: this.user?.traits ?? {},
|
|
2574
3037
|
accountAttributes: this.user?.account?.traits ?? {},
|
|
2575
|
-
url:
|
|
3038
|
+
url: this.pageHref(),
|
|
2576
3039
|
firstVisit: this.firstVisit,
|
|
2577
3040
|
eventsSeen: this.session.events,
|
|
2578
3041
|
completedGuides: keysOf(Object.keys(guides).filter((id) => guides[id]?.completedAt)),
|
|
@@ -2581,17 +3044,64 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2581
3044
|
completedChecklists: keysOf(completedChecklists),
|
|
2582
3045
|
audienceIds: this.state.audienceIds,
|
|
2583
3046
|
environment: this.options.environment,
|
|
2584
|
-
platform: "web"
|
|
3047
|
+
platform: "web",
|
|
3048
|
+
...this.options.appVersion ? { appVersion: this.options.appVersion } : {}
|
|
2585
3049
|
});
|
|
2586
3050
|
}
|
|
2587
3051
|
isTargeted(guide, ctx, url) {
|
|
2588
3052
|
const c = guide.config;
|
|
2589
|
-
if (c.urlPatterns.length && !c.urlPatterns.some((p) => (0,
|
|
2590
|
-
if (c.trigger.urlPattern && !(0,
|
|
3053
|
+
if (c.urlPatterns.length && !c.urlPatterns.some((p) => (0, import_guide_runtime5.urlMatches)(p, url))) return false;
|
|
3054
|
+
if (c.trigger.urlPattern && !(0, import_guide_runtime5.urlMatches)(c.trigger.urlPattern, url)) return false;
|
|
2591
3055
|
if (!isWithinSchedule(guide)) return false;
|
|
2592
3056
|
if (guide.kind === "survey" && (this.state.surveysSubmitted.includes(guide.id) || this.state.surveysSubmitted.includes(guide.key))) return false;
|
|
2593
3057
|
ctx.rolloutSalt = guide.id;
|
|
2594
|
-
return (0,
|
|
3058
|
+
return (0, import_runtime18.evaluateRules)(c.targeting, ctx);
|
|
3059
|
+
}
|
|
3060
|
+
/** Guide throttling: may this guide launch automatically now? */
|
|
3061
|
+
throttleAllows(guide) {
|
|
3062
|
+
if (!countsTowardThrottle(guide)) return true;
|
|
3063
|
+
return isThrottleEligible(this.config?.settings.throttling, this.state.throttle ?? []);
|
|
3064
|
+
}
|
|
3065
|
+
recordLaunch(guide, manual) {
|
|
3066
|
+
if (manual || !countsTowardThrottle(guide) || !this.config?.settings.throttling?.enabled) return;
|
|
3067
|
+
this.state.throttle = pruneLaunches([...this.state.throttle ?? [], nowIso()], this.config.settings.throttling);
|
|
3068
|
+
this.persistState();
|
|
3069
|
+
}
|
|
3070
|
+
/** Guides and A/B test control groups, which are evaluated the same way but never render. */
|
|
3071
|
+
candidates() {
|
|
3072
|
+
const guides = this.config?.guides ?? [];
|
|
3073
|
+
const holdouts = this.config?.guideHoldouts ?? [];
|
|
3074
|
+
return holdouts.length ? [...guides, ...holdouts.map((h) => h.guide)] : guides;
|
|
3075
|
+
}
|
|
3076
|
+
holdoutFor(guide) {
|
|
3077
|
+
return this.config?.guideHoldouts?.find((h) => h.guide.id === guide.id) ?? null;
|
|
3078
|
+
}
|
|
3079
|
+
/** The visitor's language candidates, most preferred first. */
|
|
3080
|
+
locales() {
|
|
3081
|
+
const nav = typeof navigator !== "undefined" ? [...navigator.languages ?? [], navigator.language] : [];
|
|
3082
|
+
return (0, import_runtime18.visitorLocales)(this.locale ?? this.options.locale, this.user?.traits, nav);
|
|
3083
|
+
}
|
|
3084
|
+
defaultLocale() {
|
|
3085
|
+
return this.config?.settings.localization?.defaultLocale || "en";
|
|
3086
|
+
}
|
|
3087
|
+
/** The guide in the visitor's language (remembered so its events carry `locale`). */
|
|
3088
|
+
localize(guide) {
|
|
3089
|
+
const localized = (0, import_runtime18.localizeGuide)(guide, this.locales(), this.defaultLocale());
|
|
3090
|
+
this.guideLocales.set(guide.id, localized.locale);
|
|
3091
|
+
return localized;
|
|
3092
|
+
}
|
|
3093
|
+
/**
|
|
3094
|
+
* Sets the language for guides and the resource center (`null` = back to the traits / browser).
|
|
3095
|
+
* Guides already on screen keep their language; the resource center re-renders.
|
|
3096
|
+
*/
|
|
3097
|
+
setLocale(locale) {
|
|
3098
|
+
this.locale = locale ? (0, import_runtime18.normalizeLocale)(locale) || null : null;
|
|
3099
|
+
if (this.resourceCenter) {
|
|
3100
|
+
this.resourceCenter.destroy();
|
|
3101
|
+
this.resourceCenterScope?.destroy();
|
|
3102
|
+
this.resourceCenter = null;
|
|
3103
|
+
this.renderPersistentWidgets();
|
|
3104
|
+
}
|
|
2595
3105
|
}
|
|
2596
3106
|
isEligible(guide, ctx, url) {
|
|
2597
3107
|
if (!this.isTargeted(guide, ctx, url)) return false;
|
|
@@ -2609,13 +3119,13 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2609
3119
|
if (t.delayMs > 0) return reason === "timer" && detail.guideId === guide.id;
|
|
2610
3120
|
return reason === "ready" || reason === "navigation" || reason === "identify";
|
|
2611
3121
|
case "url_match":
|
|
2612
|
-
return (reason === "ready" || reason === "navigation" || reason === "identify") && (0,
|
|
3122
|
+
return (reason === "ready" || reason === "navigation" || reason === "identify") && (0, import_guide_runtime5.urlMatches)(t.value, this.pageHref());
|
|
2613
3123
|
case "event":
|
|
2614
3124
|
return reason === "event" && !!t.value && detail.eventName === t.value;
|
|
2615
3125
|
case "element_click":
|
|
2616
3126
|
if (reason !== "click" || !t.value || !detail.target) return false;
|
|
2617
3127
|
try {
|
|
2618
|
-
return !!(0,
|
|
3128
|
+
return !!(0, import_guide_runtime5.closestComposed)(detail.target, t.value);
|
|
2619
3129
|
} catch {
|
|
2620
3130
|
return false;
|
|
2621
3131
|
}
|
|
@@ -2634,15 +3144,15 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2634
3144
|
}
|
|
2635
3145
|
evaluate(reason, detail = {}) {
|
|
2636
3146
|
if (!this.config || this.destroyed || this.editing()) return;
|
|
2637
|
-
const url =
|
|
3147
|
+
const url = this.pageHref();
|
|
2638
3148
|
const ctx = this.getContext();
|
|
2639
3149
|
if (reason === "identify") {
|
|
2640
|
-
const
|
|
2641
|
-
detail.newAudiences = new Set(Array.from(
|
|
2642
|
-
this.lastAudiences =
|
|
3150
|
+
const current3 = new Set(this.state.audienceIds);
|
|
3151
|
+
detail.newAudiences = new Set(Array.from(current3).filter((a) => !this.lastAudiences.has(a)));
|
|
3152
|
+
this.lastAudiences = current3;
|
|
2643
3153
|
}
|
|
2644
3154
|
const candidates = [];
|
|
2645
|
-
for (const guide of this.
|
|
3155
|
+
for (const guide of this.candidates()) {
|
|
2646
3156
|
const t = guide.config.trigger;
|
|
2647
3157
|
if (detail.endedGuide && detail.endedGuide.id === guide.id) continue;
|
|
2648
3158
|
if (reason === "ready" || reason === "navigation") {
|
|
@@ -2658,6 +3168,10 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2658
3168
|
if (t.type === "page_load" && t.delayMs > 0 && reason === "timer" && detail.guideId !== guide.id) continue;
|
|
2659
3169
|
if (!this.triggerFires(guide, reason, detail)) continue;
|
|
2660
3170
|
if (!this.isEligible(guide, ctx, url)) continue;
|
|
3171
|
+
if (!this.throttleAllows(guide)) {
|
|
3172
|
+
this.log("throttled", guide.key);
|
|
3173
|
+
continue;
|
|
3174
|
+
}
|
|
2661
3175
|
candidates.push(guide);
|
|
2662
3176
|
}
|
|
2663
3177
|
if (reason === "after_guide" || reason === "navigation") this.drainQueue(url, ctx);
|
|
@@ -2685,7 +3199,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2685
3199
|
this.visibilityObservers.delete(guide.id);
|
|
2686
3200
|
};
|
|
2687
3201
|
this.visibilityObservers.set(guide.id, stop);
|
|
2688
|
-
(0,
|
|
3202
|
+
(0, import_guide_runtime5.waitForTarget)([{ kind: "css", value: selector }], { timeoutMs: 0, signal: ac.signal }).then((el) => {
|
|
2689
3203
|
if (ac.signal.aborted) return;
|
|
2690
3204
|
const fire = () => {
|
|
2691
3205
|
stop();
|
|
@@ -2708,6 +3222,13 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2708
3222
|
/** Decides whether a guide can show now, otherwise queues it. */
|
|
2709
3223
|
tryShow(guide, url, manual) {
|
|
2710
3224
|
if (this.destroyed) return false;
|
|
3225
|
+
if (!manual && !this.throttleAllows(guide)) return false;
|
|
3226
|
+
const holdout = this.holdoutFor(guide);
|
|
3227
|
+
if (holdout) {
|
|
3228
|
+
if (this.active && !manual) return false;
|
|
3229
|
+
this.recordImpression(guide, manual);
|
|
3230
|
+
return true;
|
|
3231
|
+
}
|
|
2711
3232
|
if (!isExclusiveGuide(guide)) {
|
|
2712
3233
|
if (this.widgets.has(guide.id)) return false;
|
|
2713
3234
|
this.show(guide, manual);
|
|
@@ -2748,14 +3269,18 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2748
3269
|
guideState(id) {
|
|
2749
3270
|
return this.state.guides[id] ?? (this.state.guides[id] = { impressions: 0 });
|
|
2750
3271
|
}
|
|
2751
|
-
recordImpression(guide) {
|
|
3272
|
+
recordImpression(guide, manual = false) {
|
|
2752
3273
|
const s = this.guideState(guide.id);
|
|
2753
3274
|
s.impressions += 1;
|
|
2754
3275
|
s.lastShownAt = nowIso();
|
|
2755
3276
|
if (!this.session.shownGuides.includes(guide.id)) this.session.shownGuides.push(guide.id);
|
|
3277
|
+
this.recordLaunch(guide, manual);
|
|
2756
3278
|
this.persistState();
|
|
2757
3279
|
this.persistSession();
|
|
2758
|
-
this.
|
|
3280
|
+
const holdout = this.holdoutFor(guide);
|
|
3281
|
+
const experiment = holdout ? { id: holdout.experimentId, variantId: holdout.variantId } : guide.experiment;
|
|
3282
|
+
if (experiment) this.enqueue("experiment_viewed", { properties: { guideExperimentId: experiment.id, variantId: experiment.variantId, ...holdout ? {} : { guideId: guide.id } } });
|
|
3283
|
+
if (!holdout) this.syncGuideState(guide.id, { impression: true });
|
|
2759
3284
|
}
|
|
2760
3285
|
recordEnd(guide, reason) {
|
|
2761
3286
|
const s = this.guideState(guide.id);
|
|
@@ -2779,13 +3304,17 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2779
3304
|
this.reportError(err);
|
|
2780
3305
|
}
|
|
2781
3306
|
}
|
|
2782
|
-
showFlow(
|
|
3307
|
+
showFlow(raw, manual, stepId) {
|
|
2783
3308
|
if (this.editing()) return;
|
|
3309
|
+
const guide = this.localize(raw);
|
|
2784
3310
|
const exclusive = isExclusiveGuide(guide);
|
|
2785
|
-
const entry = { guide, manual, startUrl:
|
|
2786
|
-
const controller = (0,
|
|
3311
|
+
const entry = { guide, manual, startUrl: this.pageHref() };
|
|
3312
|
+
const controller = (0, import_guide_runtime5.runGuide)(guide, {
|
|
2787
3313
|
theme: this.theme(),
|
|
3314
|
+
// Code blocks only arrive when a workspace admin allowed them (the server strips them otherwise).
|
|
3315
|
+
runCode: true,
|
|
2788
3316
|
ctx: () => this.getContext(),
|
|
3317
|
+
currentUrl: () => this.pageHref(),
|
|
2789
3318
|
startAtStep: stepId,
|
|
2790
3319
|
eventBus: this.eventBus,
|
|
2791
3320
|
// Started from Help, Scout or startGuide(): steps on other pages offer to take the user there.
|
|
@@ -2799,7 +3328,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2799
3328
|
const step = e.stepId ? guide.config.steps.find((s) => s.id === e.stepId) : void 0;
|
|
2800
3329
|
this.enqueue(e.name, { guideId: guide.id, guideVersion: guide.version, stepId: e.stepId, stepIndex: e.stepIndex, properties: e.properties });
|
|
2801
3330
|
if (e.name === "guide_started") {
|
|
2802
|
-
this.recordImpression(guide);
|
|
3331
|
+
this.recordImpression(guide, manual);
|
|
2803
3332
|
this.emitter.emit("guide:start", guide);
|
|
2804
3333
|
}
|
|
2805
3334
|
if (e.name === "step_viewed" && step) {
|
|
@@ -2846,14 +3375,15 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2846
3375
|
this.widgets.delete(guide.id);
|
|
2847
3376
|
}
|
|
2848
3377
|
}
|
|
2849
|
-
showChecklist(
|
|
3378
|
+
showChecklist(raw) {
|
|
2850
3379
|
if (this.editing()) return;
|
|
3380
|
+
const guide = this.localize(raw);
|
|
2851
3381
|
const config = guide.config.checklist;
|
|
2852
3382
|
if (!config || this.widgets.has(guide.id)) return;
|
|
2853
|
-
const scope = (0,
|
|
3383
|
+
const scope = (0, import_guide_runtime6.createScope)(this.theme(), guide.config.appearance);
|
|
2854
3384
|
const progress = this.getChecklistProgress(guide.id);
|
|
2855
3385
|
const first = !this.state.guides[guide.id]?.impressions;
|
|
2856
|
-
const widget = (0,
|
|
3386
|
+
const widget = (0, import_guide_runtime5.renderChecklist)({
|
|
2857
3387
|
scope: scope.el,
|
|
2858
3388
|
config,
|
|
2859
3389
|
progress,
|
|
@@ -2868,12 +3398,13 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2868
3398
|
}, checklist: widget, scope });
|
|
2869
3399
|
if (first) this.recordImpression(guide);
|
|
2870
3400
|
}
|
|
2871
|
-
showSurvey(
|
|
3401
|
+
showSurvey(raw, manual) {
|
|
2872
3402
|
if (this.editing()) return;
|
|
3403
|
+
const guide = this.localize(raw);
|
|
2873
3404
|
const config = guide.config.survey;
|
|
2874
3405
|
if (!config) return;
|
|
2875
|
-
const scope = (0,
|
|
2876
|
-
const entry = { guide, manual, startUrl:
|
|
3406
|
+
const scope = (0, import_guide_runtime6.createScope)(this.theme(), guide.config.appearance);
|
|
3407
|
+
const entry = { guide, manual, startUrl: this.pageHref(), scope };
|
|
2877
3408
|
const finish = (reason) => {
|
|
2878
3409
|
widget.destroy();
|
|
2879
3410
|
scope.destroy();
|
|
@@ -2885,13 +3416,13 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2885
3416
|
}
|
|
2886
3417
|
this.setTimer(() => this.evaluate("after_guide", { endedGuide: guide }), 0);
|
|
2887
3418
|
};
|
|
2888
|
-
const widget = (0,
|
|
3419
|
+
const widget = (0, import_guide_runtime5.renderSurvey)({
|
|
2889
3420
|
scope: scope.el,
|
|
2890
3421
|
config,
|
|
2891
3422
|
title: guide.name,
|
|
2892
3423
|
onDismiss: () => finish(this.state.surveysSubmitted.includes(guide.id) ? "submitted" : "dismissed"),
|
|
2893
3424
|
onSubmit: (answers) => {
|
|
2894
|
-
const responseId = (0,
|
|
3425
|
+
const responseId = (0, import_runtime18.randomId)(24);
|
|
2895
3426
|
this.state.surveysSubmitted.push(guide.id);
|
|
2896
3427
|
this.persistState();
|
|
2897
3428
|
this.recordEnd(guide, "completed");
|
|
@@ -2904,7 +3435,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2904
3435
|
});
|
|
2905
3436
|
entry.survey = widget;
|
|
2906
3437
|
this.active = entry;
|
|
2907
|
-
this.recordImpression(guide);
|
|
3438
|
+
this.recordImpression(guide, manual);
|
|
2908
3439
|
this.enqueue("survey_displayed", { guideId: guide.id, guideVersion: guide.version });
|
|
2909
3440
|
this.emitter.emit("guide:start", guide);
|
|
2910
3441
|
}
|
|
@@ -2913,6 +3444,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2913
3444
|
}
|
|
2914
3445
|
navigate(url, newTab) {
|
|
2915
3446
|
try {
|
|
3447
|
+
if (this.openPrototypeTest(url)) return;
|
|
2916
3448
|
if (newTab) window.open(url, "_blank", "noopener");
|
|
2917
3449
|
else location.assign(url);
|
|
2918
3450
|
} catch (err) {
|
|
@@ -2933,11 +3465,17 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2933
3465
|
}
|
|
2934
3466
|
this.destroyMessenger();
|
|
2935
3467
|
if (rc && rc.enabled && !this.resourceCenter) {
|
|
2936
|
-
const scope = (0,
|
|
3468
|
+
const scope = (0, import_guide_runtime6.createScope)(this.theme());
|
|
2937
3469
|
this.resourceCenterScope = scope;
|
|
2938
|
-
this.resourceCenter = (0,
|
|
3470
|
+
this.resourceCenter = (0, import_guide_runtime5.renderResourceCenter)({
|
|
2939
3471
|
scope: scope.el,
|
|
2940
|
-
config: rc,
|
|
3472
|
+
config: (0, import_runtime18.localizeResourceCenter)(rc, this.locales(), this.defaultLocale()),
|
|
3473
|
+
evaluate: (rules) => (0, import_runtime18.evaluateRules)(rules, this.getContext()),
|
|
3474
|
+
announcementsSeenAt: () => this.state.announcementsSeenAt ?? null,
|
|
3475
|
+
onAnnouncementsSeen: () => {
|
|
3476
|
+
this.state.announcementsSeenAt = nowIso();
|
|
3477
|
+
this.persistState();
|
|
3478
|
+
},
|
|
2941
3479
|
guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
|
|
2942
3480
|
search: (q) => this.searchResourceCenter(q),
|
|
2943
3481
|
audienceIds: () => this.state.audienceIds,
|
|
@@ -2968,10 +3506,10 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2968
3506
|
loadMessenger() {
|
|
2969
3507
|
if (this.messenger) return Promise.resolve(this.messenger);
|
|
2970
3508
|
if (this.messengerLoading) return this.messengerLoading;
|
|
2971
|
-
this.messengerLoading = import("@guidelytech/guide-runtime/messenger").then(({ renderMessenger }) => {
|
|
3509
|
+
this.messengerLoading = import("@guidelytech/guide-runtime/messenger").then(({ renderMessenger, parseLauncherSpot }) => {
|
|
2972
3510
|
const cfg = this.config?.messenger;
|
|
2973
3511
|
if (this.destroyed || !this.config || !cfg?.enabled || this.messenger) return this.messenger;
|
|
2974
|
-
const scope = (0,
|
|
3512
|
+
const scope = (0, import_guide_runtime6.createScope)(this.theme());
|
|
2975
3513
|
this.messengerScope = scope;
|
|
2976
3514
|
this.messenger = renderMessenger({
|
|
2977
3515
|
scope: scope.el,
|
|
@@ -2982,16 +3520,20 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
2982
3520
|
projectId: this.options.projectId,
|
|
2983
3521
|
anonymousId: () => this.anonymousId,
|
|
2984
3522
|
userId: () => this.user?.id || null,
|
|
2985
|
-
pageUrl: () => this.collectUrls() ?
|
|
3523
|
+
pageUrl: () => this.collectUrls() ? this.pageHref() : void 0,
|
|
3524
|
+
locale: () => this.rcLocale()
|
|
2986
3525
|
}),
|
|
2987
3526
|
visitor: () => this.messengerVisitor(),
|
|
3527
|
+
launcherSpot: parseLauncherSpot(this.store.get(KEYS.launcher(this.options.projectId))),
|
|
3528
|
+
onLauncherMove: (spot) => writeJson(this.store, KEYS.launcher(this.options.projectId), spot),
|
|
2988
3529
|
brand: { logoUrl: this.theme().logoUrl ?? null, name: this.theme().brandName ?? null },
|
|
2989
3530
|
onResumeGuide: (guideId, stepId) => this.startGuide(guideId, { stepId }),
|
|
2990
3531
|
onNewsSeen: (ids) => {
|
|
2991
3532
|
const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
|
|
2992
3533
|
writeJson(this.store, KEYS.newsSeen(this.options.projectId), Array.from(/* @__PURE__ */ new Set([...ids, ...Array.isArray(seen) ? seen : []])).slice(0, 200));
|
|
2993
3534
|
},
|
|
2994
|
-
resourceCenter: this.config.resourceCenter,
|
|
3535
|
+
resourceCenter: this.config.resourceCenter ? (0, import_runtime18.localizeResourceCenter)(this.config.resourceCenter, this.locales(), this.defaultLocale()) : null,
|
|
3536
|
+
evaluate: (rules) => (0, import_runtime18.evaluateRules)(rules, this.getContext()),
|
|
2995
3537
|
audienceIds: () => this.state.audienceIds,
|
|
2996
3538
|
guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
|
|
2997
3539
|
onStartGuide: (idOrKey) => this.startGuide(idOrKey),
|
|
@@ -3015,7 +3557,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
3015
3557
|
/** What the messenger personalises from, computed fresh on every render. */
|
|
3016
3558
|
messengerVisitor() {
|
|
3017
3559
|
const ctx = this.getContext();
|
|
3018
|
-
const url =
|
|
3560
|
+
const url = this.pageHref();
|
|
3019
3561
|
const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
|
|
3020
3562
|
const visitor = buildMessengerVisitor({
|
|
3021
3563
|
guides: this.config?.guides ?? [],
|
|
@@ -3026,11 +3568,24 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
3026
3568
|
isAudienceTargeted: (g) => {
|
|
3027
3569
|
if (!isWithinSchedule(g)) return false;
|
|
3028
3570
|
ctx.rolloutSalt = g.id;
|
|
3029
|
-
return (0,
|
|
3571
|
+
return (0, import_runtime18.evaluateRules)(g.config.targeting, ctx);
|
|
3030
3572
|
},
|
|
3031
3573
|
newsSeen: Array.isArray(seen) ? seen : []
|
|
3032
3574
|
});
|
|
3033
|
-
|
|
3575
|
+
const rc = this.config?.resourceCenter;
|
|
3576
|
+
const rcNews = rc?.announcements?.length ? (0, import_runtime18.visibleAnnouncements)((0, import_runtime18.localizeResourceCenter)(rc, this.locales(), this.defaultLocale()).announcements, this.state.audienceIds).map((a) => ({
|
|
3577
|
+
id: `rc_${a.id}`,
|
|
3578
|
+
key: a.guideId ?? "",
|
|
3579
|
+
title: a.title,
|
|
3580
|
+
body: plainText(a.body),
|
|
3581
|
+
imageUrl: null,
|
|
3582
|
+
publishedAt: a.publishedAt,
|
|
3583
|
+
unread: !(Array.isArray(seen) && seen.includes(`rc_${a.id}`)),
|
|
3584
|
+
articleId: a.articleId ?? null,
|
|
3585
|
+
url: a.url ?? null
|
|
3586
|
+
})) : [];
|
|
3587
|
+
const news = rcNews.length ? [...visitor.news, ...rcNews].sort((a, b) => b.publishedAt.localeCompare(a.publishedAt)).slice(0, 10) : visitor.news;
|
|
3588
|
+
return { ...visitor, news, identified: !!this.user?.id };
|
|
3034
3589
|
}
|
|
3035
3590
|
destroyMessenger() {
|
|
3036
3591
|
this.messenger?.destroy();
|
|
@@ -3038,9 +3593,17 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
3038
3593
|
this.messenger = null;
|
|
3039
3594
|
this.messengerScope = null;
|
|
3040
3595
|
}
|
|
3596
|
+
/** The resource center's language for this visitor, or null for the default language. */
|
|
3597
|
+
rcLocale() {
|
|
3598
|
+
const rc = this.config?.resourceCenter;
|
|
3599
|
+
if (!rc) return null;
|
|
3600
|
+
const locale = (0, import_runtime18.localizeResourceCenter)(rc, this.locales(), this.defaultLocale()).locale;
|
|
3601
|
+
return locale.toLowerCase() === this.defaultLocale().toLowerCase() ? null : locale;
|
|
3602
|
+
}
|
|
3041
3603
|
async loadArticle(id) {
|
|
3042
3604
|
try {
|
|
3043
|
-
const
|
|
3605
|
+
const locale = this.rcLocale();
|
|
3606
|
+
const res = await this.api.get(`/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : void 0);
|
|
3044
3607
|
return res.ok && res.data ? res.data.article : null;
|
|
3045
3608
|
} catch {
|
|
3046
3609
|
return null;
|
|
@@ -3078,7 +3641,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
3078
3641
|
if (progress.completedTaskIds.includes(task.id)) return;
|
|
3079
3642
|
progress.completedTaskIds.push(task.id);
|
|
3080
3643
|
this.enqueue("checklist_task_completed", { guideId: guide.id, guideVersion: guide.version, properties: { taskId: task.id } });
|
|
3081
|
-
const complete = (0,
|
|
3644
|
+
const complete = (0, import_guide_runtime5.isChecklistComplete)(config, progress) && !progress.completedAt;
|
|
3082
3645
|
if (complete) {
|
|
3083
3646
|
progress.completedAt = nowIso();
|
|
3084
3647
|
this.enqueue("checklist_completed", { guideId: guide.id, guideVersion: guide.version });
|
|
@@ -3101,11 +3664,11 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
3101
3664
|
if (task.completion !== kind || !task.completionValue) continue;
|
|
3102
3665
|
let hit = false;
|
|
3103
3666
|
if (kind === "event") hit = value === task.completionValue;
|
|
3104
|
-
else if (kind === "url_visited") hit = (0,
|
|
3667
|
+
else if (kind === "url_visited") hit = (0, import_guide_runtime5.urlMatches)(task.completionValue, value);
|
|
3105
3668
|
else if (kind === "guide_completed") hit = value.id === task.completionValue || value.key === task.completionValue;
|
|
3106
3669
|
else if (kind === "click") {
|
|
3107
3670
|
try {
|
|
3108
|
-
hit = !!(0,
|
|
3671
|
+
hit = !!(0, import_guide_runtime5.closestComposed)(value, task.completionValue);
|
|
3109
3672
|
} catch {
|
|
3110
3673
|
hit = false;
|
|
3111
3674
|
}
|
|
@@ -3216,6 +3779,18 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
3216
3779
|
closeMessenger() {
|
|
3217
3780
|
this.messenger?.close();
|
|
3218
3781
|
}
|
|
3782
|
+
/**
|
|
3783
|
+
* Opens a Guidely prototype test (its `/validate/<token>` URL on this SDK's API origin, or the bare
|
|
3784
|
+
* token) in a full-screen overlay, attributed to the current user. Returns false, doing nothing, for
|
|
3785
|
+
* any other URL. Guide buttons that open a test URL use this automatically.
|
|
3786
|
+
*/
|
|
3787
|
+
openPrototypeTest(urlOrToken) {
|
|
3788
|
+
if (typeof document === "undefined") return false;
|
|
3789
|
+
const url = matchPrototypeTestUrl(urlOrToken, this.options.apiUrl);
|
|
3790
|
+
if (!url) return false;
|
|
3791
|
+
openPrototypeOverlay(url, { key: this.options.projectId, anonymousId: this.anonymousId, userId: this.user?.id ?? null });
|
|
3792
|
+
return true;
|
|
3793
|
+
}
|
|
3219
3794
|
/**
|
|
3220
3795
|
* Opens the complaint form in the messenger, optionally with a category picked (its id from
|
|
3221
3796
|
* Complaints → Categories). Does nothing when the project doesn't take complaints.
|
|
@@ -3224,6 +3799,14 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
3224
3799
|
if (!this.config?.messenger?.enabled || !this.config.messenger.complaints?.categories.length) return;
|
|
3225
3800
|
void this.loadMessenger().then((m) => m?.openComplaint(categoryId));
|
|
3226
3801
|
}
|
|
3802
|
+
/**
|
|
3803
|
+
* Opens the Ideas tab in the messenger (feature requests and voting), or one idea by its id.
|
|
3804
|
+
* Does nothing when the project hasn't turned on Ideas.
|
|
3805
|
+
*/
|
|
3806
|
+
openIdeas(requestId) {
|
|
3807
|
+
if (!this.config?.messenger?.enabled || !this.config.messenger.ideas) return;
|
|
3808
|
+
void this.loadMessenger().then((m) => m?.openIdeas(requestId));
|
|
3809
|
+
}
|
|
3227
3810
|
/** Opens a new messenger conversation, optionally pre-filling the message. */
|
|
3228
3811
|
showNewMessage(text) {
|
|
3229
3812
|
if (!this.config?.messenger?.enabled) return;
|
|
@@ -3253,14 +3836,15 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
3253
3836
|
const device = detectDevice(navigator.userAgent, window.innerWidth);
|
|
3254
3837
|
const account = this.user?.account;
|
|
3255
3838
|
return {
|
|
3256
|
-
url:
|
|
3257
|
-
path:
|
|
3839
|
+
url: this.pageHref(),
|
|
3840
|
+
path: pathOf2(this.pageHref()),
|
|
3258
3841
|
deviceType: device.type,
|
|
3259
3842
|
browser: device.browser,
|
|
3260
3843
|
os: device.os,
|
|
3261
3844
|
locale: navigator.language || void 0,
|
|
3262
3845
|
environment: this.options.environment,
|
|
3263
3846
|
platform: "web",
|
|
3847
|
+
...this.options.appVersion ? { appVersion: this.options.appVersion } : {},
|
|
3264
3848
|
...this.user?.traits ?? {},
|
|
3265
3849
|
id: this.user?.id || this.anonymousId,
|
|
3266
3850
|
anonymousId: this.anonymousId,
|
|
@@ -3420,20 +4004,20 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
3420
4004
|
// ---------------------------------------------------------------------------
|
|
3421
4005
|
/** Creates (or returns the existing) client and starts initialisation. */
|
|
3422
4006
|
static init(options) {
|
|
3423
|
-
if (
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
void
|
|
3427
|
-
return
|
|
4007
|
+
if (current2 && !current2.destroyed && current2.options.projectId === options.projectId) return current2;
|
|
4008
|
+
current2?.destroy();
|
|
4009
|
+
current2 = new _GuidelyClient(options);
|
|
4010
|
+
void current2.init();
|
|
4011
|
+
return current2;
|
|
3428
4012
|
}
|
|
3429
4013
|
/** The most recently created client, if any. */
|
|
3430
4014
|
static get() {
|
|
3431
|
-
return
|
|
4015
|
+
return current2 && !current2.destroyed ? current2 : null;
|
|
3432
4016
|
}
|
|
3433
4017
|
};
|
|
3434
|
-
var
|
|
4018
|
+
var current2 = null;
|
|
3435
4019
|
function webOnly(config) {
|
|
3436
|
-
return config.guides.every((g) => (0,
|
|
4020
|
+
return config.guides.every((g) => (0, import_runtime18.supportsPlatform)(g.config, "web")) ? config : { ...config, guides: config.guides.filter((g) => (0, import_runtime18.supportsPlatform)(g.config, "web")) };
|
|
3437
4021
|
}
|
|
3438
4022
|
function pathOf2(url) {
|
|
3439
4023
|
try {
|
|
@@ -3453,7 +4037,7 @@ function externalReferrer() {
|
|
|
3453
4037
|
}
|
|
3454
4038
|
function createGuidely(options) {
|
|
3455
4039
|
const client = new GuidelyClient(options);
|
|
3456
|
-
|
|
4040
|
+
current2 = client;
|
|
3457
4041
|
void client.init();
|
|
3458
4042
|
return client;
|
|
3459
4043
|
}
|
|
@@ -3476,17 +4060,24 @@ var index_default = Guidely;
|
|
|
3476
4060
|
GuidelyClient,
|
|
3477
4061
|
HEARTBEAT_MS,
|
|
3478
4062
|
KEYS,
|
|
4063
|
+
PROTOTYPE_CLOSE_MESSAGE,
|
|
3479
4064
|
SDK_NAME,
|
|
3480
4065
|
SDK_VERSION,
|
|
3481
4066
|
areMutuallyExclusive,
|
|
3482
4067
|
buildContext,
|
|
3483
4068
|
captureSnapshot,
|
|
4069
|
+
countsTowardThrottle,
|
|
3484
4070
|
createGuidely,
|
|
3485
4071
|
createStorage,
|
|
3486
4072
|
detectDevice,
|
|
4073
|
+
isAppUrl,
|
|
3487
4074
|
isExclusiveGuide,
|
|
3488
4075
|
isFrequencyEligible,
|
|
4076
|
+
isThrottleEligible,
|
|
3489
4077
|
isWithinSchedule,
|
|
4078
|
+
matchPrototypeTestUrl,
|
|
4079
|
+
normalizeAppUrl,
|
|
4080
|
+
pruneLaunches,
|
|
3490
4081
|
watchNavigation
|
|
3491
4082
|
});
|
|
3492
4083
|
//# sourceMappingURL=index.cjs.map
|