@magicfeedback/popup-sdk 1.1.8 → 1.3.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
@@ -279,13 +282,16 @@ interface ReactNativeErrorUtils {
279
282
  * el funnel se correlaciona por `message_id` y se agrupa por `message_title`.
280
283
  */
281
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];
282
288
  interface TrackMessageOptions {
283
289
  /** Identificador del mensaje/notificación — correlaciona el funnel delivered→clicked→converted. */
284
290
  id: string;
285
291
  /** Título del mensaje — dimensión de agrupación de #18–22. */
286
292
  title: string;
287
293
  /** Canal de entrega. */
288
- channel: 'push' | 'in_app';
294
+ channel: MessageChannel;
289
295
  /** Nombre de la campaña (opcional). */
290
296
  campaign?: string;
291
297
  /** Valor de conversión (opcional, típico en `stage: 'converted'`). */
@@ -327,14 +333,24 @@ declare class DeepdotsPopups {
327
333
  private analyticsFlushTimer;
328
334
  /** Crash & error reporting (#14–17). Null hasta init(). */
329
335
  private crashReporter;
336
+ /** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */
337
+ private messageGuard;
330
338
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
331
339
  private navObserver;
332
340
  /** Tiempo activo (engagement time, #8). */
333
341
  private engagement;
334
342
  /** Atributos internos del usuario identificado → POST /sdk/popups/contact. Null si no hay userId. */
335
343
  private contact;
344
+ /** Storage resuelto en init() (inyectado por el host o localStorage). Reusado al cambiar de usuario. */
345
+ private storage;
336
346
  /** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
337
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;
338
354
  /**
339
355
  * Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
340
356
  * verdad para el context de analytics Y para la segmentación por idioma de los popups.
@@ -378,20 +394,55 @@ declare class DeepdotsPopups {
378
394
  trackFindabilityFriction(frictionTopic: string, params?: Record<string, unknown>): void;
379
395
  /** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
380
396
  trackFunnelStep(funnel: string, step: string, taskId: string, params?: Record<string, unknown>): void;
381
- /** 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
+ */
382
403
  trackMessage(stage: MessageStage, options: TrackMessageOptions): void;
383
404
  /**
384
405
  * Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
385
406
  * El host la llama en cada cambio de pantalla; emite `page_view` al salir de la anterior.
386
407
  */
387
408
  setScreen(name: string): void;
388
- /** 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. */
389
410
  onForeground(): void;
390
411
  /**
391
- * App a background (RN: AppState 'background'): cierra pantalla actual (page_view),
392
- * 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.
393
417
  */
394
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;
395
446
  /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
396
447
  previewAnalytics(): AnalyticsEnvelope;
397
448
  /**
@@ -401,6 +452,7 @@ declare class DeepdotsPopups {
401
452
  */
402
453
  flushAnalytics(options?: {
403
454
  final?: boolean;
455
+ sessionEnd?: boolean;
404
456
  }): void;
405
457
  /** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
406
458
  private flushEngagement;
@@ -560,7 +612,7 @@ declare class ReactNativePopupRenderer implements PopupRenderer {
560
612
  private partialEmitted;
561
613
  constructor(options?: ReactNativeRendererOptions);
562
614
  init(): void;
563
- 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;
615
+ 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;
564
616
  hide(): void;
565
617
  /**
566
618
  * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a
@@ -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
@@ -279,13 +282,16 @@ interface ReactNativeErrorUtils {
279
282
  * el funnel se correlaciona por `message_id` y se agrupa por `message_title`.
280
283
  */
281
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];
282
288
  interface TrackMessageOptions {
283
289
  /** Identificador del mensaje/notificación — correlaciona el funnel delivered→clicked→converted. */
284
290
  id: string;
285
291
  /** Título del mensaje — dimensión de agrupación de #18–22. */
286
292
  title: string;
287
293
  /** Canal de entrega. */
288
- channel: 'push' | 'in_app';
294
+ channel: MessageChannel;
289
295
  /** Nombre de la campaña (opcional). */
290
296
  campaign?: string;
291
297
  /** Valor de conversión (opcional, típico en `stage: 'converted'`). */
@@ -327,14 +333,24 @@ declare class DeepdotsPopups {
327
333
  private analyticsFlushTimer;
328
334
  /** Crash & error reporting (#14–17). Null hasta init(). */
329
335
  private crashReporter;
336
+ /** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */
337
+ private messageGuard;
330
338
  /** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
331
339
  private navObserver;
332
340
  /** Tiempo activo (engagement time, #8). */
333
341
  private engagement;
334
342
  /** Atributos internos del usuario identificado → POST /sdk/popups/contact. Null si no hay userId. */
335
343
  private contact;
344
+ /** Storage resuelto en init() (inyectado por el host o localStorage). Reusado al cambiar de usuario. */
345
+ private storage;
336
346
  /** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
337
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;
338
354
  /**
339
355
  * Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
340
356
  * verdad para el context de analytics Y para la segmentación por idioma de los popups.
@@ -378,20 +394,55 @@ declare class DeepdotsPopups {
378
394
  trackFindabilityFriction(frictionTopic: string, params?: Record<string, unknown>): void;
379
395
  /** Funnel: un paso del embudo, correlacionado por `taskId`. El backend reconstruye conversión/drop-off/tiempo. */
380
396
  trackFunnelStep(funnel: string, step: string, taskId: string, params?: Record<string, unknown>): void;
381
- /** 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
+ */
382
403
  trackMessage(stage: MessageStage, options: TrackMessageOptions): void;
383
404
  /**
384
405
  * Navegación MANUAL (entornos sin History API, p. ej. React Native con React Navigation).
385
406
  * El host la llama en cada cambio de pantalla; emite `page_view` al salir de la anterior.
386
407
  */
387
408
  setScreen(name: string): void;
388
- /** 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. */
389
410
  onForeground(): void;
390
411
  /**
391
- * App a background (RN: AppState 'background'): cierra pantalla actual (page_view),
392
- * 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.
393
417
  */
394
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;
395
446
  /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
396
447
  previewAnalytics(): AnalyticsEnvelope;
397
448
  /**
@@ -401,6 +452,7 @@ declare class DeepdotsPopups {
401
452
  */
402
453
  flushAnalytics(options?: {
403
454
  final?: boolean;
455
+ sessionEnd?: boolean;
404
456
  }): void;
405
457
  /** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
406
458
  private flushEngagement;
@@ -560,7 +612,7 @@ declare class ReactNativePopupRenderer implements PopupRenderer {
560
612
  private partialEmitted;
561
613
  constructor(options?: ReactNativeRendererOptions);
562
614
  init(): void;
563
- 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;
615
+ 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;
564
616
  hide(): void;
565
617
  /**
566
618
  * El host la conecta al `onMessage` del WebView. Traduce los mensajes del survey a