@magicfeedback/popup-sdk 1.1.8 → 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.
- package/dist/index.d.mts +119 -9
- package/dist/index.d.ts +119 -9
- package/dist/index.js +191 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +187 -21
- package/dist/index.mjs.map +1 -1
- package/dist/react-native.d.mts +59 -7
- package/dist/react-native.d.ts +59 -7
- package/dist/react-native.js +188 -19
- package/dist/react-native.js.map +1 -1
- package/dist/react-native.mjs +184 -21
- package/dist/react-native.mjs.map +1 -1
- package/dist/{renderPopup-PS3NJDUR.mjs → renderPopup-NHWJ7YSI.mjs} +7 -1
- package/dist/renderPopup-NHWJ7YSI.mjs.map +1 -0
- package/dist/{renderPopup-EZKDMLVZ.mjs → renderPopup-R53C7DB3.mjs} +7 -1
- package/dist/renderPopup-R53C7DB3.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/renderPopup-EZKDMLVZ.mjs.map +0 -1
- package/dist/renderPopup-PS3NJDUR.mjs.map +0 -1
package/dist/react-native.d.mts
CHANGED
|
@@ -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
|
|
90
|
-
*
|
|
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:
|
|
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
|
-
/**
|
|
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'):
|
|
392
|
-
* mini-
|
|
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;
|
package/dist/react-native.d.ts
CHANGED
|
@@ -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
|
|
90
|
-
*
|
|
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:
|
|
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
|
-
/**
|
|
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'):
|
|
392
|
-
* mini-
|
|
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;
|
package/dist/react-native.js
CHANGED
|
@@ -1149,6 +1149,11 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
1149
1149
|
const isDark = style?.theme === "dark";
|
|
1150
1150
|
const theme = {
|
|
1151
1151
|
popupBg: isDark ? "#1e1e1e" : "#fff",
|
|
1152
|
+
// El popup fija su propio fondo, así que debe fijar también el esquema de color.
|
|
1153
|
+
// Sin esto los controles de formulario (input/textarea/select) caen al estilo por
|
|
1154
|
+
// defecto del navegador: en un host con `prefers-color-scheme: dark` se pintan
|
|
1155
|
+
// oscuros con texto blanco sobre el fondo claro del popup.
|
|
1156
|
+
colorScheme: isDark ? "dark" : "light",
|
|
1152
1157
|
textPrimary: isDark ? "#f0f0f0" : "#111",
|
|
1153
1158
|
closeBtnHoverBg: isDark ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.06)",
|
|
1154
1159
|
closeBtnHoverColor: isDark ? "#fff" : "#000000"
|
|
@@ -1171,6 +1176,7 @@ async function renderPopup(container, surveyId, productId, actions, emit, onClos
|
|
|
1171
1176
|
flex-direction: column;
|
|
1172
1177
|
justify-content: flex-start;
|
|
1173
1178
|
background: ${theme.popupBg};
|
|
1179
|
+
color-scheme: ${theme.colorScheme};
|
|
1174
1180
|
border-radius: 8px;
|
|
1175
1181
|
padding: 24px;
|
|
1176
1182
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
@@ -2079,6 +2085,15 @@ var AnalyticsManager = class {
|
|
|
2079
2085
|
this.exitMiniService(name);
|
|
2080
2086
|
}
|
|
2081
2087
|
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Olvida lo que pertenecía al usuario anterior (user attributes + métricas) al cambiar de
|
|
2090
|
+
* usuario. NO toca el buffer de eventos: el cierre de sesión ya lo vació con la identidad
|
|
2091
|
+
* vieja, y lo que llegue después es del usuario nuevo.
|
|
2092
|
+
*/
|
|
2093
|
+
resetUserScope() {
|
|
2094
|
+
this.attributes = {};
|
|
2095
|
+
this.metrics = {};
|
|
2096
|
+
}
|
|
2082
2097
|
/** Registra un evento de analítica (modelo GA: nombre + params). */
|
|
2083
2098
|
track(name, params) {
|
|
2084
2099
|
const current = this.getMiniService();
|
|
@@ -2166,7 +2181,7 @@ function pushKV(list, key, value) {
|
|
|
2166
2181
|
if (value === void 0 || value === null || value === "") return;
|
|
2167
2182
|
list.push({ key, value: [String(value)] });
|
|
2168
2183
|
}
|
|
2169
|
-
function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
|
|
2184
|
+
function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId, options) {
|
|
2170
2185
|
const { context } = envelope;
|
|
2171
2186
|
const profile = [];
|
|
2172
2187
|
pushKV(profile, "external-user-id", envelope.userId);
|
|
@@ -2217,7 +2232,8 @@ function buildAnalyticsFeedbackBody(envelope, keys, feedbackSessionId) {
|
|
|
2217
2232
|
},
|
|
2218
2233
|
publicKey: keys.publicKey,
|
|
2219
2234
|
integration: keys.integration,
|
|
2220
|
-
|
|
2235
|
+
// Único marcador de cierre acordado con backend: el último lote de la sesión.
|
|
2236
|
+
completed: options?.sessionEnd === true,
|
|
2221
2237
|
...feedbackSessionId ? { sessionId: feedbackSessionId } : {}
|
|
2222
2238
|
};
|
|
2223
2239
|
}
|
|
@@ -2236,7 +2252,7 @@ function createFeedbackSink(options) {
|
|
|
2236
2252
|
let feedbackSessionId;
|
|
2237
2253
|
let firstPostInFlight = null;
|
|
2238
2254
|
const url = `${options.baseUrl}/sdk/feedback`;
|
|
2239
|
-
const post = async (body, final) => {
|
|
2255
|
+
const post = async (body, final, sessionEnd = false) => {
|
|
2240
2256
|
const json = JSON.stringify(body);
|
|
2241
2257
|
const small = json.length <= KEEPALIVE_MAX_BYTES;
|
|
2242
2258
|
if (final && options.sendBeaconImpl && small) {
|
|
@@ -2270,6 +2286,7 @@ function createFeedbackSink(options) {
|
|
|
2270
2286
|
);
|
|
2271
2287
|
return;
|
|
2272
2288
|
}
|
|
2289
|
+
if (sessionEnd) return;
|
|
2273
2290
|
try {
|
|
2274
2291
|
const data = await res.json();
|
|
2275
2292
|
if (data?.sessionId && data.sessionId !== feedbackSessionId) {
|
|
@@ -2282,13 +2299,18 @@ function createFeedbackSink(options) {
|
|
|
2282
2299
|
};
|
|
2283
2300
|
return async (envelope, meta) => {
|
|
2284
2301
|
const final = meta?.final === true;
|
|
2302
|
+
const sessionEnd = meta?.sessionEnd === true;
|
|
2285
2303
|
if (!feedbackSessionId && firstPostInFlight && !final) {
|
|
2286
2304
|
await firstPostInFlight;
|
|
2287
2305
|
}
|
|
2288
|
-
const body = buildAnalyticsFeedbackBody(envelope, options.keys, feedbackSessionId);
|
|
2306
|
+
const body = buildAnalyticsFeedbackBody(envelope, options.keys, feedbackSessionId, { sessionEnd });
|
|
2289
2307
|
options.log?.("[DeepdotsAnalytics] POST /sdk/feedback \u2192", body);
|
|
2290
|
-
const sent = post(body, final);
|
|
2291
|
-
if (
|
|
2308
|
+
const sent = post(body, final, sessionEnd);
|
|
2309
|
+
if (sessionEnd) {
|
|
2310
|
+
feedbackSessionId = void 0;
|
|
2311
|
+
firstPostInFlight = null;
|
|
2312
|
+
options.onSessionReset?.();
|
|
2313
|
+
} else if (!feedbackSessionId) {
|
|
2292
2314
|
firstPostInFlight = sent.then(
|
|
2293
2315
|
() => void 0,
|
|
2294
2316
|
() => void 0
|
|
@@ -2727,6 +2749,7 @@ var CrashReporter = class {
|
|
|
2727
2749
|
init_logger();
|
|
2728
2750
|
|
|
2729
2751
|
// src/analytics/messaging.ts
|
|
2752
|
+
var MESSAGE_CHANNELS = ["push", "in_app"];
|
|
2730
2753
|
function buildMessageParams(stage, o) {
|
|
2731
2754
|
const p = {
|
|
2732
2755
|
stage,
|
|
@@ -2739,6 +2762,57 @@ function buildMessageParams(stage, o) {
|
|
|
2739
2762
|
if (o.currency) p.currency = o.currency;
|
|
2740
2763
|
return { ...p, ...o.params ?? {} };
|
|
2741
2764
|
}
|
|
2765
|
+
var MAX_TRACKED_MESSAGES = 500;
|
|
2766
|
+
var MessageGuard = class {
|
|
2767
|
+
constructor(maxTrackedMessages = MAX_TRACKED_MESSAGES) {
|
|
2768
|
+
this.maxTrackedMessages = maxTrackedMessages;
|
|
2769
|
+
/** message_id → canal fijado + stages ya emitidos. El orden de inserción marca la eviction. */
|
|
2770
|
+
this.seen = /* @__PURE__ */ new Map();
|
|
2771
|
+
}
|
|
2772
|
+
evaluate(stage, options) {
|
|
2773
|
+
const channel = options.channel;
|
|
2774
|
+
if (!MESSAGE_CHANNELS.includes(channel)) {
|
|
2775
|
+
return {
|
|
2776
|
+
emit: false,
|
|
2777
|
+
reason: "invalid_channel",
|
|
2778
|
+
detail: `channel "${channel}" no v\xE1lido (esperado ${MESSAGE_CHANNELS.join(" | ")})`
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
const entry = this.seen.get(options.id);
|
|
2782
|
+
if (!entry) {
|
|
2783
|
+
this.remember(options.id, channel, stage);
|
|
2784
|
+
return { emit: true };
|
|
2785
|
+
}
|
|
2786
|
+
if (entry.channel !== channel) {
|
|
2787
|
+
return {
|
|
2788
|
+
emit: false,
|
|
2789
|
+
reason: "channel_conflict",
|
|
2790
|
+
detail: `message_id "${options.id}" ya se report\xF3 en channel "${entry.channel}"; se descarta "${channel}"`
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
if (entry.stages.has(stage)) {
|
|
2794
|
+
return {
|
|
2795
|
+
emit: false,
|
|
2796
|
+
reason: "duplicate_stage",
|
|
2797
|
+
detail: `stage "${stage}" ya emitido para message_id "${options.id}"`
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
entry.stages.add(stage);
|
|
2801
|
+
return { emit: true };
|
|
2802
|
+
}
|
|
2803
|
+
/** Olvida todo lo vigilado (nueva sesión). */
|
|
2804
|
+
reset() {
|
|
2805
|
+
this.seen.clear();
|
|
2806
|
+
}
|
|
2807
|
+
remember(id, channel, stage) {
|
|
2808
|
+
this.seen.set(id, { channel, stages: /* @__PURE__ */ new Set([stage]) });
|
|
2809
|
+
while (this.seen.size > this.maxTrackedMessages) {
|
|
2810
|
+
const oldest = this.seen.keys().next();
|
|
2811
|
+
if (oldest.done) break;
|
|
2812
|
+
this.seen.delete(oldest.value);
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
};
|
|
2742
2816
|
|
|
2743
2817
|
// src/core/deepdots-popups.ts
|
|
2744
2818
|
var EXIT_QUEUE_STORAGE_KEY = "__deepdots_exit_popup_queue__";
|
|
@@ -2780,14 +2854,24 @@ var DeepdotsPopups = class {
|
|
|
2780
2854
|
this.analyticsFlushTimer = void 0;
|
|
2781
2855
|
/** Crash & error reporting (#14–17). Null hasta init(). */
|
|
2782
2856
|
this.crashReporter = null;
|
|
2857
|
+
/** Protecciones del funnel de Messaging (#18–22). Vigencia de sesión: se reinicia en init(). */
|
|
2858
|
+
this.messageGuard = new MessageGuard();
|
|
2783
2859
|
/** Observador de navegación (Fase 2): emite page_view por el canal de analytics. */
|
|
2784
2860
|
this.navObserver = null;
|
|
2785
2861
|
/** Tiempo activo (engagement time, #8). */
|
|
2786
2862
|
this.engagement = null;
|
|
2787
2863
|
/** Atributos internos del usuario identificado → POST /sdk/popups/contact. Null si no hay userId. */
|
|
2788
2864
|
this.contact = null;
|
|
2865
|
+
/** Storage resuelto en init() (inyectado por el host o localStorage). Reusado al cambiar de usuario. */
|
|
2866
|
+
this.storage = null;
|
|
2789
2867
|
/** Marca si ya se inició la navegación manual (setScreen) — para RN sin History API. */
|
|
2790
2868
|
this.navStarted = false;
|
|
2869
|
+
/**
|
|
2870
|
+
* `true` entre `session_start` y `session_end`. Hace idempotente tanto la apertura como
|
|
2871
|
+
* el cierre (p. ej. `visibilitychange` + `pagehide` seguidos) y permite abrir una sesión
|
|
2872
|
+
* nueva al volver a foreground o al conceder el consentimiento más tarde.
|
|
2873
|
+
*/
|
|
2874
|
+
this.sessionOpen = false;
|
|
2791
2875
|
/**
|
|
2792
2876
|
* Idioma resuelto en init() (explícito > navigator.language > Intl). Única fuente de
|
|
2793
2877
|
* verdad para el context de analytics Y para la segmentación por idioma de los popups.
|
|
@@ -2799,6 +2883,11 @@ var DeepdotsPopups = class {
|
|
|
2799
2883
|
this.logger = config.logger ?? console;
|
|
2800
2884
|
setLogger(this.logger);
|
|
2801
2885
|
if (this.initialized) {
|
|
2886
|
+
const nextUserId = config.userId || void 0;
|
|
2887
|
+
if (nextUserId !== this.config?.userId) {
|
|
2888
|
+
this.setUserId(nextUserId);
|
|
2889
|
+
return;
|
|
2890
|
+
}
|
|
2802
2891
|
this.log("SDK already initialized");
|
|
2803
2892
|
return;
|
|
2804
2893
|
}
|
|
@@ -2811,6 +2900,7 @@ var DeepdotsPopups = class {
|
|
|
2811
2900
|
userId: config.userId || void 0
|
|
2812
2901
|
};
|
|
2813
2902
|
const storage = config.storage ?? createDefaultStorage();
|
|
2903
|
+
this.storage = storage;
|
|
2814
2904
|
this.tracking = new TrackingManager({
|
|
2815
2905
|
storage,
|
|
2816
2906
|
clientUserId: this.config.userId,
|
|
@@ -2838,6 +2928,9 @@ var DeepdotsPopups = class {
|
|
|
2838
2928
|
sendBeaconImpl: typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function" ? (url, body) => navigator.sendBeacon(url, body) : void 0,
|
|
2839
2929
|
onSessionId: (id) => {
|
|
2840
2930
|
this.analyticsFeedbackSessionId = id;
|
|
2931
|
+
},
|
|
2932
|
+
onSessionReset: () => {
|
|
2933
|
+
this.analyticsFeedbackSessionId = void 0;
|
|
2841
2934
|
}
|
|
2842
2935
|
}) : createDryRunSink((...a) => this.logger.log(...a));
|
|
2843
2936
|
const device = config.device ?? collectDeviceInfo(config.appVersion);
|
|
@@ -2846,6 +2939,7 @@ var DeepdotsPopups = class {
|
|
|
2846
2939
|
navigator: typeof navigator !== "undefined" ? navigator : void 0,
|
|
2847
2940
|
intl: typeof Intl !== "undefined" ? Intl : void 0
|
|
2848
2941
|
});
|
|
2942
|
+
this.messageGuard.reset();
|
|
2849
2943
|
this.analytics = new AnalyticsManager({
|
|
2850
2944
|
sink: analyticsSink,
|
|
2851
2945
|
publicKey: config.analytics?.publicKey ?? this.config.apiKey,
|
|
@@ -2881,7 +2975,7 @@ var DeepdotsPopups = class {
|
|
|
2881
2975
|
this.engagement = new EngagementTracker();
|
|
2882
2976
|
this.engagement.resume();
|
|
2883
2977
|
this.setupAnalyticsFlush();
|
|
2884
|
-
this.
|
|
2978
|
+
this.openSession();
|
|
2885
2979
|
const pendingCrashes = this.crashReporter.drainPendingCrashes();
|
|
2886
2980
|
if (this.tracking?.isTrackingEnabled()) {
|
|
2887
2981
|
for (const rec of pendingCrashes) {
|
|
@@ -2913,7 +3007,9 @@ var DeepdotsPopups = class {
|
|
|
2913
3007
|
}
|
|
2914
3008
|
/** Activa/desactiva el tracking (identidad + sesión + analytics). Kill-switch del contrato §7bis. */
|
|
2915
3009
|
setTrackingEnabled(enabled) {
|
|
3010
|
+
if (!enabled) this.closeSession("tracking_disabled");
|
|
2916
3011
|
this.tracking?.setTrackingEnabled(enabled);
|
|
3012
|
+
if (enabled) this.openSession();
|
|
2917
3013
|
this.log("tracking \xB7 setTrackingEnabled:", enabled, "\xB7 session_id:", this.tracking?.getSessionId() ?? null);
|
|
2918
3014
|
}
|
|
2919
3015
|
// ───────── Analytics (canal separado del feedback, vinculado por user_id) ─────────
|
|
@@ -2983,8 +3079,18 @@ var DeepdotsPopups = class {
|
|
|
2983
3079
|
trackFunnelStep(funnel, step, taskId, params) {
|
|
2984
3080
|
this.track("deepdots_funnel_step", { funnel, step, task_id: taskId, ...params ?? {} });
|
|
2985
3081
|
}
|
|
2986
|
-
/**
|
|
3082
|
+
/**
|
|
3083
|
+
* Messaging (#18–22): registra una etapa del funnel de una notificación (push/in-app).
|
|
3084
|
+
* No-op si tracking off. El evento se descarta (con warning) si el `channel` no es válido,
|
|
3085
|
+
* si ese `(message_id, stage)` ya se emitió en la sesión, o si el `message_id` ya se reportó
|
|
3086
|
+
* en otro canal — ver `MessageGuard`.
|
|
3087
|
+
*/
|
|
2987
3088
|
trackMessage(stage, options) {
|
|
3089
|
+
const verdict = this.messageGuard.evaluate(stage, options);
|
|
3090
|
+
if (!verdict.emit) {
|
|
3091
|
+
console.warn(`[DeepdotsPopups] trackMessage descartado (${verdict.reason}): ${verdict.detail}`);
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
2988
3094
|
this.track("deepdots_message", buildMessageParams(stage, options));
|
|
2989
3095
|
}
|
|
2990
3096
|
/**
|
|
@@ -3000,21 +3106,86 @@ var DeepdotsPopups = class {
|
|
|
3000
3106
|
this.navObserver.visit(name);
|
|
3001
3107
|
}
|
|
3002
3108
|
}
|
|
3003
|
-
/** App a foreground (RN: AppState 'active'): reanuda el engagement time. */
|
|
3109
|
+
/** App a foreground (RN: AppState 'active'): reanuda el engagement time y abre sesión nueva. */
|
|
3004
3110
|
onForeground() {
|
|
3005
3111
|
this.engagement?.resume();
|
|
3112
|
+
this.openSession();
|
|
3006
3113
|
}
|
|
3007
3114
|
/**
|
|
3008
|
-
* App a background (RN: AppState 'background'):
|
|
3009
|
-
* mini-
|
|
3115
|
+
* App a background (RN: AppState 'background'): FIN DE SESIÓN. Cierra pantalla actual
|
|
3116
|
+
* (page_view), mini-services (mini_service_exit), emite engagement + `session_end` y
|
|
3117
|
+
* envía el último lote con `completed:true`.
|
|
3118
|
+
*
|
|
3119
|
+
* Es la única señal disponible en móvil: el kill de la app (swipe) NO da callback.
|
|
3010
3120
|
*/
|
|
3011
3121
|
onBackground() {
|
|
3122
|
+
this.closeSession("background");
|
|
3123
|
+
this.engagement?.pause();
|
|
3124
|
+
}
|
|
3125
|
+
/**
|
|
3126
|
+
* Cierre EXPLÍCITO de la sesión por parte del host (logout, fin de flujo…): emite
|
|
3127
|
+
* `session_end` y envía el último lote con `completed:true`.
|
|
3128
|
+
*/
|
|
3129
|
+
endSession() {
|
|
3130
|
+
this.closeSession("manual");
|
|
3131
|
+
}
|
|
3132
|
+
/**
|
|
3133
|
+
* Cambio de usuario (login / logout / cambio de cuenta). Cierra la sesión del usuario
|
|
3134
|
+
* anterior (`session_end` con `reason: 'user_change'` + `completed:true`), cambia la
|
|
3135
|
+
* identidad y abre una sesión nueva. Sin `userId` vuelve al id anónimo del SDK.
|
|
3136
|
+
*
|
|
3137
|
+
* Equivale a llamar a `init()` otra vez con otro `userId`.
|
|
3138
|
+
*/
|
|
3139
|
+
setUserId(userId) {
|
|
3140
|
+
const next = userId || void 0;
|
|
3141
|
+
if (!this.initialized || !this.config || next === this.config.userId) return;
|
|
3142
|
+
this.closeSession("user_change");
|
|
3143
|
+
const enabled = this.tracking?.isTrackingEnabled() ?? true;
|
|
3144
|
+
const storage = this.storage ?? createDefaultStorage();
|
|
3145
|
+
this.config.userId = next;
|
|
3146
|
+
this.tracking = new TrackingManager({ storage, clientUserId: next, enabled });
|
|
3147
|
+
this.contact = next ? new ContactManager({
|
|
3148
|
+
storage,
|
|
3149
|
+
publicKey: this.config.apiKey ?? "",
|
|
3150
|
+
userId: next,
|
|
3151
|
+
post: (body) => this.postContact(body)
|
|
3152
|
+
}) : null;
|
|
3153
|
+
this.analytics?.resetUserScope();
|
|
3154
|
+
this.messageGuard.reset();
|
|
3155
|
+
this.log("tracking \xB7 user_change \xB7 user_id:", this.tracking.getUserId());
|
|
3156
|
+
this.openSession();
|
|
3157
|
+
}
|
|
3158
|
+
/**
|
|
3159
|
+
* Cierra la sesión actual: vuelca todo lo que quedaba abierto y envía el último lote
|
|
3160
|
+
* marcado con `completed:true`, para que el backend cierre el registro. El `sessionId`
|
|
3161
|
+
* cacheado se olvida → el lote siguiente abre un registro nuevo.
|
|
3162
|
+
*
|
|
3163
|
+
* Idempotente: dos cierres seguidos (p. ej. `visibilitychange` + `pagehide`) no duplican
|
|
3164
|
+
* el `session_end`.
|
|
3165
|
+
*/
|
|
3166
|
+
closeSession(reason) {
|
|
3167
|
+
if (!this.sessionOpen) return;
|
|
3168
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
3169
|
+
this.sessionOpen = false;
|
|
3012
3170
|
this.navObserver?.stop();
|
|
3013
3171
|
this.navStarted = false;
|
|
3014
|
-
|
|
3172
|
+
this.analytics?.exitAllMiniServices();
|
|
3015
3173
|
this.flushEngagement();
|
|
3016
|
-
this.
|
|
3017
|
-
this.flushAnalytics();
|
|
3174
|
+
this.track("deepdots_session_end", { reason });
|
|
3175
|
+
this.flushAnalytics({ final: reason === "page_hide", sessionEnd: true });
|
|
3176
|
+
this.analyticsFeedbackSessionId = void 0;
|
|
3177
|
+
this.tracking?.setSessionId(null);
|
|
3178
|
+
this.log("tracking \xB7 session_end:", reason);
|
|
3179
|
+
}
|
|
3180
|
+
/**
|
|
3181
|
+
* Abre sesión (`session_start`) si no hay una abierta y el tracking está activo. Idempotente:
|
|
3182
|
+
* se llama en init(), al volver a foreground y al conceder el consentimiento.
|
|
3183
|
+
*/
|
|
3184
|
+
openSession() {
|
|
3185
|
+
if (this.sessionOpen) return;
|
|
3186
|
+
if (!this.tracking?.isTrackingEnabled()) return;
|
|
3187
|
+
this.sessionOpen = true;
|
|
3188
|
+
this.track("deepdots_session_start", {});
|
|
3018
3189
|
}
|
|
3019
3190
|
/** Payload que se ENVIARÍA al endpoint de analytics (no envía ni vacía el buffer). */
|
|
3020
3191
|
previewAnalytics() {
|
|
@@ -3052,10 +3223,7 @@ var DeepdotsPopups = class {
|
|
|
3052
3223
|
this.analyticsFlushTimer = setInterval(() => this.flushAnalytics(), ANALYTICS_FLUSH_INTERVAL_MS);
|
|
3053
3224
|
window.addEventListener("pagehide", () => {
|
|
3054
3225
|
clearInterval(this.analyticsFlushTimer);
|
|
3055
|
-
this.
|
|
3056
|
-
if (this.tracking?.isTrackingEnabled()) this.analytics?.exitAllMiniServices();
|
|
3057
|
-
this.flushEngagement();
|
|
3058
|
-
this.flushAnalytics({ final: true });
|
|
3226
|
+
this.closeSession("page_hide");
|
|
3059
3227
|
});
|
|
3060
3228
|
document.addEventListener("visibilitychange", () => {
|
|
3061
3229
|
if (document.visibilityState === "hidden") {
|
|
@@ -3730,7 +3898,8 @@ function setupReactNative(sdk2, config, deps = {}) {
|
|
|
3730
3898
|
if (deps.appState?.addEventListener) {
|
|
3731
3899
|
subscription = deps.appState.addEventListener("change", (state) => {
|
|
3732
3900
|
if (state === "active") sdk2.onForeground();
|
|
3733
|
-
else sdk2.onBackground();
|
|
3901
|
+
else if (state === "background") sdk2.onBackground();
|
|
3902
|
+
else sdk2.flushAnalytics();
|
|
3734
3903
|
});
|
|
3735
3904
|
}
|
|
3736
3905
|
return () => {
|