@magicfeedback/popup-sdk 1.1.7 → 1.1.8
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/README.md +5 -0
- package/dist/index.d.mts +50 -10
- package/dist/index.d.ts +50 -10
- package/dist/index.js +126 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +126 -34
- package/dist/index.mjs.map +1 -1
- package/dist/react-native.d.mts +18 -5
- package/dist/react-native.d.ts +18 -5
- package/dist/react-native.js +126 -34
- package/dist/react-native.js.map +1 -1
- package/dist/react-native.mjs +126 -34
- package/dist/react-native.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -405,6 +405,7 @@ var AnalyticsManager = class {
|
|
|
405
405
|
this.device = options.device;
|
|
406
406
|
this.platform = options.platform ?? "web";
|
|
407
407
|
this.maxBatchSize = options.maxBatchSize ?? 20;
|
|
408
|
+
this.maxBufferedEvents = options.maxBufferedEvents ?? 200;
|
|
408
409
|
this.onFlushNeeded = options.onFlushNeeded;
|
|
409
410
|
}
|
|
410
411
|
/** Actualiza campos de device en runtime (p.ej. country/city tras resolver geo). */
|
|
@@ -465,6 +466,7 @@ var AnalyticsManager = class {
|
|
|
465
466
|
timestamp: this.now(),
|
|
466
467
|
params: Object.keys(merged).length ? merged : void 0
|
|
467
468
|
});
|
|
469
|
+
this.enforceBufferCap();
|
|
468
470
|
if (this.events.length >= this.maxBatchSize) {
|
|
469
471
|
this.onFlushNeeded?.();
|
|
470
472
|
}
|
|
@@ -495,14 +497,43 @@ var AnalyticsManager = class {
|
|
|
495
497
|
events: [...this.events]
|
|
496
498
|
};
|
|
497
499
|
}
|
|
498
|
-
/**
|
|
499
|
-
|
|
500
|
+
/**
|
|
501
|
+
* Envía (vía sink) el lote acumulado y vacía el buffer. No-op si no hay eventos.
|
|
502
|
+
*
|
|
503
|
+
* Si el sink falla de forma transitoria (promesa rechazada o excepción síncrona) el lote
|
|
504
|
+
* se **re-encola** al principio del buffer y se reintenta en el siguiente flush: un error
|
|
505
|
+
* de red o un 5xx no debe hacer desaparecer los eventos.
|
|
506
|
+
*/
|
|
507
|
+
flush(identity, meta) {
|
|
500
508
|
if (this.events.length === 0) return null;
|
|
509
|
+
const batch = this.events;
|
|
501
510
|
const payload = this.buildPayload(identity);
|
|
502
|
-
this.sink(payload);
|
|
503
511
|
this.events = [];
|
|
512
|
+
let result;
|
|
513
|
+
try {
|
|
514
|
+
result = meta ? this.sink(payload, meta) : this.sink(payload);
|
|
515
|
+
} catch {
|
|
516
|
+
this.requeue(batch);
|
|
517
|
+
return payload;
|
|
518
|
+
}
|
|
519
|
+
if (result && typeof result.then === "function") {
|
|
520
|
+
result.catch(() => this.requeue(batch));
|
|
521
|
+
}
|
|
504
522
|
return payload;
|
|
505
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* Devuelve un lote fallido al buffer, delante de lo que haya llegado mientras se enviaba
|
|
526
|
+
* (orden cronológico). Con el buffer lleno se descartan los eventos más antiguos.
|
|
527
|
+
*/
|
|
528
|
+
requeue(batch) {
|
|
529
|
+
this.events = [...batch, ...this.events];
|
|
530
|
+
this.enforceBufferCap();
|
|
531
|
+
}
|
|
532
|
+
/** Techo del buffer: si el envío lleva rato fallando, se sacrifica lo más antiguo. */
|
|
533
|
+
enforceBufferCap() {
|
|
534
|
+
const overflow = this.events.length - this.maxBufferedEvents;
|
|
535
|
+
if (overflow > 0) this.events.splice(0, overflow);
|
|
536
|
+
}
|
|
506
537
|
};
|
|
507
538
|
|
|
508
539
|
// src/analytics/feedback-payload.ts
|
|
@@ -565,33 +596,80 @@ function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
|
|
|
565
596
|
...feedbackSessionId ? { sessionId: feedbackSessionId } : {}
|
|
566
597
|
};
|
|
567
598
|
}
|
|
599
|
+
var KEEPALIVE_MAX_BYTES = 6e4;
|
|
600
|
+
function isRetryableStatus(status) {
|
|
601
|
+
return status >= 500 || status === 408 || status === 429;
|
|
602
|
+
}
|
|
603
|
+
async function safeBodyText(res) {
|
|
604
|
+
try {
|
|
605
|
+
return (await res.text()).slice(0, 500);
|
|
606
|
+
} catch {
|
|
607
|
+
return "";
|
|
608
|
+
}
|
|
609
|
+
}
|
|
568
610
|
function createFeedbackSink(options) {
|
|
569
611
|
let feedbackSessionId;
|
|
570
|
-
|
|
571
|
-
|
|
612
|
+
let firstPostInFlight = null;
|
|
613
|
+
const url = `${options.baseUrl}/sdk/feedback`;
|
|
614
|
+
const post = async (body, final) => {
|
|
615
|
+
const json = JSON.stringify(body);
|
|
616
|
+
const small = json.length <= KEEPALIVE_MAX_BYTES;
|
|
617
|
+
if (final && options.sendBeaconImpl && small) {
|
|
618
|
+
const payload = typeof Blob !== "undefined" ? new Blob([json], { type: "application/json" }) : json;
|
|
619
|
+
if (options.sendBeaconImpl(url, payload)) {
|
|
620
|
+
options.log?.("[DeepdotsAnalytics] flush final v\xEDa sendBeacon");
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
options.log?.("[DeepdotsAnalytics] sendBeacon rechaz\xF3 el lote; fallback a fetch");
|
|
624
|
+
}
|
|
572
625
|
const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
573
626
|
if (!f) {
|
|
574
627
|
options.log?.("[DeepdotsAnalytics] no fetch disponible; payload no enviado", body);
|
|
575
628
|
return;
|
|
576
629
|
}
|
|
577
|
-
|
|
578
|
-
f(`${options.baseUrl}/sdk/feedback`, {
|
|
630
|
+
const res = await f(url, {
|
|
579
631
|
method: "POST",
|
|
580
632
|
headers: { "Content-Type": "application/json" },
|
|
581
|
-
body:
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
|
|
633
|
+
body: json,
|
|
634
|
+
// keepalive: el POST sobrevive a la navegación mientras el body sea pequeño.
|
|
635
|
+
...small ? { keepalive: true } : {}
|
|
636
|
+
});
|
|
637
|
+
if (!res.ok) {
|
|
638
|
+
const detail = await safeBodyText(res);
|
|
639
|
+
if (isRetryableStatus(res.status)) {
|
|
640
|
+
throw new Error(`POST /sdk/feedback ${res.status} (reintentable): ${detail}`);
|
|
641
|
+
}
|
|
642
|
+
options.log?.(
|
|
643
|
+
`[DeepdotsAnalytics] POST /sdk/feedback rechazado con ${res.status}; lote DESCARTADO:`,
|
|
644
|
+
detail
|
|
645
|
+
);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
try {
|
|
649
|
+
const data = await res.json();
|
|
650
|
+
if (data?.sessionId && data.sessionId !== feedbackSessionId) {
|
|
651
|
+
feedbackSessionId = data.sessionId;
|
|
652
|
+
options.log?.("[DeepdotsAnalytics] feedbackSessionId cacheado:", feedbackSessionId);
|
|
653
|
+
options.onSessionId?.(feedbackSessionId);
|
|
593
654
|
}
|
|
594
|
-
}
|
|
655
|
+
} catch {
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
return async (envelope, meta) => {
|
|
659
|
+
const final = meta?.final === true;
|
|
660
|
+
if (!feedbackSessionId && firstPostInFlight && !final) {
|
|
661
|
+
await firstPostInFlight;
|
|
662
|
+
}
|
|
663
|
+
const body = buildAnalyticsFeedbackBody(envelope, options.keys, feedbackSessionId);
|
|
664
|
+
options.log?.("[DeepdotsAnalytics] POST /sdk/feedback \u2192", body);
|
|
665
|
+
const sent = post(body, final);
|
|
666
|
+
if (!feedbackSessionId) {
|
|
667
|
+
firstPostInFlight = sent.then(
|
|
668
|
+
() => void 0,
|
|
669
|
+
() => void 0
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
await sent;
|
|
595
673
|
};
|
|
596
674
|
}
|
|
597
675
|
|
|
@@ -1081,6 +1159,11 @@ var DeepdotsPopups = class {
|
|
|
1081
1159
|
this.contact = null;
|
|
1082
1160
|
/** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
|
|
1083
1161
|
this.navStarted = false;
|
|
1162
|
+
/**
|
|
1163
|
+
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
1164
|
+
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
1165
|
+
*/
|
|
1166
|
+
this.language = void 0;
|
|
1084
1167
|
}
|
|
1085
1168
|
/** Initialize the SDK with configuration */
|
|
1086
1169
|
init(config) {
|
|
@@ -1122,19 +1205,22 @@ var DeepdotsPopups = class {
|
|
|
1122
1205
|
baseUrl: this.baseUrl,
|
|
1123
1206
|
keys: config.analytics,
|
|
1124
1207
|
log: (...a) => this.log(...a),
|
|
1208
|
+
// sendBeacon: único transporte que sobrevive al cierre de la página.
|
|
1209
|
+
sendBeaconImpl: typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" ? (url, body) => navigator.sendBeacon(url, body) : void 0,
|
|
1125
1210
|
onSessionId: (id) => {
|
|
1126
1211
|
this.analyticsFeedbackSessionId = id;
|
|
1127
1212
|
}
|
|
1128
1213
|
}) : createDryRunSink((...a) => this.logger.log(...a));
|
|
1129
1214
|
const device = config.device ?? collectDeviceInfo(config.appVersion);
|
|
1215
|
+
this.language = resolveLanguage({
|
|
1216
|
+
explicit: config.language,
|
|
1217
|
+
navigator: typeof navigator !== "undefined" ? navigator : void 0,
|
|
1218
|
+
intl: typeof Intl !== "undefined" ? Intl : void 0
|
|
1219
|
+
});
|
|
1130
1220
|
this.analytics = new AnalyticsManager({
|
|
1131
1221
|
sink: analyticsSink,
|
|
1132
1222
|
publicKey: config.analytics?.publicKey ?? this.config.apiKey,
|
|
1133
|
-
language:
|
|
1134
|
-
explicit: config.language,
|
|
1135
|
-
navigator: typeof navigator !== "undefined" ? navigator : void 0,
|
|
1136
|
-
intl: typeof Intl !== "undefined" ? Intl : void 0
|
|
1137
|
-
}),
|
|
1223
|
+
language: this.language,
|
|
1138
1224
|
platform: config.platform ?? "web",
|
|
1139
1225
|
device,
|
|
1140
1226
|
maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,
|
|
@@ -1310,10 +1396,14 @@ var DeepdotsPopups = class {
|
|
|
1310
1396
|
events: []
|
|
1311
1397
|
};
|
|
1312
1398
|
}
|
|
1313
|
-
/**
|
|
1314
|
-
|
|
1399
|
+
/**
|
|
1400
|
+
* Envía el lote acumulado de analytics y vacía el buffer.
|
|
1401
|
+
* `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
|
|
1402
|
+
* al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
|
|
1403
|
+
*/
|
|
1404
|
+
flushAnalytics(options) {
|
|
1315
1405
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
1316
|
-
this.analytics?.flush(this.analyticsIdentity());
|
|
1406
|
+
this.analytics?.flush(this.analyticsIdentity(), options);
|
|
1317
1407
|
}
|
|
1318
1408
|
/** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
|
|
1319
1409
|
flushEngagement() {
|
|
@@ -1336,7 +1426,7 @@ var DeepdotsPopups = class {
|
|
|
1336
1426
|
this.navObserver?.stop();
|
|
1337
1427
|
if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
|
|
1338
1428
|
this.flushEngagement();
|
|
1339
|
-
this.flushAnalytics();
|
|
1429
|
+
this.flushAnalytics({ final: true });
|
|
1340
1430
|
});
|
|
1341
1431
|
document.addEventListener("visibilitychange", () => {
|
|
1342
1432
|
if (document.visibilityState === "hidden") {
|
|
@@ -1457,14 +1547,16 @@ var DeepdotsPopups = class {
|
|
|
1457
1547
|
matchesSegmentsLang(def) {
|
|
1458
1548
|
const langs = def.segments?.lang;
|
|
1459
1549
|
if (!langs || langs.length === 0) return true;
|
|
1460
|
-
const
|
|
1461
|
-
if (!
|
|
1462
|
-
this.debug("No
|
|
1550
|
+
const currentLang = (this.language ?? "").toLowerCase();
|
|
1551
|
+
if (!currentLang) {
|
|
1552
|
+
this.debug("No language available for lang comparison", { popupId: def.id, langs });
|
|
1463
1553
|
return true;
|
|
1464
1554
|
}
|
|
1465
|
-
const matches = langs.some(
|
|
1555
|
+
const matches = langs.some(
|
|
1556
|
+
(lang) => typeof lang === "string" && currentLang.startsWith(lang.toLowerCase())
|
|
1557
|
+
);
|
|
1466
1558
|
if (!matches) {
|
|
1467
|
-
this.debug("Lang mismatch for popup", { popupId: def.id, langs,
|
|
1559
|
+
this.debug("Lang mismatch for popup", { popupId: def.id, langs, currentLang });
|
|
1468
1560
|
}
|
|
1469
1561
|
return matches;
|
|
1470
1562
|
}
|