@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.js CHANGED
@@ -51,12 +51,13 @@ function createDefaultStorage() {
51
51
  }
52
52
  return new InMemoryStorage();
53
53
  }
54
- function buildSurveyIdentity(userId, sessionId, miniService = null) {
54
+ function buildSurveyIdentity(userId, sessionId, miniService = null, analyticsFeedbackSessionId = null) {
55
55
  const profile = userId ? [{ key: "external-user-id", value: [userId] }] : [];
56
56
  const metadata = [];
57
57
  if (sessionId) metadata.push({ key: "session_id", value: [sessionId] });
58
58
  if (userId) metadata.push({ key: "user_id", value: [userId] });
59
59
  if (miniService) metadata.push({ key: "mini_service", value: [miniService] });
60
+ if (analyticsFeedbackSessionId) metadata.push({ key: "deepdots_analytics_feedback_session_id", value: [analyticsFeedbackSessionId] });
60
61
  return { profile, metadata };
61
62
  }
62
63
  function defaultUuid() {
@@ -2991,7 +2992,7 @@ function ensureResponsiveStyles(_popup) {
2991
2992
  `;
2992
2993
  document.head.appendChild(style);
2993
2994
  }
2994
- async function renderPopup(container, surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService) {
2995
+ async function renderPopup(container, surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId) {
2995
2996
  let surveyCompletedEmitted = false;
2996
2997
  let stylesInjected = false;
2997
2998
  const isDark = style?.theme === "dark";
@@ -3302,7 +3303,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
3302
3303
  env: env === "production" ? "prod" : "dev"
3303
3304
  }
3304
3305
  );
3305
- const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null);
3306
+ const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null, analyticsFeedbackSessionId ?? null);
3306
3307
  formInstance = import_native.default.form(surveyId, productId, profile, metadata);
3307
3308
  const generateOptions = {
3308
3309
  addButton: false,
@@ -3457,6 +3458,7 @@ var index_exports = {};
3457
3458
  __export(index_exports, {
3458
3459
  BrowserPopupRenderer: () => BrowserPopupRenderer,
3459
3460
  ContactManager: () => ContactManager,
3461
+ CrashReporter: () => CrashReporter,
3460
3462
  DeepdotsPopups: () => DeepdotsPopups,
3461
3463
  InMemoryStorage: () => InMemoryStorage,
3462
3464
  NoopPopupRenderer: () => NoopPopupRenderer,
@@ -3465,6 +3467,7 @@ __export(index_exports, {
3465
3467
  buildAnalyticsFeedbackBody: () => buildAnalyticsFeedbackBody,
3466
3468
  buildSurveyHtml: () => buildSurveyHtml,
3467
3469
  collectRnDevice: () => collectRnDevice,
3470
+ crashRecordToParams: () => crashRecordToParams,
3468
3471
  createDefaultRenderer: () => createDefaultRenderer,
3469
3472
  createFeedbackSink: () => createFeedbackSink,
3470
3473
  mmkvStorage: () => mmkvStorage,
@@ -3722,12 +3725,12 @@ var ReactNativePopupRenderer = class {
3722
3725
  }
3723
3726
  init() {
3724
3727
  }
3725
- show(surveyId, productId, _actions, emit, onClose, env = "production", userId, _style, sessionId, miniService) {
3728
+ show(surveyId, productId, _actions, emit, onClose, env = "production", userId, _style, sessionId, miniService, analyticsFeedbackSessionId) {
3726
3729
  this.emitFn = emit;
3727
3730
  this.onCloseFn = onClose;
3728
3731
  this.currentSurveyId = surveyId;
3729
3732
  this.partialEmitted = false;
3730
- const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null);
3733
+ const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null, analyticsFeedbackSessionId ?? null);
3731
3734
  const html = buildSurveyHtml({ surveyId, productId, env, profile, metadata });
3732
3735
  if (this.options.onShow) {
3733
3736
  this.options.onShow({ surveyId, productId, html });
@@ -3818,14 +3821,14 @@ var BrowserPopupRenderer = class {
3818
3821
  document.body.appendChild(this.container);
3819
3822
  }
3820
3823
  }
3821
- show(surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService) {
3824
+ show(surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId) {
3822
3825
  if (this.visible) return;
3823
3826
  if (!this.container || !document.body.contains(this.container)) this.init();
3824
3827
  if (!this.container) return;
3825
3828
  this.visible = true;
3826
3829
  const container = this.container;
3827
3830
  void Promise.resolve().then(() => (init_renderPopup(), renderPopup_exports)).then(({ renderPopup: renderPopup2 }) => {
3828
- renderPopup2(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService);
3831
+ renderPopup2(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService, analyticsFeedbackSessionId);
3829
3832
  });
3830
3833
  }
3831
3834
  hide() {
@@ -3859,14 +3862,20 @@ var AnalyticsManager = class {
3859
3862
  constructor(options = {}) {
3860
3863
  this.events = [];
3861
3864
  this.attributes = {};
3862
- this.miniService = null;
3863
- this.miniServiceEnteredAt = 0;
3865
+ /** Mini-services activos: nombre → timestamp de entrada. El orden de inserción marca el "más reciente". */
3866
+ this.activeMiniServices = /* @__PURE__ */ new Map();
3864
3867
  this.sink = options.sink ?? dryRunSink;
3865
3868
  this.now = options.now ?? (() => Date.now());
3866
3869
  this.publicKey = options.publicKey;
3867
3870
  this.language = options.language;
3868
3871
  this.device = options.device;
3869
3872
  this.platform = options.platform ?? "web";
3873
+ this.maxBatchSize = options.maxBatchSize ?? 20;
3874
+ this.onFlushNeeded = options.onFlushNeeded;
3875
+ }
3876
+ /** Actualiza campos de device en runtime (p.ej. country/city tras resolver geo). */
3877
+ updateDevice(partial) {
3878
+ this.device = { ...this.device, ...partial };
3870
3879
  }
3871
3880
  /** Mezcla user attributes (se coercionan a string). Mutable en runtime. */
3872
3881
  setUserAttributes(attrs) {
@@ -3875,24 +3884,38 @@ var AnalyticsManager = class {
3875
3884
  this.attributes[k] = String(v);
3876
3885
  }
3877
3886
  }
3878
- /** Marca el inicio de un mini-service; etiqueta los eventos siguientes con `mini_service`. */
3887
+ /**
3888
+ * Marca el inicio de un mini-service; etiqueta los eventos siguientes con `mini_service`.
3889
+ * Admite varios activos a la vez (concurrencia); el "actual" para etiquetar es el más reciente.
3890
+ * Reentrar con un nombre ya activo refresca su orden y su tiempo de entrada.
3891
+ */
3879
3892
  enterMiniService(name, entryPointType) {
3880
- this.miniService = name;
3881
- this.miniServiceEnteredAt = this.now();
3882
- this.track("mini_service_enter", { entry_point_type: entryPointType ?? null });
3883
- }
3884
- /** Cierra el mini-service activo emitiendo `mini_service_exit` con su duración (#27). No-op si no hay ninguno. */
3885
- exitMiniService() {
3886
- const name = this.miniService;
3887
- if (name == null) return;
3888
- const durationSeconds = Math.max(0, Math.round((this.now() - this.miniServiceEnteredAt) / 1e3));
3889
- this.miniService = null;
3890
- this.track("mini_service_exit", { mini_service: name, duration_seconds: durationSeconds });
3893
+ this.activeMiniServices.delete(name);
3894
+ this.activeMiniServices.set(name, this.now());
3895
+ this.track("deepdots_mini_service_enter", { entry_point_type: entryPointType ?? null });
3896
+ }
3897
+ /**
3898
+ * Cierra el mini-service `name` emitiendo `mini_service_exit` con su duración (#27).
3899
+ * No-op si ese nombre no está activo. Cierre coherente con concurrencia.
3900
+ */
3901
+ exitMiniService(name) {
3902
+ const enteredAt = this.activeMiniServices.get(name);
3903
+ if (enteredAt === void 0) return;
3904
+ const durationSeconds = Math.max(0, Math.round((this.now() - enteredAt) / 1e3));
3905
+ this.activeMiniServices.delete(name);
3906
+ this.track("deepdots_mini_service_exit", { mini_service: name, duration_seconds: durationSeconds });
3907
+ }
3908
+ /** Cierra TODOS los mini-services activos (orden LIFO). Para el cierre por lifecycle (background/cierre). */
3909
+ exitAllMiniServices() {
3910
+ for (const name of Array.from(this.activeMiniServices.keys()).reverse()) {
3911
+ this.exitMiniService(name);
3912
+ }
3891
3913
  }
3892
3914
  /** Registra un evento de analítica (modelo GA: nombre + params). */
3893
3915
  track(name, params) {
3916
+ const current = this.getMiniService();
3894
3917
  const merged = {
3895
- ...this.miniService ? { mini_service: this.miniService } : {},
3918
+ ...current ? { mini_service: current } : {},
3896
3919
  ...params ?? {}
3897
3920
  };
3898
3921
  this.events.push({
@@ -3900,10 +3923,15 @@ var AnalyticsManager = class {
3900
3923
  timestamp: this.now(),
3901
3924
  params: Object.keys(merged).length ? merged : void 0
3902
3925
  });
3926
+ if (this.events.length >= this.maxBatchSize) {
3927
+ this.onFlushNeeded?.();
3928
+ }
3903
3929
  }
3904
- /** Mini-service activo (para inyectarlo en la metadata del survey, #33). */
3930
+ /** Mini-service actual (el más reciente aún activo) para etiquetar eventos + metadata del survey (#33). */
3905
3931
  getMiniService() {
3906
- return this.miniService;
3932
+ let last = null;
3933
+ for (const name of this.activeMiniServices.keys()) last = name;
3934
+ return last;
3907
3935
  }
3908
3936
  /** Nº de eventos pendientes de flush. */
3909
3937
  pending() {
@@ -3937,44 +3965,55 @@ var AnalyticsManager = class {
3937
3965
  // src/analytics/feedback-payload.ts
3938
3966
  function pushKV(list, key, value) {
3939
3967
  if (value === void 0 || value === null || value === "") return;
3940
- list.push({ key, value: String(value) });
3968
+ list.push({ key, value: [String(value)] });
3941
3969
  }
3942
3970
  function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
3943
3971
  const { context } = envelope;
3944
- const metrics = envelope.events.map((e) => ({
3945
- key: e.name,
3946
- value: JSON.stringify({ timestamp: e.timestamp, ...e.params ?? {} })
3947
- }));
3948
3972
  const profile = [];
3949
3973
  pushKV(profile, "external-user-id", envelope.userId);
3950
3974
  const metadata = [];
3951
- pushKV(metadata, "user_id", envelope.userId);
3952
- pushKV(metadata, "session_id", envelope.sessionId);
3953
- pushKV(metadata, "platform", context.platform);
3954
- pushKV(metadata, "language", context.language);
3975
+ pushKV(metadata, "deepdots_user_id", envelope.userId);
3976
+ pushKV(metadata, "deepdots_session_id", envelope.sessionId);
3977
+ pushKV(metadata, "deepdots_platform", context.platform);
3978
+ pushKV(metadata, "deepdots_language", context.language);
3955
3979
  if (context.device) {
3956
- pushKV(metadata, "device_type", context.device.device_type);
3957
- pushKV(metadata, "os_version", context.device.os_version);
3958
- pushKV(metadata, "device_model", context.device.device_model);
3959
- pushKV(metadata, "app_version", context.device.app_version);
3960
- pushKV(metadata, "user_agent", context.device.user_agent);
3980
+ const d = context.device;
3981
+ pushKV(metadata, "deepdots_device_type", d.device_type);
3982
+ pushKV(metadata, "deepdots_os_version", d.os_version);
3983
+ pushKV(metadata, "deepdots_device_model", d.device_model);
3984
+ pushKV(metadata, "deepdots_app_version", d.app_version);
3985
+ pushKV(metadata, "deepdots_user_agent", d.user_agent);
3986
+ pushKV(metadata, "deepdots_timezone", d.timezone);
3987
+ pushKV(metadata, "deepdots_referrer", d.referrer);
3988
+ pushKV(metadata, "deepdots_viewport_size", d.viewport_size);
3989
+ pushKV(metadata, "deepdots_screen_resolution", d.screen_resolution);
3990
+ pushKV(metadata, "deepdots_pixel_ratio", d.pixel_ratio);
3991
+ pushKV(metadata, "deepdots_entry_type", d.entry_type);
3992
+ pushKV(metadata, "deepdots_page_load_ms", d.page_load_ms);
3993
+ pushKV(metadata, "deepdots_connection_type", d.connection_type);
3994
+ pushKV(metadata, "deepdots_country", d.country);
3995
+ pushKV(metadata, "deepdots_city", d.city);
3961
3996
  }
3962
3997
  for (const [k, v] of Object.entries(context.attributes ?? {})) {
3963
3998
  pushKV(metadata, k, v);
3964
3999
  }
4000
+ for (const e of envelope.events) {
4001
+ metadata.push({
4002
+ key: e.name,
4003
+ value: [JSON.stringify({ timestamp: e.timestamp, ...e.params ?? {} })]
4004
+ });
4005
+ }
3965
4006
  return {
3966
4007
  feedback: {
3967
4008
  text: "",
3968
4009
  answers: [],
3969
4010
  metadata,
3970
- metrics,
3971
4011
  profile,
3972
4012
  finished: false
3973
4013
  },
3974
4014
  publicKey: keys.publicKey,
3975
4015
  integration: keys.integration,
3976
4016
  completed: false,
3977
- // finished: false,
3978
4017
  ...feedbackSessionId ? { sessionId: feedbackSessionId } : {}
3979
4018
  };
3980
4019
  }
@@ -3999,6 +4038,7 @@ function createFeedbackSink(options) {
3999
4038
  if (data?.sessionId && data.sessionId !== feedbackSessionId) {
4000
4039
  feedbackSessionId = data.sessionId;
4001
4040
  options.log?.("[DeepdotsAnalytics] feedbackSessionId cacheado:", feedbackSessionId);
4041
+ options.onSessionId?.(feedbackSessionId);
4002
4042
  }
4003
4043
  } catch {
4004
4044
  }
@@ -4112,15 +4152,57 @@ function parseDeviceType(ua) {
4112
4152
  }
4113
4153
  return "desktop";
4114
4154
  }
4155
+ function getEntryType() {
4156
+ if (typeof performance === "undefined") return void 0;
4157
+ const entries = performance.getEntriesByType("navigation");
4158
+ if (!entries.length) return void 0;
4159
+ return entries[0].type;
4160
+ }
4161
+ function getPageLoadMs() {
4162
+ if (typeof performance === "undefined") return void 0;
4163
+ const entries = performance.getEntriesByType("navigation");
4164
+ if (!entries.length) return void 0;
4165
+ const load = entries[0].loadEventEnd;
4166
+ return load > 0 ? Math.round(load) : void 0;
4167
+ }
4168
+ function getConnectionType() {
4169
+ if (typeof navigator === "undefined") return void 0;
4170
+ return navigator.connection?.effectiveType ?? void 0;
4171
+ }
4115
4172
  function collectDeviceInfo(appVersion) {
4116
4173
  const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
4117
4174
  return {
4118
4175
  device_type: parseDeviceType(ua),
4119
4176
  user_agent: ua || void 0,
4120
- app_version: appVersion
4177
+ app_version: appVersion,
4178
+ timezone: typeof Intl !== "undefined" ? Intl.DateTimeFormat().resolvedOptions().timeZone : void 0,
4179
+ referrer: typeof document !== "undefined" && document.referrer ? document.referrer : void 0,
4180
+ viewport_size: typeof window !== "undefined" ? `${window.innerWidth}x${window.innerHeight}` : void 0,
4181
+ screen_resolution: typeof screen !== "undefined" ? `${screen.width}x${screen.height}` : void 0,
4182
+ pixel_ratio: typeof window !== "undefined" ? window.devicePixelRatio : void 0,
4183
+ entry_type: getEntryType(),
4184
+ page_load_ms: getPageLoadMs(),
4185
+ connection_type: getConnectionType()
4121
4186
  };
4122
4187
  }
4123
4188
 
4189
+ // src/analytics/geo-info.ts
4190
+ async function collectGeoInfo(fetchImpl) {
4191
+ const f = fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
4192
+ if (!f) return null;
4193
+ try {
4194
+ const res = await f("https://ipapi.co/json/");
4195
+ if (!res.ok) return null;
4196
+ const data = await res.json();
4197
+ const country = data.country_code || void 0;
4198
+ const city = data.city || void 0;
4199
+ if (!country && !city) return null;
4200
+ return { country, city };
4201
+ } catch {
4202
+ return null;
4203
+ }
4204
+ }
4205
+
4124
4206
  // src/analytics/engagement-tracker.ts
4125
4207
  var EngagementTracker = class {
4126
4208
  constructor(now = () => Date.now()) {
@@ -4189,9 +4271,137 @@ var ContactManager = class {
4189
4271
  }
4190
4272
  };
4191
4273
 
4274
+ // src/analytics/crash-reporter.ts
4275
+ var STACK_MAX = 8e3;
4276
+ var QUEUE_KEY = "deepdots.crash.queue";
4277
+ var MAX_QUEUED = 20;
4278
+ function crashRecordToParams(r) {
4279
+ const params = {
4280
+ crashed_at: r.crashedAt,
4281
+ crash_type: r.crashType,
4282
+ message: r.message,
4283
+ stack: r.stack,
4284
+ fatal: r.fatal,
4285
+ handled: r.handled,
4286
+ severity: r.severity
4287
+ };
4288
+ if (r.sessionId) params.crashed_session_id = r.sessionId;
4289
+ if (r.appVersion) params.crashed_app_version = r.appVersion;
4290
+ if (r.osVersion) params.crashed_os_version = r.osVersion;
4291
+ if (r.deviceModel) params.crashed_device_model = r.deviceModel;
4292
+ if (r.context) {
4293
+ for (const [k, v] of Object.entries(r.context)) params[`ctx_${k}`] = v;
4294
+ }
4295
+ return params;
4296
+ }
4297
+ var CrashReporter = class {
4298
+ constructor(options) {
4299
+ this.options = options;
4300
+ this.now = options.now ?? (() => Date.now());
4301
+ }
4302
+ isEnabled() {
4303
+ return this.options.enabled ? this.options.enabled() : true;
4304
+ }
4305
+ /** Construye un CrashRecord capturando contexto en el momento del crash. */
4306
+ buildRecord(error, severity, handled, fatal, context) {
4307
+ const err = error;
4308
+ const isErr = error instanceof Error;
4309
+ const dev = this.options.device();
4310
+ const ctx = context ? Object.fromEntries(Object.entries(context).map(([k, v]) => [k, String(v)])) : void 0;
4311
+ return {
4312
+ crashedAt: this.now(),
4313
+ crashType: isErr && err.name || "Error",
4314
+ message: isErr ? String(err.message ?? "") : String(error),
4315
+ stack: (isErr && typeof err.stack === "string" ? err.stack : "").slice(0, STACK_MAX),
4316
+ fatal,
4317
+ handled,
4318
+ severity,
4319
+ sessionId: this.options.sessionId(),
4320
+ appVersion: dev.appVersion,
4321
+ osVersion: dev.osVersion,
4322
+ deviceModel: dev.deviceModel,
4323
+ context: ctx
4324
+ };
4325
+ }
4326
+ /** API pública del host: reporta un error (app viva) → emite el evento ya. */
4327
+ reportError(error, options = {}) {
4328
+ if (!this.isEnabled()) return;
4329
+ const severity = options.severity ?? "error";
4330
+ const handled = options.handled ?? true;
4331
+ const fatal = severity === "fatal";
4332
+ const record = this.buildRecord(error, severity, handled, fatal, options.context);
4333
+ this.options.emit(crashRecordToParams(record));
4334
+ }
4335
+ /** Captura un error no manejado (fatal) → persiste a disco para replay en el siguiente arranque. */
4336
+ captureUnhandled(error) {
4337
+ this.persist(this.buildRecord(error, "fatal", false, true));
4338
+ }
4339
+ /** Persiste un crash no capturado a disco para reenviarlo en el siguiente arranque. */
4340
+ persist(record) {
4341
+ let queue = [];
4342
+ try {
4343
+ const raw = this.options.storage.getItem(QUEUE_KEY);
4344
+ if (raw) queue = JSON.parse(raw);
4345
+ if (!Array.isArray(queue)) queue = [];
4346
+ } catch {
4347
+ queue = [];
4348
+ }
4349
+ queue.push(record);
4350
+ if (queue.length > MAX_QUEUED) queue = queue.slice(queue.length - MAX_QUEUED);
4351
+ try {
4352
+ this.options.storage.setItem(QUEUE_KEY, JSON.stringify(queue));
4353
+ } catch {
4354
+ }
4355
+ }
4356
+ /** Lee y vacía la cola de crashes pendientes (llamado en init para el replay). */
4357
+ drainPendingCrashes() {
4358
+ let queue = [];
4359
+ try {
4360
+ const raw = this.options.storage.getItem(QUEUE_KEY);
4361
+ if (raw) queue = JSON.parse(raw);
4362
+ if (!Array.isArray(queue)) queue = [];
4363
+ } catch {
4364
+ queue = [];
4365
+ }
4366
+ if (queue.length) {
4367
+ try {
4368
+ this.options.storage.removeItem(QUEUE_KEY);
4369
+ } catch {
4370
+ }
4371
+ }
4372
+ return queue;
4373
+ }
4374
+ /** Instala los handlers globales de errores no capturados (no-op sin `window`). */
4375
+ install() {
4376
+ if (typeof window === "undefined") return;
4377
+ window.addEventListener("error", (e) => {
4378
+ if (!this.isEnabled()) return;
4379
+ this.captureUnhandled(e.error ?? e.message ?? "error");
4380
+ });
4381
+ window.addEventListener("unhandledrejection", (e) => {
4382
+ if (!this.isEnabled()) return;
4383
+ this.captureUnhandled(e.reason ?? "unhandledrejection");
4384
+ });
4385
+ }
4386
+ /** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. */
4387
+ installReactNative(errorUtils) {
4388
+ const previous = errorUtils.getGlobalHandler?.();
4389
+ errorUtils.setGlobalHandler((error, isFatal) => {
4390
+ if (this.isEnabled()) this.captureUnhandled(error);
4391
+ previous?.(error, isFatal);
4392
+ });
4393
+ }
4394
+ /** @internal Test seam: ejercita el camino de persistencia (window handlers no son testeables en vitest). */
4395
+ _persistForTest(error) {
4396
+ this.captureUnhandled(error);
4397
+ }
4398
+ };
4399
+
4192
4400
  // src/core/deepdots-popups.ts
4193
4401
  var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
4194
4402
  var DAY_IN_MS = 24 * 60 * 60 * 1e3;
4403
+ var ANALYTICS_MAX_BATCH_SIZE = 20;
4404
+ var ANALYTICS_FLUSH_INTERVAL_MS = 3e4;
4195
4405
  var DeepdotsPopups = class {
4196
4406
  constructor() {
4197
4407
  this.config = null;
@@ -4217,6 +4427,12 @@ var DeepdotsPopups = class {
4217
4427
  this.tracking = null;
4218
4428
  /** Capa de analytics (canal separado del feedback). Null hasta init(). */
4219
4429
  this.analytics = null;
4430
+ /** feedbackSessionId cacheado del canal de analytics (devuelto por POST /sdk/feedback). */
4431
+ this.analyticsFeedbackSessionId = void 0;
4432
+ /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
4433
+ this.analyticsFlushTimer = void 0;
4434
+ /** Crash & error reporting (#14–17). Null hasta init(). */
4435
+ this.crashReporter = null;
4220
4436
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
4221
4437
  this.navObserver = null;
4222
4438
  /** Tiempo activo (engagement time, #8). */
@@ -4263,21 +4479,49 @@ var DeepdotsPopups = class {
4263
4479
  const analyticsSink = config.analytics ? createFeedbackSink({
4264
4480
  baseUrl: this.baseUrl,
4265
4481
  keys: config.analytics,
4266
- log: (...a) => this.log(...a)
4482
+ log: (...a) => this.log(...a),
4483
+ onSessionId: (id) => {
4484
+ this.analyticsFeedbackSessionId = id;
4485
+ }
4267
4486
  }) : void 0;
4487
+ const device = config.device ?? collectDeviceInfo(config.appVersion);
4268
4488
  this.analytics = new AnalyticsManager({
4269
4489
  sink: analyticsSink,
4270
4490
  publicKey: config.analytics?.publicKey ?? this.config.apiKey,
4271
4491
  language: typeof navigator !== "undefined" ? navigator.language : void 0,
4272
4492
  platform: config.platform ?? "web",
4273
- device: config.device ?? collectDeviceInfo(config.appVersion)
4493
+ device,
4494
+ maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,
4495
+ onFlushNeeded: () => this.flushAnalytics()
4496
+ });
4497
+ this.crashReporter = new CrashReporter({
4498
+ storage,
4499
+ emit: (params) => this.track("deepdots_app_crash", params),
4500
+ device: () => ({ appVersion: device.app_version, osVersion: device.os_version, deviceModel: device.device_model }),
4501
+ sessionId: () => this.tracking?.getSessionId() ?? null,
4502
+ now: () => Date.now(),
4503
+ enabled: () => this.tracking?.isTrackingEnabled() ?? false
4504
+ });
4505
+ if (this.tracking?.isTrackingEnabled()) {
4506
+ this.crashReporter.install();
4507
+ }
4508
+ collectGeoInfo().then((geo) => {
4509
+ if (geo) this.analytics?.updateDevice(geo);
4510
+ }).catch(() => {
4274
4511
  });
4275
4512
  this.navObserver = new NavigationObserver();
4276
- this.navObserver.onVisit((v) => this.track("page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
4513
+ this.navObserver.onVisit((v) => this.track("deepdots_page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
4277
4514
  this.navObserver.install();
4278
4515
  this.engagement = new EngagementTracker();
4279
4516
  this.engagement.resume();
4280
4517
  this.setupAnalyticsFlush();
4518
+ this.track("deepdots_session_start", {});
4519
+ const pendingCrashes = this.crashReporter.drainPendingCrashes();
4520
+ if (this.tracking?.isTrackingEnabled()) {
4521
+ for (const rec of pendingCrashes) {
4522
+ this.track("deepdots_app_crash", crashRecordToParams(rec));
4523
+ }
4524
+ }
4281
4525
  this.initialized = true;
4282
4526
  this.log("SDK initialized", this.config);
4283
4527
  if (this.renderer.init) this.renderer.init();
@@ -4318,6 +4562,16 @@ var DeepdotsPopups = class {
4318
4562
  if (!this.tracking?.isTrackingEnabled()) return;
4319
4563
  this.analytics?.setUserAttributes(attributes);
4320
4564
  }
4565
+ /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
4566
+ reportError(error, options) {
4567
+ if (!this.tracking?.isTrackingEnabled()) return;
4568
+ this.crashReporter?.reportError(error, options);
4569
+ }
4570
+ /** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
4571
+ installReactNativeCrashHandler(errorUtils) {
4572
+ if (!this.tracking?.isTrackingEnabled()) return;
4573
+ this.crashReporter?.installReactNative(errorUtils);
4574
+ }
4321
4575
  /**
4322
4576
  * Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
4323
4577
  * Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
@@ -4335,23 +4589,23 @@ var DeepdotsPopups = class {
4335
4589
  this.analytics?.enterMiniService(name, entryPointType);
4336
4590
  this.log("analytics \xB7 enterMiniService:", name, entryPointType ?? "");
4337
4591
  }
4338
- /** Cierra el mini-service activo (emite `mini_service_exit` con duración, #27). No-op si tracking off. */
4339
- exitMiniService() {
4592
+ /** Cierra el mini-service `name` (emite `mini_service_exit` con duración, #27). No-op si tracking off o si ese no está activo. */
4593
+ exitMiniService(name) {
4340
4594
  if (!this.tracking?.isTrackingEnabled()) return;
4341
- this.analytics?.exitMiniService();
4342
- this.log("analytics \xB7 exitMiniService");
4595
+ this.analytics?.exitMiniService(name);
4596
+ this.log("analytics \xB7 exitMiniService:", name);
4343
4597
  }
4344
4598
  /** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */
4345
4599
  trackSearch(query, resultsCount, params) {
4346
- this.track("search", { query, results_count: resultsCount, has_results: resultsCount > 0, ...params ?? {} });
4600
+ this.track("deepdots_search", { query, results_count: resultsCount, has_results: resultsCount > 0, ...params ?? {} });
4347
4601
  }
4348
4602
  /** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */
4349
4603
  trackFindabilityFriction(frictionTopic, params) {
4350
- this.track("findability_friction", { friction_topic: frictionTopic, ...params ?? {} });
4604
+ this.track("deepdots_findability_friction", { friction_topic: frictionTopic, ...params ?? {} });
4351
4605
  }
4352
4606
  /** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
4353
4607
  trackFunnelStep(funnel, step, taskId, params) {
4354
- this.track("funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
4608
+ this.track("deepdots_funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
4355
4609
  }
4356
4610
  /**
4357
4611
  * Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
@@ -4377,7 +4631,7 @@ var DeepdotsPopups = class {
4377
4631
  onBackground() {
4378
4632
  this.navObserver?.stop();
4379
4633
  this.navStarted = false;
4380
- this.exitMiniService();
4634
+ if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
4381
4635
  this.flushEngagement();
4382
4636
  this.engagement?.pause();
4383
4637
  this.flushAnalytics();
@@ -4400,7 +4654,7 @@ var DeepdotsPopups = class {
4400
4654
  flushEngagement() {
4401
4655
  if (!this.tracking?.isTrackingEnabled()) return;
4402
4656
  const ms = this.engagement?.consume() ?? 0;
4403
- if (ms > 0) this.track("user_engagement", { engagement_time_msec: ms });
4657
+ if (ms > 0) this.track("deepdots_user_engagement", { engagement_time_msec: ms });
4404
4658
  }
4405
4659
  analyticsIdentity() {
4406
4660
  return {
@@ -4411,9 +4665,11 @@ var DeepdotsPopups = class {
4411
4665
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
4412
4666
  setupAnalyticsFlush() {
4413
4667
  if (typeof document === "undefined" || typeof window === "undefined") return;
4668
+ this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
4414
4669
  window.addEventListener("pagehide", () => {
4670
+ clearInterval(this.analyticsFlushTimer);
4415
4671
  this.navObserver?.stop();
4416
- this.exitMiniService();
4672
+ if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
4417
4673
  this.flushEngagement();
4418
4674
  this.flushAnalytics();
4419
4675
  });
@@ -4855,7 +5111,8 @@ var DeepdotsPopups = class {
4855
5111
  const userId = this.tracking?.getUserId() ?? this.config?.userId;
4856
5112
  const sessionId = this.tracking?.getSessionId() ?? void 0;
4857
5113
  const miniService = this.analytics?.getMiniService() ?? void 0;
4858
- this.log("tracking \xB7 survey identity \u2192", { userId: userId ?? null, sessionId: sessionId ?? null, miniService: miniService ?? null });
5114
+ const analyticsFeedbackSessionId = this.analyticsFeedbackSessionId;
5115
+ this.log("tracking \xB7 survey identity \u2192", { userId: userId ?? null, sessionId: sessionId ?? null, miniService: miniService ?? null, analyticsFeedbackSessionId: analyticsFeedbackSessionId ?? null });
4859
5116
  if (this.renderer) {
4860
5117
  this.renderer.show(
4861
5118
  surveyId,
@@ -4867,7 +5124,8 @@ var DeepdotsPopups = class {
4867
5124
  userId,
4868
5125
  style,
4869
5126
  sessionId,
4870
- miniService
5127
+ miniService,
5128
+ analyticsFeedbackSessionId
4871
5129
  );
4872
5130
  return;
4873
5131
  }
@@ -4885,7 +5143,8 @@ var DeepdotsPopups = class {
4885
5143
  userId,
4886
5144
  style,
4887
5145
  sessionId,
4888
- miniService
5146
+ miniService,
5147
+ analyticsFeedbackSessionId
4889
5148
  );
4890
5149
  });
4891
5150
  }
@@ -5079,6 +5338,10 @@ function setupReactNative(sdk, config, deps = {}) {
5079
5338
  const device = deps.deviceInfo ? collectRnDevice(deps.deviceInfo) : config.device;
5080
5339
  const platform = deps.platform ?? config.platform;
5081
5340
  sdk.init({ ...config, storage, device, platform });
5341
+ const errorUtils = deps.errorUtils ?? globalThis.ErrorUtils;
5342
+ if (errorUtils && typeof errorUtils.setGlobalHandler === "function") {
5343
+ sdk.installReactNativeCrashHandler(errorUtils);
5344
+ }
5082
5345
  let subscription;
5083
5346
  if (deps.appState?.addEventListener) {
5084
5347
  subscription = deps.appState.addEventListener("change", (state) => {