Lanzamiento gradual: porcentajes, vigilancia y el mito de revertir
Cómo funciona el despliegue por porcentaje en Google Play y qué señales lo gobiernan: la elección de la fracción inicial y su significado estadístico, las métricas vitales de fallos y ANR con sus umbrales de mal comportamiento, la diferencia entre detener y revertir, y cómo diseñar una app cuyo comportamiento se pueda cambiar sin publicar una versión nueva.
El despliegue por porcentaje parece una función simple: en lugar de dar la versión nueva a todos, se la das a una fracción y observas. Debajo hay algo bastante menos simple, porque la fracción determina cuánta señal recoges y cuánto tardas en recogerla, y esas dos cantidades se mueven en direcciones opuestas. Un uno por ciento acota el daño pero puede tardar días en producir suficientes eventos para distinguir una regresión real del ruido de fondo; un veinte por ciento te da certeza en horas a costa de exponer a mucha gente. Y por encima de esa tensión pesa una asimetría fundamental que casi todo el mundo descubre demasiado tarde: puedes detener un despliegue, pero no puedes deshacerlo. Quien ya actualizó, ya actualizó.
- Entender la mecánica del despliegue por porcentaje y cómo se asignan los dispositivos.
- Elegir la fracción inicial en función del volumen y del tiempo hasta obtener señal.
- Vigilar las métricas vitales con umbrales que distingan regresión de ruido.
- Diseñar la capacidad de mitigar sin publicar, porque revertir no existe.
Cómo funciona el despliegue por porcentaje
Cuando publicas al veinte por ciento, la tienda no reparte la versión aleatoriamente cada vez que alguien consulta: asigna dispositivos a la nueva versión de forma estable, de modo que quien entró en el grupo no vuelve a salir. Esa estabilidad es lo que permite comparar cohortes, y también lo que hace irreversible la exposición: un dispositivo que ya recibió la versión no puede volver a la anterior por el simple hecho de que reduzcas el porcentaje.
flowchart TD PUB[Publicas al cinco por ciento] --> ASIG[Play asigna dispositivos de forma estable] ASIG --> ACT[Ese grupo actualiza en horas o dias] ACT --> MET[Llegan metricas de fallos y ANR] MET --> DEC[Comparas con la version anterior] DEC --> SUBE[Amplias el porcentaje] DEC --> HALT[Detienes el despliegue] HALT --> QUEDA[Quienes ya actualizaron siguen en la version nueva] QUEDA --> FIX[Unica salida publicar una version superior]
El segundo factor que se ignora es la latencia de adopción. Publicar al cinco por ciento no significa que el cinco por ciento de tu base tenga la versión mañana. Significa que el cinco por ciento es elegible, y la actualización real llega en oleadas durante días según cuándo cada dispositivo tenga red adecuada, batería y ventana de mantenimiento. Las primeras horas están sesgadas hacia dispositivos activos y bien conectados, que suelen ser los más modernos, precisamente los que menos fallan. Cualquier lectura hecha en las primeras horas mide una población que no representa a tu base.
Porcentaje Sesiones diarias aproximadas Tiempo hasta senal utilizable
1 % escaso salvo bases muy grandes varios dias
5 % suficiente en bases medianas uno o dos dias
20 % abundante horas
50 % practicamente definitivo horas, con daño ya amplio
Ese sesgo temprano tiene una consecuencia práctica poco intuitiva: durante las primeras horas, las métricas de una versión nueva suelen verse mejores que las de la anterior, porque se están midiendo sobre la población más sana de tu base. Interpretar esa mejora como validación y ampliar el porcentaje es el error de lectura más caro del despliegue gradual, y explica buena parte de los incidentes en los que un equipo pasó del cinco al cincuenta por ciento y descubrió el problema con diez veces más gente afectada.
De aquí sale la regla operativa que usa la gente con experiencia: el porcentaje inicial se elige para que el número absoluto de sesiones diarias en la versión nueva alcance un mínimo que permita detectar la regresión que te preocupa, no para que el porcentaje suene prudente. Una app con cien mil sesiones diarias obtiene más señal al uno por ciento que otra con dos mil al cincuenta.
Qué vigilar y con qué umbrales
Las dos métricas que gobiernan la salud de una versión son la tasa de sesiones con fallo y la tasa de sesiones con aplicación no responde. La tienda define umbrales de mal comportamiento por encima de los cuales una app pierde visibilidad en las recomendaciones y puede recibir avisos, y esos umbrales están fijados sobre sesiones perceptibles por el usuario, no sobre todas las sesiones.
Tasa de fallos
Sesiones en las que el proceso murió por una excepción no capturada. El umbral de mal comportamiento vive en el entorno del dos por ciento de sesiones perceptibles.
Tasa de ANR
Sesiones en las que el hilo principal quedó bloqueado más allá del límite del sistema. Su umbral es más bajo, cercano al medio por ciento, porque molesta más.
Valoraciones recientes
Reaccionan más lento que las métricas técnicas pero capturan lo que ninguna de ellas ve: regresiones funcionales que no rompen el proceso.
Señal propia
Tus métricas de negocio y tus errores capturados. Detectan antes que nadie que una pantalla dejó de cargar aunque la app no se caiga nunca.
Conviene entender por qué el umbral de los ANR es mucho más estricto que el de los fallos pese a que un ANR no mata la app. La razón es de percepción: un fallo cierra la app y el usuario reintenta, mientras que un bloqueo del hilo principal deja la interfaz congelada durante segundos y produce la sensación de un producto roto, con una tasa de desinstalación asociada más alta. Además, los ANR correlacionan con dispositivos lentos y con mercados donde el hardware es más modesto, que es exactamente la población que la plataforma no quiere perder.
Un error de método muy extendido consiste en comparar la tasa de la versión nueva contra el umbral absoluto en lugar de contra la versión anterior en la misma ventana. La comparación correcta es siempre relativa y con poblaciones equivalentes: misma franja de días, mismos mercados, y a ser posible desglosada por versión de sistema y por fabricante, porque las regresiones rara vez son uniformes. Una subida global del veinte por ciento en la tasa de fallos suele ser una subida del quinientos por ciento en un solo modelo, y ese desglose es el que dice qué está roto.
Las regresiones más caras no se caen. Un cambio que rompe el flujo de pago, que deja la pantalla de inicio en blanco tras un error de red o que desactiva sin querer las notificaciones no produce ni un solo fallo ni un solo ANR: produce usuarios que se van. Si tu única señal de despliegue es la telemetría de fallos, estás ciego ante toda la clase de regresiones que más dinero cuesta. Antes de ampliar un porcentaje, mira también las métricas de negocio segmentadas por versión.
Detener, reanudar y por qué revertir no existe
Cuando una versión va mal tienes exactamente dos palancas en la tienda, y ninguna de las dos es una reversión.
Detener el despliegue congela la asignación: nadie nuevo recibirá la versión, pero quien ya la tiene se queda con ella indefinidamente. Es la acción correcta e inmediata ante cualquier duda, porque acota la población afectada y no impide reanudar más tarde si resulta ser una falsa alarma. Reanudar continúa desde el punto donde estaba.
Publicar una versión superior es la única mitigación real. Puede ser la corrección, si la tienes lista y confías en ella, o puede ser el código anterior recompilado con un código de versión mayor, que es lo más parecido a revertir que existe y que la gente llama informalmente reversión. Tiene dos costes que hay que anticipar: pasa por revisión otra vez, y llega con la misma latencia de adopción de cualquier actualización, así que los afectados seguirán rotos durante horas o días.
# Detener de inmediato el despliegue de la version en curso
fastlane supply --track production --rollout 0 --skip_upload_aab
# Mitigacion real: publicar el codigo anterior con codigo de version superior
./gradlew :app:bundleRelease -PversionCodeOverride=413
fastlane supply \
--aab app/build/outputs/bundle/release/app-release.aab \
--track production --rollout 1.0
Hay un detalle que agrava el problema y que conviene tener presente antes de que ocurra: el código de versión de la corrección debe ser mayor que el de la versión rota, así que la numeración avanza aunque el contenido retroceda. Si tu esquema de numeración deriva mecánicamente del nombre de versión o de una etiqueta del repositorio, esa maniobra puede quedar bloqueada justo en el peor momento. Un esquema que permita forzar el entero por parámetro, como en el ejemplo anterior, es una de esas precauciones que solo se agradecen una vez y compensan para siempre.
Existe una excepción parcial que conviene conocer: si detectas el problema mientras la versión sigue en fase muy temprana y la tienda aún permite la operación, puedes retirar la versión de un canal para que deje de servirse a nuevas instalaciones. Sigue sin recuperar a los ya actualizados, pero evita que la versión mala sea la que reciban quienes instalen la app por primera vez, que es un grupo especialmente sensible porque su primera experiencia decide la desinstalación.
Diseñar para poder mitigar sin publicar
La conclusión operativa de todo lo anterior es que si tu única palanca de control es publicar, tu tiempo de reacción está atado al ciclo de revisión y de adopción. Los equipos que operan bien invierten en palancas que no pasan por la tienda.
La primera es la configuración remota: parámetros que la app lee de un servidor y que permiten apagar una funcionalidad concreta sin publicar nada. Una funcionalidad nueva desplegada tras un interruptor remoto convierte una regresión en un cambio de un valor booleano, y el efecto llega en minutos en lugar de días.
La forma canónica de esa palanca es un interruptor consultado en el punto donde nace la funcionalidad, con un valor por defecto seguro para el caso de que la consulta falle o el dispositivo esté sin red.
class PantallaPagoViewModel(
private val config: ConfiguracionRemota,
private val flujoNuevo: FlujoPagoNuevo,
private val flujoAnterior: FlujoPagoAnterior,
) : ViewModel() {
// Valor por defecto conservador: si la consulta falla, ruta conocida
private val usarFlujoNuevo = config.booleano(
clave = "pago_flujo_v2",
porDefecto = false,
)
fun iniciarPago(cesta: Cesta) = viewModelScope.launch {
if (usarFlujoNuevo) flujoNuevo.procesar(cesta) else flujoAnterior.procesar(cesta)
}
}
El precio de este patrón es real y hay que asumirlo: mantener dos caminos vivos duplica la superficie que hay que probar y tiende a acumular interruptores muertos que nadie retira. La disciplina que lo hace sostenible es tratar cada interruptor como temporal, con una fecha de retirada anotada en el momento de crearlo y con la eliminación del camino antiguo planificada como tarea, no como intención.
La segunda es la actualización dentro de la app, que permite pedir o forzar la actualización desde la propia aplicación cuando detectas que hay una versión más nueva disponible. En modo flexible el usuario sigue usando la app mientras descarga; en modo inmediato se le bloquea hasta actualizar, que es la herramienta apropiada cuando la versión instalada es peligrosa.
val gestor = AppUpdateManagerFactory.create(context)
gestor.appUpdateInfo.addOnSuccessListener { info ->
val hayVersion = info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
val esVieja = info.clientVersionStalenessDays() ?: 0
if (hayVersion && esVieja >= 7 && info.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
gestor.startUpdateFlowForResult(info, AppUpdateType.IMMEDIATE, activity, CODIGO)
}
}
El modo inmediato tiene un coste que hay que respetar: bloquea a alguien que abrió la app para hacer algo. Usarlo por comodidad, o para empujar versiones que solo traen mejoras, quema la paciencia del usuario y aparece en las valoraciones. La regla razonable es reservarlo para dos casos —una vulnerabilidad de seguridad y una versión instalada que ya no funciona contra tus servidores— y usar el modo flexible para todo lo demás, incluida la campaña lenta de reducir la cola de versiones antiguas.
La tercera, más estructural, es desacoplar el despliegue de la activación: publicar el código de una funcionalidad apagado, dejar que la versión se difunda tranquilamente durante días, y encender la funcionalidad después mediante configuración. Así el riesgo del binario y el riesgo de la funcionalidad se separan en dos eventos distintos, cada uno con su propia vigilancia, en lugar de acumularse en el mismo momento.
La imagen mental equivocada del despliegue por porcentaje es la de un airbag: algo que amortigua el golpe si algo sale mal. La imagen correcta es la de un experimento con presupuesto. Cada punto porcentual que expones es dinero que gastas —usuarios reales, sesiones reales, alguna desinstalación real— y lo que compras con él es evidencia estadística sobre si tu versión es sana. Igual que en cualquier experimento, hay un tamaño mínimo de muestra por debajo del cual no compras nada: exponer al uno por ciento durante dos horas en una app pequeña no es prudencia, es teatro, porque el resultado no distingue una versión sana de una catastrófica y sin embargo ya ha gastado exposición. Y como el gasto es irreversible —no recuperas a los usuarios que ya actualizaron— la decisión relevante no es si desplegar despacio, sino cuánta evidencia necesitas para la decisión que vas a tomar y cuál es la forma más barata de obtenerla. A veces la respuesta es un porcentaje pequeño durante días. A veces es un canal de prueba con voluntarios, que produce señal parecida sin coste reputacional. A veces es un interruptor remoto que convierte la pregunta arriesgada en algo que puedes encender y apagar a voluntad, y esa es casi siempre la mejor compra porque desacopla la difusión del binario de la exposición al comportamiento. El equipo maduro no es el que despliega más despacio, es el que ha construido la maquinaria para que la mayor parte de sus decisiones ya no dependan de un despliegue.
- Calcula cuántas sesiones diarias produce un uno por ciento de tu base y estima cuánto tardarías en detectar una duplicación de la tasa de fallos.
- Define el porcentaje inicial que te da esa detección en menos de veinticuatro horas y justifícalo con el número absoluto, no con el porcentaje.
- Escribe los umbrales exactos, relativos a la versión anterior y segmentados por versión de sistema, que dispararán una detención automática.
- Añade a tu app un interruptor remoto para la última funcionalidad que publicaste y comprueba que apagarla deja la app en un estado coherente.
- Ensaya la mitigación completa: publica el código anterior con un código de versión superior y cronometra el tiempo total hasta que llega al primer dispositivo.