@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.
package/dist/index.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  TrackingManager,
5
5
  buildSurveyIdentity,
6
6
  createDefaultStorage
7
- } from "./chunk-5VVCJBND.mjs";
7
+ } from "./chunk-6WTOQGET.mjs";
8
8
 
9
9
  // src/types/index.ts
10
10
  var POPUP_TRIGGER_CONDITION_STATUSES = [
@@ -253,12 +253,12 @@ var ReactNativePopupRenderer = class {
253
253
  }
254
254
  init() {
255
255
  }
256
- show(surveyId, productId, _actions, emit, onClose, env = "production", userId, _style, sessionId, miniService) {
256
+ show(surveyId, productId, _actions, emit, onClose, env = "production", userId, _style, sessionId, miniService, analyticsFeedbackSessionId) {
257
257
  this.emitFn = emit;
258
258
  this.onCloseFn = onClose;
259
259
  this.currentSurveyId = surveyId;
260
260
  this.partialEmitted = false;
261
- const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null);
261
+ const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null, analyticsFeedbackSessionId ?? null);
262
262
  const html = buildSurveyHtml({ surveyId, productId, env, profile, metadata });
263
263
  if (this.options.onShow) {
264
264
  this.options.onShow({ surveyId, productId, html });
@@ -349,14 +349,14 @@ var BrowserPopupRenderer = class {
349
349
  document.body.appendChild(this.container);
350
350
  }
351
351
  }
352
- show(surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService) {
352
+ show(surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId) {
353
353
  if (this.visible) return;
354
354
  if (!this.container || !document.body.contains(this.container)) this.init();
355
355
  if (!this.container) return;
356
356
  this.visible = true;
357
357
  const container = this.container;
358
- void import("./renderPopup-Y4WG3575.mjs").then(({ renderPopup }) => {
359
- renderPopup(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService);
358
+ void import("./renderPopup-YLFRGPQN.mjs").then(({ renderPopup }) => {
359
+ renderPopup(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService, analyticsFeedbackSessionId);
360
360
  });
361
361
  }
362
362
  hide() {
@@ -387,14 +387,20 @@ var AnalyticsManager = class {
387
387
  constructor(options = {}) {
388
388
  this.events = [];
389
389
  this.attributes = {};
390
- this.miniService = null;
391
- this.miniServiceEnteredAt = 0;
390
+ /** Mini-services activos: nombre → timestamp de entrada. El orden de inserción marca el "más reciente". */
391
+ this.activeMiniServices = /* @__PURE__ */ new Map();
392
392
  this.sink = options.sink ?? dryRunSink;
393
393
  this.now = options.now ?? (() => Date.now());
394
394
  this.publicKey = options.publicKey;
395
395
  this.language = options.language;
396
396
  this.device = options.device;
397
397
  this.platform = options.platform ?? "web";
398
+ this.maxBatchSize = options.maxBatchSize ?? 20;
399
+ this.onFlushNeeded = options.onFlushNeeded;
400
+ }
401
+ /** Actualiza campos de device en runtime (p.ej. country/city tras resolver geo). */
402
+ updateDevice(partial) {
403
+ this.device = { ...this.device, ...partial };
398
404
  }
399
405
  /** Mezcla user attributes (se coercionan a string). Mutable en runtime. */
400
406
  setUserAttributes(attrs) {
@@ -403,24 +409,38 @@ var AnalyticsManager = class {
403
409
  this.attributes[k] = String(v);
404
410
  }
405
411
  }
406
- /** Marca el inicio de un mini-service; etiqueta los eventos siguientes con `mini_service`. */
412
+ /**
413
+ * Marca el inicio de un mini-service; etiqueta los eventos siguientes con `mini_service`.
414
+ * Admite varios activos a la vez (concurrencia); el "actual" para etiquetar es el más reciente.
415
+ * Reentrar con un nombre ya activo refresca su orden y su tiempo de entrada.
416
+ */
407
417
  enterMiniService(name, entryPointType) {
408
- this.miniService = name;
409
- this.miniServiceEnteredAt = this.now();
410
- this.track("mini_service_enter", { entry_point_type: entryPointType ?? null });
411
- }
412
- /** Cierra el mini-service activo emitiendo `mini_service_exit` con su duración (#27). No-op si no hay ninguno. */
413
- exitMiniService() {
414
- const name = this.miniService;
415
- if (name == null) return;
416
- const durationSeconds = Math.max(0, Math.round((this.now() - this.miniServiceEnteredAt) / 1e3));
417
- this.miniService = null;
418
- this.track("mini_service_exit", { mini_service: name, duration_seconds: durationSeconds });
418
+ this.activeMiniServices.delete(name);
419
+ this.activeMiniServices.set(name, this.now());
420
+ this.track("deepdots_mini_service_enter", { entry_point_type: entryPointType ?? null });
421
+ }
422
+ /**
423
+ * Cierra el mini-service `name` emitiendo `mini_service_exit` con su duración (#27).
424
+ * No-op si ese nombre no está activo. Cierre coherente con concurrencia.
425
+ */
426
+ exitMiniService(name) {
427
+ const enteredAt = this.activeMiniServices.get(name);
428
+ if (enteredAt === void 0) return;
429
+ const durationSeconds = Math.max(0, Math.round((this.now() - enteredAt) / 1e3));
430
+ this.activeMiniServices.delete(name);
431
+ this.track("deepdots_mini_service_exit", { mini_service: name, duration_seconds: durationSeconds });
432
+ }
433
+ /** Cierra TODOS los mini-services activos (orden LIFO). Para el cierre por lifecycle (background/cierre). */
434
+ exitAllMiniServices() {
435
+ for (const name of Array.from(this.activeMiniServices.keys()).reverse()) {
436
+ this.exitMiniService(name);
437
+ }
419
438
  }
420
439
  /** Registra un evento de analítica (modelo GA: nombre + params). */
421
440
  track(name, params) {
441
+ const current = this.getMiniService();
422
442
  const merged = {
423
- ...this.miniService ? { mini_service: this.miniService } : {},
443
+ ...current ? { mini_service: current } : {},
424
444
  ...params ?? {}
425
445
  };
426
446
  this.events.push({
@@ -428,10 +448,15 @@ var AnalyticsManager = class {
428
448
  timestamp: this.now(),
429
449
  params: Object.keys(merged).length ? merged : void 0
430
450
  });
451
+ if (this.events.length >= this.maxBatchSize) {
452
+ this.onFlushNeeded?.();
453
+ }
431
454
  }
432
- /** Mini-service activo (para inyectarlo en la metadata del survey, #33). */
455
+ /** Mini-service actual (el más reciente aún activo) para etiquetar eventos + metadata del survey (#33). */
433
456
  getMiniService() {
434
- return this.miniService;
457
+ let last = null;
458
+ for (const name of this.activeMiniServices.keys()) last = name;
459
+ return last;
435
460
  }
436
461
  /** Nº de eventos pendientes de flush. */
437
462
  pending() {
@@ -465,44 +490,55 @@ var AnalyticsManager = class {
465
490
  // src/analytics/feedback-payload.ts
466
491
  function pushKV(list, key, value) {
467
492
  if (value === void 0 || value === null || value === "") return;
468
- list.push({ key, value: String(value) });
493
+ list.push({ key, value: [String(value)] });
469
494
  }
470
495
  function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
471
496
  const { context } = envelope;
472
- const metrics = envelope.events.map((e) => ({
473
- key: e.name,
474
- value: JSON.stringify({ timestamp: e.timestamp, ...e.params ?? {} })
475
- }));
476
497
  const profile = [];
477
498
  pushKV(profile, "external-user-id", envelope.userId);
478
499
  const metadata = [];
479
- pushKV(metadata, "user_id", envelope.userId);
480
- pushKV(metadata, "session_id", envelope.sessionId);
481
- pushKV(metadata, "platform", context.platform);
482
- pushKV(metadata, "language", context.language);
500
+ pushKV(metadata, "deepdots_user_id", envelope.userId);
501
+ pushKV(metadata, "deepdots_session_id", envelope.sessionId);
502
+ pushKV(metadata, "deepdots_platform", context.platform);
503
+ pushKV(metadata, "deepdots_language", context.language);
483
504
  if (context.device) {
484
- pushKV(metadata, "device_type", context.device.device_type);
485
- pushKV(metadata, "os_version", context.device.os_version);
486
- pushKV(metadata, "device_model", context.device.device_model);
487
- pushKV(metadata, "app_version", context.device.app_version);
488
- pushKV(metadata, "user_agent", context.device.user_agent);
505
+ const d = context.device;
506
+ pushKV(metadata, "deepdots_device_type", d.device_type);
507
+ pushKV(metadata, "deepdots_os_version", d.os_version);
508
+ pushKV(metadata, "deepdots_device_model", d.device_model);
509
+ pushKV(metadata, "deepdots_app_version", d.app_version);
510
+ pushKV(metadata, "deepdots_user_agent", d.user_agent);
511
+ pushKV(metadata, "deepdots_timezone", d.timezone);
512
+ pushKV(metadata, "deepdots_referrer", d.referrer);
513
+ pushKV(metadata, "deepdots_viewport_size", d.viewport_size);
514
+ pushKV(metadata, "deepdots_screen_resolution", d.screen_resolution);
515
+ pushKV(metadata, "deepdots_pixel_ratio", d.pixel_ratio);
516
+ pushKV(metadata, "deepdots_entry_type", d.entry_type);
517
+ pushKV(metadata, "deepdots_page_load_ms", d.page_load_ms);
518
+ pushKV(metadata, "deepdots_connection_type", d.connection_type);
519
+ pushKV(metadata, "deepdots_country", d.country);
520
+ pushKV(metadata, "deepdots_city", d.city);
489
521
  }
490
522
  for (const [k, v] of Object.entries(context.attributes ?? {})) {
491
523
  pushKV(metadata, k, v);
492
524
  }
525
+ for (const e of envelope.events) {
526
+ metadata.push({
527
+ key: e.name,
528
+ value: [JSON.stringify({ timestamp: e.timestamp, ...e.params ?? {} })]
529
+ });
530
+ }
493
531
  return {
494
532
  feedback: {
495
533
  text: "",
496
534
  answers: [],
497
535
  metadata,
498
- metrics,
499
536
  profile,
500
537
  finished: false
501
538
  },
502
539
  publicKey: keys.publicKey,
503
540
  integration: keys.integration,
504
541
  completed: false,
505
- // finished: false,
506
542
  ...feedbackSessionId ? { sessionId: feedbackSessionId } : {}
507
543
  };
508
544
  }
@@ -527,6 +563,7 @@ function createFeedbackSink(options) {
527
563
  if (data?.sessionId && data.sessionId !== feedbackSessionId) {
528
564
  feedbackSessionId = data.sessionId;
529
565
  options.log?.("[DeepdotsAnalytics] feedbackSessionId cacheado:", feedbackSessionId);
566
+ options.onSessionId?.(feedbackSessionId);
530
567
  }
531
568
  } catch {
532
569
  }
@@ -640,15 +677,57 @@ function parseDeviceType(ua) {
640
677
  }
641
678
  return "desktop";
642
679
  }
680
+ function getEntryType() {
681
+ if (typeof performance === "undefined") return void 0;
682
+ const entries = performance.getEntriesByType("navigation");
683
+ if (!entries.length) return void 0;
684
+ return entries[0].type;
685
+ }
686
+ function getPageLoadMs() {
687
+ if (typeof performance === "undefined") return void 0;
688
+ const entries = performance.getEntriesByType("navigation");
689
+ if (!entries.length) return void 0;
690
+ const load = entries[0].loadEventEnd;
691
+ return load > 0 ? Math.round(load) : void 0;
692
+ }
693
+ function getConnectionType() {
694
+ if (typeof navigator === "undefined") return void 0;
695
+ return navigator.connection?.effectiveType ?? void 0;
696
+ }
643
697
  function collectDeviceInfo(appVersion) {
644
698
  const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
645
699
  return {
646
700
  device_type: parseDeviceType(ua),
647
701
  user_agent: ua || void 0,
648
- app_version: appVersion
702
+ app_version: appVersion,
703
+ timezone: typeof Intl !== "undefined" ? Intl.DateTimeFormat().resolvedOptions().timeZone : void 0,
704
+ referrer: typeof document !== "undefined" && document.referrer ? document.referrer : void 0,
705
+ viewport_size: typeof window !== "undefined" ? `${window.innerWidth}x${window.innerHeight}` : void 0,
706
+ screen_resolution: typeof screen !== "undefined" ? `${screen.width}x${screen.height}` : void 0,
707
+ pixel_ratio: typeof window !== "undefined" ? window.devicePixelRatio : void 0,
708
+ entry_type: getEntryType(),
709
+ page_load_ms: getPageLoadMs(),
710
+ connection_type: getConnectionType()
649
711
  };
650
712
  }
651
713
 
714
+ // src/analytics/geo-info.ts
715
+ async function collectGeoInfo(fetchImpl) {
716
+ const f = fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
717
+ if (!f) return null;
718
+ try {
719
+ const res = await f("https://ipapi.co/json/");
720
+ if (!res.ok) return null;
721
+ const data = await res.json();
722
+ const country = data.country_code || void 0;
723
+ const city = data.city || void 0;
724
+ if (!country && !city) return null;
725
+ return { country, city };
726
+ } catch {
727
+ return null;
728
+ }
729
+ }
730
+
652
731
  // src/analytics/engagement-tracker.ts
653
732
  var EngagementTracker = class {
654
733
  constructor(now = () => Date.now()) {
@@ -716,9 +795,137 @@ var ContactManager = class {
716
795
  }
717
796
  };
718
797
 
798
+ // src/analytics/crash-reporter.ts
799
+ var STACK_MAX = 8e3;
800
+ var QUEUE_KEY = "deepdots.crash.queue";
801
+ var MAX_QUEUED = 20;
802
+ function crashRecordToParams(r) {
803
+ const params = {
804
+ crashed_at: r.crashedAt,
805
+ crash_type: r.crashType,
806
+ message: r.message,
807
+ stack: r.stack,
808
+ fatal: r.fatal,
809
+ handled: r.handled,
810
+ severity: r.severity
811
+ };
812
+ if (r.sessionId) params.crashed_session_id = r.sessionId;
813
+ if (r.appVersion) params.crashed_app_version = r.appVersion;
814
+ if (r.osVersion) params.crashed_os_version = r.osVersion;
815
+ if (r.deviceModel) params.crashed_device_model = r.deviceModel;
816
+ if (r.context) {
817
+ for (const [k, v] of Object.entries(r.context)) params[`ctx_${k}`] = v;
818
+ }
819
+ return params;
820
+ }
821
+ var CrashReporter = class {
822
+ constructor(options) {
823
+ this.options = options;
824
+ this.now = options.now ?? (() => Date.now());
825
+ }
826
+ isEnabled() {
827
+ return this.options.enabled ? this.options.enabled() : true;
828
+ }
829
+ /** Construye un CrashRecord capturando contexto en el momento del crash. */
830
+ buildRecord(error, severity, handled, fatal, context) {
831
+ const err = error;
832
+ const isErr = error instanceof Error;
833
+ const dev = this.options.device();
834
+ const ctx = context ? Object.fromEntries(Object.entries(context).map(([k, v]) => [k, String(v)])) : void 0;
835
+ return {
836
+ crashedAt: this.now(),
837
+ crashType: isErr && err.name || "Error",
838
+ message: isErr ? String(err.message ?? "") : String(error),
839
+ stack: (isErr && typeof err.stack === "string" ? err.stack : "").slice(0, STACK_MAX),
840
+ fatal,
841
+ handled,
842
+ severity,
843
+ sessionId: this.options.sessionId(),
844
+ appVersion: dev.appVersion,
845
+ osVersion: dev.osVersion,
846
+ deviceModel: dev.deviceModel,
847
+ context: ctx
848
+ };
849
+ }
850
+ /** API pública del host: reporta un error (app viva) → emite el evento ya. */
851
+ reportError(error, options = {}) {
852
+ if (!this.isEnabled()) return;
853
+ const severity = options.severity ?? "error";
854
+ const handled = options.handled ?? true;
855
+ const fatal = severity === "fatal";
856
+ const record = this.buildRecord(error, severity, handled, fatal, options.context);
857
+ this.options.emit(crashRecordToParams(record));
858
+ }
859
+ /** Captura un error no manejado (fatal) → persiste a disco para replay en el siguiente arranque. */
860
+ captureUnhandled(error) {
861
+ this.persist(this.buildRecord(error, "fatal", false, true));
862
+ }
863
+ /** Persiste un crash no capturado a disco para reenviarlo en el siguiente arranque. */
864
+ persist(record) {
865
+ let queue = [];
866
+ try {
867
+ const raw = this.options.storage.getItem(QUEUE_KEY);
868
+ if (raw) queue = JSON.parse(raw);
869
+ if (!Array.isArray(queue)) queue = [];
870
+ } catch {
871
+ queue = [];
872
+ }
873
+ queue.push(record);
874
+ if (queue.length > MAX_QUEUED) queue = queue.slice(queue.length - MAX_QUEUED);
875
+ try {
876
+ this.options.storage.setItem(QUEUE_KEY, JSON.stringify(queue));
877
+ } catch {
878
+ }
879
+ }
880
+ /** Lee y vacía la cola de crashes pendientes (llamado en init para el replay). */
881
+ drainPendingCrashes() {
882
+ let queue = [];
883
+ try {
884
+ const raw = this.options.storage.getItem(QUEUE_KEY);
885
+ if (raw) queue = JSON.parse(raw);
886
+ if (!Array.isArray(queue)) queue = [];
887
+ } catch {
888
+ queue = [];
889
+ }
890
+ if (queue.length) {
891
+ try {
892
+ this.options.storage.removeItem(QUEUE_KEY);
893
+ } catch {
894
+ }
895
+ }
896
+ return queue;
897
+ }
898
+ /** Instala los handlers globales de errores no capturados (no-op sin `window`). */
899
+ install() {
900
+ if (typeof window === "undefined") return;
901
+ window.addEventListener("error", (e) => {
902
+ if (!this.isEnabled()) return;
903
+ this.captureUnhandled(e.error ?? e.message ?? "error");
904
+ });
905
+ window.addEventListener("unhandledrejection", (e) => {
906
+ if (!this.isEnabled()) return;
907
+ this.captureUnhandled(e.reason ?? "unhandledrejection");
908
+ });
909
+ }
910
+ /** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. */
911
+ installReactNative(errorUtils) {
912
+ const previous = errorUtils.getGlobalHandler?.();
913
+ errorUtils.setGlobalHandler((error, isFatal) => {
914
+ if (this.isEnabled()) this.captureUnhandled(error);
915
+ previous?.(error, isFatal);
916
+ });
917
+ }
918
+ /** @internal Test seam: ejercita el camino de persistencia (window handlers no son testeables en vitest). */
919
+ _persistForTest(error) {
920
+ this.captureUnhandled(error);
921
+ }
922
+ };
923
+
719
924
  // src/core/deepdots-popups.ts
720
925
  var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
721
926
  var DAY_IN_MS = 24 * 60 * 60 * 1e3;
927
+ var ANALYTICS_MAX_BATCH_SIZE = 20;
928
+ var ANALYTICS_FLUSH_INTERVAL_MS = 3e4;
722
929
  var DeepdotsPopups = class {
723
930
  constructor() {
724
931
  this.config = null;
@@ -744,6 +951,12 @@ var DeepdotsPopups = class {
744
951
  this.tracking = null;
745
952
  /** Capa de analytics (canal separado del feedback). Null hasta init(). */
746
953
  this.analytics = null;
954
+ /** feedbackSessionId cacheado del canal de analytics (devuelto por POST /sdk/feedback). */
955
+ this.analyticsFeedbackSessionId = void 0;
956
+ /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
957
+ this.analyticsFlushTimer = void 0;
958
+ /** Crash & error reporting (#14–17). Null hasta init(). */
959
+ this.crashReporter = null;
747
960
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
748
961
  this.navObserver = null;
749
962
  /** Tiempo activo (engagement time, #8). */
@@ -790,21 +1003,49 @@ var DeepdotsPopups = class {
790
1003
  const analyticsSink = config.analytics ? createFeedbackSink({
791
1004
  baseUrl: this.baseUrl,
792
1005
  keys: config.analytics,
793
- log: (...a) => this.log(...a)
1006
+ log: (...a) => this.log(...a),
1007
+ onSessionId: (id) => {
1008
+ this.analyticsFeedbackSessionId = id;
1009
+ }
794
1010
  }) : void 0;
1011
+ const device = config.device ?? collectDeviceInfo(config.appVersion);
795
1012
  this.analytics = new AnalyticsManager({
796
1013
  sink: analyticsSink,
797
1014
  publicKey: config.analytics?.publicKey ?? this.config.apiKey,
798
1015
  language: typeof navigator !== "undefined" ? navigator.language : void 0,
799
1016
  platform: config.platform ?? "web",
800
- device: config.device ?? collectDeviceInfo(config.appVersion)
1017
+ device,
1018
+ maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,
1019
+ onFlushNeeded: () => this.flushAnalytics()
1020
+ });
1021
+ this.crashReporter = new CrashReporter({
1022
+ storage,
1023
+ emit: (params) => this.track("deepdots_app_crash", params),
1024
+ device: () => ({ appVersion: device.app_version, osVersion: device.os_version, deviceModel: device.device_model }),
1025
+ sessionId: () => this.tracking?.getSessionId() ?? null,
1026
+ now: () => Date.now(),
1027
+ enabled: () => this.tracking?.isTrackingEnabled() ?? false
1028
+ });
1029
+ if (this.tracking?.isTrackingEnabled()) {
1030
+ this.crashReporter.install();
1031
+ }
1032
+ collectGeoInfo().then((geo) => {
1033
+ if (geo) this.analytics?.updateDevice(geo);
1034
+ }).catch(() => {
801
1035
  });
802
1036
  this.navObserver = new NavigationObserver();
803
- this.navObserver.onVisit((v) => this.track("page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
1037
+ this.navObserver.onVisit((v) => this.track("deepdots_page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
804
1038
  this.navObserver.install();
805
1039
  this.engagement = new EngagementTracker();
806
1040
  this.engagement.resume();
807
1041
  this.setupAnalyticsFlush();
1042
+ this.track("deepdots_session_start", {});
1043
+ const pendingCrashes = this.crashReporter.drainPendingCrashes();
1044
+ if (this.tracking?.isTrackingEnabled()) {
1045
+ for (const rec of pendingCrashes) {
1046
+ this.track("deepdots_app_crash", crashRecordToParams(rec));
1047
+ }
1048
+ }
808
1049
  this.initialized = true;
809
1050
  this.log("SDK initialized", this.config);
810
1051
  if (this.renderer.init) this.renderer.init();
@@ -845,6 +1086,16 @@ var DeepdotsPopups = class {
845
1086
  if (!this.tracking?.isTrackingEnabled()) return;
846
1087
  this.analytics?.setUserAttributes(attributes);
847
1088
  }
1089
+ /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
1090
+ reportError(error, options) {
1091
+ if (!this.tracking?.isTrackingEnabled()) return;
1092
+ this.crashReporter?.reportError(error, options);
1093
+ }
1094
+ /** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
1095
+ installReactNativeCrashHandler(errorUtils) {
1096
+ if (!this.tracking?.isTrackingEnabled()) return;
1097
+ this.crashReporter?.installReactNative(errorUtils);
1098
+ }
848
1099
  /**
849
1100
  * Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
850
1101
  * Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
@@ -862,23 +1113,23 @@ var DeepdotsPopups = class {
862
1113
  this.analytics?.enterMiniService(name, entryPointType);
863
1114
  this.log("analytics \xB7 enterMiniService:", name, entryPointType ?? "");
864
1115
  }
865
- /** Cierra el mini-service activo (emite `mini_service_exit` con duración, #27). No-op si tracking off. */
866
- exitMiniService() {
1116
+ /** Cierra el mini-service `name` (emite `mini_service_exit` con duración, #27). No-op si tracking off o si ese no está activo. */
1117
+ exitMiniService(name) {
867
1118
  if (!this.tracking?.isTrackingEnabled()) return;
868
- this.analytics?.exitMiniService();
869
- this.log("analytics \xB7 exitMiniService");
1119
+ this.analytics?.exitMiniService(name);
1120
+ this.log("analytics \xB7 exitMiniService:", name);
870
1121
  }
871
1122
  /** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */
872
1123
  trackSearch(query, resultsCount, params) {
873
- this.track("search", { query, results_count: resultsCount, has_results: resultsCount > 0, ...params ?? {} });
1124
+ this.track("deepdots_search", { query, results_count: resultsCount, has_results: resultsCount > 0, ...params ?? {} });
874
1125
  }
875
1126
  /** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */
876
1127
  trackFindabilityFriction(frictionTopic, params) {
877
- this.track("findability_friction", { friction_topic: frictionTopic, ...params ?? {} });
1128
+ this.track("deepdots_findability_friction", { friction_topic: frictionTopic, ...params ?? {} });
878
1129
  }
879
1130
  /** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
880
1131
  trackFunnelStep(funnel, step, taskId, params) {
881
- this.track("funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
1132
+ this.track("deepdots_funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
882
1133
  }
883
1134
  /**
884
1135
  * Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
@@ -904,7 +1155,7 @@ var DeepdotsPopups = class {
904
1155
  onBackground() {
905
1156
  this.navObserver?.stop();
906
1157
  this.navStarted = false;
907
- this.exitMiniService();
1158
+ if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
908
1159
  this.flushEngagement();
909
1160
  this.engagement?.pause();
910
1161
  this.flushAnalytics();
@@ -927,7 +1178,7 @@ var DeepdotsPopups = class {
927
1178
  flushEngagement() {
928
1179
  if (!this.tracking?.isTrackingEnabled()) return;
929
1180
  const ms = this.engagement?.consume() ?? 0;
930
- if (ms > 0) this.track("user_engagement", { engagement_time_msec: ms });
1181
+ if (ms > 0) this.track("deepdots_user_engagement", { engagement_time_msec: ms });
931
1182
  }
932
1183
  analyticsIdentity() {
933
1184
  return {
@@ -938,9 +1189,11 @@ var DeepdotsPopups = class {
938
1189
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
939
1190
  setupAnalyticsFlush() {
940
1191
  if (typeof document === "undefined" || typeof window === "undefined") return;
1192
+ this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
941
1193
  window.addEventListener("pagehide", () => {
1194
+ clearInterval(this.analyticsFlushTimer);
942
1195
  this.navObserver?.stop();
943
- this.exitMiniService();
1196
+ if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
944
1197
  this.flushEngagement();
945
1198
  this.flushAnalytics();
946
1199
  });
@@ -1382,7 +1635,8 @@ var DeepdotsPopups = class {
1382
1635
  const userId = this.tracking?.getUserId() ?? this.config?.userId;
1383
1636
  const sessionId = this.tracking?.getSessionId() ?? void 0;
1384
1637
  const miniService = this.analytics?.getMiniService() ?? void 0;
1385
- this.log("tracking \xB7 survey identity \u2192", { userId: userId ?? null, sessionId: sessionId ?? null, miniService: miniService ?? null });
1638
+ const analyticsFeedbackSessionId = this.analyticsFeedbackSessionId;
1639
+ this.log("tracking \xB7 survey identity \u2192", { userId: userId ?? null, sessionId: sessionId ?? null, miniService: miniService ?? null, analyticsFeedbackSessionId: analyticsFeedbackSessionId ?? null });
1386
1640
  if (this.renderer) {
1387
1641
  this.renderer.show(
1388
1642
  surveyId,
@@ -1394,13 +1648,14 @@ var DeepdotsPopups = class {
1394
1648
  userId,
1395
1649
  style,
1396
1650
  sessionId,
1397
- miniService
1651
+ miniService,
1652
+ analyticsFeedbackSessionId
1398
1653
  );
1399
1654
  return;
1400
1655
  }
1401
1656
  if (!this.popupContainer) return;
1402
1657
  const container = this.popupContainer;
1403
- void import("./renderPopup-Y4WG3575.mjs").then(({ renderPopup }) => {
1658
+ void import("./renderPopup-YLFRGPQN.mjs").then(({ renderPopup }) => {
1404
1659
  renderPopup(
1405
1660
  container,
1406
1661
  surveyId,
@@ -1412,7 +1667,8 @@ var DeepdotsPopups = class {
1412
1667
  userId,
1413
1668
  style,
1414
1669
  sessionId,
1415
- miniService
1670
+ miniService,
1671
+ analyticsFeedbackSessionId
1416
1672
  );
1417
1673
  });
1418
1674
  }
@@ -1603,6 +1859,10 @@ function setupReactNative(sdk, config, deps = {}) {
1603
1859
  const device = deps.deviceInfo ? collectRnDevice(deps.deviceInfo) : config.device;
1604
1860
  const platform = deps.platform ?? config.platform;
1605
1861
  sdk.init({ ...config, storage, device, platform });
1862
+ const errorUtils = deps.errorUtils ?? globalThis.ErrorUtils;
1863
+ if (errorUtils && typeof errorUtils.setGlobalHandler === "function") {
1864
+ sdk.installReactNativeCrashHandler(errorUtils);
1865
+ }
1606
1866
  let subscription;
1607
1867
  if (deps.appState?.addEventListener) {
1608
1868
  subscription = deps.appState.addEventListener("change", (state) => {
@@ -1617,6 +1877,7 @@ function setupReactNative(sdk, config, deps = {}) {
1617
1877
  export {
1618
1878
  BrowserPopupRenderer,
1619
1879
  ContactManager,
1880
+ CrashReporter,
1620
1881
  DeepdotsPopups,
1621
1882
  InMemoryStorage,
1622
1883
  NoopPopupRenderer,
@@ -1625,6 +1886,7 @@ export {
1625
1886
  buildAnalyticsFeedbackBody,
1626
1887
  buildSurveyHtml,
1627
1888
  collectRnDevice,
1889
+ crashRecordToParams,
1628
1890
  createDefaultRenderer,
1629
1891
  createFeedbackSink,
1630
1892
  mmkvStorage,