@magicfeedback/popup-sdk 1.1.2 → 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.
@@ -216,6 +216,30 @@ interface PopupRenderer {
216
216
  type ContactAttributeValue = string | number | boolean;
217
217
  type ContactAttributes = Record<string, ContactAttributeValue>;
218
218
 
219
+ /**
220
+ * Crash & error reporting (Stability #14–17) — captura de errores GESTIONADOS.
221
+ *
222
+ * - `reportError()`: API pública para el host (app viva) → emite `deepdots_app_crash` ya.
223
+ * - handlers de `window` (errores no capturados): persisten a disco y se reenvían
224
+ * en el siguiente arranque (el proceso puede morir antes del flush normal).
225
+ *
226
+ * El contexto de device/sesión se captura EN EL MOMENTO del crash y se guarda en el
227
+ * record — no se lee del envelope en el replay (el replay puede ocurrir en otra
228
+ * app_version/OS). Las dims OS/device en Web suelen ir vacías (se derivan del UA en backend).
229
+ */
230
+
231
+ type CrashSeverity = 'fatal' | 'error' | 'warning';
232
+ interface ReportErrorOptions {
233
+ severity?: CrashSeverity;
234
+ handled?: boolean;
235
+ context?: Record<string, unknown>;
236
+ }
237
+ /** Forma mínima de `global.ErrorUtils` de React Native. */
238
+ interface ReactNativeErrorUtils {
239
+ getGlobalHandler?(): ((error: unknown, isFatal?: boolean) => void) | undefined;
240
+ setGlobalHandler(handler: (error: unknown, isFatal?: boolean) => void): void;
241
+ }
242
+
219
243
  /**
220
244
  * Main class for managing survey popups
221
245
  */
@@ -244,6 +268,8 @@ declare class DeepdotsPopups {
244
268
  private analyticsFeedbackSessionId;
245
269
  /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
246
270
  private analyticsFlushTimer;
271
+ /** Crash & error reporting (#14–17). Null hasta init(). */
272
+ private crashReporter;
247
273
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
248
274
  private navObserver;
249
275
  /** Tiempo activo (engagement time, #8). */
@@ -264,6 +290,10 @@ declare class DeepdotsPopups {
264
290
  track(name: string, params?: Record<string, unknown>): void;
265
291
  /** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
266
292
  setUserAttributes(attributes: Record<string, string | number | boolean>): void;
293
+ /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
294
+ reportError(error: unknown, options?: ReportErrorOptions): void;
295
+ /** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
296
+ installReactNativeCrashHandler(errorUtils: ReactNativeErrorUtils): void;
267
297
  /**
268
298
  * Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
269
299
  * Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
@@ -273,8 +303,8 @@ declare class DeepdotsPopups {
273
303
  setContactAttributes(attributes: ContactAttributes): Promise<boolean>;
274
304
  /** Marca el inicio de un mini-service; etiqueta los eventos siguientes. No-op si tracking off. */
275
305
  enterMiniService(name: string, entryPointType?: string): void;
276
- /** Cierra el mini-service activo (emite `mini_service_exit` con duración, #27). No-op si tracking off. */
277
- exitMiniService(): void;
306
+ /** Cierra el mini-service `name` (emite `mini_service_exit` con duración, #27). No-op si tracking off o si ese no está activo. */
307
+ exitMiniService(name: string): void;
278
308
  /** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */
279
309
  trackSearch(query: string, resultsCount: number, params?: Record<string, unknown>): void;
280
310
  /** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */
@@ -406,6 +436,8 @@ interface ReactNativeSetupDeps {
406
436
  appState?: AppStateLike | null;
407
437
  platform?: 'web' | 'android' | 'ios';
408
438
  renderer?: PopupRenderer;
439
+ /** `global.ErrorUtils` de RN (para capturar errores JS no manejados). Default: `globalThis.ErrorUtils`. */
440
+ errorUtils?: ReactNativeErrorUtils | null;
409
441
  }
410
442
  /**
411
443
  * Configura el SDK para React Native en una sola llamada: storage persistente (MMKV),
@@ -216,6 +216,30 @@ interface PopupRenderer {
216
216
  type ContactAttributeValue = string | number | boolean;
217
217
  type ContactAttributes = Record<string, ContactAttributeValue>;
218
218
 
219
+ /**
220
+ * Crash & error reporting (Stability #14–17) — captura de errores GESTIONADOS.
221
+ *
222
+ * - `reportError()`: API pública para el host (app viva) → emite `deepdots_app_crash` ya.
223
+ * - handlers de `window` (errores no capturados): persisten a disco y se reenvían
224
+ * en el siguiente arranque (el proceso puede morir antes del flush normal).
225
+ *
226
+ * El contexto de device/sesión se captura EN EL MOMENTO del crash y se guarda en el
227
+ * record — no se lee del envelope en el replay (el replay puede ocurrir en otra
228
+ * app_version/OS). Las dims OS/device en Web suelen ir vacías (se derivan del UA en backend).
229
+ */
230
+
231
+ type CrashSeverity = 'fatal' | 'error' | 'warning';
232
+ interface ReportErrorOptions {
233
+ severity?: CrashSeverity;
234
+ handled?: boolean;
235
+ context?: Record<string, unknown>;
236
+ }
237
+ /** Forma mínima de `global.ErrorUtils` de React Native. */
238
+ interface ReactNativeErrorUtils {
239
+ getGlobalHandler?(): ((error: unknown, isFatal?: boolean) => void) | undefined;
240
+ setGlobalHandler(handler: (error: unknown, isFatal?: boolean) => void): void;
241
+ }
242
+
219
243
  /**
220
244
  * Main class for managing survey popups
221
245
  */
@@ -244,6 +268,8 @@ declare class DeepdotsPopups {
244
268
  private analyticsFeedbackSessionId;
245
269
  /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
246
270
  private analyticsFlushTimer;
271
+ /** Crash & error reporting (#14–17). Null hasta init(). */
272
+ private crashReporter;
247
273
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
248
274
  private navObserver;
249
275
  /** Tiempo activo (engagement time, #8). */
@@ -264,6 +290,10 @@ declare class DeepdotsPopups {
264
290
  track(name: string, params?: Record<string, unknown>): void;
265
291
  /** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
266
292
  setUserAttributes(attributes: Record<string, string | number | boolean>): void;
293
+ /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
294
+ reportError(error: unknown, options?: ReportErrorOptions): void;
295
+ /** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
296
+ installReactNativeCrashHandler(errorUtils: ReactNativeErrorUtils): void;
267
297
  /**
268
298
  * Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
269
299
  * Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
@@ -273,8 +303,8 @@ declare class DeepdotsPopups {
273
303
  setContactAttributes(attributes: ContactAttributes): Promise<boolean>;
274
304
  /** Marca el inicio de un mini-service; etiqueta los eventos siguientes. No-op si tracking off. */
275
305
  enterMiniService(name: string, entryPointType?: string): void;
276
- /** Cierra el mini-service activo (emite `mini_service_exit` con duración, #27). No-op si tracking off. */
277
- exitMiniService(): void;
306
+ /** Cierra el mini-service `name` (emite `mini_service_exit` con duración, #27). No-op si tracking off o si ese no está activo. */
307
+ exitMiniService(name: string): void;
278
308
  /** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */
279
309
  trackSearch(query: string, resultsCount: number, params?: Record<string, unknown>): void;
280
310
  /** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */
@@ -406,6 +436,8 @@ interface ReactNativeSetupDeps {
406
436
  appState?: AppStateLike | null;
407
437
  platform?: 'web' | 'android' | 'ios';
408
438
  renderer?: PopupRenderer;
439
+ /** `global.ErrorUtils` de RN (para capturar errores JS no manejados). Default: `globalThis.ErrorUtils`. */
440
+ errorUtils?: ReactNativeErrorUtils | null;
409
441
  }
410
442
  /**
411
443
  * Configura el SDK para React Native en una sola llamada: storage persistente (MMKV),
@@ -1925,8 +1925,8 @@ var AnalyticsManager = class {
1925
1925
  constructor(options = {}) {
1926
1926
  this.events = [];
1927
1927
  this.attributes = {};
1928
- this.miniService = null;
1929
- 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();
1930
1930
  this.sink = options.sink ?? dryRunSink;
1931
1931
  this.now = options.now ?? (() => Date.now());
1932
1932
  this.publicKey = options.publicKey;
@@ -1947,24 +1947,38 @@ var AnalyticsManager = class {
1947
1947
  this.attributes[k] = String(v);
1948
1948
  }
1949
1949
  }
1950
- /** 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
+ */
1951
1955
  enterMiniService(name, entryPointType) {
1952
- this.miniService = name;
1953
- this.miniServiceEnteredAt = this.now();
1954
- this.track("mini_service_enter", { entry_point_type: entryPointType ?? null });
1955
- }
1956
- /** Cierra el mini-service activo emitiendo `mini_service_exit` con su duración (#27). No-op si no hay ninguno. */
1957
- exitMiniService() {
1958
- const name = this.miniService;
1959
- if (name == null) return;
1960
- const durationSeconds = Math.max(0, Math.round((this.now() - this.miniServiceEnteredAt) / 1e3));
1961
- this.miniService = null;
1962
- 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
+ }
1963
1976
  }
1964
1977
  /** Registra un evento de analítica (modelo GA: nombre + params). */
1965
1978
  track(name, params) {
1979
+ const current = this.getMiniService();
1966
1980
  const merged = {
1967
- ...this.miniService ? { mini_service: this.miniService } : {},
1981
+ ...current ? { mini_service: current } : {},
1968
1982
  ...params ?? {}
1969
1983
  };
1970
1984
  this.events.push({
@@ -1976,9 +1990,11 @@ var AnalyticsManager = class {
1976
1990
  this.onFlushNeeded?.();
1977
1991
  }
1978
1992
  }
1979
- /** 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). */
1980
1994
  getMiniService() {
1981
- return this.miniService;
1995
+ let last = null;
1996
+ for (const name of this.activeMiniServices.keys()) last = name;
1997
+ return last;
1982
1998
  }
1983
1999
  /** Nº de eventos pendientes de flush. */
1984
2000
  pending() {
@@ -2318,6 +2334,132 @@ var ContactManager = class {
2318
2334
  }
2319
2335
  };
2320
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
+
2321
2463
  // src/core/deepdots-popups.ts
2322
2464
  var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
2323
2465
  var DAY_IN_MS = 24 * 60 * 60 * 1e3;
@@ -2352,6 +2494,8 @@ var DeepdotsPopups = class {
2352
2494
  this.analyticsFeedbackSessionId = void 0;
2353
2495
  /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
2354
2496
  this.analyticsFlushTimer = void 0;
2497
+ /** Crash & error reporting (#14–17). Null hasta init(). */
2498
+ this.crashReporter = null;
2355
2499
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
2356
2500
  this.navObserver = null;
2357
2501
  /** Tiempo activo (engagement time, #8). */
@@ -2403,25 +2547,44 @@ var DeepdotsPopups = class {
2403
2547
  this.analyticsFeedbackSessionId = id;
2404
2548
  }
2405
2549
  }) : void 0;
2550
+ const device = config.device ?? collectDeviceInfo(config.appVersion);
2406
2551
  this.analytics = new AnalyticsManager({
2407
2552
  sink: analyticsSink,
2408
2553
  publicKey: config.analytics?.publicKey ?? this.config.apiKey,
2409
2554
  language: typeof navigator !== "undefined" ? navigator.language : void 0,
2410
2555
  platform: config.platform ?? "web",
2411
- device: config.device ?? collectDeviceInfo(config.appVersion),
2556
+ device,
2412
2557
  maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,
2413
2558
  onFlushNeeded: () => this.flushAnalytics()
2414
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
+ }
2415
2571
  collectGeoInfo().then((geo) => {
2416
2572
  if (geo) this.analytics?.updateDevice(geo);
2417
2573
  }).catch(() => {
2418
2574
  });
2419
2575
  this.navObserver = new NavigationObserver();
2420
- 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 }));
2421
2577
  this.navObserver.install();
2422
2578
  this.engagement = new EngagementTracker();
2423
2579
  this.engagement.resume();
2424
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
+ }
2425
2588
  this.initialized = true;
2426
2589
  this.log("SDK initialized", this.config);
2427
2590
  if (this.renderer.init) this.renderer.init();
@@ -2462,6 +2625,16 @@ var DeepdotsPopups = class {
2462
2625
  if (!this.tracking?.isTrackingEnabled()) return;
2463
2626
  this.analytics?.setUserAttributes(attributes);
2464
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
+ }
2465
2638
  /**
2466
2639
  * Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
2467
2640
  * Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
@@ -2479,23 +2652,23 @@ var DeepdotsPopups = class {
2479
2652
  this.analytics?.enterMiniService(name, entryPointType);
2480
2653
  this.log("analytics \xB7 enterMiniService:", name, entryPointType ?? "");
2481
2654
  }
2482
- /** Cierra el mini-service activo (emite `mini_service_exit` con duración, #27). No-op si tracking off. */
2483
- 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) {
2484
2657
  if (!this.tracking?.isTrackingEnabled()) return;
2485
- this.analytics?.exitMiniService();
2486
- this.log("analytics \xB7 exitMiniService");
2658
+ this.analytics?.exitMiniService(name);
2659
+ this.log("analytics \xB7 exitMiniService:", name);
2487
2660
  }
2488
2661
  /** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */
2489
2662
  trackSearch(query, resultsCount, params) {
2490
- 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 ?? {} });
2491
2664
  }
