@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/react-native.mjs
CHANGED
|
@@ -1279,97 +1279,166 @@ async function safeBodyText(res) {
|
|
|
1279
1279
|
return "";
|
|
1280
1280
|
}
|
|
1281
1281
|
}
|
|
1282
|
+
var PENDING_MAX = 50;
|
|
1283
|
+
var PENDING_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
1284
|
+
var POST_TIMEOUT_MS = 2e4;
|
|
1285
|
+
function pendingKey(integration) {
|
|
1286
|
+
return `__deepdots_analytics_pending__:${integration}`;
|
|
1287
|
+
}
|
|
1288
|
+
function generateSessionId() {
|
|
1289
|
+
const c = globalThis.crypto;
|
|
1290
|
+
if (typeof c?.randomUUID === "function") return c.randomUUID();
|
|
1291
|
+
const b = new Uint8Array(16);
|
|
1292
|
+
if (typeof c?.getRandomValues === "function") c.getRandomValues(b);
|
|
1293
|
+
else for (let i = 0; i < 16; i++) b[i] = Math.floor(Math.random() * 256);
|
|
1294
|
+
b[6] = b[6] & 15 | 64;
|
|
1295
|
+
b[8] = b[8] & 63 | 128;
|
|
1296
|
+
const h = Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
1297
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
1298
|
+
}
|
|
1282
1299
|
function createFeedbackSink(options) {
|
|
1283
|
-
let feedbackSessionId = options.initialSessionId || void 0;
|
|
1284
|
-
let firstPostInFlight = null;
|
|
1285
|
-
let generation = 0;
|
|
1286
1300
|
const url = `${options.baseUrl}/sdk/feedback`;
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1301
|
+
const newSessionId = options.newSessionId ?? generateSessionId;
|
|
1302
|
+
const now = options.now ?? (() => Date.now());
|
|
1303
|
+
const storageKey = pendingKey(options.keys.integration);
|
|
1304
|
+
let feedbackSessionId = options.initialSessionId || void 0;
|
|
1305
|
+
let announcedId = feedbackSessionId;
|
|
1306
|
+
const pending2 = loadPending();
|
|
1307
|
+
let draining = null;
|
|
1308
|
+
function loadPending() {
|
|
1309
|
+
if (!options.storage) return [];
|
|
1310
|
+
try {
|
|
1311
|
+
const raw = options.storage.getItem(storageKey);
|
|
1312
|
+
const list = raw ? JSON.parse(raw) : [];
|
|
1313
|
+
return Array.isArray(list) ? list.filter((p) => p?.body && now() - p.at <= PENDING_MAX_AGE_MS) : [];
|
|
1314
|
+
} catch {
|
|
1315
|
+
return [];
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
function savePending() {
|
|
1319
|
+
if (!options.storage) return;
|
|
1320
|
+
try {
|
|
1321
|
+
if (pending2.length) options.storage.setItem(storageKey, JSON.stringify(pending2));
|
|
1322
|
+
else options.storage.removeItem(storageKey);
|
|
1323
|
+
} catch {
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
function addPending(batch) {
|
|
1327
|
+
pending2.push(batch);
|
|
1328
|
+
if (pending2.length > PENDING_MAX) pending2.splice(0, pending2.length - PENDING_MAX);
|
|
1329
|
+
savePending();
|
|
1330
|
+
}
|
|
1331
|
+
function removePending(batch) {
|
|
1332
|
+
const i = pending2.indexOf(batch);
|
|
1333
|
+
if (i >= 0) pending2.splice(i, 1);
|
|
1334
|
+
savePending();
|
|
1335
|
+
}
|
|
1336
|
+
async function post(body, final) {
|
|
1289
1337
|
const json = JSON.stringify(body);
|
|
1290
1338
|
const small = json.length <= KEEPALIVE_MAX_BYTES;
|
|
1291
1339
|
if (final) options.log?.("[DeepdotsAnalytics] final flush via fetch keepalive");
|
|
1292
1340
|
const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
1293
1341
|
if (!f) {
|
|
1294
1342
|
options.log?.("[DeepdotsAnalytics] no fetch available; payload not sent", body);
|
|
1295
|
-
return;
|
|
1343
|
+
return "drop";
|
|
1296
1344
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1345
|
+
let timer;
|
|
1346
|
+
try {
|
|
1347
|
+
const request = f(url, {
|
|
1348
|
+
method: "POST",
|
|
1349
|
+
headers: { "Content-Type": "application/json" },
|
|
1350
|
+
body: json,
|
|
1351
|
+
// La API autentica por `publicKey` en el body y responde `Allow-Origin: *`: una
|
|
1352
|
+
// petición con credenciales fallaría el CORS. Explícito para no depender del default.
|
|
1353
|
+
credentials: "omit",
|
|
1354
|
+
// keepalive: el POST sobrevive a la navegación mientras el body sea pequeño.
|
|
1355
|
+
...small ? { keepalive: true } : {}
|
|
1356
|
+
});
|
|
1357
|
+
const timeout = new Promise((_, reject) => {
|
|
1358
|
+
timer = setTimeout(() => reject(new Error("timeout")), POST_TIMEOUT_MS);
|
|
1359
|
+
});
|
|
1360
|
+
const res = await Promise.race([request, timeout]);
|
|
1361
|
+
if (res.ok) return "ok";
|
|
1308
1362
|
const detail = await safeBodyText(res);
|
|
1309
1363
|
if (isRetryableStatus(res.status)) {
|
|
1310
|
-
|
|
1364
|
+
options.log?.(`[DeepdotsAnalytics] POST /sdk/feedback ${res.status} (retryable): ${detail}`);
|
|
1365
|
+
return "retry";
|
|
1311
1366
|
}
|
|
1312
1367
|
options.log?.(
|
|
1313
1368
|
`[DeepdotsAnalytics] POST /sdk/feedback rejected with ${res.status}; batch DISCARDED:`,
|
|
1314
1369
|
detail
|
|
1315
1370
|
);
|
|
1316
|
-
return;
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
closedId = data?.sessionId ?? closedId;
|
|
1323
|
-
} catch {
|
|
1324
|
-
}
|
|
1325
|
-
try {
|
|
1326
|
-
if (closedId) options.onSessionClosed?.(closedId);
|
|
1327
|
-
} catch {
|
|
1328
|
-
}
|
|
1329
|
-
return;
|
|
1371
|
+
return "drop";
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
options.log?.("[DeepdotsAnalytics] POST /sdk/feedback failed (will retry):", error);
|
|
1374
|
+
return "retry";
|
|
1375
|
+
} finally {
|
|
1376
|
+
clearTimeout(timer);
|
|
1330
1377
|
}
|
|
1378
|
+
}
|
|
1379
|
+
function delivered(body) {
|
|
1380
|
+
const id = body.sessionId;
|
|
1381
|
+
if (!id) return;
|
|
1331
1382
|
try {
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
feedbackSessionId = data.sessionId;
|
|
1339
|
-
options.log?.("[DeepdotsAnalytics] feedbackSessionId cached:", feedbackSessionId);
|
|
1340
|
-
options.onSessionId?.(feedbackSessionId);
|
|
1383
|
+
if (body.completed) {
|
|
1384
|
+
options.onSessionClosed?.(id);
|
|
1385
|
+
} else if (id === feedbackSessionId && id !== announcedId) {
|
|
1386
|
+
announcedId = id;
|
|
1387
|
+
options.log?.("[DeepdotsAnalytics] feedback session open:", id);
|
|
1388
|
+
options.onSessionId?.(id);
|
|
1341
1389
|
}
|
|
1342
1390
|
} catch {
|
|
1343
1391
|
}
|
|
1344
|
-
}
|
|
1392
|
+
}
|
|
1393
|
+
async function send(batch, final, alreadyPending) {
|
|
1394
|
+
const result = await post(batch.body, final);
|
|
1395
|
+
if (result === "retry") {
|
|
1396
|
+
if (!alreadyPending) addPending(batch);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
if (alreadyPending) removePending(batch);
|
|
1400
|
+
if (result === "ok") delivered(batch.body);
|
|
1401
|
+
}
|
|
1402
|
+
function drain() {
|
|
1403
|
+
const run = async () => {
|
|
1404
|
+
for (let i = 0; i < pending2.length; ) {
|
|
1405
|
+
const batch = pending2[i];
|
|
1406
|
+
await send(batch, false, true);
|
|
1407
|
+
if (pending2[i] === batch) return;
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
const next = draining ? draining.then(run) : run();
|
|
1411
|
+
draining = next;
|
|
1412
|
+
void next.finally(() => {
|
|
1413
|
+
if (draining === next) draining = null;
|
|
1414
|
+
});
|
|
1415
|
+
return next;
|
|
1416
|
+
}
|
|
1417
|
+
if (pending2.length) {
|
|
1418
|
+
savePending();
|
|
1419
|
+
setTimeout(() => void drain(), 0);
|
|
1420
|
+
}
|
|
1345
1421
|
return async (envelope, meta) => {
|
|
1346
1422
|
if (meta?.resetSession) {
|
|
1347
1423
|
feedbackSessionId = void 0;
|
|
1348
|
-
firstPostInFlight = null;
|
|
1349
|
-
generation++;
|
|
1350
1424
|
options.onSessionReset?.();
|
|
1351
1425
|
return;
|
|
1352
1426
|
}
|
|
1353
1427
|
const final = meta?.final === true;
|
|
1354
1428
|
const sessionEnd = meta?.sessionEnd === true;
|
|
1355
|
-
if (!feedbackSessionId
|
|
1356
|
-
await firstPostInFlight;
|
|
1357
|
-
}
|
|
1429
|
+
if (!feedbackSessionId) feedbackSessionId = newSessionId();
|
|
1358
1430
|
const body = buildAnalyticsFeedbackBody(envelope, options.keys, feedbackSessionId, { sessionEnd });
|
|
1359
1431
|
options.log?.("[DeepdotsAnalytics] POST /sdk/feedback \u2192", body);
|
|
1360
|
-
const
|
|
1432
|
+
const batch = { body, at: now() };
|
|
1361
1433
|
if (sessionEnd) {
|
|
1362
1434
|
feedbackSessionId = void 0;
|
|
1363
|
-
firstPostInFlight = null;
|
|
1364
|
-
generation++;
|
|
1365
1435
|
options.onSessionReset?.();
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
() => void 0
|
|
1370
|
-
);
|
|
1436
|
+
addPending(batch);
|
|
1437
|
+
await drain();
|
|
1438
|
+
return;
|
|
1371
1439
|
}
|
|
1372
|
-
|
|
1440
|
+
const retries = pending2.length ? drain() : null;
|
|
1441
|
+
await Promise.all([retries, send(batch, final, false)]);
|
|
1373
1442
|
};
|
|
1374
1443
|
}
|
|
1375
1444
|
|
|
@@ -1788,6 +1857,10 @@ function crashRecordToParams(r) {
|
|
|
1788
1857
|
}
|
|
1789
1858
|
var CrashReporter = class {
|
|
1790
1859
|
constructor(options) {
|
|
1860
|
+
/** Los handlers de `window` ya están puestos: `install()` es idempotente. */
|
|
1861
|
+
this.windowInstalled = false;
|
|
1862
|
+
/** ErrorUtils ya enganchados: engancharlos dos veces capturaría cada error dos veces. */
|
|
1863
|
+
this.hookedErrorUtils = /* @__PURE__ */ new Set();
|
|
1791
1864
|
this.options = options;
|
|
1792
1865
|
this.now = options.now ?? (() => Date.now());
|
|
1793
1866
|
}
|
|
@@ -1863,9 +1936,11 @@ var CrashReporter = class {
|
|
|
1863
1936
|
}
|
|
1864
1937
|
return queue;
|
|
1865
1938
|
}
|
|
1866
|
-
/** Instala los handlers globales de errores no capturados (no-op sin `window`). */
|
|
1939
|
+
/** Instala los handlers globales de errores no capturados (no-op sin `window`). Idempotente. */
|
|
1867
1940
|
install() {
|
|
1941
|
+
if (this.windowInstalled) return;
|
|
1868
1942
|
if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
|
|
1943
|
+
this.windowInstalled = true;
|
|
1869
1944
|
window.addEventListener("error", (e) => {
|
|
1870
1945
|
if (!this.isEnabled()) return;
|
|
1871
1946
|
this.captureUnhandled(e.error ?? e.message ?? "error");
|
|
@@ -1875,8 +1950,10 @@ var CrashReporter = class {
|
|
|
1875
1950
|
this.captureUnhandled(e.reason ?? "unhandledrejection");
|
|
1876
1951
|
});
|
|
1877
1952
|
}
|
|
1878
|
-
/** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. */
|
|
1953
|
+
/** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. Idempotente por instancia de ErrorUtils. */
|
|
1879
1954
|
installReactNative(errorUtils) {
|
|
1955
|
+
if (this.hookedErrorUtils.has(errorUtils)) return;
|
|
1956
|
+
this.hookedErrorUtils.add(errorUtils);
|
|
1880
1957
|
const previous = errorUtils.getGlobalHandler?.();
|
|
1881
1958
|
errorUtils.setGlobalHandler((error, isFatal) => {
|
|
1882
1959
|
if (this.isEnabled()) this.captureUnhandled(error);
|
|
@@ -2089,6 +2166,8 @@ var DeepdotsPopups = class {
|
|
|
2089
2166
|
this.analyticsFlushTimer = void 0;
|
|
2090
2167
|
/** Crash & error reporting (#14–17). Null hasta init(). */
|
|
2091
2168
|
this.crashReporter = null;
|
|
2169
|
+
/** ErrorUtils de RN recibido del host (o del Provider); se engancha en cuanto haya consentimiento. */
|
|
2170
|
+
this.rnErrorUtils = null;
|
|
2092
2171
|
/** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */
|
|
2093
2172
|
this.messageGuard = new MessageGuard();
|
|
2094
2173
|
/** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
|
|
@@ -2189,6 +2268,10 @@ var DeepdotsPopups = class {
|
|
|
2189
2268
|
keys: config.analytics,
|
|
2190
2269
|
log: (...a) => this.log(...a),
|
|
2191
2270
|
initialSessionId: resumable?.sessionId,
|
|
2271
|
+
// En móvil (RN) la app puede morir con el lote de cierre sin entregar: se
|
|
2272
|
+
// persiste y se reenvía en el arranque siguiente. En web el cierre sale con
|
|
2273
|
+
// `keepalive` y varias pestañas compartirían el mismo storage.
|
|
2274
|
+
storage: (config.platform ?? "web") !== "web" ? storage : void 0,
|
|
2192
2275
|
onSessionId: (id) => {
|
|
2193
2276
|
this.analyticsFeedbackSessionId = id;
|
|
2194
2277
|
this.notifyFeedbackSession(id, "open");
|
|
@@ -2224,9 +2307,7 @@ var DeepdotsPopups = class {
|
|
|
2224
2307
|
now: () => Date.now(),
|
|
2225
2308
|
enabled: () => this.tracking?.isTrackingEnabled() ?? false
|
|
2226
2309
|
});
|
|
2227
|
-
|
|
2228
|
-
this.crashReporter.install();
|
|
2229
|
-
}
|
|
2310
|
+
this.installCrashHandlers();
|
|
2230
2311
|
this.geoEnabled = config.geolocation ?? true;
|
|
2231
2312
|
this.geoWanted = this.geoEnabled && !!config.analytics;
|
|
2232
2313
|
this.resolveGeo();
|
|
@@ -2295,6 +2376,7 @@ var DeepdotsPopups = class {
|
|
|
2295
2376
|
if (!enabled) this.closeSession("tracking_disabled");
|
|
2296
2377
|
this.tracking?.setTrackingEnabled(enabled);
|
|
2297
2378
|
if (enabled) this.openSession("tracking_enabled");
|
|
2379
|
+
if (enabled) this.installCrashHandlers();
|
|
2298
2380
|
if (enabled) this.resolveGeo();
|
|
2299
2381
|
this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
|
|
2300
2382
|
}
|
|
@@ -2325,10 +2407,27 @@ var DeepdotsPopups = class {
|
|
|
2325
2407
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
2326
2408
|
this.crashReporter?.reportError(error, options);
|
|
2327
2409
|
}
|
|
2328
|
-
/**
|
|
2410
|
+
/**
|
|
2411
|
+
* Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. Con tracking off
|
|
2412
|
+
* se recuerda y se engancha cuando se conceda el consentimiento (`setTrackingEnabled(true)`).
|
|
2413
|
+
*/
|
|
2329
2414
|
installReactNativeCrashHandler(errorUtils) {
|
|
2330
|
-
|
|
2331
|
-
this.
|
|
2415
|
+
this.rnErrorUtils = errorUtils;
|
|
2416
|
+
this.installCrashHandlers();
|
|
2417
|
+
}
|
|
2418
|
+
/**
|
|
2419
|
+
* Pone los handlers de errores no capturados: `window` en web y `ErrorUtils` en RN (el que
|
|
2420
|
+
* pasó el host o, si no pasó ninguno, `globalThis.ErrorUtils`, para que un `init()` sin
|
|
2421
|
+
* `setupReactNative` también capture). Solo con consentimiento; idempotente, así que se
|
|
2422
|
+
* llama en `init()` y cada vez que se activa el tracking.
|
|
2423
|
+
*/
|
|
2424
|
+
installCrashHandlers() {
|
|
2425
|
+
if (!this.crashReporter || !this.tracking?.isTrackingEnabled()) return;
|
|
2426
|
+
this.crashReporter.install();
|
|
2427
|
+
const errorUtils = this.rnErrorUtils ?? globalThis.ErrorUtils;
|
|
2428
|
+
if (errorUtils && typeof errorUtils.setGlobalHandler === "function") {
|
|
2429
|
+
this.crashReporter.installReactNative(errorUtils);
|
|
2430
|
+
}
|
|
2332
2431
|
}
|
|
2333
2432
|
/**
|
|
2334
2433
|
* Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
|