veo-sdk 0.6.2 → 0.6.4

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.
@@ -127,6 +132,8 @@ type VeoClientEventListener<E extends VeoClientEventName> = (payload: VeoClientE
127
132
  * Forward declaration del client (implementación viene en core/client.ts).
128
133
  */
129
134
  interface VeoClient {
135
+ /** Versión del SDK cargado (la de package.json en el build). `window.veo.version` en consola. */
136
+ readonly version: string;
130
137
  identify(payload: {
131
138
  visitor: VeoVisitor;
132
139
  organization?: VeoOrganization;
@@ -180,7 +187,8 @@ declare function guidesPlugin(config: GuidesPluginConfig): VeoPlugin;
180
187
  * - veo-backend/src/modules/ingest/dto/*.dto.ts
181
188
  * - CONTRACT.md
182
189
  */
183
- declare const SDK_VERSION = "0.0.1";
190
+ /** Versión del SDK, inyectada en build desde package.json (tsup `define`); fallback en tests/dev. */
191
+ declare const SDK_VERSION: string;
184
192
 
185
193
  /**
186
194
  * Cliente principal del SDK. Expone la API pública (`identify`, `track`,
@@ -189,6 +197,7 @@ declare const SDK_VERSION = "0.0.1";
189
197
  */
190
198
  declare class Client implements VeoClient {
191
199
  private readonly config;
200
+ readonly version: string;
192
201
  private readonly transport;
193
202
  private readonly storage;
194
203
  private readonly session;
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.
@@ -127,6 +132,8 @@ type VeoClientEventListener<E extends VeoClientEventName> = (payload: VeoClientE
127
132
  * Forward declaration del client (implementación viene en core/client.ts).
128
133
  */
129
134
  interface VeoClient {
135
+ /** Versión del SDK cargado (la de package.json en el build). `window.veo.version` en consola. */
136
+ readonly version: string;
130
137
  identify(payload: {
131
138
  visitor: VeoVisitor;
132
139
  organization?: VeoOrganization;
@@ -180,7 +187,8 @@ declare function guidesPlugin(config: GuidesPluginConfig): VeoPlugin;
180
187
  * - veo-backend/src/modules/ingest/dto/*.dto.ts
181
188
  * - CONTRACT.md
182
189
  */
183
- declare const SDK_VERSION = "0.0.1";
190
+ /** Versión del SDK, inyectada en build desde package.json (tsup `define`); fallback en tests/dev. */
191
+ declare const SDK_VERSION: string;
184
192
 
185
193
  /**
186
194
  * Cliente principal del SDK. Expone la API pública (`identify`, `track`,
@@ -189,6 +197,7 @@ declare const SDK_VERSION = "0.0.1";
189
197
  */
190
198
  declare class Client implements VeoClient {
191
199
  private readonly config;
200
+ readonly version: string;
192
201
  private readonly transport;
193
202
  private readonly storage;
194
203
  private readonly session;
package/dist/index.mjs CHANGED
@@ -1,11 +1,11 @@
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-72YH2GXC.mjs';
2
+ export { definePlugin, guidesPlugin } from './chunk-72YH2GXC.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-CMPOHTUM.mjs';
4
+ export { closeGuidePreview, previewGuide } from './chunk-CMPOHTUM.mjs';
5
5
  import './chunk-PTG3BTJE.mjs';
6
6
 
7
7
  // src/contract/v1.ts
8
- var SDK_VERSION = "0.0.1";
8
+ var SDK_VERSION = "0.6.4";
9
9
  var DEFAULT_FLUSH_INTERVAL_MS = 5e3;
10
10
  var DEFAULT_FLUSH_BATCH_SIZE = 20;
11
11
  var DEFAULT_API_URL = "https://veo-backend-production-8c78.up.railway.app";
@@ -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
  }
@@ -704,6 +1004,7 @@ function stableStringify(value) {
704
1004
  var Client = class {
705
1005
  constructor(config) {
706
1006
  this.config = config;
1007
+ this.version = SDK_VERSION;
707
1008
  this.plugins = [];
708
1009
  this.currentOrganizationId = null;
709
1010
  this.listeners = {
@@ -966,7 +1267,7 @@ function init(config) {
966
1267
  // Fallback de carga del builder para bundlers: `import()` dinámico → el bundler
967
1268
  // lo code-splitea (solo se descarga en modo builder). Así el consumidor por
968
1269
  // bundler NO necesita copiar `veo-builder.js` ni setear `builderUrl`.
969
- () => import('./builder-ENKEIVF2.mjs').then((m) => m.initBuilderMode()),
1270
+ () => import('./builder-5F2EFT4X.mjs').then((m) => m.initBuilderMode()),
970
1271
  // Detector de tema: también por `import()` dinámico → chunk aparte que el
971
1272
  // bundler solo descarga cuando toca detectar (como mucho una vez al día).
972
1273
  () => import('./detect-theme-X4K3TLUL.mjs')