@magicfeedback/popup-sdk 1.8.2 → 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.
@@ -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. */
@@ -522,6 +539,26 @@ declare class DeepdotsPopups {
522
539
  }): void;
523
540
  /** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
524
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;
525
562
  private analyticsIdentity;
526
563
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
527
564
  private setupAnalyticsFlush;
@@ -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. */
@@ -522,6 +539,26 @@ declare class DeepdotsPopups {
522
539
  }): void;
523
540
  /** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
524
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;
525
562
  private analyticsIdentity;
526
563
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
527
564
  private setupAnalyticsFlush;
@@ -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) => log(
4128
- "[DeepdotsAnalytics] (dry-run \xB7 NOT sent \xB7 no init.analytics) POST /sdk/feedback \u2192",
4129
- JSON.stringify(payload, null, 2)
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
@@ -4403,6 +4419,12 @@ function createFeedbackSink(options) {
4403
4419
  }
4404
4420
  };
4405
4421
  return async (envelope, meta) => {
4422
+ if (meta?.resetSession) {
4423
+ feedbackSessionId = void 0;
4424
+ firstPostInFlight = null;
4425
+ options.onSessionReset?.();
4426
+ return;
4427
+ }
4406
4428
  const final = meta?.final === true;
4407
4429
  const sessionEnd = meta?.sessionEnd === true;
4408
4430
  if (!feedbackSessionId && firstPostInFlight && !final) {
@@ -4469,6 +4491,14 @@ var NavigationObserver = class {
4469
4491
  this.closeCurrent();
4470
4492
  this.currentScreen = null;
4471
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
+ }
4472
4502
  /**
4473
4503
  * Retoma el seguimiento en la pantalla actual tras un `stop()`, cuando se abre una sesión
4474
4504
  * nueva sin navegar (cambio de usuario, vuelta de la bfcache). No-op si nunca se instaló
@@ -5002,6 +5032,10 @@ var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
5002
5032
  var DAY_IN_MS = 24 * 60 * 60 * 1e3;
5003
5033
  var ANALYTICS_MAX_BATCH_SIZE = 20;
5004
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;
5005
5039
  var RESERVED_EVENT_PREFIX = "deepdots_";
5006
5040
  var CUSTOM_EVENT_PREFIX = "deepdots_event_";
5007
5041
  var DeepdotsPopups = class {
@@ -5065,11 +5099,21 @@ var DeepdotsPopups = class {
5065
5099
  * nueva al volver a foreground o al conceder el consentimiento más tarde.
5066
5100
  */
5067
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;
5068
5106
  /**
5069
5107
  * Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
5070
5108
  * verdad para el context de analytics Y para la segmentación por idioma de los popups.
5071
5109
  */
5072
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;
5073
5117
  }
5074
5118
  /** Initialize the SDK with configuration */
5075
5119
  init(config) {
@@ -5155,15 +5199,9 @@ var DeepdotsPopups = class {
5155
5199
  if (this.tracking?.isTrackingEnabled()) {
5156
5200
  this.crashReporter.install();
5157
5201
  }
5158
- const cachedGeo = readCachedGeo(storage, Date.now());
5159
- if (cachedGeo) this.analytics.updateDevice(cachedGeo);
5160
- fetchGeo().then((geo) => {
5161
- if (geo) {
5162
- this.analytics?.updateDevice(geo);
5163
- writeCachedGeo(storage, geo, Date.now());
5164
- }
5165
- }).catch(() => {
5166
- });
5202
+ this.geoEnabled = config.geolocation ?? true;
5203
+ this.geoWanted = this.geoEnabled && !!config.analytics;
5204
+ this.resolveGeo();
5167
5205
  this.navObserver = new NavigationObserver();
5168
5206
  this.navObserver.onVisit((v) => this.track("deepdots_page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
5169
5207
  this.navObserver.install();
@@ -5205,6 +5243,7 @@ var DeepdotsPopups = class {
5205
5243
  if (!enabled) this.closeSession("tracking_disabled");
5206
5244
  this.tracking?.setTrackingEnabled(enabled);
5207
5245
  if (enabled) this.openSession();
5246
+ if (enabled) this.resolveGeo();
5208
5247
  this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
5209
5248
  }
5210
5249
  // ───────── Analytics (canal separado del feedback, vinculado por user_id) ─────────
@@ -5401,6 +5440,7 @@ var DeepdotsPopups = class {
5401
5440
  if (this.sessionOpen) return;
5402
5441
  if (!this.tracking?.isTrackingEnabled()) return;
5403
5442
  this.sessionOpen = true;
5443
+ this.lastEngagementAt = Date.now();
5404
5444
  this.track("deepdots_session_start", {});
5405
5445
  this.navObserver?.restart();
5406
5446
  if (this.isDocumentVisible()) this.engagement?.resume();
@@ -5431,7 +5471,76 @@ var DeepdotsPopups = class {
5431
5471
  flushEngagement() {
5432
5472
  if (!this.tracking?.isTrackingEnabled()) return;
5433
5473
  const ms = this.engagement?.consume() ?? 0;
5434
- if (ms > 0) this.track("deepdots_user_engagement", { engagement_time_msec: ms });
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
+ });
5435
5544
  }
5436
5545
  analyticsIdentity() {
5437
5546
  return {
@@ -5441,8 +5550,8 @@ var DeepdotsPopups = class {
5441
5550
  }
5442
5551
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
5443
5552
  setupAnalyticsFlush() {
5444
- if (typeof document === "undefined" || typeof window === "undefined") return;
5445
5553
  this.startAnalyticsFlushTimer();
5554
+ if (typeof document === "undefined" || typeof window === "undefined") return;
5446
5555
  window.addEventListener("pagehide", () => {
5447
5556
  clearInterval(this.analyticsFlushTimer);
5448
5557
  this.closeSession("page_hide");
@@ -5457,14 +5566,15 @@ var DeepdotsPopups = class {
5457
5566
  this.flushEngagement();
5458
5567
  this.engagement?.pause();
5459
5568
  this.flushAnalytics();
5569
+ this.hiddenAt = Date.now();
5460
5570
  } else if (document.visibilityState === "visible") {
5461
- if (this.sessionOpen) this.engagement?.resume();
5571
+ this.onDocumentVisible();
5462
5572
  }
5463
5573
  });
5464
5574
  }
5465
5575
  startAnalyticsFlushTimer() {
5466
5576
  clearInterval(this.analyticsFlushTimer);
5467
- this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
5577
+ this.analyticsFlushTimer = setInterval(() => this.onAnalyticsFlushTick(), ANALYTICS_FLUSH_INTERVAL_MS);
5468
5578
  }
5469
5579
  /** Enable auto-launch functionality with configured triggers */
5470
5580
  autoLaunch() {