veo-sdk 0.2.1 → 0.3.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/README.md +36 -0
- package/dist/builder-UEGKG2UR.mjs +5 -0
- package/dist/builder-UEGKG2UR.mjs.map +1 -0
- package/dist/builder.cjs +69 -15
- package/dist/builder.cjs.map +1 -1
- package/dist/builder.mjs +5 -444
- package/dist/builder.mjs.map +1 -1
- package/dist/chunk-66UQ474G.mjs +439 -0
- package/dist/chunk-66UQ474G.mjs.map +1 -0
- package/dist/{chunk-BBAA5BRS.mjs → chunk-CI7NWEH2.mjs} +66 -1042
- package/dist/chunk-CI7NWEH2.mjs.map +1 -0
- package/dist/chunk-G5CKTGEG.mjs +1042 -0
- package/dist/chunk-G5CKTGEG.mjs.map +1 -0
- package/dist/chunk-JIU5JLI3.mjs +7 -0
- package/dist/chunk-JIU5JLI3.mjs.map +1 -0
- package/dist/index.cjs +3355 -2590
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +50 -48
- package/dist/index.mjs.map +1 -1
- package/dist/next.cjs +90 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +108 -0
- package/dist/next.d.ts +108 -0
- package/dist/next.mjs +82 -0
- package/dist/next.mjs.map +1 -0
- package/dist/veo-builder.js +6 -6
- package/dist/veo-builder.js.map +1 -1
- package/dist/veo.js +2 -2
- package/dist/veo.js.map +1 -1
- package/package.json +6 -1
- package/dist/chunk-BBAA5BRS.mjs.map +0 -1
|
@@ -0,0 +1,1042 @@
|
|
|
1
|
+
import { hasDocument, hasWindow, previewGuide, DEFAULT_TRACKER_MAX_RETRIES, DEFAULT_TRACKER_BATCH_SIZE, DEFAULT_TRACKER_FLUSH_INTERVAL_MS, WalkthroughRenderer, InlineFormRenderer, FormRenderer, InlineCustomRenderer, InlineRenderer, CustomRenderer, TooltipRenderer, BannerRenderer, ModalRenderer, FREQUENCY_CACHE_KEY_PREFIX, FREQUENCY_CACHE_TTL_MS, FREQUENCY_CACHE_MAX_ENTRIES, WALKTHROUGH_STATE_KEY_PREFIX, WALKTHROUGH_ABANDONMENT_TIMEOUT_MS } from './chunk-CI7NWEH2.mjs';
|
|
2
|
+
|
|
3
|
+
// src/plugins/plugin.ts
|
|
4
|
+
function definePlugin(plugin) {
|
|
5
|
+
return plugin;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// src/plugins/guides/preview-mode.ts
|
|
9
|
+
var active = null;
|
|
10
|
+
async function runPreviewMode(opts) {
|
|
11
|
+
if (!hasDocument()) return null;
|
|
12
|
+
active?.teardown();
|
|
13
|
+
let guide = null;
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch(`${opts.apiUrl}/v1/guides/preview-resolve`, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: { "Content-Type": "application/json", "X-Api-Key": opts.apiKey },
|
|
18
|
+
body: JSON.stringify({ token: opts.token })
|
|
19
|
+
});
|
|
20
|
+
if (res.ok) {
|
|
21
|
+
const data = await res.json();
|
|
22
|
+
guide = data.guide ?? null;
|
|
23
|
+
} else if (opts.debug) {
|
|
24
|
+
console.warn("[veo] preview-resolve respondi\xF3", res.status);
|
|
25
|
+
}
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (opts.debug) console.warn("[veo] preview-resolve fall\xF3:", err);
|
|
28
|
+
}
|
|
29
|
+
const submitToBackend = async (answers) => {
|
|
30
|
+
if (!guide) return false;
|
|
31
|
+
const veo = window.veo;
|
|
32
|
+
const endUserId = veo?.getEndUserId?.() ?? veo?.getAnonymousId?.() ?? null;
|
|
33
|
+
if (!endUserId) return false;
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch(`${opts.apiUrl}/v1/guides/${guide.guideId}/form-response`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "Content-Type": "application/json", "X-Api-Key": opts.apiKey },
|
|
38
|
+
body: JSON.stringify({ endUserId, answers })
|
|
39
|
+
});
|
|
40
|
+
return res.ok;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
let preview = null;
|
|
46
|
+
const show = () => {
|
|
47
|
+
if (!guide) return;
|
|
48
|
+
preview?.close();
|
|
49
|
+
preview = previewGuide({
|
|
50
|
+
guideType: guide.guideType,
|
|
51
|
+
guideSteps: guide.guideSteps,
|
|
52
|
+
activationRules: guide.activationRules,
|
|
53
|
+
guideName: guide.guideName,
|
|
54
|
+
formSubmit: submitToBackend,
|
|
55
|
+
formSubmitNote: "Vista previa: guardado en TU usuario para probar el flujo."
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
const chip = mountChip({
|
|
59
|
+
label: guide ? `Vista previa: ${guide.guideName}` : "Link de preview inv\xE1lido o vencido",
|
|
60
|
+
error: !guide,
|
|
61
|
+
onReshow: guide ? show : null,
|
|
62
|
+
onClose: () => handle.teardown()
|
|
63
|
+
});
|
|
64
|
+
const handle = {
|
|
65
|
+
teardown() {
|
|
66
|
+
preview?.close();
|
|
67
|
+
preview = null;
|
|
68
|
+
chip.remove();
|
|
69
|
+
if (active === handle) active = null;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
active = handle;
|
|
73
|
+
show();
|
|
74
|
+
return handle;
|
|
75
|
+
}
|
|
76
|
+
function mountChip(opts) {
|
|
77
|
+
const host = document.createElement("div");
|
|
78
|
+
host.setAttribute("data-veo-preview-chip", "");
|
|
79
|
+
const root = host.attachShadow({ mode: "open" });
|
|
80
|
+
const style = document.createElement("style");
|
|
81
|
+
style.textContent = `
|
|
82
|
+
.chip { position: fixed; bottom: 16px; right: 16px; z-index: 2147483647;
|
|
83
|
+
display: flex; align-items: center; gap: 8px; padding: 8px 12px;
|
|
84
|
+
background: #111827; color: #f9fafb; border-radius: 9999px;
|
|
85
|
+
font: 500 12px/1.2 system-ui, sans-serif; box-shadow: 0 4px 12px rgba(0,0,0,.25); }
|
|
86
|
+
.chip.error { background: #7f1d1d; }
|
|
87
|
+
.label { max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
88
|
+
button { all: unset; cursor: pointer; padding: 2px 6px; border-radius: 9999px;
|
|
89
|
+
font-size: 12px; line-height: 1; }
|
|
90
|
+
button:hover { background: rgba(255,255,255,.15); }
|
|
91
|
+
`;
|
|
92
|
+
root.appendChild(style);
|
|
93
|
+
const chip = document.createElement("div");
|
|
94
|
+
chip.className = opts.error ? "chip error" : "chip";
|
|
95
|
+
const label = document.createElement("span");
|
|
96
|
+
label.className = "label";
|
|
97
|
+
label.textContent = opts.label;
|
|
98
|
+
chip.appendChild(label);
|
|
99
|
+
if (opts.onReshow) {
|
|
100
|
+
const reshow = document.createElement("button");
|
|
101
|
+
reshow.textContent = "\u21BB";
|
|
102
|
+
reshow.title = "Volver a mostrar la gu\xEDa";
|
|
103
|
+
reshow.addEventListener("click", opts.onReshow);
|
|
104
|
+
chip.appendChild(reshow);
|
|
105
|
+
}
|
|
106
|
+
const close = document.createElement("button");
|
|
107
|
+
close.textContent = "\u2715";
|
|
108
|
+
close.title = "Salir de la vista previa";
|
|
109
|
+
close.addEventListener("click", opts.onClose);
|
|
110
|
+
chip.appendChild(close);
|
|
111
|
+
root.appendChild(chip);
|
|
112
|
+
document.body.appendChild(host);
|
|
113
|
+
return { remove: () => host.remove() };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/plugins/guides/guide-frequency-cache.ts
|
|
117
|
+
var STATUS_PRIORITY = {
|
|
118
|
+
shown: 1,
|
|
119
|
+
dismissed: 2,
|
|
120
|
+
completed: 2
|
|
121
|
+
};
|
|
122
|
+
function namespaceFromApiKey(apiKey) {
|
|
123
|
+
return apiKey.slice(-16) || "default";
|
|
124
|
+
}
|
|
125
|
+
var GuideFrequencyCache = class {
|
|
126
|
+
constructor(namespace) {
|
|
127
|
+
this.storageKey = `${FREQUENCY_CACHE_KEY_PREFIX}${namespace}`;
|
|
128
|
+
this.cache = this.load();
|
|
129
|
+
this.prune();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Decide si una guía debe filtrarse localmente antes de pintarla.
|
|
133
|
+
*
|
|
134
|
+
* - `every_visit` / `always` → nunca filtra (siempre re-pinta).
|
|
135
|
+
* - `once` → cualquier entry registrada filtra (verla ya cuenta).
|
|
136
|
+
* - `until_dismissed` → si fue descartada O COMPLETADA (responder un
|
|
137
|
+
* form / terminar un walkthrough es más fuerte que descartar — sin
|
|
138
|
+
* esto, una encuesta seguiría preguntando después de respondida).
|
|
139
|
+
*/
|
|
140
|
+
shouldFilter(guideId, displayFrequency) {
|
|
141
|
+
if (displayFrequency === "every_visit" || displayFrequency === "always") return false;
|
|
142
|
+
const entry = this.cache.get(guideId);
|
|
143
|
+
if (!entry) return false;
|
|
144
|
+
if (displayFrequency === "once") return true;
|
|
145
|
+
if (displayFrequency === "until_dismissed") {
|
|
146
|
+
return entry.status === "dismissed" || entry.status === "completed";
|
|
147
|
+
}
|
|
148
|
+
if (displayFrequency === "until_answered") return entry.status === "completed";
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Registra/actualiza la entry de una guía. Nunca degrada un estado más
|
|
153
|
+
* fuerte (no convierte `dismissed` en `shown` por un re-render). Persiste
|
|
154
|
+
* cada llamada — escribir a localStorage es lo bastante barato.
|
|
155
|
+
*/
|
|
156
|
+
record(guideId, status) {
|
|
157
|
+
const existing = this.cache.get(guideId);
|
|
158
|
+
if (existing && STATUS_PRIORITY[status] < STATUS_PRIORITY[existing.status]) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
this.cache.set(guideId, {
|
|
162
|
+
guideId,
|
|
163
|
+
status,
|
|
164
|
+
lastInteractionAt: Date.now()
|
|
165
|
+
});
|
|
166
|
+
this.persist();
|
|
167
|
+
}
|
|
168
|
+
/** Limpia el cache (botón "reset" del demo, tests). */
|
|
169
|
+
clear() {
|
|
170
|
+
this.cache.clear();
|
|
171
|
+
this.persist();
|
|
172
|
+
}
|
|
173
|
+
/** Útil para debugging — devuelve un snapshot. */
|
|
174
|
+
snapshot() {
|
|
175
|
+
return Array.from(this.cache.values());
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Limpia entries expiradas por TTL y, si aún excede el límite, descarta
|
|
179
|
+
* las más viejas hasta caber. Se ejecuta una vez en el constructor; no
|
|
180
|
+
* vale la pena correrlo a cada lectura.
|
|
181
|
+
*/
|
|
182
|
+
prune() {
|
|
183
|
+
const now = Date.now();
|
|
184
|
+
let mutated = false;
|
|
185
|
+
for (const [id, entry] of this.cache) {
|
|
186
|
+
if (now - entry.lastInteractionAt > FREQUENCY_CACHE_TTL_MS) {
|
|
187
|
+
this.cache.delete(id);
|
|
188
|
+
mutated = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (this.cache.size > FREQUENCY_CACHE_MAX_ENTRIES) {
|
|
192
|
+
const sorted = Array.from(this.cache.entries()).sort(
|
|
193
|
+
(a, b) => a[1].lastInteractionAt - b[1].lastInteractionAt
|
|
194
|
+
);
|
|
195
|
+
const excess = this.cache.size - FREQUENCY_CACHE_MAX_ENTRIES;
|
|
196
|
+
for (let i = 0; i < excess; i++) {
|
|
197
|
+
const tuple = sorted[i];
|
|
198
|
+
if (tuple) this.cache.delete(tuple[0]);
|
|
199
|
+
}
|
|
200
|
+
mutated = true;
|
|
201
|
+
}
|
|
202
|
+
if (mutated) this.persist();
|
|
203
|
+
}
|
|
204
|
+
load() {
|
|
205
|
+
if (typeof localStorage === "undefined") return /* @__PURE__ */ new Map();
|
|
206
|
+
try {
|
|
207
|
+
const raw = localStorage.getItem(this.storageKey);
|
|
208
|
+
if (!raw) return /* @__PURE__ */ new Map();
|
|
209
|
+
const parsed = JSON.parse(raw);
|
|
210
|
+
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Map();
|
|
211
|
+
const entries = [];
|
|
212
|
+
for (const item of parsed) {
|
|
213
|
+
if (item && typeof item === "object" && typeof item.guideId === "string" && typeof item.lastInteractionAt === "number" && ["shown", "dismissed", "completed"].includes(item.status)) {
|
|
214
|
+
const e = item;
|
|
215
|
+
entries.push([e.guideId, e]);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return new Map(entries);
|
|
219
|
+
} catch {
|
|
220
|
+
return /* @__PURE__ */ new Map();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
persist() {
|
|
224
|
+
if (typeof localStorage === "undefined") return;
|
|
225
|
+
try {
|
|
226
|
+
const serialized = JSON.stringify(Array.from(this.cache.values()));
|
|
227
|
+
localStorage.setItem(this.storageKey, serialized);
|
|
228
|
+
} catch {
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// src/plugins/guides/guide-resolver-client.ts
|
|
234
|
+
var GuideResolverClient = class {
|
|
235
|
+
constructor(apiUrl, apiKey, debug = false) {
|
|
236
|
+
this.apiUrl = apiUrl;
|
|
237
|
+
this.apiKey = apiKey;
|
|
238
|
+
this.debug = debug;
|
|
239
|
+
}
|
|
240
|
+
async resolve(endUserId, pageUrl) {
|
|
241
|
+
const params = new URLSearchParams({ endUserId, pageUrl });
|
|
242
|
+
let response;
|
|
243
|
+
try {
|
|
244
|
+
response = await fetch(`${this.apiUrl}/v1/guides/resolve?${params.toString()}`, {
|
|
245
|
+
method: "GET",
|
|
246
|
+
headers: { "X-Api-Key": this.apiKey },
|
|
247
|
+
credentials: "omit"
|
|
248
|
+
});
|
|
249
|
+
} catch (err) {
|
|
250
|
+
if (this.debug) console.error("[veo] guides resolve fetch failed:", err);
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
if (!response.ok) {
|
|
254
|
+
if (this.debug) console.warn("[veo] guides resolve HTTP", response.status);
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
const data = await response.json();
|
|
259
|
+
return Array.isArray(data.guides) ? data.guides : [];
|
|
260
|
+
} catch (err) {
|
|
261
|
+
if (this.debug) console.error("[veo] guides resolve parse failed:", err);
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Trae una guía específica por ID, sin pasar por la lógica de
|
|
267
|
+
* activación. La usa el controller para resumir un walkthrough entre
|
|
268
|
+
* páginas: el `resolve` no devuelve la guía si la URL actual no
|
|
269
|
+
* matchea, pero el walkthrough activo debe poder consultarla igual.
|
|
270
|
+
*
|
|
271
|
+
* El endpoint backend (`GET /v1/guides/:guideId`) devuelve `GuideResponse`,
|
|
272
|
+
* un superset de `ResolvedGuide` con campos de audit. Acá descartamos los
|
|
273
|
+
* extra y nos quedamos con el shape que el resto del SDK ya consume.
|
|
274
|
+
*
|
|
275
|
+
* Devuelve `null` en cualquier error (404, red, parse) — el caller debe
|
|
276
|
+
* tratarlo como "guía ya no existe" y cancelar el walkthrough limpiamente.
|
|
277
|
+
*/
|
|
278
|
+
async fetchById(guideId) {
|
|
279
|
+
let response;
|
|
280
|
+
try {
|
|
281
|
+
response = await fetch(`${this.apiUrl}/v1/guides/${encodeURIComponent(guideId)}`, {
|
|
282
|
+
method: "GET",
|
|
283
|
+
headers: { "X-Api-Key": this.apiKey },
|
|
284
|
+
credentials: "omit"
|
|
285
|
+
});
|
|
286
|
+
} catch (err) {
|
|
287
|
+
if (this.debug) console.error("[veo] guides fetchById network failed:", err);
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
if (!response.ok) {
|
|
291
|
+
if (this.debug) console.warn("[veo] guides fetchById HTTP", response.status);
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const data = await response.json();
|
|
296
|
+
if (typeof data.guideId !== "string" || typeof data.guideName !== "string" || typeof data.guideType !== "string" || !Array.isArray(data.guideSteps) || !data.activationRules || typeof data.displayFrequency !== "string" || typeof data.displayPriority !== "number") {
|
|
297
|
+
if (this.debug) console.warn("[veo] guides fetchById: unexpected shape");
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
guideId: data.guideId,
|
|
302
|
+
guideName: data.guideName,
|
|
303
|
+
guideType: data.guideType,
|
|
304
|
+
guideSteps: data.guideSteps,
|
|
305
|
+
activationRules: data.activationRules,
|
|
306
|
+
displayFrequency: data.displayFrequency,
|
|
307
|
+
displayPriority: data.displayPriority
|
|
308
|
+
};
|
|
309
|
+
} catch (err) {
|
|
310
|
+
if (this.debug) console.error("[veo] guides fetchById parse failed:", err);
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// src/plugins/guides/guide-tracker-client.ts
|
|
317
|
+
function toWire(payload) {
|
|
318
|
+
const wire = {
|
|
319
|
+
endUserId: payload.endUserId,
|
|
320
|
+
interactionType: payload.action
|
|
321
|
+
};
|
|
322
|
+
if (payload.sessionId) wire.sessionId = payload.sessionId;
|
|
323
|
+
if (typeof payload.stepIndex === "number") wire.stepPosition = payload.stepIndex;
|
|
324
|
+
if (payload.pageUrl) wire.pageUrl = payload.pageUrl;
|
|
325
|
+
if (payload.pagePath) wire.pagePath = payload.pagePath;
|
|
326
|
+
return wire;
|
|
327
|
+
}
|
|
328
|
+
var GuideTrackerClient = class {
|
|
329
|
+
constructor(apiUrl, apiKey) {
|
|
330
|
+
this.apiUrl = apiUrl;
|
|
331
|
+
this.apiKey = apiKey;
|
|
332
|
+
}
|
|
333
|
+
async send(request) {
|
|
334
|
+
const url = this.buildUrl(request.guideId);
|
|
335
|
+
const response = await fetch(url, {
|
|
336
|
+
method: "POST",
|
|
337
|
+
headers: {
|
|
338
|
+
"X-Api-Key": this.apiKey,
|
|
339
|
+
"Content-Type": "application/json"
|
|
340
|
+
},
|
|
341
|
+
body: JSON.stringify(toWire(request.payload)),
|
|
342
|
+
credentials: "omit"
|
|
343
|
+
});
|
|
344
|
+
if (!response.ok) {
|
|
345
|
+
throw new Error(`guide interaction send failed: HTTP ${response.status}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Envío "fire-and-forget" en pagehide. Devuelve `false` si el navegador
|
|
350
|
+
* no acepta el beacon (cola llena, no soportado) — en ese caso la queue
|
|
351
|
+
* sabrá que perdió el envío.
|
|
352
|
+
*/
|
|
353
|
+
sendBeacon(request) {
|
|
354
|
+
if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
const url = `${this.buildUrl(request.guideId)}?key=${encodeURIComponent(this.apiKey)}`;
|
|
358
|
+
const blob = new Blob([JSON.stringify(toWire(request.payload))], {
|
|
359
|
+
type: "application/json"
|
|
360
|
+
});
|
|
361
|
+
try {
|
|
362
|
+
return navigator.sendBeacon(url, blob);
|
|
363
|
+
} catch {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
buildUrl(guideId) {
|
|
368
|
+
return `${this.apiUrl}/v1/guides/${encodeURIComponent(guideId)}/interactions`;
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// src/plugins/guides/guide-tracker-queue.ts
|
|
373
|
+
var GuideTrackerQueue = class {
|
|
374
|
+
constructor(config) {
|
|
375
|
+
this.config = config;
|
|
376
|
+
this.buffer = [];
|
|
377
|
+
this.timer = null;
|
|
378
|
+
this.flushing = false;
|
|
379
|
+
this.unloadCleanup = null;
|
|
380
|
+
this.startTimer();
|
|
381
|
+
this.installUnloadHandler();
|
|
382
|
+
}
|
|
383
|
+
/** Tamaño actual del buffer (útil para debug/stats en el demo). */
|
|
384
|
+
get size() {
|
|
385
|
+
return this.buffer.length;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Encola una interaction. Si el buffer alcanza `batchSize`, dispara
|
|
389
|
+
* `flush()` inmediato.
|
|
390
|
+
*/
|
|
391
|
+
enqueue(request) {
|
|
392
|
+
this.buffer.push({
|
|
393
|
+
guideId: request.guideId,
|
|
394
|
+
payload: request.payload,
|
|
395
|
+
attempts: 0,
|
|
396
|
+
enqueuedAt: Date.now()
|
|
397
|
+
});
|
|
398
|
+
if (this.buffer.length >= this.config.batchSize) {
|
|
399
|
+
void this.flush();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Envía todo el buffer en paralelo. Items que fallan vuelven al buffer
|
|
404
|
+
* si todavía tienen reintentos disponibles. Idempotente con respecto a
|
|
405
|
+
* flushes concurrentes (el segundo es un no-op).
|
|
406
|
+
*/
|
|
407
|
+
async flush() {
|
|
408
|
+
if (this.flushing || this.buffer.length === 0) return;
|
|
409
|
+
this.flushing = true;
|
|
410
|
+
const batch = this.buffer.splice(0, this.buffer.length);
|
|
411
|
+
const results = await Promise.allSettled(
|
|
412
|
+
batch.map(
|
|
413
|
+
(item) => this.config.client.send({ guideId: item.guideId, payload: item.payload })
|
|
414
|
+
)
|
|
415
|
+
);
|
|
416
|
+
for (let i = 0; i < results.length; i++) {
|
|
417
|
+
const result = results[i];
|
|
418
|
+
const item = batch[i];
|
|
419
|
+
if (!result || !item) continue;
|
|
420
|
+
if (result.status === "fulfilled") continue;
|
|
421
|
+
item.attempts += 1;
|
|
422
|
+
if (item.attempts < this.config.maxRetries) {
|
|
423
|
+
this.buffer.push(item);
|
|
424
|
+
} else {
|
|
425
|
+
this.safeOnError(result.reason, item);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
this.flushing = false;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Drena el buffer vía `sendBeacon`. Síncrono, sin retry. Para pagehide.
|
|
432
|
+
*/
|
|
433
|
+
flushBeacon() {
|
|
434
|
+
if (this.buffer.length === 0) return;
|
|
435
|
+
const batch = this.buffer.splice(0, this.buffer.length);
|
|
436
|
+
for (const item of batch) {
|
|
437
|
+
this.config.client.sendBeacon({ guideId: item.guideId, payload: item.payload });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
destroy() {
|
|
441
|
+
if (this.timer) clearInterval(this.timer);
|
|
442
|
+
this.timer = null;
|
|
443
|
+
this.unloadCleanup?.();
|
|
444
|
+
this.unloadCleanup = null;
|
|
445
|
+
}
|
|
446
|
+
startTimer() {
|
|
447
|
+
if (typeof window === "undefined") return;
|
|
448
|
+
this.timer = setInterval(() => {
|
|
449
|
+
void this.flush();
|
|
450
|
+
}, this.config.flushIntervalMs);
|
|
451
|
+
}
|
|
452
|
+
installUnloadHandler() {
|
|
453
|
+
if (typeof window === "undefined") return;
|
|
454
|
+
const onPagehide = () => this.flushBeacon();
|
|
455
|
+
const onVisibility = () => {
|
|
456
|
+
if (document.visibilityState === "hidden") this.flushBeacon();
|
|
457
|
+
};
|
|
458
|
+
window.addEventListener("pagehide", onPagehide);
|
|
459
|
+
window.addEventListener("visibilitychange", onVisibility);
|
|
460
|
+
this.unloadCleanup = () => {
|
|
461
|
+
window.removeEventListener("pagehide", onPagehide);
|
|
462
|
+
window.removeEventListener("visibilitychange", onVisibility);
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
safeOnError(reason, item) {
|
|
466
|
+
if (!this.config.onError) return;
|
|
467
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
468
|
+
try {
|
|
469
|
+
this.config.onError(error, item);
|
|
470
|
+
} catch {
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// src/plugins/guides/walkthrough-state.ts
|
|
476
|
+
var WalkthroughStateManager = class {
|
|
477
|
+
constructor(namespace) {
|
|
478
|
+
this.state = null;
|
|
479
|
+
this.storageKey = `${WALKTHROUGH_STATE_KEY_PREFIX}${namespace}`;
|
|
480
|
+
this.state = this.load();
|
|
481
|
+
if (this.state && this.isAbandoned(this.state)) {
|
|
482
|
+
this.clear();
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Estado del walkthrough activo. Devuelve `null` si no hay ninguno o si
|
|
487
|
+
* el último progreso fue hace más del timeout (en ese caso también
|
|
488
|
+
* limpia el storage como side-effect).
|
|
489
|
+
*/
|
|
490
|
+
current() {
|
|
491
|
+
if (!this.state) return null;
|
|
492
|
+
if (this.isAbandoned(this.state)) {
|
|
493
|
+
this.clear();
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
return this.state;
|
|
497
|
+
}
|
|
498
|
+
/** Atajo legible para el controller. */
|
|
499
|
+
hasActive() {
|
|
500
|
+
return this.current() !== null;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Inicia un nuevo walkthrough en step 0. Sobreescribe cualquier estado
|
|
504
|
+
* previo — los callers (controller) son responsables de verificar
|
|
505
|
+
* `hasActive()` antes si necesitan exclusividad.
|
|
506
|
+
*/
|
|
507
|
+
start(guideId, totalSteps) {
|
|
508
|
+
const now = Date.now();
|
|
509
|
+
this.state = {
|
|
510
|
+
guideId,
|
|
511
|
+
totalSteps,
|
|
512
|
+
currentStepIndex: 0,
|
|
513
|
+
startedAt: now,
|
|
514
|
+
lastProgressAt: now
|
|
515
|
+
};
|
|
516
|
+
this.persist();
|
|
517
|
+
return this.state;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Avanza al siguiente step. Si el step resultante excede `totalSteps`,
|
|
521
|
+
* devuelve `null` y NO toca el estado — el controller debe interpretar
|
|
522
|
+
* eso como "ya estamos en el último, no hay próximo" y disparar
|
|
523
|
+
* `complete` vía el handler del botón "Finalizar".
|
|
524
|
+
*/
|
|
525
|
+
advance() {
|
|
526
|
+
if (!this.state) return null;
|
|
527
|
+
const next = this.state.currentStepIndex + 1;
|
|
528
|
+
if (next >= this.state.totalSteps) return null;
|
|
529
|
+
this.state = {
|
|
530
|
+
...this.state,
|
|
531
|
+
currentStepIndex: next,
|
|
532
|
+
lastProgressAt: Date.now()
|
|
533
|
+
};
|
|
534
|
+
this.persist();
|
|
535
|
+
return this.state;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Retrocede al step anterior. En step 0 no hace nada y devuelve el
|
|
539
|
+
* estado tal cual (el botón "Atrás" no debería estar visible en el
|
|
540
|
+
* primer step, pero por defensa el manager también lo guard-ea).
|
|
541
|
+
*/
|
|
542
|
+
back() {
|
|
543
|
+
if (!this.state) return null;
|
|
544
|
+
if (this.state.currentStepIndex === 0) return this.state;
|
|
545
|
+
this.state = {
|
|
546
|
+
...this.state,
|
|
547
|
+
currentStepIndex: this.state.currentStepIndex - 1,
|
|
548
|
+
lastProgressAt: Date.now()
|
|
549
|
+
};
|
|
550
|
+
this.persist();
|
|
551
|
+
return this.state;
|
|
552
|
+
}
|
|
553
|
+
/** Limpia el estado activo (skip, complete, abandono, guía borrada). */
|
|
554
|
+
clear() {
|
|
555
|
+
this.state = null;
|
|
556
|
+
if (typeof localStorage === "undefined") return;
|
|
557
|
+
try {
|
|
558
|
+
localStorage.removeItem(this.storageKey);
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
isAbandoned(state) {
|
|
563
|
+
return Date.now() - state.lastProgressAt > WALKTHROUGH_ABANDONMENT_TIMEOUT_MS;
|
|
564
|
+
}
|
|
565
|
+
load() {
|
|
566
|
+
if (typeof localStorage === "undefined") return null;
|
|
567
|
+
try {
|
|
568
|
+
const raw = localStorage.getItem(this.storageKey);
|
|
569
|
+
if (!raw) return null;
|
|
570
|
+
const parsed = JSON.parse(raw);
|
|
571
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
572
|
+
const candidate = parsed;
|
|
573
|
+
if (typeof candidate.guideId !== "string" || typeof candidate.totalSteps !== "number" || typeof candidate.currentStepIndex !== "number" || typeof candidate.startedAt !== "number" || typeof candidate.lastProgressAt !== "number") {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
return candidate;
|
|
577
|
+
} catch {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
persist() {
|
|
582
|
+
if (typeof localStorage === "undefined" || !this.state) return;
|
|
583
|
+
try {
|
|
584
|
+
localStorage.setItem(this.storageKey, JSON.stringify(this.state));
|
|
585
|
+
} catch {
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
// src/plugins/guides/guides-controller.ts
|
|
591
|
+
var STATUS_BY_ACTION = {
|
|
592
|
+
shown: "shown",
|
|
593
|
+
dismissed: "dismissed",
|
|
594
|
+
cta_clicked: null,
|
|
595
|
+
step_advanced: null,
|
|
596
|
+
step_back: null,
|
|
597
|
+
completed: "completed"
|
|
598
|
+
};
|
|
599
|
+
var GuidesController = class {
|
|
600
|
+
constructor(client, config) {
|
|
601
|
+
this.client = client;
|
|
602
|
+
this.activeRenderers = /* @__PURE__ */ new Set();
|
|
603
|
+
/**
|
|
604
|
+
* Guías single-step actualmente en pantalla, por `guideId`. Evita repintar/
|
|
605
|
+
* duplicar la misma guía en cada pageview de una SPA (p.ej. una guía inline
|
|
606
|
+
* anclada a un elemento persistente). Se libera al cerrarse la guía.
|
|
607
|
+
*/
|
|
608
|
+
this.activeByGuideId = /* @__PURE__ */ new Map();
|
|
609
|
+
this.dispatched = /* @__PURE__ */ new Set();
|
|
610
|
+
this.activeWalkthroughRenderer = null;
|
|
611
|
+
this.activeWalkthroughGuide = null;
|
|
612
|
+
this.debug = config.debug === true;
|
|
613
|
+
this.apiUrl = config.apiUrl;
|
|
614
|
+
this.apiKey = config.apiKey;
|
|
615
|
+
this.resolver = config.resolver ?? new GuideResolverClient(config.apiUrl, config.apiKey, this.debug);
|
|
616
|
+
if (config.trackerQueue) {
|
|
617
|
+
this.trackerQueue = config.trackerQueue;
|
|
618
|
+
} else {
|
|
619
|
+
const httpClient = new GuideTrackerClient(config.apiUrl, config.apiKey);
|
|
620
|
+
this.trackerQueue = new GuideTrackerQueue({
|
|
621
|
+
client: httpClient,
|
|
622
|
+
flushIntervalMs: DEFAULT_TRACKER_FLUSH_INTERVAL_MS,
|
|
623
|
+
batchSize: DEFAULT_TRACKER_BATCH_SIZE,
|
|
624
|
+
maxRetries: DEFAULT_TRACKER_MAX_RETRIES,
|
|
625
|
+
onError: (err, item) => {
|
|
626
|
+
if (this.debug) {
|
|
627
|
+
console.warn(
|
|
628
|
+
`[veo] dropped guide interaction after retries: guideId=${item.guideId} action=${item.payload.action}`,
|
|
629
|
+
err
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
const namespace = namespaceFromApiKey(config.apiKey);
|
|
636
|
+
this.frequencyCache = config.frequencyCache ?? new GuideFrequencyCache(namespace);
|
|
637
|
+
this.walkthroughState = config.walkthroughState ?? new WalkthroughStateManager(namespace);
|
|
638
|
+
}
|
|
639
|
+
/** Cuántas interactions hay pendientes de envío. Útil para debug en demo. */
|
|
640
|
+
get pendingInteractions() {
|
|
641
|
+
return this.trackerQueue.size;
|
|
642
|
+
}
|
|
643
|
+
/** Limpia el cache local (botón "reset" del demo, tests). */
|
|
644
|
+
clearFrequencyCache() {
|
|
645
|
+
this.frequencyCache.clear();
|
|
646
|
+
}
|
|
647
|
+
/** Limpia el walkthrough activo (debug del demo). */
|
|
648
|
+
clearWalkthroughState() {
|
|
649
|
+
this.tearDownWalkthrough();
|
|
650
|
+
}
|
|
651
|
+
/** Punto de entrada principal. Llamado en cada pageview/identify. */
|
|
652
|
+
async checkGuides(pageUrl, sessionId) {
|
|
653
|
+
const endUserId = this.client.getEndUserId();
|
|
654
|
+
if (!endUserId) {
|
|
655
|
+
if (this.debug) console.log("[veo] guides: skipped, no endUserId yet");
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (await this.tryResumeWalkthrough(endUserId, sessionId, pageUrl)) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
let guides;
|
|
662
|
+
try {
|
|
663
|
+
guides = await this.resolver.resolve(endUserId, pageUrl);
|
|
664
|
+
} catch (err) {
|
|
665
|
+
if (this.debug) console.error("[veo] guides resolver threw:", err);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (this.debug) console.log(`[veo] guides: ${guides.length} eligible at ${pageUrl}`);
|
|
669
|
+
for (const guide of guides) {
|
|
670
|
+
if (this.frequencyCache.shouldFilter(guide.guideId, guide.displayFrequency)) {
|
|
671
|
+
if (this.debug) console.log(`[veo] guides: filtered locally guideId=${guide.guideId}`);
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
if (this.activeByGuideId.has(guide.guideId)) {
|
|
675
|
+
if (this.debug) console.log(`[veo] guides: already shown guideId=${guide.guideId}`);
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if (guide.guideType === "walkthrough" && guide.guideSteps.length > 1) {
|
|
679
|
+
if (this.walkthroughState.hasActive()) {
|
|
680
|
+
if (this.debug)
|
|
681
|
+
console.log(`[veo] guides: walkthrough ${guide.guideId} skipped (another active)`);
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
await this.startWalkthrough(guide, endUserId, sessionId, pageUrl);
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
this.runSingleStepRender(guide, sessionId, pageUrl);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/** Cierra todas las guías activas, limpia cache de dispatch y para timers. */
|
|
691
|
+
destroy() {
|
|
692
|
+
for (const renderer of this.activeRenderers) {
|
|
693
|
+
try {
|
|
694
|
+
renderer.destroy();
|
|
695
|
+
} catch (err) {
|
|
696
|
+
if (this.debug) console.error("[veo] renderer destroy failed:", err);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
this.activeRenderers.clear();
|
|
700
|
+
this.activeByGuideId.clear();
|
|
701
|
+
if (this.activeWalkthroughRenderer) {
|
|
702
|
+
try {
|
|
703
|
+
this.activeWalkthroughRenderer.destroy();
|
|
704
|
+
} catch {
|
|
705
|
+
}
|
|
706
|
+
this.activeWalkthroughRenderer = null;
|
|
707
|
+
this.activeWalkthroughGuide = null;
|
|
708
|
+
}
|
|
709
|
+
this.dispatched.clear();
|
|
710
|
+
this.trackerQueue.destroy();
|
|
711
|
+
}
|
|
712
|
+
runSingleStepRender(guide, sessionId, pageUrl) {
|
|
713
|
+
const renderer = this.createSingleStepRenderer(guide.guideType);
|
|
714
|
+
if (!renderer) return;
|
|
715
|
+
this.activeByGuideId.set(guide.guideId, renderer);
|
|
716
|
+
const delayMs = guide.activationRules.delayMs ?? 0;
|
|
717
|
+
window.setTimeout(
|
|
718
|
+
() => this.mountSingleStepRenderer(guide, renderer, sessionId, pageUrl),
|
|
719
|
+
delayMs
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
mountSingleStepRenderer(guide, renderer, sessionId, pageUrl) {
|
|
723
|
+
this.activeRenderers.add(renderer);
|
|
724
|
+
this.activeByGuideId.set(guide.guideId, renderer);
|
|
725
|
+
const onClose = () => {
|
|
726
|
+
renderer.destroy();
|
|
727
|
+
this.activeRenderers.delete(renderer);
|
|
728
|
+
if (this.activeByGuideId.get(guide.guideId) === renderer) {
|
|
729
|
+
this.activeByGuideId.delete(guide.guideId);
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
const onInteraction = (event) => {
|
|
733
|
+
this.handleInteraction(guide, sessionId, pageUrl, event);
|
|
734
|
+
};
|
|
735
|
+
const onTrack = (eventName, properties) => {
|
|
736
|
+
this.client.track(eventName, properties);
|
|
737
|
+
};
|
|
738
|
+
const onFormSubmit = (answers) => this.submitFormResponse(guide.guideId, answers);
|
|
739
|
+
try {
|
|
740
|
+
void renderer.render({ guide, onInteraction, onClose, onTrack, onFormSubmit });
|
|
741
|
+
} catch (err) {
|
|
742
|
+
if (this.debug) console.error("[veo] renderer.render threw:", err);
|
|
743
|
+
this.activeRenderers.delete(renderer);
|
|
744
|
+
if (this.activeByGuideId.get(guide.guideId) === renderer) {
|
|
745
|
+
this.activeByGuideId.delete(guide.guideId);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Si hay un walkthrough persistido, intenta pintar el step actual en
|
|
751
|
+
* la página actual. Retorna `true` si lo intentó (sea éxito o cancel),
|
|
752
|
+
* `false` si no había walkthrough activo.
|
|
753
|
+
*
|
|
754
|
+
* Cancela limpiamente (sin enviar `dismissed` falso) cuando:
|
|
755
|
+
* - La guía fue eliminada en el backend (404 → `fetchById` devuelve null).
|
|
756
|
+
* - La guía cambió de tipo y ya no es walkthrough.
|
|
757
|
+
* - El selector del step actual no aparece dentro del timeout.
|
|
758
|
+
*/
|
|
759
|
+
async tryResumeWalkthrough(endUserId, sessionId, pageUrl) {
|
|
760
|
+
const state = this.walkthroughState.current();
|
|
761
|
+
if (!state) return false;
|
|
762
|
+
const guide = await this.resolver.fetchById(state.guideId);
|
|
763
|
+
if (!guide || guide.guideType !== "walkthrough") {
|
|
764
|
+
if (this.debug)
|
|
765
|
+
console.log(`[veo] guides: walkthrough ${state.guideId} no longer available, cleaning up`);
|
|
766
|
+
this.tearDownWalkthrough();
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
769
|
+
const stepIndex = Math.min(state.currentStepIndex, guide.guideSteps.length - 1);
|
|
770
|
+
await this.renderWalkthroughStep(guide, stepIndex, endUserId, sessionId, pageUrl);
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
async startWalkthrough(guide, endUserId, sessionId, pageUrl) {
|
|
774
|
+
this.walkthroughState.start(guide.guideId, guide.guideSteps.length);
|
|
775
|
+
const rendered = await this.renderWalkthroughStep(guide, 0, endUserId, sessionId, pageUrl);
|
|
776
|
+
if (!rendered) return;
|
|
777
|
+
this.enqueueInteractionOnce(guide.guideId, endUserId, sessionId, pageUrl, "shown", 0);
|
|
778
|
+
this.frequencyCache.record(guide.guideId, "shown");
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Pinta el step indicado. Reusa la guía (no fetcha): el caller debe
|
|
782
|
+
* traerla. Retorna `true` si el render fue exitoso. En caso contrario
|
|
783
|
+
* ya disparó el teardown — el caller no debe seguir reportando.
|
|
784
|
+
*/
|
|
785
|
+
async renderWalkthroughStep(guide, stepIndex, endUserId, sessionId, pageUrl) {
|
|
786
|
+
if (this.activeWalkthroughRenderer) {
|
|
787
|
+
this.activeWalkthroughRenderer.destroy();
|
|
788
|
+
this.activeWalkthroughRenderer = null;
|
|
789
|
+
}
|
|
790
|
+
const renderer = new WalkthroughRenderer();
|
|
791
|
+
this.activeWalkthroughGuide = guide;
|
|
792
|
+
const callbacks = {
|
|
793
|
+
onNext: () => this.handleWalkthroughNext(endUserId, sessionId, pageUrl),
|
|
794
|
+
onBack: () => this.handleWalkthroughBack(endUserId, sessionId, pageUrl),
|
|
795
|
+
onSkip: () => this.handleWalkthroughSkip(endUserId, sessionId, pageUrl),
|
|
796
|
+
onComplete: () => this.handleWalkthroughComplete(endUserId, sessionId, pageUrl)
|
|
797
|
+
};
|
|
798
|
+
let success = false;
|
|
799
|
+
try {
|
|
800
|
+
success = await renderer.render({ guide, currentStepIndex: stepIndex, callbacks });
|
|
801
|
+
} catch (err) {
|
|
802
|
+
if (this.debug) console.error("[veo] walkthrough renderer threw:", err);
|
|
803
|
+
}
|
|
804
|
+
if (!success) {
|
|
805
|
+
if (this.debug)
|
|
806
|
+
console.warn(
|
|
807
|
+
`[veo] walkthrough ${guide.guideId} step ${stepIndex} could not render, cancelling`
|
|
808
|
+
);
|
|
809
|
+
this.tearDownWalkthrough();
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
this.activeWalkthroughRenderer = renderer;
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
handleWalkthroughNext(endUserId, sessionId, pageUrl) {
|
|
816
|
+
const guide = this.activeWalkthroughGuide;
|
|
817
|
+
if (!guide) return;
|
|
818
|
+
const newState = this.walkthroughState.advance();
|
|
819
|
+
if (!newState) {
|
|
820
|
+
this.handleWalkthroughComplete(endUserId, sessionId, pageUrl);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
this.enqueueInteraction(
|
|
824
|
+
guide.guideId,
|
|
825
|
+
endUserId,
|
|
826
|
+
sessionId,
|
|
827
|
+
pageUrl,
|
|
828
|
+
"step_advanced",
|
|
829
|
+
newState.currentStepIndex
|
|
830
|
+
);
|
|
831
|
+
void this.renderWalkthroughStep(
|
|
832
|
+
guide,
|
|
833
|
+
newState.currentStepIndex,
|
|
834
|
+
endUserId,
|
|
835
|
+
sessionId,
|
|
836
|
+
pageUrl
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
handleWalkthroughBack(endUserId, sessionId, pageUrl) {
|
|
840
|
+
const guide = this.activeWalkthroughGuide;
|
|
841
|
+
if (!guide) return;
|
|
842
|
+
const state = this.walkthroughState.current();
|
|
843
|
+
if (!state || state.currentStepIndex === 0) return;
|
|
844
|
+
const newState = this.walkthroughState.back();
|
|
845
|
+
if (!newState) return;
|
|
846
|
+
this.enqueueInteraction(
|
|
847
|
+
guide.guideId,
|
|
848
|
+
endUserId,
|
|
849
|
+
sessionId,
|
|
850
|
+
pageUrl,
|
|
851
|
+
"step_back",
|
|
852
|
+
newState.currentStepIndex
|
|
853
|
+
);
|
|
854
|
+
void this.renderWalkthroughStep(
|
|
855
|
+
guide,
|
|
856
|
+
newState.currentStepIndex,
|
|
857
|
+
endUserId,
|
|
858
|
+
sessionId,
|
|
859
|
+
pageUrl
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
handleWalkthroughSkip(endUserId, sessionId, pageUrl) {
|
|
863
|
+
const guide = this.activeWalkthroughGuide;
|
|
864
|
+
const state = this.walkthroughState.current();
|
|
865
|
+
if (!guide || !state) return;
|
|
866
|
+
this.enqueueInteraction(
|
|
867
|
+
guide.guideId,
|
|
868
|
+
endUserId,
|
|
869
|
+
sessionId,
|
|
870
|
+
pageUrl,
|
|
871
|
+
"dismissed",
|
|
872
|
+
state.currentStepIndex
|
|
873
|
+
);
|
|
874
|
+
this.frequencyCache.record(guide.guideId, "dismissed");
|
|
875
|
+
this.tearDownWalkthrough();
|
|
876
|
+
}
|
|
877
|
+
handleWalkthroughComplete(endUserId, sessionId, pageUrl) {
|
|
878
|
+
const guide = this.activeWalkthroughGuide;
|
|
879
|
+
const state = this.walkthroughState.current();
|
|
880
|
+
if (!guide || !state) return;
|
|
881
|
+
this.enqueueInteraction(
|
|
882
|
+
guide.guideId,
|
|
883
|
+
endUserId,
|
|
884
|
+
sessionId,
|
|
885
|
+
pageUrl,
|
|
886
|
+
"completed",
|
|
887
|
+
state.currentStepIndex
|
|
888
|
+
);
|
|
889
|
+
this.frequencyCache.record(guide.guideId, "completed");
|
|
890
|
+
this.tearDownWalkthrough();
|
|
891
|
+
}
|
|
892
|
+
tearDownWalkthrough() {
|
|
893
|
+
if (this.activeWalkthroughRenderer) {
|
|
894
|
+
try {
|
|
895
|
+
this.activeWalkthroughRenderer.destroy();
|
|
896
|
+
} catch {
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
this.activeWalkthroughRenderer = null;
|
|
900
|
+
this.activeWalkthroughGuide = null;
|
|
901
|
+
this.walkthroughState.clear();
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Tras cada interaction single-step:
|
|
905
|
+
* 1. dedupe (no enviar 2× la misma `(guideId, action)` por sesión)
|
|
906
|
+
* 2. actualizar el cache local si corresponde
|
|
907
|
+
* 3. encolar el POST
|
|
908
|
+
*
|
|
909
|
+
* El endUserId se lee del client EN ESTE MOMENTO (no en el render),
|
|
910
|
+
* por si el usuario se identificó después de que la guía apareció.
|
|
911
|
+
*/
|
|
912
|
+
handleInteraction(guide, sessionId, pageUrl, event) {
|
|
913
|
+
const dedupKey = `${guide.guideId}:${event.action}`;
|
|
914
|
+
if (this.dispatched.has(dedupKey)) {
|
|
915
|
+
if (this.debug) console.log(`[veo] guides: dedup hit ${dedupKey}`);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
this.dispatched.add(dedupKey);
|
|
919
|
+
const cacheStatus = STATUS_BY_ACTION[event.action];
|
|
920
|
+
if (cacheStatus) this.frequencyCache.record(guide.guideId, cacheStatus);
|
|
921
|
+
const endUserId = this.client.getEndUserId();
|
|
922
|
+
if (!endUserId) {
|
|
923
|
+
if (this.debug)
|
|
924
|
+
console.warn("[veo] guides: interaction without endUserId \u2014 skipping enqueue");
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
this.trackerQueue.enqueue({
|
|
928
|
+
guideId: guide.guideId,
|
|
929
|
+
payload: {
|
|
930
|
+
endUserId,
|
|
931
|
+
sessionId,
|
|
932
|
+
action: event.action,
|
|
933
|
+
stepIndex: event.stepIndex,
|
|
934
|
+
pageUrl,
|
|
935
|
+
pagePath: extractPath(pageUrl)
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Encola una interacción de walkthrough SIN pasar por el dedup set.
|
|
941
|
+
* Acciones como `step_advanced` se repiten naturalmente (cada next es
|
|
942
|
+
* una navegación distinta) y deduplicarlas perdería datos.
|
|
943
|
+
*/
|
|
944
|
+
enqueueInteraction(guideId, endUserId, sessionId, pageUrl, action, stepIndex) {
|
|
945
|
+
this.trackerQueue.enqueue({
|
|
946
|
+
guideId,
|
|
947
|
+
payload: {
|
|
948
|
+
endUserId,
|
|
949
|
+
sessionId,
|
|
950
|
+
action,
|
|
951
|
+
stepIndex,
|
|
952
|
+
pageUrl,
|
|
953
|
+
pagePath: extractPath(pageUrl)
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Encola con dedup por sesión del SDK. Para `shown` del primer step:
|
|
959
|
+
* no queremos re-emitirlo si el usuario navega entre dos páginas que
|
|
960
|
+
* matchean el mismo step.
|
|
961
|
+
*/
|
|
962
|
+
enqueueInteractionOnce(guideId, endUserId, sessionId, pageUrl, action, stepIndex) {
|
|
963
|
+
const dedupKey = `${guideId}:${action}:${stepIndex}`;
|
|
964
|
+
if (this.dispatched.has(dedupKey)) return;
|
|
965
|
+
this.dispatched.add(dedupKey);
|
|
966
|
+
this.enqueueInteraction(guideId, endUserId, sessionId, pageUrl, action, stepIndex);
|
|
967
|
+
}
|
|
968
|
+
createSingleStepRenderer(type) {
|
|
969
|
+
switch (type) {
|
|
970
|
+
case "modal":
|
|
971
|
+
return new ModalRenderer();
|
|
972
|
+
case "banner":
|
|
973
|
+
return new BannerRenderer();
|
|
974
|
+
case "tooltip":
|
|
975
|
+
return new TooltipRenderer();
|
|
976
|
+
case "custom":
|
|
977
|
+
return new CustomRenderer();
|
|
978
|
+
case "inline":
|
|
979
|
+
return new InlineRenderer();
|
|
980
|
+
case "inline-custom":
|
|
981
|
+
return new InlineCustomRenderer();
|
|
982
|
+
case "form":
|
|
983
|
+
return new FormRenderer();
|
|
984
|
+
case "inline-form":
|
|
985
|
+
return new InlineFormRenderer();
|
|
986
|
+
case "walkthrough":
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Envía las respuestas de una guía `form` al backend, que las mergea como
|
|
992
|
+
* propiedades del usuario. Usa el endUserId actual (o el anónimo si aún no
|
|
993
|
+
* hubo identify — el backend pre-crea la fila igual que identify).
|
|
994
|
+
*/
|
|
995
|
+
async submitFormResponse(guideId, answers) {
|
|
996
|
+
const endUserId = this.client.getEndUserId() ?? this.client.getAnonymousId();
|
|
997
|
+
try {
|
|
998
|
+
const res = await fetch(`${this.apiUrl}/v1/guides/${guideId}/form-response`, {
|
|
999
|
+
method: "POST",
|
|
1000
|
+
headers: { "Content-Type": "application/json", "X-Api-Key": this.apiKey },
|
|
1001
|
+
body: JSON.stringify({ endUserId, answers })
|
|
1002
|
+
});
|
|
1003
|
+
if (!res.ok && this.debug) {
|
|
1004
|
+
console.warn("[veo] form-response respondi\xF3", res.status);
|
|
1005
|
+
}
|
|
1006
|
+
return res.ok;
|
|
1007
|
+
} catch (err) {
|
|
1008
|
+
if (this.debug) console.warn("[veo] form-response fall\xF3:", err);
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
function extractPath(url) {
|
|
1014
|
+
try {
|
|
1015
|
+
return new URL(url).pathname;
|
|
1016
|
+
} catch {
|
|
1017
|
+
return "/";
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// src/plugins/guides/index.ts
|
|
1022
|
+
function guidesPlugin(config) {
|
|
1023
|
+
return definePlugin({
|
|
1024
|
+
name: "guides",
|
|
1025
|
+
install(client) {
|
|
1026
|
+
if (!hasWindow()) return;
|
|
1027
|
+
const controller = new GuidesController(client, config);
|
|
1028
|
+
window.__veoGuides = { controller };
|
|
1029
|
+
client.on("identify", ({ sessionId }) => {
|
|
1030
|
+
void controller.checkGuides(window.location.href, sessionId);
|
|
1031
|
+
});
|
|
1032
|
+
client.on("pageview", ({ url, sessionId }) => {
|
|
1033
|
+
void controller.checkGuides(url, sessionId);
|
|
1034
|
+
});
|
|
1035
|
+
void controller.checkGuides(window.location.href, client.getSessionId());
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
export { definePlugin, guidesPlugin, runPreviewMode };
|
|
1041
|
+
//# sourceMappingURL=chunk-G5CKTGEG.mjs.map
|
|
1042
|
+
//# sourceMappingURL=chunk-G5CKTGEG.mjs.map
|