Integración del SDK de Android
SDK nativo de Android para rastrear eventos, gestionar sesiones de usuario, enviar notificaciones push mediante FCM y mostrar mensajes in-app.
Funciones
- Ligero: ocupa poco espacio.
- Rápido: optimizado para el rendimiento.
- Compatibilidad sin conexión: cola de eventos basada en SQLite.
- Reintento automático: espera exponencial ante fallos.
- Procesamiento por lotes: agrupación eficiente de eventos (50 eventos / 5 s).
- Mensajería in-app: mensajes nativos y HTML, 5 tipos, con límite de frecuencia.
- Notificaciones push: Firebase Cloud Messaging (FCM).
- Privacidad primero: cumple el RGPD y respeta el consentimiento de los usuarios.
- Android 6.0+: compatible con API 23+.
Requisitos
- Android 6.0 (nivel de API 23) o superior: el mínimo para el almacenamiento cifrado en reposo (
EncryptedSharedPreferences) y el mismo que exige Firebase Cloud Messaging. - Kotlin 1.9.20 o superior.
- Java 17 para compilar con Android Gradle Plugin 8. Las clases del SDK son bytecode de Java 11, así que una compilación con JDK 11 (Unity 2022.3) también compila contra ellas.
- Gradle 8.0 o superior.
Configuración local del SDK
Para compilaciones locales, define la ruta del SDK de Android en local.properties:
sdk.dir=/Users/your-user/Library/Android/sdk
También puedes exportar ANDROID_HOME/ANDROID_SDK_ROOT antes de ejecutar Gradle.
Instalación
Gradle (recomendado)
Añade esto a build.gradle.kts:
El SDK se distribuye en dos artefactos. Añade uno de ellos: el artefacto de UI contiene el base, así que nunca declaras los dos:
dependencies {
// Todo, incluida la visualización de mensajes in-app. Empieza aquí.
implementation("io.joryio:joryio-android-ui:1.3.3")
}
dependencies {
// Solo tracking, identidad y push: sin renderizado in-app y sin WebView
// enlazado. Elige esto si tu app no usa mensajes in-app, o si los
// renderizas tú mismo.
implementation("io.joryio:joryio-android:1.3.3")
}
O con Groovy (build.gradle):
dependencies {
implementation 'io.joryio:joryio-android-ui:1.3.3'
}
joryio-android-ui depende de joryio-android, así que una línea te da ambos y
no pueden desincronizarse: solo nombras una versión.
Usa el artefacto base solo cuando tu app no necesite mensajes in-app, o cuando una revisión de seguridad se oponga a que haya un WebView enlazable. Los mensajes in-app simplemente no se mostrarán; todo lo demás funciona igual.
Inicio rápido
1. Inicializa el SDK
En tu clase Application:
import io.joryio.sdk.Joryio
import io.joryio.sdk.JoryioConfig
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Inicializa con tu clave de SDK
Joryio.initialize(
context = this,
sdkKey = "jry_sdk_android_YOUR_SDK_KEY",
apiHost = "https://api-eu1.joryio.com/api"
)
}
}
Encuentra tu clave de SDK en el dashboard de Joryio: Configuración → Apps → [Tu app] → Claves de SDK.
2. Rastrear eventos
// Evento básico
Joryio.track("Button Tapped")
// Evento con propiedades
Joryio.track("Product Viewed", mapOf(
"product_id" to "abc123",
"product_name" to "Wireless Headphones",
"price" to 99.99,
"category" to "Electronics"
))
// Vista de pantalla
Joryio.trackScreen("ProductDetail", mapOf(
"product_id" to "abc123"
))
3. Identificar usuarios
Envía siempre los números de teléfono en formato internacional completo (E.164): +, el código de país y luego el número - por ejemplo +972501234567. Un número sin + se lee con el país predeterminado para números de teléfono del espacio de trabajo; si no hay ninguno, o los dígitos se pueden leer de dos formas, se guarda sin verificar y no se usa para mensajes. Cómo se tratan los números de teléfono
// Identifica un usuario
Joryio.identify("user-123")
Joryio.getInstance().setAttributes(mapOf(
"name" to "John Doe",
"plan" to "premium"
))
// Establece atributos más adelante
Joryio.setAttribute("last_purchase", Date())
Joryio.incrementAttribute("lifetime_value", 99.99)
// Asocia otro identificador tuyo (etiqueta: minúsculas, dígitos, _).
// Funciona también antes de identify(): acompaña al visitante al iniciar sesión.
Joryio.addAlias("loyalty_id", "L-12345")
// Al cerrar sesión: olvida al usuario Y empieza un nuevo ID anónimo
Joryio.reset()
identify es la forma en que un visitante anónimo se convierte en un usuario conocido; no existe una llamada alias(userId) aparte. reset() también descarta las escrituras de atributos aún no enviadas y cualquier mensaje in-app pendiente de mostrarse, vuelve a registrar el token push con el nuevo ID anónimo y sincroniza de nuevo los mensajes in-app, de modo que la siguiente persona en el dispositivo nunca se registra como la anterior.
Las llamadas a Joryio hechas antes de que termine la inicialización se ponen en cola y se ejecutan en orden cuando termina, tanto con initialize como con initializeAsync. Ninguna llamada pública lanza una excepción en tu código: un argumento ausente (un null desde Java) se registra en el log y se ignora. La única excepción es Joryio.getInstance() antes de initialize(); consulta más abajo.
Datos automáticos de sesión
El SDK de Android enriquece los eventos Session Start con datos de dispositivo y entorno:
$device_id$platform(android)$manufacturer/$model$os_name/$os_version/$os_sdk_int$app_version/$build_number$package_name$screen_width/$screen_height$locale$language/$languages$timezonecountry(ISO-3166-1 alfa-2, derivado de la IP al iniciar la sesión)
Notificaciones push
Pide el permiso cuando la persona tenga un motivo para aceptar. Una solicitud en los primeros segundos de una sesión es la que más se rechaza, y revertir un rechazo es mucho más difícil que retrasar la petición: la solicitud del sistema es de una sola vez, así que un "no" ahí es prácticamente permanente, mientras que pedirlo más tarde no te cuesta nada.
El SDK nunca muestra la solicitud por ti. Si has decidido que la quieres al
arrancar, activa requestPushPermissionAtLaunch: está desactivada por defecto,
no ausente.
Activa las notificaciones push con Firebase Cloud Messaging (FCM).
1. Añadir Firebase a tu proyecto
Sigue la guía de configuración de Firebase para añadir Firebase a tu proyecto de Android.
2. Añadir el servicio a AndroidManifest.xml
<service
android:name="io.joryio.sdk.push.JoryioFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
3. Registrar notificaciones push
import com.google.firebase.messaging.FirebaseMessaging
// Obtén el token de FCM y regístralo
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (task.isSuccessful) {
val token = task.result
Joryio.getInstance().registerPushToken(token) { result ->
if (!result.success && result.isRetryable) {
// transitorio (sin conexión, DNS, 5xx): vuelve a registrar cuando vuelva la red
}
if (result.isUnauthorized) {
// clave del SDK incorrecta - reintentar no ayudará
}
}
}
}
// Comprueba si push está activado
val isEnabled = Joryio.getInstance().isPushEnabled()
// Anula el registro cuando sea necesario
Joryio.getInstance().unregisterPush()
Pasa onResult para saber cómo fue: isRetryable indica un fallo transitorio (sin conexión, DNS, 5xx) que merece un nuevo registro; isUnauthorized indica una clave del SDK incorrecta. Sin él la llamada es fire-and-forget. Un registro solo cuenta cuando el servidor lo confirma; una respuesta success: false se informa como fallo y se reintenta la próxima vez que la app pase a primer plano. Después de unregisterPush(), llamar a registerPushToken() vuelve a registrar el dispositivo. No pases a registerPushToken() las llamadas automáticas de token de Firebase (onNewToken() en tu propio servicio de mensajería, el token que se obtiene en cada arranque) mientras el usuario tenga el push desactivado: es la llamada que lo vuelve a activar. El servicio de mensajería del SDK gestiona los tokens nuevos por sí mismo y nunca deshace unregisterPush(), y un Joryio.unregisterPush() antes de initialize() prevalece sobre el token que Firebase entrega al arrancar.
requestPushPermission(activity) funciona con cualquier Activity. Cuando la tuya es un android.app.Activity simple (como en Unity), el SDK muestra el diálogo del sistema desde su propia actividad invisible y te avisa cuando el usuario responde. PushPermissionStatus.publicName da el estado con el nombre que usan todos los SDK de Joryio: granted, denied, notDetermined o unavailable.
El canal de notificaciones del SDK se llama "Notifications" en los ajustes del sistema. Define notificationChannelName en JoryioConfig para mostrar un nombre propio y localizado.
4. Configurar FCM en el dashboard
Para enviar notificaciones push, introduce tus credenciales de Firebase en la app Android de Joryio:
- Ve a Configuración → Apps y haz clic en el icono Notificaciones push (campana) de la fila de tu app Android.
- Haz clic en Upload service account JSON y elige la clave de cuenta de servicio de Firebase (consola de Firebase: Project settings → Service accounts → Generate New Private Key).
- Haz clic en Save configuration.
Para ver instrucciones paso a paso sobre cómo generar la clave, consulta Obtener tus credenciales de Firebase (FCM).
Mensajería in-app
El SDK muestra los mensajes in-app por ti. Una vez inicializado, las campañas elegibles aparecen solas y las impresiones, los clics y los descartes se registran automáticamente: no hay nada que conectar.
Tokens de entrega (delivery tokens)
Cuando el backend sirve una campaña elegible, emite también un token de entrega firmado y de corta duración. El SDK lo devuelve al informar una impresión, un clic o un cierre, y el servidor verifica la firma antes de registrar nada.
No tienes que hacer nada: el SDK se encarga por ti. Se documenta porque el servidor puede configurarse para exigirlo. Por defecto, un informe sin token sigue aceptándose - el servidor lo registra como un cliente anterior a los tokens - y solo cuando el backend se ejecuta con IN_APP_REQUIRE_DELIVERY_TOKEN=true ese mismo informe se rechaza:
POST /v1/in-app/track (no deliveryToken)
{ "success": false, "error": "A delivery token is required" }
Un token presente pero que no se verifica - falsificado, manipulado o caducado - se rechaza siempre, sea cual sea la configuración.
El token es lo que hace que una impresión sea fiable: sin él, cualquiera que tenga la SDK key - que viaja dentro de cada app y cada página - podría informar impresiones y clics de una campaña que nunca se mostró, y tus informes los contarían.
Si dejan de registrarse impresiones in-app en un backend que exige tokens, comprueba que la app esté compilada contra una versión actual del SDK: una compilación anterior a los tokens de entrega no envía token.
Dos tipos de contenido
Cada campaña llega en una de dos formas de contenido, y el SDK renderiza cada una de manera distinta:
| Contenido | Qué es | Cómo se renderiza |
|---|---|---|
| Nativo | Datos estructurados: titular, texto, imagen, botones | Vistas Android reales, con el tema, las fuentes, el modo oscuro y TalkBack de tu app. Sin WebView. |
| HTML | Marcado y CSS escritos por el autor, guardados tal cual (solo se quitan <base> y <meta http-equiv="refresh">) | Un WebView dentro del mensaje, con una Content-Security-Policy donde solo se ejecuta el script del puente del SDK. |
Permitir mensajes HTML
Los mensajes HTML están desactivados por defecto. Los scripts escritos en un mensaje no se ejecutan: cada mensaje recibe un nonce nuevo en su Content-Security-Policy, así que solo se ejecuta el script del puente del SDK - los <script>, los manejadores on* y los embeds del autor quedan inertes, y el puente gestiona los botones, los atributos data-joryio-action y los formularios data-jry-form. Sigue siendo un web view dentro de tu app, así que activarlo es una decisión del equipo de la app, no algo que se enciende desde una plataforma de marketing:
val config = JoryioConfig(
allowHtmlJsInAppMessages = true // por defecto: false
)
Joryio.initialize(
context = this,
sdkKey = "jry_sdk_android_YOUR_KEY",
apiHost = "https://api-eu1.joryio.com/api",
config = config
)
Dejarlo desactivado no desactiva la mensajería in-app. Los mensajes nativos siguen mostrándose, porque son datos que tu app dibuja con sus propias vistas, sin ningún intérprete. Las campañas HTML se omiten y se registran, de modo que una app que no lo ha activado no muestra nada en lugar de un mensaje roto.
Si tu política de seguridad prohíbe ejecutar HTML de terceros en el proceso, déjalo desactivado y crea tus campañas como mensajes nativos.
Encargarte tú del renderizado
Define un callback para dibujar tu propia interfaz. Sustituye al renderizado del SDK, así que no recibirás dos copias del mensaje:
Joryio.getInstance().setInAppMessageCallback { campaign ->
showInAppMessage(campaign) // tu interfaz
}
// Informa de lo ocurrido: el SDK solo registra automáticamente lo que muestra él
Joryio.getInstance().trackInAppImpression(campaignId, "displayed")
Joryio.getInstance().trackInAppImpression(campaignId, "clicked", "cta") // qué botón
Joryio.getInstance().trackInAppImpression(campaignId, "dismissed")
La acción es una de displayed, clicked, dismissed o converted; cualquier otra se registra en el log y no se envía. Solo displayed cuenta para el límite de frecuencia de la campaña - un clic o un descarte no consume otra visualización. El tercer argumento, elementId, indica el botón o enlace y se envía por separado de la acción.
Desde la versión 1.3.2. Si configuras el callback después de initialize(), la primera sincronización puede terminar antes y el SDK dibuja ese mensaje por su cuenta. Para evitarlo, pasa JoryioConfig(holdInAppMessagesForHandler = true): un mensaje que esté listo antes de configurar tu callback se retiene - no se dibuja, no se informa y no cuenta para ningún límite de frecuencia - y se entrega al callback en el momento en que lo configuras. Si en su lugar llamas a clearInAppMessageCallback(), la retención termina y el SDK dibuja el mensaje retenido. La retención solo cubre el tiempo hasta la primera configuración o eliminación.
Renderizar los mensajes tú mismo
El SDK dibuja los mensajes nativos, pero puedes tomar el control por completo: el equivalente a un custom view factory.
Joryio.getInstance().setInAppMessageCallback { campaign ->
// dibújalo como quieras
}
Definir un callback sustituye al renderizador integrado en vez de ejecutarse junto a él, así que el mensaje aparece una vez, no dos.
campaign.content ya viene resuelto: el Liquid se renderiza en el servidor y los
campos nativos llegan como texto plano (no los pases a un WebView; no llevan
escape HTML precisamente porque son para vistas de texto). Informa de lo que
muestres con trackInAppImpression(campaignId, "displayed" | "clicked" | "dismissed" | "converted") para que la analítica siga cuadrando.
Tipos de mensaje
El SDK admite 5 tipos de mensaje:
- Modal - Centro de la pantalla con fondo atenuado
- Banner - Parte superior de la pantalla
- Slide-Up - Notificación pequeña desde abajo
- Full-Screen - Mensaje a pantalla completa
- Custom - Tu app decide la ubicación
Cuándo un mensaje cuenta como mostrado
Un mensaje cuenta como mostrado - se envía in_app.displayed y se consume el límite de frecuencia de la campaña - solo cuando la persona pudo verlo de verdad. El tiempo que debe estar en pantalla es IN_APP_DISPLAY_DWELL_MS (1000 ms), la misma constante en todos los SDK de Joryio.
- El SDK muestra un mensaje solo en la Activity reanudada (resumed), y solo si no se está cerrando, no está destruida ni se está recreando (una rotación). Si no, el mensaje se guarda para la pantalla siguiente.
- El mensaje cuenta como mostrado después de estar 1 segundo en pantalla mientras la Activity sigue reanudada. Si la Activity se pausa antes, el segundo vuelve a empezar cuando se reanuda.
- Si la pantalla se cierra antes - una redirección tras iniciar sesión, un cierre de sesión - no se envía nada y el límite no se consume; el mensaje se ofrece en la pantalla siguiente.
- Un toque o un cierre antes cuenta como visto: primero se envía
displayedy después el clic o el descarte. Un reset, un cambio de usuario o una baja (opt-out) antes no envía nada. - Si renderizas los mensajes tú mismo: envía
displayedcon la misma regla - cuando el mensaje lleve visibleIN_APP_DISPLAY_DWELL_MS, o justo antes de un clic o un descarte que llegue antes. UnInAppMessagePresenterpropio envíaInAppMessagePresenter.EVENT_HIDDENcuando quita un mensaje sin que la persona haya actuado sobre él.
Opciones de configuración
Personaliza el comportamiento del SDK con JoryioConfig:
val config = JoryioConfig(
// ID de usuario inicial (opcional)
userId = "user-123",
anonymousId = null, // ID anónimo propio; por defecto se genera
// Procesamiento de eventos por lotes
batchSize = 50, // Eventos por lote (como máximo 500)
flushInterval = 5000, // Intervalo de vaciado en ms (5 s); nunca menos de 1000
sendImmediately = false, // Enviar cada evento al instante, sin lotes
maxQueueSize = 1000, // Máximo de eventos guardados en el dispositivo; se descartan primero los más antiguos
// Gestión de sesión
sessionTimeout = 1800000, // Tiempo de espera de sesión en ms (30 min), incluido el tiempo en segundo plano
trackSessionStart = true, // Rastrea automáticamente el inicio de sesión
// Reintentos de red
maxRetries = 3, // Reintentos inmediatos de una petición antes de esperar
retryBackoffMs = 1000, // Espera base entre reintentos (ms)
requestTimeout = 10000, // Tiempo máximo por petición HTTP (ms)
// Autenticación del SDK (opcional, JWT)
enableSdkAuthentication = false,
sdkAuthenticationToken = null,
// Controles de privacidad
optOut = false, // Empezar excluido del rastreo (se recuerda, como optOut())
trackingConsent = null, // null = sin definir; consulta Consentimiento de rastreo más abajo
// Push: pedir el permiso de notificaciones al primer paso a primer plano
requestPushPermissionAtLaunch = false,
// Mensajería in-app
// Permite mensajes in-app HTML, mostrados en un WebView. Sus scripts no se
// ejecutan; el puente del SDK gestiona botones y formularios. Los mensajes
// nativos se muestran siempre, sea cual sea este ajuste.
allowHtmlJsInAppMessages = false,
holdInAppMessagesForHandler = false, // since 1.3.2; true: your app draws messages and sets its callback after initialize()
// Push: el nombre del canal que ven los usuarios en los ajustes del sistema (Android 8+)
notificationChannelName = "Notifications",
// Depuración
enableDebug = false, // Activa el registro de depuración
logLevel = null // null (predeterminado): sigue a enableDebug. Fíjalo para forzar un nivel.
)
Joryio.initialize(
context = this,
sdkKey = "jry_sdk_android_YOUR_KEY",
apiHost = "https://api-eu1.joryio.com/api",
config = config
)
Consentimiento de rastreo
enum class TrackingConsent {
GRANTED, // Se permite el rastreo completo
PENDING, // A la espera de la decisión del usuario
DENIED // El usuario rechazó el rastreo
}
El consentimiento y la exclusión del rastreo son dos interruptores distintos, iguales en todos los SDK de Joryio:
| Interruptor | Lo define | Llamadas |
|---|---|---|
| Consentimiento de rastreo | tu banner de consentimiento | updateConsent(...), trackingConsent en la configuración |
| Exclusión del rastreo (opt-out) | la elección de la propia persona | optOut() / optIn(), optOut en la configuración |
El SDK solo recopila datos cuando el consentimiento es GRANTED y la persona no se ha excluido: cualquier "no" prevalece. El consentimiento nunca levanta una exclusión (solo lo hace optIn()), y optOut() nunca cambia el consentimiento guardado.
Joryio.updateConsent(TrackingConsent.GRANTED) // concedido: todo lo retenido se envía, en orden
Joryio.updateConsent(TrackingConsent.DENIED) // denegado: todo lo retenido se descarta
Joryio.updateConsent(TrackingConsent.PENDING) // pendiente: todo se retiene en el dispositivo
PENDINGguarda los eventos y atributos en el dispositivo (hastamaxQueueSize) y no envía nada - ni mensajes in-app, ni registro push - hasta queGRANTEDlos libera en orden oDENIEDlos descarta. Solo se aplica a este arranque y nunca anula unDENIEDguardado. Un toque en una notificación también se retiene y se informa conGRANTED.DENIEDno recopila nada y descarta lo retenido. Se recuerda en el dispositivo: sigue vigente entre reinicios hastaupdateConsent(TrackingConsent.GRANTED)o untrackingConsent = TrackingConsent.GRANTEDexplícito en la configuración. No excluye a la persona del rastreo:isUserOptedOut()sigue siendofalse.- Sin definir (
trackingConsent = null, el valor por defecto): unDENIEDguardado sigue vigente; si no lo hay, el SDK se comporta como conGRANTED, que es lo adecuado para una app sin banner de consentimiento.
Consulta el valor actual con getTrackingConsent().
Niveles de registro
enum class LogLevel {
VERBOSE, // Todos los registros
DEBUG, // Depuración y superiores
INFO, // Información y superiores
WARN, // Advertencias y errores
ERROR // Solo errores
}
logLevel es null por defecto, que significa "seguir a enableDebug": DEBUG con la depuración activada, ERROR con ella desactivada. Fíjalo explícitamente solo para forzar un nivel - un LogLevel.ERROR explícito con enableDebug = true deja el SDK en silencio.
Funciones avanzadas
Gestión de sesión
Las sesiones rastrean automáticamente la interacción del usuario:
// Las sesiones se gestionan automáticamente con un tiempo de espera de 30 minutos
// Obtén el ID de sesión actual
val sessionId = Joryio.getInstance().getSessionId()
// Las sesiones se actualizan con la actividad del usuario
El tiempo en segundo plano cuenta para el tiempo de espera: una app que vuelve a primer plano tras más de sessionTimeout empieza una sesión nueva y envía un nuevo Session Start.
Atributos de usuario
// Establece varios atributos
Joryio.getInstance().setAttributes(mapOf(
"age" to 28,
"city" to "San Francisco",
"premium" to true
))
// Establece un atributo
Joryio.setAttribute("language", "en")
// Incrementa un atributo numérico
Joryio.incrementAttribute("page_views", 1)
Joryio.incrementAttribute("total_spent", 29.99)
// Elimina un atributo
Joryio.getInstance().unsetAttribute("temporary_flag")
Controles de privacidad
// Deja de recopilar. PERSISTENTE: sobrevive a un reinicio de la app.
Joryio.optOut()
// Volver a aceptar el rastreo: la única llamada que levanta una exclusión
Joryio.optIn()
// Solo el interruptor de exclusión; el consentimiento es getTrackingConsent()
if (Joryio.isUserOptedOut()) {
Log.i("MyApp", "User has opted out")
}
// Borra todo lo que el SDK guardó en este dispositivo.
// DISTINTO de optOut(): "deja de recopilar" y "borra lo que tienes" son
// peticiones diferentes. Esta es la que necesita una solicitud de supresión.
// NO excluye al usuario del rastreo: llama también a optOut() si es lo que quieres.
Joryio.wipeData()
// Obtén la información de identidad
val (userId, anonymousId) = Joryio.getInstance().getIdentity()
println("User: ${userId ?: "anonymous"}, Anonymous ID: $anonymousId")
Qué hace optOut(), con precisión:
| detiene la recopilación | sí - cada escritura deja de tener efecto: eventos, identify, atributos (incluidos incrementos y eliminaciones), alias, altas de suscripción, registro push, confirmaciones de entrega y seguimiento de clics, informes in-app. Las bajas de suscripción y la baja del registro push sí se envían |
| sobrevive a un reinicio | sí - el indicador se guarda en el dispositivo y se lee antes de recopilar nada |
| descarta lo que ya está en cola | sí - los eventos en cola y las escrituras de atributos sin confirmar se descartan, no se envían más tarde. Una baja de suscripción aún no entregada se conserva y se envía |
| avisa al servidor | sí - el atributo de perfil tracking_opted_out, enviado cuando el interruptor cambia (también para una exclusión decidida en la inicialización o antes) y reintentado hasta que el servidor lo tiene, sea cual sea el consentimiento para true (el false de optIn() espera al consentimiento GRANTED) |
| borra los datos guardados | no - usa wipeData() |
El atributo tracking_opted_out es un registro, no un mecanismo de bloqueo:
llega al perfil para que las campañas puedan excluir a quien lo tenga. La
supresión en el servidor es un ajuste aparte.
Vaciado manual de la cola
// Vacía los eventos de inmediato
Joryio.flush()
// Útil antes de terminar la aplicación
override fun onDestroy() {
super.onDestroy()
Joryio.flush()
}
Prácticas recomendadas
1. Inicializar pronto
Inicializa en tu clase Application:
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
Joryio.initialize(
context = this,
sdkKey = "jry_sdk_android_YOUR_KEY",
apiHost = "https://api-eu1.joryio.com/api"
)
}
}
2. Rastrear vistas de pantalla
Usa el rastreo de pantallas para la navegación:
override fun onResume() {
super.onResume()
Joryio.trackScreen(this::class.simpleName ?: "Unknown")
}
3. Gestionar el cierre de sesión del usuario
Restablece siempre al cerrar sesión:
fun logout() {
// Borra la sesión de usuario
clearUserSession()
// Restablece el SDK
Joryio.reset()
}
Envío, consentimiento y ciclo de vida de los datos
Las escrituras se guardan hasta que el servidor las confirma. identify, addAlias, incrementAttribute, unsetAttribute, setSubscription y las llamadas de grupos de suscripción se guardan en una bandeja de salida cifrada en el dispositivo y se envían en el orden en que se hicieron. Un fallo - sin conexión, un timeout, un 5xx, un 429 o un 200 que responde success: false - conserva la escritura y la reintenta con espera creciente, también después de reiniciar. Se respeta Retry-After, en segundos o como fecha, e incluso flush() espera a que pase. Una escritura que el servidor nunca aceptará - un 4xx como 400, 409, 413 o 422, o un 403 de tu política de identidad - se descarta con una línea en el log, para que no bloquee las siguientes. Estar sin conexión, un timeout, un 429 o un 5xx nunca cuentan contra una escritura. Un 401 o un 200 con success: false se reintenta como máximo diez veces para esa escritura, y un 404 (todavía no hay perfil) como máximo cinco: un cambio de suscripción respondido con 404 se reintenta después de enviar los eventos en cola, porque son ellos los que crean el perfil. Los valores que envía setAttributes tienen las mismas diez respuestas. Las llamadas de atributos sobre la misma clave se aplican en el orden en que las hiciste, tanto si usaste setAttribute, unsetAttribute como incrementAttribute. setAttributes tiene su propia cola duradera, descrita más arriba; allí también se descartan los atributos que el servidor rechaza como no válidos (400).
Límites de los eventos. El servidor rechaza un evento cuyas propiedades ocupan más de 50 KB, tienen más de 200 claves o se anidan a más de 5 niveles, o cuyo nombre está vacío o supera los 255 caracteres. track() lo comprueba en el dispositivo, midiendo el tamaño como lo mide el servidor (un carácter como < o & cuenta una vez), y descarta ese evento con una línea en el log, en vez de poner en cola algo que nunca podrá entregarse. Un nombre formado solo por espacios es un nombre válido. Un evento que el servidor aun así rechaza de forma definitiva se descarta, no se reintenta.
Un valor null elimina un atributo. setAttribute("plan", null) es lo mismo que unsetAttribute("plan"), en todos los SDK de Joryio.
Sesiones. Una sesión termina tras sessionTimeout sin actividad, tanto si la app estaba en segundo plano como si siguió abierta en una pantalla. El primer evento después de eso inicia una sesión nueva y envía antes su Session Start. reset() también inicia una sesión nueva. Un arranque dentro de sessionTimeout desde la última actividad continúa esa sesión y no envía un nuevo Session Start. Una sesión que empezó mientras el seguimiento estaba bloqueado (opt-out o consentimiento denegado) no continúa así: el primer arranque al que se le permite hacer seguimiento inicia una sesión nueva con su Session Start.
Consentimiento al iniciar. optOut = true funciona como optOut(): se recuerda en el dispositivo, así que un inicio posterior sigue excluido hasta optIn(). trackingConsent = TrackingConsent.DENIED también se recuerda, como consentimiento (consulta Consentimiento de rastreo). En ambos casos se descarta lo que un inicio anterior dejó en cola. Joryio.optOut(), Joryio.optIn(), Joryio.updateConsent() y Joryio.wipeData() llamados antes de initialize() los aplica el propio initialize(), antes de recopilar o enviar nada, y hasta entonces se guardan en el dispositivo, de modo que una decisión tomada antes de que arrancara el SDK sobrevive a que se cierre la app. Antes de initialize(), Joryio.isUserOptedOut() y Joryio.getTrackingConsent() responden lo que está guardado en el dispositivo (una exclusión o una denegación de un inicio anterior), no un valor por defecto. Un Joryio.updateConsent(TrackingConsent.PENDING) antes de initialize() vale solo para ese inicio y nunca reabre una denegación, y un Joryio.optIn() o Joryio.optOut() antes de initialize() prevalece sobre optOut en la configuración. Sin nada guardado, Joryio.getTrackingConsent() responde GRANTED, igual que en iOS.
Autenticación del SDK. Con enableSdkAuthentication activado, el handler definido con setSdkAuthenticationErrorHandler se llama en cada 401 de autenticación, también cuando todavía no se ha definido ningún token (motivo MISSING): proporciona uno con setSdkAuthenticationToken. Los eventos se envían en lotes de un solo usuario cada vez; los eventos o escrituras de un usuario que ya cerró sesión y que el servidor rechaza por cualquier motivo de autenticación se descartan, para que nunca retrasen los del siguiente usuario. El handler solo se llama para peticiones del usuario que tiene la sesión iniciada ahora, nunca por restos de un usuario que cerró sesión. reset() saca el token de la sesión; se queda con lo que el usuario saliente dejó en cola. Cada petición lleva el token cuyo sub es el usuario de esa petición - nunca uno que nombre a otra persona - y una petición de un visitante anónimo no lleva ninguno. Un token definido para el siguiente usuario antes de reset(), también cuando no hay nadie con la sesión iniciada, se guarda para él y se vuelve a usar en su identify(); wipeData() lo borra. Si no, define el del siguiente usuario con setSdkAuthenticationToken cuando inicie sesión. Los eventos y escrituras que el usuario saliente ya había puesto en cola siguen enviándose con su propio token, así que un cierre de sesión normal no pierde nada.
Antes de la inicialización. Las llamadas a Joryio hechas antes de que termine initialize() se guardan - hasta 1000, se descartan primero las más antiguas - y se ejecutan en orden. Un evento, una escritura de atributo, un alias o una suscripción hechos a través de ellos mientras la persona ha hecho opt-out o el consentimiento está denegado - decidido antes de initialize() o guardado por un arranque anterior - se descarta en el momento, así que un optIn() o una concesión posteriores nunca lo envían; las bajas, identify() y reset() se conservan. Un optOut() o updateConsent(DENIED) antes de initialize() también descarta los eventos, escrituras de atributos, alias y suscripciones puestos en cola antes, igual que después de la inicialización, así que un optIn() o una concesión posteriores antes de initialize() nunca los envían. Un token fijado con Joryio.setSdkAuthenticationToken() antes de cualquier identify(), reset() o wipeData() lo aplica el propio initialize(), antes de su primera petición. Define el token y el handler con los métodos estáticos Joryio.setSdkAuthenticationToken() y Joryio.setSdkAuthenticationErrorHandler(), no dentro de whenReady: solo ellos se toman antes de la primera petición y se conservan tras un wipeData() hecho antes de initialize(), así que las bajas del usuario borrado siguen saliendo con su token. Joryio.getInstance() es la única llamada que lanza una excepción (IllegalStateException) antes de initialize(); donde la inicialización quizá no haya terminado, usa los métodos estáticos o Joryio.whenReady { sdk -> ... }.
Cuenta en pausa (402). Un 402 significa que la ingesta de datos de tu cuenta está en pausa. Nunca cuenta para el límite de reintentos de una escritura y no retiene las retiradas: una baja de suscripción, una salida de grupo y el registro tracking_opted_out = true que esperan detrás de una escritura rechazada se siguen enviando, mientras las demás escrituras esperan y se reintentan con espera creciente.
Cierre de sesión y cambio de cuenta. El token de autenticación del SDK del usuario saliente se guarda en el almacenamiento cifrado junto con sus eventos y escrituras en cola, así que se entregan con él aunque la app se cierre antes de enviarlos; se borra una vez entregados. Un identify() de otro usuario sin reset() se trata igual para el usuario anterior: sus atributos no enviados salen con sus propios identificadores y el token va con lo que le queda en cola - salvo que ya hayas definido el token del nuevo usuario, que el SDK reconoce por su sub. Una baja de suscripción en cola solo la sustituye un cambio más reciente del mismo usuario.
identify() con exclusión o consentimiento denegado. El identificador de usuario se guarda solo en el dispositivo - no se envía nada ni se escribe ningún perfil - para que una baja de suscripción hecha mientras tanto lo lleve. Cuando el seguimiento vuelve a estar permitido (optIn(), updateConsent(TrackingConsent.GRANTED)), identify() se ejecuta como siempre. reset() y wipeData() lo olvidan. También ocurre en un arranque posterior cuyo estado guardado o configuración permite el seguimiento. Un identify() nuevo sustituye al identificador retenido, y un userId en la configuración prevalece sobre él. El registro de exclusión y una baja del registro push hechos mientras tanto también lo llevan, y con SDK Authentication un rechazo de estos pide a tu handler el token de ese usuario.
Números que JSON no puede llevar. Un NaN o Infinity en el valor de un atributo o en una propiedad de evento o de comercio electrónico se descarta - solo esa clave - con una línea de log, y el resto de la llamada sigue adelante. Una línea null en una lista items de comercio electrónico se omite.
Leer la sesión no cambia nada. getSessionId() y getSessionInfo() nunca inician ni prolongan una sesión. Solo lo hacen los eventos y la vuelta de la app al primer plano, y una sesión nueva siempre envía su Session Start.
Búfer lleno antes de la inicialización. Cuando el búfer de 1000 llamadas está lleno, se descarta primero la llamada más antigua que no sea una baja de suscripción, una salida de grupo o Joryio.unregisterPush().
Claves, almacenamiento e hilos. Una clave de SDK emitida para otra plataforma (jry_sdk_ios_..., jry_sdk_web_..., una tienda) se rechaza con un error y el SDK no envía nada. Un error del Keystore se reintenta y nunca borra el almacenamiento cifrado; solo se vuelve a crear un almacenamiento que no se puede descifrar (una restauración desde copia de seguridad), y un arranque que se ejecutó mientras el Keystore no estaba disponible empieza desde un archivo normal sin la identidad guardada; en el siguiente arranque que abre el almacenamiento cifrado, su último cambio de cada valor prevalece - incluidos un cierre de sesión, wipeData(), optIn() o una concesión de consentimiento hechos entonces - mientras que el identificador anónimo y el del dispositivo que tuvo que generar no sustituyen a los guardados, y las bajas y escrituras en cola de ambos arranques se conservan. Si ese arranque cerró la sesión o identificó a otro usuario, el identificador anónimo del usuario guardado no se traslada y sus atributos sin enviar nunca se envían como el nuevo usuario - se descartan tras un cierre de sesión y, si no, se envían con sus propios identificadores. Una clave maestra que sigue inutilizable - en tres arranques seguidos sin que ningún almacenamiento se abra con ella, con una causa que indica que no puede descifrar o con fallos que duran al menos un día - se sustituye por una clave nueva propia del SDK (la clave androidx de tu app nunca se toca). El almacenamiento cifrado con la clave antigua se descarta entonces, y lo que aún contenía y los arranques con el archivo normal no volvieron a escribir - incluidas las bajas en cola - se pierde; un almacenamiento que la clave antigua aún abre se traslada antes a la clave nueva. El hilo principal nunca espera un reintento. Con initializeAsync(), una Activity que se abrió mientras el SDK arrancaba se detecta - para los mensajes in-app, la solicitud de permiso al arrancar y el toque en la notificación que la abrió. Tu callback de mensajes in-app se ejecuta en el hilo principal, y un tipo de mensaje que esta versión del SDK no conoce llega como MessageType.CUSTOM.
Los mensajes HTML se quedan en su documento. Solo se carga el documento del propio mensaje. Cualquier navegación que inicie - un enlace, una recarga, un frame - se cancela, y un enlace que toca el usuario se abre en el navegador. Su CSS no puede cerrar su propio elemento <style>.
unregisterPush() perdura. El token se olvida en el dispositivo al instante y la petición se guarda hasta que el servidor la confirma. El SDK no vuelve a registrar un token por su cuenta - ni en el siguiente inicio ni cuando Firebase rota el token - hasta que llames a registerPushToken() o el usuario conceda el permiso con requestPushPermission().
wipeData() borra el id anónimo y el de usuario, la cola de eventos, las escrituras pendientes, el estado in-app y el historial de impresiones, la sesión, el id del dispositivo y el token push, y da de baja ese token en el servidor, con la identidad que se borra. Conserva una exclusión de rastreo existente: borrar datos no es dar consentimiento.
Después, el push queda desactivado hasta que la app vuelva a llamar a registerPushToken o a pedir el permiso; un token que llegue en un arranque posterior no se registra automáticamente.
Una visualización por disparador. Una campaña se reserva en el momento en que se elige, antes de cualquier retraso de visualización, así que dos disparadores muy seguidos (el mismo evento rastreado dos veces, una sincronización que llega mientras espera un mensaje retrasado) la muestran una sola vez. La reserva termina cuando el mensaje se descarta (también cuando no se puede enviar nada) o no se pudo mostrar. No se muestra ningún mensaje mientras el consentimiento no sea GRANTED o la persona se haya excluido. Una sincronización que ya estaba en curso cuando ocurrió identify(), reset(), wipeData() o un cambio de consentimiento se descarta, y le sigue una sincronización nueva.
Frecuencia in-app. Solo un informe displayed consume el límite de frecuencia de la campaña y el intervalo antes de la siguiente campaña. El renderizador del SDK lo informa cuando dibuja un mensaje; con setInAppMessageCallback lo informas tú con trackInAppImpression(campaignId, "displayed"). Entregar un mensaje a tu callback no consume nada, así que un mensaje que decides no mostrar no retrasa el siguiente. clearInAppMessageCallback() devuelve el renderizado al SDK. Un presenter instalado con setInAppPresenter informa a través del callback onEvent que recibe ("displayed", "clicked", "dismissed") y no debe llamar además a trackInAppImpression, o el mensaje se cuenta dos veces.
Registro de exclusión. optOut() escribe el atributo de perfil tracking_opted_out = true (sin prefijo $) y optIn() lo vuelve a false, cada uno solo cuando el interruptor cambia de verdad. Un registro que sigue en cola cuando su usuario cierra la sesión sigue siendo de ese usuario y se entrega en su perfil, nunca se redirige a la persona siguiente.
Referencia de API
Todos los métodos públicos, con los nombres de sus parámetros. Las llamadas de la forma Joryio.x() son estáticas y seguras antes de la inicialización; las llamadas a través de Joryio.getInstance() necesitan el SDK inicializado.
// Setup
Joryio.initialize(context: Context, sdkKey: String?, apiHost: String?, config: JoryioConfig? = JoryioConfig()): Joryio
Joryio.initializeAsync(context: Context, sdkKey: String?, apiHost: String?, config: JoryioConfig? = JoryioConfig(), onReady: ((Joryio) -> Unit)? = null)
Joryio.isInitialized(): Boolean
Joryio.getInstance(): Joryio // throws IllegalStateException before initialize()
Joryio.whenReady(action: ((Joryio) -> Unit)?) // runs now, or as soon as the SDK is initialized
Joryio.SDK_VERSION: String
// Events
Joryio.track(eventName: String?, properties: Map<String, Any?>? = emptyMap())
Joryio.trackScreen(screenName: String?, properties: Map<String, Any?>? = emptyMap())
Joryio.flush()
Joryio.getInstance().ecommerce(config: EcommerceConfig? = null): EcommerceTracker
// Identity and attributes
Joryio.identify(userId: String?)
Joryio.addAlias(label: String?, name: String?) // works before identify() too
Joryio.reset() // logout: forgets the user, new anonymous id
Joryio.setAttribute(key: String?, value: Any?) // a null value removes the attribute
Joryio.setAttributes(attributes: Map<String, Any?>?)
Joryio.incrementAttribute(key: String?, by: Number? = 1)
Joryio.unsetAttribute(key: String?)
Joryio.getInstance().getAnonymousId(): String
Joryio.getInstance().getUserId(): String?
Joryio.getInstance().getSessionId(): String
Joryio.getInstance().getSessionInfo(): SessionInfo
Joryio.getInstance().getIdentity(): Pair<String?, String> // (userId, anonymousId)
// Tracking consent, opt-out and stored data
Joryio.optOut()
Joryio.optIn()
Joryio.isUserOptedOut(): Boolean // the opt-out switch only
Joryio.updateConsent(consent: TrackingConsent?) // GRANTED, DENIED or PENDING
Joryio.getTrackingConsent(): TrackingConsent? // before initialize(): the stored decision, or GRANTED when there is none
Joryio.getInstance().getTrackingConsent(): TrackingConsent
Joryio.wipeData() // safe before initialize()
// Marketing subscription
Joryio.setSubscription(channel: String?, status: String?)
Joryio.addToSubscriptionGroup(groupId: String?, channel: String?)
Joryio.removeFromSubscriptionGroup(groupId: String?, channel: String?)
Joryio.unregisterPush()
// In-app messages
Joryio.getInstance().setInAppMessageCallback(callback: ((InAppCampaign) -> Unit)?, capabilities: List<String>? = null)
Joryio.clearInAppMessageCallback() // hands rendering back to the SDK
Joryio.getInstance().setInAppPresenter(presenter: InAppMessagePresenter?)
Joryio.syncInAppCampaigns()
Joryio.getInstance().trackInAppImpression(campaignId: String?, action: String?, elementId: String? = null)
Joryio.getInstance().submitInAppForm(campaignId: String?, values: Map<String, Any?>?, onResult: ((String) -> Unit)?)
Joryio.resetDisplayedCampaigns() // testing only: needs enableDebug
Joryio.getInstance().evaluateInAppCampaigns() // testing only: needs enableDebug
// Push
Joryio.getInstance().registerPushToken(token: String?, onResult: ((PushRegistrationResult) -> Unit)? = null)
Joryio.getInstance().unregisterPush()
Joryio.getInstance().isPushEnabled(): Boolean
Joryio.getInstance().getDeviceToken(): String?
Joryio.getInstance().getPushPermissionStatus(): PushPermissionStatus
Joryio.getInstance().requestPushPermission(activity: Activity?, callback: ((PushPermissionStatus) -> Unit)? = null)
Joryio.getInstance().reportPushDelivered(trackingId: String?) // only with your own messaging service
Joryio.getInstance().trackPushClick(trackingId: String?) // only when you handle the tap yourself
// SDK authentication
Joryio.setSdkAuthenticationToken(token: String?)
Joryio.setSdkAuthenticationErrorHandler(handler: ((SdkAuthError) -> Unit)?)
// Diagnostics
Joryio.getInstance().getQueueSize(): Int
Joryio.getInstance().apiHost: String
Joryio.getInstance().currentApiEndpoint: String
Joryio.getInstance().lastTransportError: JoryioTransportError?
Solución de problemas
Los eventos no aparecen
- Comprueba que la clave de SDK sea correcta:
jry_sdk_android_*. - Activa el registro de depuración:
enableDebug = true. - Comprueba logcat en busca de errores.
- Verifica los permisos de red en el manifest.
- Llama a
flush()para enviar de inmediato.
Push no funciona
- Verifica que Firebase esté configurado correctamente.
- Comprueba que el JSON de cuenta de servicio de Firebase esté cargado en el dashboard.
- Asegúrate de que el token de dispositivo esté registrado.
- Prueba primero con Firebase Console.
- Busca el error de Enviar Push de prueba en Obtener tus credenciales de Firebase (FCM).
Errores de compilación
- Asegúrate de que la versión mínima de SDK sea 23.
- Sincroniza las dependencias de Gradle.
- Limpia y recompila el proyecto.
Compilaciones de release (R8) y copia de seguridad
- R8 / ProGuard: ambos artefactos incluyen sus propias reglas, que se aplican automáticamente cuando tu app define
isMinifyEnabled = true(también en el modo completo de R8). No tienes que añadir reglas, y una app sinfirebase-messagingsigue compilando: push simplemente queda desactivado. - Copia de seguridad: no hace falta nada. La cola de eventos vive en el directorio sin copia de seguridad de la app, y un almacén cifrado restaurado desde una copia de seguridad (que nunca incluye la clave del Keystore) se detecta y se vuelve a crear, cifrado igualmente. Si tu app declara sus propias reglas de copia de seguridad y quieres excluir explícitamente los archivos del SDK, referencia
@xml/joryio_backup_rules(android:fullBackupContent) y@xml/joryio_data_extraction_rules(android:dataExtractionRules), o copia sus líneas en tus reglas. El SDK no define estos atributos por sí mismo, así que nunca entra en conflicto con tu manifest.
Rastreo de e-commerce
El SDK de Android incluye un rastreador de e-commerce integrado para eventos de producto, carrito, checkout y pedido. Consulta la guía de rastreo de e-commerce común a todos los SDK para ver la API completa con ejemplos en Kotlin.
Una compra, con las claves que leen los informes de ingresos:
import io.joryio.sdk.ecommerce.EcommerceCartItem
import io.joryio.sdk.ecommerce.EcommerceOrder
Joryio.getInstance().ecommerce().purchase(
EcommerceOrder(
orderId = "ORD-1001",
items = listOf(EcommerceCartItem(productId = "SKU-1", name = "Blue Shirt", price = 29.99, quantity = 2)),
value = 59.98,
currency = "USD",
)
)
Esto envía Order Completed con orderId y total. Si rastreas una compra a mano, usa esas dos claves: un pedido enviado con order_id o value se registra sin ingresos.
Una compra sin orderId, con un value que no es un número finito o con un artículo sin productId se ignora con una línea en el log y nunca se registra como un pedido de importe cero; ningún método del tracker lanza excepciones. EcommerceTracker.create() funciona antes de initialize(): sus eventos esperan al SDK.
Siguientes pasos
- SDK de iOS: integra el SDK de iOS.
- SDK web: integra el SDK web.
- Guía de notificaciones push: conoce las campañas push.
- Guía de campañas in-app: crea mensajes in-app.
- Eventos personalizados: rastrea eventos personalizados.
- API de e-commerce: integración de e-commerce del lado del servidor.
APIs de prueba y diagnóstico
Dos grupos, y la diferencia importa.
APIs de prueba: se ignoran salvo que enableDebug esté activo. Cambian estado
real, así que una llamada perdida en una compilación de producción corrompería
límites de frecuencia e informes reales.
| Método | Qué hace |
|---|---|
resetDisplayedCampaigns() | Olvida qué campañas in-app ya se mostraron. El estado de frecuencia vive en el dispositivo, así que cambiar la campaña en el servidor NO hará que se muestre de nuevo. |
evaluateInAppCampaigns() | Repite la decisión de visualización sobre las campañas ya sincronizadas, sin red. |
APIs de diagnóstico: siempre disponibles, también en producción. Solo leen estado, así que no pueden dañar nada.
| Método | Responde a |
|---|---|
getQueueSize() | ¿Cuántos eventos esperan envío? |
currentApiEndpoint / lastTransportError | ¿Con qué servidor hablamos y falló la última llamada? |
getIdentity(), getSessionInfo() y getDeviceToken() NO pertenecen a ninguno de los dos grupos: son API normal. Leer la identidad del dispositivo es algo habitual en una app.
Entrega de atributos
setAttributes es duradero. Una escritura se guarda en cola hasta que el
servidor la confirma, así que un atributo definido mientras el dispositivo está
sin conexión se entrega cuando vuelve la conectividad, en vez de perderse.
- Se reintenta en el siguiente
setAttributes, al pasar a primer plano y antes de una sincronización in-app. - Se agrupa - una ráfaga de llamadas se convierte en una sola petición (ventana de 800 ms). Una escritura aislada sigue saliendo enseguida.
- Se persiste en el almacén cifrado de la plataforma (Keychain en iOS,
EncryptedSharedPreferences en Android), así que una escritura sobrevive a que
se cierre el proceso. Se borra en cuanto el servidor la confirma, y la purgan
optOut()ywipeData().
Los atributos también se usan para la segmentación in-app. El perfil del servidor es la referencia: solo las escrituras que el servidor aún no ha confirmado pueden anularlo, y eso es lo que evita que dos dispositivos del mismo contacto discrepen sobre quién es ese contacto.
Suscripción de marketing
Si se puede enviar mensajes a esta persona en un canal. Es un consentimiento distinto de optOut(), que trata de si se la puede observar: ninguno implica el otro.
Esto es lo que escribe un centro de preferencias dentro de la app.
Joryio.setSubscription("email", "unsubscribed")
Joryio.addToSubscriptionGroup("list_abc123", "email")
Canales: email, sms, whatsapp, push, viber. Estados: optedIn, subscribed, unsubscribed.
Se requiere un identificador: la llamada se omite, con un error registrado, si no hay ni user id ni anonymous id.
Darse de baja siempre funciona. setSubscription(channel, "unsubscribed") y removeFromSubscriptionGroup se aceptan y se entregan incluso cuando la persona se ha excluido del rastreo, el consentimiento es DENIED o PENDING, y después de wipeData(): una petición de dejar de recibir mensajes nunca se retiene ni se descarta. Suscribirse (subscribed, optedIn, addToSubscriptionGroup) se rechaza mientras hay exclusión o denegación, y se retiene mientras el consentimiento está pendiente. Un cambio más reciente del mismo canal o grupo reemplaza a uno anterior que aún espera envío, y un cambio hecho antes de identify() que se envía después lleva también el user id.