@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.
@@ -4,7 +4,7 @@ import {
4
4
  __require,
5
5
  buildSurveyIdentity,
6
6
  createDefaultStorage
7
- } from "./chunk-M6PEFLLQ.mjs";
7
+ } from "./chunk-IOAZQPZE.mjs";
8
8
 
9
9
  // src/react-native/index.tsx
10
10
  import { createContext, useContext, useEffect, useRef, useState } from "react";
@@ -258,12 +258,12 @@ var ReactNativePopupRenderer = class {
258
258
  }
259
259
  init() {
260
260
  }
261
- show(surveyId, productId, _actions, emit, onClose, env = "production", userId, _style, sessionId, miniService) {
261
+ show(surveyId, productId, _actions, emit, onClose, env = "production", userId, _style, sessionId, miniService, analyticsFeedbackSessionId) {
262
262
  this.emitFn = emit;
263
263
  this.onCloseFn = onClose;
264
264
  this.currentSurveyId = surveyId;
265
265
  this.partialEmitted = false;
266
- const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null);
266
+ const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null, analyticsFeedbackSessionId ?? null);
267
267
  const html = buildSurveyHtml({ surveyId, productId, env, profile, metadata });
268
268
  if (this.options.onShow) {
269
269
  this.options.onShow({ surveyId, productId, html });
@@ -354,14 +354,14 @@ var BrowserPopupRenderer = class {
354
354
  document.body.appendChild(this.container);
355
355
  }
356
356
  }
