Ir al contenido

Analytics

El SDK de Popups de Deepdots incluye una capa de analytics integrada que recopila datos de comportamiento de tus usuarios y los envía a una integración dedicada en tu workspace de Deepdots. Esto te permite medir engagement, patrones de navegación y eventos clave de negocio sin añadir una herramienta de analytics separada.

Añade un objeto analytics en init() con el publicKey y el ID de integration de la integración creada en tu workspace de Deepdots. Sin él, el SDK funciona en modo dry-run — todos los eventos se registran en la consola pero no se envía nada.

import { DeepdotsPopups } from '@magicfeedback/popup-sdk';
const popups = new DeepdotsPopups();
popups.init({
apiKey: 'TU_API_KEY_PÚBLICA',
analytics: {
publicKey: 'TU_PUBLIC_KEY_DE_ANALYTICS',
integration: 'TU_ID_DE_INTEGRACIÓN',
},
});

Los siguientes datos se recopilan sin ningún código adicional mientras el SDK esté inicializado:

DatoCómoDónde aparece
Vistas de pantalla (deepdots_page_view)History API (pushState / popstate / hashchange)Eventos
Tiempo activo de engagement (deepdots_user_engagement)Listener de visibilitychangeEventos
Identidad persistente del usuario (user_id)Generada en la primera visita, guardada en localStorageMetadata
Tipo de dispositivoCalculado a partir del User-Agent (mobile / tablet / desktop)Contexto
User agentnavigator.userAgentContexto
Idioma (deepdots_language)Detectado automáticamente (ver Detección de idioma)Contexto
Versión de la appappVersion pasado en init()Contexto

Cada flush (pestaña oculta, página cerrada o flushAnalytics() manual) envía los eventos acumulados como un lote. El backend agrupa los lotes por sesión para que veas una única línea de tiempo por visita de usuario, no un registro por flush.

El idioma que se reporta en el contexto de analytics — enviado como deepdots_language en el metadata del Feedback — se resuelve automáticamente, en este orden:

  1. El language pasado a init() — una etiqueta BCP-47 explícita como 'es-ES'. Úsalo cuando tu app tiene su propio i18n y quieres forzar el idioma reportado.
  2. navigator.language — el idioma del navegador (web).
  3. El locale de Intl (Intl.DateTimeFormat().resolvedOptions().locale) — el fallback usado cuando navigator.language no está disponible. Es lo que hace que la detección funcione en React Native con Hermes, donde navigator.language no existe.
  4. Si nada de lo anterior resuelve, el campo se omite.
popups.init({
apiKey: 'YOUR_PUBLIC_API_KEY',
analytics: { publicKey: 'YOUR_ANALYTICS_PUBLIC_KEY', integration: 'YOUR_INTEGRATION_ID' },
language: 'es-ES', // opcional — fuerza el idioma de analytics; se detecta automáticamente si se omite
});

El idioma resuelto es también aquello contra lo que se compara la segmentación por idioma de los popups (segments.lang), así que fijar language explícitamente fija ambas cosas a la vez. En React Native esto requiere 1.1.8 o superior — ver React Native → Segmentación por idioma.


Una sesión es una visita continua. El id de sesión es propiedad del backend, que cose los lotes por user_id, así que una visita se lee como una única línea temporal en lugar de un registro por flush.

Desde la 1.2.0 los dos extremos de la sesión se señalizan explícitamente:

  • deepdots_session_start — en cada apertura de sesión. Es decir: init(), la vuelta a primer plano, dar consentimiento con setTrackingEnabled(true) y después de un cambio de usuario. Si inicializas con trackingEnabled: false, la primera sesión se abre al dar el consentimiento.
  • deepdots_session_end — al cerrar, con un reason. El lote de cierre se envía con completed: true, que es lo que le indica al backend que el registro está terminado.

El lote de cierre hace flush de todo lo que quedaba abierto, en orden: el deepdots_page_view de la pantalla actual, cualquier deepdots_mini_service_exit pendiente, el deepdots_user_engagement acumulado y, por último, deepdots_session_end. Nada queda esperando un flush que no llegará.

reasonCuándo
page_hideSe cierra la página (pagehide) — web
backgroundLa app pasa a segundo plano (onBackground()) — React Native
user_changesetUserId() cambió de usuario
tracking_disabledsetTrackingEnabled(false)
manualendSession()

Cierra la sesión explícitamente. Úsalo al hacer logout o al terminar un flujo autocontenido, cuando la visita ha acabado pero la página o la app no:

