@magicfeedback/popup-sdk 1.1.6 → 1.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -177,6 +177,11 @@ Config fields:
177
177
  - `mode?: 'server' | 'client'`
178
178
  - `debug?: boolean`
179
179
  - `popups?: PopupDefinition[]`
180
+ - `language?: string` — BCP-47 tag (`'en'`, `'en-US'`, …) used for popup language
181
+ segmentation (`segments.lang`) and for the analytics context. If omitted it is
182
+ auto-detected from `navigator.language`, falling back to the `Intl` locale (which is
183
+ what makes it work in React Native, where `navigator.language` does not exist). If no
184
+ source resolves a language, popups with `segments.lang` cannot be filtered and are shown.
180
185
 
181
186
  ### `autoLaunch()`
182
187
 
package/dist/index.d.mts CHANGED
@@ -86,10 +86,25 @@ interface AnalyticsEnvelope {
86
86
  device?: DeviceInfo;
87
87
  /** User attributes del cliente, usados para breakdowns (registration_status, pass_type, …). */
88
88
  attributes: Record<string, string>;
89
+ /** Métricas del host: valores medibles persistentes → van en `feedback.metrics` del body. */
90
+ metrics?: Record<string, string>;
89
91
  };
90
92
  events: AnalyticsEvent[];
91
93
  }
92
- type AnalyticsSink = (payload: AnalyticsEnvelope) => void;
94
+ /** Contexto del flush, para que el sink pueda adaptar el transporte. */
95
+ interface AnalyticsFlushMeta {
96
+ /**
97
+ * `true` cuando el flush ocurre porque la página/app se está cerrando. El sink debe usar
98
+ * un transporte que sobreviva al unload (sendBeacon) y no puede esperar respuestas.
99
+ */
100
+ final?: boolean;
101
+ }
102
+ /**
103
+ * Envío del lote. Si devuelve una promesa que **rechaza**, el manager re-encola los
104
+ * eventos para reintentarlos en el siguiente flush (fallo transitorio: red o 5xx).
105
+ * Resolver = lote entregado o descartado definitivamente (no se reintenta).
106
+ */
107
+ type AnalyticsSink = (payload: AnalyticsEnvelope, meta?: AnalyticsFlushMeta) => void | Promise<void>;
93
108
 
94
109
  /**
95
110
  * Mapeo del envelope de analytics → body de `POST /sdk/feedback`.
@@ -103,7 +118,8 @@ type AnalyticsSink = (payload: AnalyticsEnvelope) => void;
103
118
  * - cada entrada usa `value: string[]` (array de un elemento)
104
119
  * - eventos: {key: nombre_evento, value: [JSON(timestamp + params)]}
105
120
  * - identidad (user_id) → `profile` como `external-user-id`
106
- * - `answers` vacío, `metrics` no se usa
121
+ * - métricas del host (setMetric) → `feedback.metrics` (mismo shape que metadata, sin prefijo)
122
+ * - `answers` vacío
107
123
  * - `text` vacío
108
124
  */
109
125
 
