@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.
@@ -1208,14 +1208,7 @@ function createFeedbackSink(options) {
1208
1208
  const post = async (body, final, sessionEnd = false) => {
1209
1209
  const json = JSON.stringify(body);
1210
1210
  const small = json.length <= KEEPALIVE_MAX_BYTES;
1211
- if (final && options.sendBeaconImpl && small) {
1212
- const payload = typeof Blob !== "undefined" ? new Blob([json], { type: "application/json" }) : json;
1213
- if (options.sendBeaconImpl(url, payload)) {
1214
- options.log?.("[DeepdotsAnalytics] final flush via sendBeacon");
1215
- return;
1216
- }
1217
- options.log?.("[DeepdotsAnalytics] sendBeacon rejected the batch; falling back to fetch");
1218
- }
1211
+ if (final) options.log?.("[DeepdotsAnalytics] final flush via fetch keepalive");
1219
1212
  const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
1220
1213
  if (!f) {
1221
1214
  options.log?.("[DeepdotsAnalytics] no fetch available; payload not sent", body);
@@ -1225,6 +1218,9 @@ function createFeedbackSink(options) {
1225
1218
  method: "POST",
1226
1219
  headers: { "Content-Type": "application/json" },
1227
1220
  body: json,
1221
+ // La API autentica por `publicKey` en el body y responde `Allow-Origin: *`: una
1222
+ // petición con credenciales fallaría el CORS. Explícito para no depender del default.
1223
+ credentials: "omit",
1228
1224
  // keepalive: el POST sobrevive a la navegación mientras el body sea pequeño.
1229
1225
  ...small ? { keepalive: true } : {}
1230
1226
  });
@@ -1317,6 +1313,15 @@ var NavigationObserver = class {
1317
1313
  this.closeCurrent();
1318
1314
  this.currentScreen = null;
1319
1315
  }
1316
+ /**
1317
+ * Retoma el seguimiento en la pantalla actual tras un `stop()`, cuando se abre una sesión
1318
+ * nueva sin navegar (cambio de usuario, vuelta de la bfcache). No-op si nunca se instaló
1319
+ * (navegación manual por `setScreen`, RN) o si ya hay pantalla en curso.
1320
+ */
1321
+ restart() {
1322
+ if (!this.installed || this.currentScreen != null || typeof window === "undefined") return;
1323
+ this.begin(window.location.href);
1324
+ }
1320
1325
  closeCurrent() {
1321
1326
  if (this.currentScreen == null) return;
1322
1327
  const exit = this.now();
@@ -1956,8 +1961,6 @@ var DeepdotsPopups = class {
1956
1961
  baseUrl: this.baseUrl,
1957
1962
  keys: config.analytics,
1958
1963
  log: (...a) => this.log(...a),
1959
- // sendBeacon: único transporte que sobrevive al cierre de la página.
1960
- sendBeaconImpl: typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" ? (url, body) => navigator.sendBeacon(url, body) : void 0,
1961
1964
  onSessionId: (id) => {
1962
1965
  this.analyticsFeedbackSessionId = id;
1963
1966
  },
@@ -2005,7 +2008,6 @@ var DeepdotsPopups = class {
2005
2008
  this.navObserver.onVisit((v) => this.track("deepdots_page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
2006
2009
  this.navObserver.install();
2007
2010
  this.engagement = new EngagementTracker();
2008
- this.engagement.resume();
2009
2011
  this.setupAnalyticsFlush();
2010
2012
  this.openSession();
2011
2013
  const pendingCrashes = this.crashReporter.drainPendingCrashes();
@@ -2224,6 +2226,7 @@ var DeepdotsPopups = class {
2224
2226
  this.navStarted = false;
2225
2227
  this.analytics?.exitAllMiniServices();
2226
2228
  this.flushEngagement();
2229
+ this.engagement?.pause();
2227
2230
  this.track("deepdots_session_end", { reason });
2228
2231
  this.flushAnalytics({ final: reason === "page_hide", sessionEnd: true });
2229
2232
  this.analyticsFeedbackSessionId = void 0;
@@ -2239,6 +2242,12 @@ var DeepdotsPopups = class {
2239
2242
  if (!this.tracking?.isTrackingEnabled()) return;
2240
2243
  this.sessionOpen = true;
2241
2244
  this.track("deepdots_session_start", {});
2245
+ this.navObserver?.restart();
2246
+ if (this.isDocumentVisible()) this.engagement?.resume();
2247
+ }
2248
+ /** Web: si la pestaña se ve. Sin `document` (RN) manda AppState vía onForeground/onBackground. */
2249
+ isDocumentVisible() {
2250
+ return typeof document === "undefined" || document.visibilityState !== "hidden";
2242
2251
  }
2243
2252
  /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
2244
2253
  previewAnalytics() {
@@ -2251,8 +2260,8 @@ var DeepdotsPopups = class {
2251
2260
  }
2252
2261
  /**
2253
2262
  * Envía el lote acumulado de analytics y vacía el buffer.
2254
- * `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
2255
- * al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
2263
+ * `final: true` (cierre de página/app): el lote no espera al primer POST en vuelo, porque
2264
+ * el documento se muere; un lote que falle por red o 5xx se re-encola para el siguiente flush.
2256
2265
  */
2257
2266
  flushAnalytics(options) {
2258
2267
  if (!this.tracking?.isTrackingEnabled()) return;
@@ -2273,21 +2282,30 @@ var DeepdotsPopups = class {
2273
2282
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
2274
2283
  setupAnalyticsFlush() {
2275
2284
  if (typeof document === "undefined" || typeof window === "undefined") return;
2276
- this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
2285
+ this.startAnalyticsFlushTimer();
2277
2286
  window.addEventListener("pagehide", () => {
2278
2287
  clearInterval(this.analyticsFlushTimer);
2279
2288
  this.closeSession("page_hide");
2280
2289
  });
2290
+ window.addEventListener("pageshow", (event) => {
2291
+ if (!event.persisted) return;
2292
+ this.startAnalyticsFlushTimer();
2293
+ this.openSession();
2294
+ });
2281
2295
  document.addEventListener("visibilitychange", () => {
2282
2296
  if (document.visibilityState === "hidden") {
2283
2297
  this.flushEngagement();
2284
2298
  this.engagement?.pause();
2285
2299
  this.flushAnalytics();
2286
2300
  } else if (document.visibilityState === "visible") {
2287
- this.engagement?.resume();
2301
+ if (this.sessionOpen) this.engagement?.resume();
2288
2302
  }
2289
2303
  });
2290
2304
  }
2305
+ startAnalyticsFlushTimer() {
2306
+ clearInterval(this.analyticsFlushTimer);
2307
+ this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
2308
+ }
2291
2309
  /** Enable auto-launch functionality with configured triggers */
2292
2310
  autoLaunch() {
2293
2311
  if (!this.initialized) {
@@ -2381,6 +2399,7 @@ var DeepdotsPopups = class {
2381
2399
  this.emitEvent("popup_shown", def.surveyId, { popupId: def.id });
2382
2400
  }
2383
2401
  shouldShow(def, pathUrl, skipPathCheck = false) {
2402
+ if (this.storage) this.loadPopupState(this.storage);
2384
2403
  if (this.isPathExcluded(def, pathUrl)) {
2385
2404
  return false;
2386
2405
  }
@@ -2907,22 +2926,32 @@ var DeepdotsPopups = class {
2907
2926
  this.persistPopupState();
2908
2927
  }
2909
2928
  /**
2910
- * Rehidrata el estado de redisplay desde el storage. `answeredSurveys` se reconstruye
2911
- * desde las entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
2929
+ * Fusiona el estado de redisplay del storage con el de memoria: por cada popup/survey gana
2930
+ * la entrada MÁS RECIENTE. Fusionar y no sustituir es lo que hace convivir a varias
2931
+ * pestañas sobre el mismo `localStorage`: se llama en el `init()`, antes de cada decisión
2932
+ * de mostrar y antes de cada escritura. `answeredSurveys` se reconstruye desde las
2933
+ * entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
2912
2934
  */
2913
2935
  loadPopupState(storage) {
2914
2936
  const state = readPopupState(storage);
2915
2937
  Object.entries(state.lastShown).forEach(([popupId, timestamp]) => {
2916
- this.lastShown.set(popupId, timestamp);
2938
+ if (timestamp > (this.lastShown.get(popupId) ?? -Infinity)) this.lastShown.set(popupId, timestamp);
2917
2939
  });
2918
2940
  Object.entries(state.progress).forEach(([surveyId, entry]) => {
2941
+ const current = this.surveyProgress.get(surveyId);
2942
+ if (current && current.timestamp >= entry.timestamp) return;
2919
2943
  this.surveyProgress.set(surveyId, { status: entry.status, timestamp: entry.timestamp });
2920
2944
  if (entry.status === "COMPLETED") this.answeredSurveys.add(surveyId);
2921
2945
  });
2922
2946
  }
2923
- /** Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria. */
2947
+ /**
2948
+ * Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria.
2949
+ * Fusiona antes de escribir: escribir solo la foto en memoria borraba lo que otra pestaña
2950
+ * hubiese guardado desde nuestro `init()`.
2951
+ */
2924
2952
  persistPopupState() {
2925
2953
  if (!this.storage) return;
2954
+ this.loadPopupState(this.storage);
2926
2955
  writePopupState(this.storage, {
2927
2956
  lastShown: Object.fromEntries(this.lastShown),
2928
2957
  progress: Object.fromEntries(this.surveyProgress)