@magicfeedback/popup-sdk 1.1.1 → 1.1.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.
@@ -48,12 +48,13 @@ function createDefaultStorage() {
48
48
  }
49
49
  return new InMemoryStorage();
50
50
  }
51
- function buildSurveyIdentity(userId, sessionId, miniService = null) {
51
+ function buildSurveyIdentity(userId, sessionId, miniService = null, analyticsFeedbackSessionId = null) {
52
52
  const profile = userId ? [{ key: "external-user-id", value: [userId] }] : [];
53
53
  const metadata = [];
54
54
  if (sessionId) metadata.push({ key: "session_id", value: [sessionId] });
55
55
  if (userId) metadata.push({ key: "user_id", value: [userId] });
56
56
  if (miniService) metadata.push({ key: "mini_service", value: [miniService] });
57
+ if (analyticsFeedbackSessionId) metadata.push({ key: "deepdots_analytics_feedback_session_id", value: [analyticsFeedbackSessionId] });
57
58
  return { profile, metadata };
58
59
  }
59
60
  function defaultUuid() {
@@ -1063,7 +1064,7 @@ function ensureResponsiveStyles(_popup) {
1063
1064
  `;
1064
1065
  document.head.appendChild(style);
1065
1066
  }
1066
- async function renderPopup(container, surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService) {
1067
+ async function renderPopup(container, surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId) {
1067
1068
  let surveyCompletedEmitted = false;
1068
1069
  let stylesInjected = false;
1069
1070
  const isDark = style?.theme === "dark";
@@ -1374,7 +1375,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
1374
1375
  env: env === "production" ? "prod" : "dev"
1375
1376
  }
1376
1377
  );
1377
- const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null);
1378
+ const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null, analyticsFeedbackSessionId ?? null);
1378
1379
  formInstance = import_native.default.form(surveyId, productId, profile, metadata);
1379
1380
  const generateOptions = {
1380
1381
  addButton: false,
@@ -1787,12 +1788,12 @@ var ReactNativePopupRenderer = class {
1787
1788
  }
1788
1789
  init() {
1789
1790
  }
1790
- show(surveyId, productId, _actions, emit, onClose, env = "production", userId, _style, sessionId, miniService) {
1791
+ show(surveyId, productId, _actions, emit, onClose, env = "production", userId, _style, sessionId, miniService, analyticsFeedbackSessionId) {
1791
1792
  this.emitFn = emit;
1792
1793
  this.onCloseFn = onClose;
1793
1794
  this.currentSurveyId = surveyId;
1794
1795
  this.partialEmitted = false;
1795
- const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null);
1796
+ const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null, analyticsFeedbackSessionId ?? null);
1796
1797
  const html = buildSurveyHtml({ surveyId, productId, env, profile, metadata });
1797
1798
  if (this.options.onShow) {
1798
1799
  this.options.onShow({ surveyId, productId, html });
@@ -1883,14 +1884,14 @@ var BrowserPopupRenderer = class {
1883
1884
  document.body.appendChild(this.container);
1884
1885
  }
1885
1886
  }
1886
- show(surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService) {
1887
+ show(surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId) {
1887
1888
  if (this.visible) return;
1888
1889
  if (!this.container || !document.body.contains(this.container)) this.init();
1889
1890
  if (!this.container) return;
1890
1891
  this.visible = true;
1891
1892
  const container = this.container;
1892
1893
  void Promise.resolve().then(() => (init_renderPopup(), renderPopup_exports)).then(({ renderPopup: renderPopup2 }) => {
1893
- renderPopup2(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService);
1894
+ renderPopup2(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService, analyticsFeedbackSessionId);
1894
1895
  });
1895
1896
  }
1896
1897
  hide() {
@@ -1924,14 +1925,20 @@ var AnalyticsManager = class {
1924
1925
  constructor(options = {}) {
1925
1926
  this.events = [];
1926
1927
  this.attributes = {};
1927
- this.miniService = null;
1928
- this.miniServiceEnteredAt = 0;
1928
+ /** Mini-services activos: nombre → timestamp de entrada. El orden de inserción marca el "más reciente". */
1929
+ this.activeMiniServices = /* @__PURE__ */ new Map();
1929
1930
  this.sink = options.sink ?? dryRunSink;
1930
1931
  this.now = options.now ?? (() => Date.now());
1931
1932
  this.publicKey = options.publicKey;
1932
1933
  this.language = options.language;
1933
1934
  this.device = options.device;
1934
1935
  this.platform = options.platform ?? "web";
1936
+ this.maxBatchSize = options.maxBatchSize ?? 20;
1937
+ this.onFlushNeeded = options.onFlushNeeded;
1938
+ }
1939
+ /** Actualiza campos de device en runtime (p.ej. country/city tras resolver geo). */
1940
+ updateDevice(partial) {
1941
+ this.device = { ...this.device, ...partial };
1935
1942
  }
1936
1943
  /** Mezcla user attributes (se coercionan a string). Mutable en runtime. */
1937
1944
  setUserAttributes(attrs) {
@@ -1940,24 +1947,38 @@ var AnalyticsManager = class {
1940
1947
  this.attributes[k] = String(v);
1941
1948
  }
1942
1949
  }
1943
- /** Marca el inicio de un mini-service; etiqueta los eventos siguientes con `mini_service`. */
1950
+ /**
1951
+ * Marca el inicio de un mini-service; etiqueta los eventos siguientes con `mini_service`.
1952
+ * Admite varios activos a la vez (concurrencia); el "actual" para etiquetar es el más reciente.
1953
+ * Reentrar con un nombre ya activo refresca su orden y su tiempo de entrada.
1954
+ */
1944
1955
  enterMiniService(name, entryPointType) {
1945
- this.miniService = name;
1946
- this.miniServiceEnteredAt = this.now();
1947
- this.track("mini_service_enter", { entry_point_type: entryPointType ?? null });
1948
- }
1949
- /** Cierra el mini-service activo emitiendo `mini_service_exit` con su duración (#27). No-op si no hay ninguno. */
1950
- exitMiniService() {
1951
- const name = this.miniService;
1952
- if (name == null) return;
1953
- const durationSeconds = Math.max(0, Math.round((this.now() - this.miniServiceEnteredAt) / 1e3));
1954
- this.miniService = null;
1955
- this.track("mini_service_exit", { mini_service: name, duration_seconds: durationSeconds });
1956
+ this.activeMiniServices.delete(name);
1957
+ this.activeMiniServices.set(name, this.now());
1958
+ this.track("deepdots_mini_service_enter", { entry_point_type: entryPointType ?? null });
1959
+ }
1960
+ /**
1961
+ * Cierra el mini-service `name` emitiendo `mini_service_exit` con su duración (#27).
1962
+ * No-op si ese nombre no está activo. Cierre coherente con concurrencia.
1963
+ */
1964
+ exitMiniService(name) {
1965
+ const enteredAt = this.activeMiniServices.get(name);
1966
+ if (enteredAt === void 0) return;
1967
+ const durationSeconds = Math.max(0, Math.round((this.now() - enteredAt) / 1e3));
1968
+ this.activeMiniServices.delete(name);
1969
+ this.track("deepdots_mini_service_exit", { mini_service: name, duration_seconds: durationSeconds });
1970
+ }
1971
+ /** Cierra TODOS los mini-services activos (orden LIFO). Para el cierre por lifecycle (background/cierre). */
1972
+ exitAllMiniServices() {
1973
+ for (const name of Array.from(this.activeMiniServices.keys()).reverse()) {
1974
+ this.exitMiniService(name);
1975
+ }
1956
1976
  }
1957
1977
  /** Registra un evento de analítica (modelo GA: nombre + params). */
1958
1978
  track(name, params) {
1979
+ const current = this.getMiniService();
1959
1980
  const merged = {
1960
- ...this.miniService ? { mini_service: this.miniService } : {},
1981
+ ...current ? { mini_service: current } : {},
1961
1982
  ...params ?? {}
1962
1983
  };
1963
1984
  this.events.push({
@@ -1965,10 +1986,15 @@ var AnalyticsManager = class {
1965
1986
  timestamp: this.now(),
1966
1987
  params: Object.keys(merged).length ? merged : void 0
1967
1988
  });
1989
+ if (this.events.length >= this.maxBatchSize) {
1990
+ this.onFlushNeeded?.();
1991
+ }
1968
1992
  }
1969
- /** Mini-service activo (para inyectarlo en la metadata del survey, #33). */
1993
+ /** Mini-service actual (el más reciente aún activo) para etiquetar eventos + metadata del survey (#33). */
1970
1994
  getMiniService() {
1971
- return this.miniService;
1995
+ let last = null;
1996
+ for (const name of this.activeMiniServices.keys()) last = name;
1997
+ return last;
1972
1998
  }
1973
1999
  /** Nº de eventos pendientes de flush. */
1974
2000
  pending() {
@@ -2002,44 +2028,55 @@ var AnalyticsManager = class {
2002
2028
  // src/analytics/feedback-payload.ts
2003
2029
  function pushKV(list, key, value) {
2004
2030
  if (value === void 0 || value === null || value === "") return;
2005
- list.push({ key, value: String(value) });
2031
+ list.push({ key, value: [String(value)] });
2006
2032
  }
2007
2033
  function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
2008
2034
  const { context } = envelope;
2009
- const metrics = envelope.events.map((e) => ({
2010
- key: e.name,
2011
- value: JSON.stringify({ timestamp: e.timestamp, ...e.params ?? {} })
2012
- }));
2013
2035
  const profile = [];
2014
2036
  pushKV(profile, "external-user-id", envelope.userId);
2015
2037
  const metadata = [];
2016
- pushKV(metadata, "user_id", envelope.userId);
2017
- pushKV(metadata, "session_id", envelope.sessionId);
2018
- pushKV(metadata, "platform", context.platform);
2019
- pushKV(metadata, "language", context.language);
2038
+ pushKV(metadata, "deepdots_user_id", envelope.userId);
2039
+ pushKV(metadata, "deepdots_session_id", envelope.sessionId);
2040
+ pushKV(metadata, "deepdots_platform", context.platform);
2041
+ pushKV(metadata, "deepdots_language", context.language);
2020
2042
  if (context.device) {
2021
- pushKV(metadata, "device_type", context.device.device_type);
2022
- pushKV(metadata, "os_version", context.device.os_version);
2023
- pushKV(metadata, "device_model", context.device.device_model);
2024
- pushKV(metadata, "app_version", context.device.app_version);
2025
- pushKV(metadata, "user_agent", context.device.user_agent);
2043
+ const d = context.device;
2044
+ pushKV(metadata, "deepdots_device_type", d.device_type);
2045
+ pushKV(metadata, "deepdots_os_version", d.os_version);
2046
+ pushKV(metadata, "deepdots_device_model", d.device_model);
2047
+ pushKV(metadata, "deepdots_app_version", d.app_version);
2048
+ pushKV(metadata, "deepdots_user_agent", d.user_agent);
2049
+ pushKV(metadata, "deepdots_timezone", d.timezone);
2050
+ pushKV(metadata, "deepdots_referrer", d.referrer);
2051
+ pushKV(metadata, "deepdots_viewport_size", d.viewport_size);
2052
+ pushKV(metadata, "deepdots_screen_resolution", d.screen_resolution);
2053
+ pushKV(metadata, "deepdots_pixel_ratio", d.pixel_ratio);
2054
+ pushKV(metadata, "deepdots_entry_type", d.entry_type);
2055
+ pushKV(metadata, "deepdots_page_load_ms", d.page_load_ms);
2056
+ pushKV(metadata, "deepdots_connection_type", d.connection_type);
2057
+ pushKV(metadata, "deepdots_country", d.country);
2058
+ pushKV(metadata, "deepdots_city", d.city);
2026
2059
  }
2027
2060
  for (const [k, v] of Object.entries(context.attributes ?? {})) {
2028
2061
  pushKV(metadata, k, v);
2029
2062
  }
2063
+ for (const e of envelope.events) {
2064
+ metadata.push({
2065
+ key: e.name,
2066
+ value: [JSON.stringify({ timestamp: e.timestamp, ...e.params ?? {} })]
2067
+ });
2068
+ }
2030
2069
  return {
2031
2070
  feedback: {
2032
2071
  text: "",
2033
2072
  answers: [],
2034
2073
  metadata,
2035
- metrics,
2036
2074
  profile,
2037
2075
  finished: false
2038
2076
  },
2039
2077
  publicKey: keys.publicKey,
2040
2078
  integration: keys.integration,
2041
2079
  completed: false,
2042
- // finished: false,
2043
2080
  ...feedbackSessionId ? { sessionId: feedbackSessionId } : {}
2044
2081
  };
2045
2082
  }
@@ -2064,6 +2101,7 @@ function createFeedbackSink(options) {
2064
2101
  if (data?.sessionId && data.sessionId !== feedbackSessionId) {
2065
2102
  feedbackSessionId = data.sessionId;
2066
2103
  options.log?.("[DeepdotsAnalytics] feedbackSessionId cacheado:", feedbackSessionId);
2104
+ options.onSessionId?.(feedbackSessionId);
2067
2105
  }
2068
2106
  } catch {
2069
2107
  }
@@ -2177,15 +2215,57 @@ function parseDeviceType(ua) {
2177
2215
  }
2178
2216
  return "desktop";
2179
2217
  }
2218
+ function getEntryType() {
2219
+ if (typeof performance === "undefined") return void 0;
2220
+ const entries = performance.getEntriesByType("navigation");
2221
+ if (!entries.length) return void 0;
2222
+ return entries[0].type;
2223
+ }
2224
+ function getPageLoadMs() {
2225
+ if (typeof performance === "undefined") return void 0;
2226
+ const entries = performance.getEntriesByType("navigation");
2227
+ if (!entries.length) return void 0;
2228
+ const load = entries[0].loadEventEnd;
2229
+ return load > 0 ? Math.round(load) : void 0;
2230
+ }
2231
+ function getConnectionType() {
2232
+ if (typeof navigator === "undefined") return void 0;
2233
+ return navigator.connection?.effectiveType ?? void 0;
2234
+ }
2180
2235
  function collectDeviceInfo(appVersion) {
2181
2236
  const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
2182
2237
  return {
2183
2238
  device_type: parseDeviceType(ua),
2184
2239
  user_agent: ua || void 0,
2185
- app_version: appVersion
2240
+ app_version: appVersion,
2241
+ timezone: typeof Intl !== "undefined" ? Intl.DateTimeFormat().resolvedOptions().timeZone : void 0,
2242
+ referrer: typeof document !== "undefined" && document.referrer ? document.referrer : void 0,
2243
+ viewport_size: typeof window !== "undefined" ? `${window.innerWidth}x${window.innerHeight}` : void 0,
2244
+ screen_resolution: typeof screen !== "undefined" ? `${screen.width}x${screen.height}` : void 0,
2245
+ pixel_ratio: typeof window !== "undefined" ? window.devicePixelRatio : void 0,
2246
+ entry_type: getEntryType(),
2247
+ page_load_ms: getPageLoadMs(),
2248
+ connection_type: getConnectionType()
2186
2249
  };
2187
2250
  }
2188
2251
 
2252
+ // src/analytics/geo-info.ts
2253
+ async function collectGeoInfo(fetchImpl) {
2254
+ const f = fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
2255
+ if (!f) return null;
2256
+ try {
2257
+ const res = await f("https://ipapi.co/json/");
2258
+ if (!res.ok) return null;
2259
+ const data = await res.json();
2260
+ const country = data.country_code || void 0;
2261
+ const city = data.city || void 0;
2262
+ if (!country && !city) return null;
2263
+ return { country, city };
2264
+ } catch {
2265
+ return null;
2266
+ }
2267
+ }
2268
+
2189
2269
  // src/analytics/engagement-tracker.ts
2190
2270
  var EngagementTracker = class {
2191
2271
  constructor(now = () => Date.now()) {
@@ -2254,9 +2334,137 @@ var ContactManager = class {
2254
2334
  }
2255
2335
  };
2256
2336
 
2337
+ // src/analytics/crash-reporter.ts
2338
+ var STACK_MAX = 8e3;
2339
+ var QUEUE_KEY = "deepdots.crash.queue";
2340
+ var MAX_QUEUED = 20;
2341
+ function crashRecordToParams(r) {
2342
+ const params = {
2343
+ crashed_at: r.crashedAt,
2344
+ crash_type: r.crashType,
2345
+ message: r.message,
2346
+ stack: r.stack,
2347
+ fatal: r.fatal,
2348
+ handled: r.handled,
2349
+ severity: r.severity
2350
+ };
2351
+ if (r.sessionId) params.crashed_session_id = r.sessionId;
2352
+ if (r.appVersion) params.crashed_app_version = r.appVersion;
2353
+ if (r.osVersion) params.crashed_os_version = r.osVersion;
2354
+ if (r.deviceModel) params.crashed_device_model = r.deviceModel;
2355
+ if (r.context) {
2356
+ for (const [k, v] of Object.entries(r.context)) params[`ctx_${k}`] = v;
2357
+ }
2358
+ return params;
2359
+ }
2360
+ var CrashReporter = class {
2361
+ constructor(options) {
2362
+ this.options = options;
2363
+ this.now = options.now ?? (() => Date.now());
2364
+ }
2365
+ isEnabled() {
2366
+ return this.options.enabled ? this.options.enabled() : true;
2367
+ }
2368
+ /** Construye un CrashRecord capturando contexto en el momento del crash. */
2369
+ buildRecord(error, severity, handled, fatal, context) {
2370
+ const err = error;
2371
+ const isErr = error instanceof Error;
2372
+ const dev = this.options.device();
2373
+ const ctx = context ? Object.fromEntries(Object.entries(context).map(([k, v]) => [k, String(v)])) : void 0;
2374
+ return {
2375
+ crashedAt: this.now(),
2376
+ crashType: isErr && err.name || "Error",
2377
+ message: isErr ? String(err.message ?? "") : String(error),
2378
+ stack: (isErr && typeof err.stack === "string" ? err.stack : "").slice(0, STACK_MAX),
2379
+ fatal,
2380
+ handled,
2381
+ severity,
2382
+ sessionId: this.options.sessionId(),
2383
+ appVersion: dev.appVersion,
2384
+ osVersion: dev.osVersion,
2385
+ deviceModel: dev.deviceModel,
2386
+ context: ctx
2387
+ };
2388
+ }
2389
+ /** API pública del host: reporta un error (app viva) → emite el evento ya. */
2390
+ reportError(error, options = {}) {
2391
+ if (!this.isEnabled()) return;
2392
+ const severity = options.severity ?? "error";
2393
+ const handled = options.handled ?? true;
2394
+ const fatal = severity === "fatal";
2395
+ const record = this.buildRecord(error, severity, handled, fatal, options.context);
2396
+ this.options.emit(crashRecordToParams(record));
2397
+ }
2398
+ /** Captura un error no manejado (fatal) → persiste a disco para replay en el siguiente arranque. */
2399
+ captureUnhandled(error) {
2400
+ this.persist(this.buildRecord(error, "fatal", false, true));
2401
+ }
2402
+ /** Persiste un crash no capturado a disco para reenviarlo en el siguiente arranque. */
2403
+ persist(record) {
2404
+ let queue = [];
2405
+ try {
2406
+ const raw = this.options.storage.getItem(QUEUE_KEY);
2407
+ if (raw) queue = JSON.parse(raw);
2408
+ if (!Array.isArray(queue)) queue = [];
2409
+ } catch {
2410
+ queue = [];
2411
+ }
2412
+ queue.push(record);
2413
+ if (queue.length > MAX_QUEUED) queue = queue.slice(queue.length - MAX_QUEUED);
2414
+ try {
2415
+ this.options.storage.setItem(QUEUE_KEY, JSON.stringify(queue));
2416
+ } catch {
2417
+ }
2418
+ }
2419
+ /** Lee y vacía la cola de crashes pendientes (llamado en init para el replay). */
2420
+ drainPendingCrashes() {
2421
+ let queue = [];
2422
+ try {
2423
+ const raw = this.options.storage.getItem(QUEUE_KEY);
2424
+ if (raw) queue = JSON.parse(raw);
2425
+ if (!Array.isArray(queue)) queue = [];
2426
+ } catch {
2427
+ queue = [];
2428
+ }
2429
+ if (queue.length) {
2430
+ try {
2431
+ this.options.storage.removeItem(QUEUE_KEY);
2432
+ } catch {
2433
+ }
2434
+ }
2435
+ return queue;
2436
+ }
2437
+ /** Instala los handlers globales de errores no capturados (no-op sin `window`). */
2438
+ install() {
2439
+ if (typeof window === "undefined") return;
2440
+ window.addEventListener("error", (e) => {
2441
+ if (!this.isEnabled()) return;
2442
+ this.captureUnhandled(e.error ?? e.message ?? "error");
2443
+ });
2444
+ window.addEventListener("unhandledrejection", (e) => {
2445
+ if (!this.isEnabled()) return;
2446
+ this.captureUnhandled(e.reason ?? "unhandledrejection");
2447
+ });
2448
+ }
2449
+ /** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. */
2450
+ installReactNative(errorUtils) {
2451
+ const previous = errorUtils.getGlobalHandler?.();
2452
+ errorUtils.setGlobalHandler((error, isFatal) => {
2453
+ if (this.isEnabled()) this.captureUnhandled(error);
2454
+ previous?.(error, isFatal);
2455
+ });
2456
+ }
2457
+ /** @internal Test seam: ejercita el camino de persistencia (window handlers no son testeables en vitest). */
2458
+ _persistForTest(error) {
2459
+ this.captureUnhandled(error);
2460
+ }
2461
+ };
2462
+
2257
2463
  // src/core/deepdots-popups.ts
2258
2464
  var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
2259
2465
  var DAY_IN_MS = 24 * 60 * 60 * 1e3;
2466
+ var ANALYTICS_MAX_BATCH_SIZE = 20;
2467
+ var ANALYTICS_FLUSH_INTERVAL_MS = 3e4;
2260
2468
  var DeepdotsPopups = class {
2261
2469
  constructor() {
2262
2470
  this.config = null;
@@ -2282,6 +2490,12 @@ var DeepdotsPopups = class {
2282
2490
  this.tracking = null;
2283
2491
  /** Capa de analytics (canal separado del feedback). Null hasta init(). */
2284
2492
  this.analytics = null;
2493
+ /** feedbackSessionId cacheado del canal de analytics (devuelto por POST /sdk/feedback). */
2494
+ this.analyticsFeedbackSessionId = void 0;
2495
+ /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
2496
+ this.analyticsFlushTimer = void 0;
2497
+ /** Crash & error reporting (#14–17). Null hasta init(). */
2498
+ this.crashReporter = null;
2285
2499
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
2286
2500
  this.navObserver = null;
2287
2501
  /** Tiempo activo (engagement time, #8). */
@@ -2328,21 +2542,49 @@ var DeepdotsPopups = class {
2328
2542
  const analyticsSink = config.analytics ? createFeedbackSink({
2329
2543
  baseUrl: this.baseUrl,
2330
2544
  keys: config.analytics,
2331
- log: (...a) => this.log(...a)
2545
+ log: (...a) => this.log(...a),
2546
+ onSessionId: (id) => {
2547
+ this.analyticsFeedbackSessionId = id;
2548
+ }
2332
2549
  }) : void 0;
2550
+ const device = config.device ?? collectDeviceInfo(config.appVersion);
2333
2551
  this.analytics = new AnalyticsManager({
2334
2552
  sink: analyticsSink,
2335
2553
  publicKey: config.analytics?.publicKey ?? this.config.apiKey,
2336
2554
  language: typeof navigator !== "undefined" ? navigator.language : void 0,
2337
2555
  platform: config.platform ?? "web",
2338
- device: config.device ?? collectDeviceInfo(config.appVersion)
2556
+ device,
2557
+ maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,
2558
+ onFlushNeeded: () => this.flushAnalytics()
2559
+ });
2560
+ this.crashReporter = new CrashReporter({
2561
+ storage,
2562
+ emit: (params) => this.track("deepdots_app_crash", params),
2563
+ device: () => ({ appVersion: device.app_version, osVersion: device.os_version, deviceModel: device.device_model }),
2564
+ sessionId: () => this.tracking?.getSessionId() ?? null,
2565
+ now: () => Date.now(),
2566
+ enabled: () => this.tracking?.isTrackingEnabled() ?? false
2567
+ });
2568
+ if (this.tracking?.isTrackingEnabled()) {
2569
+ this.crashReporter.install();
2570
+ }
2571
+ collectGeoInfo().then((geo) => {
2572
+ if (geo) this.analytics?.updateDevice(geo);
2573
+ }).catch(() => {
2339
2574
  });
2340
2575
  this.navObserver = new NavigationObserver();
2341
- this.navObserver.onVisit((v) => this.track("page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
2576
+ this.navObserver.onVisit((v) => this.track("deepdots_page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
2342
2577
  this.navObserver.install();
2343
2578
  this.engagement = new EngagementTracker();
2344
2579
  this.engagement.resume();
2345
2580
  this.setupAnalyticsFlush();
2581
+ this.track("deepdots_session_start", {});
2582
+ const pendingCrashes = this.crashReporter.drainPendingCrashes();
2583
+ if (this.tracking?.isTrackingEnabled()) {
2584
+ for (const rec of pendingCrashes) {
2585
+ this.track("deepdots_app_crash", crashRecordToParams(rec));
2586
+ }
2587
+ }
2346
2588
  this.initialized = true;
2347
2589
  this.log("SDK initialized", this.config);
2348
2590
  if (this.renderer.init) this.renderer.init();
@@ -2383,6 +2625,16 @@ var DeepdotsPopups = class {
2383
2625
  if (!this.tracking?.isTrackingEnabled()) return;
2384
2626
  this.analytics?.setUserAttributes(attributes);
2385
2627
  }
2628
+ /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
2629
+ reportError(error, options) {
2630
+ if (!this.tracking?.isTrackingEnabled()) return;
2631
+ this.crashReporter?.reportError(error, options);
2632
+ }
2633
+ /** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
2634
+ installReactNativeCrashHandler(errorUtils) {
2635
+ if (!this.tracking?.isTrackingEnabled()) return;
2636
+ this.crashReporter?.installReactNative(errorUtils);
2637
+ }
2386
2638
  /**
2387
2639
  * Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
2388
2640
  * Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
@@ -2400,23 +2652,23 @@ var DeepdotsPopups = class {
2400
2652
  this.analytics?.enterMiniService(name, entryPointType);
2401
2653
  this.log("analytics \xB7 enterMiniService:", name, entryPointType ?? "");
2402
2654
  }
2403
- /** Cierra el mini-service activo (emite `mini_service_exit` con duración, #27). No-op si tracking off. */
2404
- exitMiniService() {
2655
+ /** Cierra el mini-service `name` (emite `mini_service_exit` con duración, #27). No-op si tracking off o si ese no está activo. */
2656
+ exitMiniService(name) {
2405
2657
  if (!this.tracking?.isTrackingEnabled()) return;
2406
- this.analytics?.exitMiniService();
2407
- this.log("analytics \xB7 exitMiniService");
2658
+ this.analytics?.exitMiniService(name);
2659
+ this.log("analytics \xB7 exitMiniService:", name);
2408
2660
  }
2409
2661
  /** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */
2410
2662
  trackSearch(query, resultsCount, params) {
2411
- this.track("search", { query, results_count: resultsCount, has_results: resultsCount > 0, ...params ?? {} });
2663
+ this.track("deepdots_search", { query, results_count: resultsCount, has_results: resultsCount > 0, ...params ?? {} });
2412
2664
  }
2413
2665
  /** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */
2414
2666
  trackFindabilityFriction(frictionTopic, params) {
2415
- this.track("findability_friction", { friction_topic: frictionTopic, ...params ?? {} });
2667
+ this.track("deepdots_findability_friction", { friction_topic: frictionTopic, ...params ?? {} });
2416
2668
  }
2417
2669
  /** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
2418
2670
  trackFunnelStep(funnel, step, taskId, params) {
2419
- this.track("funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
2671
+ this.track("deepdots_funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
2420
2672
  }
2421
2673
  /**
2422
2674
  * Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
@@ -2442,7 +2694,7 @@ var DeepdotsPopups = class {
2442
2694
  onBackground() {
2443
2695
  this.navObserver?.stop();
2444
2696
  this.navStarted = false;
2445
- this.exitMiniService();
2697
+ if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
2446
2698
  this.flushEngagement();
2447
2699
  this.engagement?.pause();
2448
2700
  this.flushAnalytics();
@@ -2465,7 +2717,7 @@ var DeepdotsPopups = class {
2465
2717
  flushEngagement() {
2466
2718
  if (!this.tracking?.isTrackingEnabled()) return;
2467
2719
  const ms = this.engagement?.consume() ?? 0;
2468
- if (ms > 0) this.track("user_engagement", { engagement_time_msec: ms });
2720
+ if (ms > 0) this.track("deepdots_user_engagement", { engagement_time_msec: ms });
2469
2721
  }
2470
2722
  analyticsIdentity() {
2471
2723
  return {
@@ -2476,9 +2728,11 @@ var DeepdotsPopups = class {
2476
2728
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
2477
2729
  setupAnalyticsFlush() {
2478
2730
  if (typeof document === "undefined" || typeof window === "undefined") return;
2731
+ this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
2479
2732
  window.addEventListener("pagehide", () => {
2733
+ clearInterval(this.analyticsFlushTimer);
2480
2734
  this.navObserver?.stop();
2481
- this.exitMiniService();
2735
+ if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
2482
2736
  this.flushEngagement();
2483
2737
  this.flushAnalytics();
2484
2738
  });
@@ -2920,7 +3174,8 @@ var DeepdotsPopups = class {
2920
3174
  const userId = this.tracking?.getUserId() ?? this.config?.userId;
2921
3175
  const sessionId = this.tracking?.getSessionId() ?? void 0;
2922
3176
  const miniService = this.analytics?.getMiniService() ?? void 0;
2923
- this.log("tracking \xB7 survey identity \u2192", { userId: userId ?? null, sessionId: sessionId ?? null, miniService: miniService ?? null });
3177
+ const analyticsFeedbackSessionId = this.analyticsFeedbackSessionId;
3178
+ this.log("tracking \xB7 survey identity \u2192", { userId: userId ?? null, sessionId: sessionId ?? null, miniService: miniService ?? null, analyticsFeedbackSessionId: analyticsFeedbackSessionId ?? null });
2924
3179
  if (this.renderer) {
2925
3180
  this.renderer.show(
2926
3181
  surveyId,
@@ -2932,7 +3187,8 @@ var DeepdotsPopups = class {
2932
3187
  userId,
2933
3188
  style,
2934
3189
  sessionId,
2935
- miniService
3190
+ miniService,
3191
+ analyticsFeedbackSessionId
2936
3192
  );
2937
3193
  return;
2938
3194
  }
@@ -2950,7 +3206,8 @@ var DeepdotsPopups = class {
2950
3206
  userId,
2951
3207
  style,
2952
3208
  sessionId,
2953
- miniService
3209
+ miniService,
3210
+ analyticsFeedbackSessionId
2954
3211
  );
2955
3212
  });
2956
3213
  }
@@ -3141,6 +3398,10 @@ function setupReactNative(sdk2, config, deps = {}) {
3141
3398
  const device = deps.deviceInfo ? collectRnDevice(deps.deviceInfo) : config.device;
3142
3399
  const platform = deps.platform ?? config.platform;
3143
3400
  sdk2.init({ ...config, storage, device, platform });
3401
+ const errorUtils = deps.errorUtils ?? globalThis.ErrorUtils;
3402
+ if (errorUtils && typeof errorUtils.setGlobalHandler === "function") {
3403
+ sdk2.installReactNativeCrashHandler(errorUtils);
3404
+ }
3144
3405
  let subscription;
3145
3406
  if (deps.appState?.addEventListener) {
3146
3407
  subscription = deps.appState.addEventListener("change", (state) => {