popups.endSession();

El siguiente evento trackeado abre una sesión nueva.

Reporta un cambio de usuario — login, logout o cambio de cuenta. Cierra la sesión del usuario anterior con reason: 'user_change', intercambia la identidad y abre una sesión nueva, de forma que los dos usuarios nunca comparten línea temporal:

// Login: atribuye lo que viene a tu propio id de usuario
popups.setUserId('customer-123');
// Logout: vuelta al id anónimo del SDK
popups.setUserId();

Usa track(name, params?) para registrar cualquier evento de negocio. Los nombres de evento son cadenas libres — usa snake_case en minúsculas para mantener consistencia con los eventos automáticos.

popups.track('add_to_cart', { product_id: 'p-123', value: 49.9, currency: 'EUR' });
popups.track('checkout_started');
popups.track('plan_upgraded', { plan: 'pro', billing: 'annual' });

trackSearch registra una consulta de búsqueda junto con el número de resultados. El SDK añade automáticamente has_results: boolean a partir del recuento.

popups.trackSearch('zapatillas running', 0); // sin resultados — has_results: false
popups.trackSearch('camiseta', 142); // has_results: true

Registra los momentos en que los usuarios tienen dificultades para encontrar lo que buscan:

popups.trackFindabilityFriction('checkout_address');
popups.trackFindabilityFriction('comparacion_planes');

Rastrea los pasos dentro de un funnel con nombre. Agrupa los pasos relacionados bajo el mismo funnel y taskId para que el backend pueda calcular tasas de conversión:

popups.trackFunnelStep('onboarding', 'cuenta_creada', 'task-42');
popups.trackFunnelStep('onboarding', 'perfil_completado', 'task-42');
popups.trackFunnelStep('onboarding', 'primer_popup_visto', 'task-42');

Registra una interacción significativa: un momento que indica que el usuario obtuvo valor real de tu app. interactionType es la dimensión de agrupación, así que mantén un conjunto de nombres pequeño y estable (get_help, homepage, contact_support):

popups.trackMeaningfulInteraction('get_help');
popups.trackMeaningfulInteraction('homepage', { screen: '/home' });

Cada llamada emite un evento deepdots_meaningful_interaction que alimenta el panel de Effectiveness.


Un mini-servicio es cualquier flujo acotado dentro de tu app (proceso de pago, asistente de onboarding, chat de soporte). El SDK rastrea la entrada, la salida y la duración automáticamente una vez que señalizas los límites:

// El usuario entra en el flujo de pago
popups.enterMiniService('checkout', 'banner_home');
// … el usuario completa o abandona el flujo …
// El usuario sale — pasa el mismo nombre; la duración se calcula automáticamente
popups.exitMiniService('checkout');

Puede haber varios mini-servicios activos a la vez (p. ej. un chat de soporte abierto durante el pago). Cierra siempre cada uno por nombre para que el flujo correcto reciba su deepdots_mini_service_exit y su duración:

popups.enterMiniService('checkout', 'banner_home');
popups.enterMiniService('support_chat', 'fab'); // ambos activos ahora
popups.exitMiniService('checkout'); // cierra checkout; support_chat sigue abierto

Cualquier encuesta mostrada mientras un mini-servicio está activo recibe automáticamente una etiqueta de metadata mini_service (el más reciente), lo que te permite filtrar resultados de CSAT por contexto de flujo en Deepdots.


Llama a setUserAttributes para adjuntar atributos de negocio al contexto de analytics del usuario. Se incluyen en cada flush posterior.

popups.setUserAttributes({
plan: 'pro',
registration_status: 'registered',
sector: 'retail',
});

Los atributos son acumulativos — cada llamada se fusiona con los anteriores.

setContactAttributes envía los atributos a POST /sdk/popups/contact, creando o actualizando el registro de contacto del usuario en Deepdots. Este endpoint solo se llama cuando se proporcionó un userId en init() y el tracking está activado.

const sent = await popups.setContactAttributes({ language: 'es', age: 34, plan: 'premium' });
// sent: true si se hizo un POST, false si los atributos no han cambiado (deduplicación)

También puedes pasar contactAttributes directamente en init() para disparar la actualización de contacto al arranque:

popups.init({
apiKey: 'TU_API_KEY_PÚBLICA',
userId: 'user-123',
contactAttributes: { plan: 'premium', language: 'es' },
});

Llama a setMetric(key, value) para registrar un valor medible — una cantidad que quieres reportar junto al contexto de analytics del usuario, como el valor del carrito o el número de artículos en él.