2492
2665
  /** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */
2493
2666
  trackFindabilityFriction(frictionTopic, params) {
2494
- this.track("findability_friction", { friction_topic: frictionTopic, ...params ?? {} });
2667
+ this.track("deepdots_findability_friction", { friction_topic: frictionTopic, ...params ?? {} });
2495
2668
  }
2496
2669
  /** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
2497
2670
  trackFunnelStep(funnel, step, taskId, params) {
2498
- this.track("funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
2671
+ this.track("deepdots_funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
2499
2672
  }
2500
2673
  /**
2501
2674
  * Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
@@ -2521,7 +2694,7 @@ var DeepdotsPopups = class {
2521
2694
  onBackground() {
2522
2695
  this.navObserver?.stop();
2523
2696
  this.navStarted = false;
2524
- this.exitMiniService();
2697
+ if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
2525
2698
  this.flushEngagement();
2526
2699
  this.engagement?.pause();
2527
2700
  this.flushAnalytics();
@@ -2544,7 +2717,7 @@ var DeepdotsPopups = class {
2544
2717
  flushEngagement() {
2545
2718
  if (!this.tracking?.isTrackingEnabled()) return;
2546
2719
  const ms = this.engagement?.consume() ?? 0;
2547
- if (ms > 0) this.track("user_engagement", { engagement_time_msec: ms });
2720
+ if (ms > 0) this.track("deepdots_user_engagement", { engagement_time_msec: ms });
2548
2721
  }
2549
2722
  analyticsIdentity() {
2550
2723
  return {
@@ -2559,7 +2732,7 @@ var DeepdotsPopups = class {
2559
2732
  window.addEventListener("pagehide", () => {
2560
2733
  clearInterval(this.analyticsFlushTimer);
2561
2734
  this.navObserver?.stop();
2562
- this.exitMiniService();
2735
+ if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
2563
2736
  this.flushEngagement();
2564
2737
  this.flushAnalytics();
2565
2738
  });
@@ -3225,6 +3398,10 @@ function setupReactNative(sdk2, config, deps = {}) {
3225
3398
  const device = deps.deviceInfo ? collectRnDevice(deps.deviceInfo) : config.device;
3226
3399
  const platform = deps.platform ?? config.platform;
3227
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
+ }
3228
3405
  let subscription;
3229
3406
  if (deps.appState?.addEventListener) {
3230
3407
  subscription = deps.appState.addEventListener("change", (state) => {