@@ -119,6 +135,7 @@ interface AnalyticsFeedbackBody {
119
135
  feedback: {
120
136
  text: string;
121
137
  answers: FeedbackKV[];
138
+ metrics: FeedbackKV[];
122
139
  metadata: FeedbackKV[];
123
140
  profile: FeedbackKV[];
124
141
  finished: boolean;
@@ -139,15 +156,29 @@ interface FeedbackSinkOptions {
139
156
  log?: (...args: unknown[]) => void;
140
157
  /** fetch inyectable (tests / RN). */
141
158
  fetchImpl?: typeof fetch;
159
+ /**
160
+ * `navigator.sendBeacon` inyectable. Solo se usa en el flush final (cierre de página):
161
+ * sobrevive al unload, a cambio de no poder leer la respuesta. Si no se pasa, el flush
162
+ * final usa `fetch` con `keepalive`.
163
+ */
164
+ sendBeaconImpl?: (url: string, body: Blob | string) => boolean;
142
165
  /** Callback invocado cuando el backend devuelve un nuevo sessionId (primer POST). */
143
166
  onSessionId?: (id: string) => void;
144
167
  }
145
168
  /**
146
169
  * Sink real: transforma el envelope y hace `POST {baseUrl}/sdk/feedback`.
147
- * Fire-and-forget (no bloquea el flush); errores solo se loguean.
148
- * Stateful: cachea el `sessionId` que devuelve el backend en la primera respuesta
149
- * y lo envía en los siguientes POSTs para que el backend agrupe todos los eventos
150
- * en un único registro de feedback.
170
+ *
171
+ * Stateful: cachea el `sessionId` que devuelve el backend en la primera respuesta y lo
172
+ * envía en los siguientes POSTs para que el backend agrupe todos los eventos en un único
173
+ * registro de feedback.
174
+ *
175
+ * Garantías de entrega:
176
+ * - mientras no se conozca el `sessionId`, los lotes se **serializan** (esperan la primera
177
+ * respuesta): dos POSTs a la vez sin `sessionId` crearían dos registros y partirían los datos;
178
+ * - `keepalive` para que el navegador no cancele el POST al navegar fuera, y `sendBeacon`
179
+ * en el flush final (cierre de página), donde `fetch` puede morir con el documento;
180
+ * - la promesa **rechaza** en fallo transitorio (red, 5xx, 408, 429) para que el
181
+ * `AnalyticsManager` re-encole el lote; un 4xx se loguea y se descarta.
151
182
  */
152
183
  declare function createFeedbackSink(options: FeedbackSinkOptions): AnalyticsSink;
153
184
 
@@ -185,6 +216,14 @@ interface DeepdotsInitParams {
185
216
  storage?: KeyValueStorage;
186
217
  /** Platform used in the analytics envelope. Default is 'web'; in RN pass 'android'/'ios'. */
187
218
  platform?: 'web' | 'android' | 'ios';
219
+ /**
220
+ * BCP-47 language tag (e.g. 'es-ES') used for the analytics context AND for popup
221
+ * language segmentation (`segments.lang`). If omitted, the SDK auto-detects:
222
+ * `navigator.language` on web, or the `Intl` locale as a fallback (works in
223
+ * React Native with Hermes, where `navigator.language` is absent). If no source
224
+ * resolves a language, popups with `segments.lang` cannot be filtered and are shown.
225
+ */
226
+ language?: string;
188
227
  /** Injectable device info (RN: from react-native-device-info). If missing, it is derived from the browser. */
189
228
  device?: DeviceInfo;
190
229
  /**
@@ -517,6 +556,11 @@ declare class DeepdotsPopups {
517
556
  private contact;
518
557
  /** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
519
558
  private navStarted;
559
+ /**
560
+ * Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
561
+ * verdad para el context de analytics Y para la segmentación por idioma de los popups.
562
+ */
563
+ private language;
520
564
  /** Initialize the SDK with configuration */
521
565
  init(config: DeepdotsInitParams): void;
522
566
  /** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
@@ -529,6 +573,11 @@ declare class DeepdotsPopups {
529
573
  track(name: string, params?: Record<string, unknown>): void;
530
574
  /** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
531
575
  setUserAttributes(attributes: Record<string, string | number | boolean>): void;
576
+ /**
577
+ * Registra/actualiza una métrica (valor medible) → `feedback.metrics` del body.
578
+ * Persistente y sobrescribe por key. No-op si tracking off.
579
+ */
580
+ setMetric(key: string, value: string | number | boolean): void;
532
581
  /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
533
582
  reportError(error: unknown, options?: ReportErrorOptions): void;
534
583
  /** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
@@ -566,8 +615,14 @@ declare class DeepdotsPopups {
566
615
  onBackground(): void;
567
616
  /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
568
617
  previewAnalytics(): AnalyticsEnvelope;
569
- /** Envía (hoy dry-run → console.log) el lote acumulado de analytics y vacía el buffer. */
570
- flushAnalytics(): void;
618
+ /**
619
+ * Envía el lote acumulado de analytics y vacía el buffer.
620
+ * `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
621
+ * al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
622
+ */
623
+ flushAnalytics(options?: {
624
+ final?: boolean;
625
+ }): void;
571
626
  /** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
572
627
  private flushEngagement;
573
628
  private analyticsIdentity;
package/dist/index.d.ts CHANGED
@@ -86,10 +86,25 @@ interface AnalyticsEnvelope {
86
86
  device?: DeviceInfo;
87
87
  /** User attributes del cliente, usados para breakdowns (registration_status, pass_type, …). */
88
88
  attributes: Record<string, string>;
89
+ /** Métricas del host: valores medibles persistentes → van en `feedback.metrics` del body. */
90
+ metrics?: Record<string, string>;
89
91
  };
90
92
  events: AnalyticsEvent[];
91
93
  }
92
- type AnalyticsSink = (payload: AnalyticsEnvelope) => void;
94
+ /** Contexto del flush, para que el sink pueda adaptar el transporte. */
95
+ interface AnalyticsFlushMeta {
96
+ /**
97
+ * `true` cuando el flush ocurre porque la página/app se está cerrando. El sink debe usar
98
+ * un transporte que sobreviva al unload (sendBeacon) y no puede esperar respuestas.
99
+ */
100
+ final?: boolean;
101
+ }
102
+ /**
103
+ * Envío del lote. Si devuelve una promesa que **rechaza**, el manager re-encola los
104
+ * eventos para reintentarlos en el siguiente flush (fallo transitorio: red o 5xx).
105
+ * Resolver = lote entregado o descartado definitivamente (no se reintenta).
106
+ */
107
+ type AnalyticsSink = (payload: AnalyticsEnvelope, meta?: AnalyticsFlushMeta) => void | Promise<void>;
93
108
 
94
109
  /**
95
110
  * Mapeo del envelope de analytics → body de `POST /sdk/feedback`.
@@ -103,7 +118,8 @@ type AnalyticsSink = (payload: AnalyticsEnvelope) => void;
103
118
  * - cada entrada usa `value: string[]` (array de un elemento)
104
119
  * - eventos: {key: nombre_evento, value: [JSON(timestamp + params)]}
105
120
  * - identidad (user_id) → `profile` como `external-user-id`
106
- * - `answers` vacío, `metrics` no se usa
121
+ * - métricas del host (setMetric) → `feedback.metrics` (mismo shape que metadata, sin prefijo)
122
+ * - `answers` vacío
107
123
  * - `text` vacío
108
124
  */
109
125
 
@@ -119,6 +135,7 @@ interface AnalyticsFeedbackBody {
119
135
  feedback: {
120
136
  text: string;
121
137
  answers: FeedbackKV[];
138
+ metrics: FeedbackKV[];
122
139
  metadata: FeedbackKV[];
123
140
  profile: FeedbackKV[];
124
141
  finished: boolean;
@@ -139,15 +156,29 @@ interface FeedbackSinkOptions {
139
156
  log?: (...args: unknown[]) => void;
140
157
  /** fetch inyectable (tests / RN). */
141
158
  fetchImpl?: typeof fetch;
159
+ /**
160
+ * `navigator.sendBeacon` inyectable. Solo se usa en el flush final (cierre de página):
161
+ * sobrevive al unload, a cambio de no poder leer la respuesta. Si no se pasa, el flush
162
+ * final usa `fetch` con `keepalive`.
163
+ */
164
+ sendBeaconImpl?: (url: string, body: Blob | string) => boolean;
142
165
  /** Callback invocado cuando el backend devuelve un nuevo sessionId (primer POST). */
143
166
  onSessionId?: (id: string) => void;
144
167
  }
145
168
  /**
146
169
  * Sink real: transforma el envelope y hace `POST {baseUrl}/sdk/feedback`.
147
- * Fire-and-forget (no bloquea el flush); errores solo se loguean.
148
- * Stateful: cachea el `sessionId` que devuelve el backend en la primera respuesta
149
- * y lo envía en los siguientes POSTs para que el backend agrupe todos los eventos
150
- * en un único registro de feedback.
170
+ *
171
+ * Stateful: cachea el `sessionId` que devuelve el backend en la primera respuesta y lo
172
+ * envía en los siguientes POSTs para que el backend agrupe todos los eventos en un único
173
+ * registro de feedback.
174
+ *
175
+ * Garantías de entrega:
176
+ * - mientras no se conozca el `sessionId`, los lotes se **serializan** (esperan la primera
177
+ * respuesta): dos POSTs a la vez sin `sessionId` crearían dos registros y partirían los datos;
178
+ * - `keepalive` para que el navegador no cancele el POST al navegar fuera, y `sendBeacon`
179
+ * en el flush final (cierre de página), donde `fetch` puede morir con el documento;
180
+ * - la promesa **rechaza** en fallo transitorio (red, 5xx, 408, 429) para que el
181
+ * `AnalyticsManager` re-encole el lote; un 4xx se loguea y se descarta.
151
182
  */
152
183
  declare function createFeedbackSink(options: FeedbackSinkOptions): AnalyticsSink;
153
184
 
@@ -185,6 +216,14 @@ interface DeepdotsInitParams {
185
216
  storage?: KeyValueStorage;
186
217
  /** Platform used in the analytics envelope. Default is 'web'; in RN pass 'android'/'ios'. */
187
218
  platform?: 'web' | 'android' | 'ios';
219
+ /**
220
+ * BCP-47 language tag (e.g. 'es-ES') used for the analytics context AND for popup
221
+ * language segmentation (`segments.lang`). If omitted, the SDK auto-detects:
222
+ * `navigator.language` on web, or the `Intl` locale as a fallback (works in
223
+ * React Native with Hermes, where `navigator.language` is absent). If no source
224
+ * resolves a language, popups with `segments.lang` cannot be filtered and are shown.
225
+ */
226
+ language?: string;
188
227
  /** Injectable device info (RN: from react-native-device-info). If missing, it is derived from the browser. */
189
228
  device?: DeviceInfo;
190
229
  /**
@@ -517,6 +556,11 @@ declare class DeepdotsPopups {
517
556
  private contact;
518
557
  /** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
519
558
  private navStarted;
559
+ /**
560
+ * Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
561
+ * verdad para el context de analytics Y para la segmentación por idioma de los popups.
562
+ */
563
+ private language;
520
564
  /** Initialize the SDK with configuration */
521
565
  init(config: DeepdotsInitParams): void;
522
566
  /** User id actual (generado por el SDK o provisto por el cliente). Null si tracking off. */
@@ -529,6 +573,11 @@ declare class DeepdotsPopups {
529
573
  track(name: string, params?: Record<string, unknown>): void;
530
574
  /** User attributes para breakdowns (registration_status, pass_type, sector, pass_status…). */
531
575
  setUserAttributes(attributes: Record<string, string | number | boolean>): void;
576
+ /**
577
+ * Registra/actualiza una métrica (valor medible) → `feedback.metrics` del body.
578
+ * Persistente y sobrescribe por key. No-op si tracking off.
579
+ */
580
+ setMetric(key: string, value: string | number | boolean): void;
532
581
  /** Reporta un error del host (manejado o no) → evento `deepdots_app_crash`. No-op si tracking off. */
533
582
  reportError(error: unknown, options?: ReportErrorOptions): void;
534
583
  /** Engancha `global.ErrorUtils` de RN para capturar errores JS no manejados. No-op si tracking off. */
@@ -566,8 +615,14 @@ declare class DeepdotsPopups {
566
615
  onBackground(): void;
567
616
  /** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
568
617
  previewAnalytics(): AnalyticsEnvelope;
569
- /** Envía (hoy dry-run → console.log) el lote acumulado de analytics y vacía el buffer. */
570
- flushAnalytics(): void;
618
+ /**
619
+ * Envía el lote acumulado de analytics y vacía el buffer.
620
+ * `final: true` (cierre de página/app) cambia el transporte a `sendBeacon`, que sobrevive
621
+ * al unload; un lote que falle por red o 5xx se re-encola para el siguiente flush.
622
+ */
623
+ flushAnalytics(options?: {
624
+ final?: boolean;
625
+ }): void;
571
626
  /** Emite un evento `user_engagement` con el tiempo activo acumulado (#8). */
572
627
  private flushEngagement;
573
628
  private analyticsIdentity;