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