357
- show(surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService) {
357
+ show(surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId) {
358
358
  if (this.visible) return;
359
359
  if (!this.container || !document.body.contains(this.container)) this.init();
360
360
  if (!this.container) return;
361
361
  this.visible = true;
362
362
  const container = this.container;
363
- void import("./renderPopup-7GGKR5BY.mjs").then(({ renderPopup }) => {
364
- renderPopup(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService);
363
+ void import("./renderPopup-KBDWWSUU.mjs").then(({ renderPopup }) => {
364
+ renderPopup(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService, analyticsFeedbackSessionId);
365
365
  });
366
366
  }
367
367
  hide() {
@@ -392,14 +392,20 @@ var AnalyticsManager = class {
392
392
  constructor(options = {}) {
393
393
  this.events = [];
394
394
  this.attributes = {};
395
- this.miniService = null;
396
- this.miniServiceEnteredAt = 0;
395
+ /** Mini-services activos: nombre → timestamp de entrada. El orden de inserción marca el "más reciente". */
396
+ this.activeMiniServices = /* @__PURE__ */ new Map();
397
397
  this.sink = options.sink ?? dryRunSink;
398
398
  this.now = options.now ?? (() => Date.now());
399
399
  this.publicKey = options.publicKey;
400
400
  this.language = options.language;
401
401
  this.device = options.device;
402
402
  this.platform = options.platform ?? "web";
403
+ this.maxBatchSize = options.maxBatchSize ?? 20;
404
+ this.onFlushNeeded = options.onFlushNeeded;
405
+ }
406
+ /** Actualiza campos de device en runtime (p.ej. country/city tras resolver geo). */
407
+ updateDevice(partial) {
408
+ this.device = { ...this.device, ...partial };
403
409
  }
404
410
  /** Mezcla user attributes (se coercionan a string). Mutable en runtime. */
405
411
  setUserAttributes(attrs) {
@@ -408,24 +414,38 @@ var AnalyticsManager = class {
408
414
  this.attributes[k] = String(v);
409
415
  }
410
416
  }
411
- /** Marca el inicio de un mini-service; etiqueta los eventos siguientes con `mini_service`. */
417
+ /**
418
+ * Marca el inicio de un mini-service; etiqueta los eventos siguientes con `mini_service`.
419
+ * Admite varios activos a la vez (concurrencia); el "actual" para etiquetar es el más reciente.
420
+ * Reentrar con un nombre ya activo refresca su orden y su tiempo de entrada.
421
+ */
412
422
  enterMiniService(name, entryPointType) {
413
- this.miniService = name;
414
- this.miniServiceEnteredAt = this.now();
415
- this.track("mini_service_enter", { entry_point_type: entryPointType ?? null });
416
- }
417
- /** Cierra el mini-service activo emitiendo `mini_service_exit` con su duración (#27). No-op si no hay ninguno. */
418
- exitMiniService() {
419
- const name = this.miniService;
420
- if (name == null) return;
421
- const durationSeconds = Math.max(0, Math.round((this.now() - this.miniServiceEnteredAt) / 1e3));
422
- this.miniService = null;
423
- this.track("mini_service_exit", { mini_service: name, duration_seconds: durationSeconds });
423
+ this.activeMiniServices.delete(name);
424
+ this.activeMiniServices.set(name, this.now());
425
+ this.track("deepdots_mini_service_enter", { entry_point_type: entryPointType ?? null });
426
+ }
427
+ /**
428
+ * Cierra el mini-service `name` emitiendo `mini_service_exit` con su duración (#27).
429
+ * No-op si ese nombre no está activo. Cierre coherente con concurrencia.
430
+ */
431
+ exitMiniService(name) {
432
+ const enteredAt = this.activeMiniServices.get(name);
433
+ if (enteredAt === void 0) return;
434
+ const durationSeconds = Math.max(0, Math.round((this.now() - enteredAt) / 1e3));
435
+ this.activeMiniServices.delete(name);
436
+ this.track("deepdots_mini_service_exit", { mini_service: name, duration_seconds: durationSeconds });
437
+ }
438
+ /** Cierra TODOS los mini-services activos (orden LIFO). Para el cierre por lifecycle (background/cierre). */
439
+ exitAllMiniServices() {
440
+ for (const name of Array.from(this.activeMiniServices.keys()).reverse()) {
441
+ this.exitMiniService(name);
442
+ }
424
443
  }
425
444
  /** Registra un evento de analítica (modelo GA: nombre + params). */
426
445
  track(name, params) {
446
+ const current = this.getMiniService();
427
447
  const merged = {
428
- ...this.miniService ? { mini_service: this.miniService } : {},
448
+ ...current ? { mini_service: current } : {},
429
449
  ...params ?? {}
430
450
  };
431
451
  this.events.push({
@@ -433,10 +453,15 @@ var AnalyticsManager = class {
433
453
  timestamp: this.now(),
434
454
  params: Object.keys(merged).length ? merged : void 0
435
455
  });
456
+ if (this.events.length >= this.maxBatchSize) {
457
+ this.onFlushNeeded?.();
458
+ }
436
459
  }
437
- /** Mini-service activo (para inyectarlo en la metadata del survey, #33). */
460
+ /** Mini-service actual (el más reciente aún activo) para etiquetar eventos + metadata del survey (#33). */
438
461
  getMiniService() {
439
- return this.miniService;
462
+ let last = null;
463
+ for (const name of this.activeMiniServices.keys()) last = name;
464
+ return last;
440
465
  }
441
466
  /** Nº de eventos pendientes de flush. */
442
467
  pending() {
@@ -470,44 +495,55 @@ var AnalyticsManager = class {
470
495
  // src/analytics/feedback-payload.ts
471
496
  function pushKV(list, key, value) {
472
497
  if (value === void 0 || value === null || value === "") return;
473
- list.push({ key, value: String(value) });
498
+ list.push({ key, value: [String(value)] });
474
499
  }
475
500
  function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
476
501
  const { context } = envelope;
477
- const metrics = envelope.events.map((e) => ({
478
- key: e.name,
479
- value: JSON.stringify({ timestamp: e.timestamp, ...e.params ?? {} })
480
- }));
481
502
  const profile = [];
482
503
  pushKV(profile, "external-user-id", envelope.userId);
483
504
  const metadata = [];
484
- pushKV(metadata, "user_id", envelope.userId);
485
- pushKV(metadata, "session_id", envelope.sessionId);
486
- pushKV(metadata, "platform", context.platform);
487
- pushKV(metadata, "language", context.language);
505
+ pushKV(metadata, "deepdots_user_id", envelope.userId);
506
+ pushKV(metadata, "deepdots_session_id", envelope.sessionId);
507
+ pushKV(metadata, "deepdots_platform", context.platform);
508
+ pushKV(metadata, "deepdots_language", context.language);
488
509
  if (context.device) {
489
- pushKV(metadata, "device_type", context.device.device_type);
490
- pushKV(metadata, "os_version", context.device.os_version);
491
- pushKV(metadata, "device_model", context.device.device_model);
492
- pushKV(metadata, "app_version", context.device.app_version);
493
- pushKV(metadata, "user_agent", context.device.user_agent);
510
+ const d = context.device;
511
+ pushKV(metadata, "deepdots_device_type", d.device_type);
512
+ pushKV(metadata, "deepdots_os_version", d.os_version);
513
+ pushKV(metadata, "deepdots_device_model", d.device_model);
514
+ pushKV(metadata, "deepdots_app_version", d.app_version);
515
+ pushKV(metadata, "deepdots_user_agent", d.user_agent);
516
+ pushKV(metadata, "deepdots_timezone", d.timezone);
517
+ pushKV(metadata, "deepdots_referrer", d.referrer);
518
+ pushKV(metadata, "deepdots_viewport_size", d.viewport_size);
519
+ pushKV(metadata, "deepdots_screen_resolution", d.screen_resolution);
520
+ pushKV(metadata, "deepdots_pixel_ratio", d.pixel_ratio);
521
+ pushKV(metadata, "deepdots_entry_type", d.entry_type);
522
+ pushKV(metadata, "deepdots_page_load_ms", d.page_load_ms);
523
+ pushKV(metadata, "deepdots_connection_type", d.connection_type);
524
+ pushKV(metadata, "deepdots_country", d.country);
525
+ pushKV(metadata, "deepdots_city", d.city);
494
526
  }
495
527
  for (const [k, v] of Object.entries(context.attributes ?? {})) {
496
528
  pushKV(metadata, k, v);
497
529
  }
530
+ for (const e of envelope.events) {
531
+ metadata.push({
532
+ key: e.name,
533
+ value: [JSON.stringify({ timestamp: e.timestamp, ...e.params ?? {} })]
534
+ });
535
+ }
498
536
  return {
499
537
  feedback: {
500
538
  text: "",
501
539
  answers: [],
502
540
  metadata,
503
- metrics,
504
541
  profile,
505
542
  finished: false
506
543
  },
507
544
  publicKey: keys.publicKey,
508
545
  integration: keys.integration,
509
546
  completed: false,
510
- // finished: false,
511
547
  ...feedbackSessionId ? { sessionId: feedbackSessionId } : {}
512
548
  };
513
549
  }
@@ -532,6 +568,7 @@ function createFeedbackSink(options) {
532
568
  if (data?.sessionId && data.sessionId !== feedbackSessionId) {
533
569
  feedbackSessionId = data.sessionId;
534
570
  options.log?.("[DeepdotsAnalytics] feedbackSessionId cacheado:", feedbackSessionId);
571
+ options.onSessionId?.(feedbackSessionId);
535
572
  }
536
573
  } catch {
537
574
  }
@@ -645,15 +682,57 @@ function parseDeviceType(ua) {
645
682
  }
646
683
  return "desktop";
647
684
  }
685
+ function getEntryType() {
686
+ if (typeof performance === "undefined") return void 0;
687
+ const entries = performance.getEntriesByType("navigation");
688
+ if (!entries.length) return void 0;
689
+ return entries[0].type;
690
+ }
691
+ function getPageLoadMs() {
692
+ if (typeof performance === "undefined") return void 0;
693
+ const entries = performance.getEntriesByType("navigation");
694
+ if (!entries.length) return void 0;
695
+ const load = entries[0].loadEventEnd;
696
+ return load > 0 ? Math.round(load) : void 0;
697
+ }
698
+ function getConnectionType() {
699
+ if (typeof navigator === "undefined") return void 0;
700
+ return navigator.connection?.effectiveType ?? void 0;
701
+ }
648
702
  function collectDeviceInfo(appVersion) {
649
703
  const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
650
704
  return {
651
705
  device_type: parseDeviceType(ua),
652
706
  user_agent: ua || void 0,
653
- app_version: appVersion
707
+ app_version: appVersion,
708
+ timezone: typeof Intl !== "undefined" ? Intl.DateTimeFormat().resolvedOptions().timeZone : void 0,
709
+ referrer: typeof document !== "undefined" && document.referrer ? document.referrer : void 0,
710
+ viewport_size: typeof window !== "undefined" ? `${window.innerWidth}x${window.innerHeight}` : void 0,
711
+ screen_resolution: typeof screen !== "undefined" ? `${screen.width}x${screen.height}` : void 0,
712
+ pixel_ratio: typeof window !== "undefined" ? window.devicePixelRatio : void 0,
713
+ entry_type: getEntryType(),
714
+ page_load_ms: getPageLoadMs(),
715
+ connection_type: getConnectionType()
654
716
  };
655
717
  }
656
718
 
719
+ // src/analytics/geo-info.ts
720
+ async function collectGeoInfo(fetchImpl) {
721
+ const f = fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
722
+ if (!f) return null;
723
+ try {
724
+ const res = await f("https://ipapi.co/json/");
725
+ if (!res.ok) return null;
726
+ const data = await res.json();
727
+ const country = data.country_code || void 0;
728
+ const city = data.city || void 0;
729
+ if (!country && !city) return null;
730
+ return { country, city };
731
+ } catch {
732
+ return null;
733
+ }
734
+ }
735
+
657
736
  // src/analytics/engagement-tracker.ts
658
737
  var EngagementTracker = class {
659
738
  constructor(now = () => Date.now()) {
@@ -721,9 +800,137 @@ var ContactManager = class {
721
800
  }
722
801
  };
723
802
 
803
+ // src/analytics/crash-reporter.ts
804
+ var STACK_MAX = 8e3;
805
+ var QUEUE_KEY = "deepdots.crash.queue";
806
+ var MAX_QUEUED = 20;
807
+ function crashRecordToParams(r) {
808
+ const params = {
809
+ crashed_at: r.crashedAt,
810
+ crash_type: r.crashType,
811
+ message: r.message,
812
+ stack: r.stack,
813
+ fatal: r.fatal,
814
+ handled: r.handled,
815
+ severity: r.severity
816
+ };
817
+ if (r.sessionId) params.crashed_session_id = r.sessionId;
818
+ if (r.appVersion) params.crashed_app_version = r.appVersion;
819
+ if (r.osVersion) params.crashed_os_version = r.osVersion;
820
+ if (r.deviceModel) params.crashed_device_model = r.deviceModel;
821
+ if (r.context) {
822
+ for (const [k, v] of Object.entries(r.context)) params[`ctx_${k}`] = v;
823
+ }
824
+ return params;
825
+ }
826
+ var CrashReporter = class {
827
+ constructor(options) {
828
+ this.options = options;
829
+ this.now = options.now ?? (() => Date.now());
830
+ }
831
+ isEnabled() {
832
+ return this.options.enabled ? this.options.enabled() : true;
833
+ }
834
+ /** Construye un CrashRecord capturando contexto en el momento del crash. */
835
+ buildRecord(error, severity, handled, fatal, context) {
836
+ const err = error;
837
+ const isErr = error instanceof Error;
838
+ const dev = this.options.device();
839
+ const ctx = context ? Object.fromEntries(Object.entries(context).map(([k, v]) => [k, String(v)])) : void 0;
840
+ return {
841
+ crashedAt: this.now(),
842
+ crashType: isErr && err.name || "Error",
843
+ message: isErr ? String(err.message ?? "") : String(error),
844
+ stack: (isErr && typeof err.stack === "string" ? err.stack : "").slice(0, STACK_MAX),
845
+ fatal,
846
+ handled,
847
+ severity,
848
+ sessionId: this.options.sessionId(),
849
+ appVersion: dev.appVersion,
850
+ osVersion: dev.osVersion,
851
+ deviceModel: dev.deviceModel,
852
+ context: ctx
853
+ };
854
+ }
855
+ /** API pública del host: reporta un error (app viva) → emite el evento ya. */
856
+ reportError(error, options = {}) {
857
+ if (!this.isEnabled()) return;
858
+ const severity = options.severity ?? "error";
859
+ const handled = options.handled ?? true;
860
+ const fatal = severity === "fatal";
861
+ const record = this.buildRecord(error, severity, handled, fatal, options.context);
862
+ this.options.emit(crashRecordToParams(record));
863
+ }
864
+ /** Captura un error no manejado (fatal) → persiste a disco para replay en el siguiente arranque. */
865
+ captureUnhandled(error) {
866
+ this.persist(this.buildRecord(error, "fatal", false, true));
867
+ }
868
+ /** Persiste un crash no capturado a disco para reenviarlo en el siguiente arranque. */
869
+ persist(record) {
870
+ let queue = [];
871
+ try {
872
+ const raw = this.options.storage.getItem(QUEUE_KEY);
873
+ if (raw) queue = JSON.parse(raw);
874
+ if (!Array.isArray(queue)) queue = [];
875
+ } catch {
876
+ queue = [];
877
+ }
878
+ queue.push(record);
879
+ if (queue.length > MAX_QUEUED) queue = queue.slice(queue.length - MAX_QUEUED);
880
+ try {
881
+ this.options.storage.setItem(QUEUE_KEY, JSON.stringify(queue));
882
+ } catch {
883
+ }
884
+ }
885
+ /** Lee y vacía la cola de crashes pendientes (llamado en init para el replay). */
886
+ drainPendingCrashes() {
887
+ let queue = [];
888
+ try {
889
+ const raw = this.options.storage.getItem(QUEUE_KEY);
890
+ if (raw) queue = JSON.parse(raw);
891
+ if (!Array.isArray(queue)) queue = [];
892
+ } catch {
893
+ queue = [];
894
+ }
895
+ if (queue.length) {
896
+ try {
897
+ this.options.storage.removeItem(QUEUE_KEY);
898
+ } catch {
899
+ }
900
+ }
901
+ return queue;
902
+ }
903
+ /** Instala los handlers globales de errores no capturados (no-op sin `window`). */
904
+ install() {
905
+ if (typeof window === "undefined") return;
906
+ window.addEventListener("error", (e) => {
907
+ if (!this.isEnabled()) return;
908
+ this.captureUnhandled(e.error ?? e.message ?? "error");
909
+ });
910
+ window.addEventListener("unhandledrejection", (e) => {
911
+ if (!this.isEnabled()) return;
912
+ this.captureUnhandled(e.reason ?? "unhandledrejection");
913
+ });
914
+ }
915
+ /** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. */
916
+ installReactNative(errorUtils) {
917
+ const previous = errorUtils.getGlobalHandler?.();
918
+ errorUtils.setGlobalHandler((error, isFatal) => {
919
+ if (this.isEnabled()) this.captureUnhandled(error);
920
+ previous?.(error, isFatal);
921
+ });
922
+ }
923
+ /** @internal Test seam: ejercita el camino de persistencia (window handlers no son testeables en vitest). */
924
+ _persistForTest(error) {
925
+ this.captureUnhandled(error);
926
+ }
927
+ };
928
+
724
929
  // src/core/deepdots-popups.ts
725
930
  var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
726
931
  var DAY_IN_MS = 24 * 60 * 60 * 1e3;
932
+ var ANALYTICS_MAX_BATCH_SIZE = 20;
933
+ var ANALYTICS_FLUSH_INTERVAL_MS = 3e4;
727
934
  var DeepdotsPopups = class {
728
935
  constructor() {
729
936
  this.config = null;
@@ -749,6 +956,12 @@ var DeepdotsPopups = class {
749
956
  this.tracking = null;
750
957
  /** Capa de analytics (canal separado del feedback). Null hasta init(). */
751
958
  this.analytics = null;
959
+ /** feedbackSessionId cacheado del canal de analytics (devuelto por POST /sdk/feedback). */
960
+ this.analyticsFeedbackSessionId = void 0;
961
+ /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
962
+ this.analyticsFlushTimer = void 0;
963
+ /** Crash & error reporting (#14–17). Null hasta init(). */
964
+ this.crashReporter = null;
752
965
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
753
966
  this.navObserver = null;
754
967
  /** Tiempo activo (engagement time, #8). */
@@ -795,21 +1008,49 @@ var DeepdotsPopups = class {
795
1008
  const analyticsSink = config.analytics ? createFeedbackSink({
796
1009
  baseUrl: this.baseUrl,
797
1010
  keys: config.analytics,
798
- log: (...a) => this.log(...a)
1011
+ log: (...a) => this.log(...a),
1012
+ onSessionId: (id) => {
1013
+ this.analyticsFeedbackSessionId = id;
1014
+ }
799
1015
  }) : void 0;
1016
+ const device = config.device ?? collectDeviceInfo(config.appVersion);
800
1017
  this.analytics = new AnalyticsManager({
801
1018
  sink: analyticsSink,
802
1019
  publicKey: config.analytics?.publicKey ?? this.config.apiKey,
803
1020
  language: typeof navigator !== "undefined" ? navigator.language : void 0,
804
1021
  platform: config.platform ?? "web",
805
- device: config.device ?? collectDeviceInfo(config.appVersion)
1022
+ device,
1023
+ maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,
1024
+ onFlushNeeded: () => this.flushAnalytics()
1025
+ });
1026
+ this.crashReporter = new CrashReporter({
1027
+ storage,
1028
+ emit: (params) => this.track("deepdots_app_crash", params),
1029
+ device: () => ({ appVersion: device.app_version, osVersion: device.os_version, deviceModel: device.device_model }),
1030
+ sessionId: () => this.tracking?.getSessionId() ?? null,
1031
+ now: () => Date.now(),
1032
+ enabled: () => this.tracking?.isTrackingEnabled() ?? false
1033
+ });
1034
+ if (this.tracking?.isTrackingEnabled()) {
1035
+ this.crashReporter.install();
1036
+ }
1037
+ collectGeoInfo().then((geo) => {
1038
+ if (geo) this.analytics?.updateDevice(geo);
1039
+ }).catch(() => {
806
1040
  });
807
1041
  this.navObserver = new NavigationObserver();
808
- this.navObserver.onVisit((v) => this.track("page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
1042
+ this.navObserver.onVisit((v) => this.track("deepdots_page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
809
1043
  this.navObserver.install();
810
1044
  this.engagement = new EngagementTracker();
811
1045
  this.engagement.resume();
812
1046
  this.setupAnalyticsFlush();
1047
+ this.track("deepdots_session_start", {});
1048
+ const pendingCrashes = this.crashReporter.drainPendingCrashes();
1049
+ if (this.tracking?.isTrackingEnabled()) {
1050
+ for (const rec of pendingCrashes) {
1051
+ this.track("deepdots_app_crash", crashRecordToParams(rec));
1052
+ }
1053
+ }
813
1054
  this.initialized = true;
814
1055
  this.log("SDK initialized", this.config);
815
1056
  if (this.renderer.init) this.renderer.init();
@@ -850,6 +1091,16 @@ var DeepdotsPopups = class {
850
1091
  if (!this.tracking?.isTrackingEnabled()) return;
851
1092
  this.analytics?.setUserAttributes(attributes);
852
1093
  }
1094
+ /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
1095
+ reportError(error, options) {
1096
+ if (!this.tracking?.isTrackingEnabled()) return;
1097
+ this.crashReporter?.reportError(error, options);
1098
+ }
1099
+ /** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
1100
+ installReactNativeCrashHandler(errorUtils) {
1101
+ if (!this.tracking?.isTrackingEnabled()) return;
1102
+ this.crashReporter?.installReactNative(errorUtils);
1103
+ }
853
1104
  /**
854
1105
  * Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
855
1106
  * Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
@@ -867,23 +1118,23 @@ var DeepdotsPopups = class {
867
1118
  this.analytics?.enterMiniService(name, entryPointType);
868
1119
  this.log("analytics \xB7 enterMiniService:", name, entryPointType ?? "");
869
1120
  }
870
- /** Cierra el mini-service activo (emite `mini_service_exit` con duración, #27). No-op si tracking off. */
871
- exitMiniService() {
1121
+ /** Cierra el mini-service `name` (emite `mini_service_exit` con duración, #27). No-op si tracking off o si ese no está activo. */
1122
+ exitMiniService(name) {
872
1123
  if (!this.tracking?.isTrackingEnabled()) return;
873
- this.analytics?.exitMiniService();
874
- this.log("analytics \xB7 exitMiniService");
1124
+ this.analytics?.exitMiniService(name);
1125
+ this.log("analytics \xB7 exitMiniService:", name);
875
1126
  }
876
1127
  /** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */
877
1128
  trackSearch(query, resultsCount, params) {
878
- this.track("search", { query, results_count: resultsCount, has_results: resultsCount > 0, ...params ?? {} });
1129
+ this.track("deepdots_search", { query, results_count: resultsCount, has_results: resultsCount > 0, ...params ?? {} });
879
1130
  }
880
1131
  /** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */
881
1132
  trackFindabilityFriction(frictionTopic, params) {
882
- this.track("findability_friction", { friction_topic: frictionTopic, ...params ?? {} });
1133
+ this.track("deepdots_findability_friction", { friction_topic: frictionTopic, ...params ?? {} });
883
1134
  }
884
1135
  /** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
885
1136
  trackFunnelStep(funnel, step, taskId, params) {
886
- this.track("funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
1137
+ this.track("deepdots_funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
887
1138
  }
888
1139
  /**
889
1140
  * Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
@@ -909,7 +1160,7 @@ var DeepdotsPopups = class {
909
1160
  onBackground() {
910
1161
  this.navObserver?.stop();
911
1162
  this.navStarted = false;
912
- this.exitMiniService();
1163
+ if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
913
1164
  this.flushEngagement();
914
1165
  this.engagement?.pause();
915
1166
  this.flushAnalytics();
@@ -932,7 +1183,7 @@ var DeepdotsPopups = class {
932
1183
  flushEngagement() {
933
1184
  if (!this.tracking?.isTrackingEnabled()) return;
934
1185
  const ms = this.engagement?.consume() ?? 0;
935
- if (ms > 0) this.track("user_engagement", { engagement_time_msec: ms });
1186
+ if (ms > 0) this.track("deepdots_user_engagement", { engagement_time_msec: ms });
936
1187
  }
937
1188
  analyticsIdentity() {
938
1189
  return {
@@ -943,9 +1194,11 @@ var DeepdotsPopups = class {
943
1194
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
944
1195
  setupAnalyticsFlush() {
945
1196
  if (typeof document === "undefined" || typeof window === "undefined") return;
1197
+ this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
946
1198
  window.addEventListener("pagehide", () => {
1199
+ clearInterval(this.analyticsFlushTimer);
947
1200
  this.navObserver?.stop();
948
- this.exitMiniService();
1201
+ if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
949
1202
  this.flushEngagement();
950
1203
  this.flushAnalytics();
951
1204
  });
@@ -1387,7 +1640,8 @@ var DeepdotsPopups = class {
1387
1640
  const userId = this.tracking?.getUserId() ?? this.config?.userId;
1388
1641
  const sessionId = this.tracking?.getSessionId() ?? void 0;
1389
1642
  const miniService = this.analytics?.getMiniService() ?? void 0;
1390
- this.log("tracking \xB7 survey identity \u2192", { userId: userId ?? null, sessionId: sessionId ?? null, miniService: miniService ?? null });
1643
+ const analyticsFeedbackSessionId = this.analyticsFeedbackSessionId;
1644
+ this.log("tracking \xB7 survey identity \u2192", { userId: userId ?? null, sessionId: sessionId ?? null, miniService: miniService ?? null, analyticsFeedbackSessionId: analyticsFeedbackSessionId ?? null });
1391
1645
  if (this.renderer) {
1392
1646
  this.renderer.show(
1393
1647
  surveyId,
@@ -1399,13 +1653,14 @@ var DeepdotsPopups = class {
1399
1653
  userId,
1400
1654
  style,
1401
1655
  sessionId,
1402
- miniService
1656
+ miniService,
1657
+ analyticsFeedbackSessionId
1403
1658
  );
1404
1659
  return;
1405
1660
  }
1406
1661
  if (!this.popupContainer) return;
1407
1662
  const container = this.popupContainer;
1408
- void import("./renderPopup-7GGKR5BY.mjs").then(({ renderPopup }) => {
1663
+ void import("./renderPopup-KBDWWSUU.mjs").then(({ renderPopup }) => {
1409
1664
  renderPopup(
1410
1665
  container,
1411
1666
  surveyId,
@@ -1417,7 +1672,8 @@ var DeepdotsPopups = class {
1417
1672
  userId,
1418
1673
  style,
1419
1674
  sessionId,
1420
- miniService
1675
+ miniService,
1676
+ analyticsFeedbackSessionId
1421
1677
  );
1422
1678
  });
1423
1679
  }
@@ -1608,6 +1864,10 @@ function setupReactNative(sdk2, config, deps = {}) {
1608
1864
  const device = deps.deviceInfo ? collectRnDevice(deps.deviceInfo) : config.device;
1609
1865
  const platform = deps.platform ?? config.platform;
1610
1866
  sdk2.init({ ...config, storage, device, platform });
1867
+ const errorUtils = deps.errorUtils ?? globalThis.ErrorUtils;
1868
+ if (errorUtils && typeof errorUtils.setGlobalHandler === "function") {
1869
+ sdk2.installReactNativeCrashHandler(errorUtils);
1870
+ }
1611
1871
  let subscription;
1612
1872
  if (deps.appState?.addEventListener) {
1613
1873
  subscription = deps.appState.addEventListener("change", (state) => {