Crear APK gratis

API del puente de JavaScript

Informes de fallos e integridad del dispositivo

Errores que registra la página, el último fallo nativo y si el teléfono parece tener root o estar modificado, con un token opcional de Play Integrity para tu servidor.

AppMint.integrity ayudante#

AppMint.integrity.check() / AppMint.integrity.token(nonce, cloudProjectNumber?)

Ejemplo

Versiones con Promise de getDeviceIntegrity y requestIntegrityToken, presentes antes del primer script de tu página cuando la compilación activó las Comprobaciones de integridad del dispositivo (paso Integrar, Paso 3).

Devuelve: check() se resuelve con el objeto de heurísticas ({rooted, emulator, debuggerAttached, signals, installer, installedFromPlay, signatureSha256, method: 'heuristic'}); token(nonce) se resuelve con la cadena del token de Play Integrity, o se rechaza con un Error cuyo mensaje es el nombre del error (not_enabled, nonce_too_short, network_error, …) y code el código de error de Play. Las heurísticas son indicios; solo la decodificación del token por tu servidor es un veredicto.

async function guard() {
  if (!(window.AppMint && AppMint.integrity)) return;          // browser, or option off
  const local = await AppMint.integrity.check();
  if (local.rooted) console.warn('rooted signals:', local.signals);

  const { nonce } = await fetch('/api/integrity/nonce').then(r => r.json());
  try {
    const token = await AppMint.integrity.token(nonce);          // project number from the build
    await fetch('/api/integrity/verify', { method: 'POST', body: token });
  } catch (e) {
    console.warn('Play Integrity unavailable:', e.message, e.code);
  }
}

Avisa cuando la app sea una copia reempaquetada:

if (window.AppMint && AppMint.integrity) {
  const d = await AppMint.integrity.check();
  if (!d.installedFromPlay) console.log('installed by', d.installer || 'side-loaded');
  if (d.signatureSha256 && d.signatureSha256 !== MY_CERT_SHA256) reportTamper(d.signatureSha256);
}

Maneja un fallo del token por su nombre:

async function getToken(nonce) {
  if (!(window.AppMint && AppMint.integrity)) return null;
  try {
    return await AppMint.integrity.token(nonce);
  } catch (e) {
    if (e.message === 'not_enabled') console.warn('Device integrity checks are off in this build');
    else if (e.message === 'nonce_too_short') console.warn('the server nonce must be longer');
    else console.warn('Play Integrity error', e.message, e.code);
    return null;
  }
}

getDeviceIntegrity puente#

window.WebToApk.getDeviceIntegrity(): String

HEURÍSTICAS locales de root / emulador / depurador (consulta DeviceIntegrity) como JSON: `{rooted, emulator, debuggerAttached, signals:[…], installer, installedFromPlay, signatureSha256, method:"heuristic", sdk}`. Un resultado limpio significa "no se encontró ninguna señal", no una prueba; para un veredicto fiable, envía un token de Play Integrity a tu servidor (requestIntegrityToken). `{"error":"not_enabled"}` a menos que la compilación haya activado la Integridad del dispositivo. Bloquea unos milisegundos (lee archivos): llámalo una sola vez.

Ejemplo

HEURÍSTICAS locales de root, emulador y depurador como cadena JSON. Un resultado limpio significa "no se encontró ninguna señal", no una prueba: un usuario decidido con un módulo que oculta el root pasa. Úsalo para avisar o para añadir una comprobación; para un veredicto fiable, envía un token de Play Integrity a tu servidor (requestIntegrityToken).

Devuelve: '{"rooted":false,"emulator":false,"debuggerAttached":false,"signals":[],"installer":"com.android.vending","installedFromPlay":true,"signatureSha256":"AB:CD:…","method":"heuristic","sdk":34}', o '{"error":"not_enabled"}'. Las señales nombran lo que se encontró: test-keys, su-binary:/system/xbin/su, root-path:/data/adb/magisk, root-app:com.topjohnwu.magisk, system-writable, ro.debuggable=1, ro.secure=0, selinux-permissive, bootloader-unlocked, emulator:*, debugger-attached. Requiere: activa las Comprobaciones de integridad del dispositivo en el paso Integrar (Paso 3). Tarda unos milisegundos (lee archivos): llámalo una vez, no en un bucle.

