@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 +66 -24
- package/dist/index.d.ts +66 -24
- package/dist/index.js +192 -82
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +192 -82
- package/dist/index.mjs.map +1 -1
- package/dist/react-native.d.mts +21 -5
- package/dist/react-native.d.ts +21 -5
- package/dist/react-native.js +192 -82
- package/dist/react-native.js.map +1 -1
- package/dist/react-native.mjs +192 -82
- package/dist/react-native.mjs.map +1 -1
- package/package.json +1 -1
package/dist/react-native.d.mts
CHANGED
|
@@ -87,9 +87,9 @@ interface AnalyticsEnvelope {
|
|
|
87
87
|
*
|
|
88
88
|
* La analítica se envía como un Feedback del modelo del Surveys SDK, agrupado por una
|
|
89
89
|
* INTEGRACIÓN creada en la plataforma. Se manda en streaming con `completed:false`; el
|
|
90
|
-
* backend
|
|
91
|
-
* app a background, cambio de usuario) va con
|
|
92
|
-
*
|
|
90
|
+
* backend agrupa los lotes por `sessionId`, que genera el SDK al abrir cada sesión. El
|
|
91
|
+
* ÚLTIMO lote de una sesión (cierre de página, app a background, cambio de usuario) va con
|
|
92
|
+
* `completed:true`: cierra el registro y el lote siguiente abre otro con un sessionId nuevo.
|
|
93
93
|
* `feedback.finished` se queda siempre en `false` (no es la señal de cierre acordada).
|
|
94
94
|
*
|
|
95
95
|
* Encoding:
|
|
@@ -445,6 +445,8 @@ declare class DeepdotsPopups {
|
|
|
445
445
|
private analyticsFlushTimer;
|
|
446
446
|
/** Crash & error reporting (#14–17). Null hasta init(). */
|
|
447
447
|
private crashReporter;
|
|
448
|
+
/** ErrorUtils de RN recibido del host (o del Provider); se engancha en cuanto haya consentimiento. */
|
|
449
|
+
private rnErrorUtils;
|
|
448
450
|
/** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */
|
|
449
451
|
private messageGuard;
|
|
450
452
|
/** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
|
|
@@ -513,8 +515,18 @@ declare class DeepdotsPopups {
|
|
|
513
515
|
setMetric(key: string, value: string | number | boolean): void;
|
|
514
516
|
/** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
|
|
515
517
|
reportError(error: unknown, options?: ReportErrorOptions): void;
|
|
516
|
-
/**
|
|
518
|
+
/**
|
|
519
|
+
* Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. Con tracking off
|
|
520
|
+
* se recuerda y se engancha cuando se conceda el consentimiento (`setTrackingEnabled(true)`).
|
|
521
|
+
*/
|
|
517
522
|
installReactNativeCrashHandler(errorUtils: ReactNativeErrorUtils): void;
|
|
523
|
+
/**
|
|
524
|
+
* Pone los handlers de errores no capturados: `window` en web y `ErrorUtils` en RN (el que
|
|
525
|
+
* pasó el host o, si no pasó ninguno, `globalThis.ErrorUtils`, para que un `init()` sin
|
|
526
|
+
* `setupReactNative` también capture). Solo con consentimiento; idempotente, así que se
|
|
527
|
+
* llama en `init()` y cada vez que se activa el tracking.
|
|
528
|
+
*/
|
|
529
|
+
private installCrashHandlers;
|
|
518
530
|
/**
|
|
519
531
|
* Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
|
|
520
532
|
* Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
|
|
@@ -737,7 +749,11 @@ declare class DeepdotsPopups {
|
|
|
737
749
|
private setDeferredExitQueue;
|
|
738
750
|
/** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */
|
|
739
751
|
private fetchPopupsFromServer;
|
|
740
|
-
/**
|
|
752
|
+
/**
|
|
753
|
+
* Envía los atributos de contact a la API (`POST /sdk/popups/contact`). Rechaza si no se
|
|
754
|
+
* pudo enviar (sin red o respuesta no-2xx), para que `ContactManager` no guarde la caché
|
|
755
|
+
* y la llamada siguiente reintente.
|
|
756
|
+
*/
|
|
741
757
|
private postContact;
|
|
742
758
|
/** Notifica eventos de popup a la API */
|
|
743
759
|
private postPopupEvent;
|
package/dist/react-native.d.ts
CHANGED
|
@@ -87,9 +87,9 @@ interface AnalyticsEnvelope {
|
|
|
87
87
|
*
|
|
88
88
|
* La analítica se envía como un Feedback del modelo del Surveys SDK, agrupado por una
|
|
89
89
|
* INTEGRACIÓN creada en la plataforma. Se manda en streaming con `completed:false`; el
|
|
90
|
-
* backend
|
|
91
|
-
* app a background, cambio de usuario) va con
|
|
92
|
-
*
|
|
90
|
+
* backend agrupa los lotes por `sessionId`, que genera el SDK al abrir cada sesión. El
|
|
91
|
+
* ÚLTIMO lote de una sesión (cierre de página, app a background, cambio de usuario) va con
|
|
92
|
+
* `completed:true`: cierra el registro y el lote siguiente abre otro con un sessionId nuevo.
|
|
93
93
|
* `feedback.finished` se queda siempre en `false` (no es la señal de cierre acordada).
|
|
94
94
|
*
|
|
95
95
|
* Encoding:
|
|
@@ -445,6 +445,8 @@ declare class DeepdotsPopups {
|
|
|
445
445
|
private analyticsFlushTimer;
|
|
446
446
|
/** Crash & error reporting (#14–17). Null hasta init(). */
|
|
447
447
|
private crashReporter;
|
|
448
|
+
/** ErrorUtils de RN recibido del host (o del Provider); se engancha en cuanto haya consentimiento. */
|
|
449
|
+
private rnErrorUtils;
|
|
448
450
|
/** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */
|
|
449
451
|
private messageGuard;
|
|
450
452
|
/** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
|
|
@@ -513,8 +515,18 @@ declare class DeepdotsPopups {
|
|
|
513
515
|
setMetric(key: string, value: string | number | boolean): void;
|
|
514
516
|
/** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
|
|
515
517
|
reportError(error: unknown, options?: ReportErrorOptions): void;
|
|
516
|
-
/**
|
|
518
|
+
/**
|
|
519
|
+
* Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. Con tracking off
|
|
520
|
+
* se recuerda y se engancha cuando se conceda el consentimiento (`setTrackingEnabled(true)`).
|
|
521
|
+
*/
|
|
517
522
|
installReactNativeCrashHandler(errorUtils: ReactNativeErrorUtils): void;
|
|
523
|
+
/**
|
|
524
|
+
* Pone los handlers de errores no capturados: `window` en web y `ErrorUtils` en RN (el que
|
|
525
|
+
* pasó el host o, si no pasó ninguno, `globalThis.ErrorUtils`, para que un `init()` sin
|
|
526
|
+
* `setupReactNative` también capture). Solo con consentimiento; idempotente, así que se
|
|
527
|
+
* llama en `init()` y cada vez que se activa el tracking.
|
|
528
|
+
*/
|
|
529
|
+
private installCrashHandlers;
|
|
518
530
|
/**
|
|
519
531
|
* Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
|
|
520
532
|
* Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
|
|
@@ -737,7 +749,11 @@ declare class DeepdotsPopups {
|
|
|
737
749
|
private setDeferredExitQueue;
|
|
738
750
|
/** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */
|
|
739
751
|
private fetchPopupsFromServer;
|
|
740
|
-
/**
|
|
752
|
+
/**
|
|
753
|
+
* Envía los atributos de contact a la API (`POST /sdk/popups/contact`). Rechaza si no se
|
|
754
|
+
* pudo enviar (sin red o respuesta no-2xx), para que `ContactManager` no guarde la caché
|
|
755
|
+
* y la llamada siguiente reintente.
|
|
756
|
+
*/
|
|
741
757
|
private postContact;
|
|
742
758
|
/** Notifica eventos de popup a la API */
|
|
743
759
|
private postPopupEvent;
|
package/dist/react-native.js
CHANGED
|
@@ -4851,97 +4851,166 @@ async function safeBodyText(res) {
|
|
|
4851
4851
|
return "";
|
|
4852
4852
|
}
|
|
4853
4853
|
}
|
|
4854
|
+
var PENDING_MAX = 50;
|
|
4855
|
+
var PENDING_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
4856
|
+
var POST_TIMEOUT_MS = 2e4;
|
|
4857
|
+
function pendingKey(integration) {
|
|
4858
|
+
return `__deepdots_analytics_pending__:${integration}`;
|
|
4859
|
+
}
|
|
4860
|
+
function generateSessionId() {
|
|
4861
|
+
const c = globalThis.crypto;
|
|
4862
|
+
if (typeof c?.randomUUID === "function") return c.randomUUID();
|
|
4863
|
+
const b = new Uint8Array(16);
|
|
4864
|
+
if (typeof c?.getRandomValues === "function") c.getRandomValues(b);
|
|
4865
|
+
else for (let i = 0; i < 16; i++) b[i] = Math.floor(Math.random() * 256);
|
|
4866
|
+
b[6] = b[6] & 15 | 64;
|
|
4867
|
+
b[8] = b[8] & 63 | 128;
|
|
4868
|
+
const h = Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
4869
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
4870
|
+
}
|
|
4854
4871
|
function createFeedbackSink(options) {
|
|
4855
|
-
let feedbackSessionId = options.initialSessionId || void 0;
|
|
4856
|
-
let firstPostInFlight = null;
|
|
4857
|
-
let generation = 0;
|
|
4858
4872
|
const url = `${options.baseUrl}/sdk/feedback`;
|
|
4859
|
-
const
|
|
4860
|
-
|
|
4873
|
+
const newSessionId = options.newSessionId ?? generateSessionId;
|
|
4874
|
+
const now = options.now ?? (() => Date.now());
|
|
4875
|
+
const storageKey = pendingKey(options.keys.integration);
|
|
4876
|
+
let feedbackSessionId = options.initialSessionId || void 0;
|
|
4877
|
+
let announcedId = feedbackSessionId;
|
|
4878
|
+
const pending2 = loadPending();
|
|
4879
|
+
let draining = null;
|
|
4880
|
+
function loadPending() {
|
|
4881
|
+
if (!options.storage) return [];
|
|
4882
|
+
try {
|
|
4883
|
+
const raw = options.storage.getItem(storageKey);
|
|
4884
|
+
const list = raw ? JSON.parse(raw) : [];
|
|
4885
|
+
return Array.isArray(list) ? list.filter((p) => p?.body && now() - p.at <= PENDING_MAX_AGE_MS) : [];
|
|
4886
|
+
} catch {
|
|
4887
|
+
return [];
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
4890
|
+
function savePending() {
|
|
4891
|
+
if (!options.storage) return;
|
|
4892
|
+
try {
|
|
4893
|
+
if (pending2.length) options.storage.setItem(storageKey, JSON.stringify(pending2));
|
|
4894
|
+
else options.storage.removeItem(storageKey);
|
|
4895
|
+
} catch {
|
|
4896
|
+
}
|
|
4897
|
+
}
|
|
4898
|
+
function addPending(batch) {
|
|
4899
|
+
pending2.push(batch);
|
|
4900
|
+
if (pending2.length > PENDING_MAX) pending2.splice(0, pending2.length - PENDING_MAX);
|
|
4901
|
+
savePending();
|
|
4902
|
+
}
|
|
4903
|
+
function removePending(batch) {
|
|
4904
|
+
const i = pending2.indexOf(batch);
|
|
4905
|
+
if (i >= 0) pending2.splice(i, 1);
|
|
4906
|
+
savePending();
|
|
4907
|
+
}
|
|
4908
|
+
async function post(body, final) {
|
|
4861
4909
|
const json = JSON.stringify(body);
|
|
4862
4910
|
const small = json.length <= KEEPALIVE_MAX_BYTES;
|
|
4863
4911
|
if (final) options.log?.("[DeepdotsAnalytics] final flush via fetch keepalive");
|
|
4864
4912
|
const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
4865
4913
|
if (!f) {
|
|
4866
4914
|
options.log?.("[DeepdotsAnalytics] no fetch available; payload not sent", body);
|
|
4867
|
-
return;
|
|
4915
|
+
return "drop";
|
|
4868
4916
|
}
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4917
|
+
let timer;
|
|
4918
|
+
try {
|
|
4919
|
+
const request = f(url, {
|
|
4920
|
+
method: "POST",
|
|
4921
|
+
headers: { "Content-Type": "application/json" },
|
|
4922
|
+
body: json,
|
|
4923
|
+
// La API autentica por `publicKey` en el body y responde `Allow-Origin: *`: una
|
|
4924
|
+
// petición con credenciales fallaría el CORS. Explícito para no depender del default.
|
|
4925
|
+
credentials: "omit",
|
|
4926
|
+
// keepalive: el POST sobrevive a la navegación mientras el body sea pequeño.
|
|
4927
|
+
...small ? { keepalive: true } : {}
|
|
4928
|
+
});
|
|
4929
|
+
const timeout = new Promise((_, reject) => {
|
|
4930
|
+
timer = setTimeout(() => reject(new Error("timeout")), POST_TIMEOUT_MS);
|
|
4931
|
+
});
|
|
4932
|
+
const res = await Promise.race([request, timeout]);
|
|
4933
|
+
if (res.ok) return "ok";
|
|
4880
4934
|
const detail = await safeBodyText(res);
|
|
4881
4935
|
if (isRetryableStatus(res.status)) {
|
|
4882
|
-
|
|
4936
|
+
options.log?.(`[DeepdotsAnalytics] POST /sdk/feedback ${res.status} (retryable): ${detail}`);
|
|
4937
|
+
return "retry";
|
|
4883
4938
|
}
|
|
4884
4939
|
options.log?.(
|
|
4885
4940
|
`[DeepdotsAnalytics] POST /sdk/feedback rejected with ${res.status}; batch DISCARDED:`,
|
|
4886
4941
|
detail
|
|
4887
4942
|
);
|
|
4888
|
-
return;
|
|
4889
|
-
}
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
closedId = data?.sessionId ?? closedId;
|
|
4895
|
-
} catch {
|
|
4896
|
-
}
|
|
4897
|
-
try {
|
|
4898
|
-
if (closedId) options.onSessionClosed?.(closedId);
|
|
4899
|
-
} catch {
|
|
4900
|
-
}
|
|
4901
|
-
return;
|
|
4943
|
+
return "drop";
|
|
4944
|
+
} catch (error) {
|
|
4945
|
+
options.log?.("[DeepdotsAnalytics] POST /sdk/feedback failed (will retry):", error);
|
|
4946
|
+
return "retry";
|
|
4947
|
+
} finally {
|
|
4948
|
+
clearTimeout(timer);
|
|
4902
4949
|
}
|
|
4950
|
+
}
|
|
4951
|
+
function delivered(body) {
|
|
4952
|
+
const id = body.sessionId;
|
|
4953
|
+
if (!id) return;
|
|
4903
4954
|
try {
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
feedbackSessionId = data.sessionId;
|
|
4911
|
-
options.log?.("[DeepdotsAnalytics] feedbackSessionId cached:", feedbackSessionId);
|
|
4912
|
-
options.onSessionId?.(feedbackSessionId);
|
|
4955
|
+
if (body.completed) {
|
|
4956
|
+
options.onSessionClosed?.(id);
|
|
4957
|
+
} else if (id === feedbackSessionId && id !== announcedId) {
|
|
4958
|
+
announcedId = id;
|
|
4959
|
+
options.log?.("[DeepdotsAnalytics] feedback session open:", id);
|
|
4960
|
+
options.onSessionId?.(id);
|
|
4913
4961
|
}
|
|
4914
4962
|
} catch {
|
|
4915
4963
|
}
|
|
4916
|
-
}
|
|
4964
|
+
}
|
|
4965
|
+
async function send(batch, final, alreadyPending) {
|
|
4966
|
+
const result = await post(batch.body, final);
|
|
4967
|
+
if (result === "retry") {
|
|
4968
|
+
if (!alreadyPending) addPending(batch);
|
|
4969
|
+
return;
|
|
4970
|
+
}
|
|
4971
|
+
if (alreadyPending) removePending(batch);
|
|
4972
|
+
if (result === "ok") delivered(batch.body);
|
|
4973
|
+
}
|
|
4974
|
+
function drain() {
|
|
4975
|
+
const run = async () => {
|
|
4976
|
+
for (let i = 0; i < pending2.length; ) {
|
|
4977
|
+
const batch = pending2[i];
|
|
4978
|
+
await send(batch, false, true);
|
|
4979
|
+
if (pending2[i] === batch) return;
|
|
4980
|
+
}
|
|
4981
|
+
};
|
|
4982
|
+
const next = draining ? draining.then(run) : run();
|
|
4983
|
+
draining = next;
|
|
4984
|
+
void next.finally(() => {
|
|
4985
|
+
if (draining === next) draining = null;
|
|
4986
|
+
});
|
|
4987
|
+
return next;
|
|
4988
|
+
}
|
|
4989
|
+
if (pending2.length) {
|
|
4990
|
+
savePending();
|
|
4991
|
+
setTimeout(() => void drain(), 0);
|
|
4992
|
+
}
|
|
4917
4993
|
return async (envelope, meta) => {
|
|
4918
4994
|
if (meta?.resetSession) {
|
|
4919
4995
|
feedbackSessionId = void 0;
|
|
4920
|
-
firstPostInFlight = null;
|
|
4921
|
-
generation++;
|
|
4922
4996
|
options.onSessionReset?.();
|
|
4923
4997
|
return;
|
|
4924
4998
|
}
|
|
4925
4999
|
const final = meta?.final === true;
|
|
4926
5000
|
const sessionEnd = meta?.sessionEnd === true;
|
|
4927
|
-
if (!feedbackSessionId
|
|
4928
|
-
await firstPostInFlight;
|
|
4929
|
-
}
|
|
5001
|
+
if (!feedbackSessionId) feedbackSessionId = newSessionId();
|
|
4930
5002
|
const body = buildAnalyticsFeedbackBody(envelope, options.keys, feedbackSessionId, { sessionEnd });
|
|
4931
5003
|
options.log?.("[DeepdotsAnalytics] POST /sdk/feedback \u2192", body);
|
|
4932
|
-
const
|
|
5004
|
+
const batch = { body, at: now() };
|
|
4933
5005
|
if (sessionEnd) {
|
|
4934
5006
|
feedbackSessionId = void 0;
|
|
4935
|
-
firstPostInFlight = null;
|
|
4936
|
-
generation++;
|
|
4937
5007
|
options.onSessionReset?.();
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
() => void 0
|
|
4942
|
-
);
|
|
5008
|
+
addPending(batch);
|
|
5009
|
+
await drain();
|
|
5010
|
+
return;
|
|
4943
5011
|
}
|
|
4944
|
-
|
|
5012
|
+
const retries = pending2.length ? drain() : null;
|
|
5013
|
+
await Promise.all([retries, send(batch, final, false)]);
|
|
4945
5014
|
};
|
|
4946
5015
|
}
|
|
4947
5016
|
|
|
@@ -5299,12 +5368,12 @@ var EngagementTracker = class {
|
|
|
5299
5368
|
|
|
5300
5369
|
// src/contact/contact-manager.ts
|
|
5301
5370
|
init_tracking_manager();
|
|
5302
|
-
function stableSerialize(attrs) {
|
|
5371
|
+
function stableSerialize(userId, attrs) {
|
|
5303
5372
|
const sorted = {};
|
|
5304
5373
|
for (const key of Object.keys(attrs).sort()) {
|
|
5305
5374
|
sorted[key] = attrs[key];
|
|
5306
5375
|
}
|
|
5307
|
-
return JSON.stringify(sorted);
|
|
5376
|
+
return JSON.stringify({ userId, attributes: sorted });
|
|
5308
5377
|
}
|
|
5309
5378
|
var ContactManager = class {
|
|
5310
5379
|
constructor(options) {
|
|
@@ -5314,18 +5383,23 @@ var ContactManager = class {
|
|
|
5314
5383
|
this.post = options.post;
|
|
5315
5384
|
}
|
|
5316
5385
|
/**
|
|
5317
|
-
* Envía los atributos si cambiaron respecto a lo último enviado.
|
|
5318
|
-
*
|
|
5386
|
+
* Envía los atributos si cambiaron respecto a lo último enviado. La caché solo se guarda
|
|
5387
|
+
* cuando la API acepta el envío; si falla, la llamada siguiente reintenta. Nunca rechaza.
|
|
5388
|
+
* @returns `true` si se envió, `false` si no hubo cambios o el envío falló.
|
|
5319
5389
|
*/
|
|
5320
5390
|
async setAttributes(attributes) {
|
|
5321
|
-
const serialized = stableSerialize(attributes);
|
|
5391
|
+
const serialized = stableSerialize(this.userId, attributes);
|
|
5322
5392
|
const last = this.storage.getItem(STORAGE_KEYS.contactAttributes);
|
|
5323
5393
|
if (last === serialized) return false;
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5394
|
+
try {
|
|
5395
|
+
await this.post({
|
|
5396
|
+
publicKey: this.publicKey,
|
|
5397
|
+
userId: this.userId,
|
|
5398
|
+
userAttributes: attributes
|
|
5399
|
+
});
|
|
5400
|
+
} catch {
|
|
5401
|
+
return false;
|
|
5402
|
+
}
|
|
5329
5403
|
this.storage.setItem(STORAGE_KEYS.contactAttributes, serialized);
|
|
5330
5404
|
return true;
|
|
5331
5405
|
}
|
|
@@ -5356,6 +5430,10 @@ function crashRecordToParams(r) {
|
|
|
5356
5430
|
}
|
|
5357
5431
|
var CrashReporter = class {
|
|
5358
5432
|
constructor(options) {
|
|
5433
|
+
/** Los handlers de `window` ya están puestos: `install()` es idempotente. */
|
|
5434
|
+
this.windowInstalled = false;
|
|
5435
|
+
/** ErrorUtils ya enganchados: engancharlos dos veces capturaría cada error dos veces. */
|
|
5436
|
+
this.hookedErrorUtils = /* @__PURE__ */ new Set();
|
|
5359
5437
|
this.options = options;
|
|
5360
5438
|
this.now = options.now ?? (() => Date.now());
|
|
5361
5439
|
}
|
|
@@ -5431,9 +5509,11 @@ var CrashReporter = class {
|
|
|
5431
5509
|
}
|
|
5432
5510
|
return queue;
|
|
5433
5511
|
}
|
|
5434
|
-
/** Instala los handlers globales de errores no capturados (no-op sin `window`). */
|
|
5512
|
+
/** Instala los handlers globales de errores no capturados (no-op sin `window`). Idempotente. */
|
|
5435
5513
|
install() {
|
|
5514
|
+
if (this.windowInstalled) return;
|
|
5436
5515
|
if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
|
|
5516
|
+
this.windowInstalled = true;
|
|
5437
5517
|
window.addEventListener("error", (e) => {
|
|
5438
5518
|
if (!this.isEnabled()) return;
|
|
5439
5519
|
this.captureUnhandled(e.error ?? e.message ?? "error");
|
|
@@ -5443,8 +5523,10 @@ var CrashReporter = class {
|
|
|
5443
5523
|
this.captureUnhandled(e.reason ?? "unhandledrejection");
|
|
5444
5524
|
});
|
|
5445
5525
|
}
|
|
5446
|
-
/** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. */
|
|
5526
|
+
/** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. Idempotente por instancia de ErrorUtils. */
|
|
5447
5527
|
installReactNative(errorUtils) {
|
|
5528
|
+
if (this.hookedErrorUtils.has(errorUtils)) return;
|
|
5529
|
+
this.hookedErrorUtils.add(errorUtils);
|
|
5448
5530
|
const previous = errorUtils.getGlobalHandler?.();
|
|
5449
5531
|
errorUtils.setGlobalHandler((error, isFatal) => {
|
|
5450
5532
|
if (this.isEnabled()) this.captureUnhandled(error);
|
|
@@ -5660,6 +5742,8 @@ var DeepdotsPopups = class {
|
|
|
5660
5742
|
this.analyticsFlushTimer = void 0;
|
|
5661
5743
|
/** Crash & error reporting (#14–17). Null hasta init(). */
|
|
5662
5744
|
this.crashReporter = null;
|
|
5745
|
+
/** ErrorUtils de RN recibido del host (o del Provider); se engancha en cuanto haya consentimiento. */
|
|
5746
|
+
this.rnErrorUtils = null;
|
|
5663
5747
|
/** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */
|
|
5664
5748
|
this.messageGuard = new MessageGuard();
|
|
5665
5749
|
/** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
|
|
@@ -5760,6 +5844,10 @@ var DeepdotsPopups = class {
|
|
|
5760
5844
|
keys: config.analytics,
|
|
5761
5845
|
log: (...a) => this.log(...a),
|
|
5762
5846
|
initialSessionId: resumable?.sessionId,
|
|
5847
|
+
// En móvil (RN) la app puede morir con el lote de cierre sin entregar: se
|
|
5848
|
+
// persiste y se reenvía en el arranque siguiente. En web el cierre sale con
|
|
5849
|
+
// `keepalive` y varias pestañas compartirían el mismo storage.
|
|
5850
|
+
storage: (config.platform ?? "web") !== "web" ? storage : void 0,
|
|
5763
5851
|
onSessionId: (id) => {
|
|
5764
5852
|
this.analyticsFeedbackSessionId = id;
|
|
5765
5853
|
this.notifyFeedbackSession(id, "open");
|
|
@@ -5795,9 +5883,7 @@ var DeepdotsPopups = class {
|
|
|
5795
5883
|
now: () => Date.now(),
|
|
5796
5884
|
enabled: () => this.tracking?.isTrackingEnabled() ?? false
|
|
5797
5885
|
});
|
|
5798
|
-
|
|
5799
|
-
this.crashReporter.install();
|
|
5800
|
-
}
|
|
5886
|
+
this.installCrashHandlers();
|
|
5801
5887
|
this.geoEnabled = config.geolocation ?? true;
|
|
5802
5888
|
this.geoWanted = this.geoEnabled && !!config.analytics;
|
|
5803
5889
|
this.resolveGeo();
|
|
@@ -5866,6 +5952,7 @@ var DeepdotsPopups = class {
|
|
|
5866
5952
|
if (!enabled) this.closeSession("tracking_disabled");
|
|
5867
5953
|
this.tracking?.setTrackingEnabled(enabled);
|
|
5868
5954
|
if (enabled) this.openSession("tracking_enabled");
|
|
5955
|
+
if (enabled) this.installCrashHandlers();
|
|
5869
5956
|
if (enabled) this.resolveGeo();
|
|
5870
5957
|
this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
|
|
5871
5958
|
}
|
|
@@ -5896,10 +5983,27 @@ var DeepdotsPopups = class {
|
|
|
5896
5983
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
5897
5984
|
this.crashReporter?.reportError(error, options);
|
|
5898
5985
|
}
|
|
5899
|
-
/**
|
|
5986
|
+
/**
|
|
5987
|
+
* Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. Con tracking off
|
|
5988
|
+
* se recuerda y se engancha cuando se conceda el consentimiento (`setTrackingEnabled(true)`).
|
|
5989
|
+
*/
|
|
5900
5990
|
installReactNativeCrashHandler(errorUtils) {
|
|
5901
|
-
|
|
5902
|
-
this.
|
|
5991
|
+
this.rnErrorUtils = errorUtils;
|
|
5992
|
+
this.installCrashHandlers();
|
|
5993
|
+
}
|
|
5994
|
+
/**
|
|
5995
|
+
* Pone los handlers de errores no capturados: `window` en web y `ErrorUtils` en RN (el que
|
|
5996
|
+
* pasó el host o, si no pasó ninguno, `globalThis.ErrorUtils`, para que un `init()` sin
|
|
5997
|
+
* `setupReactNative` también capture). Solo con consentimiento; idempotente, así que se
|
|
5998
|
+
* llama en `init()` y cada vez que se activa el tracking.
|
|
5999
|
+
*/
|
|
6000
|
+
installCrashHandlers() {
|
|
6001
|
+
if (!this.crashReporter || !this.tracking?.isTrackingEnabled()) return;
|
|
6002
|
+
this.crashReporter.install();
|
|
6003
|
+
const errorUtils = this.rnErrorUtils ?? globalThis.ErrorUtils;
|
|
6004
|
+
if (errorUtils && typeof errorUtils.setGlobalHandler === "function") {
|
|
6005
|
+
this.crashReporter.installReactNative(errorUtils);
|
|
6006
|
+
}
|
|
5903
6007
|
}
|
|
5904
6008
|
/**
|
|
5905
6009
|
* Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
|
|
@@ -6722,25 +6826,31 @@ var DeepdotsPopups = class {
|
|
|
6722
6826
|
return null;
|
|
6723
6827
|
}
|
|
6724
6828
|
}
|
|
6725
|
-
/**
|
|
6829
|
+
/**
|
|
6830
|
+
* Envía los atributos de contact a la API (`POST /sdk/popups/contact`). Rechaza si no se
|
|
6831
|
+
* pudo enviar (sin red o respuesta no-2xx), para que `ContactManager` no guarde la caché
|
|
6832
|
+
* y la llamada siguiente reintente.
|
|
6833
|
+
*/
|
|
6726
6834
|
async postContact(body) {
|
|
6727
6835
|
const baseUrl = this.baseUrl;
|
|
6728
|
-
if (!baseUrl)
|
|
6836
|
+
if (!baseUrl) throw new Error("no base URL");
|
|
6729
6837
|
const endpoint = `${baseUrl}/sdk/popups/contact`;
|
|
6838
|
+
let response;
|
|
6730
6839
|
try {
|
|
6731
|
-
|
|
6840
|
+
response = await fetch(endpoint, {
|
|
6732
6841
|
method: "POST",
|
|
6733
6842
|
headers: { "Content-Type": "application/json" },
|
|
6734
6843
|
body: JSON.stringify(body)
|
|
6735
6844
|
});
|
|
6736
|
-
if (!response.ok) {
|
|
6737
|
-
this.log("Failed to post contact", response.status, response.statusText);
|
|
6738
|
-
return;
|
|
6739
|
-
}
|
|
6740
|
-
this.log("contact \xB7 sent attributes for user:", body.userId);
|
|
6741
6845
|
} catch (error) {
|
|
6742
6846
|
this.log("Error posting contact", error);
|
|
6847
|
+
throw error;
|
|
6848
|
+
}
|
|
6849
|
+
if (!response.ok) {
|
|
6850
|
+
this.log("Failed to post contact", response.status, response.statusText);
|
|
6851
|
+
throw new Error(`HTTP ${response.status}`);
|
|
6743
6852
|
}
|
|
6853
|
+
this.log("contact \xB7 sent attributes for user:", body.userId);
|
|
6744
6854
|
}
|
|
6745
6855
|
/** Notifica eventos de popup a la API */
|
|
6746
6856
|
async postPopupEvent(status, popupId, userId) {
|