@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.
package/dist/index.d.mts CHANGED
@@ -124,9 +124,9 @@ type AnalyticsSink = (payload: AnalyticsEnvelope, meta?: AnalyticsFlushMeta) =>
124
124
  *
125
125
  * La analítica se envía como un Feedback del modelo del Surveys SDK, agrupado por una
126
126
  * INTEGRACIÓN creada en la plataforma. Se manda en streaming con `completed:false`; el
127
- * backend cose por `sessionId` + `user_id`. El ÚLTIMO lote de una sesión (cierre de página,
128
- * app a background, cambio de usuario) va con `completed:true`: cierra el registro y el
129
- * siguiente lote omite el `sessionId` viejo para que el backend abra uno nuevo.
127
+ * backend agrupa los lotes por `sessionId`, que genera el SDK al abrir cada sesión. El
128
+ * ÚLTIMO lote de una sesión (cierre de página, app a background, cambio de usuario) va con
129
+ * `completed:true`: cierra el registro y el lote siguiente abre otro con un sessionId nuevo.
130
130
  * `feedback.finished` se queda siempre en `false` (no es la señal de cierre acordada).
131
131
  *
132
132
  * Encoding:
@@ -160,14 +160,14 @@ interface AnalyticsFeedbackBody {
160
160
  integration: string;
161
161
  completed: boolean;
162
162
  finished?: boolean;
163
- /** sessionId devuelto por el primer POST; permite que el backend agrupe todos los eventos en un solo registro. */
163
+ /** sessionId de la sesión (lo genera el SDK): el backend agrupa todos sus lotes en un solo registro. */
164
164
  sessionId?: string;
165
165
  }
166
166
  interface BuildBodyOptions {
167
167
  /**
168
168
  * `true` en el último lote de la sesión → `completed:true` (cierra el registro en backend).
169
- * El body SÍ lleva el `sessionId` actual (es el registro que se cierra); es el lote
170
- * SIGUIENTE el que lo omite.
169
+ * El body lleva el `sessionId` de la sesión que se cierra; el lote siguiente abre otro
170
+ * registro con un sessionId nuevo.
171
171
  */
172
172
  sessionEnd?: boolean;
173
173
  }
@@ -193,32 +193,49 @@ interface FeedbackSinkOptions {
193
193
  * `pagehide` y reanudada tras recargar la página (ver `suspended-session.ts`).
194
194
  */
195
195
  initialSessionId?: string;
196
- /** Callback invocado cuando el backend devuelve un nuevo sessionId (primer POST). */
196
+ /**
197
+ * Genera el sessionId de cada sesión (default: UUID v4). Inyectable para tests.
198
+ */
199
+ newSessionId?: () => string;
200
+ /**
201
+ * Persistencia de los lotes pendientes (React Native: MMKV). Con él, el lote de cierre se
202
+ * guarda antes de enviarse y cualquier lote que falle sobrevive a que la app muera: el sink
203
+ * del arranque siguiente lo reenvía a su registro. Sin él, los pendientes viven en memoria.
204
+ */
205
+ storage?: KeyValueStorage;
206
+ /** Reloj inyectable (tests). */
207
+ now?: () => number;
208
+ /** Callback invocado cuando la API acepta el primer lote de una sesión (registro abierto). */
197
209
  onSessionId?: (id: string) => void;
198
210
  /** Callback invocado al cerrar la sesión (`completed:true`): el sessionId cacheado se olvida. */
199
211
  onSessionReset?: () => void;
200
212
  /**
201
213
  * Callback invocado cuando el backend acepta el lote de cierre, con el sessionId del
202
- * registro cerrado: el que llevaba el lote o, en una sesión de un solo lote, el que
203
- * devuelve la respuesta. Ese registro se convierte en Feedback en segundos.
214
+ * registro cerrado. Ese registro se convierte en Feedback en segundos.
204
215
  */
205
216
  onSessionClosed?: (id: string) => void;
206
217
  }
