@magicfeedback/popup-sdk 1.8.2 → 1.8.4

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) {
@@ -1965,7 +2009,9 @@ var DeepdotsPopups = class {
1965
2009
  onSessionReset: () => {
1966
2010
  this.analyticsFeedbackSessionId = void 0;
1967
2011
  }
1968
- }) : createDryRunSink((...a) => this.logger.log(...a));
2012
+ }) : createDryRunSink((...a) => {
2013
+ if (this.config?.debug) this.logger.log(...a);
2014
+ });
1969
2015
  const device = config.device ?? collectDeviceInfo(config.appVersion);
1970
2016
  this.language = resolveLanguage({
1971
2017
  explicit: config.language,
@@ -1993,15 +2039,9 @@ var DeepdotsPopups = class {
1993
2039
  if (this.tracking?.isTrackingEnabled()) {
1994
2040
  this.crashReporter.install();
1995
2041
  }
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
- });
2042
+ this.geoEnabled = config.geolocation ?? true;
2043
+ this.geoWanted = this.geoEnabled && !!config.analytics;
2044
+ this.resolveGeo();
2005
2045
  this.navObserver = new NavigationObserver();
2006
2046
  this.navObserver.onVisit((v) => this.track("deepdots_page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
2007
2047
  this.navObserver.install();
@@ -2043,6 +2083,7 @@ var DeepdotsPopups = class {
2043
2083
  if (!enabled) this.closeSession("tracking_disabled");
2044
2084
  this.tracking?.setTrackingEnabled(enabled);
2045
2085
  if (enabled) this.openSession();
2086
+ if (enabled) this.resolveGeo();
2046
2087
  this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
2047
2088
  }
2048
2089
  // ───────── Analytics (canal separado del feedback, vinculado por user_id) ─────────
@@ -2239,6 +2280,7 @@ var DeepdotsPopups = class {
2239
2280
  if (this.sessionOpen) return;
2240
2281
  if (!this.tracking?.isTrackingEnabled()) return;
2241
2282
  this.sessionOpen = true;
2283
+ this.lastEngagementAt = Date.now();
2242
2284
  this.track("deepdots_session_start", {});
2243
2285
  this.navObserver?.restart();
2244
2286
  if (this.isDocumentVisible()) this.engagement?.resume();
@@ -2269,7 +2311,76 @@ var DeepdotsPopups = class {
2269
2311
  flushEngagement() {
2270
2312
  if (!this.tracking?.isTrackingEnabled()) return;
2271
2313
  const ms = this.engagement?.consume() ?? 0;
2272
- if (ms > 0) this.track("deepdots_user_engagement", { engagement_time_msec: ms });
2314
+ if (ms > 0) {
2315
+ this.track("deepdots_user_engagement", { engagement_time_msec: ms });
2316
+ this.lastEngagementAt = Date.now();
2317
+ }
2318
+ }
2319
+ /** Tick del flush periódico: latido de engagement si toca, y envío de lo acumulado. */
2320
+ onAnalyticsFlushTick() {
2321
+ if (this.sessionOpen && this.isDocumentVisible() && Date.now() - this.lastEngagementAt >= ANALYTICS_HEARTBEAT_MS) {
2322
+ this.flushEngagement();
2323
+ }
2324
+ this.flushAnalytics();
2325
+ }
2326
+ /**
2327
+ * La pestaña vuelve a verse. Tras más de ANALYTICS_SESSION_TIMEOUT_MS oculta, la sesión
2328
+ * anterior termina y empieza otra: seguir con el mismo sessionId mezclaba dos visitas y,
2329
+ * si el backend ya la había cerrado por inactividad, la partía en dos feedbacks.
2330
+ * - Oculta menos de ANALYTICS_SESSION_DISCARD_MS: el backend aún la tiene abierta → se
2331
+ * cierra con normalidad (page_view de la pantalla, session_end `idle_timeout`, completed).
2332
+ * - Más: ya la cerró el backend → se descarta sin enviar nada.
2333
+ */
2334
+ onDocumentVisible() {
2335
+ const hiddenFor = this.hiddenAt === null ? 0 : Date.now() - this.hiddenAt;
2336
+ this.hiddenAt = null;
2337
+ if (!this.sessionOpen) return;
2338
+ if (hiddenFor < ANALYTICS_SESSION_TIMEOUT_MS) {
2339
+ this.engagement?.resume();
2340
+ return;
2341
+ }
2342
+ if (hiddenFor < ANALYTICS_SESSION_DISCARD_MS) {
2343
+ this.closeSession("idle_timeout");
2344
+ } else {
2345
+ this.discardSession();
2346
+ }
2347
+ this.openSession();
2348
+ }
2349
+ /** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
2350
+ discardSession() {
2351
+ this.sessionOpen = false;
2352
+ this.navObserver?.discard();
2353
+ this.analytics?.discardSession(this.analyticsIdentity());
2354
+ this.engagement?.consume();
2355
+ this.engagement?.pause();
2356
+ this.analyticsFeedbackSessionId = void 0;
2357
+ this.tracking?.setSessionId(null);
2358
+ this.log("tracking \xB7 session discarded after", ANALYTICS_SESSION_DISCARD_MS / 6e4, "min hidden");
2359
+ }
2360
+ /**
2361
+ * Geolocalización por IP (país/ciudad para analytics). La caché persistente es local y se
2362
+ * aplica siempre que `geolocation` no esté desactivado. El lookup a terceros (ipapi.co →
2363
+ * ipwho.is → ipinfo.io, fallback con timeout) solo se hace si el dato se va a ENVIAR:
2364
+ * analytics configurado + tracking activo + caché ausente o caducada. Una vez por instancia.
2365
+ */
2366
+ resolveGeo() {
2367
+ const storage = this.storage;
2368
+ if (!this.geoEnabled || !storage || !this.analytics) return;
2369
+ const cachedGeo = readCachedGeo(storage, Date.now());
2370
+ if (cachedGeo) {
2371
+ this.analytics.updateDevice(cachedGeo);
2372
+ return;
2373
+ }
2374
+ if (!this.geoWanted || this.geoLookupStarted) return;
2375
+ if (!this.tracking?.isTrackingEnabled()) return;
2376
+ this.geoLookupStarted = true;
2377
+ fetchGeo().then((geo) => {
2378
+ if (geo) {
2379
+ this.analytics?.updateDevice(geo);
2380
+ writeCachedGeo(storage, geo, Date.now());
2381
+ }
2382
+ }).catch(() => {
2383
+ });
2273
2384
  }
2274
2385
  analyticsIdentity() {
2275
2386
  return {
@@ -2279,8 +2390,8 @@ var DeepdotsPopups = class {
2279
2390
  }
2280
2391
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
2281
2392
  setupAnalyticsFlush() {
2282
- if (typeof document === "undefined" || typeof window === "undefined") return;
2283
2393
  this.startAnalyticsFlushTimer();
2394
+ if (typeof document === "undefined" || typeof window === "undefined") return;
2284
2395
  window.addEventListener("pagehide", () => {
2285
2396
  clearInterval(this.analyticsFlushTimer);
2286
2397
  this.closeSession("page_hide");
@@ -2295,14 +2406,15 @@ var DeepdotsPopups = class {
2295
2406
  this.flushEngagement();
2296
2407
  this.engagement?.pause();
2297
2408
  this.flushAnalytics();
2409
+ this.hiddenAt = Date.now();
2298
2410
  } else if (document.visibilityState === "visible") {
2299
- if (this.sessionOpen) this.engagement?.resume();
2411
+ this.onDocumentVisible();
2300
2412
  }
2301
2413
  });
2302
2414
  }
2303
2415
  startAnalyticsFlushTimer() {
2304
2416
  clearInterval(this.analyticsFlushTimer);
2305
- this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
2417
+ this.analyticsFlushTimer = setInterval(() => this.onAnalyticsFlushTick(), ANALYTICS_FLUSH_INTERVAL_MS);
2306
2418
  }
2307
2419
  /** Enable auto-launch functionality with configured triggers */
2308
2420
  autoLaunch() {
@@ -2806,7 +2918,7 @@ var DeepdotsPopups = class {
2806
2918
  }
2807
2919
  if (!this.popupContainer) return;
2808
2920
  const container = this.popupContainer;
2809
- void import("./renderPopup-ERUKQE4U.mjs").then(({ renderPopup }) => {
2921
+ void import("./renderPopup-3CZ6WTXF.mjs").then(({ renderPopup }) => {
2810
2922
  renderPopup(
2811
2923
  container,
2812
2924
  surveyId,