@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.
@@ -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) {
@@ -1995,15 +2039,9 @@ var DeepdotsPopups = class {
1995
2039
  if (this.tracking?.isTrackingEnabled()) {
1996
2040
  this.crashReporter.install();
1997
2041
  }
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
- });
2042
+ this.geoEnabled = config.geolocation ?? true;
2043
+ this.geoWanted = this.geoEnabled && !!config.analytics;
2044
+ this.resolveGeo();
2007
2045
  this.navObserver = new NavigationObserver();
2008
2046
  this.navObserver.onVisit((v) => this.track("deepdots_page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
2009
2047
  this.navObserver.install();
@@ -2045,6 +2083,7 @@ var DeepdotsPopups = class {
2045
2083
  if (!enabled) this.closeSession("tracking_disabled");
2046
2084
  this.tracking?.setTrackingEnabled(enabled);
2047
2085
  if (enabled) this.openSession();
2086
+ if (enabled) this.resolveGeo();
2048
2087
  this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
2049
2088
  }
2050
2089
  // ───────── Analytics (canal separado del feedback, vinculado por user_id) ─────────
@@ -2241,6 +2280,7 @@ var DeepdotsPopups = class {
2241
2280
  if (this.sessionOpen) return;
2242
2281
  if (!this.tracking?.isTrackingEnabled()) return;
2243
2282
  this.sessionOpen = true;
2283
+ this.lastEngagementAt = Date.now();
2244
2284
  this.track("deepdots_session_start", {});
2245
2285
  this.navObserver?.restart();
2246
2286
  if (this.isDocumentVisible()) this.engagement?.resume();
@@ -2271,7 +2311,76 @@ var DeepdotsPopups = class {
2271
2311
  flushEngagement() {
2272
2312
  if (!this.tracking?.isTrackingEnabled()) return;
2273
2313
  const ms = this.engagement?.consume() ?? 0;
2274
- 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
+ });
2275
2384
  }
2276
2385
  analyticsIdentity() {
2277
2386
  return {
@@ -2281,8 +2390,8 @@ var DeepdotsPopups = class {
2281
2390
  }
2282
2391
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
2283
2392
  setupAnalyticsFlush() {
2284
- if (typeof document === "undefined" || typeof window === "undefined") return;
2285
2393
  this.startAnalyticsFlushTimer();
2394
+ if (typeof document === "undefined" || typeof window === "undefined") return;
2286
2395
  window.addEventListener("pagehide", () => {
2287
2396
  clearInterval(this.analyticsFlushTimer);
2288
2397
  this.closeSession("page_hide");
@@ -2297,14 +2406,15 @@ var DeepdotsPopups = class {
2297
2406
  this.flushEngagement();
2298
2407
  this.engagement?.pause();
2299
2408
  this.flushAnalytics();
2409
+ this.hiddenAt = Date.now();
2300
2410
  } else if (document.visibilityState === "visible") {
2301
- if (this.sessionOpen) this.engagement?.resume();
2411
+ this.onDocumentVisible();
2302
2412
  }
2303
2413
  });
2304
2414
  }
2305
2415
  startAnalyticsFlushTimer() {
2306
2416
  clearInterval(this.analyticsFlushTimer);
2307
- this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
2417
+ this.analyticsFlushTimer = setInterval(() => this.onAnalyticsFlushTick(), ANALYTICS_FLUSH_INTERVAL_MS);
2308
2418
  }
2309
2419
  /** Enable auto-launch functionality with configured triggers */
2310
2420
  autoLaunch() {