@magicfeedback/popup-sdk 1.1.1 → 1.1.2

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 */
@@ -363,6 +376,10 @@ declare class DeepdotsPopups {
363
376
  private tracking;
364
377
  /** Capa de analytics (canal separado del feedback). Null hasta init(). */
365
378
  private analytics;
379
+ /** feedbackSessionId cacheado del canal de analytics (devuelto por POST /sdk/feedback). */
380
+ private analyticsFeedbackSessionId;
381
+ /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
382
+ private analyticsFlushTimer;
366
383
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
367
384
  private navObserver;
368
385
  /** Tiempo activo (engagement time, #8). */
@@ -530,7 +547,7 @@ declare class ReactNativePopupRenderer implements PopupRenderer {
530
547
  private partialEmitted;
531
548
  constructor(options?: ReactNativeRendererOptions);
532
549
  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;
550
+ 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
551
  hide(): void;
535
552
  /**
536
553
  * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
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 */
@@ -363,6 +376,10 @@ declare class DeepdotsPopups {
363
376
  private tracking;
364
377
  /** Capa de analytics (canal separado del feedback). Null hasta init(). */
365
378
  private analytics;
379
+ /** feedbackSessionId cacheado del canal de analytics (devuelto por POST /sdk/feedback). */
380
+ private analyticsFeedbackSessionId;
381
+ /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
382
+ private analyticsFlushTimer;
366
383
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
367
384
  private navObserver;
368
385
  /** Tiempo activo (engagement time, #8). */
@@ -530,7 +547,7 @@ declare class ReactNativePopupRenderer implements PopupRenderer {
530
547
  private partialEmitted;
531
548
  constructor(options?: ReactNativeRendererOptions);
532
549
  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;
550
+ 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
551
  hide(): void;
535
552
  /**
536
553
  * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
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,
@@ -3722,12 +3723,12 @@ var ReactNativePopupRenderer = class {
3722
3723
  }
3723
3724
  init() {
3724
3725
  }
3725
- show(surveyId, productId, _actions, emit, onClose, env = "production", userId, _style, sessionId, miniService) {
3726
+ show(surveyId, productId, _actions, emit, onClose, env = "production", userId, _style, sessionId, miniService, analyticsFeedbackSessionId) {
3726
3727
  this.emitFn = emit;
3727
3728
  this.onCloseFn = onClose;
3728
3729
  this.currentSurveyId = surveyId;
3729
3730
  this.partialEmitted = false;
3730
- const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null);
3731
+ const { profile, metadata } = buildSurveyIdentity(userId ?? null, sessionId ?? null, miniService ?? null, analyticsFeedbackSessionId ?? null);
3731
3732
  const html = buildSurveyHtml({ surveyId, productId, env, profile, metadata });
3732
3733
  if (this.options.onShow) {
3733
3734
  this.options.onShow({ surveyId, productId, html });
@@ -3818,14 +3819,14 @@ var BrowserPopupRenderer = class {
3818
3819
  document.body.appendChild(this.container);
3819
3820
  }
3820
3821
  }
3821
- show(surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService) {
3822
+ show(surveyId, productId, actions, emit, onClose, env = "production", userId, style, sessionId, miniService, analyticsFeedbackSessionId) {
3822
3823
  if (this.visible) return;
3823
3824
  if (!this.container || !document.body.contains(this.container)) this.init();
3824
3825
  if (!this.container) return;
3825
3826
  this.visible = true;
3826
3827
  const container = this.container;
3827
3828
  void Promise.resolve().then(() => (init_renderPopup(), renderPopup_exports)).then(({ renderPopup: renderPopup2 }) => {
3828
- renderPopup2(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService);
3829
+ renderPopup2(container, surveyId, productId, actions, emit, onClose, env, userId, style, sessionId, miniService, analyticsFeedbackSessionId);
3829
3830
  });
3830
3831
  }
3831
3832
  hide() {
@@ -3867,6 +3868,12 @@ var AnalyticsManager = class {
3867
3868
  this.language = options.language;
3868
3869
  this.device = options.device;
3869
3870
  this.platform = options.platform ?? "web";
3871
+ this.maxBatchSize = options.maxBatchSize ?? 20;
3872
+ this.onFlushNeeded = options.onFlushNeeded;
3873
+ }
3874
+ /** Actualiza campos de device en runtime (p.ej. country/city tras resolver geo). */
3875
+ updateDevice(partial) {
3876
+ this.device = { ...this.device, ...partial };
3870
3877
  }
3871
3878
  /** Mezcla user attributes (se coercionan a string). Mutable en runtime. */
3872
3879
  setUserAttributes(attrs) {
@@ -3900,6 +3907,9 @@ var AnalyticsManager = class {
3900
3907
  timestamp: this.now(),
3901
3908
  params: Object.keys(merged).length ? merged : void 0
3902
3909
  });
3910
+ if (this.events.length >= this.maxBatchSize) {
3911
+ this.onFlushNeeded?.();
3912
+ }
3903
3913
  }
3904
3914
  /** Mini-service activo (para inyectarlo en la metadata del survey, #33). */
3905
3915
  getMiniService() {
@@ -3937,44 +3947,55 @@ var AnalyticsManager = class {
3937
3947
  // src/analytics/feedback-payload.ts
3938
3948
  function pushKV(list, key, value) {
3939
3949
  if (value === void 0 || value === null || value === "") return;
3940
- list.push({ key, value: String(value) });
3950
+ list.push({ key, value: [String(value)] });
3941
3951
  }
3942
3952
  function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
3943
3953
  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
3954
  const profile = [];
3949
3955
  pushKV(profile, "external-user-id", envelope.userId);
3950
3956
  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);
3957
+ pushKV(metadata, "deepdots_user_id", envelope.userId);
3958
+ pushKV(metadata, "deepdots_session_id", envelope.sessionId);
3959
+ pushKV(metadata, "deepdots_platform", context.platform);
3960
+ pushKV(metadata, "deepdots_language", context.language);
3955
3961
  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);
3962
+ const d = context.device;
3963
+ pushKV(metadata, "deepdots_device_type", d.device_type);
3964
+ pushKV(metadata, "deepdots_os_version", d.os_version);
3965
+ pushKV(metadata, "deepdots_device_model", d.device_model);
3966
+ pushKV(metadata, "deepdots_app_version", d.app_version);
3967
+ pushKV(metadata, "deepdots_user_agent", d.user_agent);
3968
+ pushKV(metadata, "deepdots_timezone", d.timezone);
3969
+ pushKV(metadata, "deepdots_referrer", d.referrer);
3970
+ pushKV(metadata, "deepdots_viewport_size", d.viewport_size);
3971
+ pushKV(metadata, "deepdots_screen_resolution", d.screen_resolution);
3972
+ pushKV(metadata, "deepdots_pixel_ratio", d.pixel_ratio);
3973
+ pushKV(metadata, "deepdots_entry_type", d.entry_type);
3974
+ pushKV(metadata, "deepdots_page_load_ms", d.page_load_ms);
3975
+ pushKV(metadata, "deepdots_connection_type", d.connection_type);
3976
+ pushKV(metadata, "deepdots_country", d.country);
3977
+ pushKV(metadata, "deepdots_city", d.city);
3961
3978
  }
3962
3979
  for (const [k, v] of Object.entries(context.attributes ?? {})) {
3963
3980
  pushKV(metadata, k, v);
3964
3981
  }
3982
+ for (const e of envelope.events) {
3983
+ metadata.push({
3984
+ key: e.name,
3985
+ value: [JSON.stringify({ timestamp: e.timestamp, ...e.params ?? {} })]
3986
+ });
3987
+ }
3965
3988
  return {
3966
3989
  feedback: {
3967
3990
  text: "",
3968
3991
  answers: [],
3969
3992
  metadata,
3970
- metrics,
3971
3993
  profile,
3972
3994
  finished: false
3973
3995
  },
3974
3996
  publicKey: keys.publicKey,
3975
3997
  integration: keys.integration,
3976
3998
  completed: false,
3977
- // finished: false,
3978
3999
  ...feedbackSessionId ? { sessionId: feedbackSessionId } : {}
3979
4000
  };
3980
4001
  }
@@ -3999,6 +4020,7 @@ function createFeedbackSink(options) {
3999
4020
  if (data?.sessionId && data.sessionId !== feedbackSessionId) {
4000
4021
  feedbackSessionId = data.sessionId;
4001
4022
  options.log?.("[DeepdotsAnalytics] feedbackSessionId cacheado:", feedbackSessionId);
4023
+ options.onSessionId?.(feedbackSessionId);
4002
4024
  }
4003
4025
  } catch {
4004
4026
  }
@@ -4112,15 +4134,57 @@ function parseDeviceType(ua) {
4112
4134
  }
4113
4135
  return "desktop";
4114
4136
  }
