@magicfeedback/popup-sdk 1.10.0 → 1.11.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 +57 -1
- package/dist/index.d.ts +57 -1
- package/dist/index.js +237 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +219 -11
- package/dist/index.mjs.map +1 -1
- package/dist/react-native.d.mts +52 -1
- package/dist/react-native.d.ts +52 -1
- package/dist/react-native.js +238 -14
- package/dist/react-native.js.map +1 -1
- package/dist/react-native.mjs +220 -11
- 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/react-native.d.mts
CHANGED
|
@@ -396,6 +396,14 @@ declare class DeepdotsPopups {
|
|
|
396
396
|
private popupContainer;
|
|
397
397
|
private popupDefinitions;
|
|
398
398
|
private popupsLoaded;
|
|
399
|
+
/**
|
|
400
|
+
* `true` mientras el `GET /sdk/{apiKey}/popups` de `init()` está en vuelo. En esa ventana los
|
|
401
|
+
* disparos del host (`triggerEvent`, `triggerSurvey`) no tienen contra qué evaluarse, así que
|
|
402
|
+
* se guardan en `pendingTriggers` y se reproducen al cargar. Click, scroll, time y exit no
|
|
403
|
+
* hacen falta: son listeners que `autoLaunch` instala ya con las definiciones cargadas.
|
|
404
|
+
*/
|
|
405
|
+
private awaitingPopups;
|
|
406
|
+
private pendingTriggers;
|
|
399
407
|
private pendingAutoLaunch;
|
|
400
408
|
private answeredSurveys;
|
|
401
409
|
private surveyProgress;
|
|
@@ -444,6 +452,13 @@ declare class DeepdotsPopups {
|
|
|
444
452
|
private lastEngagementAt;
|
|
445
453
|
/** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */
|
|
446
454
|
private hiddenAt;
|
|
455
|
+
/**
|
|
456
|
+
* Sesión suspendida por un `pagehide` (ms epoch y `deepdots_user_id`); null si no hay.
|
|
457
|
+
* `openSession` la reanuda si sigue a tiempo (ver ANALYTICS_RESUME_WINDOW_MS).
|
|
458
|
+
*/
|
|
459
|
+
private suspended;
|
|
460
|
+
/** Integración de analytics del init: la sesión suspendida solo la hereda la misma. */
|
|
461
|
+
private analyticsIntegration;
|
|
447
462
|
/**
|
|
448
463
|
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
449
464
|
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
@@ -586,6 +601,31 @@ declare class DeepdotsPopups {
|
|
|
586
601
|
* - Más: ya la cerró el backend → se descarta sin enviar nada.
|
|
587
602
|
*/
|
|
588
603
|
private onDocumentVisible;
|
|
604
|
+
/**
|
|
605
|
+
* `pagehide` en web: SUSPENDE la sesión en vez de cerrarla, porque el navegador lo lanza
|
|
606
|
+
* igual al recargar que al cerrar la pestaña. Vuelca lo pendiente (page_view de la
|
|
607
|
+
* pantalla, mini-services, engagement) con un `deepdots_session_suspend` y SIN
|
|
608
|
+
* `completed`, y deja el registro en `sessionStorage` (ver `suspended-session.ts`):
|
|
609
|
+
* - si la página vuelve a cargar en esta pestaña, `init()` sigue en el mismo registro;
|
|
610
|
+
* - si la pestaña se cerró, el backend lo cierra a los pocos minutos sin otro lote.
|
|
611
|
+
*
|
|
612
|
+
* Devuelve false cuando no se puede suspender — aún no se conoce el registro (el primer
|
|
613
|
+
* POST sigue en vuelo) o no hay `sessionStorage` — y entonces el caller cierra como antes.
|
|
614
|
+
*/
|
|
615
|
+
private suspendSession;
|
|
616
|
+
/**
|
|
617
|
+
* Recoge la sesión que dejó suspendida la carga anterior de esta pestaña (ver
|
|
618
|
+
* `suspendSession`). Solo con analytics configurado y el tracking activo: sin ellos no hay
|
|
619
|
+
* registro en el que seguir.
|
|
620
|
+
*/
|
|
621
|
+
private readResumableSession;
|
|
622
|
+
/**
|
|
623
|
+
* Consume la sesión suspendida al abrir: los ms que estuvo suspendida si se puede seguir en
|
|
624
|
+
* ella (mismo usuario, dentro de ANALYTICS_RESUME_WINDOW_MS); null si no había o ya no toca.
|
|
625
|
+
* En ese caso se olvida su registro sin enviar nada: lo cierra el backend, y la sesión que
|
|
626
|
+
* se abre ahora va a uno nuevo.
|
|
627
|
+
*/
|
|
628
|
+
private takeSuspendedSession;
|
|
589
629
|
/** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
|
|
590
630
|
private discardSession;
|
|
591
631
|
/**
|
|
@@ -611,6 +651,17 @@ declare class DeepdotsPopups {
|
|
|
611
651
|
triggerSurvey(surveyId: string, popupId?: string): void;
|
|
612
652
|
/** Trigger popups configured with trigger.type = 'event' and matching trigger.value */
|
|
613
653
|
triggerEvent(eventName: string): void;
|
|
654
|
+
private evaluateEvent;
|
|
655
|
+
/**
|
|
656
|
+
* Guarda `trigger` si las definiciones aún no han llegado. Devuelve `true` si lo guardó (el
|
|
657
|
+
* llamador no debe evaluarlo ahora) y `false` si no hay carga en vuelo (se evalúa ya, como
|
|
658
|
+
* siempre: tras cargar, tras un fallo o sin apiKey).
|
|
659
|
+
*/
|
|
660
|
+
private bufferIfAwaitingPopups;
|
|
661
|
+
/** La carga falló: sin definiciones no hay nada que reproducir, se descarta todo. */
|
|
662
|
+
private dropPendingTriggers;
|
|
663
|
+
/** Reproduce en orden por la evaluación normal (segmentos, cooldowns y condiciones de AHORA). */
|
|
664
|
+
private replayPendingTriggers;
|
|
614
665
|
private showDefinition;
|
|
615
666
|
private shouldShow;
|
|
616
667
|
private matchesSegmentsLang;
|
|
@@ -649,7 +700,7 @@ declare class DeepdotsPopups {
|
|
|
649
700
|
private removeDeferredExitPopup;
|
|
650
701
|
private getDeferredExitQueue;
|
|
651
702
|
private setDeferredExitQueue;
|
|
652
|
-
/** Fetch al servidor para obtener popups */
|
|
703
|
+
/** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */
|
|
653
704
|
private fetchPopupsFromServer;
|
|
654
705
|
/** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */
|
|
655
706
|
private postContact;
|
package/dist/react-native.d.ts
CHANGED
|
@@ -396,6 +396,14 @@ declare class DeepdotsPopups {
|
|
|
396
396
|
private popupContainer;
|
|
397
397
|
private popupDefinitions;
|
|
398
398
|
private popupsLoaded;
|
|
399
|
+
/**
|
|
400
|
+
* `true` mientras el `GET /sdk/{apiKey}/popups` de `init()` está en vuelo. En esa ventana los
|
|
401
|
+
* disparos del host (`triggerEvent`, `triggerSurvey`) no tienen contra qué evaluarse, así que
|
|
402
|
+
* se guardan en `pendingTriggers` y se reproducen al cargar. Click, scroll, time y exit no
|
|
403
|
+
* hacen falta: son listeners que `autoLaunch` instala ya con las definiciones cargadas.
|
|
404
|
+
*/
|
|
405
|
+
private awaitingPopups;
|
|
406
|
+
private pendingTriggers;
|
|
399
407
|
private pendingAutoLaunch;
|
|
400
408
|
private answeredSurveys;
|
|
401
409
|
private surveyProgress;
|
|
@@ -444,6 +452,13 @@ declare class DeepdotsPopups {
|
|
|
444
452
|
private lastEngagementAt;
|
|
445
453
|
/** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */
|
|
446
454
|
private hiddenAt;
|
|
455
|
+
/**
|
|
456
|
+
* Sesión suspendida por un `pagehide` (ms epoch y `deepdots_user_id`); null si no hay.
|
|
457
|
+
* `openSession` la reanuda si sigue a tiempo (ver ANALYTICS_RESUME_WINDOW_MS).
|
|
458
|
+
*/
|
|
459
|
+
private suspended;
|
|
460
|
+
/** Integración de analytics del init: la sesión suspendida solo la hereda la misma. */
|
|
461
|
+
private analyticsIntegration;
|
|
447
462
|
/**
|
|
448
463
|
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
449
464
|
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
@@ -586,6 +601,31 @@ declare class DeepdotsPopups {
|
|
|
586
601
|
* - Más: ya la cerró el backend → se descarta sin enviar nada.
|
|
587
602
|
*/
|
|
588
603
|
private onDocumentVisible;
|
|
604
|
+
/**
|
|
605
|
+
* `pagehide` en web: SUSPENDE la sesión en vez de cerrarla, porque el navegador lo lanza
|
|
606
|
+
* igual al recargar que al cerrar la pestaña. Vuelca lo pendiente (page_view de la
|
|
607
|
+
* pantalla, mini-services, engagement) con un `deepdots_session_suspend` y SIN
|
|
608
|
+
* `completed`, y deja el registro en `sessionStorage` (ver `suspended-session.ts`):
|
|
609
|
+
* - si la página vuelve a cargar en esta pestaña, `init()` sigue en el mismo registro;
|
|
610
|
+
* - si la pestaña se cerró, el backend lo cierra a los pocos minutos sin otro lote.
|
|
611
|
+
*
|
|
612
|
+
* Devuelve false cuando no se puede suspender — aún no se conoce el registro (el primer
|
|
613
|
+
* POST sigue en vuelo) o no hay `sessionStorage` — y entonces el caller cierra como antes.
|
|
614
|
+
*/
|
|
615
|
+
private suspendSession;
|
|
616
|
+
/**
|
|
617
|
+
* Recoge la sesión que dejó suspendida la carga anterior de esta pestaña (ver
|
|
618
|
+
* `suspendSession`). Solo con analytics configurado y el tracking activo: sin ellos no hay
|
|
619
|
+
* registro en el que seguir.
|
|
620
|
+
*/
|
|
621
|
+
private readResumableSession;
|
|
622
|
+
/**
|
|
623
|
+
* Consume la sesión suspendida al abrir: los ms que estuvo suspendida si se puede seguir en
|
|
624
|
+
* ella (mismo usuario, dentro de ANALYTICS_RESUME_WINDOW_MS); null si no había o ya no toca.
|
|
625
|
+
* En ese caso se olvida su registro sin enviar nada: lo cierra el backend, y la sesión que
|
|
626
|
+
* se abre ahora va a uno nuevo.
|
|
627
|
+
*/
|
|
628
|
+
private takeSuspendedSession;
|
|
589
629
|
/** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
|
|
590
630
|
private discardSession;
|
|
591
631
|
/**
|
|
@@ -611,6 +651,17 @@ declare class DeepdotsPopups {
|
|
|
611
651
|
triggerSurvey(surveyId: string, popupId?: string): void;
|
|
612
652
|
/** Trigger popups configured with trigger.type = 'event' and matching trigger.value */
|
|
613
653
|
triggerEvent(eventName: string): void;
|
|
654
|
+
private evaluateEvent;
|
|
655
|
+
/**
|
|
656
|
+
* Guarda `trigger` si las definiciones aún no han llegado. Devuelve `true` si lo guardó (el
|
|
657
|
+
* llamador no debe evaluarlo ahora) y `false` si no hay carga en vuelo (se evalúa ya, como
|
|
658
|
+
* siempre: tras cargar, tras un fallo o sin apiKey).
|
|
659
|
+
*/
|
|
660
|
+
private bufferIfAwaitingPopups;
|
|
661
|
+
/** La carga falló: sin definiciones no hay nada que reproducir, se descarta todo. */
|
|
662
|
+
private dropPendingTriggers;
|
|
663
|
+
/** Reproduce en orden por la evaluación normal (segmentos, cooldowns y condiciones de AHORA). */
|
|
664
|
+
private replayPendingTriggers;
|
|
614
665
|
private showDefinition;
|
|
615
666
|
private shouldShow;
|
|
616
667
|
private matchesSegmentsLang;
|
|
@@ -649,7 +700,7 @@ declare class DeepdotsPopups {
|
|
|
649
700
|
private removeDeferredExitPopup;
|
|
650
701
|
private getDeferredExitQueue;
|
|
651
702
|
private setDeferredExitQueue;
|
|
652
|
-
/** Fetch al servidor para obtener popups */
|
|
703
|
+
/** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */
|
|
653
704
|
private fetchPopupsFromServer;
|
|
654
705
|
/** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */
|
|
655
706
|
private postContact;
|
package/dist/react-native.js
CHANGED
|
@@ -2894,6 +2894,9 @@ function ensureFontFace(family, url) {
|
|
|
2894
2894
|
}
|
|
2895
2895
|
if (el.textContent !== css) el.textContent = css;
|
|
2896
2896
|
}
|
|
2897
|
+
function hasFinePointer() {
|
|
2898
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(pointer: fine)").matches;
|
|
2899
|
+
}
|
|
2897
2900
|
async function renderPopup(container, surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId, options) {
|
|
2898
2901
|
let surveyCompletedEmitted = false;
|
|
2899
2902
|
let stylesInjected = false;
|
|
@@ -3324,6 +3327,13 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
3324
3327
|
if (pos.padding) container.style.padding = pos.padding;
|
|
3325
3328
|
let revealed = false;
|
|
3326
3329
|
let revealTimer = null;
|
|
3330
|
+
let surveyLoaded = false;
|
|
3331
|
+
let openFocusDone = false;
|
|
3332
|
+
function autofocusOnOpen() {
|
|
3333
|
+
if (openFocusDone || !revealed || !surveyLoaded) return;
|
|
3334
|
+
openFocusDone = true;
|
|
3335
|
+
if (hasFinePointer()) formInstance?.focusFirstQuestion?.();
|
|
3336
|
+
}
|
|
3327
3337
|
function reveal() {
|
|
3328
3338
|
if (revealed) return;
|
|
3329
3339
|
revealed = true;
|
|
@@ -3334,6 +3344,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
3334
3344
|
if (!container.contains(popup)) return;
|
|
3335
3345
|
container.style.visibility = "visible";
|
|
3336
3346
|
container.style.pointerEvents = "";
|
|
3347
|
+
autofocusOnOpen();
|
|
3337
3348
|
}
|
|
3338
3349
|
function revealWhenPainted2() {
|
|
3339
3350
|
if (revealed) return;
|
|
@@ -3386,7 +3397,11 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
3386
3397
|
addSuccessScreen: false,
|
|
3387
3398
|
// Surveys multi-idioma: native pide a la API este idioma (el del init o el del
|
|
3388
3399
|
// dispositivo) y, si el survey no lo tiene, la API sirve el idioma por defecto.
|
|
3389
|
-
lang: options?.language
|
|
3400
|
+
lang: options?.language,
|
|
3401
|
+
// Foco en la primera pregunta de escribir tras Start, Siguiente o Atrás. Native lo
|
|
3402
|
+
// aplica después de afterSubmitEvent/onBackEvent, cuando `setLoading(false)` ya ha
|
|
3403
|
+
// quitado el `inert`. La apertura va aparte (ver autofocusOnOpen).
|
|
3404
|
+
autofocus: "navigation"
|
|
3390
3405
|
};
|
|
3391
3406
|
generateOptions.onLoadedEvent = ({ formData, lang }) => {
|
|
3392
3407
|
const shownLang = typeof lang === "string" && lang.trim().length > 0 ? lang : void 0;
|
|
@@ -3441,12 +3456,14 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
3441
3456
|
updateButtons("in_progress_first");
|
|
3442
3457
|
}
|
|
3443
3458
|
updateProgress({ progress: formInstance.progress, total: formInstance.total });
|
|
3444
|
-
emit("popup_clicked", surveyId, { action: "loaded" });
|
|
3445
3459
|
setLoading(false);
|
|
3460
|
+
if (!surveyLoaded) {
|
|
3461
|
+
surveyLoaded = true;
|
|
3462
|
+
autofocusOnOpen();
|
|
3463
|
+
}
|
|
3446
3464
|
};
|
|
3447
3465
|
generateOptions.beforeSubmitEvent = () => {
|
|
3448
3466
|
setLoading(true);
|
|
3449
|
-
emit("popup_clicked", surveyId, { action: "before_submit" });
|
|
3450
3467
|
};
|
|
3451
3468
|
generateOptions.afterSubmitEvent = ({ error, completed, total, progress, followup }) => {
|
|
3452
3469
|
const errText = error ? typeof error === "string" ? error : error.message ?? String(error) : "";
|
|
@@ -3455,13 +3472,12 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
3455
3472
|
if (errText.toLowerCase().includes("no response")) {
|
|
3456
3473
|
errorHint.textContent = labels.errorRequired;
|
|
3457
3474
|
errorHint.style.display = "block";
|
|
3458
|
-
emit("popup_clicked", surveyId, { action: "validation_error_required" });
|
|
3459
3475
|
updateNavButtons();
|
|
3460
3476
|
return;
|
|
3461
3477
|
}
|
|
3462
3478
|
errorHint.textContent = labels.errorSubmit;
|
|
3463
3479
|
errorHint.style.display = "block";
|
|
3464
|
-
|
|
3480
|
+
sdkWarn("[Deepdots] survey submit error:", errText);
|
|
3465
3481
|
return;
|
|
3466
3482
|
}
|
|
3467
3483
|
errorHint.style.display = "none";
|
|
@@ -3480,7 +3496,6 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
3480
3496
|
updateProgress({ progress, total, followup });
|
|
3481
3497
|
};
|
|
3482
3498
|
generateOptions.onBackEvent = ({ progress, error, followup }) => {
|
|
3483
|
-
emit("popup_clicked", surveyId, { action: "back" });
|
|
3484
3499
|
if (!error) pageDepth = Math.max(0, pageDepth - 1);
|
|
3485
3500
|
updateNavButtons();
|
|
3486
3501
|
updateProgress({ progress, total: formInstance.total, followup });
|
|
@@ -4104,11 +4119,25 @@ ${customCss}
|
|
|
4104
4119
|
${PRIMARY_COLOR_JS}
|
|
4105
4120
|
${BUSY_JS}
|
|
4106
4121
|
${REVEAL_JS}
|
|
4122
|
+
// Foco en la primera pregunta si es de escribir. Al navegar lo pone @magicfeedback/native
|
|
4123
|
+
// (autofocus:'navigation'); la apertura la decide el popup, que es quien sabe cu\xE1ndo se revela.
|
|
4124
|
+
// Necesita el popup revelado y el survey cargado, que llegan en cualquier orden, y solo con
|
|
4125
|
+
// rat\xF3n o trackpad: en t\xE1ctil sacar\xEDa el teclado sin que el usuario haya tocado nada.
|
|
4126
|
+
var ddRevealed=false, ddSurveyLoaded=false, ddOpenFocusDone=false;
|
|
4127
|
+
function ddAutofocusOnOpen(){
|
|
4128
|
+
if(ddOpenFocusDone || !ddRevealed || !ddSurveyLoaded){ return; }
|
|
4129
|
+
ddOpenFocusDone=true;
|
|
4130
|
+
var fine=typeof window.matchMedia==='function' && window.matchMedia('(pointer: fine)').matches;
|
|
4131
|
+
var f=window.DeepdotsForm;
|
|
4132
|
+
if(fine && f && typeof f.focusFirstQuestion==='function'){ f.focusFirstQuestion(); }
|
|
4133
|
+
}
|
|
4107
4134
|
// El popup se ense\xF1a cuando el survey est\xE1 pintado; \`ready\` deja al host abrir su propio
|
|
4108
4135
|
// Modal en ese momento en vez de montar un WebView en blanco.
|
|
4109
4136
|
var ddReveal=ddCreateReveal(popup, function(){
|
|
4110
4137
|
document.body.classList.add('dd-ready');
|
|
4111
4138
|
emitJSON('ready');
|
|
4139
|
+
ddRevealed=true;
|
|
4140
|
+
ddAutofocusOnOpen();
|
|
4112
4141
|
}, ${REVEAL_TIMEOUT_MS});
|
|
4113
4142
|
|
|
4114
4143
|
function setLoading(isLoading){
|
|
@@ -4149,6 +4178,9 @@ ${REVEAL_JS}
|
|
|
4149
4178
|
// Surveys multi-idioma: native pide a la API este idioma (el del init o el del
|
|
4150
4179
|
// dispositivo); si el survey no lo tiene, la API sirve el idioma por defecto.
|
|
4151
4180
|
lang:${initLangJson}||undefined,
|
|
4181
|
+
// Foco en la primera pregunta de escribir tras Start, Siguiente o Atr\xE1s. Las versiones
|
|
4182
|
+
// de native sin esta opci\xF3n la ignoran.
|
|
4183
|
+
autofocus:'navigation',
|
|
4152
4184
|
onLoadedEvent:function(args){
|
|
4153
4185
|
// Idioma en el que native muestra el survey: manda sobre el del init para que el
|
|
4154
4186
|
// chrome no se quede en ingl\xE9s delante de un survey traducido. Native < 2.3 no lo
|
|
@@ -4208,6 +4240,8 @@ ${REVEAL_JS}
|
|
|
4208
4240
|
updateProgress({ progress:form.progress, total:form.total, completed:false, followup:false });
|
|
4209
4241
|
emitJSON('loaded');
|
|
4210
4242
|
setLoading(false);
|
|
4243
|
+
// La primera carga es la apertura; las siguientes (tras Start) las enfoca native.
|
|
4244
|
+
if(!ddSurveyLoaded){ ddSurveyLoaded=true; ddAutofocusOnOpen(); }
|
|
4211
4245
|
},
|
|
4212
4246
|
beforeSubmitEvent:function(){
|
|
4213
4247
|
setLoading(true);
|
|
@@ -4360,6 +4394,7 @@ var ReactNativePopupRenderer = class {
|
|
|
4360
4394
|
this.emitReady(surveyId);
|
|
4361
4395
|
break;
|
|
4362
4396
|
case "loaded":
|
|
4397
|
+
break;
|
|
4363
4398
|
case "before_submit":
|
|
4364
4399
|
case "after_submit":
|
|
4365
4400
|
case "back":
|
|
@@ -4738,7 +4773,7 @@ async function safeBodyText(res) {
|
|
|
4738
4773
|
}
|
|
4739
4774
|
}
|
|
4740
4775
|
function createFeedbackSink(options) {
|
|
4741
|
-
let feedbackSessionId;
|
|
4776
|
+
let feedbackSessionId = options.initialSessionId || void 0;
|
|
4742
4777
|
let firstPostInFlight = null;
|
|
4743
4778
|
let generation = 0;
|
|
4744
4779
|
const url = `${options.baseUrl}/sdk/feedback`;
|
|
@@ -4831,6 +4866,50 @@ function createFeedbackSink(options) {
|
|
|
4831
4866
|
};
|
|
4832
4867
|
}
|
|
4833
4868
|
|
|
4869
|
+
// src/analytics/suspended-session.ts
|
|
4870
|
+
var SUSPENDED_SESSION_KEY = "deepdots.analytics.suspended";
|
|
4871
|
+
function getSessionStorage() {
|
|
4872
|
+
try {
|
|
4873
|
+
return typeof window !== "undefined" && window.sessionStorage ? window.sessionStorage : null;
|
|
4874
|
+
} catch {
|
|
4875
|
+
return null;
|
|
4876
|
+
}
|
|
4877
|
+
}
|
|
4878
|
+
function writeSuspendedSession(storage, session) {
|
|
4879
|
+
try {
|
|
4880
|
+
storage.setItem(SUSPENDED_SESSION_KEY, JSON.stringify(session));
|
|
4881
|
+
return true;
|
|
4882
|
+
} catch {
|
|
4883
|
+
return false;
|
|
4884
|
+
}
|
|
4885
|
+
}
|
|
4886
|
+
function clearSuspendedSession(storage) {
|
|
4887
|
+
try {
|
|
4888
|
+
storage.removeItem(SUSPENDED_SESSION_KEY);
|
|
4889
|
+
} catch {
|
|
4890
|
+
}
|
|
4891
|
+
}
|
|
4892
|
+
function takeSuspendedSession(storage, match, now, windowMs) {
|
|
4893
|
+
let raw = null;
|
|
4894
|
+
try {
|
|
4895
|
+
raw = storage.getItem(SUSPENDED_SESSION_KEY);
|
|
4896
|
+
} catch {
|
|
4897
|
+
return null;
|
|
4898
|
+
}
|
|
4899
|
+
if (!raw) return null;
|
|
4900
|
+
clearSuspendedSession(storage);
|
|
4901
|
+
try {
|
|
4902
|
+
const s = JSON.parse(raw);
|
|
4903
|
+
if (typeof s.sessionId !== "string" || !s.sessionId) return null;
|
|
4904
|
+
if (typeof s.at !== "number" || now - s.at < 0 || now - s.at > windowMs) return null;
|
|
4905
|
+
if (s.integration !== match.integration) return null;
|
|
4906
|
+
if ((s.userId ?? null) !== match.userId) return null;
|
|
4907
|
+
return { sessionId: s.sessionId, integration: s.integration, userId: s.userId ?? null, at: s.at };
|
|
4908
|
+
} catch {
|
|
4909
|
+
return null;
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
|
|
4834
4913
|
// src/tracking/navigation-observer.ts
|
|
4835
4914
|
var UUID_PREFIX = /^[0-9a-f]{8}-[0-9a-f]{4}-/i;
|
|
4836
4915
|
function screenPath(href) {
|
|
@@ -5429,8 +5508,10 @@ var ANALYTICS_HEARTBEAT_MS = 5 * 6e4;
|
|
|
5429
5508
|
var ANALYTICS_SESSION_TIMEOUT_MS = 30 * 6e4;
|
|
5430
5509
|
var ANALYTICS_SERVER_IDLE_MS = 60 * 6e4;
|
|
5431
5510
|
var ANALYTICS_SESSION_DISCARD_MS = ANALYTICS_SERVER_IDLE_MS - 10 * 6e4;
|
|
5511
|
+
var ANALYTICS_RESUME_WINDOW_MS = 6e4;
|
|
5432
5512
|
var RESERVED_EVENT_PREFIX = "deepdots_";
|
|
5433
5513
|
var CUSTOM_EVENT_PREFIX = "deepdots_event_";
|
|
5514
|
+
var MAX_PENDING_TRIGGERS = 20;
|
|
5434
5515
|
var DeepdotsPopups = class {
|
|
5435
5516
|
constructor() {
|
|
5436
5517
|
this.config = null;
|
|
@@ -5444,6 +5525,14 @@ var DeepdotsPopups = class {
|
|
|
5444
5525
|
// deprecated: mantenido para compatibilidad interna
|
|
5445
5526
|
this.popupDefinitions = [];
|
|
5446
5527
|
this.popupsLoaded = false;
|
|
5528
|
+
/**
|
|
5529
|
+
* `true` mientras el `GET /sdk/{apiKey}/popups` de `init()` está en vuelo. En esa ventana los
|
|
5530
|
+
* disparos del host (`triggerEvent`, `triggerSurvey`) no tienen contra qué evaluarse, así que
|
|
5531
|
+
* se guardan en `pendingTriggers` y se reproducen al cargar. Click, scroll, time y exit no
|
|
5532
|
+
* hacen falta: son listeners que `autoLaunch` instala ya con las definiciones cargadas.
|
|
5533
|
+
*/
|
|
5534
|
+
this.awaitingPopups = false;
|
|
5535
|
+
this.pendingTriggers = [];
|
|
5447
5536
|
this.pendingAutoLaunch = false;
|
|
5448
5537
|
// Estado que alimenta las reglas de redisplay. Copia de trabajo en memoria; la fuente
|
|
5449
5538
|
// duradera es el storage (`popup-state-store`), rehidratada en `init()` — sin eso, una
|
|
@@ -5498,6 +5587,13 @@ var DeepdotsPopups = class {
|
|
|
5498
5587
|
this.lastEngagementAt = 0;
|
|
5499
5588
|
/** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */
|
|
5500
5589
|
this.hiddenAt = null;
|
|
5590
|
+
/**
|
|
5591
|
+
* Sesión suspendida por un `pagehide` (ms epoch y `deepdots_user_id`); null si no hay.
|
|
5592
|
+
* `openSession` la reanuda si sigue a tiempo (ver ANALYTICS_RESUME_WINDOW_MS).
|
|
5593
|
+
*/
|
|
5594
|
+
this.suspended = null;
|
|
5595
|
+
/** Integración de analytics del init: la sesión suspendida solo la hereda la misma. */
|
|
5596
|
+
this.analyticsIntegration = void 0;
|
|
5501
5597
|
/**
|
|
5502
5598
|
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
5503
5599
|
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
@@ -5557,10 +5653,17 @@ var DeepdotsPopups = class {
|
|
|
5557
5653
|
}
|
|
5558
5654
|
}
|
|
5559
5655
|
this.log("tracking \xB7 user_id:", this.tracking.getUserId(), "\xB7 new_user:", this.tracking.isNewUser(), "\xB7 enabled:", this.tracking.isTrackingEnabled());
|
|
5656
|
+
this.analyticsIntegration = config.analytics?.integration;
|
|
5657
|
+
const resumable = this.readResumableSession();
|
|
5658
|
+
if (resumable) {
|
|
5659
|
+
this.analyticsFeedbackSessionId = resumable.sessionId;
|
|
5660
|
+
this.suspended = { at: resumable.at, userId: resumable.userId };
|
|
5661
|
+
}
|
|
5560
5662
|
const analyticsSink = config.analytics ? createFeedbackSink({
|
|
5561
5663
|
baseUrl: this.baseUrl,
|
|
5562
5664
|
keys: config.analytics,
|
|
5563
5665
|
log: (...a) => this.log(...a),
|
|
5666
|
+
initialSessionId: resumable?.sessionId,
|
|
5564
5667
|
onSessionId: (id) => {
|
|
5565
5668
|
this.analyticsFeedbackSessionId = id;
|
|
5566
5669
|
this.notifyFeedbackSession(id, "open");
|
|
@@ -5610,6 +5713,9 @@ var DeepdotsPopups = class {
|
|
|
5610
5713
|
this.engagement = new EngagementTracker();
|
|
5611
5714
|
this.setupAnalyticsFlush();
|
|
5612
5715
|
this.openSession();
|
|
5716
|
+
if (resumable && this.analyticsFeedbackSessionId === resumable.sessionId) {
|
|
5717
|
+
this.notifyFeedbackSession(resumable.sessionId, "open");
|
|
5718
|
+
}
|
|
5613
5719
|
const pendingCrashes = this.crashReporter.drainPendingCrashes();
|
|
5614
5720
|
if (this.tracking?.isTrackingEnabled()) {
|
|
5615
5721
|
for (const rec of pendingCrashes) {
|
|
@@ -5620,8 +5726,10 @@ var DeepdotsPopups = class {
|
|
|
5620
5726
|
this.log("SDK initialized", this.config);
|
|
5621
5727
|
if (this.renderer.init) this.renderer.init();
|
|
5622
5728
|
this.setupPopupContainer();
|
|
5729
|
+
this.awaitingPopups = Boolean(this.config.apiKey && this.baseUrl);
|
|
5623
5730
|
this.fetchPopupsFromServer().then((defs) => {
|
|
5624
|
-
|
|
5731
|
+
if (!defs) this.dropPendingTriggers();
|
|
5732
|
+
this.popupDefinitions = this.validatePopupDefinitions(defs ?? []);
|
|
5625
5733
|
this.popupsLoaded = true;
|
|
5626
5734
|
this.log("Popups loaded from API", this.popupDefinitions);
|
|
5627
5735
|
if (this.popupDefinitions.length) this.renderer.preload?.();
|
|
@@ -5630,6 +5738,7 @@ var DeepdotsPopups = class {
|
|
|
5630
5738
|
if (this.pendingAutoLaunch) {
|
|
5631
5739
|
this.startTriggers();
|
|
5632
5740
|
}
|
|
5741
|
+
this.replayPendingTriggers();
|
|
5633
5742
|
});
|
|
5634
5743
|
}
|
|
5635
5744
|
/** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
|
|
@@ -5858,9 +5967,15 @@ var DeepdotsPopups = class {
|
|
|
5858
5967
|
openSession() {
|
|
5859
5968
|
if (this.sessionOpen) return;
|
|
5860
5969
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
5970
|
+
const suspendedMs = this.takeSuspendedSession();
|
|
5861
5971
|
this.sessionOpen = true;
|
|
5862
5972
|
this.lastEngagementAt = Date.now();
|
|
5863
|
-
|
|
5973
|
+
if (suspendedMs === null) {
|
|
5974
|
+
this.track("deepdots_session_start", {});
|
|
5975
|
+
} else {
|
|
5976
|
+
this.track("deepdots_session_resume", { suspended_ms: suspendedMs });
|
|
5977
|
+
this.log("tracking \xB7 session resumed after", suspendedMs, "ms:", this.analyticsFeedbackSessionId);
|
|
5978
|
+
}
|
|
5864
5979
|
this.navObserver?.restart();
|
|
5865
5980
|
if (this.isDocumentVisible()) this.engagement?.resume();
|
|
5866
5981
|
}
|
|
@@ -5925,6 +6040,75 @@ var DeepdotsPopups = class {
|
|
|
5925
6040
|
}
|
|
5926
6041
|
this.openSession();
|
|
5927
6042
|
}
|
|
6043
|
+
/**
|
|
6044
|
+
* `pagehide` en web: SUSPENDE la sesión en vez de cerrarla, porque el navegador lo lanza
|
|
6045
|
+
* igual al recargar que al cerrar la pestaña. Vuelca lo pendiente (page_view de la
|
|
6046
|
+
* pantalla, mini-services, engagement) con un `deepdots_session_suspend` y SIN
|
|
6047
|
+
* `completed`, y deja el registro en `sessionStorage` (ver `suspended-session.ts`):
|
|
6048
|
+
* - si la página vuelve a cargar en esta pestaña, `init()` sigue en el mismo registro;
|
|
6049
|
+
* - si la pestaña se cerró, el backend lo cierra a los pocos minutos sin otro lote.
|
|
6050
|
+
*
|
|
6051
|
+
* Devuelve false cuando no se puede suspender — aún no se conoce el registro (el primer
|
|
6052
|
+
* POST sigue en vuelo) o no hay `sessionStorage` — y entonces el caller cierra como antes.
|
|
6053
|
+
*/
|
|
6054
|
+
suspendSession() {
|
|
6055
|
+
if (!this.sessionOpen || !this.tracking?.isTrackingEnabled()) return false;
|
|
6056
|
+
const sessionId = this.analyticsFeedbackSessionId;
|
|
6057
|
+
const integration = this.analyticsIntegration;
|
|
6058
|
+
const storage = getSessionStorage();
|
|
6059
|
+
if (!sessionId || !integration || !storage) return false;
|
|
6060
|
+
const at = Date.now();
|
|
6061
|
+
const userId = this.tracking.getUserId();
|
|
6062
|
+
if (!writeSuspendedSession(storage, { sessionId, integration, userId, at })) return false;
|
|
6063
|
+
this.sessionOpen = false;
|
|
6064
|
+
this.suspended = { at, userId };
|
|
6065
|
+
this.navObserver?.stop();
|
|
6066
|
+
this.navStarted = false;
|
|
6067
|
+
this.analytics?.exitAllMiniServices();
|
|
6068
|
+
this.flushEngagement();
|
|
6069
|
+
this.engagement?.pause();
|
|
6070
|
+
this.track("deepdots_session_suspend", { reason: "page_hide" });
|
|
6071
|
+
this.flushAnalytics({ final: true });
|
|
6072
|
+
this.log("tracking \xB7 session suspended:", sessionId);
|
|
6073
|
+
return true;
|
|
6074
|
+
}
|
|
6075
|
+
/**
|
|
6076
|
+
* Recoge la sesión que dejó suspendida la carga anterior de esta pestaña (ver
|
|
6077
|
+
* `suspendSession`). Solo con analytics configurado y el tracking activo: sin ellos no hay
|
|
6078
|
+
* registro en el que seguir.
|
|
6079
|
+
*/
|
|
6080
|
+
readResumableSession() {
|
|
6081
|
+
const integration = this.analyticsIntegration;
|
|
6082
|
+
const storage = getSessionStorage();
|
|
6083
|
+
if (!integration || !storage || !this.tracking?.isTrackingEnabled()) return null;
|
|
6084
|
+
return takeSuspendedSession(
|
|
6085
|
+
storage,
|
|
6086
|
+
{ integration, userId: this.tracking.getUserId() },
|
|
6087
|
+
Date.now(),
|
|
6088
|
+
ANALYTICS_RESUME_WINDOW_MS
|
|
6089
|
+
);
|
|
6090
|
+
}
|
|
6091
|
+
/**
|
|
6092
|
+
* Consume la sesión suspendida al abrir: los ms que estuvo suspendida si se puede seguir en
|
|
6093
|
+
* ella (mismo usuario, dentro de ANALYTICS_RESUME_WINDOW_MS); null si no había o ya no toca.
|
|
6094
|
+
* En ese caso se olvida su registro sin enviar nada: lo cierra el backend, y la sesión que
|
|
6095
|
+
* se abre ahora va a uno nuevo.
|
|
6096
|
+
*/
|
|
6097
|
+
takeSuspendedSession() {
|
|
6098
|
+
const suspended = this.suspended;
|
|
6099
|
+
if (!suspended) return null;
|
|
6100
|
+
this.suspended = null;
|
|
6101
|
+
const storage = getSessionStorage();
|
|
6102
|
+
if (storage) clearSuspendedSession(storage);
|
|
6103
|
+
const suspendedMs = Math.max(0, Date.now() - suspended.at);
|
|
6104
|
+
if (this.analyticsFeedbackSessionId && suspendedMs <= ANALYTICS_RESUME_WINDOW_MS && suspended.userId === (this.tracking?.getUserId() ?? null)) {
|
|
6105
|
+
return suspendedMs;
|
|
6106
|
+
}
|
|
6107
|
+
this.analytics?.discardSession(this.analyticsIdentity());
|
|
6108
|
+
this.analyticsFeedbackSessionId = void 0;
|
|
6109
|
+
this.tracking?.setSessionId(null);
|
|
6110
|
+
return null;
|
|
6111
|
+
}
|
|
5928
6112
|
/** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
|
|
5929
6113
|
discardSession() {
|
|
5930
6114
|
this.sessionOpen = false;
|
|
@@ -5973,7 +6157,7 @@ var DeepdotsPopups = class {
|
|
|
5973
6157
|
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
5974
6158
|
window.addEventListener("pagehide", () => {
|
|
5975
6159
|
clearInterval(this.analyticsFlushTimer);
|
|
5976
|
-
this.closeSession("page_hide");
|
|
6160
|
+
if (!this.suspendSession()) this.closeSession("page_hide");
|
|
5977
6161
|
});
|
|
5978
6162
|
window.addEventListener("pageshow", (event) => {
|
|
5979
6163
|
if (!event.persisted) return;
|
|
@@ -6041,6 +6225,7 @@ var DeepdotsPopups = class {
|
|
|
6041
6225
|
}
|
|
6042
6226
|
/** Lógica para evaluar condiciones antes de mostrar una encuesta */
|
|
6043
6227
|
triggerSurvey(surveyId, popupId) {
|
|
6228
|
+
if (this.bufferIfAwaitingPopups({ kind: "survey", surveyId, popupId })) return;
|
|
6044
6229
|
const def = this.findPopupDefinition(surveyId, popupId);
|
|
6045
6230
|
if (!def) {
|
|
6046
6231
|
this.debug("No popup definition for trigger", { surveyId, popupId });
|
|
@@ -6062,6 +6247,10 @@ var DeepdotsPopups = class {
|
|
|
6062
6247
|
this.debug("Ignoring empty event trigger name");
|
|
6063
6248
|
return;
|
|
6064
6249
|
}
|
|
6250
|
+
if (this.bufferIfAwaitingPopups({ kind: "event", name: normalized })) return;
|
|
6251
|
+
this.evaluateEvent(normalized);
|
|
6252
|
+
}
|
|
6253
|
+
evaluateEvent(normalized) {
|
|
6065
6254
|
const candidates = this.popupDefinitions.filter((def) => {
|
|
6066
6255
|
return def.triggers.some((trigger) => {
|
|
6067
6256
|
const triggerValue = String(trigger.value ?? "").trim();
|
|
@@ -6079,6 +6268,40 @@ var DeepdotsPopups = class {
|
|
|
6079
6268
|
}
|
|
6080
6269
|
this.showDefinition(matched);
|
|
6081
6270
|
}
|
|
6271
|
+
/**
|
|
6272
|
+
* Guarda `trigger` si las definiciones aún no han llegado. Devuelve `true` si lo guardó (el
|
|
6273
|
+
* llamador no debe evaluarlo ahora) y `false` si no hay carga en vuelo (se evalúa ya, como
|
|
6274
|
+
* siempre: tras cargar, tras un fallo o sin apiKey).
|
|
6275
|
+
*/
|
|
6276
|
+
bufferIfAwaitingPopups(trigger) {
|
|
6277
|
+
if (!this.awaitingPopups) return false;
|
|
6278
|
+
if (this.pendingTriggers.length >= MAX_PENDING_TRIGGERS) {
|
|
6279
|
+
const dropped = this.pendingTriggers.shift();
|
|
6280
|
+
this.debug("Pending trigger buffer full; dropped oldest", dropped);
|
|
6281
|
+
}
|
|
6282
|
+
this.pendingTriggers.push(trigger);
|
|
6283
|
+
this.debug("Popups not loaded yet; trigger buffered", trigger);
|
|
6284
|
+
return true;
|
|
6285
|
+
}
|
|
6286
|
+
/** La carga falló: sin definiciones no hay nada que reproducir, se descarta todo. */
|
|
6287
|
+
dropPendingTriggers() {
|
|
6288
|
+
const dropped = this.pendingTriggers.length;
|
|
6289
|
+
this.awaitingPopups = false;
|
|
6290
|
+
this.pendingTriggers = [];
|
|
6291
|
+
if (dropped > 0) this.debug("Popups failed to load; dropped pending triggers", dropped);
|
|
6292
|
+
}
|
|
6293
|
+
/** Reproduce en orden por la evaluación normal (segmentos, cooldowns y condiciones de AHORA). */
|
|
6294
|
+
replayPendingTriggers() {
|
|
6295
|
+
this.awaitingPopups = false;
|
|
6296
|
+
const replay = this.pendingTriggers;
|
|
6297
|
+
this.pendingTriggers = [];
|
|
6298
|
+
if (!replay.length) return;
|
|
6299
|
+
this.debug("Replaying triggers received before popups loaded", replay.length);
|
|
6300
|
+
replay.forEach((trigger) => {
|
|
6301
|
+
if (trigger.kind === "event") this.evaluateEvent(trigger.name);
|
|
6302
|
+
else this.triggerSurvey(trigger.surveyId, trigger.popupId);
|
|
6303
|
+
});
|
|
6304
|
+
}
|
|
6082
6305
|
showDefinition(def) {
|
|
6083
6306
|
this.log("Showing popup (definition)", def);
|
|
6084
6307
|
this.surveyToPopupId.set(def.surveyId, def.id);
|
|
@@ -6349,7 +6572,7 @@ var DeepdotsPopups = class {
|
|
|
6349
6572
|
} catch {
|
|
6350
6573
|
}
|
|
6351
6574
|
}
|
|
6352
|
-
/** Fetch al servidor para obtener popups */
|
|
6575
|
+
/** Fetch al servidor para obtener popups. `null` si la carga falló (red, no-2xx, payload inválido). */
|
|
6353
6576
|
async fetchPopupsFromServer() {
|
|
6354
6577
|
const apiKey = this.config?.apiKey;
|
|
6355
6578
|
const baseUrl = this.baseUrl;
|
|
@@ -6365,19 +6588,19 @@ var DeepdotsPopups = class {
|
|
|
6365
6588
|
const response = await fetch(endpoint);
|
|
6366
6589
|
if (!response.ok) {
|
|
6367
6590
|
this.log("Failed to fetch popups", response.status, response.statusText);
|
|
6368
|
-
return
|
|
6591
|
+
return null;
|
|
6369
6592
|
}
|
|
6370
6593
|
const raw = await response.text();
|
|
6371
6594
|
const parsed = JSON.parse(raw);
|
|
6372
6595
|
if (!Array.isArray(parsed)) {
|
|
6373
6596
|
this.log("Unexpected popups payload", parsed);
|
|
6374
|
-
return
|
|
6597
|
+
return null;
|
|
6375
6598
|
}
|
|
6376
6599
|
this.debug("Fetched popups payload", parsed);
|
|
6377
6600
|
return parsed;
|
|
6378
6601
|
} catch (error) {
|
|
6379
6602
|
this.log("Error fetching popups", error);
|
|
6380
|
-
return
|
|
6603
|
+
return null;
|
|
6381
6604
|
}
|
|
6382
6605
|
}
|
|
6383
6606
|
/** Envía los atributos de contact a la API (`POST /sdk/popups/contact`). */
|
|
@@ -6801,6 +7024,7 @@ function DeepdotsProvider({ config, children }) {
|
|
|
6801
7024
|
style: { flex: 1 },
|
|
6802
7025
|
originWhitelist: ["*"],
|
|
6803
7026
|
javaScriptEnabled: true,
|
|
7027
|
+
keyboardDisplayRequiresUserAction: false,
|
|
6804
7028
|
source: { html: survey.html, baseUrl: "https://sdk.deepdots.com/" },
|
|
6805
7029
|
onMessage: (e) => rendererRef.current?.handleMessage(e.nativeEvent.data)
|
|
6806
7030
|
}
|