popups.setMetric('cart_value', 49.99);
popups.setMetric('items_in_cart', 3);

La firma es:

setMetric(key: string, value: string | number | boolean): void

Las métricas aterrizan en un campo dedicado metrics del payload de analytics (POST /sdk/feedback), separado de metadata y de los atributos de usuario.

  • Persistente — una vez fijado, el valor se reenvía en cada flush hasta que cambie.
  • Sobrescribe por key — volver a llamar a setMetric con la misma key reemplaza el valor anterior.
  • Se coerciona a string — el valor se almacena como string en el envío (49.99"49.99").
  • Las keys vacías se ignoran — una llamada con key vacía es un no-op.
  • Respeta el kill-switch — es un no-op mientras el tracking está desactivado (ver Privacidad y consentimiento).

Ambos adjuntan contexto al usuario, pero responden a preguntas distintas:

setUserAttributessetMetric
RepresentaDimensiones para desglosarValores medibles a reportar
Ejemploplan: 'pro', sector: 'retail'cart_value: 49.99, items_in_cart: 3
Campo del payloadmetadatametrics

Usa los atributos para el quién — las categorías por las que filtras y agrupas — y las métricas para el cuánto — las cantidades que mides.


Rastrea el ciclo de vida de las notificaciones de tu app (push e in-app) para que Deepdots pueda medir entrega, click-through y conversión por mensaje. Usa un único método, trackMessage(stage, options), en cada etapa del funnel del mensaje:

// La notificación se entregó (push recibida, o mensaje in-app mostrado)
popups.trackMessage('delivered', { id: 'msg-42', title: 'Rebajas de verano', channel: 'push', campaign: 'summer_sale' });
// El usuario la pulsó / hizo click
popups.trackMessage('clicked', { id: 'msg-42', title: 'Rebajas de verano', channel: 'push' });
// El usuario completó la acción prevista (p. ej. compró)
popups.trackMessage('converted', { id: 'msg-42', title: 'Rebajas de verano', channel: 'push', value: 49.9, currency: 'EUR' });
CampoTipoDescripción
stage (1er arg)'delivered' / 'clicked' / 'converted'Etapa del funnel del mensaje
idstringCorrelaciona las etapas del mismo mensaje
titlestringDimensión de agrupación de las métricas de Messaging
channel'push' / 'in_app'Canal de entrega
campaignstring?Nombre de la campaña (opcional)
value / currencynumber / stringValor de conversión (típico en converted)
paramsobject?Pares clave/valor adicionales

Cada llamada emite un evento deepdots_message; el backend agrupa por title (y desglosa por estado de registro / canal) para calcular entregas, CTR, usuarios únicos con click, tasa de conversión y usuarios que realizaron una acción.

El CTR y la tasa de conversión son ratios sobre delivered. Si las etapas no encajan, esas métricas salen mal — y si falta delivered salen valores imposibles, porque el denominador es cero.

  1. Envía las tres etapas. delivered se manda cuando el mensaje llega al dispositivo, antes de que el usuario lo abra — en mensajes in-app, cuando se renderiza. Sin él no hay denominador.
  2. Usa el mismo id en las tres etapas. Es lo que correlaciona el funnel, y debe ser único por envío, no por campaña.
  3. Un id, un canal. Si una campaña sale como push y como mensaje in-app, usa dos id distintos que compartan el mismo campaign.
  4. Una llamada por etapa. Si tu handler de click puede ejecutarse por dos rutas — abrir la notificación más un deep link — asegúrate de que solo una emite clicked.

Desde la 1.2.0 el SDK descarta las llamadas que rompen estas reglas en lugar de reenviarlas, y avisa por consola:

[DeepdotsPopups] trackMessage descartado (channel_conflict): message_id "msg-42" ya se reportó en channel "push"; se descarta "in_app"
ReglaQué se descartareason
channel solo puede ser push o in_appCualquier otro valorinvalid_channel
Cada par (id, stage) se envía una vezLa 2ª llamada a la misma etapa del mismo mensajeduplicate_stage
Un id conserva su canalEventos de un canal distinto al primero vistochannel_conflict

Las comprobaciones tienen vigencia de sesión y son por dispositivo, y vigilan hasta 500 ids de mensaje (se descartan primero los más antiguos). Una llamada rechazada no consume estado: tras un channel_conflict en in_app, la misma etapa en el canal correcto sí se envía.

Si ves estos avisos mientras integras, están señalando un doble conteo real — corrige el punto de llamada en vez de ignorarlos.