function deviceCheck() {
  if (!(window.WebToApk && window.WebToApk.getDeviceIntegrity)) return null;
  const r = JSON.parse(WebToApk.getDeviceIntegrity());
  if (r.error) return null;                       // 'not_enabled'
  return r;
}

const d = deviceCheck();
if (d && d.rooted) {
  showNotice('This phone looks rooted (' + d.signals.join(', ') + '). Some features are turned off.');
}
if (d && !d.installedFromPlay) console.log('installed by', d.installer || 'unknown (side-loaded)');

Comprueba tu propia firma: una copia reempaquetada de tu app está firmada con otra clave:

const MY_CERT = 'AB:CD:EF:…';   // Play Console → App integrity → App signing key SHA-256
const d = deviceCheck();
if (d && d.signatureSha256 && d.signatureSha256 !== MY_CERT) reportTamper();

Pide a un servidor un veredicto fiable con un token de Play Integrity (la respuesta llega como un evento).

window.addEventListener('appmint:integrity', function (e) {
  if (e.detail.error) { showNotice('Check failed: ' + e.detail.error); return; }
  fetch('https://api.example.com/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ token: e.detail.token })
  });
});
if (window.WebToApk && WebToApk.requestIntegrityToken) {
  const nonce = crypto.randomUUID().replace(/-/g, '') + '0000';   // your server issues and checks this
  WebToApk.requestIntegrityToken(nonce, '123456789012');          // your Google Cloud project number
}

getLastCrash puente#

window.WebToApk.getLastCrash(): String

El fallo más reciente que registró esta instalación (la app cerrándose por un error no capturado, o el renderizador de la página fallando), como JSON `{kind, message, stack, url, at, count, appVersion, user, fp}`, o "" cuando no ha habido ninguno (o los Informes de fallos están desactivados). Se conserva tras subirlo, así que una página puede decir "lo sentimos, la app se cerró la última vez".

Ejemplo

El fallo más reciente que registró esta instalación (la app cerrándose por un error no capturado (kind: "native") o el renderizador de la página fallando (kind: "renderer")) como cadena JSON, o '' cuando no ha habido ninguno.

Devuelve: '{"kind","message","stack","url","at","count","appVersion","user","fp"}' o ''. También '' cuando la compilación no activó los Informes de fallos. Requiere: Informes de fallos activados en el paso Integrar (Paso 3).

Pide disculpas una sola vez tras un fallo:

(function () {
  if (!(window.WebToApk && window.WebToApk.getLastCrash)) return;
  const raw = WebToApk.getLastCrash();
  if (!raw) return;
  const crash = JSON.parse(raw);
  if (localStorage.getItem('seenCrash') === String(crash.at)) return;   // shown already
  localStorage.setItem('seenCrash', String(crash.at));
  showBanner('Sorry, the app closed unexpectedly last time. We have been told about it.');
})();

Sube a tu propio servidor el fallo guardado una vez, con la versión de la app en la que ocurrió.

(function () {
  if (!(window.WebToApk && WebToApk.getLastCrash)) return;
  const raw = WebToApk.getLastCrash();
  if (!raw) return;                         // no crash, or Crash reports are off
  const c = JSON.parse(raw);
  if (localStorage.getItem('sentCrash') === String(c.at)) return;
  fetch('https://api.example.com/crashes', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ kind: c.kind, message: c.message, version: c.appVersion, count: c.count })
  }).then(function () { localStorage.setItem('sentCrash', String(c.at)); });
})();

Ramifica según el tipo para dar un mensaje adecuado.

const raw = window.WebToApk && WebToApk.getLastCrash && WebToApk.getLastCrash();
if (raw) {
  const c = JSON.parse(raw);
  showBanner(c.kind === 'renderer'
    ? 'The page ran out of memory last time. Try closing other apps.'
    : 'The app closed unexpectedly last time.');
}

Notas: permanece después de subir el informe, así que compara at con lo que guardaste para mostrar el mensaje una sola vez. Los errores de la página y las llamadas a logError no son fallos y nunca aparecen aquí.

logError puente#

window.WebToApk.logError(message: String, stack: String): Boolean

Informes de fallos: registra un error que tu página CAPTURÓ (una llamada de pago fallida, un error de análisis) para que aparezca en la pantalla de Informes de fallos de AppMint junto a los fallos, agrupado y contado. [stack]: `err.stack` o "". Devuelve false cuando la compilación no activó los Informes de fallos, cuando ambos están vacíos o cuando el mismo error ya se registró muchas veces en esta sesión. Nunca pongas datos personales (correo, teléfono, tokens) en el mensaje.

