@magicfeedback/popup-sdk 1.1.6 → 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
 
@@ -135,6 +138,12 @@ interface DeepdotsInitParams {
135
138
  storage?: KeyValueStorage;
136
139
  /** Platform used in the analytics envelope. Default is 'web'; in RN pass 'android'/'ios'. */
137
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;
138
147
  /** Injectable device info (RN: from react-native-device-info). If missing, it is derived from the browser. */
139
148
  device?: DeviceInfo;
140
149
  /**
@@ -336,6 +345,11 @@ declare class DeepdotsPopups {
336
345
  track(name: string, params?: Record<string, unknown>): void;
337
346
  /** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
338
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;
339
353
  /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
340
354
  reportError(error: unknown, options?: ReportErrorOptions): void;
341
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
 
@@ -135,6 +138,12 @@ interface DeepdotsInitParams {
135
138
  storage?: KeyValueStorage;
136
139
  /** Platform used in the analytics envelope. Default is 'web'; in RN pass 'android'/'ios'. */
137
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;
138
147
  /** Injectable device info (RN: from react-native-device-info). If missing, it is derived from the browser. */
139
148
  device?: DeviceInfo;
140
149
  /**
@@ -336,6 +345,11 @@ declare class DeepdotsPopups {
336
345
  track(name: string, params?: Record<string, unknown>): void;
337
346
  /** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
338
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;
339
353
  /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
340
354
  reportError(error: unknown, options?: ReportErrorOptions): void;
341
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) {
@@ -2019,6 +2019,8 @@ var AnalyticsManager = class {
2019
2019
  constructor(options = {}) {
2020
2020
  this.events = [];
2021
2021
  this.attributes = {};
2022
+ /** Métricas del host (valores medibles persistentes). Sobrescriben por key; se reenvían en cada flush. */
2023
+ this.metrics = {};
2022
2024
  /** Mini-services activos: nombre → timestamp de entrada. El orden de inserción marca el "más reciente". */
2023
2025
  this.activeMiniServices = /* @__PURE__ */ new Map();
2024
2026
  this.sink = options.sink ?? dryRunSink;
@@ -2041,6 +2043,14 @@ var AnalyticsManager = class {
2041
2043
  this.attributes[k] = String(v);
2042
2044
  }
2043
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
+ }
2044
2054
  /**
2045
2055
  * Marca el inicio de un mini-service; etiqueta los eventos siguientes con `mini_service`.
2046
2056
  * Admite varios activos a la vez (concurrencia); el "actual" para etiquetar es el más reciente.
@@ -2104,7 +2114,8 @@ var AnalyticsManager = class {
2104
2114
  platform: this.platform,
2105
2115
  language: this.language,
2106
2116
  device: this.device,
2107
- attributes: { ...this.attributes }
2117
+ attributes: { ...this.attributes },
2118
+ metrics: { ...this.metrics }
2108
2119
  },
2109
2120
  events: [...this.events]
2110
2121
  };
@@ -2160,10 +2171,15 @@ function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
2160
2171
  value: [JSON.stringify({ timestamp: e.timestamp, ...e.params ?? {} })]
2161
2172
  });
2162
2173
  }
2174
+ const metrics = [];
2175
+ for (const [k, v] of Object.entries(context.metrics ?? {})) {
2176
+ pushKV(metrics, k, v);
2177
+ }
2163
2178
  return {
2164
2179
  feedback: {
2165
2180
  text: "",
2166
2181
  answers: [],
2182
+ metrics,
2167
2183
  metadata,
2168
2184
  profile,
2169
2185
  finished: false
@@ -2344,20 +2360,95 @@ function collectDeviceInfo(appVersion) {
2344
2360
  }
2345
2361
 
2346
2362
  // src/analytics/geo-info.ts
2347
- async function collectGeoInfo(fetchImpl) {
2348
- const f = fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
2349
- 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) {
2367
+ try {
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);
2350
2413
  try {
2351
- const res = await f("https://ipapi.co/json/");
2414
+ const res = await f(provider.url, { signal: controller.signal });
2352
2415
  if (!res.ok) return null;
2353
2416
  const data = await res.json();
2354
- const country = data.country_code || void 0;
2355
- const city = data.city || void 0;
2356
- if (!country && !city) return null;
2357
- return { country, city };
2417
+ return provider.parse(data);
2358
2418
  } catch {
2359
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 {
2360
2450
  }
2451
+ return void 0;
2361
2452
  }
2362
2453
 
2363
2454
  // src/analytics/engagement-tracker.ts
@@ -2668,7 +2759,11 @@ var DeepdotsPopups = class {
2668
2759
  this.analytics = new AnalyticsManager({
2669
2760
  sink: analyticsSink,
2670
2761
  publicKey: config.analytics?.publicKey ?? this.config.apiKey,
2671
- 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
+ }),
2672
2767
  platform: config.platform ?? "web",
2673
2768
  device,
2674
2769
  maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,
@@ -2685,8 +2780,13 @@ var DeepdotsPopups = class {
2685
2780
  if (this.tracking?.isTrackingEnabled()) {
2686
2781
  this.crashReporter.install();
2687
2782
  }
2688
- collectGeoInfo().then((geo) => {
2689
- 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
+ }
2690
2790
  }).catch(() => {
2691
2791
  });
2692
2792
  this.navObserver = new NavigationObserver();
@@ -2743,6 +2843,15 @@ var DeepdotsPopups = class {
2743
2843
  if (!this.tracking?.isTrackingEnabled()) return;
2744
2844
  this.analytics?.setUserAttributes(attributes);
2745
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
+ }
2746
2855
  /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
2747
2856
  reportError(error, options) {
2748
2857
  if (!this.tracking?.isTrackingEnabled()) return;