4137
+ function getEntryType() {
4138
+ if (typeof performance === "undefined") return void 0;
4139
+ const entries = performance.getEntriesByType("navigation");
4140
+ if (!entries.length) return void 0;
4141
+ return entries[0].type;
4142
+ }
4143
+ function getPageLoadMs() {
4144
+ if (typeof performance === "undefined") return void 0;
4145
+ const entries = performance.getEntriesByType("navigation");
4146
+ if (!entries.length) return void 0;
4147
+ const load = entries[0].loadEventEnd;
4148
+ return load > 0 ? Math.round(load) : void 0;
4149
+ }
4150
+ function getConnectionType() {
4151
+ if (typeof navigator === "undefined") return void 0;
4152
+ return navigator.connection?.effectiveType ?? void 0;
4153
+ }
4115
4154
  function collectDeviceInfo(appVersion) {
4116
4155
  const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
4117
4156
  return {
4118
4157
  device_type: parseDeviceType(ua),
4119
4158
  user_agent: ua || void 0,
4120
- app_version: appVersion
4159
+ app_version: appVersion,
4160
+ timezone: typeof Intl !== "undefined" ? Intl.DateTimeFormat().resolvedOptions().timeZone : void 0,
4161
+ referrer: typeof document !== "undefined" && document.referrer ? document.referrer : void 0,
4162
+ viewport_size: typeof window !== "undefined" ? `${window.innerWidth}x${window.innerHeight}` : void 0,
4163
+ screen_resolution: typeof screen !== "undefined" ? `${screen.width}x${screen.height}` : void 0,
4164
+ pixel_ratio: typeof window !== "undefined" ? window.devicePixelRatio : void 0,
4165
+ entry_type: getEntryType(),
4166
+ page_load_ms: getPageLoadMs(),
4167
+ connection_type: getConnectionType()
4121
4168
  };
4122
4169
  }