207
218
  /**
208
219
  * Sink real: transforma el envelope y hace `POST {baseUrl}/sdk/feedback`.
209
220
  *
210
- * Stateful: cachea el `sessionId` que devuelve el backend en la primera respuesta y lo
211
- * envía en los siguientes POSTs para que el backend agrupe todos los eventos en un único
212
- * registro de feedback.
221
+ * El `sessionId` de cada sesión lo genera el SDK (UUID) y va en TODOS sus lotes, desde el
222
+ * primero. La API guarda cada lote bajo el sessionId que recibe, así que una sesión es un
223
+ * registro aunque no llegue ninguna respuesta. Antes lo asignaba la API en la respuesta del
224
+ * primer POST: si esa respuesta se perdía (la app pasaba a background con el POST en vuelo),
225
+ * el resto de la sesión acababa en otro registro.
213
226
  *
214
227
  * Garantías de entrega:
215
- * - mientras no se conozca el `sessionId`, los lotes se **serializan** (esperan la primera
216
- * respuesta): dos POSTs a la vez sin `sessionId` crearían dos registros y partirían los datos;
228
+ * - un lote que falla de forma transitoria (red, 5xx, 408, 429, sin respuesta en
229
+ * POST_TIMEOUT_MS) se guarda TAL CUAL —con su sessionId y su `completed`— y se reenvía
230
+ * antes del lote siguiente. Nunca vuelve al buffer del manager: allí se mezclaba con la
231
+ * sesión siguiente y viajaba con su registro. La promesa del sink no rechaza;
232
+ * - con `storage` (React Native) los pendientes sobreviven a que la app muera: el lote de
233
+ * cierre se guarda ANTES de enviarse y el sink del arranque siguiente lo reenvía. La API
234
+ * descarta los eventos repetidos de una misma sesión al ensamblarla;
217
235
  * - `keepalive` para que el navegador no cancele el POST al navegar fuera ni al cerrar la
218
- * página, también en el flush final (sin `sendBeacon`: ver `sendBeaconImpl`);
236
+ * página (sin `sendBeacon`: ver `sendBeaconImpl`);
219
237
  * - sin credenciales, que es lo único que pasa el CORS de la API (`Allow-Origin: *`);
220
- * - la promesa **rechaza** en fallo transitorio (red, 5xx, 408, 429) para que el
221
- * `AnalyticsManager` re-encole el lote; un 4xx se loguea y se descarta.
238
+ * - un 4xx se loguea y se descarta.
222
239
  */
223
240
  declare function createFeedbackSink(options: FeedbackSinkOptions): AnalyticsSink;
224
241
 
@@ -572,7 +589,11 @@ interface ContactManagerOptions {
572
589
  storage: KeyValueStorage;
573
590
  publicKey: string;
574
591
  userId: string;
575
- /** Envío real (inyectable para tests): hace el `POST /sdk/popups/contact`. */
592
+ /**
593
+ * Envío real (inyectable para tests): hace el `POST /sdk/popups/contact`. Tiene que
594
+ * RECHAZAR si la API no acepta el envío (sin red o respuesta no-2xx): solo así se sabe
595
+ * que no hay que guardar la caché y que la llamada siguiente debe reintentar.
596
+ */
576
597
  post: (body: ContactBody) => Promise<void>;
577
598
  }
578
599
  /**
@@ -587,8 +608,9 @@ declare class ContactManager {
587
608
  private readonly post;
588
609
  constructor(options: ContactManagerOptions);
589
610
  /**
590
- * Envía los atributos si cambiaron respecto a lo último enviado.
591
- * @returns `true` si se envió, `false` si no hubo cambios.
611
+ * Envía los atributos si cambiaron respecto a lo último enviado. La caché solo se guarda
612
+ * cuando la API acepta el envío; si falla, la llamada siguiente reintenta. Nunca rechaza.
613
+ * @returns `true` si se envió, `false` si no hubo cambios o el envío falló.
592
614
  */
