@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.
@@ -968,10 +968,13 @@ function createDefaultRenderer() {
968
968
 
969
969
  // src/analytics/analytics-manager.ts
970
970
  function createDryRunSink(log = console.log) {
971
- return (payload) => log(
972
- "[DeepdotsAnalytics] (dry-run \xB7 NOT sent \xB7 no init.analytics) POST /sdk/feedback \u2192",
973
- JSON.stringify(payload, null, 2)
974
- );
971
+ return (payload, meta) => {
972
+ if (meta?.resetSession) return;
973
+ log(
974
+ "[DeepdotsAnalytics] (dry-run \xB7 NOT sent \xB7 no init.analytics) POST /sdk/feedback \u2192",
975
+ JSON.stringify(payload, null, 2)
976
+ );
977
+ };
975
978
  }
976
979
  var dryRunSink = createDryRunSink();
977
980
  var AnalyticsManager = class {
@@ -1038,6 +1041,19 @@ var AnalyticsManager = class {
1038
1041
  this.exitMiniService(name);
1039
1042
  }
1040
1043
  }
1044
+ /**
1045
+ * Olvida la sesión en curso SIN emitir nada: vacía el buffer y los mini-services activos
1046
+ * (sin `mini_service_exit`). Para una sesión que el backend ya cerró por inactividad: lo
1047
+ * que quedara pendiente pertenecía a ella y no debe llegar con la sesión nueva.
1048
+ */
1049
+ discardSession(identity) {
1050
+ this.events = [];
1051
+ this.activeMiniServices.clear();
1052
+ try {
1053
+ void this.sink({ ...this.buildPayload(identity), events: [] }, { resetSession: true });
1054
+ } catch {
1055
+ }
1056
+ }
1041
1057
  /**
1042
1058
  * Olvida lo que pertenecía al usuario anterior (user attributes + métricas) al cambiar de
1043
1059
  * usuario. NO toca el buffer de eventos: el cierre de sesión ya lo vació con la identidad
@@ -1247,6 +1263,12 @@ function createFeedbackSink(options) {
1247
1263
  }
1248
1264
  };
1249
1265
  return async (envelope, meta) => {
1266
+ if (meta?.resetSession) {
1267
+ feedbackSessionId = void 0;
1268
+ firstPostInFlight = null;
1269
+ options.onSessionReset?.();
1270
+ return;
1271
+ }
1250
1272
  const final = meta?.final === true;
1251
1273
  const sessionEnd = meta?.sessionEnd === true;
1252
1274
  if (!feedbackSessionId && firstPostInFlight && !final) {
@@ -1313,6 +1335,14 @@ var NavigationObserver = class {
1313
1335
  this.closeCurrent();
1314
1336
  this.currentScreen = null;
1315
1337
  }
1338
+ /**
1339
+ * Olvida la pantalla actual SIN emitir su visita. Para una sesión que el backend ya dio
1340
+ * por cerrada: su visita no tiene dónde ir, y mandarla con la sesión nueva la contaría
1341
+ * donde no ocurrió. Después, `restart()` vuelve a empezar en la pantalla actual.
1342
+ */
1343
+ discard() {
1344
+ this.currentScreen = null;
1345
+ }
1316
1346
  /**
1317
1347
  * Retoma el seguimiento en la pantalla actual tras un `stop()`, cuando se abre una sesión
1318
1348
  * nueva sin navegar (cambio de usuario, vuelta de la bfcache). No-op si nunca se instaló
@@ -1842,6 +1872,10 @@ var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
1842
1872
  var DAY_IN_MS = 24 * 60 * 60 * 1e3;
1843
1873
  var ANALYTICS_MAX_BATCH_SIZE = 20;
1844
1874
  var ANALYTICS_FLUSH_INTERVAL_MS = 3e4;
1875
+ var ANALYTICS_HEARTBEAT_MS = 5 * 6e4;
1876
+ var ANALYTICS_SESSION_TIMEOUT_MS = 30 * 6e4;
1877
+ var ANALYTICS_SERVER_IDLE_MS = 60 * 6e4;
1878
+ var ANALYTICS_SESSION_DISCARD_MS = ANALYTICS_SERVER_IDLE_MS - 10 * 6e4;
1845
1879
  var RESERVED_EVENT_PREFIX = "deepdots_";
1846
1880
  var CUSTOM_EVENT_PREFIX = "deepdots_event_";
1847
1881
  var DeepdotsPopups = class {
@@ -1905,11 +1939,21 @@ var DeepdotsPopups = class {
1905
1939
  * nueva al volver a foreground o al conceder el consentimiento más tarde.
1906
1940
  */
1907
1941
  this.sessionOpen = false;
1942
+ /** Último `user_engagement` emitido (ms epoch): marca el latido. */
1943
+ this.lastEngagementAt = 0;
1944
+ /** Cuándo se ocultó la pestaña por última vez (ms epoch); null si está visible. */
1945
+ this.hiddenAt = null;
1908
1946
  /**
1909
1947
  * Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
1910
1948
  * verdad para el context de analytics Y para la segmentación por idioma de los popups.
1911
1949
  */
1912
1950
  this.language = void 0;
1951
+ /** `geolocation` del init: si es false no se usa ni la caché ni el lookup. */
1952
+ this.geoEnabled = true;
1953
+ /** Solo se consulta a terceros si el país/ciudad se va a enviar (analytics configurado). */
1954
+ this.geoWanted = false;
1955
+ /** Evita más de un lookup por instancia (el consentimiento puede ir y venir). */
1956
+ this.geoLookupStarted = false;
1913
1957
  }
