@magicfeedback/popup-sdk 1.1.7 → 1.2.0

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.
@@ -86,8 +86,11 @@ interface AnalyticsEnvelope {
86
86
  * Mapeo del envelope de analytics → body de `POST /sdk/feedback`.
87
87
  *
88
88
  * La analítica se envía como un Feedback del modelo del Surveys SDK, agrupado por una
89
- * INTEGRACIÓN creada en la plataforma. Se manda en streaming con `completed:false` y
90
- * `finished:false` (nunca se "cierra"); el backend cose por `sessionId` + `user_id`.
89
+ * INTEGRACIÓN creada en la plataforma. Se manda en streaming con `completed:false`; el
90
+ * backend cose por `sessionId` + `user_id`. El ÚLTIMO lote de una sesión (cierre de página,
91
+ * app a background, cambio de usuario) va con `completed:true`: cierra el registro y el
92
+ * siguiente lote omite el `sessionId` viejo para que el backend abra uno nuevo.
93
+ * `feedback.finished` se queda siempre en `false` (no es la señal de cierre acordada).
91
94
  *
92
95
  * Encoding:
93
96
  * - todo va en `feedback.metadata`: contexto (user_id, session_id, platform…) + eventos
@@ -139,9 +142,11 @@ interface DeepdotsInitParams {
139
142
  /** Platform used in the analytics envelope. Default is 'web'; in RN pass 'android'/'ios'. */
140
143
  platform?: 'web' | 'android' | 'ios';
141
144
  /**
142
- * BCP-47 language tag for the analytics context (e.g. 'es-ES'). If omitted, the SDK
143
- * auto-detects: `navigator.language` on web, or the `Intl` locale as a fallback (works in
144
- * React Native with Hermes, where `navigator.language` is absent).
145
+ * BCP-47 language tag (e.g. 'es-ES') used for the analytics context AND for popup
146
+ * language segmentation (`segments.lang`). If omitted, the SDK auto-detects:
147
+ * `navigator.language` on web, or the `Intl` locale as a fallback (works in
148
+ * React Native with Hermes, where `navigator.language` is absent). If no source
149
+ * resolves a language, popups with `segments.lang` cannot be filtered and are shown.
145
150
  */
146
151
  language?: string;
147
152
  /** Injectable device info (RN: from react-native-device-info). If missing, it is derived from the browser. */
@@ -277,13 +282,16 @@ interface ReactNativeErrorUtils {
277
282
  * el funnel se correlaciona por `message_id` y se agrupa por `message_title`.
278
283
  */
279
284
  type MessageStage = 'delivered' | 'clicked' | 'converted';
285
+ /** Canales válidos. Un mensaje tiene UN canal: se valida en runtime porque el host puede no ser TS. */
286
+ declare const MESSAGE_CHANNELS: readonly ["push", "in_app"];
287
+ type MessageChannel = (typeof MESSAGE_CHANNELS)[number];
280
288
  interface TrackMessageOptions {
281
289
  /** Identificador del mensaje/notificación — correlaciona el funnel delivered→clicked→converted. */
282
290
  id: string;
283
291
  /** Título del mensaje — dimensión de agrupación de #18–22. */
284
292
  title: string;
285
293
  /** Canal de entrega. */
286
- channel: 'push' | 'in_app';
294
+ channel: MessageChannel;
287
295
  /** Nombre de la campaña (opcional). */
288
296
  campaign?: string;
289
297
  /** Valor de conversión (opcional, típico en `stage: 'converted'`). */
@@ -325,14 +333,29 @@ declare class DeepdotsPopups {
325
333
  private analyticsFlushTimer;
326
334
  /** Crash & error reporting (#14–17). Null hasta init(). */
327
335
  private crashReporter;
336
+ /** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */
337
+ private messageGuard;
328
338
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
329
339
  private navObserver;
330
340
  /** Tiempo activo (engagement time, #8). */
331
341
  private engagement;
332
342
  /** Atributos internos del usuario identificado → POST /sdk/popups/contact. Null si no hay userId. */
333
343
  private contact;
344
+ /** Storage resuelto en init() (inyectado por el host o localStorage). Reusado al cambiar de usuario. */
345
+ private storage;
334
346
  /** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
335
347
  private navStarted;
348
+ /**
349
+ * `true` entre `session_start` y `session_end`. Hace idempotente tanto la apertura como
350
+ * el cierre (p. ej. `visibilitychange` + `pagehide` seguidos) y permite abrir una sesión
351
+ * nueva al volver a foreground o al conceder el consentimiento más tarde.
352
+ */
353
+ private sessionOpen;
354
+ /**
355
+ * Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
356
+ * verdad para el context de analytics Y para la segmentación por idioma de los popups.
357
+ */
358
+ private language;
336
359
  /** Initialize the SDK with configuration */
337
360
  init(config: DeepdotsInitParams): void;
338
361
  /** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
@@ -371,24 +394,66 @@ declare class DeepdotsPopups {
371
394
  trackFindabilityFriction(frictionTopic: string, params?: Record<string, unknown>): void;
372
395
  /** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
373
396
  trackFunnelStep(funnel: string, step: string, taskId: string, params?: Record<string, unknown>): void;
374
- /** Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app). No-op si tracking off. */
397
+ /**
398
+ * Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app).
399
+ * No-op si tracking off. El evento se descarta (con warning) si el `channel` no es válido,
400
+ * si ese `(message_id, stage)` ya se emitió en la sesión, o si el `message_id` ya se reportó
401
+ * en otro canal — ver `MessageGuard`.
402
+ */
375
403
  trackMessage(stage: MessageStage, options: TrackMessageOptions): void;
376
404
  /**
377
405
  * Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
378
406
  * El host la llama en cada cambio de pantalla; emite `page_view` al salir de la anterior.
379
407
  */
380
408
  setScreen(name: string): void;
381
- /** App a foreground (RN: AppState 'active'): reanuda el engagement time. */
409
+ /** App a foreground (RN: AppState 'active'): reanuda el engagement time y abre sesión nueva. */
382
410
  onForeground(): void;
383
411
  /**
384
- * App a background (RN: AppState 'background'): cierra pantalla actual (page_view),
385
- * mini-service (mini_service_exit), emite engagement y hace flush del lote.
412
+ * App a background (RN: AppState 'background'): FIN DE SESIÓN. Cierra pantalla actual
413
+ * (page_view), mini-services (mini_service_exit), emite engagement + `session_end` y
414
+ * envía el último lote con `completed:true`.
415
+ *
416
+ * Es la única señal disponible en móvil: el kill de la app (swipe) NO da callback.
386
417
  */
387
418
  onBackground(): void;
419
+ /**
420
+ * Cierre EXPLÍCITO de la sesión por parte del host (logout, fin de flujo…): emite
421
+ * `session_end` y envía el último lote con `completed:true`.
422
+ */
423
+ endSession(): void;
424
+ /**
425
+ * Cambio de usuario (login / logout / cambio de cuenta). Cierra la sesión del usuario
426
+ * anterior (`session_end` con `reason: 'user_change'` + `completed:true`), cambia la
427
+ * identidad y abre una sesión nueva. Sin `userId` vuelve al id anónimo del SDK.
428
+ *
429
+ * Equivale a llamar a `init()` otra vez con otro `userId`.
430
+ */
431
+ setUserId(userId?: string): void;
432
+ /**
433
+ * Cierra la sesión actual: vuelca todo lo que quedaba abierto y envía el último lote
434
+ * marcado con `completed:true`, para que el backend cierre el registro. El `sessionId`
435
+ * cacheado se olvida → el lote siguiente abre un registro nuevo.
436
+ *
437
+ * Idempotente: dos cierres seguidos (p. ej. `visibilitychange` + `pagehide`) no duplican
438
+ * el `session_end`.
439
+ */
440
+ private closeSession;
441
+ /**
442
+ * Abre sesión (`session_start`) si no hay una abierta y el tracking está activo. Idempotente:
443
+ * se llama en init(), al volver a foreground y al conceder el consentimiento.
444
+ */
445
+ private openSession;
388
446
  /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
389
447
  previewAnalytics(): AnalyticsEnvelope;
390
- /** Envía (hoy dry-run → console.log) el lote acumulado de analytics y vacía el buffer. */
391
- flushAnalytics(): void;
448
+ /**
449
+ * Envía el lote acumulado de analytics y vacía el buffer.
450
+ * `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
451
+ * al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
452
+ */
453
+ flushAnalytics(options?: {
454
+ final?: boolean;
455
+ sessionEnd?: boolean;
456
+ }): void;
392
457
  /** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
393
458
  private flushEngagement;
394
459
  private analyticsIdentity;
@@ -86,8 +86,11 @@ interface AnalyticsEnvelope {
86
86
  * Mapeo del envelope de analytics → body de `POST /sdk/feedback`.
87
87
  *
88
88
  * La analítica se envía como un Feedback del modelo del Surveys SDK, agrupado por una
89
- * INTEGRACIÓN creada en la plataforma. Se manda en streaming con `completed:false` y
90
- * `finished:false` (nunca se "cierra"); el backend cose por `sessionId` + `user_id`.
89
+ * INTEGRACIÓN creada en la plataforma. Se manda en streaming con `completed:false`; el
90
+ * backend cose por `sessionId` + `user_id`. El ÚLTIMO lote de una sesión (cierre de página,
91
+ * app a background, cambio de usuario) va con `completed:true`: cierra el registro y el
92
+ * siguiente lote omite el `sessionId` viejo para que el backend abra uno nuevo.
93
+ * `feedback.finished` se queda siempre en `false` (no es la señal de cierre acordada).
91
94
  *
92
95
  * Encoding:
93
96
  * - todo va en `feedback.metadata`: contexto (user_id, session_id, platform…) + eventos
@@ -139,9 +142,11 @@ interface DeepdotsInitParams {
139
142
  /** Platform used in the analytics envelope. Default is 'web'; in RN pass 'android'/'ios'. */
140
143
  platform?: 'web' | 'android' | 'ios';
141
144
  /**
142
- * BCP-47 language tag for the analytics context (e.g. 'es-ES'). If omitted, the SDK
143
- * auto-detects: `navigator.language` on web, or the `Intl` locale as a fallback (works in
144
- * React Native with Hermes, where `navigator.language` is absent).
145
+ * BCP-47 language tag (e.g. 'es-ES') used for the analytics context AND for popup
146
+ * language segmentation (`segments.lang`). If omitted, the SDK auto-detects:
147
+ * `navigator.language` on web, or the `Intl` locale as a fallback (works in
148
+ * React Native with Hermes, where `navigator.language` is absent). If no source
149
+ * resolves a language, popups with `segments.lang` cannot be filtered and are shown.
145
150
  */
146
151
  language?: string;
147
152
  /** Injectable device info (RN: from react-native-device-info). If missing, it is derived from the browser. */
@@ -277,13 +282,16 @@ interface ReactNativeErrorUtils {
277
282
  * el funnel se correlaciona por `message_id` y se agrupa por `message_title`.
278
283
  */
279
284
  type MessageStage = 'delivered' | 'clicked' | 'converted';
285
+ /** Canales válidos. Un mensaje tiene UN canal: se valida en runtime porque el host puede no ser TS. */
286
+ declare const MESSAGE_CHANNELS: readonly ["push", "in_app"];
287
+ type MessageChannel = (typeof MESSAGE_CHANNELS)[number];
280
288
  interface TrackMessageOptions {
281
289
  /** Identificador del mensaje/notificación — correlaciona el funnel delivered→clicked→converted. */
282
290
  id: string;
283
291
  /** Título del mensaje — dimensión de agrupación de #18–22. */
284
292
  title: string;
285
293
  /** Canal de entrega. */
286
- channel: 'push' | 'in_app';
294
+ channel: MessageChannel;
287
295
  /** Nombre de la campaña (opcional). */
288
296
  campaign?: string;
289
297
  /** Valor de conversión (opcional, típico en `stage: 'converted'`). */
@@ -325,14 +333,29 @@ declare class DeepdotsPopups {
325
333
  private analyticsFlushTimer;
326
334
  /** Crash & error reporting (#14–17). Null hasta init(). */
327
335
  private crashReporter;
336
+ /** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */
337
+ private messageGuard;
328
338
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
329
339
  private navObserver;
330
340
  /** Tiempo activo (engagement time, #8). */
331
341
  private engagement;
332
342
  /** Atributos internos del usuario identificado → POST /sdk/popups/contact. Null si no hay userId. */
333
343
  private contact;
344
+ /** Storage resuelto en init() (inyectado por el host o localStorage). Reusado al cambiar de usuario. */
345
+ private storage;
334
346
  /** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
335
347
  private navStarted;
348
+ /**
349
+ * `true` entre `session_start` y `session_end`. Hace idempotente tanto la apertura como
350
+ * el cierre (p. ej. `visibilitychange` + `pagehide` seguidos) y permite abrir una sesión
351
+ * nueva al volver a foreground o al conceder el consentimiento más tarde.
352
+ */
353
+ private sessionOpen;
354
+ /**
355
+ * Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
356
+ * verdad para el context de analytics Y para la segmentación por idioma de los popups.
357
+ */
358
+ private language;
336
359
  /** Initialize the SDK with configuration */
337
360
  init(config: DeepdotsInitParams): void;
338
361
  /** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
@@ -371,24 +394,66 @@ declare class DeepdotsPopups {
371
394
  trackFindabilityFriction(frictionTopic: string, params?: Record<string, unknown>): void;
372
395
  /** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
373
396
  trackFunnelStep(funnel: string, step: string, taskId: string, params?: Record<string, unknown>): void;
374
- /** Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app). No-op si tracking off. */
397
+ /**
398
+ * Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app).
399
+ * No-op si tracking off. El evento se descarta (con warning) si el `channel` no es válido,
400
+ * si ese `(message_id, stage)` ya se emitió en la sesión, o si el `message_id` ya se reportó
401
+ * en otro canal — ver `MessageGuard`.
402
+ */
375
403
  trackMessage(stage: MessageStage, options: TrackMessageOptions): void;
376
404
  /**
377
405
  * Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
378
406
  * El host la llama en cada cambio de pantalla; emite `page_view` al salir de la anterior.
379
407
  */
380
408
  setScreen(name: string): void;
381
- /** App a foreground (RN: AppState 'active'): reanuda el engagement time. */
409
+ /** App a foreground (RN: AppState 'active'): reanuda el engagement time y abre sesión nueva. */
382
410
  onForeground(): void;
383
411
  /**
384
- * App a background (RN: AppState 'background'): cierra pantalla actual (page_view),
385
- * mini-service (mini_service_exit), emite engagement y hace flush del lote.
412
+ * App a background (RN: AppState 'background'): FIN DE SESIÓN. Cierra pantalla actual
413
+ * (page_view), mini-services (mini_service_exit), emite engagement + `session_end` y
414
+ * envía el último lote con `completed:true`.
415
+ *
416
+ * Es la única señal disponible en móvil: el kill de la app (swipe) NO da callback.
386
417
  */
387
418
  onBackground(): void;
419
+ /**
420
+ * Cierre EXPLÍCITO de la sesión por parte del host (logout, fin de flujo…): emite
421
+ * `session_end` y envía el último lote con `completed:true`.
422
+ */
423
+ endSession(): void;
424
+ /**
425
+ * Cambio de usuario (login / logout / cambio de cuenta). Cierra la sesión del usuario
426
+ * anterior (`session_end` con `reason: 'user_change'` + `completed:true`), cambia la
427
+ * identidad y abre una sesión nueva. Sin `userId` vuelve al id anónimo del SDK.
428
+ *
429
+ * Equivale a llamar a `init()` otra vez con otro `userId`.
430
+ */
431
+ setUserId(userId?: string): void;
432
+ /**
433
+ * Cierra la sesión actual: vuelca todo lo que quedaba abierto y envía el último lote
434
+ * marcado con `completed:true`, para que el backend cierre el registro. El `sessionId`
435
+ * cacheado se olvida → el lote siguiente abre un registro nuevo.
436
+ *
437
+ * Idempotente: dos cierres seguidos (p. ej. `visibilitychange` + `pagehide`) no duplican
438
+ * el `session_end`.
439
+ */
440
+ private closeSession;
441
+ /**
442
+ * Abre sesión (`session_start`) si no hay una abierta y el tracking está activo. Idempotente:
443
+ * se llama en init(), al volver a foreground y al conceder el consentimiento.
444
+ */
445
+ private openSession;
388
446
  /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
389
447
  previewAnalytics(): AnalyticsEnvelope;
390
- /** Envía (hoy dry-run → console.log) el lote acumulado de analytics y vacía el buffer. */
391
- flushAnalytics(): void;
448
+ /**
449
+ * Envía el lote acumulado de analytics y vacía el buffer.
450
+ * `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
451
+ * al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
452
+ */
453
+ flushAnalytics(options?: {
454
+ final?: boolean;
455
+ sessionEnd?: boolean;
456
+ }): void;
392
457
  /** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
393
458
  private flushEngagement;
394
459
  private analyticsIdentity;