@magicfeedback/popup-sdk 1.8.1 → 1.8.2

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.mts CHANGED
@@ -95,7 +95,8 @@ interface AnalyticsEnvelope {
95
95
  interface AnalyticsFlushMeta {
96
96
  /**
97
97
  * `true` cuando el flush ocurre porque la página/app se está cerrando. El sink debe usar
98
- * un transporte que sobreviva al unload (sendBeacon) y no puede esperar respuestas.
98
+ * un transporte que sobreviva al unload (`fetch` con `keepalive`) y no puede esperar
99
+ * respuestas.
99
100
  */
100
101
  final?: boolean;
101
102
  /**
@@ -174,9 +175,11 @@ interface FeedbackSinkOptions {
174
175
  /** fetch inyectable (tests / RN). */
175
176
  fetchImpl?: typeof fetch;
176
177
  /**
177
- * `navigator.sendBeacon` inyectable. Solo se usa en el flush final (cierre de página):
178
- * sobrevive al unload, a cambio de no poder leer la respuesta. Si no se pasa, el flush
179
- * final usa `fetch` con `keepalive`.
178
+ * @deprecated Se ignora. El flush final salía por `sendBeacon`, que va SIEMPRE con
179
+ * credenciales; contra la API (`Access-Control-Allow-Origin: *` + `Allow-Credentials:
180
+ * true`) el preflight fallaba y el lote de cierre, el único con `completed:true`, no
181
+ * llegaba nunca. Ahora sale por `fetch` con `keepalive`. Se mantiene en el tipo para no
182
+ * romper a quien lo pase.
180
183
  */
181
184
  sendBeaconImpl?: (url: string, body: Blob | string) => boolean;
182
185
  /** Callback invocado cuando el backend devuelve un nuevo sessionId (primer POST). */
@@ -194,8 +197,9 @@ interface FeedbackSinkOptions {
194
197
  * Garantías de entrega:
195
198
  * - mientras no se conozca el `sessionId`, los lotes se **serializan** (esperan la primera
196
199
  * respuesta): dos POSTs a la vez sin `sessionId` crearían dos registros y partirían los datos;
197
- * - `keepalive` para que el navegador no cancele el POST al navegar fuera, y `sendBeacon`
198
- * en el flush final (cierre de página), donde `fetch` puede morir con el documento;
200
+ * - `keepalive` para que el navegador no cancele el POST al navegar fuera ni al cerrar la
201
+ * página, también en el flush final (sin `sendBeacon`: ver `sendBeaconImpl`);
202
+ * - sin credenciales, que es lo único que pasa el CORS de la API (`Allow-Origin: *`);
199
203
  * - la promesa **rechaza** en fallo transitorio (red, 5xx, 408, 429) para que el
200
204
  * `AnalyticsManager` re-encole el lote; un 4xx se loguea y se descarta.
201
205
  */
@@ -801,12 +805,14 @@ declare class DeepdotsPopups {
801
805
  * se llama en init(), al volver a foreground y al conceder el consentimiento.
802
806
  */
803
807
  private openSession;
808
+ /** Web: si la pestaña se ve. Sin `document` (RN) manda AppState vía onForeground/onBackground. */
809
+ private isDocumentVisible;
804
810
  /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
805
811
  previewAnalytics(): AnalyticsEnvelope;
806
812
  /**
807
813
  * Envía el lote acumulado de analytics y vacía el buffer.
808
- * `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
809
- * al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
814
+ * `final: true` (cierre de página/app): el lote no espera al primer POST en vuelo, porque
815
+ * el documento se muere; un lote que falle por red o 5xx se re-encola para el siguiente flush.
810
816
  */
811
817
  flushAnalytics(options?: {
812
818
  final?: boolean;
@@ -817,6 +823,7 @@ declare class DeepdotsPopups {
817
823
  private analyticsIdentity;
818
824
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
819
825
  private setupAnalyticsFlush;
826
+ private startAnalyticsFlushTimer;
820
827
  /** Enable auto-launch functionality with configured triggers */
821
828
  autoLaunch(): void;
822
829
  /** Inicia los triggers configurados */
@@ -896,11 +903,18 @@ declare class DeepdotsPopups {
896
903
  private mapPopupTriggerType;
897
904
  private markSurveyProgress;
898
905
  /**
899
- * Rehidrata el estado de redisplay desde el storage. `answeredSurveys` se reconstruye
900
- * desde las entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
906
+ * Fusiona el estado de redisplay del storage con el de memoria: por cada popup/survey gana
907
+ * la entrada MÁS RECIENTE. Fusionar y no sustituir es lo que hace convivir a varias
908
+ * pestañas sobre el mismo `localStorage`: se llama en el `init()`, antes de cada decisión
909
+ * de mostrar y antes de cada escritura. `answeredSurveys` se reconstruye desde las
910
+ * entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
901
911
  */
902
912
  private loadPopupState;
903
- /** Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria. */
913
+ /**
914
+ * Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria.
915
+ * Fusiona antes de escribir: escribir solo la foto en memoria borraba lo que otra pestaña
916
+ * hubiese guardado desde nuestro `init()`.
917
+ */
904
918
  private persistPopupState;
905
919
  private isPopupDefinition;
906
920
  private isPopupTrigger;
package/dist/index.d.ts CHANGED
@@ -95,7 +95,8 @@ interface AnalyticsEnvelope {
95
95
  interface AnalyticsFlushMeta {
96
96
  /**
97
97
  * `true` cuando el flush ocurre porque la página/app se está cerrando. El sink debe usar
98
- * un transporte que sobreviva al unload (sendBeacon) y no puede esperar respuestas.
98
+ * un transporte que sobreviva al unload (`fetch` con `keepalive`) y no puede esperar
99
+ * respuestas.
99
100
  */
100
101
  final?: boolean;
101
102
  /**
@@ -174,9 +175,11 @@ interface FeedbackSinkOptions {
174
175
  /** fetch inyectable (tests / RN). */
175
176
  fetchImpl?: typeof fetch;
176
177
  /**
177
- * `navigator.sendBeacon` inyectable. Solo se usa en el flush final (cierre de página):
178
- * sobrevive al unload, a cambio de no poder leer la respuesta. Si no se pasa, el flush
179
- * final usa `fetch` con `keepalive`.
178
+ * @deprecated Se ignora. El flush final salía por `sendBeacon`, que va SIEMPRE con
179
+ * credenciales; contra la API (`Access-Control-Allow-Origin: *` + `Allow-Credentials:
180
+ * true`) el preflight fallaba y el lote de cierre, el único con `completed:true`, no
181
+ * llegaba nunca. Ahora sale por `fetch` con `keepalive`. Se mantiene en el tipo para no
182
+ * romper a quien lo pase.
180
183
  */
181
184
  sendBeaconImpl?: (url: string, body: Blob | string) => boolean;
182
185
  /** Callback invocado cuando el backend devuelve un nuevo sessionId (primer POST). */
@@ -194,8 +197,9 @@ interface FeedbackSinkOptions {
194
197
  * Garantías de entrega:
195
198
  * - mientras no se conozca el `sessionId`, los lotes se **serializan** (esperan la primera
196
199
  * respuesta): dos POSTs a la vez sin `sessionId` crearían dos registros y partirían los datos;
197
- * - `keepalive` para que el navegador no cancele el POST al navegar fuera, y `sendBeacon`
198
- * en el flush final (cierre de página), donde `fetch` puede morir con el documento;
200
+ * - `keepalive` para que el navegador no cancele el POST al navegar fuera ni al cerrar la
201
+ * página, también en el flush final (sin `sendBeacon`: ver `sendBeaconImpl`);
202
+ * - sin credenciales, que es lo único que pasa el CORS de la API (`Allow-Origin: *`);
199
203
  * - la promesa **rechaza** en fallo transitorio (red, 5xx, 408, 429) para que el
200
204
  * `AnalyticsManager` re-encole el lote; un 4xx se loguea y se descarta.
201
205
  */
@@ -801,12 +805,14 @@ declare class DeepdotsPopups {
801
805
  * se llama en init(), al volver a foreground y al conceder el consentimiento.
802
806
  */
803
807
  private openSession;
808
+ /** Web: si la pestaña se ve. Sin `document` (RN) manda AppState vía onForeground/onBackground. */
809
+ private isDocumentVisible;
804
810
  /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
805
811
  previewAnalytics(): AnalyticsEnvelope;
806
812
  /**
807
813
  * Envía el lote acumulado de analytics y vacía el buffer.
808
- * `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
809
- * al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
814
+ * `final: true` (cierre de página/app): el lote no espera al primer POST en vuelo, porque
815
+ * el documento se muere; un lote que falle por red o 5xx se re-encola para el siguiente flush.
810
816
  */
811
817
  flushAnalytics(options?: {
812
818
  final?: boolean;
@@ -817,6 +823,7 @@ declare class DeepdotsPopups {
817
823
  private analyticsIdentity;
818
824
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
819
825
  private setupAnalyticsFlush;
826
+ private startAnalyticsFlushTimer;
820
827
  /** Enable auto-launch functionality with configured triggers */
821
828
  autoLaunch(): void;
822
829
  /** Inicia los triggers configurados */
@@ -896,11 +903,18 @@ declare class DeepdotsPopups {
896
903
  private mapPopupTriggerType;
897
904
  private markSurveyProgress;
898
905
  /**
899
- * Rehidrata el estado de redisplay desde el storage. `answeredSurveys` se reconstruye
900
- * desde las entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
906
+ * Fusiona el estado de redisplay del storage con el de memoria: por cada popup/survey gana
907
+ * la entrada MÁS RECIENTE. Fusionar y no sustituir es lo que hace convivir a varias
908
+ * pestañas sobre el mismo `localStorage`: se llama en el `init()`, antes de cada decisión
909
+ * de mostrar y antes de cada escritura. `answeredSurveys` se reconstruye desde las
910
+ * entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
901
911
  */
902
912
  private loadPopupState;
903
- /** Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria. */
913
+ /**
914
+ * Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria.
915
+ * Fusiona antes de escribir: escribir solo la foto en memoria borraba lo que otra pestaña
916
+ * hubiese guardado desde nuestro `init()`.
917
+ */
904
918
  private persistPopupState;
905
919
  private isPopupDefinition;
906
920
  private isPopupTrigger;
package/dist/index.js CHANGED
@@ -2454,9 +2454,9 @@ var init_logo = __esm({
2454
2454
  }
2455
2455
  });
2456
2456
 
2457
- // node_modules/@magicfeedback/native/dist/magicfeedback-sdk.browser.js
2457
+ // ../../../../../../../Users/sarias/developer/Deepdots/deepdots-popup-sdk/node_modules/@magicfeedback/native/dist/magicfeedback-sdk.browser.js
2458
2458
  var require_magicfeedback_sdk_browser = __commonJS({
2459
- "node_modules/@magicfeedback/native/dist/magicfeedback-sdk.browser.js"(exports, module2) {
2459
+ "../../../../../../../Users/sarias/developer/Deepdots/deepdots-popup-sdk/node_modules/@magicfeedback/native/dist/magicfeedback-sdk.browser.js"(exports, module2) {
2460
2460
  "use strict";
2461
2461
  !(function(e, t) {
2462
2462
  "object" == typeof exports && "object" == typeof module2 ? module2.exports = t() : "function" == typeof define && define.amd ? define([], t) : "object" == typeof exports ? exports.magicfeedback = t() : e.magicfeedback = t();
@@ -6804,14 +6804,7 @@ function createFeedbackSink(options) {
6804
6804
  const post = async (body, final, sessionEnd = false) => {
6805
6805
  const json = JSON.stringify(body);
6806
6806
  const small = json.length <= KEEPALIVE_MAX_BYTES;
6807
- if (final && options.sendBeaconImpl && small) {
6808
- const payload = typeof Blob !== "undefined" ? new Blob([json], { type: "application/json" }) : json;
6809
- if (options.sendBeaconImpl(url, payload)) {
6810
- options.log?.("[DeepdotsAnalytics] final flush via sendBeacon");
6811
- return;
6812
- }
6813
- options.log?.("[DeepdotsAnalytics] sendBeacon rejected the batch; falling back to fetch");
6814
- }
6807
+ if (final) options.log?.("[DeepdotsAnalytics] final flush via fetch keepalive");
6815
6808
  const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
6816
6809
  if (!f) {
6817
6810
  options.log?.("[DeepdotsAnalytics] no fetch available; payload not sent", body);
@@ -6821,6 +6814,9 @@ function createFeedbackSink(options) {
6821
6814
  method: "POST",
6822
6815
  headers: { "Content-Type": "application/json" },
6823
6816
  body: json,
6817
+ // La API autentica por `publicKey` en el body y responde `Allow-Origin: *`: una
6818
+ // petición con credenciales fallaría el CORS. Explícito para no depender del default.
6819
+ credentials: "omit",
6824
6820
  // keepalive: el POST sobrevive a la navegación mientras el body sea pequeño.
6825
6821
  ...small ? { keepalive: true } : {}
6826
6822
  });
@@ -6913,6 +6909,15 @@ var NavigationObserver = class {
6913
6909
  this.closeCurrent();
6914
6910
  this.currentScreen = null;
6915
6911
  }
6912
+ /**
6913
+ * Retoma el seguimiento en la pantalla actual tras un `stop()`, cuando se abre una sesión
6914
+ * nueva sin navegar (cambio de usuario, vuelta de la bfcache). No-op si nunca se instaló
6915
+ * (navegación manual por `setScreen`, RN) o si ya hay pantalla en curso.
6916
+ */
6917
+ restart() {
6918
+ if (!this.installed || this.currentScreen != null || typeof window === "undefined") return;
6919
+ this.begin(window.location.href);
6920
+ }
6916
6921
  closeCurrent() {
6917
6922
  if (this.currentScreen == null) return;
6918
6923
  const exit = this.now();
@@ -7556,8 +7561,6 @@ var DeepdotsPopups = class {
7556
7561
  baseUrl: this.baseUrl,
7557
7562
  keys: config.analytics,
7558
7563
  log: (...a) => this.log(...a),
7559
- // sendBeacon: único transporte que sobrevive al cierre de la página.
7560
- sendBeaconImpl: typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" ? (url, body) => navigator.sendBeacon(url, body) : void 0,
7561
7564
  onSessionId: (id) => {
7562
7565
  this.analyticsFeedbackSessionId = id;
7563
7566
  },
@@ -7605,7 +7608,6 @@ var DeepdotsPopups = class {
7605
7608
  this.navObserver.onVisit((v) => this.track("deepdots_page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
7606
7609
  this.navObserver.install();
7607
7610
  this.engagement = new EngagementTracker();
7608
- this.engagement.resume();
7609
7611
  this.setupAnalyticsFlush();
7610
7612
  this.openSession();
7611
7613
  const pendingCrashes = this.crashReporter.drainPendingCrashes();
@@ -7824,6 +7826,7 @@ var DeepdotsPopups = class {
7824
7826
  this.navStarted = false;
7825
7827
  this.analytics?.exitAllMiniServices();
7826
7828
  this.flushEngagement();
7829
+ this.engagement?.pause();
7827
7830
  this.track("deepdots_session_end", { reason });
7828
7831
  this.flushAnalytics({ final: reason === "page_hide", sessionEnd: true });
7829
7832
  this.analyticsFeedbackSessionId = void 0;
@@ -7839,6 +7842,12 @@ var DeepdotsPopups = class {
7839
7842
  if (!this.tracking?.isTrackingEnabled()) return;
7840
7843
  this.sessionOpen = true;
7841
7844
  this.track("deepdots_session_start", {});
7845
+ this.navObserver?.restart();
7846
+ if (this.isDocumentVisible()) this.engagement?.resume();
7847
+ }
7848
+ /** Web: si la pestaña se ve. Sin `document` (RN) manda AppState vía onForeground/onBackground. */
7849
+ isDocumentVisible() {
7850
+ return typeof document === "undefined" || document.visibilityState !== "hidden";
7842
7851
  }
7843
7852
  /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
7844
7853
  previewAnalytics() {
@@ -7851,8 +7860,8 @@ var DeepdotsPopups = class {
7851
7860
  }
7852
7861
  /**
7853
7862
  * Envía el lote acumulado de analytics y vacía el buffer.
7854
- * `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
7855
- * al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
7863
+ * `final: true` (cierre de página/app): el lote no espera al primer POST en vuelo, porque
7864
+ * el documento se muere; un lote que falle por red o 5xx se re-encola para el siguiente flush.
7856
7865
  */
7857
7866
  flushAnalytics(options) {
7858
7867
  if (!this.tracking?.isTrackingEnabled()) return;
@@ -7873,21 +7882,30 @@ var DeepdotsPopups = class {
7873
7882
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
7874
7883
  setupAnalyticsFlush() {
7875
7884
  if (typeof document === "undefined" || typeof window === "undefined") return;
7876
- this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
7885
+ this.startAnalyticsFlushTimer();
7877
7886
  window.addEventListener("pagehide", () => {
7878
7887
  clearInterval(this.analyticsFlushTimer);
7879
7888
  this.closeSession("page_hide");
7880
7889
  });
7890
+ window.addEventListener("pageshow", (event) => {
7891
+ if (!event.persisted) return;
7892
+ this.startAnalyticsFlushTimer();
7893
+ this.openSession();
7894
+ });
7881
7895
  document.addEventListener("visibilitychange", () => {
7882
7896
  if (document.visibilityState === "hidden") {
7883
7897
  this.flushEngagement();
7884
7898
  this.engagement?.pause();
7885
7899
  this.flushAnalytics();
7886
7900
  } else if (document.visibilityState === "visible") {
7887
- this.engagement?.resume();
7901
+ if (this.sessionOpen) this.engagement?.resume();
7888
7902
  }
7889
7903
  });
7890
7904
  }
7905
+ startAnalyticsFlushTimer() {
7906
+ clearInterval(this.analyticsFlushTimer);
7907
+ this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
7908
+ }
7891
7909
  /** Enable auto-launch functionality with configured triggers */
7892
7910
  autoLaunch() {
7893
7911
  if (!this.initialized) {
@@ -7981,6 +7999,7 @@ var DeepdotsPopups = class {
7981
7999
  this.emitEvent("popup_shown", def.surveyId, { popupId: def.id });
7982
8000
  }
7983
8001
  shouldShow(def, pathUrl, skipPathCheck = false) {
8002
+ if (this.storage) this.loadPopupState(this.storage);
7984
8003
  if (this.isPathExcluded(def, pathUrl)) {
7985
8004
  return false;
7986
8005
  }
@@ -8507,22 +8526,32 @@ var DeepdotsPopups = class {
8507
8526
  this.persistPopupState();
8508
8527
  }
8509
8528
  /**
8510
- * Rehidrata el estado de redisplay desde el storage. `answeredSurveys` se reconstruye
8511
- * desde las entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
8529
+ * Fusiona el estado de redisplay del storage con el de memoria: por cada popup/survey gana
8530
+ * la entrada MÁS RECIENTE. Fusionar y no sustituir es lo que hace convivir a varias
8531
+ * pestañas sobre el mismo `localStorage`: se llama en el `init()`, antes de cada decisión
8532
+ * de mostrar y antes de cada escritura. `answeredSurveys` se reconstruye desde las
8533
+ * entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
8512
8534
  */
8513
8535
  loadPopupState(storage) {
8514
8536
  const state = readPopupState(storage);
8515
8537
  Object.entries(state.lastShown).forEach(([popupId, timestamp]) => {
8516
- this.lastShown.set(popupId, timestamp);
8538
+ if (timestamp > (this.lastShown.get(popupId) ?? -Infinity)) this.lastShown.set(popupId, timestamp);
8517
8539
  });
8518
8540
  Object.entries(state.progress).forEach(([surveyId, entry]) => {
8541
+ const current = this.surveyProgress.get(surveyId);
8542
+ if (current && current.timestamp >= entry.timestamp) return;
8519
8543
  this.surveyProgress.set(surveyId, { status: entry.status, timestamp: entry.timestamp });
8520
8544
  if (entry.status === "COMPLETED") this.answeredSurveys.add(surveyId);
8521
8545
  });
8522
8546
  }
8523
- /** Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria. */
8547
+ /**
8548
+ * Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria.
8549
+ * Fusiona antes de escribir: escribir solo la foto en memoria borraba lo que otra pestaña
8550
+ * hubiese guardado desde nuestro `init()`.
8551
+ */
8524
8552
  persistPopupState() {
8525
8553
  if (!this.storage) return;
8554
+ this.loadPopupState(this.storage);
8526
8555
  writePopupState(this.storage, {
8527
8556
  lastShown: Object.fromEntries(this.lastShown),
8528
8557
  progress: Object.fromEntries(this.surveyProgress)