1914
1958
  /** Initialize the SDK with configuration */
1915
1959
  init(config) {
@@ -1967,7 +2011,9 @@ var DeepdotsPopups = class {
1967
2011
  onSessionReset: () => {
1968
2012
  this.analyticsFeedbackSessionId = void 0;
1969
2013
  }
1970
- }) : createDryRunSink((...a) => this.logger.log(...a));
2014
+ }) : createDryRunSink((...a) => {
2015
+ if (this.config?.debug) this.logger.log(...a);
2016
+ });
1971
2017
  const device = config.device ?? collectDeviceInfo(config.appVersion);
1972
2018
  this.language = resolveLanguage({
1973
2019
  explicit: config.language,
@@ -1995,15 +2041,9 @@ var DeepdotsPopups = class {
1995
2041
  if (this.tracking?.isTrackingEnabled()) {
1996
2042
  this.crashReporter.install();
1997
2043
  }
1998
- const cachedGeo = readCachedGeo(storage, Date.now());
1999
- if (cachedGeo) this.analytics.updateDevice(cachedGeo);
2000
- fetchGeo().then((geo) => {
2001
- if (geo) {
2002
- this.analytics?.updateDevice(geo);
2003
- writeCachedGeo(storage, geo, Date.now());
2004
- }
2005
- }).catch(() => {
2006
- });
2044
+ this.geoEnabled = config.geolocation ?? true;
2045
+ this.geoWanted = this.geoEnabled && !!config.analytics;
2046
+ this.resolveGeo();
2007
2047
  this.navObserver = new NavigationObserver();
2008
2048
  this.navObserver.onVisit((v) => this.track("deepdots_page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
2009
2049
  this.navObserver.install();
@@ -2045,6 +2085,7 @@ var DeepdotsPopups = class {
2045
2085
  if (!enabled) this.closeSession("tracking_disabled");
2046
2086
  this.tracking?.setTrackingEnabled(enabled);
2047
2087
  if (enabled) this.openSession();
2088
+ if (enabled) this.resolveGeo();
2048
2089
  this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
2049
2090
  }
2050
2091
  // ───────── Analytics (canal separado del feedback, vinculado por user_id) ─────────
@@ -2241,6 +2282,7 @@ var DeepdotsPopups = class {
2241
2282
  if (this.sessionOpen) return;
2242
2283
  if (!this.tracking?.isTrackingEnabled()) return;
2243
2284
  this.sessionOpen = true;
2285
+ this.lastEngagementAt = Date.now();
2244
2286
  this.track("deepdots_session_start", {});
2245
2287
  this.navObserver?.restart();
2246
2288
  if (this.isDocumentVisible()) this.engagement?.resume();
@@ -2271,7 +2313,76 @@ var DeepdotsPopups = class {
2271
2313
  flushEngagement() {
2272
2314
  if (!this.tracking?.isTrackingEnabled()) return;
2273
2315
  const ms = this.engagement?.consume() ?? 0;
2274
- if (ms > 0) this.track("deepdots_user_engagement", { engagement_time_msec: ms });
2316
+ if (ms > 0) {
2317
+ this.track("deepdots_user_engagement", { engagement_time_msec: ms });
2318
+ this.lastEngagementAt = Date.now();
2319
+ }
2320
+ }
2321
+ /** Tick del flush periódico: latido de engagement si toca, y envío de lo acumulado. */
2322
+ onAnalyticsFlushTick() {
2323
+ if (this.sessionOpen && this.isDocumentVisible() && Date.now() - this.lastEngagementAt >= ANALYTICS_HEARTBEAT_MS) {
2324
+ this.flushEngagement();
2325
+ }
2326
+ this.flushAnalytics();
2327
+ }
2328
+ /**
2329
+ * La pestaña vuelve a verse. Tras más de ANALYTICS_SESSION_TIMEOUT_MS oculta, la sesión
2330
+ * anterior termina y empieza otra: seguir con el mismo sessionId mezclaba dos visitas y,
2331
+ * si el backend ya la había cerrado por inactividad, la partía en dos feedbacks.
2332
+ * - Oculta menos de ANALYTICS_SESSION_DISCARD_MS: el backend aún la tiene abierta → se
2333
+ * cierra con normalidad (page_view de la pantalla, session_end `idle_timeout`, completed).
2334
+ * - Más: ya la cerró el backend → se descarta sin enviar nada.
2335
+ */
2336
+ onDocumentVisible() {
2337
+ const hiddenFor = this.hiddenAt === null ? 0 : Date.now() - this.hiddenAt;
2338
+ this.hiddenAt = null;
2339
+ if (!this.sessionOpen) return;
2340
+ if (hiddenFor < ANALYTICS_SESSION_TIMEOUT_MS) {
2341
+ this.engagement?.resume();
2342
+ return;
2343
+ }
2344
+ if (hiddenFor < ANALYTICS_SESSION_DISCARD_MS) {
2345
+ this.closeSession("idle_timeout");
2346
+ } else {
2347
+ this.discardSession();
2348
+ }
2349
+ this.openSession();
2350
+ }
2351
+ /** Olvida la sesión abierta sin enviar nada (ver onDocumentVisible). */
2352
+ discardSession() {
2353
+ this.sessionOpen = false;
2354
+ this.navObserver?.discard();
2355
+ this.analytics?.discardSession(this.analyticsIdentity());
2356
+ this.engagement?.consume();
2357
+ this.engagement?.pause();
2358
+ this.analyticsFeedbackSessionId = void 0;
2359
+ this.tracking?.setSessionId(null);
2360
+ this.log("tracking \xB7 session discarded after", ANALYTICS_SESSION_DISCARD_MS / 6e4, "min hidden");
2361
+ }
2362
+ /**
2363
+ * Geolocalización por IP (país/ciudad para analytics). La caché persistente es local y se
2364
+ * aplica siempre que `geolocation` no esté desactivado. El lookup a terceros (ipapi.co →
2365
+ * ipwho.is → ipinfo.io, fallback con timeout) solo se hace si el dato se va a ENVIAR:
2366
+ * analytics configurado + tracking activo + caché ausente o caducada. Una vez por instancia.
2367
+ */
2368
+ resolveGeo() {
2369
+ const storage = this.storage;
2370
+ if (!this.geoEnabled || !storage || !this.analytics) return;
2371
+ const cachedGeo = readCachedGeo(storage, Date.now());
2372
+ if (cachedGeo) {
2373
+ this.analytics.updateDevice(cachedGeo);
2374
+ return;
2375
+ }
2376
+ if (!this.geoWanted || this.geoLookupStarted) return;
2377
+ if (!this.tracking?.isTrackingEnabled()) return;
2378
+ this.geoLookupStarted = true;
2379
+ fetchGeo().then((geo) => {
2380
+ if (geo) {
2381
+ this.analytics?.updateDevice(geo);
2382
+ writeCachedGeo(storage, geo, Date.now());
2383
+ }
2384
+ }).catch(() => {
2385
+ });
2275
2386
  }
2276
2387
  analyticsIdentity() {
2277
2388
  return {
@@ -2281,8 +2392,8 @@ var DeepdotsPopups = class {
2281
2392
  }
2282
2393
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
2283
2394
  setupAnalyticsFlush() {
2284
- if (typeof document === "undefined" || typeof window === "undefined") return;
2285
2395
  this.startAnalyticsFlushTimer();
2396
+ if (typeof document === "undefined" || typeof window === "undefined") return;
2286
2397
  window.addEventListener("pagehide", () => {
2287
2398
  clearInterval(this.analyticsFlushTimer);
2288
2399
  this.closeSession("page_hide");
@@ -2297,14 +2408,15 @@ var DeepdotsPopups = class {
2297
2408
  this.flushEngagement();
2298
2409
  this.engagement?.pause();
2299
2410
  this.flushAnalytics();
2411
+ this.hiddenAt = Date.now();
2300
2412
  } else if (document.visibilityState === "visible") {
2301
- if (this.sessionOpen) this.engagement?.resume();
2413
+ this.onDocumentVisible();
2302
2414
  }
2303
2415
  });
2304
2416
  }
2305
2417
  startAnalyticsFlushTimer() {
2306
2418
  clearInterval(this.analyticsFlushTimer);
2307
- this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
2419
+ this.analyticsFlushTimer = setInterval(() => this.onAnalyticsFlushTick(), ANALYTICS_FLUSH_INTERVAL_MS);
2308
2420
  }
2309
2421
  /** Enable auto-launch functionality with configured triggers */
2310
2422
  autoLaunch() {