@magicfeedback/popup-sdk 1.10.0 → 1.12.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 +23 -1
- package/dist/index.d.mts +84 -4
- package/dist/index.d.ts +84 -4
- package/dist/index.js +253 -23
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +235 -20
- package/dist/index.mjs.map +1 -1
- package/dist/react-native.d.mts +61 -3
- package/dist/react-native.d.ts +61 -3
- package/dist/react-native.js +254 -23
- package/dist/react-native.js.map +1 -1
- package/dist/react-native.mjs +236 -20
- package/dist/react-native.mjs.map +1 -1
- package/dist/{renderPopup-WLOFOCDR.mjs → renderPopup-JF6IB6HL.mjs} +22 -7
- package/dist/renderPopup-JF6IB6HL.mjs.map +1 -0
- package/dist/{renderPopup-424HR5LF.mjs → renderPopup-KSZJ5AN4.mjs} +22 -7
- package/dist/renderPopup-KSZJ5AN4.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/renderPopup-424HR5LF.mjs.map +0 -1
- package/dist/renderPopup-WLOFOCDR.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -5631,6 +5631,9 @@ function ensureFontFace(family, url) {
|
|
|
5631
5631
|
}
|
|
5632
5632
|
if (el.textContent !== css) el.textContent = css;
|
|
5633
5633
|
}
|
|
5634
|
+
function hasFinePointer() {
|
|
5635
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(pointer: fine)").matches;
|
|
5636
|
+
}
|
|
5634
5637
|
async function renderPopup(container, surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId, options) {
|
|
5635
5638
|
let surveyCompletedEmitted = false;
|
|
5636
5639
|
let stylesInjected = false;
|
|
@@ -6061,6 +6064,13 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
6061
6064
|
if (pos.padding) container.style.padding = pos.padding;
|
|
6062
6065
|
let revealed = false;
|
|
6063
6066
|
let revealTimer = null;
|
|
6067
|
+
let surveyLoaded = false;
|
|
6068
|
+
let openFocusDone = false;
|
|
6069
|
+
function autofocusOnOpen() {
|
|
6070
|
+
if (openFocusDone || !revealed || !surveyLoaded) return;
|
|
6071
|
+
openFocusDone = true;
|
|
6072
|
+
if (hasFinePointer()) formInstance?.focusFirstQuestion?.();
|
|
6073
|
+
}
|
|
6064
6074
|
function reveal() {
|
|
6065
6075
|
if (revealed) return;
|
|
6066
6076
|
revealed = true;
|
|
@@ -6071,6 +6081,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
6071
6081
|
if (!container.contains(popup)) return;
|
|
6072
6082
|
container.style.visibility = "visible";
|
|
6073
6083
|
container.style.pointerEvents = "";
|
|
6084
|
+
autofocusOnOpen();
|
|
6074
6085
|
}
|
|
6075
6086
|
function revealWhenPainted2() {
|
|
6076
6087
|
if (revealed) return;
|
|
@@ -6123,7 +6134,11 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
6123
6134
|
addSuccessScreen: false,
|
|
6124
6135
|
// Surveys multi-idioma: native pide a la API este idioma (el del init o el del
|
|
6125
6136
|
// dispositivo) y, si el survey no lo tiene, la API sirve el idioma por defecto.
|
|
6126
|
-
lang: options?.language
|
|
6137
|
+
lang: options?.language,
|
|
6138
|
+
// Foco en la primera pregunta de escribir tras Start, Siguiente o Atrás. Native lo
|
|
6139
|
+
// aplica después de afterSubmitEvent/onBackEvent, cuando `setLoading(false)` ya ha
|
|
6140
|
+
// quitado el `inert`. La apertura va aparte (ver autofocusOnOpen).
|
|
6141
|
+
autofocus: "navigation"
|
|
6127
6142
|
};
|
|
6128
6143
|
generateOptions.onLoadedEvent = ({ formData, lang }) => {
|
|
6129
6144
|
const shownLang = typeof lang === "string" && lang.trim().length > 0 ? lang : void 0;
|
|
@@ -6178,12 +6193,14 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
6178
6193
|
updateButtons("in_progress_first");
|
|
6179
6194
|
}
|
|
6180
6195
|
updateProgress({ progress: formInstance.progress, total: formInstance.total });
|
|
6181
|
-
emit("popup_clicked", surveyId, { action: "loaded" });
|
|
6182
6196
|
setLoading(false);
|
|
6197
|
+
if (!surveyLoaded) {
|
|
6198
|
+
surveyLoaded = true;
|
|
6199
|
+
autofocusOnOpen();
|
|
6200
|
+
}
|
|
6183
6201
|
};
|
|
6184
6202
|
generateOptions.beforeSubmitEvent = () => {
|
|
6185
6203
|
setLoading(true);
|
|
6186
|
-
emit("popup_clicked", surveyId, { action: "before_submit" });
|
|
6187
6204
|
};
|
|
6188
6205
|
generateOptions.afterSubmitEvent = ({ error, completed, total, progress, followup }) => {
|
|
6189
6206
|
const errText = error ? typeof error === "string" ? error : error.message ?? String(error) : "";
|
|
@@ -6192,13 +6209,12 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
6192
6209
|
if (errText.toLowerCase().includes("no response")) {
|
|
6193
6210
|
errorHint.textContent = labels.errorRequired;
|
|
6194
6211
|
errorHint.style.display = "block";
|
|
6195
|
-
emit("popup_clicked", surveyId, { action: "validation_error_required" });
|
|
6196
6212
|
updateNavButtons();
|
|
6197
6213
|
return;
|
|
6198
6214
|
}
|
|
6199
6215
|
errorHint.textContent = labels.errorSubmit;
|
|
6200
6216
|
errorHint.style.display = "block";
|
|
6201
|
-
|
|
6217
|
+
sdkWarn("[Deepdots] survey submit error:", errText);
|
|
6202
6218
|
return;
|
|
6203
6219
|
}
|
|
6204
6220
|
errorHint.style.display = "none";
|
|
@@ -6217,7 +6233,6 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
6217
6233
|
updateProgress({ progress, total, followup });
|
|
6218
6234
|
};
|
|
6219
6235
|
generateOptions.onBackEvent = ({ progress, error, followup }) => {
|
|
6220
|
-
emit("popup_clicked", surveyId, { action: "back" });
|
|
6221
6236
|
if (!error) pageDepth = Math.max(0, pageDepth - 1);
|
|
6222
6237
|
updateNavButtons();
|
|
6223
6238
|
updateProgress({ progress, total: formInstance.total, followup });
|
|
@@ -6864,11 +6879,25 @@ ${customCss}
|
|
|
6864
6879
|
${PRIMARY_COLOR_JS}
|
|
6865
6880
|
${BUSY_JS}
|
|
6866
6881
|
${REVEAL_JS}
|
|
6882
|
+
// Foco en la primera pregunta si es de escribir. Al navegar lo pone @magicfeedback/native
|
|
6883
|
+
// (autofocus:'navigation'); la apertura la decide el popup, que es quien sabe cu\xE1ndo se revela.
|
|
6884
|
+
// Necesita el popup revelado y el survey cargado, que llegan en cualquier orden, y solo con
|
|
6885
|
+
// rat\xF3n o trackpad: en t\xE1ctil sacar\xEDa el teclado sin que el usuario haya tocado nada.
|
|
6886
|
+
var ddRevealed=false, ddSurveyLoaded=false, ddOpenFocusDone=false;
|
|
6887
|
+
function ddAutofocusOnOpen(){
|
|
6888
|
+
if(ddOpenFocusDone || !ddRevealed || !ddSurveyLoaded){ return; }
|
|
6889
|
+
ddOpenFocusDone=true;
|
|
6890
|
+
var fine=typeof window.matchMedia==='function' && window.matchMedia('(pointer: fine)').matches;
|
|
6891
|
+
var f=window.DeepdotsForm;
|
|
6892
|
+
if(fine && f && typeof f.focusFirstQuestion==='function'){ f.focusFirstQuestion(); }
|
|
6893
|
+
}
|
|
6867
6894
|
// El popup se ense\xF1a cuando el survey est\xE1 pintado; \`ready\` deja al host abrir su propio
|
|
6868
6895
|
// Modal en ese momento en vez de montar un WebView en blanco.
|
|
6869
6896
|
var ddReveal=ddCreateReveal(popup, function(){
|
|
6870
6897
|
document.body.classList.add('dd-ready');
|
|
6871
6898
|
emitJSON('ready');
|
|
6899
|
+
ddRevealed=true;
|
|
6900
|
+
ddAutofocusOnOpen();
|
|
6872
6901
|
}, ${REVEAL_TIMEOUT_MS});
|
|
6873
6902
|
|
|
6874
6903
|
function setLoading(isLoading){
|
|
@@ -6909,6 +6938,9 @@ ${REVEAL_JS}
|
|
|
6909
6938
|
// Surveys multi-idioma: native pide a la API este idioma (el del init o el del
|
|
6910
6939
|
// dispositivo); si el survey no lo tiene, la API sirve el idioma por defecto.
|
|
6911
6940
|
lang:${initLangJson}||undefined,
|
|
6941
|
+
// Foco en la primera pregunta de escribir tras Start, Siguiente o Atr\xE1s. Las versiones
|
|
6942
|
+
// de native sin esta opci\xF3n la ignoran.
|
|
6943
|
+
autofocus:'navigation',
|
|
6912
6944
|
onLoadedEvent:function(args){
|
|
6913
6945
|
// Idioma en el que native muestra el survey: manda sobre el del init para que el
|
|
6914
6946
|
// chrome no se quede en ingl\xE9s delante de un survey traducido. Native < 2.3 no lo
|
|
@@ -6968,6 +7000,8 @@ ${REVEAL_JS}
|
|
|
6968
7000
|
updateProgress({ progress:form.progress, total:form.total, completed:false, followup:false });
|
|
6969
7001
|
emitJSON('loaded');
|
|
6970
7002
|
setLoading(false);
|
|
7003
|
+
// La primera carga es la apertura; las siguientes (tras Start) las enfoca native.
|
|
7004
|
+
if(!ddSurveyLoaded){ ddSurveyLoaded=true; ddAutofocusOnOpen(); }
|
|
6971
7005
|
},
|
|
6972
7006
|
beforeSubmitEvent:function(){
|
|
6973
7007
|
setLoading(true);
|
|
@@ -7120,6 +7154,7 @@ var ReactNativePopupRenderer = class {
|
|
|
7120
7154
|
this.emitReady(surveyId);
|
|
7121
7155
|
break;
|
|
7122
7156
|
case "loaded":
|
|
7157
|
+
break;
|
|
7123
7158
|
case "before_submit":
|
|
7124
7159
|
case "after_submit":
|
|
7125
7160
|
case "back":
|
|
@@ -7498,7 +7533,7 @@ async function safeBodyText(res) {
|
|
|
7498
7533
|
}
|
|
7499
7534
|
}
|
|
7500
7535
|
function createFeedbackSink(options) {
|
|
7501
|
-
let feedbackSessionId;
|
|
7536
|
+
let feedbackSessionId = options.initialSessionId || void 0;
|
|
7502
7537
|
let firstPostInFlight = null;
|
|
7503
7538
|
let generation = 0;
|
|
7504
7539
|
const url = `${options.baseUrl}/sdk/feedback`;
|
|
@@ -7591,6 +7626,50 @@ function createFeedbackSink(options) {
|
|
|
7591
7626
|
};
|
|
7592
7627
|
}
|
|
7593
7628
|
|
|
7629
|
+
// src/analytics/suspended-session.ts
|
|
7630
|
+
var SUSPENDED_SESSION_KEY = "deepdots.analytics.suspended";
|
|
7631
|
+
function getSessionStorage() {
|
|
7632
|
+
try {
|
|
7633
|
+
return typeof window !== "undefined" && window.sessionStorage ? window.sessionStorage : null;
|
|
7634
|
+
} catch {
|
|
7635
|
+
return null;
|
|
7636
|
+
}
|
|
7637
|
+
}
|
|
7638
|
+
function writeSuspendedSession(storage, session) {
|
|
7639
|
+
try {
|
|
7640
|
+
storage.setItem(SUSPENDED_SESSION_KEY, JSON.stringify(session));
|
|
7641
|
+
return true;
|
|
7642
|
+
} catch {
|
|
7643
|
+
return false;
|
|
7644
|
+
}
|
|
7645
|
+
}
|
|
7646
|
+
function clearSuspendedSession(storage) {
|
|
7647
|
+
try {
|
|
7648
|
+
storage.removeItem(SUSPENDED_SESSION_KEY);
|
|
7649
|
+
} catch {
|
|
7650
|
+
}
|
|
7651
|
+
}
|
|
7652
|
+
function takeSuspendedSession(storage, match, now, windowMs) {
|
|
7653
|
+
let raw = null;
|
|
7654
|
+
try {
|
|
7655
|
+
raw = storage.getItem(SUSPENDED_SESSION_KEY);
|
|
7656
|
+
} catch {
|
|
7657
|
+
return null;
|
|
7658
|
+
}
|
|
7659
|
+
if (!raw) return null;
|
|
7660
|
+
clearSuspendedSession(storage);
|
|
7661
|
+
try {
|
|
7662
|
+
const s = JSON.parse(raw);
|
|
7663
|
+
if (typeof s.sessionId !== "string" || !s.sessionId) return null;
|
|
7664
|
+
if (typeof s.at !== "number" || now - s.at < 0 || now - s.at > windowMs) return null;
|
|
7665
|
+
if (s.integration !== match.integration) return null;
|
|
7666
|
+
if ((s.userId ?? null) !== match.userId) return null;
|
|
7667
|
+
return { sessionId: s.sessionId, integration: s.integration, userId: s.userId ?? null, at: s.at };
|
|
7668
|
+
} catch {
|
|
7669
|
+
return null;
|
|
7670
|
+
}
|
|
7671
|
+
}
|
|
7672
|
+
|
|
7594
7673
|
// src/tracking/navigation-observer.ts
|
|
7595
7674
|
var UUID_PREFIX = /^[0-9a-f]{8}-[0-9a-f]{4}-/i;
|
|
7596
7675
|
function screenPath(href) {
|
|
@@ -8189,8 +8268,10 @@ var ANALYTICS_HEARTBEAT_MS = 5 * 6e4;
|
|
|
8189
8268
|
var ANALYTICS_SESSION_TIMEOUT_MS = 30 * 6e4;
|
|
8190
8269
|
var ANALYTICS_SERVER_IDLE_MS = 60 * 6e4;
|
|
8191
8270
|
var ANALYTICS_SESSION_DISCARD_MS = ANALYTICS_SERVER_IDLE_MS - 10 * 6e4;
|
|
8271
|
+
var ANALYTICS_RESUME_WINDOW_MS = 6e4;
|
|
8192
8272
|
var RESERVED_EVENT_PREFIX = "deepdots_";
|
|
8193
8273
|
var CUSTOM_EVENT_PREFIX = "deepdots_event_";
|
|
8274
|
+
var MAX_PENDING_TRIGGERS = 20;
|
|
8194
8275
|
var DeepdotsPopups = class {
|
|
8195
8276
|
constructor() {
|
|
8196
8277
|
this.config = null;
|
|
@@ -8204,6 +8285,14 @@ var DeepdotsPopups = class {
|
|
|
8204
8285
|
// deprecated: mantenido para compatibilidad interna
|
|
8205
8286
|
this.popupDefinitions = [];
|
|
8206
8287
|
this.popupsLoaded = false;
|
|
8288
|
+
/**
|
|
8289
|
+
* `true` mientras el `GET /sdk/{apiKey}/popups` de `init()` está en vuelo. En esa ventana los
|
|
8290
|
+
* disparos del host (`triggerEvent`, `triggerSurvey`) no tienen contra qué evaluarse, así que
|
|
8291
|
+
* se guardan en `pendingTriggers` y se reproducen al cargar. Click, scroll, time y exit no
|
|
8292
|
+
* hacen falta: son listeners que `autoLaunch` instala ya con las definiciones cargadas.
|
|
8293
|
+
*/
|
|
8294
|
+
this.awaitingPopups = false;
|
|
8295
|
+
this.pendingTriggers = [];
|
|
8207
8296
|
this.pendingAutoLaunch = false;
|
|
8208
8297
|
// Estado que alimenta las reglas de redisplay. Copia de trabajo en memoria; la fuente
|
|
8209
8298
|
// duradera es el storage (`popup-state-store`), rehidratada en `init()` — sin eso, una
|
|
@@ -8258,6 +8347,13 @@ var DeepdotsPopups = class {
|
|
|
8258
8347
|
this.lastEngagementAt = 0;
|
|
8259
8348
|
/** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */
|
|
8260
8349
|
this.hiddenAt = null;
|
|
8350
|
+
/**
|
|
8351
|
+
* Sesión suspendida por un `pagehide` (ms epoch y `deepdots_user_id`); null si no hay.
|
|
8352
|
+
* `openSession` la reanuda si sigue a tiempo (ver ANALYTICS_RESUME_WINDOW_MS).
|
|
8353
|
+
*/
|
|
8354
|
+
this.suspended = null;
|
|
8355
|
+
/** Integración de analytics del init: la sesión suspendida solo la hereda la misma. */
|
|
8356
|
+
this.analyticsIntegration = void 0;
|
|
8261
8357
|
/**
|
|
8262
8358
|
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
8263
8359
|
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
@@ -8317,10 +8413,17 @@ var DeepdotsPopups = class {
|
|
|
8317
8413
|
}
|
|
8318
8414
|
}
|
|
8319
8415
|
this.log("tracking \xB7 user_id:", this.tracking.getUserId(), "\xB7 new_user:", this.tracking.isNewUser(), "\xB7 enabled:", this.tracking.isTrackingEnabled());
|
|
8416
|
+
this.analyticsIntegration = config.analytics?.integration;
|
|
8417
|
+
const resumable = this.readResumableSession();
|
|
8418
|
+
if (resumable) {
|
|
8419
|
+
this.analyticsFeedbackSessionId = resumable.sessionId;
|
|
8420
|
+
this.suspended = { at: resumable.at, userId: resumable.userId };
|
|
8421
|
+
}
|
|
8320
8422
|
const analyticsSink = config.analytics ? createFeedbackSink({
|
|
8321
8423
|
baseUrl: this.baseUrl,
|
|
8322
8424
|
keys: config.analytics,
|
|
8323
8425
|
log: (...a) => this.log(...a),
|
|
8426
|
+
initialSessionId: resumable?.sessionId,
|
|
8324
8427
|
onSessionId: (id) => {
|
|
8325
8428
|
this.analyticsFeedbackSessionId = id;
|
|
8326
8429
|
this.notifyFeedbackSession(id, "open");
|
|
@@ -8369,7 +8472,10 @@ var DeepdotsPopups = class {
|
|
|
8369
8472
|
this.navObserver.install();
|
|
8370
8473
|
this.engagement = new EngagementTracker();
|
|
8371
8474
|
this.setupAnalyticsFlush();
|
|
8372
|
-
this.openSession();
|
|
8475
|
+
this.openSession("cold_start");
|
|
8476
|
+
if (resumable && this.analyticsFeedbackSessionId === resumable.sessionId) {
|
|
8477
|
+
this.notifyFeedbackSession(resumable.sessionId, "open");
|
|
8478
|
+
}
|
|
8373
8479
|
const pendingCrashes = this.crashReporter.drainPendingCrashes();
|
|
8374
8480
|
if (this.tracking?.isTrackingEnabled()) {
|
|
8375
8481
|
for (const rec of pendingCrashes) {
|
|
@@ -8380,8 +8486,10 @@ var DeepdotsPopups = class {
|
|
|
8380
8486
|
this.log("SDK initialized", this.config);
|
|
8381
8487
|
if (this.renderer.init) this.renderer.init();
|
|
8382
8488
|
this.setupPopupContainer();
|
|
8489
|
+
this.awaitingPopups = Boolean(this.config.apiKey && this.baseUrl);
|
|
8383
8490
|
this.fetchPopupsFromServer().then((defs) => {
|
|
8384
|
-
|
|
8491
|
+
if (!defs) this.dropPendingTriggers();
|
|
8492
|
+
this.popupDefinitions = this.validatePopupDefinitions(defs ?? []);
|
|
8385
8493
|
this.popupsLoaded = true;
|
|
8386
8494
|
this.log("Popups loaded from API", this.popupDefinitions);
|
|
8387
8495
|
if (this.popupDefinitions.length) this.renderer.preload?.();
|
|
@@ -8390,6 +8498,7 @@ var DeepdotsPopups = class {
|
|
|
8390
8498
|
if (this.pendingAutoLaunch) {
|
|
8391
8499
|
this.startTriggers();
|
|
8392
8500
|
}
|
|
8501
|
+
this.replayPendingTriggers();
|
|
8393
8502
|
});
|
|
8394
8503
|
}
|
|
8395
8504
|
/** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
|
|
@@ -8421,7 +8530,7 @@ var DeepdotsPopups = class {
|
|
|
8421
8530
|
setTrackingEnabled(enabled) {
|
|
8422
8531
|
if (!enabled) this.closeSession("tracking_disabled");
|
|
8423
8532
|
this.tracking?.setTrackingEnabled(enabled);
|
|
8424
|
-
if (enabled) this.openSession();
|
|
8533
|
+
if (enabled) this.openSession("tracking_enabled");
|
|
8425
8534
|
if (enabled) this.resolveGeo();
|
|
8426
8535
|
this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
|
|
8427
8536
|
}
|
|
@@ -8538,7 +8647,7 @@ var DeepdotsPopups = class {
|
|
|
8538
8647
|
/** App a foreground (RN: AppState 'active'): reanuda el engagement time y abre sesión nueva. */
|
|
8539
8648
|
onForeground() {
|
|
8540
8649
|
this.engagement?.resume();
|
|
8541
|
-
this.openSession();
|
|
8650
|
+
this.openSession("foreground");
|
|
8542
8651
|
}
|
|
8543
8652
|
/**
|
|
8544
8653
|
* App a background (RN: AppState 'background'): FIN DE SESIÓN. Cierra pantalla actual
|
|
@@ -8586,7 +8695,7 @@ var DeepdotsPopups = class {
|
|
|
8586
8695
|
this.answeredSurveys.clear();
|
|
8587
8696
|
clearPopupState(storage);
|
|
8588
8697
|
this.log("tracking \xB7 user_change \xB7 user_id:", this.tracking.getUserId());
|
|
8589
|
-
this.openSession();
|
|
8698
|
+
this.openSession("user_change");
|
|
8590
8699
|
}
|
|
8591
8700
|
/**
|
|
8592
8701
|
* Cierra la sesión actual: vuelca todo lo que quedaba abierto y envía el último lote
|
|
@@ -8612,15 +8721,28 @@ var DeepdotsPopups = class {
|
|
|
8612
8721
|
this.log("tracking \xB7 session_end:", reason);
|
|
8613
8722
|
}
|
|
8614
8723
|
/**
|
|
8615
|
-
* Abre sesión (`session_start`) si no hay una abierta y el tracking está activo. Idempotente
|
|
8616
|
-
*
|
|
8724
|
+
* Abre sesión (`session_start`) si no hay una abierta y el tracking está activo. Idempotente.
|
|
8725
|
+
* El `reason` dice por qué empieza (ver `SessionStartReason`):
|
|
8726
|
+
* - `cold_start`: init() (carga de la página / arranque de la app RN).
|
|
8727
|
+
* - `foreground`: onForeground() tras un cierre (RN).
|
|
8728
|
+
* - `tracking_enabled`: setTrackingEnabled(true).
|
|
8729
|
+
* - `user_change`: setUserId() o init() con otro userId.
|
|
8730
|
+
* - `idle_timeout`: la pestaña vuelve a verse tras más de 30 min oculta.
|
|
8731
|
+
* - `bfcache_restore`: vuelta desde la bfcache fuera de la ventana de reanudación.
|
|
8732
|
+
* Si hay una sesión suspendida a tiempo emite `deepdots_session_resume` en su lugar, sin reason.
|
|
8617
8733
|
*/
|
|
8618
|
-
openSession() {
|
|
8734
|
+
openSession(reason) {
|
|
8619
8735
|
if (this.sessionOpen) return;
|
|
8620
8736
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
8737
|
+
const suspendedMs = this.takeSuspendedSession();
|
|
8621
8738
|
this.sessionOpen = true;
|
|
8622
8739
|
this.lastEngagementAt = Date.now();
|
|
8623
|
-
|
|
8740
|
+
if (suspendedMs === null) {
|
|
8741
|
+
this.track("deepdots_session_start", { reason });
|
|
8742
|
+
} else {
|
|
8743
|
+
this.track("deepdots_session_resume", { suspended_ms: suspendedMs });
|
|
8744
|
+
this.log("tracking \xB7 session resumed after", suspendedMs, "ms:", this.analyticsFeedbackSessionId);
|
|
8745
|
+
}
|
|
8624
8746
|
this.navObserver?.restart();
|
|
8625
8747
|
if (this.isDocumentVisible()) this.engagement?.resume();
|
|
8626
8748
|
}
|
|
@@ -8683,7 +8805,76 @@ var DeepdotsPopups = class {
|
|
|
8683
8805
|
} else {
|
|
8684
8806
|
this.discardSession();
|
|
8685
8807
|
}
|
|
8686
|
-
this.openSession();
|
|
8808
|
+
this.openSession("idle_timeout");
|
|
8809
|
+
}
|
|
8810
|
+
/**
|
|
8811
|
+
* `pagehide` en web: SUSPENDE la sesión en vez de cerrarla, porque el navegador lo lanza
|
|
8812
|
+
* igual al recargar que al cerrar la pestaña. Vuelca lo pendiente (page_view de la
|
|
8813
|
+
* pantalla, mini-services, engagement) con un `deepdots_session_suspend` y SIN
|
|
8814
|
+
* `completed`, y deja el registro en `sessionStorage` (ver `suspended-session.ts`):
|
|
8815
|
+
* - si la página vuelve a cargar en esta pestaña, `init()` sigue en el mismo registro;
|
|
8816
|
+
* - si la pestaña se cerró, el backend lo cierra a los pocos minutos sin otro lote.
|
|
8817
|
+
*
|
|
8818
|
+
* Devuelve false cuando no se puede suspender — aún no se conoce el registro (el primer
|
|
8819
|
+
* POST sigue en vuelo) o no hay `sessionStorage` — y entonces el caller cierra como antes.
|
|
8820
|
+
*/
|
|
8821
|
+
suspendSession() {
|
|
8822
|
+
if (!this.sessionOpen || !this.tracking?.isTrackingEnabled()) return false;
|
|
8823
|
+
const sessionId = this.analyticsFeedbackSessionId;
|
|
8824
|
+
const integration = this.analyticsIntegration;
|
|
8825
|
+
const storage = getSessionStorage();
|
|
8826
|
+
if (!sessionId || !integration || !storage) return false;
|
|
8827
|
+
const at = Date.now();
|
|
8828
|
+
const userId = this.tracking.getUserId();
|
|
8829
|
+
if (!writeSuspendedSession(storage, { sessionId, integration, userId, at })) return false;
|
|
8830
|
+
this.sessionOpen = false;
|
|
8831
|
+
this.suspended = { at, userId };
|
|
8832
|
+
this.navObserver?.stop();
|
|
8833
|
+
this.navStarted = false;
|
|
8834
|
+
this.analytics?.exitAllMiniServices();
|
|
8835
|
+
this.flushEngagement();
|
|
8836
|
+
this.engagement?.pause();
|
|
8837
|
+
this.track("deepdots_session_suspend", { reason: "page_hide" });
|
|
8838
|
+
this.flushAnalytics({ final: true });
|
|
8839
|
+
this.log("tracking \xB7 session suspended:", sessionId);
|
|
8840
|
+
return true;
|
|
8841
|
+
}
|
|
8842
|
+
/**
|
|
8843
|
+
* Recoge la sesión que dejó suspendida la carga anterior de esta pestaña (ver
|
|
8844
|
+
* `suspendSession`). Solo con analytics configurado y el tracking activo: sin ellos no hay
|
|
8845
|
+
* registro en el que seguir.
|
|
8846
|
+
*/
|
|
8847
|
+
readResumableSession() {
|
|
8848
|
+
const integration = this.analyticsIntegration;
|
|
8849
|
+
const storage = getSessionStorage();
|
|
8850
|
+
if (!integration || !storage || !this.tracking?.isTrackingEnabled()) return null;
|
|
8851
|
+
return takeSuspendedSession(
|
|
8852
|
+
storage,
|
|
8853
|
+
{ integration, userId: this.tracking.getUserId() },
|
|
8854
|
+
Date.now(),
|
|
8855
|
+
ANALYTICS_RESUME_WINDOW_MS
|
|
8856
|
+
);
|
|
8857
|
+
}
|
|
8858
|
+
/**
|
|
8859
|
+
* Consume la sesión suspendida al abrir: los ms que estuvo suspendida si se puede seguir en
|
|
8860
|
+
* ella (mismo usuario, dentro de ANALYTICS_RESUME_WINDOW_MS); null si no había o ya no toca.
|
|
8861
|
+
* En ese caso se olvida su registro sin enviar nada: lo cierra el backend, y la sesión que
|
|
8862
|
+
* se abre ahora va a uno nuevo.
|
|
8863
|
+
*/
|
|
8864
|
+
takeSuspendedSession() {
|
|
8865
|
+
const suspended = this.suspended;
|
|
8866
|
+
if (!suspended) return null;
|
|
8867
|
+
this.suspended = null;
|
|
8868
|
+
const storage = getSessionStorage();
|
|
8869
|
+
if (storage) clearSuspendedSession(storage);
|
|
8870
|
+
const suspendedMs = Math.max(0, Date.now() - suspended.at);
|
|
8871
|
+
if (this.analyticsFeedbackSessionId && suspendedMs <= ANALYTICS_RESUME_WINDOW_MS && suspended.userId === (this.tracking?.getUserId() ?? null)) {
|
|
8872
|
+
return suspendedMs;
|
|
8873
|
+
}
|
|
8874
|
+
this.analytics?.discardSession(this.analyticsIdentity());
|
|
8875
|
+
this.analyticsFeedbackSessionId = void 0;
|
|
8876
|
+
this.tracking?.setSessionId(null);
|
|
8877
|
+
return null;
|
|
8687
8878
|
}
|
|
8688
8879
|
/** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
|
|
8689
8880
|
discardSession() {
|
|
@@ -8733,12 +8924,12 @@ var DeepdotsPopups = class {
|
|
|
8733
8924
|
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
8734
8925
|
window.addEventListener("pagehide", () => {
|
|
8735
8926
|
clearInterval(this.analyticsFlushTimer);
|
|
8736
|
-
this.closeSession("page_hide");
|
|
8927
|
+
if (!this.suspendSession()) this.closeSession("page_hide");
|
|
8737
8928
|
});
|
|
8738
8929
|
window.addEventListener("pageshow", (event) => {
|
|
8739
8930
|
if (!event.persisted) return;
|
|
8740
8931
|
this.startAnalyticsFlushTimer();
|
|
8741
|
-
this.openSession();
|
|
8932
|
+
this.openSession("bfcache_restore");
|
|
8742
8933
|
});
|
|
8743
8934
|
document.addEventListener("visibilitychange", () => {
|
|
8744
8935
|
if (document.visibilityState === "hidden") {
|
|
@@ -8801,6 +8992,7 @@ var DeepdotsPopups = class {
|
|
|
8801
8992
|
}
|
|
8802
8993
|
/** Lógica para evaluar condiciones antes de mostrar una encuesta */
|
|
8803
8994
|
triggerSurvey(surveyId, popupId) {
|
|
8995
|
+
if (this.bufferIfAwaitingPopups({ kind: "survey", surveyId, popupId })) return;
|
|
8804
8996
|
const def = this.findPopupDefinition(surveyId, popupId);
|
|
8805
8997
|
if (!def) {
|
|
8806
8998
|
this.debug("No popup definition for trigger", { surveyId, popupId });
|
|
@@ -8822,6 +9014,10 @@ var DeepdotsPopups = class {
|
|
|
8822
9014
|
this.debug("Ignoring empty event trigger name");
|
|
8823
9015
|
return;
|
|
8824
9016
|
}
|
|
9017
|
+
if (this.bufferIfAwaitingPopups({ kind: "event", name: normalized })) return;
|
|
9018
|
+
this.evaluateEvent(normalized);
|
|
9019
|
+
}
|
|
9020
|
+
evaluateEvent(normalized) {
|
|
8825
9021
|
const candidates = this.popupDefinitions.filter((def) => {
|
|
8826
9022
|
return def.triggers.some((trigger) => {
|
|
8827
9023
|
const triggerValue = String(trigger.value ?? "").trim();
|
|
@@ -8839,6 +9035,40 @@ var DeepdotsPopups = class {
|
|
|
8839
9035
|
}
|
|
8840
9036
|
this.showDefinition(matched);
|
|
8841
9037
|
}
|
|
9038
|
+
/**
|
|
9039
|
+
* Guarda `trigger` si las definiciones aún no han llegado. Devuelve `true` si lo guardó (el
|
|
9040
|
+
* llamador no debe evaluarlo ahora) y `false` si no hay carga en vuelo (se evalúa ya, como
|
|
9041
|
+
* siempre: tras cargar, tras un fallo o sin apiKey).
|
|
9042
|
+
*/
|
|
9043
|
+
bufferIfAwaitingPopups(trigger) {
|
|
9044
|
+
if (!this.awaitingPopups) return false;
|
|
9045
|
+
if (this.pendingTriggers.length >= MAX_PENDING_TRIGGERS) {
|
|
9046
|
+
const dropped = this.pendingTriggers.shift();
|
|
9047
|
+
this.debug("Pending trigger buffer full; dropped oldest", dropped);
|
|
9048
|
+
}
|
|
9049
|
+
this.pendingTriggers.push(trigger);
|
|
9050
|
+
this.debug("Popups not loaded yet; trigger buffered", trigger);
|
|
9051
|
+
return true;
|
|
9052
|
+
}
|
|
9053
|
+
/** La carga falló: sin definiciones no hay nada que reproducir, se descarta todo. */
|
|
9054
|
+
dropPendingTriggers() {
|
|
9055
|
+
const dropped = this.pendingTriggers.length;
|
|
9056
|
+
this.awaitingPopups = false;
|
|
9057
|
+
this.pendingTriggers = [];
|
|
9058
|
+
if (dropped > 0) this.debug("Popups failed to load; dropped pending triggers", dropped);
|
|
9059
|
+
}
|
|
9060
|
+
/** Reproduce en orden por la evaluación normal (segmentos, cooldowns y condiciones de AHORA). */
|
|
9061
|
+
replayPendingTriggers() {
|
|
9062
|
+
this.awaitingPopups = false;
|
|
9063
|
+
const replay = this.pendingTriggers;
|
|
9064
|
+
this.pendingTriggers = [];
|
|
9065
|
+
if (!replay.length) return;
|
|
9066
|
+
this.debug("Replaying triggers received before popups loaded", replay.length);
|
|
9067
|
+
replay.forEach((trigger) => {
|
|
9068
|
+
if (trigger.kind === "event") this.evaluateEvent(trigger.name);
|
|
9069
|
+
else this.triggerSurvey(trigger.surveyId, trigger.popupId);
|
|
9070
|
+
});
|
|
9071
|
+
}
|
|
8842
9072
|
showDefinition(def) {
|
|
8843
9073
|
this.log("Showing popup (definition)", def);
|
|
8844
9074
|
this.surveyToPopupId.set(def.surveyId, def.id);
|
|
@@ -9109,7 +9339,7 @@ var DeepdotsPopups = class {
|
|
|
9109
9339
|
} catch {
|
|
9110
9340
|
}
|
|
9111
9341
|
}
|
|
9112
|
-
/** Fetch al servidor para obtener popups */
|
|
9342
|
+
/** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */
|
|
9113
9343
|
async fetchPopupsFromServer() {
|
|
9114
9344
|
const apiKey = this.config?.apiKey;
|
|
9115
9345
|
const baseUrl = this.baseUrl;
|
|
@@ -9125,19 +9355,19 @@ var DeepdotsPopups = class {
|
|
|
9125
9355
|
const response = await fetch(endpoint);
|
|
9126
9356
|
if (!response.ok) {
|
|
9127
9357
|
this.log("Failed to fetch popups", response.status, response.statusText);
|
|
9128
|
-
return
|
|
9358
|
+
return null;
|
|
9129
9359
|
}
|
|
9130
9360
|
const raw = await response.text();
|
|
9131
9361
|
const parsed = JSON.parse(raw);
|
|
9132
9362
|
if (!Array.isArray(parsed)) {
|
|
9133
9363
|
this.log("Unexpected popups payload", parsed);
|
|
9134
|
-
return
|
|
9364
|
+
return null;
|
|
9135
9365
|
}
|
|
9136
9366
|
this.debug("Fetched popups payload", parsed);
|
|
9137
9367
|
return parsed;
|
|
9138
9368
|
} catch (error) {
|
|
9139
9369
|
this.log("Error fetching popups", error);
|
|
9140
|
-
return
|
|
9370
|
+
return null;
|
|
9141
9371
|
}
|
|
9142
9372
|
}
|
|
9143
9373
|
/** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */
|