@magicfeedback/popup-sdk 1.16.0 → 1.17.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.
@@ -87,9 +87,9 @@ interface AnalyticsEnvelope {
87
87
  *
88
88
  * La analítica se envía como un Feedback del modelo del Surveys SDK, agrupado por una
89
89
  * INTEGRACIÓN creada en la plataforma. Se manda en streaming con `completed:false`; el
90
- * backend cose por `sessionId` + `user_id`. El ÚLTIMO lote de una sesión (cierre de página,
91
- * app a background, cambio de usuario) va con `completed:true`: cierra el registro y el
92
- * siguiente lote omite el `sessionId` viejo para que el backend abra uno nuevo.
90
+ * backend agrupa los lotes por `sessionId`, que genera el SDK al abrir cada sesión. El
91
+ * ÚLTIMO lote de una sesión (cierre de página, app a background, cambio de usuario) va con
92
+ * `completed:true`: cierra el registro y el lote siguiente abre otro con un sessionId nuevo.
93
93
  * `feedback.finished` se queda siempre en `false` (no es la señal de cierre acordada).
94
94
  *
95
95
  * Encoding:
@@ -445,6 +445,8 @@ declare class DeepdotsPopups {
445
445
  private analyticsFlushTimer;
446
446
  /** Crash & error reporting (#14–17). Null hasta init(). */
447
447
  private crashReporter;
448
+ /** ErrorUtils de RN recibido del host (o del Provider); se engancha en cuanto haya consentimiento. */
449
+ private rnErrorUtils;
448
450
  /** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */
449
451
  private messageGuard;
450
452
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
@@ -513,8 +515,18 @@ declare class DeepdotsPopups {
513
515
  setMetric(key: string, value: string | number | boolean): void;
514
516
  /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
515
517
  reportError(error: unknown, options?: ReportErrorOptions): void;
516
- /** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
518
+ /**
519
+ * Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. Con tracking off
520
+ * se recuerda y se engancha cuando se conceda el consentimiento (`setTrackingEnabled(true)`).
521
+ */
517
522
  installReactNativeCrashHandler(errorUtils: ReactNativeErrorUtils): void;
523
+ /**
524
+ * Pone los handlers de errores no capturados: `window` en web y `ErrorUtils` en RN (el que
525
+ * pasó el host o, si no pasó ninguno, `globalThis.ErrorUtils`, para que un `init()` sin
526
+ * `setupReactNative` también capture). Solo con consentimiento; idempotente, así que se
527
+ * llama en `init()` y cada vez que se activa el tracking.
528
+ */
529
+ private installCrashHandlers;
518
530
  /**
519
531
  * Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
520
532
  * Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
@@ -737,7 +749,11 @@ declare class DeepdotsPopups {
737
749
  private setDeferredExitQueue;
738
750
  /** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */
739
751
  private fetchPopupsFromServer;
740
- /** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */
752
+ /**
753
+ * Envía los atributos de contact a la API (`POST /sdk/popups/contact`). Rechaza si no se
754
+ * pudo enviar (sin red o respuesta no-2xx), para que `ContactManager` no guarde la caché
755
+ * y la llamada siguiente reintente.
756
+ */
741
757
  private postContact;
742
758
  /** Notifica eventos de popup a la API */
743
759
  private postPopupEvent;
@@ -87,9 +87,9 @@ interface AnalyticsEnvelope {
87
87
  *
88
88
  * La analítica se envía como un Feedback del modelo del Surveys SDK, agrupado por una
89
89
  * INTEGRACIÓN creada en la plataforma. Se manda en streaming con `completed:false`; el
90
- * backend cose por `sessionId` + `user_id`. El ÚLTIMO lote de una sesión (cierre de página,
91
- * app a background, cambio de usuario) va con `completed:true`: cierra el registro y el
92
- * siguiente lote omite el `sessionId` viejo para que el backend abra uno nuevo.
90
+ * backend agrupa los lotes por `sessionId`, que genera el SDK al abrir cada sesión. El
91
+ * ÚLTIMO lote de una sesión (cierre de página, app a background, cambio de usuario) va con
92
+ * `completed:true`: cierra el registro y el lote siguiente abre otro con un sessionId nuevo.
93
93
  * `feedback.finished` se queda siempre en `false` (no es la señal de cierre acordada).
94
94
  *
95
95
  * Encoding:
@@ -445,6 +445,8 @@ declare class DeepdotsPopups {
445
445
  private analyticsFlushTimer;
446
446
  /** Crash & error reporting (#14–17). Null hasta init(). */
447
447
  private crashReporter;
448
+ /** ErrorUtils de RN recibido del host (o del Provider); se engancha en cuanto haya consentimiento. */
449
+ private rnErrorUtils;
448
450
  /** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */
449
451
  private messageGuard;
450
452
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
@@ -513,8 +515,18 @@ declare class DeepdotsPopups {
513
515
  setMetric(key: string, value: string | number | boolean): void;
514
516
  /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
515
517
  reportError(error: unknown, options?: ReportErrorOptions): void;
516
- /** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
518
+ /**
519
+ * Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. Con tracking off
520
+ * se recuerda y se engancha cuando se conceda el consentimiento (`setTrackingEnabled(true)`).
521
+ */
517
522
  installReactNativeCrashHandler(errorUtils: ReactNativeErrorUtils): void;
523
+ /**
524
+ * Pone los handlers de errores no capturados: `window` en web y `ErrorUtils` en RN (el que
525
+ * pasó el host o, si no pasó ninguno, `globalThis.ErrorUtils`, para que un `init()` sin
526
+ * `setupReactNative` también capture). Solo con consentimiento; idempotente, así que se
527
+ * llama en `init()` y cada vez que se activa el tracking.
528
+ */
529
+ private installCrashHandlers;
518
530
  /**
519
531
  * Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
520
532
  * Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
@@ -737,7 +749,11 @@ declare class DeepdotsPopups {
737
749
  private setDeferredExitQueue;
738
750
  /** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */
739
751
  private fetchPopupsFromServer;
740
- /** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */
752
+ /**
753
+ * Envía los atributos de contact a la API (`POST /sdk/popups/contact`). Rechaza si no se
754
+ * pudo enviar (sin red o respuesta no-2xx), para que `ContactManager` no guarde la caché
755
+ * y la llamada siguiente reintente.
756
+ */
741
757
  private postContact;
742
758
  /** Notifica eventos de popup a la API */
743
759
  private postPopupEvent;
@@ -4851,97 +4851,166 @@ async function safeBodyText(res) {
4851
4851
  return "";
4852
4852
  }
4853
4853
  }
4854
+ var PENDING_MAX = 50;
4855
+ var PENDING_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
4856
+ var POST_TIMEOUT_MS = 2e4;
4857
+ function pendingKey(integration) {
4858
+ return `__deepdots_analytics_pending__:${integration}`;
4859
+ }
4860
+ function generateSessionId() {
4861
+ const c = globalThis.crypto;
4862
+ if (typeof c?.randomUUID === "function") return c.randomUUID();
4863
+ const b = new Uint8Array(16);
4864
+ if (typeof c?.getRandomValues === "function") c.getRandomValues(b);
4865
+ else for (let i = 0; i < 16; i++) b[i] = Math.floor(Math.random() * 256);
4866
+ b[6] = b[6] & 15 | 64;
4867
+ b[8] = b[8] & 63 | 128;
4868
+ const h = Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
4869
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
4870
+ }
4854
4871
  function createFeedbackSink(options) {
4855
- let feedbackSessionId = options.initialSessionId || void 0;
4856
- let firstPostInFlight = null;
4857
- let generation = 0;
4858
4872
  const url = `${options.baseUrl}/sdk/feedback`;
4859
- const post = async (body, final, sessionEnd = false) => {
4860
- const postGeneration = generation;
4873
+ const newSessionId = options.newSessionId ?? generateSessionId;
4874
+ const now = options.now ?? (() => Date.now());
4875
+ const storageKey = pendingKey(options.keys.integration);
4876
+ let feedbackSessionId = options.initialSessionId || void 0;
4877
+ let announcedId = feedbackSessionId;
4878
+ const pending2 = loadPending();
4879
+ let draining = null;
4880
+ function loadPending() {
4881
+ if (!options.storage) return [];
4882
+ try {
4883
+ const raw = options.storage.getItem(storageKey);
4884
+ const list = raw ? JSON.parse(raw) : [];
4885
+ return Array.isArray(list) ? list.filter((p) => p?.body && now() - p.at <= PENDING_MAX_AGE_MS) : [];
4886
+ } catch {
4887
+ return [];
4888
+ }
4889
+ }
4890
+ function savePending() {
4891
+ if (!options.storage) return;
4892
+ try {
4893
+ if (pending2.length) options.storage.setItem(storageKey, JSON.stringify(pending2));
4894
+ else options.storage.removeItem(storageKey);
4895
+ } catch {
4896
+ }
4897
+ }
4898
+ function addPending(batch) {
4899
+ pending2.push(batch);
4900
+ if (pending2.length > PENDING_MAX) pending2.splice(0, pending2.length - PENDING_MAX);
4901
+ savePending();
4902
+ }
4903
+ function removePending(batch) {
4904
+ const i = pending2.indexOf(batch);
4905
+ if (i >= 0) pending2.splice(i, 1);
4906
+ savePending();
4907
+ }
4908
+ async function post(body, final) {
4861
4909
  const json = JSON.stringify(body);
4862
4910
  const small = json.length <= KEEPALIVE_MAX_BYTES;
4863
4911
  if (final) options.log?.("[DeepdotsAnalytics] final flush via fetch keepalive");
4864
4912
  const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
4865
4913
  if (!f) {
4866
4914
  options.log?.("[DeepdotsAnalytics] no fetch available; payload not sent", body);
4867
- return;
4915
+ return "drop";
4868
4916
  }
4869
- const res = await f(url, {
4870
- method: "POST",
4871
- headers: { "Content-Type": "application/json" },
4872
- body: json,
4873
- // La API autentica por `publicKey` en el body y responde `Allow-Origin: *`: una
4874
- // petición con credenciales fallaría el CORS. Explícito para no depender del default.
4875
- credentials: "omit",
4876
- // keepalive: el POST sobrevive a la navegación mientras el body sea pequeño.
4877
- ...small ? { keepalive: true } : {}
4878
- });
4879
- if (!res.ok) {
4917
+ let timer;
4918
+ try {
4919
+ const request = f(url, {
4920
+ method: "POST",
4921
+ headers: { "Content-Type": "application/json" },
4922
+ body: json,
4923
+ // La API autentica por `publicKey` en el body y responde `Allow-Origin: *`: una
4924
+ // petición con credenciales fallaría el CORS. Explícito para no depender del default.
4925
+ credentials: "omit",
4926
+ // keepalive: el POST sobrevive a la navegación mientras el body sea pequeño.
4927
+ ...small ? { keepalive: true } : {}
4928
+ });
4929
+ const timeout = new Promise((_, reject) => {
4930
+ timer = setTimeout(() => reject(new Error("timeout")), POST_TIMEOUT_MS);
4931
+ });
4932
+ const res = await Promise.race([request, timeout]);
4933
+ if (res.ok) return "ok";
4880
4934
  const detail = await safeBodyText(res);
4881
4935
  if (isRetryableStatus(res.status)) {
4882
- throw new Error(`POST /sdk/feedback ${res.status} (retryable): ${detail}`);
4936
+ options.log?.(`[DeepdotsAnalytics] POST /sdk/feedback ${res.status} (retryable): ${detail}`);
4937
+ return "retry";
4883
4938
  }
4884
4939
  options.log?.(
4885
4940
  `[DeepdotsAnalytics] POST /sdk/feedback rejected with ${res.status}; batch DISCARDED:`,
4886
4941
  detail
4887
4942
  );
4888
- return;
4889
- }
4890
- if (sessionEnd) {
4891
- let closedId = body.sessionId;
4892
- try {
4893
- const data = await res.json();
4894
- closedId = data?.sessionId ?? closedId;
4895
- } catch {
4896
- }
4897
- try {
4898
- if (closedId) options.onSessionClosed?.(closedId);
4899
- } catch {
4900
- }
4901
- return;
4943
+ return "drop";
4944
+ } catch (error) {
4945
+ options.log?.("[DeepdotsAnalytics] POST /sdk/feedback failed (will retry):", error);
4946
+ return "retry";
4947
+ } finally {
4948
+ clearTimeout(timer);
4902
4949
  }
4950
+ }
4951
+ function delivered(body) {
4952
+ const id = body.sessionId;
4953
+ if (!id) return;
4903
4954
  try {
4904
- const data = await res.json();
4905
- if (postGeneration !== generation) {
4906
- options.log?.("[DeepdotsAnalytics] sessionId of a closed session ignored:", data?.sessionId);
4907
- return;
4908
- }
4909
- if (data?.sessionId && data.sessionId !== feedbackSessionId) {
4910
- feedbackSessionId = data.sessionId;
4911
- options.log?.("[DeepdotsAnalytics] feedbackSessionId cached:", feedbackSessionId);
4912
- options.onSessionId?.(feedbackSessionId);
4955
+ if (body.completed) {
4956
+ options.onSessionClosed?.(id);
4957
+ } else if (id === feedbackSessionId && id !== announcedId) {
4958
+ announcedId = id;
4959
+ options.log?.("[DeepdotsAnalytics] feedback session open:", id);
4960
+ options.onSessionId?.(id);
4913
4961
  }
4914
4962
  } catch {
4915
4963
  }
4916
- };
4964
+ }
4965
+ async function send(batch, final, alreadyPending) {
4966
+ const result = await post(batch.body, final);
4967
+ if (result === "retry") {
4968
+ if (!alreadyPending) addPending(batch);
4969
+ return;
4970
+ }
4971
+ if (alreadyPending) removePending(batch);
4972
+ if (result === "ok") delivered(batch.body);
4973
+ }
4974
+ function drain() {
4975
+ const run = async () => {
4976
+ for (let i = 0; i < pending2.length; ) {
4977
+ const batch = pending2[i];
4978
+ await send(batch, false, true);
4979
+ if (pending2[i] === batch) return;
4980
+ }
4981
+ };
4982
+ const next = draining ? draining.then(run) : run();
4983
+ draining = next;
4984
+ void next.finally(() => {
4985
+ if (draining === next) draining = null;
4986
+ });
4987
+ return next;
4988
+ }
4989
+ if (pending2.length) {
4990
+ savePending();
4991
+ setTimeout(() => void drain(), 0);
4992
+ }
4917
4993
  return async (envelope, meta) => {
4918
4994
  if (meta?.resetSession) {
4919
4995
  feedbackSessionId = void 0;
4920
- firstPostInFlight = null;
4921
- generation++;
4922
4996
  options.onSessionReset?.();
4923
4997
  return;
4924
4998
  }
4925
4999
  const final = meta?.final === true;
4926
5000
  const sessionEnd = meta?.sessionEnd === true;
4927
- if (!feedbackSessionId && firstPostInFlight && !final) {
4928
- await firstPostInFlight;
4929
- }
5001
+ if (!feedbackSessionId) feedbackSessionId = newSessionId();
4930
5002
  const body = buildAnalyticsFeedbackBody(envelope, options.keys, feedbackSessionId, { sessionEnd });
4931
5003
  options.log?.("[DeepdotsAnalytics] POST /sdk/feedback \u2192", body);
4932
- const sent = post(body, final, sessionEnd);
5004
+ const batch = { body, at: now() };
4933
5005
  if (sessionEnd) {
4934
5006
  feedbackSessionId = void 0;
4935
- firstPostInFlight = null;
4936
- generation++;
4937
5007
  options.onSessionReset?.();
4938
- } else if (!feedbackSessionId) {
4939
- firstPostInFlight = sent.then(
4940
- () => void 0,
4941
- () => void 0
4942
- );
5008
+ addPending(batch);
5009
+ await drain();
5010
+ return;
4943
5011
  }
4944
- await sent;
5012
+ const retries = pending2.length ? drain() : null;
5013
+ await Promise.all([retries, send(batch, final, false)]);
4945
5014
  };
4946
5015
  }
4947
5016
 
@@ -5299,12 +5368,12 @@ var EngagementTracker = class {
5299
5368
 
5300
5369
  // src/contact/contact-manager.ts
5301
5370
  init_tracking_manager();
5302
- function stableSerialize(attrs) {
5371
+ function stableSerialize(userId, attrs) {
5303
5372
  const sorted = {};
5304
5373
  for (const key of Object.keys(attrs).sort()) {
5305
5374
  sorted[key] = attrs[key];
5306
5375
  }
5307
- return JSON.stringify(sorted);
5376
+ return JSON.stringify({ userId, attributes: sorted });
5308
5377
  }
5309
5378
  var ContactManager = class {
5310
5379
  constructor(options) {
@@ -5314,18 +5383,23 @@ var ContactManager = class {
5314
5383
  this.post = options.post;
5315
5384
  }
5316
5385
  /**
5317
- * Envía los atributos si cambiaron respecto a lo último enviado.
5318
- * @returns `true` si se envió, `false` si no hubo cambios.
5386
+ * Envía los atributos si cambiaron respecto a lo último enviado. La caché solo se guarda
5387
+ * cuando la API acepta el envío; si falla, la llamada siguiente reintenta. Nunca rechaza.
5388
+ * @returns `true` si se envió, `false` si no hubo cambios o el envío falló.
5319
5389
  */
5320
5390
  async setAttributes(attributes) {
5321
- const serialized = stableSerialize(attributes);
5391
+ const serialized = stableSerialize(this.userId, attributes);
5322
5392
  const last = this.storage.getItem(STORAGE_KEYS.contactAttributes);
5323
5393
  if (last === serialized) return false;
5324
- await this.post({
5325
- publicKey: this.publicKey,
5326
- userId: this.userId,
5327
- userAttributes: attributes
5328
- });
5394
+ try {
5395
+ await this.post({
5396
+ publicKey: this.publicKey,
5397
+ userId: this.userId,
5398
+ userAttributes: attributes
5399
+ });
5400
+ } catch {
5401
+ return false;
5402
+ }
5329
5403
  this.storage.setItem(STORAGE_KEYS.contactAttributes, serialized);
5330
5404
  return true;
5331
5405
  }
@@ -5356,6 +5430,10 @@ function crashRecordToParams(r) {
5356
5430
  }
5357
5431
  var CrashReporter = class {
5358
5432
  constructor(options) {
5433
+ /** Los handlers de `window` ya están puestos: `install()` es idempotente. */
5434
+ this.windowInstalled = false;
5435
+ /** ErrorUtils ya enganchados: engancharlos dos veces capturaría cada error dos veces. */
5436
+ this.hookedErrorUtils = /* @__PURE__ */ new Set();
5359
5437
  this.options = options;
5360
5438
  this.now = options.now ?? (() => Date.now());
5361
5439
  }
@@ -5431,9 +5509,11 @@ var CrashReporter = class {
5431
5509
  }
5432
5510
  return queue;
5433
5511
  }
5434
- /** Instala los handlers globales de errores no capturados (no-op sin `window`). */
5512
+ /** Instala los handlers globales de errores no capturados (no-op sin `window`). Idempotente. */
5435
5513
  install() {
5514
+ if (this.windowInstalled) return;
5436
5515
  if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
5516
+ this.windowInstalled = true;
5437
5517
  window.addEventListener("error", (e) => {
5438
5518
  if (!this.isEnabled()) return;
5439
5519
  this.captureUnhandled(e.error ?? e.message ?? "error");
@@ -5443,8 +5523,10 @@ var CrashReporter = class {
5443
5523
  this.captureUnhandled(e.reason ?? "unhandledrejection");
5444
5524
  });
5445
5525
  }
5446
- /** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. */
5526
+ /** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. Idempotente por instancia de ErrorUtils. */
5447
5527
  installReactNative(errorUtils) {
5528
+ if (this.hookedErrorUtils.has(errorUtils)) return;
5529
+ this.hookedErrorUtils.add(errorUtils);
5448
5530
  const previous = errorUtils.getGlobalHandler?.();
5449
5531
  errorUtils.setGlobalHandler((error, isFatal) => {
5450
5532
  if (this.isEnabled()) this.captureUnhandled(error);
@@ -5660,6 +5742,8 @@ var DeepdotsPopups = class {
5660
5742
  this.analyticsFlushTimer = void 0;
5661
5743
  /** Crash & error reporting (#14–17). Null hasta init(). */
5662
5744
  this.crashReporter = null;
5745
+ /** ErrorUtils de RN recibido del host (o del Provider); se engancha en cuanto haya consentimiento. */
5746
+ this.rnErrorUtils = null;
5663
5747
  /** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */
5664
5748
  this.messageGuard = new MessageGuard();
5665
5749
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
@@ -5760,6 +5844,10 @@ var DeepdotsPopups = class {
5760
5844
  keys: config.analytics,
5761
5845
  log: (...a) => this.log(...a),
5762
5846
  initialSessionId: resumable?.sessionId,
5847
+ // En móvil (RN) la app puede morir con el lote de cierre sin entregar: se
5848
+ // persiste y se reenvía en el arranque siguiente. En web el cierre sale con
5849
+ // `keepalive` y varias pestañas compartirían el mismo storage.
5850
+ storage: (config.platform ?? "web") !== "web" ? storage : void 0,
5763
5851
  onSessionId: (id) => {
5764
5852
  this.analyticsFeedbackSessionId = id;
5765
5853
  this.notifyFeedbackSession(id, "open");
@@ -5795,9 +5883,7 @@ var DeepdotsPopups = class {
5795
5883
  now: () => Date.now(),
5796
5884
  enabled: () => this.tracking?.isTrackingEnabled() ?? false
5797
5885
  });
5798
- if (this.tracking?.isTrackingEnabled()) {
5799
- this.crashReporter.install();
5800
- }
5886
+ this.installCrashHandlers();
5801
5887
  this.geoEnabled = config.geolocation ?? true;
5802
5888
  this.geoWanted = this.geoEnabled && !!config.analytics;
5803
5889
  this.resolveGeo();
@@ -5866,6 +5952,7 @@ var DeepdotsPopups = class {
5866
5952
  if (!enabled) this.closeSession("tracking_disabled");
5867
5953
  this.tracking?.setTrackingEnabled(enabled);
5868
5954
  if (enabled) this.openSession("tracking_enabled");
5955
+ if (enabled) this.installCrashHandlers();
5869
5956
  if (enabled) this.resolveGeo();
5870
5957
  this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
5871
5958
  }
@@ -5896,10 +5983,27 @@ var DeepdotsPopups = class {
5896
5983
  if (!this.tracking?.isTrackingEnabled()) return;
5897
5984
  this.crashReporter?.reportError(error, options);
5898
5985
  }
5899
- /** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
5986
+ /**
5987
+ * Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. Con tracking off
5988
+ * se recuerda y se engancha cuando se conceda el consentimiento (`setTrackingEnabled(true)`).
5989
+ */
5900
5990
  installReactNativeCrashHandler(errorUtils) {
5901
- if (!this.tracking?.isTrackingEnabled()) return;
5902
- this.crashReporter?.installReactNative(errorUtils);
5991
+ this.rnErrorUtils = errorUtils;
5992
+ this.installCrashHandlers();
5993
+ }
5994
+ /**
5995
+ * Pone los handlers de errores no capturados: `window` en web y `ErrorUtils` en RN (el que
5996
+ * pasó el host o, si no pasó ninguno, `globalThis.ErrorUtils`, para que un `init()` sin
5997
+ * `setupReactNative` también capture). Solo con consentimiento; idempotente, así que se
5998
+ * llama en `init()` y cada vez que se activa el tracking.
5999
+ */
6000
+ installCrashHandlers() {
6001
+ if (!this.crashReporter || !this.tracking?.isTrackingEnabled()) return;
6002
+ this.crashReporter.install();
6003
+ const errorUtils = this.rnErrorUtils ?? globalThis.ErrorUtils;
6004
+ if (errorUtils && typeof errorUtils.setGlobalHandler === "function") {
6005
+ this.crashReporter.installReactNative(errorUtils);
6006
+ }
5903
6007
  }
5904
6008
  /**
5905
6009
  * Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
@@ -6722,25 +6826,31 @@ var DeepdotsPopups = class {
6722
6826
  return null;
6723
6827
  }
6724
6828
  }
6725
- /** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */
6829
+ /**
6830
+ * Envía los atributos de contact a la API (`POST /sdk/popups/contact`). Rechaza si no se
6831
+ * pudo enviar (sin red o respuesta no-2xx), para que `ContactManager` no guarde la caché
6832
+ * y la llamada siguiente reintente.
6833
+ */
6726
6834
  async postContact(body) {
6727
6835
  const baseUrl = this.baseUrl;
6728
- if (!baseUrl) return;
6836
+ if (!baseUrl) throw new Error("no base URL");
6729
6837
  const endpoint = `${baseUrl}/sdk/popups/contact`;
6838
+ let response;
6730
6839
  try {
6731
- const response = await fetch(endpoint, {
6840
+ response = await fetch(endpoint, {
6732
6841
  method: "POST",
6733
6842
  headers: { "Content-Type": "application/json" },
6734
6843
  body: JSON.stringify(body)
6735
6844
  });
6736
- if (!response.ok) {
6737
- this.log("Failed to post contact", response.status, response.statusText);
6738
- return;
6739
- }
6740
- this.log("contact \xB7 sent attributes for user:", body.userId);
6741
6845
  } catch (error) {
6742
6846
  this.log("Error posting contact", error);
6847
+ throw error;
6848
+ }
6849
+ if (!response.ok) {
6850
+ this.log("Failed to post contact", response.status, response.statusText);
6851
+ throw new Error(`HTTP ${response.status}`);
6743
6852
  }
6853
+ this.log("contact \xB7 sent attributes for user:", body.userId);
6744
6854
  }
6745
6855
  /** Notifica eventos de popup a la API */
6746
6856
  async postPopupEvent(status, popupId, userId) {