@magicfeedback/popup-sdk 1.1.5 → 1.1.7

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.
@@ -76,6 +76,8 @@ interface AnalyticsEnvelope {
76
76
  device?: DeviceInfo;
77
77
  /** User attributes del cliente, usados para breakdowns (registration_status, pass_type, …). */
78
78
  attributes: Record<string, string>;
79
+ /** Métricas del host: valores medibles persistentes → van en `feedback.metrics` del body. */
80
+ metrics?: Record<string, string>;
79
81
  };
80
82
  events: AnalyticsEvent[];
81
83
  }
@@ -92,7 +94,8 @@ interface AnalyticsEnvelope {
92
94
  * - cada entrada usa `value: string[]` (array de un elemento)
93
95
  * - eventos: {key: nombre_evento, value: [JSON(timestamp + params)]}
94
96
  * - identidad (user_id) → `profile` como `external-user-id`
95
- * - `answers` vacío, `metrics` no se usa
97
+ * - métricas del host (setMetric) → `feedback.metrics` (mismo shape que metadata, sin prefijo)
98
+ * - `answers` vacío
96
99
  * - `text` vacío
97
100
  */
98
101
 
@@ -105,6 +108,17 @@ interface AnalyticsKeys {
105
108
  * Configuration options for initializing the DeepdotsPopups SDK
106
109
  */
107
110
 
111
+ /**
112
+ * Custom logger to receive the SDK's debug output instead of the console.
113
+ * Only `log` is required; `warn`/`error`/`info` fall back to `log` when omitted.
114
+ * Lets the host pipe debug output to a file, Firebase, etc. `console` satisfies this shape.
115
+ */
116
+ interface DeepdotsLogger {
117
+ log: (...args: unknown[]) => void;
118
+ warn?: (...args: unknown[]) => void;
119
+ error?: (...args: unknown[]) => void;
120
+ info?: (...args: unknown[]) => void;
121
+ }
108
122
  interface DeepdotsInitParams {
109
123
  /** API key for authentication */
110
124
  apiKey?: string;
@@ -112,6 +126,8 @@ interface DeepdotsInitParams {
112
126
  nodeEnv?: 'development' | 'production';
113
127
  /** Enable debug logging */
114
128
  debug?: boolean;
129
+ /** Custom logger for debug output. When set (and `debug` is true), the SDK routes its logs here instead of `console`. Default: `console`. */
130
+ logger?: DeepdotsLogger;
115
131
  /** Optional user id to send with popup events */
116
132
  userId?: string;
117
133
  /** Starts tracking enabled (default) or disabled (e.g. until consent is granted). Equivalent to calling `setTrackingEnabled` after init. */
@@ -122,6 +138,12 @@ interface DeepdotsInitParams {
122
138
  storage?: KeyValueStorage;
123
139
  /** Platform used in the analytics envelope. Default is 'web'; in RN pass 'android'/'ios'. */
124
140
  platform?: 'web' | 'android' | 'ios';
141
+ /**
142
+ * BCP-47 language tag for the analytics context (e.g. 'es-ES'). If omitted, the SDK
143
+ * auto-detects: `navigator.language` on web, or the `Intl` locale as a fallback (works in
144
+ * React Native with Hermes, where `navigator.language` is absent).
145
+ */
146
+ language?: string;
125
147
  /** Injectable device info (RN: from react-native-device-info). If missing, it is derived from the browser. */
126
148
  device?: DeviceInfo;
127
149
  /**
@@ -280,6 +302,8 @@ declare class DeepdotsPopups {
280
302
  private triggers;
281
303
  private initialized;
282
304
  private renderer;
305
+ /** Logger para el output de debug. Default `console`; el host puede inyectar el suyo en init(). */
306
+ private logger;
283
307
  private popupContainer;
284
308
  private popupDefinitions;
285
309
  private popupsLoaded;
@@ -321,6 +345,11 @@ declare class DeepdotsPopups {
321
345
  track(name: string, params?: Record<string, unknown>): void;
322
346
  /** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
323
347
  setUserAttributes(attributes: Record<string, string | number | boolean>): void;
348
+ /**
349
+ * Registra/actualiza una métrica (valor medible) → `feedback.metrics` del body.
350
+ * Persistente y sobrescribe por key. No-op si tracking off.
351
+ */
352
+ setMetric(key: string, value: string | number | boolean): void;
324
353
  /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
325
354
  reportError(error: unknown, options?: ReportErrorOptions): void;
326
355
  /** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
@@ -76,6 +76,8 @@ interface AnalyticsEnvelope {
76
76
  device?: DeviceInfo;
77
77
  /** User attributes del cliente, usados para breakdowns (registration_status, pass_type, …). */
78
78
  attributes: Record<string, string>;
79
+ /** Métricas del host: valores medibles persistentes → van en `feedback.metrics` del body. */
80
+ metrics?: Record<string, string>;
79
81
  };
80
82
  events: AnalyticsEvent[];
81
83
  }
@@ -92,7 +94,8 @@ interface AnalyticsEnvelope {
92
94
  * - cada entrada usa `value: string[]` (array de un elemento)
93
95
  * - eventos: {key: nombre_evento, value: [JSON(timestamp + params)]}
94
96
  * - identidad (user_id) → `profile` como `external-user-id`
95
- * - `answers` vacío, `metrics` no se usa
97
+ * - métricas del host (setMetric) → `feedback.metrics` (mismo shape que metadata, sin prefijo)
98
+ * - `answers` vacío
96
99
  * - `text` vacío
97
100
  */
98
101
 
@@ -105,6 +108,17 @@ interface AnalyticsKeys {
105
108
  * Configuration options for initializing the DeepdotsPopups SDK
106
109
  */
107
110
 
111
+ /**
112
+ * Custom logger to receive the SDK's debug output instead of the console.
113
+ * Only `log` is required; `warn`/`error`/`info` fall back to `log` when omitted.
114
+ * Lets the host pipe debug output to a file, Firebase, etc. `console` satisfies this shape.
115
+ */
116
+ interface DeepdotsLogger {
117
+ log: (...args: unknown[]) => void;
118
+ warn?: (...args: unknown[]) => void;
119
+ error?: (...args: unknown[]) => void;
120
+ info?: (...args: unknown[]) => void;
121
+ }
108
122
  interface DeepdotsInitParams {
109
123
  /** API key for authentication */
110
124
  apiKey?: string;
@@ -112,6 +126,8 @@ interface DeepdotsInitParams {
112
126
  nodeEnv?: 'development' | 'production';
113
127
  /** Enable debug logging */
114
128
  debug?: boolean;
129
+ /** Custom logger for debug output. When set (and `debug` is true), the SDK routes its logs here instead of `console`. Default: `console`. */
130
+ logger?: DeepdotsLogger;
115
131
  /** Optional user id to send with popup events */
116
132
  userId?: string;
117
133
  /** Starts tracking enabled (default) or disabled (e.g. until consent is granted). Equivalent to calling `setTrackingEnabled` after init. */
@@ -122,6 +138,12 @@ interface DeepdotsInitParams {
122
138
  storage?: KeyValueStorage;
123
139
  /** Platform used in the analytics envelope. Default is 'web'; in RN pass 'android'/'ios'. */
124
140
  platform?: 'web' | 'android' | 'ios';
141
+ /**
142
+ * BCP-47 language tag for the analytics context (e.g. 'es-ES'). If omitted, the SDK
143
+ * auto-detects: `navigator.language` on web, or the `Intl` locale as a fallback (works in
144
+ * React Native with Hermes, where `navigator.language` is absent).
145
+ */
146
+ language?: string;
125
147
  /** Injectable device info (RN: from react-native-device-info). If missing, it is derived from the browser. */
126
148
  device?: DeviceInfo;
127
149
  /**
@@ -280,6 +302,8 @@ declare class DeepdotsPopups {
280
302
  private triggers;
281
303
  private initialized;
282
304
  private renderer;
305
+ /** Logger para el output de debug. Default `console`; el host puede inyectar el suyo en init(). */
306
+ private logger;
283
307
  private popupContainer;
284
308
  private popupDefinitions;
285
309
  private popupsLoaded;
@@ -321,6 +345,11 @@ declare class DeepdotsPopups {
321
345
  track(name: string, params?: Record<string, unknown>): void;
322
346
  /** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
323
347
  setUserAttributes(attributes: Record<string, string | number | boolean>): void;
348
+ /**
349
+ * Registra/actualiza una métrica (valor medible) → `feedback.metrics` del body.
350
+ * Persistente y sobrescribe por key. No-op si tracking off.
351
+ */
352
+ setMetric(key: string, value: string | number | boolean): void;
324
353
  /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
325
354
  reportError(error: unknown, options?: ReportErrorOptions): void;
326
355
  /** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
@@ -143,8 +143,8 @@ var init_tracking_manager = __esm({
143
143
 
144
144
  // src/ui/font.ts
145
145
  function fontFormatFromUrl(url) {
146
- const clean = url.split("?")[0].split("#")[0];
147
- const ext = clean.slice(clean.lastIndexOf(".") + 1).toLowerCase();
146
+ const clean2 = url.split("?")[0].split("#")[0];
147
+ const ext = clean2.slice(clean2.lastIndexOf(".") + 1).toLowerCase();
148
148
  return FORMAT_BY_EXT[ext];
149
149
  }
150
150
  function sanitizeFamily(family) {
@@ -178,6 +178,27 @@ var init_font = __esm({
178
178
  }
179
179
  });
180
180
 
181
+ // src/util/logger.ts
182
+ function setLogger(logger) {
183
+ active = logger ?? console;
184
+ }
185
+ function sdkLog(...args) {
186
+ active.log(...args);
187
+ }
188
+ function sdkWarn(...args) {
189
+ (active.warn ?? active.log)(...args);
190
+ }
191
+ function sdkError(...args) {
192
+ (active.error ?? active.log)(...args);
193
+ }
194
+ var active;
195
+ var init_logger = __esm({
196
+ "src/util/logger.ts"() {
197
+ "use strict";
198
+ active = console;
199
+ }
200
+ });
201
+
181
202
  // src/assets/style.css
182
203
  var style_default;
183
204
  var init_style = __esm({
@@ -1431,7 +1452,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
1431
1452
  let formInstance = null;
1432
1453
  try {
1433
1454
  if (!import_native.default || typeof import_native.default.form !== "function") {
1434
- console.warn("[MagicFeedback] form() no disponible. Fallback manual.");
1455
+ sdkWarn("[MagicFeedback] form() no disponible. Fallback manual.");
1435
1456
  setLoading(false);
1436
1457
  return;
1437
1458
  }
@@ -1518,7 +1539,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
1518
1539
  }
1519
1540
  }
1520
1541
  if (s.startMessage && s.startMessage !== "") {
1521
- console.log(s.startMessage);
1542
+ sdkLog(s.startMessage);
1522
1543
  updateButtons("start");
1523
1544
  } else {
1524
1545
  updateButtons("in_progress_first");
@@ -1573,11 +1594,11 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
1573
1594
  }
1574
1595
  };
1575
1596
  formInstance.generate(formDivId, generateOptions).catch((err) => {
1576
- console.error("[MagicFeedback] Error generating form:", err);
1597
+ sdkError("[MagicFeedback] Error generating form:", err);
1577
1598
  setLoading(false);
1578
1599
  });
1579
1600
  } catch (e) {
1580
- console.error("[MagicFeedback] Exception initializing form:", e);
1601
+ sdkError("[MagicFeedback] Exception initializing form:", e);
1581
1602
  setLoading(false);
1582
1603
  }
1583
1604
  }
@@ -1587,6 +1608,7 @@ var init_renderPopup = __esm({
1587
1608
  "use strict";
1588
1609
  init_tracking_manager();
1589
1610
  init_font();
1611
+ init_logger();
1590
1612
  import_native = __toESM(require("@magicfeedback/native"));
1591
1613
  init_style();
1592
1614
  }
@@ -1848,6 +1870,7 @@ function buildSurveyHtml(opts) {
1848
1870
  }
1849
1871
 
1850
1872
  // src/platform/react-native-renderer.ts
1873
+ init_logger();
1851
1874
  var ReactNativePopupRenderer = class {
1852
1875
  constructor(options = {}) {
1853
1876
  this.options = options;
@@ -1868,7 +1891,7 @@ var ReactNativePopupRenderer = class {
1868
1891
  if (this.options.onShow) {
1869
1892
  this.options.onShow({ surveyId, productId, html });
1870
1893
  } else {
1871
- console.warn(
1894
+ sdkWarn(
1872
1895
  "[Deepdots] ReactNativePopupRenderer sin onShow: pasa { onShow } a new ReactNativePopupRenderer({...}) para montar el WebView del survey."
1873
1896
  );
1874
1897
  }
@@ -1985,16 +2008,19 @@ function createDefaultRenderer() {
1985
2008
  init_tracking_manager();
1986
2009
 
1987
2010
  // src/analytics/analytics-manager.ts
1988
- var dryRunSink = (payload) => {
1989
- console.log(
2011
+ function createDryRunSink(log = console.log) {
2012
+ return (payload) => log(
1990
2013
  "[DeepdotsAnalytics] (dry-run \xB7 NO enviado \xB7 sin init.analytics) POST /sdk/feedback \u2192",
1991
2014
  JSON.stringify(payload, null, 2)
1992
2015
  );
1993
- };
2016
+ }
2017
+ var dryRunSink = createDryRunSink();
1994
2018
  var AnalyticsManager = class {
1995
2019
  constructor(options = {}) {
1996
2020
  this.events = [];
1997
2021
  this.attributes = {};
2022
+ /** Métricas del host (valores medibles persistentes). Sobrescriben por key; se reenvían en cada flush. */
2023
+ this.metrics = {};
1998
2024
  /** Mini-services activos: nombre → timestamp de entrada. El orden de inserción marca el "más reciente". */
1999
2025
  this.activeMiniServices = /* @__PURE__ */ new Map();
2000
2026
  this.sink = options.sink ?? dryRunSink;
@@ -2017,6 +2043,14 @@ var AnalyticsManager = class {
2017
2043
  this.attributes[k] = String(v);
2018
2044
  }
2019
2045
  }
2046
+ /**
2047
+ * Registra/actualiza una métrica (valor medible). Persistente: se reenvía en cada flush.
2048
+ * Repetir la misma key SOBRESCRIBE; el valor se coerciona a string; ignora key vacía.
2049
+ */
2050
+ setMetric(key, value) {
2051
+ if (!key) return;
2052
+ this.metrics[key] = String(value);
2053
+ }
2020
2054
  /**
2021
2055
  * Marca el inicio de un mini-service; etiqueta los eventos siguientes con `mini_service`.
2022
2056
  * Admite varios activos a la vez (concurrencia); el "actual" para etiquetar es el más reciente.
@@ -2080,7 +2114,8 @@ var AnalyticsManager = class {
2080
2114
  platform: this.platform,
2081
2115
  language: this.language,
2082
2116
  device: this.device,
2083
- attributes: { ...this.attributes }
2117
+ attributes: { ...this.attributes },
2118
+ metrics: { ...this.metrics }
2084
2119
  },
2085
2120
  events: [...this.events]
2086
2121
  };
@@ -2136,10 +2171,15 @@ function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
2136
2171
  value: [JSON.stringify({ timestamp: e.timestamp, ...e.params ?? {} })]
2137
2172
  });
2138
2173
  }
2174
+ const metrics = [];
2175
+ for (const [k, v] of Object.entries(context.metrics ?? {})) {
2176
+ pushKV(metrics, k, v);
2177
+ }
2139
2178
  return {
2140
2179
  feedback: {
2141
2180
  text: "",
2142
2181
  answers: [],
2182
+ metrics,
2143
2183
  metadata,
2144
2184
  profile,
2145
2185
  finished: false
@@ -2320,20 +2360,95 @@ function collectDeviceInfo(appVersion) {
2320
2360
  }
2321
2361
 
2322
2362
  // src/analytics/geo-info.ts
2323
- async function collectGeoInfo(fetchImpl) {
2324
- const f = fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
2325
- if (!f) return null;
2363
+ var GEO_STORAGE_KEY = "deepdots.geo";
2364
+ var GEO_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
2365
+ var GEO_TIMEOUT_MS = 3e3;
2366
+ function readCachedGeo(storage, now) {
2326
2367
  try {
2327
- const res = await f("https://ipapi.co/json/");
2368
+ const raw = storage.getItem(GEO_STORAGE_KEY);
2369
+ if (!raw) return null;
2370
+ const cached = JSON.parse(raw);
2371
+ if (typeof cached.ts !== "number" || now - cached.ts > GEO_TTL_MS) return null;
2372
+ const geo = {};
2373
+ if (cached.country) geo.country = cached.country;
2374
+ if (cached.city) geo.city = cached.city;
2375
+ return geo.country || geo.city ? geo : null;
2376
+ } catch {
2377
+ return null;
2378
+ }
2379
+ }
2380
+ function writeCachedGeo(storage, geo, now) {
2381
+ try {
2382
+ const payload = { ...geo, ts: now };
2383
+ storage.setItem(GEO_STORAGE_KEY, JSON.stringify(payload));
2384
+ } catch {
2385
+ }
2386
+ }
2387
+ var DEFAULT_GEO_PROVIDERS = [
2388
+ {
2389
+ // https://ipapi.co/json/
2390
+ url: "https://ipapi.co/json/",
2391
+ parse: (d) => geoFrom(d.country_code, d.city)
2392
+ },
2393
+ {
2394
+ // https://ipwho.is/ (devuelve success:false con 200 en errores)
2395
+ url: "https://ipwho.is/",
2396
+ parse: (d) => d.success === false ? null : geoFrom(d.country_code, d.city)
2397
+ },
2398
+ {
2399
+ // https://ipinfo.io/json (country = código ISO)
2400
+ url: "https://ipinfo.io/json",
2401
+ parse: (d) => geoFrom(d.country, d.city)
2402
+ }
2403
+ ];
2404
+ function geoFrom(country, city) {
2405
+ const c = typeof country === "string" && country ? country : void 0;
2406
+ const t = typeof city === "string" && city ? city : void 0;
2407
+ if (!c && !t) return null;
2408
+ return { country: c, city: t };
2409
+ }
2410
+ async function fetchOne(provider, f, timeoutMs) {
2411
+ const controller = new AbortController();
2412
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
2413
+ try {
2414
+ const res = await f(provider.url, { signal: controller.signal });
2328
2415
  if (!res.ok) return null;
2329
2416
  const data = await res.json();
2330
- const country = data.country_code || void 0;
2331
- const city = data.city || void 0;
2332
- if (!country && !city) return null;
2333
- return { country, city };
2417
+ return provider.parse(data);
2334
2418
  } catch {
2335
2419
  return null;
2420
+ } finally {
2421
+ clearTimeout(timer);
2422
+ }
2423
+ }
2424
+ async function fetchGeo(options = {}) {
2425
+ const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
2426
+ if (!f) return null;
2427
+ const providers = options.providers ?? DEFAULT_GEO_PROVIDERS;
2428
+ const timeoutMs = options.timeoutMs ?? GEO_TIMEOUT_MS;
2429
+ for (const provider of providers) {
2430
+ const geo = await fetchOne(provider, f, timeoutMs);
2431
+ if (geo) return geo;
2432
+ }
2433
+ return null;
2434
+ }
2435
+
2436
+ // src/analytics/language.ts
2437
+ function clean(value) {
2438
+ const trimmed = value?.trim();
2439
+ return trimmed ? trimmed : void 0;
2440
+ }
2441
+ function resolveLanguage(sources) {
2442
+ const explicit = clean(sources.explicit);
2443
+ if (explicit) return explicit;
2444
+ const navLang = clean(sources.navigator?.language);
2445
+ if (navLang) return navLang;
2446
+ try {
2447
+ const locale = clean(sources.intl?.DateTimeFormat().resolvedOptions().locale);
2448
+ if (locale) return locale;
2449
+ } catch {
2336
2450
  }
2451
+ return void 0;
2337
2452
  }
2338
2453
 
2339
2454
  // src/analytics/engagement-tracker.ts
@@ -2506,7 +2621,7 @@ var CrashReporter = class {
2506
2621
  }
2507
2622
  /** Instala los handlers globales de errores no capturados (no-op sin `window`). */
2508
2623
  install() {
2509
- if (typeof window === "undefined") return;
2624
+ if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
2510
2625
  window.addEventListener("error", (e) => {
2511
2626
  if (!this.isEnabled()) return;
2512
2627
  this.captureUnhandled(e.error ?? e.message ?? "error");
@@ -2530,6 +2645,9 @@ var CrashReporter = class {
2530
2645
  }
2531
2646
  };
2532
2647
 
2648
+ // src/core/deepdots-popups.ts
2649
+ init_logger();
2650
+
2533
2651
  // src/analytics/messaging.ts
2534
2652
  function buildMessageParams(stage, o) {
2535
2653
  const p = {
@@ -2558,6 +2676,8 @@ var DeepdotsPopups = class {
2558
2676
  this.triggers = [];
2559
2677
  this.initialized = false;
2560
2678
  this.renderer = createDefaultRenderer();
2679
+ /** Logger para el output de debug. Default `console`; el host puede inyectar el suyo en init(). */
2680
+ this.logger = console;
2561
2681
  this.popupContainer = null;
2562
2682
  // deprecated: mantenido para compatibilidad interna
2563
2683
  this.popupDefinitions = [];
@@ -2593,6 +2713,8 @@ var DeepdotsPopups = class {
2593
2713
  }
2594
2714
  /** Initialize the SDK with configuration */
2595
2715
  init(config) {
2716
+ this.logger = config.logger ?? console;
2717
+ setLogger(this.logger);
2596
2718
  if (this.initialized) {
2597
2719
  this.log("SDK already initialized");
2598
2720
  return;
@@ -2632,12 +2754,16 @@ var DeepdotsPopups = class {
2632
2754
  onSessionId: (id) => {
2633
2755
  this.analyticsFeedbackSessionId = id;
2634
2756
  }
2635
- }) : void 0;
2757
+ }) : createDryRunSink((...a) => this.logger.log(...a));
2636
2758
  const device = config.device ?? collectDeviceInfo(config.appVersion);
2637
2759
  this.analytics = new AnalyticsManager({
2638
2760
  sink: analyticsSink,
2639
2761
  publicKey: config.analytics?.publicKey ?? this.config.apiKey,
2640
- language: typeof navigator !== "undefined" ? navigator.language : void 0,
2762
+ language: resolveLanguage({
2763
+ explicit: config.language,
2764
+ navigator: typeof navigator !== "undefined" ? navigator : void 0,
2765
+ intl: typeof Intl !== "undefined" ? Intl : void 0
2766
+ }),
2641
2767
  platform: config.platform ?? "web",
2642
2768
  device,
2643
2769
  maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,
@@ -2654,8 +2780,13 @@ var DeepdotsPopups = class {
2654
2780
  if (this.tracking?.isTrackingEnabled()) {
2655
2781
  this.crashReporter.install();
2656
2782
  }
2657
- collectGeoInfo().then((geo) => {
2658
- if (geo) this.analytics?.updateDevice(geo);
2783
+ const cachedGeo = readCachedGeo(storage, Date.now());
2784
+ if (cachedGeo) this.analytics.updateDevice(cachedGeo);
2785
+ fetchGeo().then((geo) => {
2786
+ if (geo) {
2787
+ this.analytics?.updateDevice(geo);
2788
+ writeCachedGeo(storage, geo, Date.now());
2789
+ }
2659
2790
  }).catch(() => {
2660
2791
  });
2661
2792
  this.navObserver = new NavigationObserver();
@@ -2712,6 +2843,15 @@ var DeepdotsPopups = class {
2712
2843
  if (!this.tracking?.isTrackingEnabled()) return;
2713
2844
  this.analytics?.setUserAttributes(attributes);
2714
2845
  }
2846
+ /**
2847
+ * Registra/actualiza una métrica (valor medible) → `feedback.metrics` del body.
2848
+ * Persistente y sobrescribe por key. No-op si tracking off.
2849
+ */
2850
+ setMetric(key, value) {
2851
+ if (!this.tracking?.isTrackingEnabled()) return;
2852
+ this.analytics?.setMetric(key, value);
2853
+ this.log("analytics \xB7 setMetric:", key, value);
2854
+ }
2715
2855
  /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
2716
2856
  reportError(error, options) {
2717
2857
  if (!this.tracking?.isTrackingEnabled()) return;
@@ -3341,7 +3481,8 @@ var DeepdotsPopups = class {
3341
3481
  try {
3342
3482
  listener(event);
3343
3483
  } catch (error) {
3344
- console.error("Error in event listener:", error);
3484
+ if (this.logger.error) this.logger.error("Error in event listener:", error);
3485
+ else this.logger.log("Error in event listener:", error);
3345
3486
  }
3346
3487
  });
3347
3488
  }
@@ -3349,7 +3490,7 @@ var DeepdotsPopups = class {
3349
3490
  /** Log debug messages */
3350
3491
  log(...args) {
3351
3492
  if (this.config?.debug) {
3352
- console.log("[DeepdotsPopups]", ...args);
3493
+ this.logger.log("[DeepdotsPopups]", ...args);
3353
3494
  }
3354
3495
  }
3355
3496
  /** External debug method for triggers */