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