Ejemplo

Registra un error que tu página CAPTURÓ (una llamada de pago fallida, un error de análisis JSON) para que aparezca en la pantalla Informes de fallos de AppMint (Actualizar la app → tu app → Informes de fallos), agrupado con los fallos y contado. Los errores no capturados y los rechazos de promesas no manejados se registran por ti; esto es para los que manejaste.

Devuelve: true cuando se registra; false cuando la compilación no activó los Informes de fallos, cuando ambos argumentos están vacíos o cuando el mismo error ya se registró muchas veces en esta sesión. Los informes se suben la próxima vez que se abra la app. Requiere: activa los Informes de fallos en el paso Integrar (Paso 3) al compilar. Nunca pongas datos personales (correo electrónico, teléfono, tokens) en el mensaje.

Registra un error manejado:

function logError(err, context) {
  if (!(window.WebToApk && window.WebToApk.logError)) return false;   // browser, or an older build
  var msg = (context ? context + ': ' : '') + (err && err.message ? err.message : String(err));
  return WebToApk.logError(msg, (err && err.stack) || '');
}

async function checkout(cart) {
  try {
    await payments.charge(cart.total);
  } catch (err) {
    logError(err, 'checkout');          // shows up as "Logged by your page"
    showToast('Payment failed, please try again');
  }
}

Informa de las llamadas a la API que fallan sin la cadena de consulta de la dirección (puede llevar tokens):

async function api(path) {
  const res = await fetch('/api' + path);
  if (!res.ok && window.WebToApk && WebToApk.logError) {
    WebToApk.logError('API ' + res.status + ' ' + path.split('?')[0], '');
  }
  return res;
}

Registra un error capturado con un id de usuario, para poder encontrar más tarde los problemas de un usuario.

if (window.WebToApk && WebToApk.setCrashUserId) WebToApk.setCrashUserId('user_' + currentUser.id);

try {
  JSON.parse(savedState);
} catch (err) {
  if (window.WebToApk && WebToApk.logError) {
    const logged = WebToApk.logError('state parse: ' + err.message, err.stack || '');
    if (!logged) console.warn('not recorded (Crash reports off, or repeated too often)');
  }
}

Notas: los informes con la misma causa se agrupan aunque los números del mensaje difieran (item 42 y item 7). La dirección de la página se guarda sin su consulta ni fragmento.

requestIntegrityToken puente#

window.WebToApk.requestIntegrityToken(nonce: String, cloudProjectNumber: String): String

Pide a Google Play un token de Play Integrity (solicitud clásica). [nonce]: Base64 seguro para URL, de 16 a 500 caracteres, generado por TU servidor para esta única solicitud. [cloudProjectNumber]: el número de tu proyecto de Google Cloud, o "" para usar el definido en la compilación (una app instalada desde Play puede dejar ambos vacíos). Devuelve de inmediato un id de solicitud ("" cuando la compilación no activó la Integridad del dispositivo); la respuesta llega como `appmint:integrity` / `window.onAppMintIntegrity({requestId, token})` o `{requestId, error, code}`. Envía el token a tu servidor, que lo decodifica con Google (playintegrity.googleapis.com … :decodeIntegrityToken) y comprueba el nonce.

Ejemplo

Pide a Google Play un token de Play Integrity (solicitud clásica). El token es opaco en el teléfono: tu SERVIDOR lo envía a Google, lee el veredicto (¿app auténtica? ¿dispositivo auténtico? ¿con licencia?) y comprueba que el nonce sea el que emitió. Esa comprobación del servidor es lo que hace fiable la respuesta.

Devuelve: un id de solicitud de inmediato ('' cuando la compilación no activó la Integridad del dispositivo). La respuesta llega como el evento appmint:integrity (y window.onAppMintIntegrity) con {requestId, token} o {requestId, error, code}; errores como nonce_too_short, nonce_is_not_base64, network_error, play_store_not_found, cloud_project_number_is_invalid, too_many_requests. Requiere: Comprobaciones de integridad del dispositivo activadas en el paso Integrar (Paso 3), la API de Play Integrity habilitada para la app (Play Console → Integridad de la app) y, para una app NO instalada desde Google Play, el número de tu proyecto de Google Cloud, en el asistente o como segundo argumento ('' usa el de la compilación).

