veo-sdk 0.6.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -26,6 +26,11 @@ interface VeoAutocaptureConfig {
26
26
  captureElementMetadata?: boolean;
27
27
  /** Niveles de ancestros a capturar. Default: 5. Max: 10. */
28
28
  captureAncestors?: number;
29
+ /**
30
+ * Calidad de los clics: emite `$dead_click` (clic sin reacción), `$rage_click`
31
+ * (ráfaga de clics) y `$click_error` (clic seguido de un error). Default: true.
32
+ */
33
+ captureClickQuality?: boolean;
29
34
  }
30
35
  /**
31
36
  * Configuración del cliente Veo.
package/dist/index.d.ts CHANGED
@@ -26,6 +26,11 @@ interface VeoAutocaptureConfig {
26
26
  captureElementMetadata?: boolean;
27
27
  /** Niveles de ancestros a capturar. Default: 5. Max: 10. */
28
28
  captureAncestors?: number;
29
+ /**
30
+ * Calidad de los clics: emite `$dead_click` (clic sin reacción), `$rage_click`
31
+ * (ráfaga de clics) y `$click_error` (clic seguido de un error). Default: true.
32
+ */
33
+ captureClickQuality?: boolean;
29
34
  }
30
35
  /**
31
36
  * Configuración del cliente Veo.
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { definePlugin, guidesPlugin, runPreviewMode } from './chunk-L3QQJLDJ.mjs';
2
- export { definePlugin, guidesPlugin } from './chunk-L3QQJLDJ.mjs';
3
- import { hasWindow, hasDocument, uuidv7, DEFAULT_AUTOCAPTURE_CONFIG, MAX_ANCESTORS, hasLocalStorage, hasNavigatorSendBeacon, isBuilderMode, ALWAYS_BLOCK_SELECTORS, buildSelectorPath, resolveBuilderToken, filterHumanClasses, PRIORITY_ATTRIBUTES, SENSITIVE_ATTRIBUTES, getPreviewToken, previewGuide, closeGuidePreview } from './chunk-ZILDR4YE.mjs';
4
- export { closeGuidePreview, previewGuide } from './chunk-ZILDR4YE.mjs';
1
+ import { definePlugin, guidesPlugin, runPreviewMode } from './chunk-3JEAPZ6A.mjs';
2
+ export { definePlugin, guidesPlugin } from './chunk-3JEAPZ6A.mjs';
3
+ import { hasWindow, hasDocument, uuidv7, DEFAULT_AUTOCAPTURE_CONFIG, MAX_ANCESTORS, hasLocalStorage, hasNavigatorSendBeacon, isBuilderMode, buildSelectorPath, ALWAYS_BLOCK_SELECTORS, resolveBuilderToken, filterHumanClasses, PRIORITY_ATTRIBUTES, SENSITIVE_ATTRIBUTES, getPreviewToken, previewGuide, closeGuidePreview } from './chunk-SZCIXA5X.mjs';
4
+ export { closeGuidePreview, previewGuide } from './chunk-SZCIXA5X.mjs';
5
5
  import './chunk-PTG3BTJE.mjs';
6
6
 
7
7
  // src/contract/v1.ts
@@ -62,6 +62,294 @@ function deriveBuilderUrl() {
62
62
  return null;
63
63
  }
64
64
 
65
+ // src/plugins/autocapture/click-quality.ts
66
+ var NO_DEAD_TAGS = /* @__PURE__ */ new Set(["input", "textarea", "select", "option", "label"]);
67
+ var MAX_ERROR_MESSAGE = 200;
68
+ var HISTORY_MS = 3e3;
69
+ var DEFAULTS = {
70
+ reactionWindowMs: 1e3,
71
+ errorWindowMs: 2e3,
72
+ rageClicks: 3,
73
+ rageWindowMs: 1e3,
74
+ rageClicksSlow: 5,
75
+ rageWindowSlowMs: 3e3,
76
+ maxPerElement: 50
77
+ };
78
+ function describeError(value) {
79
+ let text;
80
+ if (value instanceof Error) text = `${value.name}: ${value.message}`;
81
+ else if (typeof value === "string") text = value;
82
+ else {
83
+ try {
84
+ text = JSON.stringify(value) ?? String(value);
85
+ } catch {
86
+ text = String(value);
87
+ }
88
+ }
89
+ return text.slice(0, MAX_ERROR_MESSAGE);
90
+ }
91
+ var ClickQualityTracker = class {
92
+ constructor(emit, options = {}) {
93
+ this.emit = emit;
94
+ this.installed = false;
95
+ this.cleanups = [];
96
+ // Sensores globales (contadores: cada observación compara su foto inicial).
97
+ this.netCount = 0;
98
+ this.navCount = 0;
99
+ this.focusCount = 0;
100
+ this.unloading = false;
101
+ this.errors = [];
102
+ this.history = /* @__PURE__ */ new Map();
103
+ this.bursts = /* @__PURE__ */ new Map();
104
+ this.emitted = /* @__PURE__ */ new Map();
105
+ this.opts = { ...DEFAULTS, ...options };
106
+ }
107
+ install() {
108
+ if (this.installed || !hasWindow()) return;
109
+ this.installed = true;
110
+ const on = (type, handler, capture = true) => {
111
+ window.addEventListener(type, handler, capture);
112
+ this.cleanups.push(() => window.removeEventListener(type, handler, capture));
113
+ };
114
+ on("error", (e) => {
115
+ const err = e;
116
+ this.pushError("error", err.error ?? err.message ?? "error");
117
+ });
118
+ on("unhandledrejection", (e) => {
119
+ this.pushError("unhandledrejection", e.reason ?? "rejection");
120
+ });
121
+ on("popstate", () => this.navCount++);
122
+ on("hashchange", () => this.navCount++);
123
+ on("focusin", () => this.focusCount++);
124
+ on("pagehide", () => {
125
+ this.unloading = true;
126
+ });
127
+ on("beforeunload", () => {
128
+ this.unloading = true;
129
+ });
130
+ this.patchHistory();
131
+ this.patchConsole();
132
+ this.patchFetch();
133
+ this.patchXhr();
134
+ }
135
+ destroy() {
136
+ for (const c of this.cleanups) c();
137
+ this.cleanups = [];
138
+ for (const b of this.bursts.values()) window.clearTimeout(b.timer);
139
+ this.bursts.clear();
140
+ this.history.clear();
141
+ this.installed = false;
142
+ }
143
+ /** Llamar por cada clic real sobre un elemento interactivo, ANTES del throttle. */
144
+ onClick(element, event) {
145
+ if (!this.installed) return;
146
+ const now = Date.now();
147
+ const obs = { at: now, reacted: false };
148
+ this.remember(element, obs);
149
+ this.observeReaction(element, obs, event);
150
+ this.observeErrors(element, now);
151
+ this.trackRage(element, now);
152
+ }
153
+ // ---------------------------------------------------------------- dead ----
154
+ observeReaction(element, obs, event) {
155
+ const netAtClick = this.netCount;
156
+ const navAtClick = this.navCount;
157
+ const focusAtClick = this.focusCount;
158
+ const hrefAtClick = window.location.href;
159
+ const skipDead = this.isDeadExempt(element, event);
160
+ let observer = null;
161
+ if (typeof MutationObserver !== "undefined") {
162
+ observer = new MutationObserver(() => {
163
+ obs.reacted = true;
164
+ observer?.disconnect();
165
+ observer = null;
166
+ });
167
+ observer.observe(document.documentElement, {
168
+ childList: true,
169
+ attributes: true,
170
+ characterData: true,
171
+ subtree: true
172
+ });
173
+ }
174
+ window.setTimeout(() => {
175
+ observer?.disconnect();
176
+ const reacted = obs.reacted || this.unloading || this.netCount !== netAtClick || this.navCount !== navAtClick || this.focusCount !== focusAtClick || window.location.href !== hrefAtClick;
177
+ obs.reacted = reacted;
178
+ if (!reacted && !skipDead) this.report("$dead_click", element, {});
179
+ }, this.opts.reactionWindowMs);
180
+ }
181
+ isDeadExempt(element, event) {
182
+ if (event && (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey)) return true;
183
+ if (event && typeof event.button === "number" && event.button !== 0) return true;
184
+ const tag = element.tagName.toLowerCase();
185
+ if (NO_DEAD_TAGS.has(tag)) return true;
186
+ if (element.closest('[contenteditable=""],[contenteditable="true"]')) return true;
187
+ const anchor = element.closest("a");
188
+ if (anchor) {
189
+ if (anchor.target === "_blank" || anchor.hasAttribute("download")) return true;
190
+ const href = anchor.getAttribute("href") ?? "";
191
+ if (/^(mailto|tel|sms):/i.test(href)) return true;
192
+ }
193
+ return false;
194
+ }
195
+ // --------------------------------------------------------------- error ----
196
+ observeErrors(element, clickedAt) {
197
+ const deadline = clickedAt + this.opts.errorWindowMs;
198
+ const check = () => {
199
+ const err = this.errors.find((e) => e.at >= clickedAt && e.at <= deadline);
200
+ if (err) {
201
+ this.report("$click_error", element, {
202
+ $error_source: err.source,
203
+ $error_message: err.message
204
+ });
205
+ return;
206
+ }
207
+ if (Date.now() < deadline) window.setTimeout(check, 250);
208
+ };
209
+ window.setTimeout(check, 250);
210
+ }
211
+ pushError(source, value) {
212
+ const message = describeError(value);
213
+ if (message.startsWith("[veo]")) return;
214
+ this.errors.push({ at: Date.now(), source, message });
215
+ const cutoff = Date.now() - this.opts.errorWindowMs - 1e3;
216
+ while (this.errors.length > 0 && this.errors[0].at < cutoff) {
217
+ this.errors.shift();
218
+ }
219
+ if (this.errors.length > 50) this.errors.splice(0, this.errors.length - 50);
220
+ }
221
+ // ---------------------------------------------------------------- rage ----
222
+ remember(element, obs) {
223
+ const list = this.history.get(element) ?? [];
224
+ const cutoff = obs.at - HISTORY_MS;
225
+ const kept = list.filter((o) => o.at >= cutoff);
226
+ kept.push(obs);
227
+ this.history.set(element, kept);
228
+ if (this.history.size > 100) {
229
+ for (const [el, obsList] of this.history) {
230
+ if (obsList.every((o) => o.at < cutoff)) this.history.delete(el);
231
+ }
232
+ }
233
+ }
234
+ trackRage(element, now) {
235
+ const list = this.history.get(element) ?? [];
236
+ const fast = list.filter((o) => o.at >= now - this.opts.rageWindowMs).length;
237
+ const slow = list.filter((o) => o.at >= now - this.opts.rageWindowSlowMs).length;
238
+ const existing = this.bursts.get(element);
239
+ const isRage = fast >= this.opts.rageClicks || slow >= this.opts.rageClicksSlow;
240
+ if (!existing && !isRage) return;
241
+ const burst = existing ?? {
242
+ startedAt: (list.find((o) => o.at >= now - this.opts.rageWindowSlowMs) ?? { at: now }).at,
243
+ count: 0,
244
+ timer: 0
245
+ };
246
+ burst.count = list.filter((o) => o.at >= burst.startedAt).length;
247
+ window.clearTimeout(burst.timer);
248
+ burst.timer = window.setTimeout(() => {
249
+ this.bursts.delete(element);
250
+ const clicks = (this.history.get(element) ?? []).filter((o) => o.at >= burst.startedAt);
251
+ this.report("$rage_click", element, {
252
+ $click_count: Math.max(clicks.length, burst.count),
253
+ $reacted_clicks: clicks.filter((o) => o.reacted).length
254
+ });
255
+ }, this.opts.reactionWindowMs + 50);
256
+ this.bursts.set(element, burst);
257
+ }
258
+ // ---------------------------------------------------------------- emit ----
259
+ report(kind, element, extra) {
260
+ const key = buildSelectorPath(element, 5);
261
+ const counts = this.emitted.get(key) ?? { $dead_click: 0, $rage_click: 0, $click_error: 0 };
262
+ if (counts[kind] >= this.opts.maxPerElement) return;
263
+ counts[kind]++;
264
+ this.emitted.set(key, counts);
265
+ try {
266
+ this.emit(kind, element, extra);
267
+ } catch {
268
+ }
269
+ }
270
+ // ------------------------------------------------------------- patches ----
271
+ patchHistory() {
272
+ const h = window.history;
273
+ if (!h) return;
274
+ const origPush = h.pushState;
275
+ const origReplace = h.replaceState;
276
+ const self = this;
277
+ h.pushState = function(...args) {
278
+ self.navCount++;
279
+ return origPush.apply(this, args);
280
+ };
281
+ h.replaceState = function(...args) {
282
+ self.navCount++;
283
+ return origReplace.apply(this, args);
284
+ };
285
+ this.cleanups.push(() => {
286
+ h.pushState = origPush;
287
+ h.replaceState = origReplace;
288
+ });
289
+ }
290
+ patchConsole() {
291
+ const orig = console.error;
292
+ const self = this;
293
+ console.error = function(...args) {
294
+ try {
295
+ self.pushError("console", args.map(describeError).join(" "));
296
+ } catch {
297
+ }
298
+ return orig.apply(this, args);
299
+ };
300
+ this.cleanups.push(() => {
301
+ console.error = orig;
302
+ });
303
+ }
304
+ /**
305
+ * `fetch`: cuenta la petición (reacción) y registra respuestas ≥500 o
306
+ * rechazos como error. Se observa con `.then(ok, err)` sobre la promesa
307
+ * original: eso marca el rechazo como manejado para el navegador, así que
308
+ * un fetch fallido sin catch de la app no dispara `unhandledrejection` —
309
+ * lo registramos nosotros como error de red.
310
+ */
311
+ patchFetch() {
312
+ if (typeof window.fetch !== "function") return;
313
+ const orig = window.fetch;
314
+ const self = this;
315
+ window.fetch = function(...args) {
316
+ self.netCount++;
317
+ const p = orig.apply(this, args);
318
+ if (p && typeof p.then === "function") {
319
+ p.then(
320
+ (res) => {
321
+ if (res && res.status >= 500) self.pushError("fetch", `${res.status} ${res.url}`);
322
+ },
323
+ (err) => self.pushError("fetch", err)
324
+ );
325
+ }
326
+ return p;
327
+ };
328
+ this.cleanups.push(() => {
329
+ window.fetch = orig;
330
+ });
331
+ }
332
+ patchXhr() {
333
+ const proto = typeof XMLHttpRequest !== "undefined" ? XMLHttpRequest.prototype : null;
334
+ if (!proto) return;
335
+ const origSend = proto.send;
336
+ const self = this;
337
+ proto.send = function(...args) {
338
+ self.netCount++;
339
+ try {
340
+ this.addEventListener("loadend", () => {
341
+ if (this.status >= 500) self.pushError("xhr", `${this.status} ${this.responseURL}`);
342
+ });
343
+ } catch {
344
+ }
345
+ return origSend.apply(this, args);
346
+ };
347
+ this.cleanups.push(() => {
348
+ proto.send = origSend;
349
+ });
350
+ }
351
+ };
352
+
65
353
  // src/plugins/autocapture/pii-guard.ts
