@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.
@@ -61,12 +61,13 @@ var STORAGE_KEYS = {
61
61
  firstSeen: "deepdots.user.first_seen",
62
62
  contactAttributes: "deepdots.contact.attributes"
63
63
  };
64
- function buildSurveyIdentity(userId, sessionId, miniService = null) {
64
+ function buildSurveyIdentity(userId, sessionId, miniService = null, analyticsFeedbackSessionId = null) {
65
65
  const profile = userId ? [{ key: "external-user-id", value: [userId] }] : [];
66
66
  const metadata = [];
67
67
  if (sessionId) metadata.push({ key: "session_id", value: [sessionId] });
68
68
  if (userId) metadata.push({ key: "user_id", value: [userId] });
69
69
  if (miniService) metadata.push({ key: "mini_service", value: [miniService] });
70
+ if (analyticsFeedbackSessionId) metadata.push({ key: "deepdots_analytics_feedback_session_id", value: [analyticsFeedbackSessionId] });
70
71
  return { profile, metadata };
71
72
  }
72
73
  function defaultUuid() {
@@ -137,4 +138,4 @@ export {
137
138
  buildSurveyIdentity,
138
139
  TrackingManager
139
140
  };
140
- //# sourceMappingURL=chunk-5VVCJBND.mjs.map
141
+ //# sourceMappingURL=chunk-6WTOQGET.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tracking/tracking-manager.ts"],"sourcesContent":["/**\n * Identidad + sesión para el tracking de navegación (Fase 1).\n * Implementa el \"Contrato común Web+KMP\".\n *\n * Modelo: el SDK gestiona el `user_id` (persistente). El `session_id` es propiedad\n * del BACKEND: llega en la respuesta de `POST /sdk/popups` y se cachea; el backend\n * cose sesiones por `user_id` + ventana. El SDK NO genera ni expira sesiones.\n *\n * Sin dependencias de DOM: recibe el storage y (opcionalmente) el reloj y el\n * generador de uuid por inyección. El binding a `localStorage` vive en `createDefaultStorage`.\n */\n\n/** Store clave/valor mínimo (espejo conceptual del `KeyValueStorage` de KMP). */\nexport interface KeyValueStorage {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\n/** Impl en memoria — usada en tests y como fallback sin `localStorage`. */\nexport class InMemoryStorage implements KeyValueStorage {\n private map = new Map<string, string>();\n getItem(key: string): string | null {\n return this.map.has(key) ? (this.map.get(key) as string) : null;\n }\n setItem(key: string, value: string): void {\n this.map.set(key, value);\n }\n removeItem(key: string): void {\n this.map.delete(key);\n }\n}\n\n/** Storage por defecto en Web: `localStorage` si está disponible, si no in-memory. */\nexport function createDefaultStorage(): KeyValueStorage {\n try {\n if (typeof window !== 'undefined' && window.localStorage) {\n const ls = window.localStorage;\n const probe = '__deepdots_probe__';\n ls.setItem(probe, '1');\n ls.removeItem(probe);\n return {\n getItem: (k) => ls.getItem(k),\n setItem: (k, v) => ls.setItem(k, v),\n removeItem: (k) => ls.removeItem(k),\n };\n }\n } catch {\n // cae a in-memory\n }\n return new InMemoryStorage();\n}\n\n/** Claves de storage — namespace `deepdots.*`. Solo identidad (la sesión no se persiste). */\nexport const STORAGE_KEYS = {\n userId: 'deepdots.user_id',\n firstSeen: 'deepdots.user.first_seen',\n contactAttributes: 'deepdots.contact.attributes',\n} as const;\n\n/** Par clave/valor con el formato `NativeAnswer` que espera `@magicfeedback/native`. */\nexport interface IdentityAnswer {\n key: string;\n value: string[];\n}\n\n/**\n * Construye el `profile` y el `metadata` de identidad para inyectar en el survey\n * (`magicfeedback.form(appId, publicKey, profile, metadata)`), según el contrato §5.\n * `session_id` solo se incluye si el backend ya lo proveyó.\n */\nexport function buildSurveyIdentity(\n userId: string | null,\n sessionId: string | null,\n miniService: string | null = null,\n analyticsFeedbackSessionId: string | null = null,\n): { profile: IdentityAnswer[]; metadata: IdentityAnswer[] } {\n const profile: IdentityAnswer[] = userId ? [{ key: 'external-user-id', value: [userId] }] : [];\n const metadata: IdentityAnswer[] = [];\n if (sessionId) metadata.push({ key: 'session_id', value: [sessionId] });\n if (userId) metadata.push({ key: 'user_id', value: [userId] });\n if (miniService) metadata.push({ key: 'mini_service', value: [miniService] });\n if (analyticsFeedbackSessionId) metadata.push({ key: 'deepdots_analytics_feedback_session_id', value: [analyticsFeedbackSessionId] });\n return { profile, metadata };\n}\n\nexport interface TrackingManagerOptions {\n storage: KeyValueStorage;\n /** Id provisto por el cliente; si existe se usa y NO se persiste. */\n clientUserId?: string;\n /** Reloj inyectable (default `Date.now`). */\n now?: () => number;\n /** Generador de uuid inyectable (default uuid v4). */\n uuid?: () => string;\n /** Estado inicial del tracking (default `true`). */\n enabled?: boolean;\n}\n\nfunction defaultUuid(): string {\n const g = (typeof globalThis !== 'undefined' ? globalThis : {}) as {\n crypto?: { randomUUID?: () => string; getRandomValues?: (a: Uint8Array) => Uint8Array };\n };\n if (g.crypto?.randomUUID) return g.crypto.randomUUID();\n const bytes = new Uint8Array(16);\n if (g.crypto?.getRandomValues) {\n g.crypto.getRandomValues(bytes);\n } else {\n for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0'));\n return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex\n .slice(6, 8)\n .join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`;\n}\n\nexport class TrackingManager {\n private storage: KeyValueStorage;\n private clientUserId?: string;\n private now: () => number;\n private uuid: () => string;\n private enabled: boolean;\n\n /** session_id cacheado, provisto por el backend (no se genera ni se persiste). */\n private sessionId: string | null = null;\n private userWasJustCreated = false;\n\n constructor(options: TrackingManagerOptions) {\n this.storage = options.storage;\n this.clientUserId = options.clientUserId;\n this.now = options.now ?? (() => Date.now());\n this.uuid = options.uuid ?? defaultUuid;\n this.enabled = options.enabled ?? true;\n }\n\n isTrackingEnabled(): boolean {\n return this.enabled;\n }\n\n setTrackingEnabled(enabled: boolean): void {\n this.enabled = enabled;\n }\n\n /** Resuelve el user_id según la regla: cliente > persistido > generar+persistir. */\n getUserId(): string | null {\n if (!this.enabled) return null;\n if (this.clientUserId) return this.clientUserId;\n\n const persisted = this.storage.getItem(STORAGE_KEYS.userId);\n if (persisted) return persisted;\n\n const id = this.uuid();\n this.storage.setItem(STORAGE_KEYS.userId, id);\n this.storage.setItem(STORAGE_KEYS.firstSeen, String(this.now()));\n this.userWasJustCreated = true;\n return id;\n }\n\n /** `true` solo en la primera sesión tras crear el user_id propio del SDK. */\n isNewUser(): boolean {\n return this.userWasJustCreated;\n }\n\n /** session_id actual provisto por el backend, o null si aún no llegó. */\n getSessionId(): string | null {\n if (!this.enabled) return null;\n return this.sessionId;\n }\n\n /** Cachea el session_id devuelto por el backend (respuesta de POST /sdk/popups). */\n setSessionId(sessionId: string | null): void {\n if (!this.enabled) return;\n this.sessionId = sessionId;\n }\n}\n"],"mappings":"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","names":[]}
@@ -42,12 +42,13 @@ var STORAGE_KEYS = {
42
42
  firstSeen: "deepdots.user.first_seen",
43
43
  contactAttributes: "deepdots.contact.attributes"
44
44
  };
45
- function buildSurveyIdentity(userId, sessionId, miniService = null) {
45
+ function buildSurveyIdentity(userId, sessionId, miniService = null, analyticsFeedbackSessionId = null) {
46
46
  const profile = userId ? [{ key: "external-user-id", value: [userId] }] : [];
47
47
  const metadata = [];
48
48
  if (sessionId) metadata.push({ key: "session_id", value: [sessionId] });
49
49
  if (userId) metadata.push({ key: "user_id", value: [userId] });
50
50
  if (miniService) metadata.push({ key: "mini_service", value: [miniService] });
51
+ if (analyticsFeedbackSessionId) metadata.push({ key: "deepdots_analytics_feedback_session_id", value: [analyticsFeedbackSessionId] });
51
52
  return { profile, metadata };
52
53
  }
53
54
  function defaultUuid() {
@@ -116,4 +117,4 @@ export {
116
117
  buildSurveyIdentity,
117
118
  TrackingManager
118
119
  };
119
- //# sourceMappingURL=chunk-M6PEFLLQ.mjs.map
120
+ //# sourceMappingURL=chunk-IOAZQPZE.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tracking/tracking-manager.ts"],"sourcesContent":["/**\n * Identidad + sesión para el tracking de navegación (Fase 1).\n * Implementa el \"Contrato común Web+KMP\".\n *\n * Modelo: el SDK gestiona el `user_id` (persistente). El `session_id` es propiedad\n * del BACKEND: llega en la respuesta de `POST /sdk/popups` y se cachea; el backend\n * cose sesiones por `user_id` + ventana. El SDK NO genera ni expira sesiones.\n *\n * Sin dependencias de DOM: recibe el storage y (opcionalmente) el reloj y el\n * generador de uuid por inyección. El binding a `localStorage` vive en `createDefaultStorage`.\n */\n\n/** Store clave/valor mínimo (espejo conceptual del `KeyValueStorage` de KMP). */\nexport interface KeyValueStorage {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\n/** Impl en memoria — usada en tests y como fallback sin `localStorage`. */\nexport class InMemoryStorage implements KeyValueStorage {\n private map = new Map<string, string>();\n getItem(key: string): string | null {\n return this.map.has(key) ? (this.map.get(key) as string) : null;\n }\n setItem(key: string, value: string): void {\n this.map.set(key, value);\n }\n removeItem(key: string): void {\n this.map.delete(key);\n }\n}\n\n/** Storage por defecto en Web: `localStorage` si está disponible, si no in-memory. */\nexport function createDefaultStorage(): KeyValueStorage {\n try {\n if (typeof window !== 'undefined' && window.localStorage) {\n const ls = window.localStorage;\n const probe = '__deepdots_probe__';\n ls.setItem(probe, '1');\n ls.removeItem(probe);\n return {\n getItem: (k) => ls.getItem(k),\n setItem: (k, v) => ls.setItem(k, v),\n removeItem: (k) => ls.removeItem(k),\n };\n }\n } catch {\n // cae a in-memory\n }\n return new InMemoryStorage();\n}\n\n/** Claves de storage — namespace `deepdots.*`. Solo identidad (la sesión no se persiste). */\nexport const STORAGE_KEYS = {\n userId: 'deepdots.user_id',\n firstSeen: 'deepdots.user.first_seen',\n contactAttributes: 'deepdots.contact.attributes',\n} as const;\n\n/** Par clave/valor con el formato `NativeAnswer` que espera `@magicfeedback/native`. */\nexport interface IdentityAnswer {\n key: string;\n value: string[];\n}\n\n/**\n * Construye el `profile` y el `metadata` de identidad para inyectar en el survey\n * (`magicfeedback.form(appId, publicKey, profile, metadata)`), según el contrato §5.\n * `session_id` solo se incluye si el backend ya lo proveyó.\n */\nexport function buildSurveyIdentity(\n userId: string | null,\n sessionId: string | null,\n miniService: string | null = null,\n analyticsFeedbackSessionId: string | null = null,\n): { profile: IdentityAnswer[]; metadata: IdentityAnswer[] } {\n const profile: IdentityAnswer[] = userId ? [{ key: 'external-user-id', value: [userId] }] : [];\n const metadata: IdentityAnswer[] = [];\n if (sessionId) metadata.push({ key: 'session_id', value: [sessionId] });\n if (userId) metadata.push({ key: 'user_id', value: [userId] });\n if (miniService) metadata.push({ key: 'mini_service', value: [miniService] });\n if (analyticsFeedbackSessionId) metadata.push({ key: 'deepdots_analytics_feedback_session_id', value: [analyticsFeedbackSessionId] });\n return { profile, metadata };\n}\n\nexport interface TrackingManagerOptions {\n storage: KeyValueStorage;\n /** Id provisto por el cliente; si existe se usa y NO se persiste. */\n clientUserId?: string;\n /** Reloj inyectable (default `Date.now`). */\n now?: () => number;\n /** Generador de uuid inyectable (default uuid v4). */\n uuid?: () => string;\n /** Estado inicial del tracking (default `true`). */\n enabled?: boolean;\n}\n\nfunction defaultUuid(): string {\n const g = (typeof globalThis !== 'undefined' ? globalThis : {}) as {\n crypto?: { randomUUID?: () => string; getRandomValues?: (a: Uint8Array) => Uint8Array };\n };\n if (g.crypto?.randomUUID) return g.crypto.randomUUID();\n const bytes = new Uint8Array(16);\n if (g.crypto?.getRandomValues) {\n g.crypto.getRandomValues(bytes);\n } else {\n for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0'));\n return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex\n .slice(6, 8)\n .join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`;\n}\n\nexport class TrackingManager {\n private storage: KeyValueStorage;\n private clientUserId?: string;\n private now: () => number;\n private uuid: () => string;\n private enabled: boolean;\n\n /** session_id cacheado, provisto por el backend (no se genera ni se persiste). */\n private sessionId: string | null = null;\n private userWasJustCreated = false;\n\n constructor(options: TrackingManagerOptions) {\n this.storage = options.storage;\n this.clientUserId = options.clientUserId;\n this.now = options.now ?? (() => Date.now());\n this.uuid = options.uuid ?? defaultUuid;\n this.enabled = options.enabled ?? true;\n }\n\n isTrackingEnabled(): boolean {\n return this.enabled;\n }\n\n setTrackingEnabled(enabled: boolean): void {\n this.enabled = enabled;\n }\n\n /** Resuelve el user_id según la regla: cliente > persistido > generar+persistir. */\n getUserId(): string | null {\n if (!this.enabled) return null;\n if (this.clientUserId) return this.clientUserId;\n\n const persisted = this.storage.getItem(STORAGE_KEYS.userId);\n if (persisted) return persisted;\n\n const id = this.uuid();\n this.storage.setItem(STORAGE_KEYS.userId, id);\n this.storage.setItem(STORAGE_KEYS.firstSeen, String(this.now()));\n this.userWasJustCreated = true;\n return id;\n }\n\n /** `true` solo en la primera sesión tras crear el user_id propio del SDK. */\n isNewUser(): boolean {\n return this.userWasJustCreated;\n }\n\n /** session_id actual provisto por el backend, o null si aún no llegó. */\n getSessionId(): string | null {\n if (!this.enabled) return null;\n return this.sessionId;\n }\n\n /** Cachea el session_id devuelto por el backend (respuesta de POST /sdk/popups). */\n setSessionId(sessionId: string | null): void {\n if (!this.enabled) return;\n this.sessionId = sessionId;\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.mts CHANGED
@@ -31,10 +31,11 @@ interface IdentityAnswer {
31
31
  /**
32
32
  * Device info para el context de analytics (Technology #11–13).
33
33
  *
34
- * En Web enviamos `device_type` (heurística fiable) + `user_agent` crudo, y dejamos
35
- * que el backend derive OS/modelo/navegador del UA (como hace GA) — el parseo de UA en
36
- * cliente es frágil. `app_version` lo provee el cliente (una web no tiene versión de app
37
- * nativa). En KMP las APIs nativas dan os_version/device_model reales.
34
+ * En Web enviamos `device_type` + `user_agent` crudo y dejamos que el backend
35
+ * derive OS/navegador del UA (como hace GA). Los campos adicionales (timezone,
36
+ * referrer, viewport, etc.) se recogen de las APIs del navegador sin permisos.
37
+ * `country`/`city` se rellenan de forma asíncrona (ver geo-info.ts).
38
+ * En KMP las APIs nativas dan os_version/device_model reales.
38
39
  */
39
40
  interface DeviceInfo {
40
41
  device_type: 'mobile' | 'tablet' | 'desktop';
@@ -42,6 +43,16 @@ interface DeviceInfo {
42
43
  device_model?: string;
43
44
  app_version?: string;
44
45
  user_agent?: string;
46
+ timezone?: string;
47
+ referrer?: string;
48
+ viewport_size?: string;
49
+ screen_resolution?: string;
50
+ pixel_ratio?: number;
51
+ entry_type?: string;
52
+ page_load_ms?: number;
53
+ connection_type?: string;
54
+ country?: string;
55
+ city?: string;
45
56
  }
46
57
 
47
58
  /**
@@ -87,11 +98,12 @@ type AnalyticsSink = (payload: AnalyticsEnvelope) => void;
87
98
  * INTEGRACIÓN creada en la plataforma. Se manda en streaming con `completed:false` y
88
99
  * `finished:false` (nunca se "cierra"); el backend cose por `sessionId` + `user_id`.
89
100
  *
90
- * Encoding (decisión 2026-06-22):
91
- * - cada evento (dato recabado) → una `metric` {key: nombre_evento, value: JSON(timestamp + params)}
101
+ * Encoding:
102
+ * - todo va en `feedback.metadata`: contexto (user_id, session_id, platform…) + eventos
103
+ * - cada entrada usa `value: string[]` (array de un elemento)
104
+ * - eventos: {key: nombre_evento, value: [JSON(timestamp + params)]}
92
105
  * - identidad (user_id) → `profile` como `external-user-id`
93
- * - contexto (user_id, session_id, platform, language, device, attributes) → `metadata`
94
- * - `answers` vacío
106
+ * - `answers` vacío, `metrics` no se usa
95
107
  * - `text` vacío
96
108
  */
97
109
 
@@ -101,15 +113,14 @@ interface AnalyticsKeys {
101
113
  }
102
114
  interface FeedbackKV {
103
115
  key: string;
104
- value: string;
116
+ value: string[];
105
117
  }
106
118
  interface AnalyticsFeedbackBody {
107
119
  feedback: {
108
120
  text: string;
109
- answers?: FeedbackKV[];
121
+ answers: FeedbackKV[];
110
122
  metadata: FeedbackKV[];
111
- metrics: FeedbackKV[];
112
- profile?: FeedbackKV[];
123
+ profile: FeedbackKV[];
113
124
  finished: boolean;
114
125
  };
115
126
  publicKey: string;
@@ -128,6 +139,8 @@ interface FeedbackSinkOptions {
128
139
  log?: (...args: unknown[]) => void;
129
140
  /** fetch inyectable (tests / RN). */
130
141
  fetchImpl?: typeof fetch;
142
+ /** Callback invocado cuando el backend devuelve un nuevo sessionId (primer POST). */
143
+ onSessionId?: (id: string) => void;
131
144
  }
132
145
  /**
133
146
  * Sink real: transforma el envelope y hace `POST {baseUrl}/sdk/feedback`.
@@ -285,7 +298,7 @@ interface PopupRenderer {
285
298
  /** Preparar recursos si aplica */
286
299
  init?(): void;
287
300
  /** Mostrar popup */
288
- show(surveyId: string, productId: string, actions: PopupActions | undefined, emit: (type: DeepdotsEventType, surveyId: string, data?: Record<string, unknown>) => void, onClose: () => void, env?: string, userId?: string, style?: PopupStyle, sessionId?: string, miniService?: string): void;
301
+ show(surveyId: string, productId: string, actions: PopupActions | undefined, emit: (type: DeepdotsEventType, surveyId: string, data?: Record<string, unknown>) => void, onClose: () => void, env?: string, userId?: string, style?: PopupStyle, sessionId?: string, miniService?: string, analyticsFeedbackSessionId?: string): void;
289
302
  /** Ocultar popup */
290
303
  hide(): void;
291
304
  }
@@ -299,7 +312,7 @@ declare class BrowserPopupRenderer implements PopupRenderer {
299
312
  private container;
300
313
  private visible;
301
314
  init(): void;
302
- show(surveyId: string, productId: string, actions: PopupActions | undefined, emit: (type: DeepdotsEventType, surveyId: string, data?: Record<string, unknown>) => void, onClose: () => void, env?: string, userId?: string, style?: PopupStyle, sessionId?: string, miniService?: string): void;
315
+ show(surveyId: string, productId: string, actions: PopupActions | undefined, emit: (type: DeepdotsEventType, surveyId: string, data?: Record<string, unknown>) => void, onClose: () => void, env?: string, userId?: string, style?: PopupStyle, sessionId?: string, miniService?: string, analyticsFeedbackSessionId?: string): void;
303
316
  hide(): void;
304
317
  }
305
318
  /** Factoría para obtener renderer por defecto */
@@ -339,6 +352,85 @@ declare class ContactManager {
339
352
  setAttributes(attributes: ContactAttributes): Promise<boolean>;
340
353
  }
341
354
 
355
+ /**
356
+ * Crash & error reporting (Stability #14–17) — captura de errores GESTIONADOS.
357
+ *
358
+ * - `reportError()`: API pública para el host (app viva) → emite `deepdots_app_crash` ya.
359
+ * - handlers de `window` (errores no capturados): persisten a disco y se reenvían
360
+ * en el siguiente arranque (el proceso puede morir antes del flush normal).
361
+ *
362
+ * El contexto de device/sesión se captura EN EL MOMENTO del crash y se guarda en el
363
+ * record — no se lee del envelope en el replay (el replay puede ocurrir en otra
364
+ * app_version/OS). Las dims OS/device en Web suelen ir vacías (se derivan del UA en backend).
365
+ */
366
+
367
+ type CrashSeverity = 'fatal' | 'error' | 'warning';
368
+ interface CrashRecord {
369
+ crashedAt: number;
370
+ crashType: string;
371
+ message: string;
372
+ stack: string;
373
+ fatal: boolean;
374
+ handled: boolean;
375
+ severity: CrashSeverity;
376
+ sessionId: string | null;
377
+ appVersion?: string;
378
+ osVersion?: string;
379
+ deviceModel?: string;
380
+ context?: Record<string, string>;
381
+ }
382
+ interface ReportErrorOptions {
383
+ severity?: CrashSeverity;
384
+ handled?: boolean;
385
+ context?: Record<string, unknown>;
386
+ }
387
+ interface DeviceSnapshot {
388
+ appVersion?: string;
389
+ osVersion?: string;
390
+ deviceModel?: string;
391
+ }
392
+ /** Forma mínima de `global.ErrorUtils` de React Native. */
393
+ interface ReactNativeErrorUtils {
394
+ getGlobalHandler?(): ((error: unknown, isFatal?: boolean) => void) | undefined;
395
+ setGlobalHandler(handler: (error: unknown, isFatal?: boolean) => void): void;
396
+ }
397
+ interface CrashReporterOptions {
398
+ storage: KeyValueStorage;
399
+ /** Emite un evento deepdots_app_crash AHORA (app viva). */
400
+ emit: (params: Record<string, unknown>) => void;
401
+ /** Snapshot de device en el momento del crash. */
402
+ device: () => DeviceSnapshot;
403
+ /** session_id en el momento del crash. */
404
+ sessionId: () => string | null;
405
+ now?: () => number;
406
+ /** Kill-switch de consentimiento. */
407
+ enabled?: () => boolean;
408
+ }
409
+ /** Convierte un CrashRecord en los params del evento `deepdots_app_crash` (omite undefined). */
410
+ declare function crashRecordToParams(r: CrashRecord): Record<string, unknown>;
411
+ declare class CrashReporter {
412
+ private options;
413
+ private now;
414
+ constructor(options: CrashReporterOptions);
415
+ private isEnabled;
416
+ /** Construye un CrashRecord capturando contexto en el momento del crash. */
417
+ private buildRecord;
418
+ /** API pública del host: reporta un error (app viva) → emite el evento ya. */
419
+ reportError(error: unknown, options?: ReportErrorOptions): void;
420
+ /** Captura un error no manejado (fatal) → persiste a disco para replay en el siguiente arranque. */
421
+ captureUnhandled(error: unknown): void;
422
+ /** Persiste un crash no capturado a disco para reenviarlo en el siguiente arranque. */
423
+ private persist;
424
+ /** Lee y vacía la cola de crashes pendientes (llamado en init para el replay). */
425
+ drainPendingCrashes(): CrashRecord[];
426
+ /** Instala los handlers globales de errores no capturados (no-op sin `window`). */
427
+ install(): void;
428
+ /** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. */
429
+ installReactNative(errorUtils: ReactNativeErrorUtils): void;
430
+ /** @internal Test seam: ejercita el camino de persistencia (window handlers no son testeables en vitest). */
431
+ _persistForTest(error: unknown): void;
432
+ }
433
+
342
434
  /**
343
435
  * Main class for managing survey popups
344
436
  */
@@ -363,6 +455,12 @@ declare class DeepdotsPopups {
363
455
  private tracking;
364
456
  /** Capa de analytics (canal separado del feedback). Null hasta init(). */
365
457
  private analytics;
458
+ /** feedbackSessionId cacheado del canal de analytics (devuelto por POST /sdk/feedback). */
459
+ private analyticsFeedbackSessionId;
460
+ /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
461
+ private analyticsFlushTimer;
462
+ /** Crash & error reporting (#14–17). Null hasta init(). */
463
+ private crashReporter;
366
464
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
367
465
  private navObserver;
368
466
  /** Tiempo activo (engagement time, #8). */
@@ -383,6 +481,10 @@ declare class DeepdotsPopups {
383
481
  track(name: string, params?: Record<string, unknown>): void;
384
482
  /** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
385
483
  setUserAttributes(attributes: Record<string, string | number | boolean>): void;
484
+ /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
485
+ reportError(error: unknown, options?: ReportErrorOptions): void;
486
+ /** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
487
+ installReactNativeCrashHandler(errorUtils: ReactNativeErrorUtils): void;
386
488
  /**
387
489
  * Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
388
490
  * Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
@@ -392,8 +494,8 @@ declare class DeepdotsPopups {
392
494
  setContactAttributes(attributes: ContactAttributes): Promise<boolean>;
393
495
  /** Marca el inicio de un mini-service; etiqueta los eventos siguientes. No-op si tracking off. */
394
496
  enterMiniService(name: string, entryPointType?: string): void;
395
- /** Cierra el mini-service activo (emite `mini_service_exit` con duración, #27). No-op si tracking off. */
396
- exitMiniService(): void;
497
+ /** Cierra el mini-service `name` (emite `mini_service_exit` con duración, #27). No-op si tracking off o si ese no está activo. */
498
+ exitMiniService(name: string): void;
397
499
  /** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */
398
500
  trackSearch(query: string, resultsCount: number, params?: Record<string, unknown>): void;
399
501
  /** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */
@@ -530,7 +632,7 @@ declare class ReactNativePopupRenderer implements PopupRenderer {
530
632
  private partialEmitted;
531
633
  constructor(options?: ReactNativeRendererOptions);
532
634
  init(): void;
533
- show(surveyId: string, productId: string, _actions: PopupActions | undefined, emit: EmitFn, onClose: () => void, env?: string, userId?: string, _style?: PopupStyle, sessionId?: string, miniService?: string): void;
635
+ show(surveyId: string, productId: string, _actions: PopupActions | undefined, emit: EmitFn, onClose: () => void, env?: string, userId?: string, _style?: PopupStyle, sessionId?: string, miniService?: string, analyticsFeedbackSessionId?: string): void;
534
636
  hide(): void;
535
637
  /**
536
638
  * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
@@ -593,6 +695,8 @@ interface ReactNativeSetupDeps {
593
695
  appState?: AppStateLike | null;
594
696
  platform?: 'web' | 'android' | 'ios';
595
697
  renderer?: PopupRenderer;
698
+ /** `global.ErrorUtils` de RN (para capturar errores JS no manejados). Default: `globalThis.ErrorUtils`. */
699
+ errorUtils?: ReactNativeErrorUtils | null;
596
700
  }
597
701
  /** Adaptador KeyValueStorage (síncrono) sobre una instancia de MMKV. */
598
702
  declare function mmkvStorage(mmkv: MmkvLike): KeyValueStorage;
@@ -605,4 +709,4 @@ declare function collectRnDevice(d: DeviceInfoLike): DeviceInfo;
605
709
  */
606
710
  declare function setupReactNative(sdk: DeepdotsPopups, config: DeepdotsInitParams, deps?: ReactNativeSetupDeps): () => void;
607
711
 
608
- export { type AnalyticsFeedbackBody, type AnalyticsKeys, type AppStateLike, BrowserPopupRenderer, type ContactAttributeValue, type ContactAttributes, type ContactBody, ContactManager, type DeepdotsConfig, type DeepdotsEvent, type DeepdotsEventType, DeepdotsPopups, type DeviceInfo, type DeviceInfoLike, type EventListener, type FeedbackKV, InMemoryStorage, type KeyValueStorage, type MmkvLike, NoopPopupRenderer, POPUP_TRIGGER_CONDITION_STATUSES, type PopupDefinition, type PopupRenderer, type PopupTrigger, type PopupTriggerCondition, type PopupTriggerConditionStatus, ReactNativePopupRenderer, type ReactNativeRendererOptions, type ReactNativeSetupDeps, type ReactNativeSurveyPayload, type TriggerConfig, buildAnalyticsFeedbackBody, buildSurveyHtml, collectRnDevice, createDefaultRenderer, createFeedbackSink, mmkvStorage, setupReactNative };
712
+ export { type AnalyticsFeedbackBody, type AnalyticsKeys, type AppStateLike, BrowserPopupRenderer, type ContactAttributeValue, type ContactAttributes, type ContactBody, ContactManager, type CrashRecord, CrashReporter, type CrashSeverity, type DeepdotsConfig, type DeepdotsEvent, type DeepdotsEventType, DeepdotsPopups, type DeviceInfo, type DeviceInfoLike, type DeviceSnapshot, type EventListener, type FeedbackKV, InMemoryStorage, type KeyValueStorage, type MmkvLike, NoopPopupRenderer, POPUP_TRIGGER_CONDITION_STATUSES, type PopupDefinition, type PopupRenderer, type PopupTrigger, type PopupTriggerCondition, type PopupTriggerConditionStatus, type ReactNativeErrorUtils, ReactNativePopupRenderer, type ReactNativeRendererOptions, type ReactNativeSetupDeps, type ReactNativeSurveyPayload, type ReportErrorOptions, type TriggerConfig, buildAnalyticsFeedbackBody, buildSurveyHtml, collectRnDevice, crashRecordToParams, createDefaultRenderer, createFeedbackSink, mmkvStorage, setupReactNative };
package/dist/index.d.ts CHANGED
@@ -31,10 +31,11 @@ interface IdentityAnswer {
31
31
  /**
32
32
  * Device info para el context de analytics (Technology #11–13).
33
33
  *
34
- * En Web enviamos `device_type` (heurística fiable) + `user_agent` crudo, y dejamos
35
- * que el backend derive OS/modelo/navegador del UA (como hace GA) — el parseo de UA en
36
- * cliente es frágil. `app_version` lo provee el cliente (una web no tiene versión de app
37
- * nativa). En KMP las APIs nativas dan os_version/device_model reales.
34
+ * En Web enviamos `device_type` + `user_agent` crudo y dejamos que el backend
35
+ * derive OS/navegador del UA (como hace GA). Los campos adicionales (timezone,
36
+ * referrer, viewport, etc.) se recogen de las APIs del navegador sin permisos.
37
+ * `country`/`city` se rellenan de forma asíncrona (ver geo-info.ts).
38
+ * En KMP las APIs nativas dan os_version/device_model reales.
38
39
  */
39
40
  interface DeviceInfo {
40
41
  device_type: 'mobile' | 'tablet' | 'desktop';
@@ -42,6 +43,16 @@ interface DeviceInfo {
42
43
  device_model?: string;
43
44
  app_version?: string;
44
45
  user_agent?: string;
46
+ timezone?: string;
47
+ referrer?: string;
48
+ viewport_size?: string;
49
+ screen_resolution?: string;
50
+ pixel_ratio?: number;
51
+ entry_type?: string;
52
+ page_load_ms?: number;
53
+ connection_type?: string;
54
+ country?: string;
55
+ city?: string;
45
56
  }
46
57
 
47
58
  /**
@@ -87,11 +98,12 @@ type AnalyticsSink = (payload: AnalyticsEnvelope) => void;
87
98
  * INTEGRACIÓN creada en la plataforma. Se manda en streaming con `completed:false` y
88
99
  * `finished:false` (nunca se "cierra"); el backend cose por `sessionId` + `user_id`.
89
100
  *
90
- * Encoding (decisión 2026-06-22):
91
- * - cada evento (dato recabado) → una `metric` {key: nombre_evento, value: JSON(timestamp + params)}
101
+ * Encoding:
102
+ * - todo va en `feedback.metadata`: contexto (user_id, session_id, platform…) + eventos
103
+ * - cada entrada usa `value: string[]` (array de un elemento)
104
+ * - eventos: {key: nombre_evento, value: [JSON(timestamp + params)]}
92
105
  * - identidad (user_id) → `profile` como `external-user-id`
93
- * - contexto (user_id, session_id, platform, language, device, attributes) → `metadata`
94
- * - `answers` vacío
106
+ * - `answers` vacío, `metrics` no se usa
95
107
  * - `text` vacío
96
108
  */
97
109
 
@@ -101,15 +113,14 @@ interface AnalyticsKeys {
101
113
  }
102
114
  interface FeedbackKV {
103
115
  key: string;
104
- value: string;
116
+ value: string[];
105
117
  }
106
118
  interface AnalyticsFeedbackBody {
107
119
  feedback: {
108
120
  text: string;
109
- answers?: FeedbackKV[];
121
+ answers: FeedbackKV[];
110
122
  metadata: FeedbackKV[];
111
- metrics: FeedbackKV[];
112
- profile?: FeedbackKV[];
123
+ profile: FeedbackKV[];
113
124
  finished: boolean;
114
125
  };
115
126
  publicKey: string;
@@ -128,6 +139,8 @@ interface FeedbackSinkOptions {
128
139
  log?: (...args: unknown[]) => void;
129
140
  /** fetch inyectable (tests / RN). */
130
141
  fetchImpl?: typeof fetch;
142
+ /** Callback invocado cuando el backend devuelve un nuevo sessionId (primer POST). */
143
+ onSessionId?: (id: string) => void;
131
144
  }
132
145
  /**
133
146
  * Sink real: transforma el envelope y hace `POST {baseUrl}/sdk/feedback`.
@@ -285,7 +298,7 @@ interface PopupRenderer {
285
298
  /** Preparar recursos si aplica */
286
299
  init?(): void;
287
300
  /** Mostrar popup */
288
- show(surveyId: string, productId: string, actions: PopupActions | undefined, emit: (type: DeepdotsEventType, surveyId: string, data?: Record<string, unknown>) => void, onClose: () => void, env?: string, userId?: string, style?: PopupStyle, sessionId?: string, miniService?: string): void;
301
+ show(surveyId: string, productId: string, actions: PopupActions | undefined, emit: (type: DeepdotsEventType, surveyId: string, data?: Record<string, unknown>) => void, onClose: () => void, env?: string, userId?: string, style?: PopupStyle, sessionId?: string, miniService?: string, analyticsFeedbackSessionId?: string): void;
289
302
  /** Ocultar popup */
290
303
  hide(): void;
291
304
  }
@@ -299,7 +312,7 @@ declare class BrowserPopupRenderer implements PopupRenderer {
299
312
  private container;
300
313
  private visible;
301
314
  init(): void;
302
- show(surveyId: string, productId: string, actions: PopupActions | undefined, emit: (type: DeepdotsEventType, surveyId: string, data?: Record<string, unknown>) => void, onClose: () => void, env?: string, userId?: string, style?: PopupStyle, sessionId?: string, miniService?: string): void;
315
+ show(surveyId: string, productId: string, actions: PopupActions | undefined, emit: (type: DeepdotsEventType, surveyId: string, data?: Record<string, unknown>) => void, onClose: () => void, env?: string, userId?: string, style?: PopupStyle, sessionId?: string, miniService?: string, analyticsFeedbackSessionId?: string): void;
303
316
  hide(): void;
304
317
  }
305
318
  /** Factoría para obtener renderer por defecto */
@@ -339,6 +352,85 @@ declare class ContactManager {
339
352
  setAttributes(attributes: ContactAttributes): Promise<boolean>;
340
353
  }
341
354
 
355
+ /**
356
+ * Crash & error reporting (Stability #14–17) — captura de errores GESTIONADOS.
357
+ *
358
+ * - `reportError()`: API pública para el host (app viva) → emite `deepdots_app_crash` ya.
359
+ * - handlers de `window` (errores no capturados): persisten a disco y se reenvían
360
+ * en el siguiente arranque (el proceso puede morir antes del flush normal).
361
+ *
362
+ * El contexto de device/sesión se captura EN EL MOMENTO del crash y se guarda en el
363
+ * record — no se lee del envelope en el replay (el replay puede ocurrir en otra
364
+ * app_version/OS). Las dims OS/device en Web suelen ir vacías (se derivan del UA en backend).
365
+ */
366
+
367
+ type CrashSeverity = 'fatal' | 'error' | 'warning';
368
+ interface CrashRecord {
369
+ crashedAt: number;
370
+ crashType: string;
371
+ message: string;
372
+ stack: string;
373
+ fatal: boolean;
374
+ handled: boolean;
375
+ severity: CrashSeverity;
376
+ sessionId: string | null;
377
+ appVersion?: string;
378
+ osVersion?: string;
379
+ deviceModel?: string;
380
+ context?: Record<string, string>;
381
+ }
382
+ interface ReportErrorOptions {
383
+ severity?: CrashSeverity;
384
+ handled?: boolean;
385
+ context?: Record<string, unknown>;
386
+ }
387
+ interface DeviceSnapshot {
388
+ appVersion?: string;
389
+ osVersion?: string;
390
+ deviceModel?: string;
391
+ }
392
+ /** Forma mínima de `global.ErrorUtils` de React Native. */
393
+ interface ReactNativeErrorUtils {
394
+ getGlobalHandler?(): ((error: unknown, isFatal?: boolean) => void) | undefined;
395
+ setGlobalHandler(handler: (error: unknown, isFatal?: boolean) => void): void;
396
+ }
397
+ interface CrashReporterOptions {
398
+ storage: KeyValueStorage;
399
+ /** Emite un evento deepdots_app_crash AHORA (app viva). */
400
+ emit: (params: Record<string, unknown>) => void;
401
+ /** Snapshot de device en el momento del crash. */
402
+ device: () => DeviceSnapshot;
403
+ /** session_id en el momento del crash. */
404
+ sessionId: () => string | null;
405
+ now?: () => number;
406
+ /** Kill-switch de consentimiento. */
407
+ enabled?: () => boolean;
408
+ }
409
+ /** Convierte un CrashRecord en los params del evento `deepdots_app_crash` (omite undefined). */
410
+ declare function crashRecordToParams(r: CrashRecord): Record<string, unknown>;
411
+ declare class CrashReporter {
412
+ private options;
413
+ private now;
414
+ constructor(options: CrashReporterOptions);
415
+ private isEnabled;
416
+ /** Construye un CrashRecord capturando contexto en el momento del crash. */
417
+ private buildRecord;
418
+ /** API pública del host: reporta un error (app viva) → emite el evento ya. */
419
+ reportError(error: unknown, options?: ReportErrorOptions): void;
420
+ /** Captura un error no manejado (fatal) → persiste a disco para replay en el siguiente arranque. */
421
+ captureUnhandled(error: unknown): void;
422
+ /** Persiste un crash no capturado a disco para reenviarlo en el siguiente arranque. */
423
+ private persist;
424
+ /** Lee y vacía la cola de crashes pendientes (llamado en init para el replay). */
425
+ drainPendingCrashes(): CrashRecord[];
426
+ /** Instala los handlers globales de errores no capturados (no-op sin `window`). */
427
+ install(): void;
428
+ /** Engancha `global.ErrorUtils` de RN (no hay `window`): persiste y encadena al handler previo. */
429
+ installReactNative(errorUtils: ReactNativeErrorUtils): void;
430
+ /** @internal Test seam: ejercita el camino de persistencia (window handlers no son testeables en vitest). */
431
+ _persistForTest(error: unknown): void;
432
+ }
433
+
342
434
  /**
343
435
  * Main class for managing survey popups
344
436
  */
@@ -363,6 +455,12 @@ declare class DeepdotsPopups {
363
455
  private tracking;
364
456
  /** Capa de analytics (canal separado del feedback). Null hasta init(). */
365
457
  private analytics;
458
+ /** feedbackSessionId cacheado del canal de analytics (devuelto por POST /sdk/feedback). */
459
+ private analyticsFeedbackSessionId;
460
+ /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
461
+ private analyticsFlushTimer;
462
+ /** Crash & error reporting (#14–17). Null hasta init(). */
463
+ private crashReporter;
366
464
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
367
465
  private navObserver;
368
466
  /** Tiempo activo (engagement time, #8). */
@@ -383,6 +481,10 @@ declare class DeepdotsPopups {
383
481
  track(name: string, params?: Record<string, unknown>): void;
384
482
  /** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
385
483
  setUserAttributes(attributes: Record<string, string | number | boolean>): void;
484
+ /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
485
+ reportError(error: unknown, options?: ReportErrorOptions): void;
486
+ /** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
487
+ installReactNativeCrashHandler(errorUtils: ReactNativeErrorUtils): void;
386
488
  /**
387
489
  * Info interna del usuario que solo conoce el host (idioma, edad, plan…), persistida en el
388
490
  * Contact del backend para segmentación/targeting de popups. Identificada por el `userId` del
@@ -392,8 +494,8 @@ declare class DeepdotsPopups {
392
494
  setContactAttributes(attributes: ContactAttributes): Promise<boolean>;
393
495
  /** Marca el inicio de un mini-service; etiqueta los eventos siguientes. No-op si tracking off. */
394
496
  enterMiniService(name: string, entryPointType?: string): void;
395
- /** Cierra el mini-service activo (emite `mini_service_exit` con duración, #27). No-op si tracking off. */
396
- exitMiniService(): void;
497
+ /** Cierra el mini-service `name` (emite `mini_service_exit` con duración, #27). No-op si tracking off o si ese no está activo. */
498
+ exitMiniService(name: string): void;
397
499
  /** Findability (#31/#35): registra una búsqueda. `has_results` se deriva de `resultsCount`. */
398
500
  trackSearch(query: string, resultsCount: number, params?: Record<string, unknown>): void;
399
501
  /** Findability friction (#34/#35): señal de fricción con su `friction_topic`. */
@@ -530,7 +632,7 @@ declare class ReactNativePopupRenderer implements PopupRenderer {
530
632
  private partialEmitted;
531
633
  constructor(options?: ReactNativeRendererOptions);
532
634
  init(): void;
533
- show(surveyId: string, productId: string, _actions: PopupActions | undefined, emit: EmitFn, onClose: () => void, env?: string, userId?: string, _style?: PopupStyle, sessionId?: string, miniService?: string): void;
635
+ show(surveyId: string, productId: string, _actions: PopupActions | undefined, emit: EmitFn, onClose: () => void, env?: string, userId?: string, _style?: PopupStyle, sessionId?: string, miniService?: string, analyticsFeedbackSessionId?: string): void;
534
636
  hide(): void;
535
637
  /**
536
638
  * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
@@ -593,6 +695,8 @@ interface ReactNativeSetupDeps {
593
695
  appState?: AppStateLike | null;
594
696
  platform?: 'web' | 'android' | 'ios';
595
697
  renderer?: PopupRenderer;
698
+ /** `global.ErrorUtils` de RN (para capturar errores JS no manejados). Default: `globalThis.ErrorUtils`. */
699
+ errorUtils?: ReactNativeErrorUtils | null;
596
700
  }
597
701
  /** Adaptador KeyValueStorage (síncrono) sobre una instancia de MMKV. */
598
702
  declare function mmkvStorage(mmkv: MmkvLike): KeyValueStorage;
@@ -605,4 +709,4 @@ declare function collectRnDevice(d: DeviceInfoLike): DeviceInfo;
605
709
  */
606
710
  declare function setupReactNative(sdk: DeepdotsPopups, config: DeepdotsInitParams, deps?: ReactNativeSetupDeps): () => void;
607
711
 
608
- export { type AnalyticsFeedbackBody, type AnalyticsKeys, type AppStateLike, BrowserPopupRenderer, type ContactAttributeValue, type ContactAttributes, type ContactBody, ContactManager, type DeepdotsConfig, type DeepdotsEvent, type DeepdotsEventType, DeepdotsPopups, type DeviceInfo, type DeviceInfoLike, type EventListener, type FeedbackKV, InMemoryStorage, type KeyValueStorage, type MmkvLike, NoopPopupRenderer, POPUP_TRIGGER_CONDITION_STATUSES, type PopupDefinition, type PopupRenderer, type PopupTrigger, type PopupTriggerCondition, type PopupTriggerConditionStatus, ReactNativePopupRenderer, type ReactNativeRendererOptions, type ReactNativeSetupDeps, type ReactNativeSurveyPayload, type TriggerConfig, buildAnalyticsFeedbackBody, buildSurveyHtml, collectRnDevice, createDefaultRenderer, createFeedbackSink, mmkvStorage, setupReactNative };
712
+ export { type AnalyticsFeedbackBody, type AnalyticsKeys, type AppStateLike, BrowserPopupRenderer, type ContactAttributeValue, type ContactAttributes, type ContactBody, ContactManager, type CrashRecord, CrashReporter, type CrashSeverity, type DeepdotsConfig, type DeepdotsEvent, type DeepdotsEventType, DeepdotsPopups, type DeviceInfo, type DeviceInfoLike, type DeviceSnapshot, type EventListener, type FeedbackKV, InMemoryStorage, type KeyValueStorage, type MmkvLike, NoopPopupRenderer, POPUP_TRIGGER_CONDITION_STATUSES, type PopupDefinition, type PopupRenderer, type PopupTrigger, type PopupTriggerCondition, type PopupTriggerConditionStatus, type ReactNativeErrorUtils, ReactNativePopupRenderer, type ReactNativeRendererOptions, type ReactNativeSetupDeps, type ReactNativeSurveyPayload, type ReportErrorOptions, type TriggerConfig, buildAnalyticsFeedbackBody, buildSurveyHtml, collectRnDevice, crashRecordToParams, createDefaultRenderer, createFeedbackSink, mmkvStorage, setupReactNative };