El SDK captura errores de la aplicación y los envía como eventos deepdots_app_crash, que alimentan las métricas de Stability (usuarios sin crashes, crashes por versión y dispositivo). En cada apertura de sesión se emite un evento deepdots_session_start para que el backend pueda calcular la tasa de sesiones sin crash.

Los errores no manejados se capturan automáticamente — en web vía window.onerror / unhandledrejection, y en React Native vía global.ErrorUtils (cableado por setupReactNative). Los crashes capturados se persisten localmente y se reenvían en el siguiente arranque, porque el proceso puede morir antes del siguiente flush — así el crash que terminó una sesión igualmente llega a Deepdots.

Usa reportError para errores manejados, con severidad opcional y contexto libre:

try {
await checkout();
} catch (e) {
popups.reportError(e, { severity: 'error', context: { screen: 'Checkout', order_id: 'o-42' } });
}
OpciónValoresPor defecto
severity'fatal' / 'error' / 'warning''error'
handledbooleantrue
contextmapa libre clave/valor (prefijado ctx_ en el payload)

El contexto del crash (versión de la app, OS, dispositivo) se captura en el momento del crash, así que un crash en una versión antigua sigue reportando la versión en la que ocurrió.

El reporte de crashes respeta el mismo kill-switch de consentimiento que el resto de la analítica (trackingEnabled / setTrackingEnabled).


Establece trackingEnabled: false en init() para arrancar con toda la analítica y el tracking de contacto desactivados — útil cuando necesitas consentimiento explícito del usuario antes de recopilar datos.

popups.init({
apiKey: 'TU_API_KEY_PÚBLICA',
trackingEnabled: false,
});
// Más tarde, cuando el usuario da su consentimiento:
popups.setTrackingEnabled(true);

setTrackingEnabled(false) cierra la sesión actual con reason: 'tracking_disabled' y suspende todas las llamadas salientes (analytics, contacto) — los datos recogidos antes del opt-out se entregan, no se descartan. setTrackingEnabled(true) las reanuda, asigna un user_id persistente si aún no había uno almacenado y abre una sesión nueva.


En React Native, dos comportamientos automáticos requieren integración explícita del host:

Como la History API no está disponible, reporta los cambios de pantalla manualmente después de cada evento de navegación:

// En el callback onStateChange de React Navigation:
popups.setScreen(route.name);

Conecta el SDK al ciclo de vida de primer plano/fondo de la app para que el tiempo de engagement se mida correctamente y los eventos se envíen cuando la app pasa a segundo plano:

import { AppState } from 'react-native';
AppState.addEventListener('change', (state) => {
if (state === 'active') popups.onForeground();
else popups.onBackground(); // cierra la sesión y hace flush
});

Durante el desarrollo, inspecciona el buffer de eventos actual sin hacer flush:

const preview = popups.previewAnalytics();
console.log(preview.events); // todos los eventos en cola desde el último flush

Para forzar un flush manualmente (útil para testing):

popups.flushAnalytics();

Los flushes ocurren automáticamente — cada 30 s en primer plano, cuando el buffer llega a 20 eventos, cuando se oculta la pestaña y cuando se cierra la página o la app. Rara vez necesitas llamar a flushAnalytics() tú. A partir de 1.1.8 el canal está endurecido para que no se pierda el último lote de una visita — el que lleva el deepdots_page_view y el deepdots_user_engagement de cierre:

  • Sobrevive a la navegación y al cierre — la petición usa keepalive, y el flush final al cerrar la página pasa a navigator.sendBeacon. Los navegadores ya no la cancelan a medias.
  • Reintenta los fallos transitorios — un error de red o un 5xx / 408 / 429 devuelve el lote al principio del buffer, en orden cronológico, para reintentarlo en el flush siguiente. Se retienen hasta 200 eventos; por encima se descartan los más antiguos.
  • Reporta los fallos permanentes — un 4xx (por ejemplo un 406 por un Contact desconocido) se loguea con su status y el cuerpo de la respuesta, y el lote se descarta, en lugar de fallar en silencio.
  • Mantiene un registro por visita — hasta que el backend devuelve un id de sesión, los lotes se serializan en vez de enviarse en paralelo, así una visita no se parte en dos registros.

flushAnalytics() acepta un flag final, que es el que usa el SDK internamente al cerrar la página. Pásalo solo si estás implementando tu propio camino de cierre — prefiere sendBeacon y no espera la respuesta:

popups.flushAnalytics({ final: true });