66
354
  function isElementBlocked(element, extraBlockSelectors = []) {
67
355
  const allBlockers = [...ALWAYS_BLOCK_SELECTORS, ...extraBlockSelectors];
@@ -165,6 +453,7 @@ var DomListener = class {
165
453
  constructor(client, config) {
166
454
  this.client = client;
167
455
  this.cleanups = [];
456
+ this.quality = null;
168
457
  const d = DEFAULT_AUTOCAPTURE_CONFIG;
169
458
  this.config = {
170
459
  enabled: config.enabled ?? d.enabled,
@@ -177,12 +466,20 @@ var DomListener = class {
177
466
  throttleMs: config.throttleMs ?? d.throttleMs,
178
467
  maxTextLength: config.maxTextLength ?? d.maxTextLength,
179
468
  captureElementMetadata: config.captureElementMetadata ?? d.captureElementMetadata,
180
- captureAncestors: Math.min(config.captureAncestors ?? d.captureAncestors, MAX_ANCESTORS)
469
+ captureAncestors: Math.min(config.captureAncestors ?? d.captureAncestors, MAX_ANCESTORS),
470
+ captureClickQuality: config.captureClickQuality ?? d.captureClickQuality
181
471
  };
182
472
  this.throttle = new ElementThrottle(this.config.throttleMs);
183
473
  }
184
474
  install() {
185
475
  if (!hasDocument() || !this.config.enabled) return;
476
+ if (this.config.captureClicks && this.config.captureClickQuality) {
477
+ this.quality = new ClickQualityTracker((kind, element, extra) => {
478
+ const payload = buildAutocapturePayload("click", element, this.config);
479
+ this.client.track(kind, { ...payload, ...extra });
480
+ });
481
+ this.quality.install();
482
+ }
186
483
  if (this.config.captureClicks) this.attach("click", (e) => this.handleClick(e));
187
484
  if (this.config.captureSubmits) this.attach("submit", (e) => this.handleSubmit(e), true);
188
485
  if (this.config.captureChanges) this.attach("change", (e) => this.handleChange(e), true);
@@ -190,6 +487,8 @@ var DomListener = class {
190
487
  destroy() {
191
488
  for (const cleanup of this.cleanups) cleanup();
192
489
  this.cleanups = [];
490
+ this.quality?.destroy();
491
+ this.quality = null;
193
492
  }
194
493
  attach(eventName, handler, useCapture = false) {
195
494
  const listener = (e) => {
@@ -208,6 +507,7 @@ var DomListener = class {
208
507
  const meaningful = this.findMeaningfulAncestor(target);
209
508
  if (!meaningful) return;
210
509
  if (isElementBlocked(meaningful, this.config.blockSelectors)) return;
510
+ this.quality?.onClick(meaningful, event);
211
511
  if (!this.throttle.shouldFire(meaningful)) return;
212
512
  this.emit("click", meaningful);
213
513
  }
@@ -966,7 +1266,7 @@ function init(config) {
966
1266
  // Fallback de carga del builder para bundlers: `import()` dinámico → el bundler
967
1267
  // lo code-splitea (solo se descarga en modo builder). Así el consumidor por
968
1268
  // bundler NO necesita copiar `veo-builder.js` ni setear `builderUrl`.
969
- () => import('./builder-ENKEIVF2.mjs').then((m) => m.initBuilderMode()),
1269
+ () => import('./builder-PPLIAUCM.mjs').then((m) => m.initBuilderMode()),
970
1270
  // Detector de tema: también por `import()` dinámico → chunk aparte que el
971
1271
  // bundler solo descarga cuando toca detectar (como mucho una vez al día).
972
1272
  () => import('./detect-theme-X4K3TLUL.mjs')