@magicfeedback/popup-sdk 1.8.1 → 1.8.3
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 +32 -1
- package/dist/index.d.mts +71 -12
- package/dist/index.d.ts +71 -12
- package/dist/index.js +172 -33
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +172 -33
- package/dist/index.mjs.map +1 -1
- package/dist/react-native.d.mts +52 -5
- package/dist/react-native.d.ts +52 -5
- package/dist/react-native.js +172 -33
- package/dist/react-native.js.map +1 -1
- package/dist/react-native.mjs +172 -33
- package/dist/react-native.mjs.map +1 -1
- package/package.json +1 -1
package/dist/react-native.d.mts
CHANGED
|
@@ -156,6 +156,13 @@ interface DeepdotsInitParams {
|
|
|
156
156
|
* to the configured integration; if missing, it stays in dry-run mode (console.log only).
|
|
157
157
|
*/
|
|
158
158
|
analytics?: AnalyticsKeys;
|
|
159
|
+
/**
|
|
160
|
+
* IP geolocation (country/city) added to analytics. Default `true`. The lookup calls
|
|
161
|
+
* third-party services (ipapi.co, then ipwho.is, then ipinfo.io as fallbacks, 3 s timeout
|
|
162
|
+
* each) and only runs when `analytics` is configured, tracking is enabled and the 30-day
|
|
163
|
+
* cache is missing or expired. Set `false` to never call them nor attach country/city.
|
|
164
|
+
*/
|
|
165
|
+
geolocation?: boolean;
|
|
159
166
|
/**
|
|
160
167
|
* Internal user attributes known only by the host (language, age, plan, ...), sent
|
|
161
168
|
* to the backend Contact for segmentation/targeting. Requires `userId`. Equivalent to calling
|
|
@@ -406,11 +413,21 @@ declare class DeepdotsPopups {
|
|
|
406
413
|
* nueva al volver a foreground o al conceder el consentimiento más tarde.
|
|
407
414
|
*/
|
|
408
415
|
private sessionOpen;
|
|
416
|
+
/** Último `user_engagement` emitido (ms epoch): marca el latido. */
|
|
417
|
+
private lastEngagementAt;
|
|
418
|
+
/** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */
|
|
419
|
+
private hiddenAt;
|
|
409
420
|
/**
|
|
410
421
|
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
411
422
|
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
412
423
|
*/
|
|
413
424
|
private language;
|
|
425
|
+
/** `geolocation` del init: si es false no se usa ni la caché ni el lookup. */
|
|
426
|
+
private geoEnabled;
|
|
427
|
+
/** Solo se consulta a terceros si el país/ciudad se va a enviar (analytics configurado). */
|
|
428
|
+
private geoWanted;
|
|
429
|
+
/** Evita más de un lookup por instancia (el consentimiento puede ir y venir). */
|
|
430
|
+
private geoLookupStarted;
|
|
414
431
|
/** Initialize the SDK with configuration */
|
|
415
432
|
init(config: DeepdotsInitParams): void;
|
|
416
433
|
/** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
|
|
@@ -507,12 +524,14 @@ declare class DeepdotsPopups {
|
|
|
507
524
|
* se llama en init(), al volver a foreground y al conceder el consentimiento.
|
|
508
525
|
*/
|
|
509
526
|
private openSession;
|
|
527
|
+
/** Web: si la pestaña se ve. Sin `document` (RN) manda AppState vía onForeground/onBackground. */
|
|
528
|
+
private isDocumentVisible;
|
|
510
529
|
/** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
|
|
511
530
|
previewAnalytics(): AnalyticsEnvelope;
|
|
512
531
|
/**
|
|
513
532
|
* Envía el lote acumulado de analytics y vacía el buffer.
|
|
514
|
-
* `final: true` (cierre de página/app)
|
|
515
|
-
*
|
|
533
|
+
* `final: true` (cierre de página/app): el lote no espera al primer POST en vuelo, porque
|
|
534
|
+
* el documento se muere; un lote que falle por red o 5xx se re-encola para el siguiente flush.
|
|
516
535
|
*/
|
|
517
536
|
flushAnalytics(options?: {
|
|
518
537
|
final?: boolean;
|
|
@@ -520,9 +539,30 @@ declare class DeepdotsPopups {
|
|
|
520
539
|
}): void;
|
|
521
540
|
/** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
|
|
522
541
|
private flushEngagement;
|
|
542
|
+
/** Tick del flush periódico: latido de engagement si toca, y envío de lo acumulado. */
|
|
543
|
+
private onAnalyticsFlushTick;
|
|
544
|
+
/**
|
|
545
|
+
* La pestaña vuelve a verse. Tras más de ANALYTICS_SESSION_TIMEOUT_MS oculta, la sesión
|
|
546
|
+
* anterior termina y empieza otra: seguir con el mismo sessionId mezclaba dos visitas y,
|
|
547
|
+
* si el backend ya la había cerrado por inactividad, la partía en dos feedbacks.
|
|
548
|
+
* - Oculta menos de ANALYTICS_SESSION_DISCARD_MS: el backend aún la tiene abierta → se
|
|
549
|
+
* cierra con normalidad (page_view de la pantalla, session_end `idle_timeout`, completed).
|
|
550
|
+
* - Más: ya la cerró el backend → se descarta sin enviar nada.
|
|
551
|
+
*/
|
|
552
|
+
private onDocumentVisible;
|
|
553
|
+
/** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
|
|
554
|
+
private discardSession;
|
|
555
|
+
/**
|
|
556
|
+
* Geolocalización por IP (país/ciudad para analytics). La caché persistente es local y se
|
|
557
|
+
* aplica siempre que `geolocation` no esté desactivado. El lookup a terceros (ipapi.co →
|
|
558
|
+
* ipwho.is → ipinfo.io, fallback con timeout) solo se hace si el dato se va a ENVIAR:
|
|
559
|
+
* analytics configurado + tracking activo + caché ausente o caducada. Una vez por instancia.
|
|
560
|
+
*/
|
|
561
|
+
private resolveGeo;
|
|
523
562
|
private analyticsIdentity;
|
|
524
563
|
/** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
|
|
525
564
|
private setupAnalyticsFlush;
|
|
565
|
+
private startAnalyticsFlushTimer;
|
|
526
566
|
/** Enable auto-launch functionality with configured triggers */
|
|
527
567
|
autoLaunch(): void;
|
|
528
568
|
/** Inicia los triggers configurados */
|
|
@@ -602,11 +642,18 @@ declare class DeepdotsPopups {
|
|
|
602
642
|
private mapPopupTriggerType;
|
|
603
643
|
private markSurveyProgress;
|
|
604
644
|
/**
|
|
605
|
-
*
|
|
606
|
-
*
|
|
645
|
+
* Fusiona el estado de redisplay del storage con el de memoria: por cada popup/survey gana
|
|
646
|
+
* la entrada MÁS RECIENTE. Fusionar y no sustituir es lo que hace convivir a varias
|
|
647
|
+
* pestañas sobre el mismo `localStorage`: se llama en el `init()`, antes de cada decisión
|
|
648
|
+
* de mostrar y antes de cada escritura. `answeredSurveys` se reconstruye desde las
|
|
649
|
+
* entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
|
|
607
650
|
*/
|
|
608
651
|
private loadPopupState;
|
|
609
|
-
/**
|
|
652
|
+
/**
|
|
653
|
+
* Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria.
|
|
654
|
+
* Fusiona antes de escribir: escribir solo la foto en memoria borraba lo que otra pestaña
|
|
655
|
+
* hubiese guardado desde nuestro `init()`.
|
|
656
|
+
*/
|
|
610
657
|
private persistPopupState;
|
|
611
658
|
private isPopupDefinition;
|
|
612
659
|
private isPopupTrigger;
|
package/dist/react-native.d.ts
CHANGED
|
@@ -156,6 +156,13 @@ interface DeepdotsInitParams {
|
|
|
156
156
|
* to the configured integration; if missing, it stays in dry-run mode (console.log only).
|
|
157
157
|
*/
|
|
158
158
|
analytics?: AnalyticsKeys;
|
|
159
|
+
/**
|
|
160
|
+
* IP geolocation (country/city) added to analytics. Default `true`. The lookup calls
|
|
161
|
+
* third-party services (ipapi.co, then ipwho.is, then ipinfo.io as fallbacks, 3 s timeout
|
|
162
|
+
* each) and only runs when `analytics` is configured, tracking is enabled and the 30-day
|
|
163
|
+
* cache is missing or expired. Set `false` to never call them nor attach country/city.
|
|
164
|
+
*/
|
|
165
|
+
geolocation?: boolean;
|
|
159
166
|
/**
|
|
160
167
|
* Internal user attributes known only by the host (language, age, plan, ...), sent
|
|
161
168
|
* to the backend Contact for segmentation/targeting. Requires `userId`. Equivalent to calling
|
|
@@ -406,11 +413,21 @@ declare class DeepdotsPopups {
|
|
|
406
413
|
* nueva al volver a foreground o al conceder el consentimiento más tarde.
|
|
407
414
|
*/
|
|
408
415
|
private sessionOpen;
|
|
416
|
+
/** Último `user_engagement` emitido (ms epoch): marca el latido. */
|
|
417
|
+
private lastEngagementAt;
|
|
418
|
+
/** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */
|
|
419
|
+
private hiddenAt;
|
|
409
420
|
/**
|
|
410
421
|
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
411
422
|
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
412
423
|
*/
|
|
413
424
|
private language;
|
|
425
|
+
/** `geolocation` del init: si es false no se usa ni la caché ni el lookup. */
|
|
426
|
+
private geoEnabled;
|
|
427
|
+
/** Solo se consulta a terceros si el país/ciudad se va a enviar (analytics configurado). */
|
|
428
|
+
private geoWanted;
|
|
429
|
+
/** Evita más de un lookup por instancia (el consentimiento puede ir y venir). */
|
|
430
|
+
private geoLookupStarted;
|
|
414
431
|
/** Initialize the SDK with configuration */
|
|
415
432
|
init(config: DeepdotsInitParams): void;
|
|
416
433
|
/** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
|
|
@@ -507,12 +524,14 @@ declare class DeepdotsPopups {
|
|
|
507
524
|
* se llama en init(), al volver a foreground y al conceder el consentimiento.
|
|
508
525
|
*/
|
|
509
526
|
private openSession;
|
|
527
|
+
/** Web: si la pestaña se ve. Sin `document` (RN) manda AppState vía onForeground/onBackground. */
|
|
528
|
+
private isDocumentVisible;
|
|
510
529
|
/** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
|
|
511
530
|
previewAnalytics(): AnalyticsEnvelope;
|
|
512
531
|
/**
|
|
513
532
|
* Envía el lote acumulado de analytics y vacía el buffer.
|
|
514
|
-
* `final: true` (cierre de página/app)
|
|
515
|
-
*
|
|
533
|
+
* `final: true` (cierre de página/app): el lote no espera al primer POST en vuelo, porque
|
|
534
|
+
* el documento se muere; un lote que falle por red o 5xx se re-encola para el siguiente flush.
|
|
516
535
|
*/
|
|
517
536
|
flushAnalytics(options?: {
|
|
518
537
|
final?: boolean;
|
|
@@ -520,9 +539,30 @@ declare class DeepdotsPopups {
|
|
|
520
539
|
}): void;
|
|
521
540
|
/** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
|
|
522
541
|
private flushEngagement;
|
|
542
|
+
/** Tick del flush periódico: latido de engagement si toca, y envío de lo acumulado. */
|
|
543
|
+
private onAnalyticsFlushTick;
|
|
544
|
+
/**
|
|
545
|
+
* La pestaña vuelve a verse. Tras más de ANALYTICS_SESSION_TIMEOUT_MS oculta, la sesión
|
|
546
|
+
* anterior termina y empieza otra: seguir con el mismo sessionId mezclaba dos visitas y,
|
|
547
|
+
* si el backend ya la había cerrado por inactividad, la partía en dos feedbacks.
|
|
548
|
+
* - Oculta menos de ANALYTICS_SESSION_DISCARD_MS: el backend aún la tiene abierta → se
|
|
549
|
+
* cierra con normalidad (page_view de la pantalla, session_end `idle_timeout`, completed).
|
|
550
|
+
* - Más: ya la cerró el backend → se descarta sin enviar nada.
|
|
551
|
+
*/
|
|
552
|
+
private onDocumentVisible;
|
|
553
|
+
/** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
|
|
554
|
+
private discardSession;
|
|
555
|
+
/**
|
|
556
|
+
* Geolocalización por IP (país/ciudad para analytics). La caché persistente es local y se
|
|
557
|
+
* aplica siempre que `geolocation` no esté desactivado. El lookup a terceros (ipapi.co →
|
|
558
|
+
* ipwho.is → ipinfo.io, fallback con timeout) solo se hace si el dato se va a ENVIAR:
|
|
559
|
+
* analytics configurado + tracking activo + caché ausente o caducada. Una vez por instancia.
|
|
560
|
+
*/
|
|
561
|
+
private resolveGeo;
|
|
523
562
|
private analyticsIdentity;
|
|
524
563
|
/** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
|
|
525
564
|
private setupAnalyticsFlush;
|
|
565
|
+
private startAnalyticsFlushTimer;
|
|
526
566
|
/** Enable auto-launch functionality with configured triggers */
|
|
527
567
|
autoLaunch(): void;
|
|
528
568
|
/** Inicia los triggers configurados */
|
|
@@ -602,11 +642,18 @@ declare class DeepdotsPopups {
|
|
|
602
642
|
private mapPopupTriggerType;
|
|
603
643
|
private markSurveyProgress;
|
|
604
644
|
/**
|
|
605
|
-
*
|
|
606
|
-
*
|
|
645
|
+
* Fusiona el estado de redisplay del storage con el de memoria: por cada popup/survey gana
|
|
646
|
+
* la entrada MÁS RECIENTE. Fusionar y no sustituir es lo que hace convivir a varias
|
|
647
|
+
* pestañas sobre el mismo `localStorage`: se llama en el `init()`, antes de cada decisión
|
|
648
|
+
* de mostrar y antes de cada escritura. `answeredSurveys` se reconstruye desde las
|
|
649
|
+
* entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
|
|
607
650
|
*/
|
|
608
651
|
private loadPopupState;
|
|
609
|
-
/**
|
|
652
|
+
/**
|
|
653
|
+
* Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria.
|
|
654
|
+
* Fusiona antes de escribir: escribir solo la foto en memoria borraba lo que otra pestaña
|
|
655
|
+
* hubiese guardado desde nuestro `init()`.
|
|
656
|
+
*/
|
|
610
657
|
private persistPopupState;
|
|
611
658
|
private isPopupDefinition;
|
|
612
659
|
private isPopupTrigger;
|
package/dist/react-native.js
CHANGED
|
@@ -4124,10 +4124,13 @@ init_tracking_manager();
|
|
|
4124
4124
|
|
|
4125
4125
|
// src/analytics/analytics-manager.ts
|
|
4126
4126
|
function createDryRunSink(log = console.log) {
|
|
4127
|
-
return (payload) =>
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4127
|
+
return (payload, meta) => {
|
|
4128
|
+
if (meta?.resetSession) return;
|
|
4129
|
+
log(
|
|
4130
|
+
"[DeepdotsAnalytics] (dry-run \xB7 NOT sent \xB7 no init.analytics) POST /sdk/feedback \u2192",
|
|
4131
|
+
JSON.stringify(payload, null, 2)
|
|
4132
|
+
);
|
|
4133
|
+
};
|
|
4131
4134
|
}
|
|
4132
4135
|
var dryRunSink = createDryRunSink();
|
|
4133
4136
|
var AnalyticsManager = class {
|
|
@@ -4194,6 +4197,19 @@ var AnalyticsManager = class {
|
|
|
4194
4197
|
this.exitMiniService(name);
|
|
4195
4198
|
}
|
|
4196
4199
|
}
|
|
4200
|
+
/**
|
|
4201
|
+
* Olvida la sesión en curso SIN emitir nada: vacía el buffer y los mini-services activos
|
|
4202
|
+
* (sin `mini_service_exit`). Para una sesión que el backend ya cerró por inactividad: lo
|
|
4203
|
+
* que quedara pendiente pertenecía a ella y no debe llegar con la sesión nueva.
|
|
4204
|
+
*/
|
|
4205
|
+
discardSession(identity) {
|
|
4206
|
+
this.events = [];
|
|
4207
|
+
this.activeMiniServices.clear();
|
|
4208
|
+
try {
|
|
4209
|
+
void this.sink({ ...this.buildPayload(identity), events: [] }, { resetSession: true });
|
|
4210
|
+
} catch {
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4197
4213
|
/**
|
|
4198
4214
|
* Olvida lo que pertenecía al usuario anterior (user attributes + métricas) al cambiar de
|
|
4199
4215
|
* usuario. NO toca el buffer de eventos: el cierre de sesión ya lo vació con la identidad
|
|
@@ -4364,14 +4380,7 @@ function createFeedbackSink(options) {
|
|
|
4364
4380
|
const post = async (body, final, sessionEnd = false) => {
|
|
4365
4381
|
const json = JSON.stringify(body);
|
|
4366
4382
|
const small = json.length <= KEEPALIVE_MAX_BYTES;
|
|
4367
|
-
if (final
|
|
4368
|
-
const payload = typeof Blob !== "undefined" ? new Blob([json], { type: "application/json" }) : json;
|
|
4369
|
-
if (options.sendBeaconImpl(url, payload)) {
|
|
4370
|
-
options.log?.("[DeepdotsAnalytics] final flush via sendBeacon");
|
|
4371
|
-
return;
|
|
4372
|
-
}
|
|
4373
|
-
options.log?.("[DeepdotsAnalytics] sendBeacon rejected the batch; falling back to fetch");
|
|
4374
|
-
}
|
|
4383
|
+
if (final) options.log?.("[DeepdotsAnalytics] final flush via fetch keepalive");
|
|
4375
4384
|
const f = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
4376
4385
|
if (!f) {
|
|
4377
4386
|
options.log?.("[DeepdotsAnalytics] no fetch available; payload not sent", body);
|
|
@@ -4381,6 +4390,9 @@ function createFeedbackSink(options) {
|
|
|
4381
4390
|
method: "POST",
|
|
4382
4391
|
headers: { "Content-Type": "application/json" },
|
|
4383
4392
|
body: json,
|
|
4393
|
+
// La API autentica por `publicKey` en el body y responde `Allow-Origin: *`: una
|
|
4394
|
+
// petición con credenciales fallaría el CORS. Explícito para no depender del default.
|
|
4395
|
+
credentials: "omit",
|
|
4384
4396
|
// keepalive: el POST sobrevive a la navegación mientras el body sea pequeño.
|
|
4385
4397
|
...small ? { keepalive: true } : {}
|
|
4386
4398
|
});
|
|
@@ -4407,6 +4419,12 @@ function createFeedbackSink(options) {
|
|
|
4407
4419
|
}
|
|
4408
4420
|
};
|
|
4409
4421
|
return async (envelope, meta) => {
|
|
4422
|
+
if (meta?.resetSession) {
|
|
4423
|
+
feedbackSessionId = void 0;
|
|
4424
|
+
firstPostInFlight = null;
|
|
4425
|
+
options.onSessionReset?.();
|
|
4426
|
+
return;
|
|
4427
|
+
}
|
|
4410
4428
|
const final = meta?.final === true;
|
|
4411
4429
|
const sessionEnd = meta?.sessionEnd === true;
|
|
4412
4430
|
if (!feedbackSessionId && firstPostInFlight && !final) {
|
|
@@ -4473,6 +4491,23 @@ var NavigationObserver = class {
|
|
|
4473
4491
|
this.closeCurrent();
|
|
4474
4492
|
this.currentScreen = null;
|
|
4475
4493
|
}
|
|
4494
|
+
/**
|
|
4495
|
+
* Olvida la pantalla actual SIN emitir su visita. Para una sesión que el backend ya dio
|
|
4496
|
+
* por cerrada: su visita no tiene dónde ir, y mandarla con la sesión nueva la contaría
|
|
4497
|
+
* donde no ocurrió. Después, `restart()` vuelve a empezar en la pantalla actual.
|
|
4498
|
+
*/
|
|
4499
|
+
discard() {
|
|
4500
|
+
this.currentScreen = null;
|
|
4501
|
+
}
|
|
4502
|
+
/**
|
|
4503
|
+
* Retoma el seguimiento en la pantalla actual tras un `stop()`, cuando se abre una sesión
|
|
4504
|
+
* nueva sin navegar (cambio de usuario, vuelta de la bfcache). No-op si nunca se instaló
|
|
4505
|
+
* (navegación manual por `setScreen`, RN) o si ya hay pantalla en curso.
|
|
4506
|
+
*/
|
|
4507
|
+
restart() {
|
|
4508
|
+
if (!this.installed || this.currentScreen != null || typeof window === "undefined") return;
|
|
4509
|
+
this.begin(window.location.href);
|
|
4510
|
+
}
|
|
4476
4511
|
closeCurrent() {
|
|
4477
4512
|
if (this.currentScreen == null) return;
|
|
4478
4513
|
const exit = this.now();
|
|
@@ -4997,6 +5032,10 @@ var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
|
|
|
4997
5032
|
var DAY_IN_MS = 24 * 60 * 60 * 1e3;
|
|
4998
5033
|
var ANALYTICS_MAX_BATCH_SIZE = 20;
|
|
4999
5034
|
var ANALYTICS_FLUSH_INTERVAL_MS = 3e4;
|
|
5035
|
+
var ANALYTICS_HEARTBEAT_MS = 5 * 6e4;
|
|
5036
|
+
var ANALYTICS_SESSION_TIMEOUT_MS = 30 * 6e4;
|
|
5037
|
+
var ANALYTICS_SERVER_IDLE_MS = 60 * 6e4;
|
|
5038
|
+
var ANALYTICS_SESSION_DISCARD_MS = ANALYTICS_SERVER_IDLE_MS - 10 * 6e4;
|
|
5000
5039
|
var RESERVED_EVENT_PREFIX = "deepdots_";
|
|
5001
5040
|
var CUSTOM_EVENT_PREFIX = "deepdots_event_";
|
|
5002
5041
|
var DeepdotsPopups = class {
|
|
@@ -5060,11 +5099,21 @@ var DeepdotsPopups = class {
|
|
|
5060
5099
|
* nueva al volver a foreground o al conceder el consentimiento más tarde.
|
|
5061
5100
|
*/
|
|
5062
5101
|
this.sessionOpen = false;
|
|
5102
|
+
/** Último `user_engagement` emitido (ms epoch): marca el latido. */
|
|
5103
|
+
this.lastEngagementAt = 0;
|
|
5104
|
+
/** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */
|
|
5105
|
+
this.hiddenAt = null;
|
|
5063
5106
|
/**
|
|
5064
5107
|
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
5065
5108
|
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
5066
5109
|
*/
|
|
5067
5110
|
this.language = void 0;
|
|
5111
|
+
/** `geolocation` del init: si es false no se usa ni la caché ni el lookup. */
|
|
5112
|
+
this.geoEnabled = true;
|
|
5113
|
+
/** Solo se consulta a terceros si el país/ciudad se va a enviar (analytics configurado). */
|
|
5114
|
+
this.geoWanted = false;
|
|
5115
|
+
/** Evita más de un lookup por instancia (el consentimiento puede ir y venir). */
|
|
5116
|
+
this.geoLookupStarted = false;
|
|
5068
5117
|
}
|
|
5069
5118
|
/** Initialize the SDK with configuration */
|
|
5070
5119
|
init(config) {
|
|
@@ -5116,8 +5165,6 @@ var DeepdotsPopups = class {
|
|
|
5116
5165
|
baseUrl: this.baseUrl,
|
|
5117
5166
|
keys: config.analytics,
|
|
5118
5167
|
log: (...a) => this.log(...a),
|
|
5119
|
-
// sendBeacon: único transporte que sobrevive al cierre de la página.
|
|
5120
|
-
sendBeaconImpl: typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" ? (url, body) => navigator.sendBeacon(url, body) : void 0,
|
|
5121
5168
|
onSessionId: (id) => {
|
|
5122
5169
|
this.analyticsFeedbackSessionId = id;
|
|
5123
5170
|
},
|
|
@@ -5152,20 +5199,13 @@ var DeepdotsPopups = class {
|
|
|
5152
5199
|
if (this.tracking?.isTrackingEnabled()) {
|
|
5153
5200
|
this.crashReporter.install();
|
|
5154
5201
|
}
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
if (geo) {
|
|
5159
|
-
this.analytics?.updateDevice(geo);
|
|
5160
|
-
writeCachedGeo(storage, geo, Date.now());
|
|
5161
|
-
}
|
|
5162
|
-
}).catch(() => {
|
|
5163
|
-
});
|
|
5202
|
+
this.geoEnabled = config.geolocation ?? true;
|
|
5203
|
+
this.geoWanted = this.geoEnabled && !!config.analytics;
|
|
5204
|
+
this.resolveGeo();
|
|
5164
5205
|
this.navObserver = new NavigationObserver();
|
|
5165
5206
|
this.navObserver.onVisit((v) => this.track("deepdots_page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
|
|
5166
5207
|
this.navObserver.install();
|
|
5167
5208
|
this.engagement = new EngagementTracker();
|
|
5168
|
-
this.engagement.resume();
|
|
5169
5209
|
this.setupAnalyticsFlush();
|
|
5170
5210
|
this.openSession();
|
|
5171
5211
|
const pendingCrashes = this.crashReporter.drainPendingCrashes();
|
|
@@ -5203,6 +5243,7 @@ var DeepdotsPopups = class {
|
|
|
5203
5243
|
if (!enabled) this.closeSession("tracking_disabled");
|
|
5204
5244
|
this.tracking?.setTrackingEnabled(enabled);
|
|
5205
5245
|
if (enabled) this.openSession();
|
|
5246
|
+
if (enabled) this.resolveGeo();
|
|
5206
5247
|
this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
|
|
5207
5248
|
}
|
|
5208
5249
|
// ───────── Analytics (canal separado del feedback, vinculado por user_id) ─────────
|
|
@@ -5384,6 +5425,7 @@ var DeepdotsPopups = class {
|
|
|
5384
5425
|
this.navStarted = false;
|
|
5385
5426
|
this.analytics?.exitAllMiniServices();
|
|
5386
5427
|
this.flushEngagement();
|
|
5428
|
+
this.engagement?.pause();
|
|
5387
5429
|
this.track("deepdots_session_end", { reason });
|
|
5388
5430
|
this.flushAnalytics({ final: reason === "page_hide", sessionEnd: true });
|
|
5389
5431
|
this.analyticsFeedbackSessionId = void 0;
|
|
@@ -5398,7 +5440,14 @@ var DeepdotsPopups = class {
|
|
|
5398
5440
|
if (this.sessionOpen) return;
|
|
5399
5441
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
5400
5442
|
this.sessionOpen = true;
|
|
5443
|
+
this.lastEngagementAt = Date.now();
|
|
5401
5444
|
this.track("deepdots_session_start", {});
|
|
5445
|
+
this.navObserver?.restart();
|
|
5446
|
+
if (this.isDocumentVisible()) this.engagement?.resume();
|
|
5447
|
+
}
|
|
5448
|
+
/** Web: si la pestaña se ve. Sin `document` (RN) manda AppState vía onForeground/onBackground. */
|
|
5449
|
+
isDocumentVisible() {
|
|
5450
|
+
return typeof document === "undefined" || document.visibilityState !== "hidden";
|
|
5402
5451
|
}
|
|
5403
5452
|
/** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
|
|
5404
5453
|
previewAnalytics() {
|
|
@@ -5411,8 +5460,8 @@ var DeepdotsPopups = class {
|
|
|
5411
5460
|
}
|
|
5412
5461
|
/**
|
|
5413
5462
|
* Envía el lote acumulado de analytics y vacía el buffer.
|
|
5414
|
-
* `final: true` (cierre de página/app)
|
|
5415
|
-
*
|
|
5463
|
+
* `final: true` (cierre de página/app): el lote no espera al primer POST en vuelo, porque
|
|
5464
|
+
* el documento se muere; un lote que falle por red o 5xx se re-encola para el siguiente flush.
|
|
5416
5465
|
*/
|
|
5417
5466
|
flushAnalytics(options) {
|
|
5418
5467
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
@@ -5422,7 +5471,76 @@ var DeepdotsPopups = class {
|
|
|
5422
5471
|
flushEngagement() {
|
|
5423
5472
|
if (!this.tracking?.isTrackingEnabled()) return;
|
|
5424
5473
|
const ms = this.engagement?.consume() ?? 0;
|
|
5425
|
-
if (ms > 0)
|
|
5474
|
+
if (ms > 0) {
|
|
5475
|
+
this.track("deepdots_user_engagement", { engagement_time_msec: ms });
|
|
5476
|
+
this.lastEngagementAt = Date.now();
|
|
5477
|
+
}
|
|
5478
|
+
}
|
|
5479
|
+
/** Tick del flush periódico: latido de engagement si toca, y envío de lo acumulado. */
|
|
5480
|
+
onAnalyticsFlushTick() {
|
|
5481
|
+
if (this.sessionOpen && this.isDocumentVisible() && Date.now() - this.lastEngagementAt >= ANALYTICS_HEARTBEAT_MS) {
|
|
5482
|
+
this.flushEngagement();
|
|
5483
|
+
}
|
|
5484
|
+
this.flushAnalytics();
|
|
5485
|
+
}
|
|
5486
|
+
/**
|
|
5487
|
+
* La pestaña vuelve a verse. Tras más de ANALYTICS_SESSION_TIMEOUT_MS oculta, la sesión
|
|
5488
|
+
* anterior termina y empieza otra: seguir con el mismo sessionId mezclaba dos visitas y,
|
|
5489
|
+
* si el backend ya la había cerrado por inactividad, la partía en dos feedbacks.
|
|
5490
|
+
* - Oculta menos de ANALYTICS_SESSION_DISCARD_MS: el backend aún la tiene abierta → se
|
|
5491
|
+
* cierra con normalidad (page_view de la pantalla, session_end `idle_timeout`, completed).
|
|
5492
|
+
* - Más: ya la cerró el backend → se descarta sin enviar nada.
|
|
5493
|
+
*/
|
|
5494
|
+
onDocumentVisible() {
|
|
5495
|
+
const hiddenFor = this.hiddenAt === null ? 0 : Date.now() - this.hiddenAt;
|
|
5496
|
+
this.hiddenAt = null;
|
|
5497
|
+
if (!this.sessionOpen) return;
|
|
5498
|
+
if (hiddenFor < ANALYTICS_SESSION_TIMEOUT_MS) {
|
|
5499
|
+
this.engagement?.resume();
|
|
5500
|
+
return;
|
|
5501
|
+
}
|
|
5502
|
+
if (hiddenFor < ANALYTICS_SESSION_DISCARD_MS) {
|
|
5503
|
+
this.closeSession("idle_timeout");
|
|
5504
|
+
} else {
|
|
5505
|
+
this.discardSession();
|
|
5506
|
+
}
|
|
5507
|
+
this.openSession();
|
|
5508
|
+
}
|
|
5509
|
+
/** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
|
|
5510
|
+
discardSession() {
|
|
5511
|
+
this.sessionOpen = false;
|
|
5512
|
+
this.navObserver?.discard();
|
|
5513
|
+
this.analytics?.discardSession(this.analyticsIdentity());
|
|
5514
|
+
this.engagement?.consume();
|
|
5515
|
+
this.engagement?.pause();
|
|
5516
|
+
this.analyticsFeedbackSessionId = void 0;
|
|
5517
|
+
this.tracking?.setSessionId(null);
|
|
5518
|
+
this.log("tracking \xB7 session discarded after", ANALYTICS_SESSION_DISCARD_MS / 6e4, "min hidden");
|
|
5519
|
+
}
|
|
5520
|
+
/**
|
|
5521
|
+
* Geolocalización por IP (país/ciudad para analytics). La caché persistente es local y se
|
|
5522
|
+
* aplica siempre que `geolocation` no esté desactivado. El lookup a terceros (ipapi.co →
|
|
5523
|
+
* ipwho.is → ipinfo.io, fallback con timeout) solo se hace si el dato se va a ENVIAR:
|
|
5524
|
+
* analytics configurado + tracking activo + caché ausente o caducada. Una vez por instancia.
|
|
5525
|
+
*/
|
|
5526
|
+
resolveGeo() {
|
|
5527
|
+
const storage = this.storage;
|
|
5528
|
+
if (!this.geoEnabled || !storage || !this.analytics) return;
|
|
5529
|
+
const cachedGeo = readCachedGeo(storage, Date.now());
|
|
5530
|
+
if (cachedGeo) {
|
|
5531
|
+
this.analytics.updateDevice(cachedGeo);
|
|
5532
|
+
return;
|
|
5533
|
+
}
|
|
5534
|
+
if (!this.geoWanted || this.geoLookupStarted) return;
|
|
5535
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
5536
|
+
this.geoLookupStarted = true;
|
|
5537
|
+
fetchGeo().then((geo) => {
|
|
5538
|
+
if (geo) {
|
|
5539
|
+
this.analytics?.updateDevice(geo);
|
|
5540
|
+
writeCachedGeo(storage, geo, Date.now());
|
|
5541
|
+
}
|
|
5542
|
+
}).catch(() => {
|
|
5543
|
+
});
|
|
5426
5544
|
}
|
|
5427
5545
|
analyticsIdentity() {
|
|
5428
5546
|
return {
|
|
@@ -5432,22 +5550,32 @@ var DeepdotsPopups = class {
|
|
|
5432
5550
|
}
|
|
5433
5551
|
/** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
|
|
5434
5552
|
setupAnalyticsFlush() {
|
|
5553
|
+
this.startAnalyticsFlushTimer();
|
|
5435
5554
|
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
5436
|
-
this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
|
|
5437
5555
|
window.addEventListener("pagehide", () => {
|
|
5438
5556
|
clearInterval(this.analyticsFlushTimer);
|
|
5439
5557
|
this.closeSession("page_hide");
|
|
5440
5558
|
});
|
|
5559
|
+
window.addEventListener("pageshow", (event) => {
|
|
5560
|
+
if (!event.persisted) return;
|
|
5561
|
+
this.startAnalyticsFlushTimer();
|
|
5562
|
+
this.openSession();
|
|
5563
|
+
});
|
|
5441
5564
|
document.addEventListener("visibilitychange", () => {
|
|
5442
5565
|
if (document.visibilityState === "hidden") {
|
|
5443
5566
|
this.flushEngagement();
|
|
5444
5567
|
this.engagement?.pause();
|
|
5445
5568
|
this.flushAnalytics();
|
|
5569
|
+
this.hiddenAt = Date.now();
|
|
5446
5570
|
} else if (document.visibilityState === "visible") {
|
|
5447
|
-
this.
|
|
5571
|
+
this.onDocumentVisible();
|
|
5448
5572
|
}
|
|
5449
5573
|
});
|
|
5450
5574
|
}
|
|
5575
|
+
startAnalyticsFlushTimer() {
|
|
5576
|
+
clearInterval(this.analyticsFlushTimer);
|
|
5577
|
+
this.analyticsFlushTimer = setInterval(() => this.onAnalyticsFlushTick(), ANALYTICS_FLUSH_INTERVAL_MS);
|
|
5578
|
+
}
|
|
5451
5579
|
/** Enable auto-launch functionality with configured triggers */
|
|
5452
5580
|
autoLaunch() {
|
|
5453
5581
|
if (!this.initialized) {
|
|
@@ -5541,6 +5669,7 @@ var DeepdotsPopups = class {
|
|
|
5541
5669
|
this.emitEvent("popup_shown", def.surveyId, { popupId: def.id });
|
|
5542
5670
|
}
|
|
5543
5671
|
shouldShow(def, pathUrl, skipPathCheck = false) {
|
|
5672
|
+
if (this.storage) this.loadPopupState(this.storage);
|
|
5544
5673
|
if (this.isPathExcluded(def, pathUrl)) {
|
|
5545
5674
|
return false;
|
|
5546
5675
|
}
|
|
@@ -6067,22 +6196,32 @@ var DeepdotsPopups = class {
|
|
|
6067
6196
|
this.persistPopupState();
|
|
6068
6197
|
}
|
|
6069
6198
|
/**
|
|
6070
|
-
*
|
|
6071
|
-
*
|
|
6199
|
+
* Fusiona el estado de redisplay del storage con el de memoria: por cada popup/survey gana
|
|
6200
|
+
* la entrada MÁS RECIENTE. Fusionar y no sustituir es lo que hace convivir a varias
|
|
6201
|
+
* pestañas sobre el mismo `localStorage`: se llama en el `init()`, antes de cada decisión
|
|
6202
|
+
* de mostrar y antes de cada escritura. `answeredSurveys` se reconstruye desde las
|
|
6203
|
+
* entradas COMPLETED: es exactamente lo que persiste `markSurveyAnswered`.
|
|
6072
6204
|
*/
|
|
6073
6205
|
loadPopupState(storage) {
|
|
6074
6206
|
const state = readPopupState(storage);
|
|
6075
6207
|
Object.entries(state.lastShown).forEach(([popupId, timestamp]) => {
|
|
6076
|
-
this.lastShown.set(popupId, timestamp);
|
|
6208
|
+
if (timestamp > (this.lastShown.get(popupId) ?? -Infinity)) this.lastShown.set(popupId, timestamp);
|
|
6077
6209
|
});
|
|
6078
6210
|
Object.entries(state.progress).forEach(([surveyId, entry]) => {
|
|
6211
|
+
const current = this.surveyProgress.get(surveyId);
|
|
6212
|
+
if (current && current.timestamp >= entry.timestamp) return;
|
|
6079
6213
|
this.surveyProgress.set(surveyId, { status: entry.status, timestamp: entry.timestamp });
|
|
6080
6214
|
if (entry.status === "COMPLETED") this.answeredSurveys.add(surveyId);
|
|
6081
6215
|
});
|
|
6082
6216
|
}
|
|
6083
|
-
/**
|
|
6217
|
+
/**
|
|
6218
|
+
* Vuelca el estado de redisplay al storage. Sin storage (o bloqueado) degrada a memoria.
|
|
6219
|
+
* Fusiona antes de escribir: escribir solo la foto en memoria borraba lo que otra pestaña
|
|
6220
|
+
* hubiese guardado desde nuestro `init()`.
|
|
6221
|
+
*/
|
|
6084
6222
|
persistPopupState() {
|
|
6085
6223
|
if (!this.storage) return;
|
|
6224
|
+
this.loadPopupState(this.storage);
|
|
6086
6225
|
writePopupState(this.storage, {
|
|
6087
6226
|
lastShown: Object.fromEntries(this.lastShown),
|
|
6088
6227
|
progress: Object.fromEntries(this.surveyProgress)
|