@magicfeedback/popup-sdk 1.1.7 → 1.2.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/README.md +5 -0
- package/dist/index.d.mts +169 -19
- package/dist/index.d.ts +169 -19
- package/dist/index.js +312 -48
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +308 -50
- package/dist/index.mjs.map +1 -1
- package/dist/react-native.d.mts +77 -12
- package/dist/react-native.d.ts +77 -12
- package/dist/react-native.js +309 -48
- package/dist/react-native.js.map +1 -1
- package/dist/react-native.mjs +305 -50
- package/dist/react-native.mjs.map +1 -1
- package/dist/{renderPopup-PS3NJDUR.mjs → renderPopup-NHWJ7YSI.mjs} +7 -1
- package/dist/renderPopup-NHWJ7YSI.mjs.map +1 -0
- package/dist/{renderPopup-EZKDMLVZ.mjs → renderPopup-R53C7DB3.mjs} +7 -1
- package/dist/renderPopup-R53C7DB3.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/renderPopup-EZKDMLVZ.mjs.map +0 -1
- package/dist/renderPopup-PS3NJDUR.mjs.map +0 -1
package/dist/react-native.mjs
CHANGED
|
@@ -366,7 +366,7 @@ var BrowserPopupRenderer = class {
|
|
|
366
366
|
if (!this.container) return;
|
|
367
367
|
this.visible = true;
|
|
368
368
|
const container = this.container;
|
|
369
|
-
void import("./renderPopup-
|
|
369
|
+
void import("./renderPopup-NHWJ7YSI.mjs").then(({ renderPopup }) => {
|
|
370
370
|
renderPopup(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService, analyticsFeedbackSessionId);
|
|
371
371
|
});
|
|
372
372
|
}
|
|
@@ -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). */
|
|
@@ -458,6 +459,15 @@ var AnalyticsManager = class {
|
|
|
458
459
|
this.exitMiniService(name);
|
|
459
460
|
}
|
|
460
461
|
}
|
|
462
|
+
/**
|
|
463
|
+
* Olvida lo que pertenecía al usuario anterior (user attributes + métricas) al cambiar de
|
|
464
|
+
* usuario. NO toca el buffer de eventos: el cierre de sesión ya lo vació con la identidad
|
|
465
|
+
* vieja, y lo que llegue después es del usuario nuevo.
|
|
466
|
+
*/
|
|
467
|
+
resetUserScope() {
|
|
468
|
+
this.attributes = {};
|
|
469
|
+
this.metrics = {};
|
|
470
|
+
}
|
|
461
471
|
/** Registra un evento de analítica (modelo GA: nombre + params). */
|
|
462
472
|
track(name, params) {
|
|
463
473
|
const current = this.getMiniService();
|
|
@@ -470,6 +480,7 @@ var AnalyticsManager = class {
|
|
|
470
480
|
timestamp: this.now(),
|
|
471
481
|
params: Object.keys(merged).length ? merged : void 0
|
|
472
482
|
});
|
|
483
|
+
this.enforceBufferCap();
|
|
473
484
|
if (this.events.length >= this.maxBatchSize) {
|
|
474
485
|
this.onFlushNeeded?.();
|
|
475
486
|
}
|
|
@@ -500,14 +511,43 @@ var AnalyticsManager = class {
|
|
|
500
511
|
events: [...this.events]
|
|
501
512
|
};
|
|
502
513
|
}
|
|
503
|
-
/**
|
|
504
|
-
|
|
514
|
+
/**
|
|
515
|
+
* Envía (vía sink) el lote acumulado y vacía el buffer. No-op si no hay eventos.
|
|
516
|
+
*
|
|
517
|
+
* Si el sink falla de forma transitoria (promesa rechazada o excepción síncrona) el lote
|
|
518
|
+
* se **re-encola** al principio del buffer y se reintenta en el siguiente flush: un error
|
|
519
|
+
* de red o un 5xx no debe hacer desaparecer los eventos.
|
|
520
|
+
*/
|
|
521
|
+
flush(identity, meta) {
|
|
505
522
|
if (this.events.length === 0) return null;
|
|
523
|
+
const batch = this.events;
|
|
506
524
|
const payload = this.buildPayload(identity);
|
|
507
|
-
this.sink(payload);
|
|
508
525
|
this.events = [];
|
|
526
|
+
let result;
|
|
527
|
+
try {
|
|
528
|
+
result = meta ? this.sink(payload, meta) : this.sink(payload);
|
|
529
|
+
} catch {
|
|
530
|
+
this.requeue(batch);
|
|
531
|
+
return payload;
|
|
532
|
+
}
|
|
533
|
+
if (result && typeof result.then === "function") {
|
|
534
|
+
result.catch(() => this.requeue(batch));
|
|
535
|
+
}
|
|
509
536
|
return payload;
|
|
510
537
|
}
|
|
538
|
+
/**
|
|
539
|
+
* Devuelve un lote fallido al buffer, delante de lo que haya llegado mientras se enviaba
|
|
540
|
+
* (orden cronológico). Con el buffer lleno se descartan los eventos más antiguos.
|
|
541
|
+
*/
|
|
542
|
+
requeue(batch) {
|
|
543
|
+
this.events = [...batch, ...this.events];
|
|
544
|
+
this.enforceBufferCap();
|
|
545
|
+
}
|
|
546
|
+
/** Techo del buffer: si el envío lleva rato fallando, se sacrifica lo más antiguo. */
|
|
547
|
+
enforceBufferCap() {
|
|
548
|
+
const overflow = this.events.length - this.maxBufferedEvents;
|
|
549
|
+
if (overflow > 0) this.events.splice(0, overflow);
|
|
550
|
+
}
|
|
511
551
|
};
|
|
512
552
|
|
|
513
553
|
// src/analytics/feedback-payload.ts
|
|
@@ -515,7 +555,7 @@ function pushKV(list, key, value) {
|
|
|
515
555
|
if (value === void 0 || value === null || value === "") return;
|
|
516
556
|
list.push({ key, value: [String(value)] });
|
|
517
557
|
}
|
|
518
|
-
function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
|
|
558
|
+
function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId, options) {
|
|
519
559
|
const { context } = envelope;
|
|
520
560
|
const profile = [];
|
|
521
561
|
pushKV(profile, "external-user-id", envelope.userId);
|
|
@@ -566,37 +606,91 @@ function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
|
|
|
566
606
|
},
|
|
567
607
|
publicKey: keys.publicKey,
|
|
568
608
|
integration: keys.integration,
|
|
569
|
-
|
|
609
|
+
// Único marcador de cierre acordado con backend: el último lote de la sesión.
|
|
610
|
+
completed: options?.sessionEnd === true,
|
|
570
611
|
...feedbackSessionId ? { sessionId: feedbackSessionId } : {}
|
|
571
612
|
};
|
|
572
613
|
}
|
|
614
|
+
var KEEPALIVE_MAX_BYTES = 6e4;
|
|
615
|
+
function isRetryableStatus(status) {
|
|
616
|
+
return status >= 500 || status === 408 || status === 429;
|
|
617
|
+
}
|
|
618
|
+
async function safeBodyText(res) {
|
|
619
|
+
try {
|
|
620
|
+
return (await res.text()).slice(0, 500);
|
|
621
|
+
} catch {
|
|
622
|
+
return "";
|
|
623
|
+
}
|
|
624
|
+
}
|
|
573
625
|
function createFeedbackSink(options) {
|
|
574
626
|
let feedbackSessionId;
|
|
575
|
-
|
|
576
|
-
|
|
627
|
+
let firstPostInFlight = null;
|
|
628
|
+
const url = `${options.baseUrl}/sdk/feedback`;
|
|
629
|
+
const post = async (body, final, sessionEnd = false) => {
|
|
630
|
+
const json = JSON.stringify(body);
|
|
631
|
+
const small = json.length <= KEEPALIVE_MAX_BYTES;
|
|
632
|
+
if (final && options.sendBeaconImpl && small) {
|
|
633
|
+
const payload = typeof Blob !== "undefined" ? new Blob([json], { type: "application/json" }) : json;
|
|
634
|
+
if (options.sendBeaconImpl(url, payload)) {
|
|
635
|
+
options.log?.("[DeepdotsAnalytics] flush final v\xEDa sendBeacon");
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
options.log?.("[DeepdotsAnalytics] sendBeacon rechaz\xF3 el lote; fallback a fetch");
|
|
639
|
+
}
|
|
577
640
|
const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
578
641
|
if (!f) {
|
|
579
642
|
options.log?.("[DeepdotsAnalytics] no fetch disponible; payload no enviado", body);
|
|
580
643
|
return;
|
|
581
644
|
}
|
|
582
|
-
|
|
583
|
-
f(`${options.baseUrl}/sdk/feedback`, {
|
|
645
|
+
const res = await f(url, {
|
|
584
646
|
method: "POST",
|
|
585
647
|
headers: { "Content-Type": "application/json" },
|
|
586
|
-
body:
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
options.onSessionId?.(feedbackSessionId);
|
|
595
|
-
}
|
|
596
|
-
} catch {
|
|
597
|
-
}
|
|
648
|
+
body: json,
|
|
649
|
+
// keepalive: el POST sobrevive a la navegación mientras el body sea pequeño.
|
|
650
|
+
...small ? { keepalive: true } : {}
|
|
651
|
+
});
|
|
652
|
+
if (!res.ok) {
|
|
653
|
+
const detail = await safeBodyText(res);
|
|
654
|
+
if (isRetryableStatus(res.status)) {
|
|
655
|
+
throw new Error(`POST /sdk/feedback ${res.status} (reintentable): ${detail}`);
|
|
598
656
|
}
|
|
599
|
-
|
|
657
|
+
options.log?.(
|
|
658
|
+
`[DeepdotsAnalytics] POST /sdk/feedback rechazado con ${res.status}; lote DESCARTADO:`,
|
|
659
|
+
detail
|
|
660
|
+
);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (sessionEnd) return;
|
|
664
|
+
try {
|
|
665
|
+
const data = await res.json();
|
|
666
|
+
if (data?.sessionId && data.sessionId !== feedbackSessionId) {
|
|
667
|
+
feedbackSessionId = data.sessionId;
|
|
668
|
+
options.log?.("[DeepdotsAnalytics] feedbackSessionId cacheado:", feedbackSessionId);
|
|
669
|
+
options.onSessionId?.(feedbackSessionId);
|
|
670
|
+
}
|
|
671
|
+
} catch {
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
return async (envelope, meta) => {
|
|
675
|
+
const final = meta?.final === true;
|
|
676
|
+
const sessionEnd = meta?.sessionEnd === true;
|
|
677
|
+
if (!feedbackSessionId && firstPostInFlight && !final) {
|
|
678
|
+
await firstPostInFlight;
|
|
679
|
+
}
|
|
680
|
+
const body = buildAnalyticsFeedbackBody(envelope, options.keys, feedbackSessionId, { sessionEnd });
|
|
681
|
+
options.log?.("[DeepdotsAnalytics] POST /sdk/feedback \u2192", body);
|
|
682
|
+
const sent = post(body, final, sessionEnd);
|
|
683
|
+
if (sessionEnd) {
|
|
684
|
+
feedbackSessionId = void 0;
|
|
685
|
+
firstPostInFlight = null;
|
|
686
|
+
options.onSessionReset?.();
|
|
687
|
+
} else if (!feedbackSessionId) {
|
|
688
|
+
firstPostInFlight = sent.then(
|
|
689
|
+
() => void 0,
|
|
690
|
+
() => void 0
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
await sent;
|
|
600
694
|
};
|
|
601
695
|
}
|
|
602
696
|
|
|
@@ -1025,6 +1119,7 @@ var CrashReporter = class {
|
|
|
1025
1119
|
};
|
|
1026
1120
|
|
|
1027
1121
|
// src/analytics/messaging.ts
|
|
1122
|
+
var MESSAGE_CHANNELS = ["push", "in_app"];
|
|
1028
1123
|
function buildMessageParams(stage, o) {
|
|
1029
1124
|
const p = {
|
|
1030
1125
|
stage,
|
|
@@ -1037,6 +1132,57 @@ function buildMessageParams(stage, o) {
|
|
|
1037
1132
|
if (o.currency) p.currency = o.currency;
|
|
1038
1133
|
return { ...p, ...o.params ?? {} };
|
|
1039
1134
|
}
|
|
1135
|
+
var MAX_TRACKED_MESSAGES = 500;
|
|
1136
|
+
var MessageGuard = class {
|
|
1137
|
+
constructor(maxTrackedMessages = MAX_TRACKED_MESSAGES) {
|
|
1138
|
+
this.maxTrackedMessages = maxTrackedMessages;
|
|
1139
|
+
/** message_id → canal fijado + stages ya emitidos. El orden de inserción marca la eviction. */
|
|
1140
|
+
this.seen = /* @__PURE__ */ new Map();
|
|
1141
|
+
}
|
|
1142
|
+
evaluate(stage, options) {
|
|
1143
|
+
const channel = options.channel;
|
|
1144
|
+
if (!MESSAGE_CHANNELS.includes(channel)) {
|
|
1145
|
+
return {
|
|
1146
|
+
emit: false,
|
|
1147
|
+
reason: "invalid_channel",
|
|
1148
|
+
detail: `channel "${channel}" no v\xE1lido (esperado ${MESSAGE_CHANNELS.join(" | ")})`
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
const entry = this.seen.get(options.id);
|
|
1152
|
+
if (!entry) {
|
|
1153
|
+
this.remember(options.id, channel, stage);
|
|
1154
|
+
return { emit: true };
|
|
1155
|
+
}
|
|
1156
|
+
if (entry.channel !== channel) {
|
|
1157
|
+
return {
|
|
1158
|
+
emit: false,
|
|
1159
|
+
reason: "channel_conflict",
|
|
1160
|
+
detail: `message_id "${options.id}" ya se report\xF3 en channel "${entry.channel}"; se descarta "${channel}"`
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
if (entry.stages.has(stage)) {
|
|
1164
|
+
return {
|
|
1165
|
+
emit: false,
|
|
1166
|
+
reason: "duplicate_stage",
|
|
1167
|
+
detail: `stage "${stage}" ya emitido para message_id "${options.id}"`
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
entry.stages.add(stage);
|
|
1171
|
+
return { emit: true };
|
|
1172
|
+
}
|
|
1173
|
+
/** Olvida todo lo vigilado (nueva sesión). */
|
|
1174
|
+
reset() {
|
|
1175
|
+
this.seen.clear();
|
|
1176
|
+
}
|
|
1177
|
+
remember(id, channel, stage) {
|
|
1178
|
+
this.seen.set(id, { channel, stages: /* @__PURE__ */ new Set([stage]) });
|
|
1179
|
+
while (this.seen.size > this.maxTrackedMessages) {
|
|
1180
|
+
const oldest = this.seen.keys().next();
|
|
1181
|
+
if (oldest.done) break;
|
|
1182
|
+
this.seen.delete(oldest.value);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1040
1186
|
|
|
1041
1187
|
// src/core/deepdots-popups.ts
|
|
1042
1188
|
var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
|
|
@@ -1078,20 +1224,40 @@ var DeepdotsPopups = class {
|
|
|
1078
1224
|
this.analyticsFlushTimer = void 0;
|
|
1079
1225
|
/** Crash & error reporting (#14–17). Null hasta init(). */
|
|
1080
1226
|
this.crashReporter = null;
|
|
1227
|
+
/** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */
|
|
1228
|
+
this.messageGuard = new MessageGuard();
|
|
1081
1229
|
/** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
|
|
1082
1230
|
this.navObserver = null;
|
|
1083
1231
|
/** Tiempo activo (engagement time, #8). */
|
|
1084
1232
|
this.engagement = null;
|
|
1085
1233
|
/** Atributos internos del usuario identificado → POST /sdk/popups/contact. Null si no hay userId. */
|
|
1086
1234
|
this.contact = null;
|
|
1235
|
+
/** Storage resuelto en init() (inyectado por el host o localStorage). Reusado al cambiar de usuario. */
|
|
1236
|
+
this.storage = null;
|
|
1087
1237
|
/** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
|
|
1088
1238
|
this.navStarted = false;
|
|
1239
|
+
/**
|
|
1240
|
+
* `true` entre `session_start` y `session_end`. Hace idempotente tanto la apertura como
|
|
1241
|
+
* el cierre (p. ej. `visibilitychange` + `pagehide` seguidos) y permite abrir una sesión
|
|
1242
|
+
* nueva al volver a foreground o al conceder el consentimiento más tarde.
|
|
1243
|
+
*/
|
|
1244
|
+
this.sessionOpen = false;
|
|
1245
|
+
/**
|
|
1246
|
+
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
1247
|
+
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
1248
|
+
*/
|
|
1249
|
+
this.language = void 0;
|
|
1089
1250
|
}
|
|
1090
1251
|
/** Initialize the SDK with configuration */
|
|
1091
1252
|
init(config) {
|
|
1092
1253
|
this.logger = config.logger ?? console;
|
|
1093
1254
|
setLogger(this.logger);
|
|
1094
1255
|
if (this.initialized) {
|
|
1256
|
+
const nextUserId = config.userId || void 0;
|
|
1257
|
+
if (nextUserId !== this.config?.userId) {
|
|
1258
|
+
this.setUserId(nextUserId);
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1095
1261
|
this.log("SDK already initialized");
|
|
1096
1262
|
return;
|
|
1097
1263
|
}
|
|
@@ -1104,6 +1270,7 @@ var DeepdotsPopups = class {
|
|
|
1104
1270
|
userId: config.userId || void 0
|
|
1105
1271
|
};
|
|
1106
1272
|
const storage = config.storage ?? createDefaultStorage();
|
|
1273
|
+
this.storage = storage;
|
|
1107
1274
|
this.tracking = new TrackingManager({
|
|
1108
1275
|
storage,
|
|
1109
1276
|
clientUserId: this.config.userId,
|
|
@@ -1127,19 +1294,26 @@ var DeepdotsPopups = class {
|
|
|
1127
1294
|
baseUrl: this.baseUrl,
|
|
1128
1295
|
keys: config.analytics,
|
|
1129
1296
|
log: (...a) => this.log(...a),
|
|
1297
|
+
// sendBeacon: único transporte que sobrevive al cierre de la página.
|
|
1298
|
+
sendBeaconImpl: typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" ? (url, body) => navigator.sendBeacon(url, body) : void 0,
|
|
1130
1299
|
onSessionId: (id) => {
|
|
1131
1300
|
this.analyticsFeedbackSessionId = id;
|
|
1301
|
+
},
|
|
1302
|
+
onSessionReset: () => {
|
|
1303
|
+
this.analyticsFeedbackSessionId = void 0;
|
|
1132
1304
|
}
|
|
1133
1305
|
}) : createDryRunSink((...a) => this.logger.log(...a));
|
|
1134
1306
|
const device = config.device ?? collectDeviceInfo(config.appVersion);
|
|
1307
|
+
this.language = resolveLanguage({
|
|
1308
|
+
explicit: config.language,
|
|
1309
|
+
navigator: typeof navigator !== "undefined" ? navigator : void 0,
|
|
1310
|
+
intl: typeof Intl !== "undefined" ? Intl : void 0
|
|
1311
|
+
});
|
|
1312
|
+
this.messageGuard.reset();
|
|
1135
1313
|
this.analytics = new AnalyticsManager({
|
|
1136
1314
|
sink: analyticsSink,
|
|
1137
1315
|
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
|
-
}),
|
|
1316
|
+
language: this.language,
|
|
1143
1317
|
platform: config.platform ?? "web",
|
|
1144
1318
|
device,
|
|
1145
1319
|
maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,
|
|
@@ -1171,7 +1345,7 @@ var DeepdotsPopups = class {
|
|
|
1171
1345
|
this.engagement = new EngagementTracker();
|
|
1172
1346
|
this.engagement.resume();
|
|
1173
1347
|
this.setupAnalyticsFlush();
|
|
1174
|
-
this.
|
|
1348
|
+
this.openSession();
|
|
1175
1349
|
const pendingCrashes = this.crashReporter.drainPendingCrashes();
|
|
1176
1350
|
if (this.tracking?.isTrackingEnabled()) {
|
|
1177
1351
|
for (const rec of pendingCrashes) {
|
|
@@ -1203,7 +1377,9 @@ var DeepdotsPopups = class {
|
|
|
1203
1377
|
}
|
|
1204
1378
|
/** Activa/desactiva el tracking (identidad + sesión + analytics). Kill-switch del contrato §7bis. */
|
|
1205
1379
|
setTrackingEnabled(enabled) {
|
|
1380
|
+
if (!enabled) this.closeSession("tracking_disabled");
|
|
1206
1381
|
this.tracking?.setTrackingEnabled(enabled);
|
|
1382
|
+
if (enabled) this.openSession();
|
|
1207
1383
|
this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
|
|
1208
1384
|
}
|
|
1209
1385
|
// ───────── Analytics (canal separado del feedback, vinculado por user_id) ─────────
|
|
@@ -1273,8 +1449,18 @@ var DeepdotsPopups = class {
|
|
|
1273
1449
|
trackFunnelStep(funnel, step, taskId, params) {
|
|
1274
1450
|
this.track("deepdots_funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
|
|
1275
1451
|
}
|
|
1276
|
-
/**
|
|
1452
|
+
/**
|
|
1453
|
+
* Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app).
|
|
1454
|
+
* No-op si tracking off. El evento se descarta (con warning) si el `channel` no es válido,
|
|
1455
|
+
* si ese `(message_id, stage)` ya se emitió en la sesión, o si el `message_id` ya se reportó
|
|
1456
|
+
* en otro canal — ver `MessageGuard`.
|
|
1457
|
+
*/
|
|
1277
1458
|
trackMessage(stage, options) {
|
|
1459
|
+
const verdict = this.messageGuard.evaluate(stage, options);
|
|
1460
|
+
if (!verdict.emit) {
|
|
1461
|
+
console.warn(`[DeepdotsPopups] trackMessage descartado (${verdict.reason}): ${verdict.detail}`);
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1278
1464
|
this.track("deepdots_message", buildMessageParams(stage, options));
|
|
1279
1465
|
}
|
|
1280
1466
|
/**
|
|
@@ -1290,21 +1476,86 @@ var DeepdotsPopups = class {
|
|
|
1290
1476
|
this.navObserver.visit(name);
|
|
1291
1477
|
}
|
|
1292
1478
|
}
|
|
1293
|
-
/** App a foreground (RN: AppState 'active'): reanuda el engagement time. */
|
|
1479
|
+
/** App a foreground (RN: AppState 'active'): reanuda el engagement time y abre sesión nueva. */
|
|
1294
1480
|
onForeground() {
|
|
1295
1481
|
this.engagement?.resume();
|
|
1482
|
+
this.openSession();
|
|
1296
1483
|
}
|
|
1297
1484
|
/**
|
|
1298
|
-
* App a background (RN: AppState 'background'):
|
|
1299
|
-
* mini-
|
|
1485
|
+
* App a background (RN: AppState 'background'): FIN DE SESIÓN. Cierra pantalla actual
|
|
1486
|
+
* (page_view), mini-services (mini_service_exit), emite engagement + `session_end` y
|
|
1487
|
+
* envía el último lote con `completed:true`.
|
|
1488
|
+
*
|
|
1489
|
+
* Es la única señal disponible en móvil: el kill de la app (swipe) NO da callback.
|
|
1300
1490
|
*/
|
|
1301
1491
|
onBackground() {
|
|
1492
|
+
this.closeSession("background");
|
|
1493
|
+
this.engagement?.pause();
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Cierre EXPLÍCITO de la sesión por parte del host (logout, fin de flujo…): emite
|
|
1497
|
+
* `session_end` y envía el último lote con `completed:true`.
|
|
1498
|
+
*/
|
|
1499
|
+
endSession() {
|
|
1500
|
+
this.closeSession("manual");
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Cambio de usuario (login / logout / cambio de cuenta). Cierra la sesión del usuario
|
|
1504
|
+
* anterior (`session_end` con `reason: 'user_change'` + `completed:true`), cambia la
|
|
1505
|
+
* identidad y abre una sesión nueva. Sin `userId` vuelve al id anónimo del SDK.
|
|
1506
|
+
*
|
|
1507
|
+
* Equivale a llamar a `init()` otra vez con otro `userId`.
|
|
1508
|
+
*/
|
|
1509
|
+
setUserId(userId) {
|
|
1510
|
+
const next = userId || void 0;
|
|
1511
|
+
if (!this.initialized || !this.config || next === this.config.userId) return;
|
|
1512
|
+
this.closeSession("user_change");
|
|
1513
|
+
const enabled = this.tracking?.isTrackingEnabled() ?? true;
|
|
1514
|
+
const storage = this.storage ?? createDefaultStorage();
|
|
1515
|
+
this.config.userId = next;
|
|
1516
|
+
this.tracking = new TrackingManager({ storage, clientUserId: next, enabled });
|
|
1517
|
+
this.contact = next ? new ContactManager({
|
|
1518
|
+
storage,
|
|
1519
|
+
publicKey: this.config.apiKey ?? "",
|
|
1520
|
+
userId: next,
|
|
1521
|
+
post: (body) => this.postContact(body)
|
|
1522
|
+
}) : null;
|
|
1523
|
+
this.analytics?.resetUserScope();
|
|
1524
|
+
this.messageGuard.reset();
|
|
1525
|
+
this.log("tracking \xB7 user_change \xB7 user_id:", this.tracking.getUserId());
|
|
1526
|
+
this.openSession();
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Cierra la sesión actual: vuelca todo lo que quedaba abierto y envía el último lote
|
|
1530
|
+
* marcado con `completed:true`, para que el backend cierre el registro. El `sessionId`
|
|
1531
|
+
* cacheado se olvida → el lote siguiente abre un registro nuevo.
|
|
1532
|
+
*
|
|
1533
|
+
* Idempotente: dos cierres seguidos (p. ej. `visibilitychange` + `pagehide`) no duplican
|
|
1534
|
+
* el `session_end`.
|
|
1535
|
+
*/
|
|
1536
|
+
closeSession(reason) {
|
|
1537
|
+
if (!this.sessionOpen) return;
|
|
1538
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
1539
|
+
this.sessionOpen = false;
|
|
1302
1540
|
this.navObserver?.stop();
|
|
1303
1541
|
this.navStarted = false;
|
|
1304
|
-
|
|
1542
|
+
this.analytics?.exitAllMiniServices();
|
|
1305
1543
|
this.flushEngagement();
|
|
1306
|
-
this.
|
|
1307
|
-
this.flushAnalytics();
|
|
1544
|
+
this.track("deepdots_session_end", { reason });
|
|
1545
|
+
this.flushAnalytics({ final: reason === "page_hide", sessionEnd: true });
|
|
1546
|
+
this.analyticsFeedbackSessionId = void 0;
|
|
1547
|
+
this.tracking?.setSessionId(null);
|
|
1548
|
+
this.log("tracking \xB7 session_end:", reason);
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Abre sesión (`session_start`) si no hay una abierta y el tracking está activo. Idempotente:
|
|
1552
|
+
* se llama en init(), al volver a foreground y al conceder el consentimiento.
|
|
1553
|
+
*/
|
|
1554
|
+
openSession() {
|
|
1555
|
+
if (this.sessionOpen) return;
|
|
1556
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
1557
|
+
this.sessionOpen = true;
|
|
1558
|
+
this.track("deepdots_session_start", {});
|
|
1308
1559
|
}
|
|
1309
1560
|
/** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
|
|
1310
1561
|
previewAnalytics() {
|
|
@@ -1315,10 +1566,14 @@ var DeepdotsPopups = class {
|
|
|
1315
1566
|
events: []
|
|
1316
1567
|
};
|
|
1317
1568
|
}
|
|
1318
|
-
/**
|
|
1319
|
-
|
|
1569
|
+
/**
|
|
1570
|
+
* Envía el lote acumulado de analytics y vacía el buffer.
|
|
1571
|
+
* `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
|
|
1572
|
+
* al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
|
|
1573
|
+
*/
|
|
1574
|
+
flushAnalytics(options) {
|
|
1320
1575
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
1321
|
-
this.analytics?.flush(this.analyticsIdentity());
|
|
1576
|
+
this.analytics?.flush(this.analyticsIdentity(), options);
|
|
1322
1577
|
}
|
|
1323
1578
|
/** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
|
|
1324
1579
|
flushEngagement() {
|
|
@@ -1338,10 +1593,7 @@ var DeepdotsPopups = class {
|
|
|
1338
1593
|
this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
|
|
1339
1594
|
window.addEventListener("pagehide", () => {
|
|
1340
1595
|
clearInterval(this.analyticsFlushTimer);
|
|
1341
|
-
this.
|
|
1342
|
-
if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
|
|
1343
|
-
this.flushEngagement();
|
|
1344
|
-
this.flushAnalytics();
|
|
1596
|
+
this.closeSession("page_hide");
|
|
1345
1597
|
});
|
|
1346
1598
|
document.addEventListener("visibilitychange", () => {
|
|
1347
1599
|
if (document.visibilityState === "hidden") {
|
|
@@ -1462,14 +1714,16 @@ var DeepdotsPopups = class {
|
|
|
1462
1714
|
matchesSegmentsLang(def) {
|
|
1463
1715
|
const langs = def.segments?.lang;
|
|
1464
1716
|
if (!langs || langs.length === 0) return true;
|
|
1465
|
-
const
|
|
1466
|
-
if (!
|
|
1467
|
-
this.debug("No
|
|
1717
|
+
const currentLang = (this.language ?? "").toLowerCase();
|
|
1718
|
+
if (!currentLang) {
|
|
1719
|
+
this.debug("No language available for lang comparison", { popupId: def.id, langs });
|
|
1468
1720
|
return true;
|
|
1469
1721
|
}
|
|
1470
|
-
const matches = langs.some(
|
|
1722
|
+
const matches = langs.some(
|
|
1723
|
+
(lang) => typeof lang === "string" && currentLang.startsWith(lang.toLowerCase())
|
|
1724
|
+
);
|
|
1471
1725
|
if (!matches) {
|
|
1472
|
-
this.debug("Lang mismatch for popup", { popupId: def.id, langs,
|
|
1726
|
+
this.debug("Lang mismatch for popup", { popupId: def.id, langs, currentLang });
|
|
1473
1727
|
}
|
|
1474
1728
|
return matches;
|
|
1475
1729
|
}
|
|
@@ -1801,7 +2055,7 @@ var DeepdotsPopups = class {
|
|
|
1801
2055
|
}
|
|
1802
2056
|
if (!this.popupContainer) return;
|
|
1803
2057
|
const container = this.popupContainer;
|
|
1804
|
-
void import("./renderPopup-
|
|
2058
|
+
void import("./renderPopup-NHWJ7YSI.mjs").then(({ renderPopup }) => {
|
|
1805
2059
|
renderPopup(
|
|
1806
2060
|
container,
|
|
1807
2061
|
surveyId,
|
|
@@ -2014,7 +2268,8 @@ function setupReactNative(sdk2, config, deps = {}) {
|
|
|
2014
2268
|
if (deps.appState?.addEventListener) {
|
|
2015
2269
|
subscription = deps.appState.addEventListener("change", (state) => {
|
|
2016
2270
|
if (state === "active") sdk2.onForeground();
|
|
2017
|
-
else sdk2.onBackground();
|
|
2271
|
+
else if (state === "background") sdk2.onBackground();
|
|
2272
|
+
else sdk2.flushAnalytics();
|
|
2018
2273
|
});
|
|
2019
2274
|
}
|
|
2020
2275
|
return () => {
|