@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.
package/dist/index.mjs CHANGED
@@ -868,7 +868,7 @@ function createReactNativeRenderer() {
868
868
  }
869
869
 
870
870
  // src/ui/preload.ts
871
- var defaultLoader = () => import("./renderPopup-ERUKQE4U.mjs");
871
+ var defaultLoader = () => import("./renderPopup-3CZ6WTXF.mjs");
872
872
  var pending = null;
873
873
  function preloadRenderPopup(loader = defaultLoader) {
874
874
  if (pending) return pending;
@@ -941,7 +941,7 @@ var BrowserPopupRenderer = class {
941
941
  if (!this.container) return;
942
942
  this.visible = true;
943
943
  const container = this.container;
944
- void import("./renderPopup-ERUKQE4U.mjs").then(({ renderPopup }) => {
944
+ void import("./renderPopup-3CZ6WTXF.mjs").then(({ renderPopup }) => {
945
945
  renderPopup(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService, analyticsFeedbackSessionId, options);
946
946
  });
947
947
  }
@@ -966,10 +966,13 @@ function createDefaultRenderer() {
966
966
 
967
967
  // src/analytics/analytics-manager.ts
968
968
  function createDryRunSink(log = console.log) {
969
- return (payload) => log(
970
- "[DeepdotsAnalytics] (dry-run \xB7 NOT sent \xB7 no init.analytics) POST /sdk/feedback \u2192",
971
- JSON.stringify(payload, null, 2)
972
- );
969
+ return (payload, meta) => {
970
+ if (meta?.resetSession) return;
971
+ log(
972
+ "[DeepdotsAnalytics] (dry-run \xB7 NOT sent \xB7 no init.analytics) POST /sdk/feedback \u2192",
973
+ JSON.stringify(payload, null, 2)
974
+ );
975
+ };
973
976
  }
974
977
  var dryRunSink = createDryRunSink();
975
978
  var AnalyticsManager = class {
@@ -1036,6 +1039,19 @@ var AnalyticsManager = class {
1036
1039
  this.exitMiniService(name);
1037
1040
  }
1038
1041
  }
1042
+ /**
1043
+ * Olvida la sesión en curso SIN emitir nada: vacía el buffer y los mini-services activos
1044
+ * (sin `mini_service_exit`). Para una sesión que el backend ya cerró por inactividad: lo
1045
+ * que quedara pendiente pertenecía a ella y no debe llegar con la sesión nueva.
1046
+ */
1047
+ discardSession(identity) {
1048
+ this.events = [];
1049
+ this.activeMiniServices.clear();
1050
+ try {
1051
+ void this.sink({ ...this.buildPayload(identity), events: [] }, { resetSession: true });
1052
+ } catch {
1053
+ }
1054
+ }
1039
1055
  /**
1040
1056
  * Olvida lo que pertenecía al usuario anterior (user attributes + métricas) al cambiar de
1041
1057
  * usuario. NO toca el buffer de eventos: el cierre de sesión ya lo vació con la identidad
@@ -1245,6 +1261,12 @@ function createFeedbackSink(options) {
1245
1261
  }
1246
1262
  };
1247
1263
  return async (envelope, meta) => {
1264
+ if (meta?.resetSession) {
1265
+ feedbackSessionId = void 0;
1266
+ firstPostInFlight = null;
1267
+ options.onSessionReset?.();
1268
+ return;
1269
+ }
1248
1270
  const final = meta?.final === true;
1249
1271
  const sessionEnd = meta?.sessionEnd === true;
1250
1272
  if (!feedbackSessionId && firstPostInFlight && !final) {
@@ -1311,6 +1333,14 @@ var NavigationObserver = class {
1311
1333
  this.closeCurrent();
1312
1334
  this.currentScreen = null;
1313
1335
  }
1336
+ /**
1337
+ * Olvida la pantalla actual SIN emitir su visita. Para una sesión que el backend ya dio
1338
+ * por cerrada: su visita no tiene dónde ir, y mandarla con la sesión nueva la contaría
1339
+ * donde no ocurrió. Después, `restart()` vuelve a empezar en la pantalla actual.
1340
+ */
1341
+ discard() {
1342
+ this.currentScreen = null;
1343
+ }
1314
1344
  /**
1315
1345
  * Retoma el seguimiento en la pantalla actual tras un `stop()`, cuando se abre una sesión
1316
1346
  * nueva sin navegar (cambio de usuario, vuelta de la bfcache). No-op si nunca se instaló
@@ -1840,6 +1870,10 @@ var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
1840
1870
  var DAY_IN_MS = 24 * 60 * 60 * 1e3;
1841
1871
  var ANALYTICS_MAX_BATCH_SIZE = 20;
1842
1872
  var ANALYTICS_FLUSH_INTERVAL_MS = 3e4;
1873
+ var ANALYTICS_HEARTBEAT_MS = 5 * 6e4;
1874
+ var ANALYTICS_SESSION_TIMEOUT_MS = 30 * 6e4;
1875
+ var ANALYTICS_SERVER_IDLE_MS = 60 * 6e4;
1876
+ var ANALYTICS_SESSION_DISCARD_MS = ANALYTICS_SERVER_IDLE_MS - 10 * 6e4;
1843
1877
  var RESERVED_EVENT_PREFIX = "deepdots_";
1844
1878
  var CUSTOM_EVENT_PREFIX = "deepdots_event_";
1845
1879
  var DeepdotsPopups = class {
@@ -1903,11 +1937,21 @@ var DeepdotsPopups = class {
1903
1937
  * nueva al volver a foreground o al conceder el consentimiento más tarde.
1904
1938
  */
1905
1939
  this.sessionOpen = false;
1940
+ /** Último `user_engagement` emitido (ms epoch): marca el latido. */
1941
+ this.lastEngagementAt = 0;
1942
+ /** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */
1943
+ this.hiddenAt = null;
1906
1944
  /**
1907
1945
  * Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
1908
1946
  * verdad para el context de analytics Y para la segmentación por idioma de los popups.
1909
1947
  */
1910
1948
  this.language = void 0;
1949
+ /** `geolocation` del init: si es false no se usa ni la caché ni el lookup. */
1950
+ this.geoEnabled = true;
1951
+ /** Solo se consulta a terceros si el país/ciudad se va a enviar (analytics configurado). */
1952
+ this.geoWanted = false;
1953
+ /** Evita más de un lookup por instancia (el consentimiento puede ir y venir). */
1954
+ this.geoLookupStarted = false;
1911
1955
  }
1912
1956
  /** Initialize the SDK with configuration */
1913
1957
  init(config) {
@@ -1993,15 +2037,9 @@ var DeepdotsPopups = class {
1993
2037
  if (this.tracking?.isTrackingEnabled()) {
1994
2038
  this.crashReporter.install();
1995
2039
  }
1996
- const cachedGeo = readCachedGeo(storage, Date.now());
1997
- if (cachedGeo) this.analytics.updateDevice(cachedGeo);
1998
- fetchGeo().then((geo) => {
1999
- if (geo) {
2000
- this.analytics?.updateDevice(geo);
2001
- writeCachedGeo(storage, geo, Date.now());
2002
- }
2003
- }).catch(() => {
2004
- });
2040
+ this.geoEnabled = config.geolocation ?? true;
2041
+ this.geoWanted = this.geoEnabled && !!config.analytics;
2042
+ this.resolveGeo();
2005
2043
  this.navObserver = new NavigationObserver();
2006
2044
  this.navObserver.onVisit((v) => this.track("deepdots_page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
2007
2045
  this.navObserver.install();
@@ -2043,6 +2081,7 @@ var DeepdotsPopups = class {
2043
2081
  if (!enabled) this.closeSession("tracking_disabled");
2044
2082
  this.tracking?.setTrackingEnabled(enabled);
2045
2083
  if (enabled) this.openSession();
2084
+ if (enabled) this.resolveGeo();
2046
2085
  this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
2047
2086
  }
2048
2087
  // ───────── Analytics (canal separado del feedback, vinculado por user_id) ─────────
@@ -2239,6 +2278,7 @@ var DeepdotsPopups = class {
2239
2278
  if (this.sessionOpen) return;
2240
2279
  if (!this.tracking?.isTrackingEnabled()) return;
2241
2280
  this.sessionOpen = true;
2281
+ this.lastEngagementAt = Date.now();
2242
2282
  this.track("deepdots_session_start", {});
2243
2283
  this.navObserver?.restart();
2244
2284
  if (this.isDocumentVisible()) this.engagement?.resume();
@@ -2269,7 +2309,76 @@ var DeepdotsPopups = class {
2269
2309
  flushEngagement() {
2270
2310
  if (!this.tracking?.isTrackingEnabled()) return;
2271
2311
  const ms = this.engagement?.consume() ?? 0;
2272
- if (ms > 0) this.track("deepdots_user_engagement", { engagement_time_msec: ms });
2312
+ if (ms > 0) {
2313
+ this.track("deepdots_user_engagement", { engagement_time_msec: ms });
2314
+ this.lastEngagementAt = Date.now();
2315
+ }
2316
+ }
2317
+ /** Tick del flush periódico: latido de engagement si toca, y envío de lo acumulado. */
2318
+ onAnalyticsFlushTick() {
2319
+ if (this.sessionOpen && this.isDocumentVisible() && Date.now() - this.lastEngagementAt >= ANALYTICS_HEARTBEAT_MS) {
2320
+ this.flushEngagement();
2321
+ }
2322
+ this.flushAnalytics();
2323
+ }
2324
+ /**
2325
+ * La pestaña vuelve a verse. Tras más de ANALYTICS_SESSION_TIMEOUT_MS oculta, la sesión
2326
+ * anterior termina y empieza otra: seguir con el mismo sessionId mezclaba dos visitas y,
2327
+ * si el backend ya la había cerrado por inactividad, la partía en dos feedbacks.
2328
+ * - Oculta menos de ANALYTICS_SESSION_DISCARD_MS: el backend aún la tiene abierta → se
2329
+ * cierra con normalidad (page_view de la pantalla, session_end `idle_timeout`, completed).
2330
+ * - Más: ya la cerró el backend → se descarta sin enviar nada.
2331
+ */
2332
+ onDocumentVisible() {
2333
+ const hiddenFor = this.hiddenAt === null ? 0 : Date.now() - this.hiddenAt;
2334
+ this.hiddenAt = null;
2335
+ if (!this.sessionOpen) return;
2336
+ if (hiddenFor < ANALYTICS_SESSION_TIMEOUT_MS) {
2337
+ this.engagement?.resume();
2338
+ return;
2339
+ }
2340
+ if (hiddenFor < ANALYTICS_SESSION_DISCARD_MS) {
2341
+ this.closeSession("idle_timeout");
2342
+ } else {
2343
+ this.discardSession();
2344
+ }
2345
+ this.openSession();
2346
+ }
2347
+ /** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
2348
+ discardSession() {
2349
+ this.sessionOpen = false;
2350
+ this.navObserver?.discard();
2351
+ this.analytics?.discardSession(this.analyticsIdentity());
2352
+ this.engagement?.consume();
2353
+ this.engagement?.pause();
2354
+ this.analyticsFeedbackSessionId = void 0;
2355
+ this.tracking?.setSessionId(null);
2356
+ this.log("tracking \xB7 session discarded after", ANALYTICS_SESSION_DISCARD_MS / 6e4, "min hidden");
2357
+ }
2358
+ /**
2359
+ * Geolocalización por IP (país/ciudad para analytics). La caché persistente es local y se
2360
+ * aplica siempre que `geolocation` no esté desactivado. El lookup a terceros (ipapi.co →
2361
+ * ipwho.is → ipinfo.io, fallback con timeout) solo se hace si el dato se va a ENVIAR:
2362
+ * analytics configurado + tracking activo + caché ausente o caducada. Una vez por instancia.
2363
+ */
2364
+ resolveGeo() {
2365
+ const storage = this.storage;
2366
+ if (!this.geoEnabled || !storage || !this.analytics) return;
2367
+ const cachedGeo = readCachedGeo(storage, Date.now());
2368
+ if (cachedGeo) {
2369
+ this.analytics.updateDevice(cachedGeo);
2370
+ return;
2371
+ }
2372
+ if (!this.geoWanted || this.geoLookupStarted) return;
2373
+ if (!this.tracking?.isTrackingEnabled()) return;
2374
+ this.geoLookupStarted = true;
2375
+ fetchGeo().then((geo) => {
2376
+ if (geo) {
2377
+ this.analytics?.updateDevice(geo);
2378
+ writeCachedGeo(storage, geo, Date.now());
2379
+ }
2380
+ }).catch(() => {
2381
+ });
2273
2382
  }
2274
2383
  analyticsIdentity() {
2275
2384
  return {
@@ -2279,8 +2388,8 @@ var DeepdotsPopups = class {
2279
2388
  }
2280
2389
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
2281
2390
  setupAnalyticsFlush() {
2282
- if (typeof document === "undefined" || typeof window === "undefined") return;
2283
2391
  this.startAnalyticsFlushTimer();
2392
+ if (typeof document === "undefined" || typeof window === "undefined") return;
2284
2393
  window.addEventListener("pagehide", () => {
2285
2394
  clearInterval(this.analyticsFlushTimer);
2286
2395
  this.closeSession("page_hide");
@@ -2295,14 +2404,15 @@ var DeepdotsPopups = class {
2295
2404
  this.flushEngagement();
2296
2405
  this.engagement?.pause();
2297
2406
  this.flushAnalytics();
2407
+ this.hiddenAt = Date.now();
2298
2408
  } else if (document.visibilityState === "visible") {
2299
- if (this.sessionOpen) this.engagement?.resume();
2409
+ this.onDocumentVisible();
2300
2410
  }
2301
2411
  });
2302
2412
  }
2303
2413
  startAnalyticsFlushTimer() {
2304
2414
  clearInterval(this.analyticsFlushTimer);
2305
- this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
2415
+ this.analyticsFlushTimer = setInterval(() => this.onAnalyticsFlushTick(), ANALYTICS_FLUSH_INTERVAL_MS);
2306
2416
  }
2307
2417
  /** Enable auto-launch functionality with configured triggers */
2308
2418
  autoLaunch() {
@@ -2806,7 +2916,7 @@ var DeepdotsPopups = class {
2806
2916
  }
2807
2917
  if (!this.popupContainer) return;
2808
2918
  const container = this.popupContainer;
2809
- void import("./renderPopup-ERUKQE4U.mjs").then(({ renderPopup }) => {
2919
+ void import("./renderPopup-3CZ6WTXF.mjs").then(({ renderPopup }) => {
2810
2920
  renderPopup(
2811
2921
  container,
2812
2922
  surveyId,