@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/react-native.mjs
CHANGED
|
@@ -410,6 +410,7 @@ var AnalyticsManager = class {
|
|
|
410
410
|
this.device = options.device;
|
|
411
411
|
this.platform = options.platform ?? "web";
|
|
412
412
|
this.maxBatchSize = options.maxBatchSize ?? 20;
|
|
413
|
+
this.maxBufferedEvents = options.maxBufferedEvents ?? 200;
|
|
413
414
|
this.onFlushNeeded = options.onFlushNeeded;
|
|
414
415
|
}
|
|
415
416
|
/** Actualiza campos de device en runtime (p.ej. country/city tras resolver geo). */
|
|
@@ -470,6 +471,7 @@ var AnalyticsManager = class {
|
|
|
470
471
|
timestamp: this.now(),
|
|
471
472
|
params: Object.keys(merged).length ? merged : void 0
|
|
472
473
|
});
|
|
474
|
+
this.enforceBufferCap();
|
|
473
475
|
if (this.events.length >= this.maxBatchSize) {
|
|
474
476
|
this.onFlushNeeded?.();
|
|
475
477
|
}
|
|
@@ -500,14 +502,43 @@ var AnalyticsManager = class {
|
|
|
500
502
|
events: [...this.events]
|
|
501
503
|
};
|
|
502
504
|
}
|
|
503
|
-
/**
|
|
504
|
-
|
|
505
|
+
/**
|
|
506
|
+
* Envía (vía sink) el lote acumulado y vacía el buffer. No-op si no hay eventos.
|
|
507
|
+
*
|
|
508
|
+
* Si el sink falla de forma transitoria (promesa rechazada o excepción síncrona) el lote
|
|
509
|
+
* se **re-encola** al principio del buffer y se reintenta en el siguiente flush: un error
|
|
510
|
+
* de red o un 5xx no debe hacer desaparecer los eventos.
|
|
511
|
+
*/
|
|
512
|
+
flush(identity, meta) {
|
|
505
513
|
if (this.events.length === 0) return null;
|
|
514
|
+
const batch = this.events;
|
|
506
515
|
const payload = this.buildPayload(identity);
|
|
507
|
-
this.sink(payload);
|
|
508
516
|
this.events = [];
|
|
517
|
+
let result;
|
|
518
|
+
try {
|
|
519
|
+
result = meta ? this.sink(payload, meta) : this.sink(payload);
|
|
520
|
+
} catch {
|
|
521
|
+
this.requeue(batch);
|
|
522
|
+
return payload;
|
|
523
|
+
}
|
|
524
|
+
if (result && typeof result.then === "function") {
|
|
525
|
+
result.catch(() => this.requeue(batch));
|
|
526
|
+
}
|
|
509
527
|
return payload;
|
|
510
528
|
}
|
|
529
|
+
/**
|
|
530
|
+
* Devuelve un lote fallido al buffer, delante de lo que haya llegado mientras se enviaba
|
|
531
|
+
* (orden cronológico). Con el buffer lleno se descartan los eventos más antiguos.
|
|
532
|
+
*/
|
|
533
|
+
requeue(batch) {
|
|
534
|
+
this.events = [...batch, ...this.events];
|
|
535
|
+
this.enforceBufferCap();
|
|
536
|
+
}
|
|
537
|
+
/** Techo del buffer: si el envío lleva rato fallando, se sacrifica lo más antiguo. */
|
|
538
|
+
enforceBufferCap() {
|
|
539
|
+
const overflow = this.events.length - this.maxBufferedEvents;
|
|
540
|
+
if (overflow > 0) this.events.splice(0, overflow);
|
|
541
|
+
}
|
|
511
542
|
};
|
|
512
543
|
|
|
513
544
|
// src/analytics/feedback-payload.ts
|
|
@@ -570,33 +601,80 @@ function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
|
|
|
570
601
|
...feedbackSessionId ? { sessionId: feedbackSessionId } : {}
|
|
571
602
|
};
|
|
572
603
|
}
|
|
604
|
+
var KEEPALIVE_MAX_BYTES = 6e4;
|
|
605
|
+
function isRetryableStatus(status) {
|
|
606
|
+
return status >= 500 || status === 408 || status === 429;
|
|
607
|
+
}
|
|
608
|
+
async function safeBodyText(res) {
|
|
609
|
+
try {
|
|
610
|
+
return (await res.text()).slice(0, 500);
|
|
611
|
+
} catch {
|
|
612
|
+
return "";
|
|
613
|
+
}
|
|
614
|
+
}
|
|
573
615
|
function createFeedbackSink(options) {
|
|
574
616
|
let feedbackSessionId;
|
|
575
|
-
|
|
576
|
-
|
|
617
|
+
let firstPostInFlight = null;
|
|
618
|
+
const url = `${options.baseUrl}/sdk/feedback`;
|
|
619
|
+
const post = async (body, final) => {
|
|
620
|
+
const json = JSON.stringify(body);
|
|
621
|
+
const small = json.length <= KEEPALIVE_MAX_BYTES;
|
|
622
|
+
if (final && options.sendBeaconImpl && small) {
|
|
623
|
+
const payload = typeof Blob !== "undefined" ? new Blob([json], { type: "application/json" }) : json;
|
|
624
|
+
if (options.sendBeaconImpl(url, payload)) {
|
|
625
|
+
options.log?.("[DeepdotsAnalytics] flush final v\xEDa sendBeacon");
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
options.log?.("[DeepdotsAnalytics] sendBeacon rechaz\xF3 el lote; fallback a fetch");
|
|
629
|
+
}
|
|
577
630
|
const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
578
631
|
if (!f) {
|
|
579
632
|
options.log?.("[DeepdotsAnalytics] no fetch disponible; payload no enviado", body);
|
|
580
633
|
return;
|
|
581
634
|
}
|
|
582
|
-
|
|
583
|
-
f(`${options.baseUrl}/sdk/feedback`, {
|
|
635
|
+
const res = await f(url, {
|
|
584
636
|
method: "POST",
|
|
585
637
|
headers: { "Content-Type": "application/json" },
|
|
586
|
-
body:
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
597
|
-
|
|
638
|
+
body: json,
|
|
639
|
+
// keepalive: el POST sobrevive a la navegación mientras el body sea pequeño.
|
|
640
|
+
...small ? { keepalive: true } : {}
|
|
641
|
+
});
|
|
642
|
+
if (!res.ok) {
|
|
643
|
+
const detail = await safeBodyText(res);
|
|
644
|
+
if (isRetryableStatus(res.status)) {
|
|
645
|
+
throw new Error(`POST /sdk/feedback ${res.status} (reintentable): ${detail}`);
|
|
646
|
+
}
|
|
647
|
+
options.log?.(
|
|
648
|
+
`[DeepdotsAnalytics] POST /sdk/feedback rechazado con ${res.status}; lote DESCARTADO:`,
|
|
649
|
+
detail
|
|
650
|
+
);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
const data = await res.json();
|
|
655
|
+
if (data?.sessionId && data.sessionId !== feedbackSessionId) {
|
|
656
|
+
feedbackSessionId = data.sessionId;
|
|
657
|
+
options.log?.("[DeepdotsAnalytics] feedbackSessionId cacheado:", feedbackSessionId);
|
|
658
|
+
options.onSessionId?.(feedbackSessionId);
|
|
598
659
|
}
|
|
599
|
-
}
|
|
660
|
+
} catch {
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
return async (envelope, meta) => {
|
|
664
|
+
const final = meta?.final === true;
|
|
665
|
+
if (!feedbackSessionId && firstPostInFlight && !final) {
|
|
666
|
+
await firstPostInFlight;
|
|
667
|
+
}
|
|
668
|
+
const body = buildAnalyticsFeedbackBody(envelope, options.keys, feedbackSessionId);
|
|
669
|
+
options.log?.("[DeepdotsAnalytics] POST /sdk/feedback \u2192", body);
|
|
670
|
+
const sent = post(body, final);
|
|
671
|
+
if (!feedbackSessionId) {
|
|
672
|
+
firstPostInFlight = sent.then(
|
|
673
|
+
() => void 0,
|
|
674
|
+
() => void 0
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
await sent;
|
|
600
678
|
};
|
|
601
679
|
}
|
|
602
680
|
|
|
@@ -1086,6 +1164,11 @@ var DeepdotsPopups = class {
|
|
|
1086
1164
|
this.contact = null;
|
|
1087
1165
|
/** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
|
|
1088
1166
|
this.navStarted = false;
|
|
1167
|
+
/**
|
|
1168
|
+
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
1169
|
+
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
1170
|
+
*/
|
|
1171
|
+
this.language = void 0;
|
|
1089
1172
|
}
|
|
1090
1173
|
/** Initialize the SDK with configuration */
|
|
1091
1174
|
init(config) {
|
|
@@ -1127,19 +1210,22 @@ var DeepdotsPopups = class {
|
|
|
1127
1210
|
baseUrl: this.baseUrl,
|
|
1128
1211
|
keys: config.analytics,
|
|
1129
1212
|
log: (...a) => this.log(...a),
|
|
1213
|
+
// sendBeacon: único transporte que sobrevive al cierre de la página.
|
|
1214
|
+
sendBeaconImpl: typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" ? (url, body) => navigator.sendBeacon(url, body) : void 0,
|
|
1130
1215
|
onSessionId: (id) => {
|
|
1131
1216
|
this.analyticsFeedbackSessionId = id;
|
|
1132
1217
|
}
|
|
1133
1218
|
}) : createDryRunSink((...a) => this.logger.log(...a));
|
|
1134
1219
|
const device = config.device ?? collectDeviceInfo(config.appVersion);
|
|
1220
|
+
this.language = resolveLanguage({
|
|
1221
|
+
explicit: config.language,
|
|
1222
|
+
navigator: typeof navigator !== "undefined" ? navigator : void 0,
|
|
1223
|
+
intl: typeof Intl !== "undefined" ? Intl : void 0
|
|
1224
|
+
});
|
|
1135
1225
|
this.analytics = new AnalyticsManager({
|
|
1136
1226
|
sink: analyticsSink,
|
|
1137
1227
|
publicKey: config.analytics?.publicKey ?? this.config.apiKey,
|
|
1138
|
-
language:
|
|
1139
|
-
explicit: config.language,
|
|
1140
|
-
navigator: typeof navigator !== "undefined" ? navigator : void 0,
|
|
1141
|
-
intl: typeof Intl !== "undefined" ? Intl : void 0
|
|
1142
|
-
}),
|
|
1228
|
+
language: this.language,
|
|
1143
1229
|
platform: config.platform ?? "web",
|
|
1144
1230
|
device,
|
|
1145
1231
|
maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,
|
|
@@ -1315,10 +1401,14 @@ var DeepdotsPopups = class {
|
|
|
1315
1401
|
events: []
|
|
1316
1402
|
};
|
|
1317
1403
|
}
|
|
1318
|
-
/**
|
|
1319
|
-
|
|
1404
|
+
/**
|
|
1405
|
+
* Envía el lote acumulado de analytics y vacía el buffer.
|
|
1406
|
+
* `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
|
|
1407
|
+
* al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
|
|
1408
|
+
*/
|
|
1409
|
+
flushAnalytics(options) {
|
|
1320
1410
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
1321
|
-
this.analytics?.flush(this.analyticsIdentity());
|
|
1411
|
+
this.analytics?.flush(this.analyticsIdentity(), options);
|
|
1322
1412
|
}
|
|
1323
1413
|
/** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
|
|
1324
1414
|
flushEngagement() {
|
|
@@ -1341,7 +1431,7 @@ var DeepdotsPopups = class {
|
|
|
1341
1431
|
this.navObserver?.stop();
|
|
1342
1432
|
if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
|
|
1343
1433
|
this.flushEngagement();
|
|
1344
|
-
this.flushAnalytics();
|
|
1434
|
+
this.flushAnalytics({ final: true });
|
|
1345
1435
|
});
|
|
1346
1436
|
document.addEventListener("visibilitychange", () => {
|
|
1347
1437
|
if (document.visibilityState === "hidden") {
|
|
@@ -1462,14 +1552,16 @@ var DeepdotsPopups = class {
|
|
|
1462
1552
|
matchesSegmentsLang(def) {
|
|
1463
1553
|
const langs = def.segments?.lang;
|
|
1464
1554
|
if (!langs || langs.length === 0) return true;
|
|
1465
|
-
const
|
|
1466
|
-
if (!
|
|
1467
|
-
this.debug("No
|
|
1555
|
+
const currentLang = (this.language ?? "").toLowerCase();
|
|
1556
|
+
if (!currentLang) {
|
|
1557
|
+
this.debug("No language available for lang comparison", { popupId: def.id, langs });
|
|
1468
1558
|
return true;
|
|
1469
1559
|
}
|
|
1470
|
-
const matches = langs.some(
|
|
1560
|
+
const matches = langs.some(
|
|
1561
|
+
(lang) => typeof lang === "string" && currentLang.startsWith(lang.toLowerCase())
|
|
1562
|
+
);
|
|
1471
1563
|
if (!matches) {
|
|
1472
|
-
this.debug("Lang mismatch for popup", { popupId: def.id, langs,
|
|
1564
|
+
this.debug("Lang mismatch for popup", { popupId: def.id, langs, currentLang });
|
|
1473
1565
|
}
|
|
1474
1566
|
return matches;
|
|
1475
1567
|
}
|