@magicfeedback/popup-sdk 1.8.1 → 1.8.3

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/README.md CHANGED
@@ -122,6 +122,11 @@ Config fields:
122
122
  - `trackingEnabled?: boolean` — starts analytics on (default) or off, for consent flows
123
123
  - `analytics?: { publicKey: string; integration: string }` — enables the real analytics
124
124
  delivery. Without it the analytics channel stays in dry-run and only logs the payload.
125
+ - `geolocation?: boolean` — IP geolocation (country/city) for analytics, default `true`. It
126
+ calls third-party services (`ipapi.co`, then `ipwho.is`, then `ipinfo.io` as fallbacks, 3 s
127
+ timeout each, stopping at the first answer). It only runs when `analytics` is set, tracking
128
+ is enabled and the 30-day cache is missing or expired, at most once per SDK instance. Pass
129
+ `false` to never call those services nor attach country/city.
125
130
  - `appVersion?: string` — reported in the analytics device context
126
131
  - `storage?: KeyValueStorage` — persistence override (React Native uses MMKV; the browser
127
132
  defaults to `localStorage`)
@@ -268,7 +273,7 @@ session start/end and unhandled errors are automatic. The rest is instrumented b
268
273
  | `trackFunnelStep(funnel, step, taskId, params?)` | A funnel step, correlated by `taskId` |
269
274
  | `trackFindabilityFriction(topic, params?)` | Friction signal |
270
275
  | `trackMeaningfulInteraction(interactionType, params?)` | A meaningful interaction, grouped by `interaction_type` |
271
- | `enterMiniService(name, entryPointType?)` / `exitMiniService(name)` | Mini-service, with duration |
276
+ | `enterMiniService(name, entryPointType?)` / `exitMiniService(name)` | A task flow with a start and an end — [not a page](#mini-services--enterminiservicename-entrypointtype--exitminiservicename) |
272
277
  | `setUserAttributes(map)` / `setMetric(key, value)` | Breakdown dimensions / measurable values |
273
278
  | `setUserId(userId?)` | Login, logout or account switch |
274
279
  | `endSession()` | Closes the session explicitly |
@@ -332,6 +337,32 @@ values (oldest evicted first). A rejected event does not consume state: after a
332
337
  > restricted permissions, will not fire it — so a client-side `delivered` count sits below the
333
338
  > real one. For a reliable denominator, take `delivered` from your sending provider.
334
339
 
340
+ ### Mini-services — `enterMiniService(name, entryPointType?)` / `exitMiniService(name)`
341
+
342
+ A mini-service is a **self-contained task inside the app, with a start and an end**: a booking,
343
+ a checkout, an onboarding, a form the user fills in. Deepdots treats it as a flow. It reports
344
+ completion and drop-off per mini-service, draws it in the session journey as a lane between
345
+ where it was entered and where it was left, and tags every event emitted while it is active
346
+ with `mini_service`.
347
+
348
+ ```ts
349
+ popups.enterMiniService('checkout', 'cart_button'); // the user starts the task
350
+ // ...
351
+ popups.exitMiniService('checkout'); // and finishes it
352
+ ```
353
+
354
+ - Enter it when the task starts and exit it when the task is done. One still open when the
355
+ session closes is exited by the SDK.
356
+ - `entryPointType` is optional and says where the user started it from (a banner, a menu, a
357
+ deep link). It is sent as `entry_point_type`.
358
+
359
+ **A mini-service is not a page, a section or an area of the app.** Navigation is already
360
+ recorded as `page_view`, with the screen and how long the user stayed on it, and time per area
361
+ comes from grouping those page views by screen. Opening a mini-service for every section turns
362
+ each navigation into an abandoned flow: a move from `/feedback` to `/explorer` reads as
363
+ *"entered the explorer mini-service and never completed it"*, the journey loses the edge
364
+ between the two pages, and every section counts as drop-off.
365
+
335
366
  ## Supported Trigger Types
336
367
 
337
368
  Popup definitions use these trigger types:
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
  /**
@@ -104,6 +105,12 @@ interface AnalyticsFlushMeta {
104
105
  * el `sessionId`, para que el lote siguiente abra un registro nuevo.
105
106
  */
106
107
  sessionEnd?: boolean;
108
+ /**
109
+ * `true` para olvidar la sesión SIN enviar nada (el payload llega vacío): el backend ya la
110
+ * cerró por inactividad. El sink descarta su `sessionId` para que el lote siguiente abra un
111
+ * registro nuevo, igual que tras `sessionEnd`, pero sin POST.
112
+ */
113
+ resetSession?: boolean;
107
114
  }