4123
4170
 
4171
+ // src/analytics/geo-info.ts
4172
+ async function collectGeoInfo(fetchImpl) {
4173
+ const f = fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
4174
+ if (!f) return null;
4175
+ try {
4176
+ const res = await f("https://ipapi.co/json/");
4177
+ if (!res.ok) return null;
4178
+ const data = await res.json();
4179
+ const country = data.country_code || void 0;
4180
+ const city = data.city || void 0;
4181
+ if (!country && !city) return null;
4182
+ return { country, city };
4183
+ } catch {
4184
+ return null;
4185
+ }
4186
+ }
4187
+
4124
4188
  // src/analytics/engagement-tracker.ts
4125
4189
  var EngagementTracker = class {
4126
4190
  constructor(now = () => Date.now()) {
@@ -4192,6 +4256,8 @@ var ContactManager = class {
4192
4256
  // src/core/deepdots-popups.ts
4193
4257
  var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
4194
4258
  var DAY_IN_MS = 24 * 60 * 60 * 1e3;
4259
+ var ANALYTICS_MAX_BATCH_SIZE = 20;
4260
+ var ANALYTICS_FLUSH_INTERVAL_MS = 3e4;
4195
4261
  var DeepdotsPopups = class {
4196
4262
  constructor() {
4197
4263
  this.config = null;
@@ -4217,6 +4283,10 @@ var DeepdotsPopups = class {
4217
4283
  this.tracking = null;
4218
4284
  /** Capa de analytics (canal separado del feedback). Null hasta init(). */
4219
4285
  this.analytics = null;
4286
+ /** feedbackSessionId cacheado del canal de analytics (devuelto por POST /sdk/feedback). */
4287
+ this.analyticsFeedbackSessionId = void 0;
4288
+ /** Timer del flush periódico de analytics (cada ANALYTICS_FLUSH_INTERVAL_MS). */
4289
+ this.analyticsFlushTimer = void 0;
4220
4290
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
4221
4291
  this.navObserver = null;
4222
4292
  /** Tiempo activo (engagement time, #8). */
@@ -4263,14 +4333,23 @@ var DeepdotsPopups = class {
4263
4333
  const analyticsSink = config.analytics ? createFeedbackSink({
4264
4334
  baseUrl: this.baseUrl,
4265
4335
  keys: config.analytics,
4266
- log: (...a) => this.log(...a)
4336
+ log: (...a) => this.log(...a),
4337
+ onSessionId: (id) => {
4338
+ this.analyticsFeedbackSessionId = id;
4339
+ }
4267
4340
  }) : void 0;
4268
4341
  this.analytics = new AnalyticsManager({
4269
4342
  sink: analyticsSink,
4270
4343
  publicKey: config.analytics?.publicKey ?? this.config.apiKey,
4271
4344
  language: typeof navigator !== "undefined" ? navigator.language : void 0,
4272
4345
  platform: config.platform ?? "web",
4273
- device: config.device ?? collectDeviceInfo(config.appVersion)
4346
+ device: config.device ?? collectDeviceInfo(config.appVersion),
4347
+ maxBatchSize: ANALYTICS_MAX_BATCH_SIZE,
4348
+ onFlushNeeded: () => this.flushAnalytics()
4349
+ });
4350
+ collectGeoInfo().then((geo) => {
4351
+ if (geo) this.analytics?.updateDevice(geo);
4352
+ }).catch(() => {
4274
4353
  });
4275
4354
  this.navObserver = new NavigationObserver();
4276
4355
  this.navObserver.onVisit((v) => this.track("page_view", { screen: v.screen, duration_seconds: v.durationSeconds }));
@@ -4411,7 +4490,9 @@ var DeepdotsPopups = class {
4411
4490
  /** Flush automático al ocultar/cerrar la página (no perder el lote pendiente). */
4412
4491
  setupAnalyticsFlush() {
4413
4492
  if (typeof document === "undefined" || typeof window === "undefined") return;
4493
+ this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
4414
4494
  window.addEventListener("pagehide", () => {
4495
+ clearInterval(this.analyticsFlushTimer);
4415
4496
  this.navObserver?.stop();
4416
4497
  this.exitMiniService();
4417
4498
  this.flushEngagement();
@@ -4855,7 +4936,8 @@ var DeepdotsPopups = class {
4855
4936
  const userId = this.tracking?.getUserId() ?? this.config?.userId;
4856
4937
  const sessionId = this.tracking?.getSessionId() ?? void 0;
4857
4938
  const miniService = this.analytics?.getMiniService() ?? void 0;
4858
- this.log("tracking \xB7 survey identity \u2192", { userId: userId ?? null, sessionId: sessionId ?? null, miniService: miniService ?? null });
4939
+ const analyticsFeedbackSessionId = this.analyticsFeedbackSessionId;
4940
+ this.log("tracking \xB7 survey identity \u2192", { userId: userId ?? null, sessionId: sessionId ?? null, miniService: miniService ?? null, analyticsFeedbackSessionId: analyticsFeedbackSessionId ?? null });
4859
4941
  if (this.renderer) {
4860
4942
  this.renderer.show(
4861
4943
  surveyId,
@@ -4867,7 +4949,8 @@ var DeepdotsPopups = class {
4867
4949
  userId,
4868
4950
  style,
4869
4951
  sessionId,
4870
- miniService
4952
+ miniService,
4953
+ analyticsFeedbackSessionId
4871
4954
  );
4872
4955
  return;
4873
4956
  }
@@ -4885,7 +4968,8 @@ var DeepdotsPopups = class {
4885
4968
  userId,
4886
4969
  style,
4887
4970
  sessionId,
4888
- miniService
4971
+ miniService,
4972
+ analyticsFeedbackSessionId
4889
4973
  );
4890
4974
  });
4891
4975
  }