@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.
@@ -507,12 +507,14 @@ declare class DeepdotsPopups {
507
507
  * se llama en init(), al volver a foreground y al conceder el consentimiento.
508
508
  */
509
509
  private openSession;
510
+ /** Web: si la pestaña se ve. Sin `document` (RN) manda AppState vía onForeground/onBackground. */
511
+ private isDocumentVisible;
510
512
  /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
511
513
  previewAnalytics(): AnalyticsEnvelope;
512
514
  /**
513
515
  * Envía el lote acumulado de analytics y vacía el buffer.
514
- * `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
515
- * al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
516
+ * `final: true` (cierre de página/app): el lote no espera al primer POST en vuelo, porque
517
+ * el documento se muere; un lote que falle por red o 5xx se re-encola para el siguiente flush.
516
518
  */
517
519
  flushAnalytics(options?: {
518
520
  final?: boolean;
@@ -523,6 +525,7 @@ declare class DeepdotsPopups {
523
525
  private analyticsIdentity;
524
526
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
525
527
  private setupAnalyticsFlush;
528
+ private startAnalyticsFlushTimer;
526
529
  /** Enable auto-launch functionality with configured triggers */
527
530
  autoLaunch(): void;
528
531
  /** Inicia los triggers configurados */
@@ -602,11 +605,18 @@ declare class DeepdotsPopups {
602
605
  private mapPopupTriggerType;
603
606
  private markSurveyProgress;
604
607
  /**
605
- * Rehidrata el estado de redisplay desde el storage. `answeredSurveys` se reconstruye
606
- * desde las entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
608
+ * Fusiona el estado de redisplay del storage con el de memoria: por cada popup/survey gana
609
+ * la entrada MÁS RECIENTE. Fusionar y no sustituir es lo que hace convivir a varias
610
+ * pestañas sobre el mismo `localStorage`: se llama en el `init()`, antes de cada decisión
611
+ * de mostrar y antes de cada escritura. `answeredSurveys` se reconstruye desde las
612
+ * entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
607
613
  */
608
614
  private loadPopupState;
609
- /** Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria. */
615
+ /**
616
+ * Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria.
617
+ * Fusiona antes de escribir: escribir solo la foto en memoria borraba lo que otra pestaña
618
+ * hubiese guardado desde nuestro `init()`.
619
+ */
610
620
  private persistPopupState;
611
621
  private isPopupDefinition;
612
622
  private isPopupTrigger;
@@ -507,12 +507,14 @@ declare class DeepdotsPopups {
507
507
  * se llama en init(), al volver a foreground y al conceder el consentimiento.
508
508
  */
509
509
  private openSession;
510
+ /** Web: si la pestaña se ve. Sin `document` (RN) manda AppState vía onForeground/onBackground. */
511
+ private isDocumentVisible;
510
512
  /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
511
513
  previewAnalytics(): AnalyticsEnvelope;
512
514
  /**
513
515
  * Envía el lote acumulado de analytics y vacía el buffer.
514
- * `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
515
- * al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
516
+ * `final: true` (cierre de página/app): el lote no espera al primer POST en vuelo, porque
517
+ * el documento se muere; un lote que falle por red o 5xx se re-encola para el siguiente flush.
516
518
  */
517
519
  flushAnalytics(options?: {
518
520
  final?: boolean;
@@ -523,6 +525,7 @@ declare class DeepdotsPopups {
523
525
  private analyticsIdentity;
524
526
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
525
527
  private setupAnalyticsFlush;
528
+ private startAnalyticsFlushTimer;
526
529
  /** Enable auto-launch functionality with configured triggers */
527
530
  autoLaunch(): void;
528
531
  /** Inicia los triggers configurados */
@@ -602,11 +605,18 @@ declare class DeepdotsPopups {
602
605
  private mapPopupTriggerType;
603
606
  private markSurveyProgress;
604
607
  /**
605
- * Rehidrata el estado de redisplay desde el storage. `answeredSurveys` se reconstruye
606
- * desde las entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
608
+ * Fusiona el estado de redisplay del storage con el de memoria: por cada popup/survey gana
609
+ * la entrada MÁS RECIENTE. Fusionar y no sustituir es lo que hace convivir a varias
610
+ * pestañas sobre el mismo `localStorage`: se llama en el `init()`, antes de cada decisión
611
+ * de mostrar y antes de cada escritura. `answeredSurveys` se reconstruye desde las
612
+ * entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
607
613
  */
608
614
  private loadPopupState;
609
- /** Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria. */
615
+ /**
616
+ * Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria.
617
+ * Fusiona antes de escribir: escribir solo la foto en memoria borraba lo que otra pestaña
618
+ * hubiese guardado desde nuestro `init()`.
619
+ */
610
620
  private persistPopupState;
611
621
  private isPopupDefinition;
612
622
  private isPopupTrigger;
@@ -4364,14 +4364,7 @@ function createFeedbackSink(options) {
4364
4364
  const post = async (body, final, sessionEnd = false) => {
4365
4365
  const json = JSON.stringify(body);
4366
4366
  const small = json.length <= KEEPALIVE_MAX_BYTES;
4367
- if (final && options.sendBeaconImpl && small) {
4368
- const payload = typeof Blob !== "undefined" ? new Blob([json], { type: "application/json" }) : json;
4369
- if (options.sendBeaconImpl(url, payload)) {
4370
- options.log?.("[DeepdotsAnalytics] final flush via sendBeacon");
4371
- return;
4372
- }
4373
- options.log?.("[DeepdotsAnalytics] sendBeacon rejected the batch; falling back to fetch");
4374
- }
4367
+ if (final) options.log?.("[DeepdotsAnalytics] final flush via fetch keepalive");
4375
4368
  const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
4376
4369
  if (!f) {
4377
4370
  options.log?.("[DeepdotsAnalytics] no fetch available; payload not sent", body);
@@ -4381,6 +4374,9 @@ function createFeedbackSink(options) {
4381
4374
  method: "POST",
4382
4375
  headers: { "Content-Type": "application/json" },
4383
4376
  body: json,
4377
+ // La API autentica por `publicKey` en el body y responde `Allow-Origin: *`: una
4378
+ // petición con credenciales fallaría el CORS. Explícito para no depender del default.
4379
+ credentials: "omit",
4384
4380
  // keepalive: el POST sobrevive a la navegación mientras el body sea pequeño.
4385
4381
  ...small ? { keepalive: true } : {}
4386
4382
  });
@@ -4473,6 +4469,15 @@ var NavigationObserver = class {
4473
4469
  this.closeCurrent();
4474
4470
  this.currentScreen = null;
4475
4471
  }
4472
+ /**
4473
+ * Retoma el seguimiento en la pantalla actual tras un `stop()`, cuando se abre una sesión
4474
+ * nueva sin navegar (cambio de usuario, vuelta de la bfcache). No-op si nunca se instaló
4475
+ * (navegación manual por `setScreen`, RN) o si ya hay pantalla en curso.
4476
+ */
4477
+ restart() {
4478
+ if (!this.installed || this.currentScreen != null || typeof window === "undefined") return;
4479
+ this.begin(window.location.href);
4480
+ }
4476
4481
  closeCurrent() {
4477
4482
  if (this.currentScreen == null) return;
4478
4483
  const exit = this.now();
@@ -5116,8 +5121,6 @@ var DeepdotsPopups = class {
5116
5121
  baseUrl: this.baseUrl,
5117
5122
  keys: config.analytics,
5118
5123
  log: (...a) => this.log(...a),
5119
- // sendBeacon: único transporte que sobrevive al cierre de la página.
5120
- sendBeaconImpl: typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" ? (url, body) => navigator.sendBeacon(url, body) : void 0,
5121
5124
  onSessionId: (id) => {
5122
5125
  this.analyticsFeedbackSessionId = id;
5123
5126
  },
@@ -5165,7 +5168,6 @@ var DeepdotsPopups = class {
5165
5168
  this.navObserver.onVisit((v) => this.track("deepdots_page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
5166
5169
  this.navObserver.install();
5167
5170
  this.engagement = new EngagementTracker();
5168
- this.engagement.resume();
5169
5171
  this.setupAnalyticsFlush();
5170
5172
  this.openSession();
5171
5173
  const pendingCrashes = this.crashReporter.drainPendingCrashes();
@@ -5384,6 +5386,7 @@ var DeepdotsPopups = class {
5384
5386
  this.navStarted = false;
5385
5387
  this.analytics?.exitAllMiniServices();
5386
5388
  this.flushEngagement();
5389
+ this.engagement?.pause();
5387
5390
  this.track("deepdots_session_end", { reason });
5388
5391
  this.flushAnalytics({ final: reason === "page_hide", sessionEnd: true });
5389
5392
  this.analyticsFeedbackSessionId = void 0;
@@ -5399,6 +5402,12 @@ var DeepdotsPopups = class {
5399
5402
  if (!this.tracking?.isTrackingEnabled()) return;
5400
5403
  this.sessionOpen = true;
5401
5404
  this.track("deepdots_session_start", {});
5405
+ this.navObserver?.restart();
5406
+ if (this.isDocumentVisible()) this.engagement?.resume();
5407
+ }
5408
+ /** Web: si la pestaña se ve. Sin `document` (RN) manda AppState vía onForeground/onBackground. */
5409
+ isDocumentVisible() {
5410
+ return typeof document === "undefined" || document.visibilityState !== "hidden";
5402
5411
  }
5403
5412
  /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
5404
5413
  previewAnalytics() {
@@ -5411,8 +5420,8 @@ var DeepdotsPopups = class {
5411
5420
  }
5412
5421
  /**
5413
5422
  * Envía el lote acumulado de analytics y vacía el buffer.
5414
- * `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
5415
- * al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
5423
+ * `final: true` (cierre de página/app): el lote no espera al primer POST en vuelo, porque
5424
+ * el documento se muere; un lote que falle por red o 5xx se re-encola para el siguiente flush.
5416
5425
  */
5417
5426
  flushAnalytics(options) {
5418
5427
  if (!this.tracking?.isTrackingEnabled()) return;
@@ -5433,21 +5442,30 @@ var DeepdotsPopups = class {
5433
5442
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
5434
5443
  setupAnalyticsFlush() {
5435
5444
  if (typeof document === "undefined" || typeof window === "undefined") return;
5436
- this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
5445
+ this.startAnalyticsFlushTimer();
5437
5446
  window.addEventListener("pagehide", () => {
5438
5447
  clearInterval(this.analyticsFlushTimer);
5439
5448
  this.closeSession("page_hide");
5440
5449
  });
5450
+ window.addEventListener("pageshow", (event) => {
5451
+ if (!event.persisted) return;
5452
+ this.startAnalyticsFlushTimer();
5453
+ this.openSession();
5454
+ });
5441
5455
  document.addEventListener("visibilitychange", () => {
5442
5456
  if (document.visibilityState === "hidden") {
5443
5457
  this.flushEngagement();
5444
5458
  this.engagement?.pause();
5445
5459
  this.flushAnalytics();
5446
5460
  } else if (document.visibilityState === "visible") {
5447
- this.engagement?.resume();
5461
+ if (this.sessionOpen) this.engagement?.resume();
5448
5462
  }
5449
5463
  });
5450
5464
  }
5465
+ startAnalyticsFlushTimer() {
5466
+ clearInterval(this.analyticsFlushTimer);
5467
+ this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
5468
+ }
5451
5469
  /** Enable auto-launch functionality with configured triggers */
5452
5470
  autoLaunch() {
5453
5471
  if (!this.initialized) {
@@ -5541,6 +5559,7 @@ var DeepdotsPopups = class {
5541
5559
  this.emitEvent("popup_shown", def.surveyId, { popupId: def.id });
5542
5560
  }
5543
5561
  shouldShow(def, pathUrl, skipPathCheck = false) {
5562
+ if (this.storage) this.loadPopupState(this.storage);
5544
5563
  if (this.isPathExcluded(def, pathUrl)) {
5545
5564
  return false;
5546
5565
  }
@@ -6067,22 +6086,32 @@ var DeepdotsPopups = class {
6067
6086
  this.persistPopupState();
6068
6087
  }
6069
6088
  /**
6070
- * Rehidrata el estado de redisplay desde el storage. `answeredSurveys` se reconstruye
6071
- * desde las entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
6089
+ * Fusiona el estado de redisplay del storage con el de memoria: por cada popup/survey gana
6090
+ * la entrada MÁS RECIENTE. Fusionar y no sustituir es lo que hace convivir a varias
6091
+ * pestañas sobre el mismo `localStorage`: se llama en el `init()`, antes de cada decisión
6092
+ * de mostrar y antes de cada escritura. `answeredSurveys` se reconstruye desde las
6093
+ * entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
6072
6094
  */
6073
6095
  loadPopupState(storage) {
6074
6096
  const state = readPopupState(storage);
6075
6097
  Object.entries(state.lastShown).forEach(([popupId, timestamp]) => {
6076
- this.lastShown.set(popupId, timestamp);
6098
+ if (timestamp > (this.lastShown.get(popupId) ?? -Infinity)) this.lastShown.set(popupId, timestamp);
6077
6099
  });
6078
6100
  Object.entries(state.progress).forEach(([surveyId, entry]) => {
6101
+ const current = this.surveyProgress.get(surveyId);
6102
+ if (current && current.timestamp >= entry.timestamp) return;
6079
6103
  this.surveyProgress.set(surveyId, { status: entry.status, timestamp: entry.timestamp });
6080
6104
  if (entry.status === "COMPLETED") this.answeredSurveys.add(surveyId);
6081
6105
  });
6082
6106
  }
6083
- /** Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria. */
6107
+ /**
6108
+ * Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria.
6109
+ * Fusiona antes de escribir: escribir solo la foto en memoria borraba lo que otra pestaña
6110
+ * hubiese guardado desde nuestro `init()`.
6111
+ */
6084
6112
  persistPopupState() {
6085
6113
  if (!this.storage) return;
6114
+ this.loadPopupState(this.storage);
6086
6115
  writePopupState(this.storage, {
6087
6116
  lastShown: Object.fromEntries(this.lastShown),
6088
6117
  progress: Object.fromEntries(this.surveyProgress)