108
115
  /**
109
116
  * Envío del lote. Si devuelve una promesa que **rechaza**, el manager re-encola los
@@ -174,9 +181,11 @@ interface FeedbackSinkOptions {
174
181
  /** fetch inyectable (tests / RN). */
175
182
  fetchImpl?: typeof fetch;
176
183
  /**
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`.
184
+ * @deprecated Se ignora. El flush final salía por `sendBeacon`, que va SIEMPRE con
185
+ * credenciales; contra la API (`Access-Control-Allow-Origin: *` + `Allow-Credentials:
186
+ * true`) el preflight fallaba y el lote de cierre, el único con `completed:true`, no
187
+ * llegaba nunca. Ahora sale por `fetch` con `keepalive`. Se mantiene en el tipo para no
188
+ * romper a quien lo pase.
180
189
  */
181
190
  sendBeaconImpl?: (url: string, body: Blob | string) => boolean;
182
191
  /** Callback invocado cuando el backend devuelve un nuevo sessionId (primer POST). */
@@ -194,8 +203,9 @@ interface FeedbackSinkOptions {
194
203
  * Garantías de entrega:
195
204
  * - mientras no se conozca el `sessionId`, los lotes se **serializan** (esperan la primera
196
205
  * 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;
206
+ * - `keepalive` para que el navegador no cancele el POST al navegar fuera ni al cerrar la
207
+ * página, también en el flush final (sin `sendBeacon`: ver `sendBeaconImpl`);
208
+ * - sin credenciales, que es lo único que pasa el CORS de la API (`Allow-Origin: *`);
199
209
  * - la promesa **rechaza** en fallo transitorio (red, 5xx, 408, 429) para que el
200
210
  * `AnalyticsManager` re-encole el lote; un 4xx se loguea y se descarta.
201
211
  */
@@ -250,6 +260,13 @@ interface DeepdotsInitParams {
250
260
  * to the configured integration; if missing, it stays in dry-run mode (console.log only).
251
261
  */
252
262
  analytics?: AnalyticsKeys;
263
+ /**
264
+ * IP geolocation (country/city) added to analytics. Default `true`. The lookup calls
265
+ * third-party services (ipapi.co, then ipwho.is, then ipinfo.io as fallbacks, 3 s timeout
266
+ * each) and only runs when `analytics` is configured, tracking is enabled and the 30-day
267
+ * cache is missing or expired. Set `false` to never call them nor attach country/city.
268
+ */
269
+ geolocation?: boolean;
253
270
  /**
254
271
  * Internal user attributes known only by the host (language, age, plan, ...), sent
255
272
  * to the backend Contact for segmentation/targeting. Requires `userId`. Equivalent to calling
@@ -326,8 +343,10 @@ type EventListener = (event: DeepdotsEvent) => void;
326
343
  * - `user_change`: `setUserId()` o un `init()` con otro `userId` (login/logout del host).
327
344
  * - `tracking_disabled`: `setTrackingEnabled(false)` (consentimiento revocado).
328
345
  * - `manual`: `endSession()` llamado por el host.
346
+ * - `idle_timeout`: la pestaña vuelve a verse tras más de 30 min oculta (web). La sesión
347
+ * anterior se cierra y se abre otra, como hace GA con su timeout de sesión.
329
348
  */
330
- type SessionEndReason = 'page_hide' | 'background' | 'user_change' | 'tracking_disabled' | 'manual';
349
+ type SessionEndReason = 'page_hide' | 'background' | 'user_change' | 'tracking_disabled' | 'manual' | 'idle_timeout';
331
350
  /** Trigger type used by remote popup definitions */
332
351
  type PopupTriggerType = 'time_on_page' | 'scroll' | 'exit' | 'click' | 'event';
333
352
  type PopupTriggerConditionStatus = 'SHOWED' | 'PARTIAL' | 'COMPLETED';
@@ -700,11 +719,21 @@ declare class DeepdotsPopups {
700
719
  * nueva al volver a foreground o al conceder el consentimiento más tarde.
701
720
  */
702
721
  private sessionOpen;
722
+ /** Último `user_engagement` emitido (ms epoch): marca el latido. */
723
+ private lastEngagementAt;
724
+ /** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */
725
+ private hiddenAt;
703
726
  /**
704
727
  * Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
705
728
  * verdad para el context de analytics Y para la segmentación por idioma de los popups.
706
729
  */
707
730
  private language;
731
+ /** `geolocation` del init: si es false no se usa ni la caché ni el lookup. */
732
+ private geoEnabled;
733
+ /** Solo se consulta a terceros si el país/ciudad se va a enviar (analytics configurado). */
734
+ private geoWanted;
735
+ /** Evita más de un lookup por instancia (el consentimiento puede ir y venir). */
736
+ private geoLookupStarted;
708
737
  /** Initialize the SDK with configuration */
709
738
  init(config: DeepdotsInitParams): void;
710
739
  /** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
@@ -801,12 +830,14 @@ declare class DeepdotsPopups {
801
830
  * se llama en init(), al volver a foreground y al conceder el consentimiento.
802
831
  */
803
832
  private openSession;
833
+ /** Web: si la pestaña se ve. Sin `document` (RN) manda AppState vía onForeground/onBackground. */
834
+ private isDocumentVisible;
804
835
  /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
805
836
  previewAnalytics(): AnalyticsEnvelope;
806
837
  /**
807
838
  * 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.
839
+ * `final: true` (cierre de página/app): el lote no espera al primer POST en vuelo, porque
840
+ * el documento se muere; un lote que falle por red o 5xx se re-encola para el siguiente flush.
810
841
  */
811
842
  flushAnalytics(options?: {
812
843
  final?: boolean;
@@ -814,9 +845,30 @@ declare class DeepdotsPopups {
814
845
  }): void;
815
846
  /** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
816
847
  private flushEngagement;
848
+ /** Tick del flush periódico: latido de engagement si toca, y envío de lo acumulado. */
849
+ private onAnalyticsFlushTick;
850
+ /**
851
+ * La pestaña vuelve a verse. Tras más de ANALYTICS_SESSION_TIMEOUT_MS oculta, la sesión
852
+ * anterior termina y empieza otra: seguir con el mismo sessionId mezclaba dos visitas y,
853
+ * si el backend ya la había cerrado por inactividad, la partía en dos feedbacks.
854
+ * - Oculta menos de ANALYTICS_SESSION_DISCARD_MS: el backend aún la tiene abierta → se
855
+ * cierra con normalidad (page_view de la pantalla, session_end `idle_timeout`, completed).
856
+ * - Más: ya la cerró el backend → se descarta sin enviar nada.
857
+ */
858
+ private onDocumentVisible;
859
+ /** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
860
+ private discardSession;
861
+ /**
862
+ * Geolocalización por IP (país/ciudad para analytics). La caché persistente es local y se
863
+ * aplica siempre que `geolocation` no esté desactivado. El lookup a terceros (ipapi.co →
864
+ * ipwho.is → ipinfo.io, fallback con timeout) solo se hace si el dato se va a ENVIAR:
865
+ * analytics configurado + tracking activo + caché ausente o caducada. Una vez por instancia.
866
+ */
867
+ private resolveGeo;
817
868
  private analyticsIdentity;
818
869
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
819
870
  private setupAnalyticsFlush;
871
+ private startAnalyticsFlushTimer;
820
872
  /** Enable auto-launch functionality with configured triggers */
821
873
  autoLaunch(): void;
822
874
  /** Inicia los triggers configurados */
@@ -896,11 +948,18 @@ declare class DeepdotsPopups {
896
948
  private mapPopupTriggerType;
897
949
  private markSurveyProgress;
898
950
  /**
899
- * Rehidrata el estado de redisplay desde el storage. `answeredSurveys` se reconstruye
900
- * desde las entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
951
+ * Fusiona el estado de redisplay del storage con el de memoria: por cada popup/survey gana
952
+ * la entrada MÁS RECIENTE. Fusionar y no sustituir es lo que hace convivir a varias
953
+ * pestañas sobre el mismo `localStorage`: se llama en el `init()`, antes de cada decisión
954
+ * de mostrar y antes de cada escritura. `answeredSurveys` se reconstruye desde las
955
+ * entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
901
956
  */
902
957
  private loadPopupState;
903
- /** Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria. */
958
+ /**
959
+ * Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria.
960
+ * Fusiona antes de escribir: escribir solo la foto en memoria borraba lo que otra pestaña
961
+ * hubiese guardado desde nuestro `init()`.
962
+ */
904
963
  private persistPopupState;
905
964
  private isPopupDefinition;
906
965
  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
  /**
@@ -104,6 +105,12 @@ interface AnalyticsFlushMeta {
104
105
  * el `sessionId`, para que el lote siguiente abra un registro nuevo.
105
106
  */
106
107
  sessionEnd?: boolean;
108
+ /**
109
+ * `true` para olvidar la sesión SIN enviar nada (el payload llega vacío): el backend ya la
110
+ * cerró por inactividad. El sink descarta su `sessionId` para que el lote siguiente abra un
111
+ * registro nuevo, igual que tras `sessionEnd`, pero sin POST.
112
+ */
113
+ resetSession?: boolean;
107
114
  }
108
115
  /**
109
116
  * Envío del lote. Si devuelve una promesa que **rechaza**, el manager re-encola los
@@ -174,9 +181,11 @@ interface FeedbackSinkOptions {
174
181
  /** fetch inyectable (tests / RN). */
175
182
  fetchImpl?: typeof fetch;
176
183
  /**
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`.
184
+ * @deprecated Se ignora. El flush final salía por `sendBeacon`, que va SIEMPRE con
185
+ * credenciales; contra la API (`Access-Control-Allow-Origin: *` + `Allow-Credentials:
186
+ * true`) el preflight fallaba y el lote de cierre, el único con `completed:true`, no
187
+ * llegaba nunca. Ahora sale por `fetch` con `keepalive`. Se mantiene en el tipo para no
188
+ * romper a quien lo pase.
180
189
  */
181
190
  sendBeaconImpl?: (url: string, body: Blob | string) => boolean;
182
191
  /** Callback invocado cuando el backend devuelve un nuevo sessionId (primer POST). */
@@ -194,8 +203,9 @@ interface FeedbackSinkOptions {
194
203
  * Garantías de entrega:
195
204
  * - mientras no se conozca el `sessionId`, los lotes se **serializan** (esperan la primera
196
205
  * 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;
206
+ * - `keepalive` para que el navegador no cancele el POST al navegar fuera ni al cerrar la
207
+ * página, también en el flush final (sin `sendBeacon`: ver `sendBeaconImpl`);
208
+ * - sin credenciales, que es lo único que pasa el CORS de la API (`Allow-Origin: *`);
199
209
  * - la promesa **rechaza** en fallo transitorio (red, 5xx, 408, 429) para que el
200
210
  * `AnalyticsManager` re-encole el lote; un 4xx se loguea y se descarta.
201
211
  */
@@ -250,6 +260,13 @@ interface DeepdotsInitParams {
250
260
  * to the configured integration; if missing, it stays in dry-run mode (console.log only).
251
261
  */
252
262
  analytics?: AnalyticsKeys;
263
+ /**
264
+ * IP geolocation (country/city) added to analytics. Default `true`. The lookup calls
265
+ * third-party services (ipapi.co, then ipwho.is, then ipinfo.io as fallbacks, 3 s timeout
266
+ * each) and only runs when `analytics` is configured, tracking is enabled and the 30-day
267
+ * cache is missing or expired. Set `false` to never call them nor attach country/city.
268
+ */
269
+ geolocation?: boolean;
253
270
  /**
254
271
  * Internal user attributes known only by the host (language, age, plan, ...), sent
255
272
  * to the backend Contact for segmentation/targeting. Requires `userId`. Equivalent to calling
@@ -326,8 +343,10 @@ type EventListener = (event: DeepdotsEvent) => void;
326
343
  * - `user_change`: `setUserId()` o un `init()` con otro `userId` (login/logout del host).
327
344
  * - `tracking_disabled`: `setTrackingEnabled(false)` (consentimiento revocado).
328
345
  * - `manual`: `endSession()` llamado por el host.
346
+ * - `idle_timeout`: la pestaña vuelve a verse tras más de 30 min oculta (web). La sesión
347
+ * anterior se cierra y se abre otra, como hace GA con su timeout de sesión.
329
348
  */
330
- type SessionEndReason = 'page_hide' | 'background' | 'user_change' | 'tracking_disabled' | 'manual';
349
+ type SessionEndReason = 'page_hide' | 'background' | 'user_change' | 'tracking_disabled' | 'manual' | 'idle_timeout';
331
350
  /** Trigger type used by remote popup definitions */
332
351
  type PopupTriggerType = 'time_on_page' | 'scroll' | 'exit' | 'click' | 'event';
333
352
  type PopupTriggerConditionStatus = 'SHOWED' | 'PARTIAL' | 'COMPLETED';
@@ -700,11 +719,21 @@ declare class DeepdotsPopups {
700
719
  * nueva al volver a foreground o al conceder el consentimiento más tarde.
701
720
  */
702
721
  private sessionOpen;
722
+ /** Último `user_engagement` emitido (ms epoch): marca el latido. */
723
+ private lastEngagementAt;
724
+ /** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */
725
+ private hiddenAt;
703
726
  /**
704
727
  * Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
705
728
  * verdad para el context de analytics Y para la segmentación por idioma de los popups.
706
729
  */
707
730
  private language;
731
+ /** `geolocation` del init: si es false no se usa ni la caché ni el lookup. */
732
+ private geoEnabled;
733
+ /** Solo se consulta a terceros si el país/ciudad se va a enviar (analytics configurado). */
734
+ private geoWanted;
735
+ /** Evita más de un lookup por instancia (el consentimiento puede ir y venir). */
736
+ private geoLookupStarted;
708
737
  /** Initialize the SDK with configuration */
709
738
  init(config: DeepdotsInitParams): void;
710
739
  /** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
@@ -801,12 +830,14 @@ declare class DeepdotsPopups {
801
830
  * se llama en init(), al volver a foreground y al conceder el consentimiento.
802
831
  */
803
832
  private openSession;
833
+ /** Web: si la pestaña se ve. Sin `document` (RN) manda AppState vía onForeground/onBackground. */
834
+ private isDocumentVisible;
804
835
  /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
805
836
  previewAnalytics(): AnalyticsEnvelope;
806
837
  /**
807
838
  * 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.
839
+ * `final: true` (cierre de página/app): el lote no espera al primer POST en vuelo, porque
840
+ * el documento se muere; un lote que falle por red o 5xx se re-encola para el siguiente flush.
810
841
  */
811
842
  flushAnalytics(options?: {
812
843
  final?: boolean;
@@ -814,9 +845,30 @@ declare class DeepdotsPopups {
814
845
  }): void;
815
846
  /** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
816
847
  private flushEngagement;
848
+ /** Tick del flush periódico: latido de engagement si toca, y envío de lo acumulado. */
849
+ private onAnalyticsFlushTick;
850
+ /**
851
+ * La pestaña vuelve a verse. Tras más de ANALYTICS_SESSION_TIMEOUT_MS oculta, la sesión
852
+ * anterior termina y empieza otra: seguir con el mismo sessionId mezclaba dos visitas y,
853
+ * si el backend ya la había cerrado por inactividad, la partía en dos feedbacks.
854
+ * - Oculta menos de ANALYTICS_SESSION_DISCARD_MS: el backend aún la tiene abierta → se
855
+ * cierra con normalidad (page_view de la pantalla, session_end `idle_timeout`, completed).
856
+ * - Más: ya la cerró el backend → se descarta sin enviar nada.
857
+ */
858
+ private onDocumentVisible;
859
+ /** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
860
+ private discardSession;
861
+ /**
862
+ * Geolocalización por IP (país/ciudad para analytics). La caché persistente es local y se
863
+ * aplica siempre que `geolocation` no esté desactivado. El lookup a terceros (ipapi.co →
864
+ * ipwho.is → ipinfo.io, fallback con timeout) solo se hace si el dato se va a ENVIAR:
865
+ * analytics configurado + tracking activo + caché ausente o caducada. Una vez por instancia.
866
+ */
867
+ private resolveGeo;
817
868
  private analyticsIdentity;
818
869
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
819
870
  private setupAnalyticsFlush;
871
+ private startAnalyticsFlushTimer;
820
872
  /** Enable auto-launch functionality with configured triggers */
821
873
  autoLaunch(): void;
822
874
  /** Inicia los triggers configurados */
@@ -896,11 +948,18 @@ declare class DeepdotsPopups {
896
948
  private mapPopupTriggerType;
897
949
  private markSurveyProgress;
898
950
  /**
899
- * Rehidrata el estado de redisplay desde el storage. `answeredSurveys` se reconstruye
900
- * desde las entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
951
+ * Fusiona el estado de redisplay del storage con el de memoria: por cada popup/survey gana
952
+ * la entrada MÁS RECIENTE. Fusionar y no sustituir es lo que hace convivir a varias
953
+ * pestañas sobre el mismo `localStorage`: se llama en el `init()`, antes de cada decisión
954
+ * de mostrar y antes de cada escritura. `answeredSurveys` se reconstruye desde las
955
+ * entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
901
956
  */
902
957
  private loadPopupState;
903
- /** Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria. */
958
+ /**
959
+ * Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria.
960
+ * Fusiona antes de escribir: escribir solo la foto en memoria borraba lo que otra pestaña
961
+ * hubiese guardado desde nuestro `init()`.
962
+ */
904
963
  private persistPopupState;
905
964
  private isPopupDefinition;
906
965
  private isPopupTrigger;