API del bridge JavaScript
Report di arresto anomalo e integrità del dispositivo
Gli errori che la pagina registra, l'ultimo arresto anomalo nativo, e se il telefono sembra con root o modificato, con un token Play Integrity facoltativo per il tuo server.
AppMint.integrity helper#
AppMint.integrity.check() / AppMint.integrity.token(nonce, cloudProjectNumber?)
Esempio
Versioni Promise di getDeviceIntegrity e requestIntegrityToken, presenti prima del primo script della tua pagina quando la build ha attivato i Controlli di integrità del dispositivo (passaggio Integrazione, Passaggio 3).
Restituisce: check() si risolve con l'oggetto delle euristiche ({rooted, emulator, debuggerAttached, signals, installer, installedFromPlay, signatureSha256, method: 'heuristic'}); token(nonce) si risolve con la stringa del token Play Integrity, oppure viene rifiutata con un Error il cui messaggio è il nome dell'errore (not_enabled, nonce_too_short, network_error, …) e code il codice di errore di Play. Le euristiche sono indizi; solo la decodifica del token da parte del tuo server è un verdetto.
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);
}
}Avvisa quando l'app è una copia riconfezionata:
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);
}Gestisci per nome un errore del token:
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 bridge#
window.WebToApk.getDeviceIntegrity(): String
EURISTICHE locali di root / emulatore / debugger (vedi DeviceIntegrity) in JSON: `{rooted, emulator, debuggerAttached, signals:[…], installer, installedFromPlay, signatureSha256, method:"heuristic", sdk}`. Un risultato pulito significa «nessun segnale trovato», non una prova - per un verdetto affidabile, invia un token Play Integrity al tuo server (requestIntegrityToken). `{"error":"not_enabled"}` a meno che la build non abbia attivato l'integrità del dispositivo. Blocca per pochi millisecondi (legge file): chiamala una volta.
Esempio
EURISTICHE locali di root, emulatore e debugger come stringa JSON. Un risultato pulito significa «nessun segnale trovato», non una prova: un utente determinato con un modulo che nasconde il root la supera. Usala per avvisare o per aggiungere un controllo; per un verdetto affidabile, invia un token Play Integrity al tuo server (requestIntegrityToken).
Restituisce: '{"rooted":false,"emulator":false,"debuggerAttached":false,"signals":[],"installer":"com.android.vending","installedFromPlay":true,"signatureSha256":"AB:CD:…","method":"heuristic","sdk":34}', oppure '{"error":"not_enabled"}'. I segnali indicano che cosa è stato trovato: 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. Richiede: attiva i Controlli di integrità del dispositivo nel passaggio Integrazione (Passaggio 3). Impiega pochi millisecondi (legge file): chiamala una volta, non in un ciclo.
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)');Controlla la tua firma - una copia riconfezionata della tua app è firmata con un'altra chiave:
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();Chiedi a un server un verdetto affidabile con un token Play Integrity (la risposta arriva come 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 bridge#
window.WebToApk.getLastCrash(): String
L'arresto anomalo più recente registrato da questa installazione (l'app che si chiude per un errore non intercettato, o il renderer della pagina che va in crash), come JSON `{kind, message, stack, url, at, count, appVersion, user, fp}`, oppure "" quando non ce ne sono stati (o i Report di arresto anomalo sono disattivati). Viene conservato dopo l'invio, così una pagina può dire «scusa, l'app si è chiusa l'ultima volta».
Esempio
L'arresto anomalo più recente registrato da questa installazione - l'app che si chiude per un errore non intercettato (kind: "native") o il renderer della pagina che va in crash (kind: "renderer") - come stringa JSON, oppure '' quando non ce ne sono stati.
Restituisce: '{"kind","message","stack","url","at","count","appVersion","user","fp"}' oppure ''. Anche '' quando la build non ha attivato i Report di arresto anomalo. Richiede: Report di arresto anomalo attivi nel passaggio Integrazione (Passaggio 3).
Chiedi scusa una sola volta dopo un arresto anomalo:
(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.');
})();Carica sul tuo server l'arresto anomalo memorizzato una volta, con la versione dell'app su cui è avvenuto.
(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)); });
})();Distingui per tipo per dare un messaggio adatto.
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.');
}Note: resta anche dopo l'invio del report, quindi confronta at con quanto hai memorizzato per mostrare il messaggio una sola volta. Gli errori della pagina e le chiamate a logError non sono arresti anomali e qui non compaiono mai.
logError bridge#
window.WebToApk.logError(message: String, stack: String): Boolean
Report di arresto anomalo: registra un errore che la tua pagina ha INTERCETTATO (una chiamata di pagamento fallita, un errore di analisi) così compare nella schermata Report di arresto anomalo di AppMint accanto agli arresti anomali, raggruppato e conteggiato. [stack]: `err.stack` oppure "". Restituisce false quando la build non ha attivato i Report di arresto anomalo, quando entrambi sono vuoti, o quando lo stesso errore è già stato registrato molte volte in questa sessione. Non inserire mai dati personali (e-mail, telefono, token) nel messaggio.
Esempio
Registra un errore che la tua pagina ha INTERCETTATO (una chiamata di pagamento fallita, un errore di analisi JSON) così compare nella schermata Report di arresto anomalo di AppMint (Aggiorna l'app → la tua app → Report di arresto anomalo), raggruppato con gli arresti anomali e conteggiato. Gli errori non intercettati e le promise rifiutate non gestite vengono registrati per te; questa funzione serve per quelli che hai gestito.
Restituisce: true quando registrato; false quando la build non ha attivato i Report di arresto anomalo, quando entrambi gli argomenti sono vuoti, o quando lo stesso errore è già stato registrato molte volte in questa sessione. I report vengono inviati la prossima volta che l'app si apre. Richiede: attiva i Report di arresto anomalo nel passaggio Integrazione (Passaggio 3) quando crei l'app. Non inserire mai dati personali (indirizzo e-mail, numero di telefono, token) nel messaggio.
Registra un errore gestito:
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');
}
}Segnala le chiamate API fallite senza la query string dell'indirizzo (potrebbe contenere token):
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 errore intercettato con un id utente, così i problemi di un utente si possono ritrovare in seguito.
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)');
}
}Note: i report con la stessa causa vengono raggruppati anche quando i numeri nel messaggio differiscono (item 42 e item 7). L'indirizzo della pagina viene memorizzato senza query e frammento.
requestIntegrityToken bridge#
window.WebToApk.requestIntegrityToken(nonce: String, cloudProjectNumber: String): String
Chiede a Google Play un token Play Integrity (richiesta classica). [nonce]: Base64 URL-safe, 16-500 caratteri, generato dal TUO server per questa singola richiesta. [cloudProjectNumber]: il numero del tuo progetto Google Cloud, oppure "" per usare quello impostato nella build (un'app installata da Play può lasciare entrambi vuoti). Restituisce subito un id richiesta ("" quando la build non ha attivato il controllo dell'integrità del dispositivo); la risposta arriva come `appmint:integrity` / `window.onAppMintIntegrity({requestId, token})` oppure `{requestId, error, code}`. Invia il token al tuo server, che lo decodifica con Google (playintegrity.googleapis.com … :decodeIntegrityToken) e verifica il nonce.
Esempio
Chiede a Google Play un token Play Integrity (richiesta classica). Il token è opaco sul telefono: il tuo SERVER lo invia a Google, legge il verdetto (app autentica? dispositivo autentico? con licenza?) e verifica che il nonce sia quello che ha emesso. È questo controllo sul server a rendere affidabile la risposta.
Restituisce: subito un id richiesta ('' quando la build non ha attivato l'integrità del dispositivo). La risposta arriva come evento appmint:integrity (e window.onAppMintIntegrity) con {requestId, token} o {requestId, error, code} - errori come nonce_too_short, nonce_is_not_base64, network_error, play_store_not_found, cloud_project_number_is_invalid, too_many_requests. Richiede: Controlli di integrità del dispositivo attivi nel passaggio Integrazione (Passaggio 3), la Play Integrity API abilitata per l'app (Play Console → Integrità dell'app) e - per un'app NON installata da Google Play - il numero del tuo progetto Google Cloud, nella procedura guidata o come secondo argomento ('' usa quello della build).
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());Il passaggio lato server - chiama POST https://playintegrity.googleapis.com/v1/<your.package>:decodeIntegrityToken con {"integrity_token": token} usando un service account del progetto Cloud collegato, poi controlla:
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, per il codice che non può ascoltare gli eventi:
window.onAppMintIntegrity = function (d) {
if (d.error) console.warn('integrity failed', d.error, d.code);
else sendToServer(d.token);
};
WebToApk.requestIntegrityToken(nonceFromServer, '123456789012');setCrashUserId bridge#
window.WebToApk.setCrashUserId(id: String): Boolean
Report di arresto anomalo: il TUO id per l'utente connesso (lettere, cifre e _ @ . : + -, fino a 128), allegato ai report da ora in poi così puoi contare gli utenti interessati e trovare gli arresti anomali di un singolo utente. "" lo cancella. Mai un indirizzo e-mail o un numero di telefono. Restituisce false per un id con altri caratteri, o quando i Report di arresto anomalo sono disattivati.
Esempio
Allega il TUO id dell'utente connesso ai report di arresto anomalo da ora in poi, così la schermata Report di arresto anomalo può contare quanti utenti sono interessati da un problema. Chiamala dopo l'accesso; chiamala con '' alla disconnessione.
Restituisce: true quando salvato; false per un id con caratteri diversi da lettere, cifre e _ @ . : + - (o più lungo di 128), o quando la build non ha attivato i Report di arresto anomalo. Richiede: Report di arresto anomalo attivi nel passaggio Integrazione (Passaggio 3). Usa l'id del tuo database, mai un indirizzo e-mail o un numero di telefono.
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 rifiutato, o una build senza Report di arresto anomalo, restituisce 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 addressInforma l'utente dell'ultimo arresto anomalo dopo un riavvio:
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 + ').');
}
}Note: l'id resta sul telefono finché non lo cambi e viene inviato solo nei report di arresto anomalo.