593
615
  setAttributes(attributes: ContactAttributes): Promise<boolean>;
594
616
  }
@@ -652,6 +674,10 @@ declare function crashRecordToParams(r: CrashRecord): Record<string, unknown>;
652
674
  declare class CrashReporter {
653
675
  private options;
654
676
  private now;
677
+ /** Los handlers de `window` ya están puestos: `install()` es idempotente. */
678
+ private windowInstalled;
679
+ /** ErrorUtils ya enganchados: engancharlos dos veces capturaría cada error dos veces. */
680
+ private hookedErrorUtils;
655
681
  constructor(options: CrashReporterOptions);
656
682
  private isEnabled;
657
683
  /** Construye un CrashRecord capturando contexto en el momento del crash. */
@@ -664,9 +690,9 @@ declare class CrashReporter {
664
690
  private persist;
665
691
  /** Lee y vacía la cola de crashes pendientes (llamado en init para el replay). */
666
692
  drainPendingCrashes(): CrashRecord[];
667
- /** Instala los handlers globales de errores no capturados (no-op sin `window`). */
693
+ /** Instala los handlers globales de errores no capturados (no-op sin `window`). Idempotente. */
668
694
  install(): void;
669
- /** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. */
695
+ /** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. Idempotente por instancia de ErrorUtils. */
670
696
  installReactNative(errorUtils: ReactNativeErrorUtils): void;
671
697
  /** @internal Test seam: ejercita el camino de persistencia (window handlers no son testeables en vitest). */
672
698
  _persistForTest(error: unknown): void;
@@ -779,6 +805,8 @@ declare class DeepdotsPopups {
779
805
  private analyticsFlushTimer;
780
806
  /** Crash & error reporting (#14–17). Null hasta init(). */
781
807
  private crashReporter;
808
+ /** ErrorUtils de RN recibido del host (o del Provider); se engancha en cuanto haya consentimiento. */
809
+ private rnErrorUtils;
782
810
  /** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */
783
811
  private messageGuard;
784
812
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
@@ -847,8 +875,18 @@ declare class DeepdotsPopups {
847
875
  setMetric(key: string, value: string | number | boolean): void;
848
876
  /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
849
877
  reportError(error: unknown, options?: ReportErrorOptions): void;
850
- /** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
878
+ /**
879
+ * Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. Con tracking off
880
+ * se recuerda y se engancha cuando se conceda el consentimiento (`setTrackingEnabled(true)`).
881
+ */
851
882
  installReactNativeCrashHandler(errorUtils: ReactNativeErrorUtils): void;
883
+ /**
884
+ * Pone los handlers de errores no capturados: `window` en web y `ErrorUtils` en RN (el que
885
+ * pasó el host o, si no pasó ninguno, `globalThis.ErrorUtils`, para que un `init()` sin
886
+ * `setupReactNative` también capture). Solo con consentimiento; idempotente, así que se
887
+ * llama en `init()` y cada vez que se activa el tracking.
888
+ */
889
+ private installCrashHandlers;
852
890
  /**
853
891
  * Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
854
892
  * Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
@@ -1071,7 +1109,11 @@ declare class DeepdotsPopups {
1071
1109
  private setDeferredExitQueue;
1072
1110
  /** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */
1073
1111
  private fetchPopupsFromServer;
1074
- /** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */
1112
+ /**
1113
+ * Envía los atributos de contact a la API (`POST /sdk/popups/contact`). Rechaza si no se
1114
+ * pudo enviar (sin red o respuesta no-2xx), para que `ContactManager` no guarde la caché
1115
+ * y la llamada siguiente reintente.
1116
+ */
1075
1117
  private postContact;
1076
1118
  /** Notifica eventos de popup a la API */
1077
1119
  private postPopupEvent;
package/dist/index.d.ts CHANGED
@@ -124,9 +124,9 @@ type AnalyticsSink = (payload: AnalyticsEnvelope, meta?: AnalyticsFlushMeta) =>
124
124
  *
125
125
  * La analítica se envía como un Feedback del modelo del Surveys SDK, agrupado por una
126
126
  * INTEGRACIÓN creada en la plataforma. Se manda en streaming con `completed:false`; el
127
- * backend cose por `sessionId` + `user_id`. El ÚLTIMO lote de una sesión (cierre de página,
128
- * app a background, cambio de usuario) va con `completed:true`: cierra el registro y el
129
- * siguiente lote omite el `sessionId` viejo para que el backend abra uno nuevo.
127
+ * backend agrupa los lotes por `sessionId`, que genera el SDK al abrir cada sesión. El
128
+ * ÚLTIMO lote de una sesión (cierre de página, app a background, cambio de usuario) va con
129
+ * `completed:true`: cierra el registro y el lote siguiente abre otro con un sessionId nuevo.
130
130
  * `feedback.finished` se queda siempre en `false` (no es la señal de cierre acordada).
131
131
  *
132
132
  * Encoding:
@@ -160,14 +160,14 @@ interface AnalyticsFeedbackBody {
160
160
  integration: string;
161
161
  completed: boolean;
162
162
  finished?: boolean;
163
- /** sessionId devuelto por el primer POST; permite que el backend agrupe todos los eventos en un solo registro. */
163
+ /** sessionId de la sesión (lo genera el SDK): el backend agrupa todos sus lotes en un solo registro. */
164
164
  sessionId?: string;
165
165
  }
166
166
  interface BuildBodyOptions {
167
167
  /**
168
168
  * `true` en el último lote de la sesión → `completed:true` (cierra el registro en backend).
169
- * El body SÍ lleva el `sessionId` actual (es el registro que se cierra); es el lote
170
- * SIGUIENTE el que lo omite.
169
+ * El body lleva el `sessionId` de la sesión que se cierra; el lote siguiente abre otro
170
+ * registro con un sessionId nuevo.
171
171
  */
172
172
  sessionEnd?: boolean;
173
173
  }
@@ -193,32 +193,49 @@ interface FeedbackSinkOptions {
193
193
  * `pagehide` y reanudada tras recargar la página (ver `suspended-session.ts`).
194
194
  */
195
195
  initialSessionId?: string;
196
- /** Callback invocado cuando el backend devuelve un nuevo sessionId (primer POST). */
196
+ /**
197
+ * Genera el sessionId de cada sesión (default: UUID v4). Inyectable para tests.
198
+ */
199
+ newSessionId?: () => string;
200
+ /**
201
+ * Persistencia de los lotes pendientes (React Native: MMKV). Con él, el lote de cierre se
202
+ * guarda antes de enviarse y cualquier lote que falle sobrevive a que la app muera: el sink
203
+ * del arranque siguiente lo reenvía a su registro. Sin él, los pendientes viven en memoria.
204
+ */
205
+ storage?: KeyValueStorage;
206
+ /** Reloj inyectable (tests). */
207
+ now?: () => number;
208
+ /** Callback invocado cuando la API acepta el primer lote de una sesión (registro abierto). */
197
209
  onSessionId?: (id: string) => void;
198
210
  /** Callback invocado al cerrar la sesión (`completed:true`): el sessionId cacheado se olvida. */
199
211
  onSessionReset?: () => void;
200
212
  /**
201
213
  * Callback invocado cuando el backend acepta el lote de cierre, con el sessionId del
202
- * registro cerrado: el que llevaba el lote o, en una sesión de un solo lote, el que
203
- * devuelve la respuesta. Ese registro se convierte en Feedback en segundos.
214
+ * registro cerrado. Ese registro se convierte en Feedback en segundos.
204
215
  */
205
216
  onSessionClosed?: (id: string) => void;
206
217
  }
207
218
  /**
208
219
  * Sink real: transforma el envelope y hace `POST {baseUrl}/sdk/feedback`.
209
220
  *
210
- * Stateful: cachea el `sessionId` que devuelve el backend en la primera respuesta y lo
211
- * envía en los siguientes POSTs para que el backend agrupe todos los eventos en un único
212
- * registro de feedback.
221
+ * El `sessionId` de cada sesión lo genera el SDK (UUID) y va en TODOS sus lotes, desde el
222
+ * primero. La API guarda cada lote bajo el sessionId que recibe, así que una sesión es un
223
+ * registro aunque no llegue ninguna respuesta. Antes lo asignaba la API en la respuesta del
224
+ * primer POST: si esa respuesta se perdía (la app pasaba a background con el POST en vuelo),
225
+ * el resto de la sesión acababa en otro registro.
213
226
  *
214
227
  * Garantías de entrega:
215
- * - mientras no se conozca el `sessionId`, los lotes se **serializan** (esperan la primera
216
- * respuesta): dos POSTs a la vez sin `sessionId` crearían dos registros y partirían los datos;
228
+ * - un lote que falla de forma transitoria (red, 5xx, 408, 429, sin respuesta en
229
+ * POST_TIMEOUT_MS) se guarda TAL CUAL —con su sessionId y su `completed`— y se reenvía
230
+ * antes del lote siguiente. Nunca vuelve al buffer del manager: allí se mezclaba con la
231
+ * sesión siguiente y viajaba con su registro. La promesa del sink no rechaza;
232
+ * - con `storage` (React Native) los pendientes sobreviven a que la app muera: el lote de
233
+ * cierre se guarda ANTES de enviarse y el sink del arranque siguiente lo reenvía. La API
234
+ * descarta los eventos repetidos de una misma sesión al ensamblarla;
217
235
  * - `keepalive` para que el navegador no cancele el POST al navegar fuera ni al cerrar la
218
- * página, también en el flush final (sin `sendBeacon`: ver `sendBeaconImpl`);
236
+ * página (sin `sendBeacon`: ver `sendBeaconImpl`);
219
237
  * - sin credenciales, que es lo único que pasa el CORS de la API (`Allow-Origin: *`);
220
- * - la promesa **rechaza** en fallo transitorio (red, 5xx, 408, 429) para que el
221
- * `AnalyticsManager` re-encole el lote; un 4xx se loguea y se descarta.
238
+ * - un 4xx se loguea y se descarta.
222
239
  */
223
240
  declare function createFeedbackSink(options: FeedbackSinkOptions): AnalyticsSink;
224
241
 
@@ -572,7 +589,11 @@ interface ContactManagerOptions {
572
589
  storage: KeyValueStorage;
573
590
  publicKey: string;
574
591
  userId: string;
575
- /** Envío real (inyectable para tests): hace el `POST /sdk/popups/contact`. */
592
+ /**
593
+ * Envío real (inyectable para tests): hace el `POST /sdk/popups/contact`. Tiene que
594
+ * RECHAZAR si la API no acepta el envío (sin red o respuesta no-2xx): solo así se sabe
595
+ * que no hay que guardar la caché y que la llamada siguiente debe reintentar.
596
+ */
576
597
  post: (body: ContactBody) => Promise<void>;
577
598
  }
578
599
  /**
@@ -587,8 +608,9 @@ declare class ContactManager {
587
608
  private readonly post;
588
609
  constructor(options: ContactManagerOptions);
589
610
  /**
590
- * Envía los atributos si cambiaron respecto a lo último enviado.
591
- * @returns `true` si se envió, `false` si no hubo cambios.
611
+ * Envía los atributos si cambiaron respecto a lo último enviado. La caché solo se guarda
612
+ * cuando la API acepta el envío; si falla, la llamada siguiente reintenta. Nunca rechaza.
613
+ * @returns `true` si se envió, `false` si no hubo cambios o el envío falló.
592
614
  */
593
615
  setAttributes(attributes: ContactAttributes): Promise<boolean>;
594
616
  }
@@ -652,6 +674,10 @@ declare function crashRecordToParams(r: CrashRecord): Record<string, unknown>;
652
674
  declare class CrashReporter {
653
675
  private options;
654
676
  private now;
677
+ /** Los handlers de `window` ya están puestos: `install()` es idempotente. */
678
+ private windowInstalled;
679
+ /** ErrorUtils ya enganchados: engancharlos dos veces capturaría cada error dos veces. */
680
+ private hookedErrorUtils;
655
681
  constructor(options: CrashReporterOptions);
656
682
  private isEnabled;
657
683
  /** Construye un CrashRecord capturando contexto en el momento del crash. */
@@ -664,9 +690,9 @@ declare class CrashReporter {
664
690
  private persist;
665
691
  /** Lee y vacía la cola de crashes pendientes (llamado en init para el replay). */
666
692
  drainPendingCrashes(): CrashRecord[];
667
- /** Instala los handlers globales de errores no capturados (no-op sin `window`). */
693
+ /** Instala los handlers globales de errores no capturados (no-op sin `window`). Idempotente. */
668
694
  install(): void;
669
- /** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. */
695
+ /** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. Idempotente por instancia de ErrorUtils. */
670
696
  installReactNative(errorUtils: ReactNativeErrorUtils): void;
671
697
  /** @internal Test seam: ejercita el camino de persistencia (window handlers no son testeables en vitest). */
672
698
  _persistForTest(error: unknown): void;
@@ -779,6 +805,8 @@ declare class DeepdotsPopups {
779
805
  private analyticsFlushTimer;
780
806
  /** Crash & error reporting (#14–17). Null hasta init(). */
781
807
  private crashReporter;
808
+ /** ErrorUtils de RN recibido del host (o del Provider); se engancha en cuanto haya consentimiento. */
809
+ private rnErrorUtils;
782
810
  /** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */
783
811
  private messageGuard;
784
812
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
@@ -847,8 +875,18 @@ declare class DeepdotsPopups {
847
875
  setMetric(key: string, value: string | number | boolean): void;
848
876
  /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
849
877
  reportError(error: unknown, options?: ReportErrorOptions): void;
850
- /** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
878
+ /**
879
+ * Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. Con tracking off
880
+ * se recuerda y se engancha cuando se conceda el consentimiento (`setTrackingEnabled(true)`).
881
+ */
851
882
  installReactNativeCrashHandler(errorUtils: ReactNativeErrorUtils): void;
883
+ /**
884
+ * Pone los handlers de errores no capturados: `window` en web y `ErrorUtils` en RN (el que
885
+ * pasó el host o, si no pasó ninguno, `globalThis.ErrorUtils`, para que un `init()` sin
886
+ * `setupReactNative` también capture). Solo con consentimiento; idempotente, así que se
887
+ * llama en `init()` y cada vez que se activa el tracking.
888
+ */
889
+ private installCrashHandlers;
852
890
  /**
853
891
  * Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
854
892
  * Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
@@ -1071,7 +1109,11 @@ declare class DeepdotsPopups {
1071
1109
  private setDeferredExitQueue;
1072
1110
  /** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */
1073
1111
  private fetchPopupsFromServer;
1074
- /** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */
1112
+ /**
1113
+ * Envía los atributos de contact a la API (`POST /sdk/popups/contact`). Rechaza si no se
1114
+ * pudo enviar (sin red o respuesta no-2xx), para que `ContactManager` no guarde la caché
1115
+ * y la llamada siguiente reintente.
1116
+ */
1075
1117
  private postContact;
1076
1118
  /** Notifica eventos de popup a la API */
1077
1119
  private postPopupEvent;