async function integrityToken() {
  // 1. Your server makes a one-time nonce: URL-safe Base64, 16-500 characters.
  const { nonce } = await fetch('/api/integrity/nonce').then(r => r.json());
  return new Promise((resolve, reject) => {
    const id = WebToApk.requestIntegrityToken(nonce, '');
    if (!id) return reject(new Error('not_enabled'));
    window.addEventListener('appmint:integrity', function on(e) {
      if (e.detail.requestId !== id) return;
      window.removeEventListener('appmint:integrity', on);
      e.detail.token ? resolve(e.detail.token) : reject(new Error(e.detail.error));
    });
  });
}

// 2. Your server decodes it with Google and decides.
const token = await integrityToken();
const verdict = await fetch('/api/integrity/verify', { method: 'POST', body: token }).then(r => r.json());

El paso del servidor: llama a POST https://playintegrity.googleapis.com/v1/<your.package>:decodeIntegrityToken con {"integrity_token": token} usando una cuenta de servicio del proyecto de Cloud vinculado y luego comprueba:

const p = decoded.tokenPayloadExternal;
const ok = p.requestDetails.nonce === issuedNonce &&
  p.appIntegrity.appRecognitionVerdict === 'PLAY_RECOGNIZED' &&
  p.deviceIntegrity.deviceRecognitionVerdict.includes('MEETS_DEVICE_INTEGRITY');

La forma con callback, para código que no puede escuchar eventos:

window.onAppMintIntegrity = function (d) {
  if (d.error) console.warn('integrity failed', d.error, d.code);
  else sendToServer(d.token);
};
WebToApk.requestIntegrityToken(nonceFromServer, '123456789012');

setCrashUserId puente#

window.WebToApk.setCrashUserId(id: String): Boolean

Informes de fallos: TU id del usuario con sesión iniciada (letras, dígitos y _ @ . : + -, hasta 128), adjunto a los informes a partir de ahora para que puedas contar usuarios afectados y encontrar los fallos de un usuario. "" lo borra. Nunca un correo electrónico ni un número de teléfono. Devuelve false para un id con otros caracteres, o cuando los Informes de fallos están desactivados.

Ejemplo

Adjunta TU id del usuario con sesión iniciada a los informes de fallos a partir de ahora, para que la pantalla de Informes de fallos pueda contar a cuántos usuarios afecta un problema. Llámalo después de iniciar sesión; llámalo con '' al cerrar sesión.

Devuelve: true cuando se guarda; false para un id con caracteres distintos de letras, dígitos y _ @ . : + - (o de más de 128), o cuando la compilación no activó los Informes de fallos. Requiere: Informes de fallos activados en el paso Integrar (Paso 3). Usa el id de tu base de datos, nunca un correo electrónico ni un número de teléfono.

function crashUser(id) {
  if (!(window.WebToApk && window.WebToApk.setCrashUserId)) return false;
  return WebToApk.setCrashUserId(id);
}

async function signIn(email, password) {
  const user = await myBackend.signIn(email, password);
  crashUser(user.id);            // e.g. 'u_42'
}

function signOut() {
  crashUser('');                 // later reports carry no user
}

Un id rechazado, o una compilación sin Informes de fallos, devuelve false:

function setCrashUser(id) {
  if (!(window.WebToApk && WebToApk.setCrashUserId)) return;
  if (!WebToApk.setCrashUserId(id)) {
    console.warn('Crash user id not saved: bad characters, over 128 long, or Crash reports are off');
  }
}
setCrashUser('u_42');       // your database id, never an e-mail address

Cuéntale al usuario el último fallo tras un reinicio:

if (window.WebToApk && WebToApk.getLastCrash) {
  const raw = WebToApk.getLastCrash();          // '' when there has been none
  if (raw) {
    const c = JSON.parse(raw);
    showMessage('Sorry, the app closed last time (' + c.kind + ').');
  }
}

Notas: el id permanece en el teléfono hasta que lo cambies y solo se envía dentro de los informes de fallos.

Generado a partir del runtime de la app y sus archivos de ejemplo en cada compilación de la documentación. Léelo como Markdown · Todas las familias.

Comprobado con el puente publicado el 2026-10-22.