@magicfeedback/popup-sdk 1.16.1 → 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 +53 -20
- package/dist/index.d.ts +53 -20
- package/dist/index.js +163 -64
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +163 -64
- package/dist/index.mjs.map +1 -1
- package/dist/react-native.d.mts +16 -4
- package/dist/react-native.d.ts +16 -4
- package/dist/react-native.js +163 -64
- package/dist/react-native.js.map +1 -1
- package/dist/react-native.mjs +163 -64
- package/dist/react-native.mjs.map +1 -1
- package/package.json +1 -1
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
|
|
128
|
-
* app a background, cambio de usuario) va con
|
|
129
|
-
*
|
|
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
|
|
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
|
|
170
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* registro
|
|
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
|
-
* -
|
|
216
|
-
*
|
|
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
|
|
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
|
-
* -
|
|
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
|
|
|
@@ -657,6 +674,10 @@ declare function crashRecordToParams(r: CrashRecord): Record<string, unknown>;
|
|
|
657
674
|
declare class CrashReporter {
|
|
658
675
|
private options;
|
|
659
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;
|
|
660
681
|
constructor(options: CrashReporterOptions);
|
|
661
682
|
private isEnabled;
|
|
662
683
|
/** Construye un CrashRecord capturando contexto en el momento del crash. */
|
|
@@ -669,9 +690,9 @@ declare class CrashReporter {
|
|
|
669
690
|
private persist;
|
|
670
691
|
/** Lee y vacía la cola de crashes pendientes (llamado en init para el replay). */
|
|
671
692
|
drainPendingCrashes(): CrashRecord[];
|
|
672
|
-
/** 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. */
|
|
673
694
|
install(): void;
|
|
674
|
-
/** 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. */
|
|
675
696
|
installReactNative(errorUtils: ReactNativeErrorUtils): void;
|
|
676
697
|
/** @internal Test seam: ejercita el camino de persistencia (window handlers no son testeables en vitest). */
|
|
677
698
|
_persistForTest(error: unknown): void;
|
|
@@ -784,6 +805,8 @@ declare class DeepdotsPopups {
|
|
|
784
805
|
private analyticsFlushTimer;
|
|
785
806
|
/** Crash & error reporting (#14–17). Null hasta init(). */
|
|
786
807
|
private crashReporter;
|
|
808
|
+
/** ErrorUtils de RN recibido del host (o del Provider); se engancha en cuanto haya consentimiento. */
|
|
809
|
+
private rnErrorUtils;
|
|
787
810
|
/** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */
|
|
788
811
|
private messageGuard;
|
|
789
812
|
/** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
|
|
@@ -852,8 +875,18 @@ declare class DeepdotsPopups {
|
|
|
852
875
|
setMetric(key: string, value: string | number | boolean): void;
|
|
853
876
|
/** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
|
|
854
877
|
reportError(error: unknown, options?: ReportErrorOptions): void;
|
|
855
|
-
/**
|
|
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
|
+
*/
|
|
856
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;
|
|
857
890
|
/**
|
|
858
891
|
* Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
|
|
859
892
|
* Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
|
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
|
|
128
|
-
* app a background, cambio de usuario) va con
|
|
129
|
-
*
|
|
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
|
|
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
|
|
170
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* registro
|
|
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
|
-
* -
|
|
216
|
-
*
|
|
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
|
|
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
|
-
* -
|
|
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
|
|
|
@@ -657,6 +674,10 @@ declare function crashRecordToParams(r: CrashRecord): Record<string, unknown>;
|
|
|
657
674
|
declare class CrashReporter {
|
|
658
675
|
private options;
|
|
659
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;
|
|
660
681
|
constructor(options: CrashReporterOptions);
|
|
661
682
|
private isEnabled;
|
|
662
683
|
/** Construye un CrashRecord capturando contexto en el momento del crash. */
|
|
@@ -669,9 +690,9 @@ declare class CrashReporter {
|
|
|
669
690
|
private persist;
|
|
670
691
|
/** Lee y vacía la cola de crashes pendientes (llamado en init para el replay). */
|
|
671
692
|
drainPendingCrashes(): CrashRecord[];
|
|
672
|
-
/** 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. */
|
|
673
694
|
install(): void;
|
|
674
|
-
/** 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. */
|
|
675
696
|
installReactNative(errorUtils: ReactNativeErrorUtils): void;
|
|
676
697
|
/** @internal Test seam: ejercita el camino de persistencia (window handlers no son testeables en vitest). */
|
|
677
698
|
_persistForTest(error: unknown): void;
|
|
@@ -784,6 +805,8 @@ declare class DeepdotsPopups {
|
|
|
784
805
|
private analyticsFlushTimer;
|
|
785
806
|
/** Crash & error reporting (#14–17). Null hasta init(). */
|
|
786
807
|
private crashReporter;
|
|
808
|
+
/** ErrorUtils de RN recibido del host (o del Provider); se engancha en cuanto haya consentimiento. */
|
|
809
|
+
private rnErrorUtils;
|
|
787
810
|
/** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */
|
|
788
811
|
private messageGuard;
|
|
789
812
|
/** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
|
|
@@ -852,8 +875,18 @@ declare class DeepdotsPopups {
|
|
|
852
875
|
setMetric(key: string, value: string | number | boolean): void;
|
|
853
876
|
/** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
|
|
854
877
|
reportError(error: unknown, options?: ReportErrorOptions): void;
|
|
855
|
-
/**
|
|
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
|
+
*/
|
|
856
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;
|
|
857
890
|
/**
|
|
858
891
|
* Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
|
|
859
892
|
* Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
|
package/dist/index.js
CHANGED
|
@@ -7706,97 +7706,166 @@ async function safeBodyText(res) {
|
|
|
7706
7706
|
return "";
|
|
7707
7707
|
}
|
|
7708
7708
|
}
|
|
7709
|
+
var PENDING_MAX = 50;
|
|
7710
|
+
var PENDING_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
7711
|
+
var POST_TIMEOUT_MS = 2e4;
|
|
7712
|
+
function pendingKey(integration) {
|
|
7713
|
+
return `__deepdots_analytics_pending__:${integration}`;
|
|
7714
|
+
}
|
|
7715
|
+
function generateSessionId() {
|
|
7716
|
+
const c = globalThis.crypto;
|
|
7717
|
+
if (typeof c?.randomUUID === "function") return c.randomUUID();
|
|
7718
|
+
const b = new Uint8Array(16);
|
|
7719
|
+
if (typeof c?.getRandomValues === "function") c.getRandomValues(b);
|
|
7720
|
+
else for (let i = 0; i < 16; i++) b[i] = Math.floor(Math.random() * 256);
|
|
7721
|
+
b[6] = b[6] & 15 | 64;
|
|
7722
|
+
b[8] = b[8] & 63 | 128;
|
|
7723
|
+
const h = Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
7724
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
7725
|
+
}
|
|
7709
7726
|
function createFeedbackSink(options) {
|
|
7710
|
-
let feedbackSessionId = options.initialSessionId || void 0;
|
|
7711
|
-
let firstPostInFlight = null;
|
|
7712
|
-
let generation = 0;
|
|
7713
7727
|
const url = `${options.baseUrl}/sdk/feedback`;
|
|
7714
|
-
const
|
|
7715
|
-
|
|
7728
|
+
const newSessionId = options.newSessionId ?? generateSessionId;
|
|
7729
|
+
const now = options.now ?? (() => Date.now());
|
|
7730
|
+
const storageKey = pendingKey(options.keys.integration);
|
|
7731
|
+
let feedbackSessionId = options.initialSessionId || void 0;
|
|
7732
|
+
let announcedId = feedbackSessionId;
|
|
7733
|
+
const pending2 = loadPending();
|
|
7734
|
+
let draining = null;
|
|
7735
|
+
function loadPending() {
|
|
7736
|
+
if (!options.storage) return [];
|
|
7737
|
+
try {
|
|
7738
|
+
const raw = options.storage.getItem(storageKey);
|
|
7739
|
+
const list = raw ? JSON.parse(raw) : [];
|
|
7740
|
+
return Array.isArray(list) ? list.filter((p) => p?.body && now() - p.at <= PENDING_MAX_AGE_MS) : [];
|
|
7741
|
+
} catch {
|
|
7742
|
+
return [];
|
|
7743
|
+
}
|
|
7744
|
+
}
|
|
7745
|
+
function savePending() {
|
|
7746
|
+
if (!options.storage) return;
|
|
7747
|
+
try {
|
|
7748
|
+
if (pending2.length) options.storage.setItem(storageKey, JSON.stringify(pending2));
|
|
7749
|
+
else options.storage.removeItem(storageKey);
|
|
7750
|
+
} catch {
|
|
7751
|
+
}
|
|
7752
|
+
}
|
|
7753
|
+
function addPending(batch) {
|
|
7754
|
+
pending2.push(batch);
|
|
7755
|
+
if (pending2.length > PENDING_MAX) pending2.splice(0, pending2.length - PENDING_MAX);
|
|
7756
|
+
savePending();
|
|
7757
|
+
}
|
|
7758
|
+
function removePending(batch) {
|
|
7759
|
+
const i = pending2.indexOf(batch);
|
|
7760
|
+
if (i >= 0) pending2.splice(i, 1);
|
|
7761
|
+
savePending();
|
|
7762
|
+
}
|
|
7763
|
+
async function post(body, final) {
|
|
7716
7764
|
const json = JSON.stringify(body);
|
|
7717
7765
|
const small = json.length <= KEEPALIVE_MAX_BYTES;
|
|
7718
7766
|
if (final) options.log?.("[DeepdotsAnalytics] final flush via fetch keepalive");
|
|
7719
7767
|
const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
7720
7768
|
if (!f) {
|
|
7721
7769
|
options.log?.("[DeepdotsAnalytics] no fetch available; payload not sent", body);
|
|
7722
|
-
return;
|
|
7770
|
+
return "drop";
|
|
7723
7771
|
}
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7772
|
+
let timer;
|
|
7773
|
+
try {
|
|
7774
|
+
const request = f(url, {
|
|
7775
|
+
method: "POST",
|
|
7776
|
+
headers: { "Content-Type": "application/json" },
|
|
7777
|
+
body: json,
|
|
7778
|
+
// La API autentica por `publicKey` en el body y responde `Allow-Origin: *`: una
|
|
7779
|
+
// petición con credenciales fallaría el CORS. Explícito para no depender del default.
|
|
7780
|
+
credentials: "omit",
|
|
7781
|
+
// keepalive: el POST sobrevive a la navegación mientras el body sea pequeño.
|
|
7782
|
+
...small ? { keepalive: true } : {}
|
|
7783
|
+
});
|
|
7784
|
+
const timeout = new Promise((_, reject) => {
|
|
7785
|
+
timer = setTimeout(() => reject(new Error("timeout")), POST_TIMEOUT_MS);
|
|
7786
|
+
});
|
|
7787
|
+
const res = await Promise.race([request, timeout]);
|
|
7788
|
+
if (res.ok) return "ok";
|
|
7735
7789
|
const detail = await safeBodyText(res);
|
|
7736
7790
|
if (isRetryableStatus(res.status)) {
|
|
7737
|
-
|
|
7791
|
+
options.log?.(`[DeepdotsAnalytics] POST /sdk/feedback ${res.status} (retryable): ${detail}`);
|
|
7792
|
+
return "retry";
|
|
7738
7793
|
}
|
|
7739
7794
|
options.log?.(
|
|
7740
7795
|
`[DeepdotsAnalytics] POST /sdk/feedback rejected with ${res.status}; batch DISCARDED:`,
|
|
7741
7796
|
detail
|
|
7742
7797
|
);
|
|
7743
|
-
return;
|
|
7744
|
-
}
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
closedId = data?.sessionId ?? closedId;
|
|
7750
|
-
} catch {
|
|
7751
|
-
}
|
|
7752
|
-
try {
|
|
7753
|
-
if (closedId) options.onSessionClosed?.(closedId);
|
|
7754
|
-
} catch {
|
|
7755
|
-
}
|
|
7756
|
-
return;
|
|
7798
|
+
return "drop";
|
|
7799
|
+
} catch (error) {
|
|
7800
|
+
options.log?.("[DeepdotsAnalytics] POST /sdk/feedback failed (will retry):", error);
|
|
7801
|
+
return "retry";
|
|
7802
|
+
} finally {
|
|
7803
|
+
clearTimeout(timer);
|
|
7757
7804
|
}
|
|
7805
|
+
}
|
|
7806
|
+
function delivered(body) {
|
|
7807
|
+
const id = body.sessionId;
|
|
7808
|
+
if (!id) return;
|
|
7758
7809
|
try {
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
feedbackSessionId = data.sessionId;
|
|
7766
|
-
options.log?.("[DeepdotsAnalytics] feedbackSessionId cached:", feedbackSessionId);
|
|
7767
|
-
options.onSessionId?.(feedbackSessionId);
|
|
7810
|
+
if (body.completed) {
|
|
7811
|
+
options.onSessionClosed?.(id);
|
|
7812
|
+
} else if (id === feedbackSessionId && id !== announcedId) {
|
|
7813
|
+
announcedId = id;
|
|
7814
|
+
options.log?.("[DeepdotsAnalytics] feedback session open:", id);
|
|
7815
|
+
options.onSessionId?.(id);
|
|
7768
7816
|
}
|
|
7769
7817
|
} catch {
|
|
7770
7818
|
}
|
|
7771
|
-
}
|
|
7819
|
+
}
|
|
7820
|
+
async function send(batch, final, alreadyPending) {
|
|
7821
|
+
const result = await post(batch.body, final);
|
|
7822
|
+
if (result === "retry") {
|
|
7823
|
+
if (!alreadyPending) addPending(batch);
|
|
7824
|
+
return;
|
|
7825
|
+
}
|
|
7826
|
+
if (alreadyPending) removePending(batch);
|
|
7827
|
+
if (result === "ok") delivered(batch.body);
|
|
7828
|
+
}
|
|
7829
|
+
function drain() {
|
|
7830
|
+
const run = async () => {
|
|
7831
|
+
for (let i = 0; i < pending2.length; ) {
|
|
7832
|
+
const batch = pending2[i];
|
|
7833
|
+
await send(batch, false, true);
|
|
7834
|
+
if (pending2[i] === batch) return;
|
|
7835
|
+
}
|
|
7836
|
+
};
|
|
7837
|
+
const next = draining ? draining.then(run) : run();
|
|
7838
|
+
draining = next;
|
|
7839
|
+
void next.finally(() => {
|
|
7840
|
+
if (draining === next) draining = null;
|
|
7841
|
+
});
|
|
7842
|
+
return next;
|
|
7843
|
+
}
|
|
7844
|
+
if (pending2.length) {
|
|
7845
|
+
savePending();
|
|
7846
|
+
setTimeout(() => void drain(), 0);
|
|
7847
|
+
}
|
|
7772
7848
|
return async (envelope, meta) => {
|
|
7773
7849
|
if (meta?.resetSession) {
|
|
7774
7850
|
feedbackSessionId = void 0;
|
|
7775
|
-
firstPostInFlight = null;
|
|
7776
|
-
generation++;
|
|
7777
7851
|
options.onSessionReset?.();
|
|
7778
7852
|
return;
|
|
7779
7853
|
}
|
|
7780
7854
|
const final = meta?.final === true;
|
|
7781
7855
|
const sessionEnd = meta?.sessionEnd === true;
|
|
7782
|
-
if (!feedbackSessionId
|
|
7783
|
-
await firstPostInFlight;
|
|
7784
|
-
}
|
|
7856
|
+
if (!feedbackSessionId) feedbackSessionId = newSessionId();
|
|
7785
7857
|
const body = buildAnalyticsFeedbackBody(envelope, options.keys, feedbackSessionId, { sessionEnd });
|
|
7786
7858
|
options.log?.("[DeepdotsAnalytics] POST /sdk/feedback \u2192", body);
|
|
7787
|
-
const
|
|
7859
|
+
const batch = { body, at: now() };
|
|
7788
7860
|
if (sessionEnd) {
|
|
7789
7861
|
feedbackSessionId = void 0;
|
|
7790
|
-
firstPostInFlight = null;
|
|
7791
|
-
generation++;
|
|
7792
7862
|
options.onSessionReset?.();
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
() => void 0
|
|
7797
|
-
);
|
|
7863
|
+
addPending(batch);
|
|
7864
|
+
await drain();
|
|
7865
|
+
return;
|
|
7798
7866
|
}
|
|
7799
|
-
|
|
7867
|
+
const retries = pending2.length ? drain() : null;
|
|
7868
|
+
await Promise.all([retries, send(batch, final, false)]);
|
|
7800
7869
|
};
|
|
7801
7870
|
}
|
|
7802
7871
|
|
|
@@ -8216,6 +8285,10 @@ function crashRecordToParams(r) {
|
|
|
8216
8285
|
}
|
|
8217
8286
|
var CrashReporter = class {
|
|
8218
8287
|
constructor(options) {
|
|
8288
|
+
/** Los handlers de `window` ya están puestos: `install()` es idempotente. */
|
|
8289
|
+
this.windowInstalled = false;
|
|
8290
|
+
/** ErrorUtils ya enganchados: engancharlos dos veces capturaría cada error dos veces. */
|
|
8291
|
+
this.hookedErrorUtils = /* @__PURE__ */ new Set();
|
|
8219
8292
|
this.options = options;
|
|
8220
8293
|
this.now = options.now ?? (() => Date.now());
|
|
8221
8294
|
}
|
|
@@ -8291,9 +8364,11 @@ var CrashReporter = class {
|
|
|
8291
8364
|
}
|
|
8292
8365
|
return queue;
|
|
8293
8366
|
}
|
|
8294
|
-
/** Instala los handlers globales de errores no capturados (no-op sin `window`). */
|
|
8367
|
+
/** Instala los handlers globales de errores no capturados (no-op sin `window`). Idempotente. */
|
|
8295
8368
|
install() {
|
|
8369
|
+
if (this.windowInstalled) return;
|
|
8296
8370
|
if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
|
|
8371
|
+
this.windowInstalled = true;
|
|
8297
8372
|
window.addEventListener("error", (e) => {
|
|
8298
8373
|
if (!this.isEnabled()) return;
|
|
8299
8374
|
this.captureUnhandled(e.error ?? e.message ?? "error");
|
|
@@ -8303,8 +8378,10 @@ var CrashReporter = class {
|
|
|
8303
8378
|
this.captureUnhandled(e.reason ?? "unhandledrejection");
|
|
8304
8379
|
});
|
|
8305
8380
|
}
|
|
8306
|
-
/** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. */
|
|
8381
|
+
/** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. Idempotente por instancia de ErrorUtils. */
|
|
8307
8382
|
installReactNative(errorUtils) {
|
|
8383
|
+
if (this.hookedErrorUtils.has(errorUtils)) return;
|
|
8384
|
+
this.hookedErrorUtils.add(errorUtils);
|
|
8308
8385
|
const previous = errorUtils.getGlobalHandler?.();
|
|
8309
8386
|
errorUtils.setGlobalHandler((error, isFatal) => {
|
|
8310
8387
|
if (this.isEnabled()) this.captureUnhandled(error);
|
|
@@ -8520,6 +8597,8 @@ var DeepdotsPopups = class {
|
|
|
8520
8597
|
this.analyticsFlushTimer = void 0;
|
|
8521
8598
|
/** Crash & error reporting (#14–17). Null hasta init(). */
|
|
8522
8599
|
this.crashReporter = null;
|
|
8600
|
+
/** ErrorUtils de RN recibido del host (o del Provider); se engancha en cuanto haya consentimiento. */
|
|
8601
|
+
this.rnErrorUtils = null;
|
|
8523
8602
|
/** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */
|
|
8524
8603
|
this.messageGuard = new MessageGuard();
|
|
8525
8604
|
/** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
|
|
@@ -8620,6 +8699,10 @@ var DeepdotsPopups = class {
|
|
|
8620
8699
|
keys: config.analytics,
|
|
8621
8700
|
log: (...a) => this.log(...a),
|
|
8622
8701
|
initialSessionId: resumable?.sessionId,
|
|
8702
|
+
// En móvil (RN) la app puede morir con el lote de cierre sin entregar: se
|
|
8703
|
+
// persiste y se reenvía en el arranque siguiente. En web el cierre sale con
|
|
8704
|
+
// `keepalive` y varias pestañas compartirían el mismo storage.
|
|
8705
|
+
storage: (config.platform ?? "web") !== "web" ? storage : void 0,
|
|
8623
8706
|
onSessionId: (id) => {
|
|
8624
8707
|
this.analyticsFeedbackSessionId = id;
|
|
8625
8708
|
this.notifyFeedbackSession(id, "open");
|
|
@@ -8655,9 +8738,7 @@ var DeepdotsPopups = class {
|
|
|
8655
8738
|
now: () => Date.now(),
|
|
8656
8739
|
enabled: () => this.tracking?.isTrackingEnabled() ?? false
|
|
8657
8740
|
});
|
|
8658
|
-
|
|
8659
|
-
this.crashReporter.install();
|
|
8660
|
-
}
|
|
8741
|
+
this.installCrashHandlers();
|
|
8661
8742
|
this.geoEnabled = config.geolocation ?? true;
|
|
8662
8743
|
this.geoWanted = this.geoEnabled && !!config.analytics;
|
|
8663
8744
|
this.resolveGeo();
|
|
@@ -8726,6 +8807,7 @@ var DeepdotsPopups = class {
|
|
|
8726
8807
|
if (!enabled) this.closeSession("tracking_disabled");
|
|
8727
8808
|
this.tracking?.setTrackingEnabled(enabled);
|
|
8728
8809
|
if (enabled) this.openSession("tracking_enabled");
|
|
8810
|
+
if (enabled) this.installCrashHandlers();
|
|
8729
8811
|
if (enabled) this.resolveGeo();
|
|
8730
8812
|
this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
|
|
8731
8813
|
}
|
|
@@ -8756,10 +8838,27 @@ var DeepdotsPopups = class {
|
|
|
8756
8838
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
8757
8839
|
this.crashReporter?.reportError(error, options);
|
|
8758
8840
|
}
|
|
8759
|
-
/**
|
|
8841
|
+
/**
|
|
8842
|
+
* Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. Con tracking off
|
|
8843
|
+
* se recuerda y se engancha cuando se conceda el consentimiento (`setTrackingEnabled(true)`).
|
|
8844
|
+
*/
|
|
8760
8845
|
installReactNativeCrashHandler(errorUtils) {
|
|
8761
|
-
|
|
8762
|
-
this.
|
|
8846
|
+
this.rnErrorUtils = errorUtils;
|
|
8847
|
+
this.installCrashHandlers();
|
|
8848
|
+
}
|
|
8849
|
+
/**
|
|
8850
|
+
* Pone los handlers de errores no capturados: `window` en web y `ErrorUtils` en RN (el que
|
|
8851
|
+
* pasó el host o, si no pasó ninguno, `globalThis.ErrorUtils`, para que un `init()` sin
|
|
8852
|
+
* `setupReactNative` también capture). Solo con consentimiento; idempotente, así que se
|
|
8853
|
+
* llama en `init()` y cada vez que se activa el tracking.
|
|
8854
|
+
*/
|
|
8855
|
+
installCrashHandlers() {
|
|
8856
|
+
if (!this.crashReporter || !this.tracking?.isTrackingEnabled()) return;
|
|
8857
|
+
this.crashReporter.install();
|
|
8858
|
+
const errorUtils = this.rnErrorUtils ?? globalThis.ErrorUtils;
|
|
8859
|
+
if (errorUtils && typeof errorUtils.setGlobalHandler === "function") {
|
|
8860
|
+
this.crashReporter.installReactNative(errorUtils);
|
|
8861
|
+
}
|
|
8763
8862
|
}
|
|
8764
8863
|
/**
|
|
8765
8864
|
* Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
|