API jembatan JavaScript
Laporan crash dan integritas perangkat
Kesalahan yang dicatat halaman, crash native terakhir, dan apakah ponsel tampak di-root atau dimodifikasi, dengan token Play Integrity opsional untuk server Anda.
AppMint.integrity helper#
AppMint.integrity.check() / AppMint.integrity.token(nonce, cloudProjectNumber?)
Contoh
Versi Promise dari getDeviceIntegrity dan requestIntegrityToken, tersedia sebelum skrip pertama halaman Anda saat build mengaktifkan Pemeriksaan integritas perangkat (langkah Integrasi, Langkah 3).
Mengembalikan: check() menghasilkan objek heuristik ({rooted, emulator, debuggerAttached, signals, installer, installedFromPlay, signatureSha256, method: 'heuristic'}); token(nonce) menghasilkan string token Play Integrity, atau ditolak dengan Error yang pesannya adalah nama kesalahan (not_enabled, nonce_too_short, network_error, …) dan code kode kesalahan Play. Heuristik hanyalah petunjuk; hanya hasil dekode token oleh server Anda yang menjadi putusan.
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);
}
}Beri peringatan saat aplikasi adalah salinan yang dikemas ulang:
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);
}Tangani kegagalan token menurut namanya:
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 jembatan#
window.WebToApk.getDeviceIntegrity(): String
HEURISTIK root / emulator / debugger lokal (lihat DeviceIntegrity) sebagai JSON: `{rooted, emulator, debuggerAttached, signals:[…], installer, installedFromPlay, signatureSha256, method:"heuristic", sdk}`. Hasil bersih berarti "tidak ada tanda ditemukan", bukan bukti - untuk putusan yang dapat dipercaya, kirim token Play Integrity ke server Anda (requestIntegrityToken). `{"error":"not_enabled"}` kecuali build mengaktifkan Integritas perangkat. Memblokir beberapa milidetik (membaca berkas): panggil sekali.
Contoh
HEURISTIK root, emulator, dan debugger lokal sebagai string JSON. Hasil bersih berarti "tidak ada tanda ditemukan", bukan bukti: pengguna yang gigih dengan modul penyembunyi root lolos. Gunakan untuk memberi peringatan atau menambah pemeriksaan; untuk putusan yang dapat dipercaya, kirim token Play Integrity ke server Anda (requestIntegrityToken).
Mengembalikan: '{"rooted":false,"emulator":false,"debuggerAttached":false,"signals":[],"installer":"com.android.vending","installedFromPlay":true,"signatureSha256":"AB:CD:…","method":"heuristic","sdk":34}', atau '{"error":"not_enabled"}'. Sinyal menyebut apa yang ditemukan: 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. Memerlukan: aktifkan Pemeriksaan integritas perangkat di langkah Integrasi (Langkah 3). Butuh beberapa milidetik (membaca berkas): panggil sekali, jangan dalam perulangan.
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)');Periksa tanda tangan Anda sendiri - salinan aplikasi Anda yang dikemas ulang ditandatangani dengan kunci lain:
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();Minta putusan yang dapat dipercaya dari server dengan token Play Integrity (jawabannya datang sebagai event).
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 jembatan#
window.WebToApk.getLastCrash(): String
Crash terbaru yang dicatat pemasangan ini (aplikasi menutup karena kesalahan yang tidak tertangkap, atau renderer halaman crash), sebagai JSON `{kind, message, stack, url, at, count, appVersion, user, fp}`, atau "" jika belum ada (atau Laporan crash mati). Disimpan setelah diunggah, sehingga halaman dapat berkata "maaf, aplikasi tertutup terakhir kali".
Contoh
Crash terbaru yang dicatat pemasangan ini - aplikasi menutup karena kesalahan yang tidak tertangkap (kind: "native") atau renderer halaman crash (kind: "renderer") - sebagai string JSON, atau '' jika belum ada.
Mengembalikan: '{"kind","message","stack","url","at","count","appVersion","user","fp"}' atau ''. Juga '' saat build tidak mengaktifkan Laporan crash. Memerlukan: Laporan crash aktif di langkah Integrasi (Langkah 3).
Minta maaf sekali setelah crash:
(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.');
})();Unggah crash tersimpan ke server Anda sendiri sekali, bersama versi aplikasi tempat terjadinya.
(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)); });
})();Cabangkan menurut jenisnya untuk memberi pesan yang sesuai.
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.');
}Catatan: ini tetap ada setelah laporan diunggah, jadi bandingkan at dengan yang Anda simpan untuk menampilkan pesan sekali saja. Kesalahan halaman dan panggilan logError bukan crash dan tidak pernah muncul di sini.
logError jembatan#
window.WebToApk.logError(message: String, stack: String): Boolean
Laporan crash: mencatat kesalahan yang DITANGKAP halaman Anda (panggilan pembayaran yang gagal, kesalahan parse) agar muncul di layar Laporan crash AppMint di samping crash, dikelompokkan dan dihitung. [stack]: `err.stack` atau "". Mengembalikan false saat build tidak mengaktifkan Laporan crash, saat keduanya kosong, atau saat kesalahan yang sama sudah dicatat berkali-kali pada sesi ini. Jangan pernah menaruh data pribadi (e-mail, telepon, token) di pesan.
Contoh
Mencatat kesalahan yang DITANGKAP halaman Anda (panggilan pembayaran yang gagal, kesalahan parse JSON) agar muncul di layar Laporan crash AppMint (Perbarui Aplikasi → aplikasi Anda → Laporan crash), dikelompokkan dengan crash dan dihitung. Kesalahan yang tidak tertangkap dan penolakan promise yang tidak tertangani dicatat untuk Anda; ini untuk yang sudah Anda tangani.
Mengembalikan: true saat tercatat; false saat build tidak mengaktifkan Laporan crash, saat kedua argumen kosong, atau saat kesalahan yang sama sudah dicatat berkali-kali pada sesi ini. Laporan diunggah saat aplikasi dibuka berikutnya. Memerlukan: aktifkan Laporan crash di langkah Integrasi (Langkah 3) saat Anda membuat build. Jangan pernah menaruh data pribadi (alamat e-mail, nomor telepon, token) di pesan.
Catat kesalahan yang ditangani:
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');
}
}Laporkan panggilan API yang gagal tanpa query string pada alamat (mungkin membawa 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;
}Catat kesalahan yang ditangkap dengan id pengguna, agar masalah satu pengguna dapat ditemukan nanti.
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)');
}
}Catatan: laporan dengan penyebab yang sama dikelompokkan meski angka dalam pesannya berbeda (item 42 dan item 7). Alamat halaman disimpan tanpa query dan fragmennya.
requestIntegrityToken jembatan#
window.WebToApk.requestIntegrityToken(nonce: String, cloudProjectNumber: String): String
Meminta token Play Integrity dari Google Play (permintaan klasik). [nonce]: Base64 aman-URL, 16-500 karakter, dibuat oleh server ANDA untuk satu permintaan ini. [cloudProjectNumber]: nomor proyek Google Cloud Anda, atau "" untuk memakai yang diatur di build (aplikasi yang dipasang dari Play boleh mengosongkan keduanya). Mengembalikan id permintaan seketika ("" jika build tidak mengaktifkan Integritas perangkat); jawabannya datang sebagai `appmint:integrity` / `window.onAppMintIntegrity({requestId, token})` atau `{requestId, error, code}`. Kirim token ke server Anda, yang mendekodenya bersama Google (playintegrity.googleapis.com … :decodeIntegrityToken) dan memeriksa nonce.
Contoh
Meminta token Play Integrity dari Google Play (permintaan klasik). Token ini buram di ponsel: SERVER Anda mengirimnya ke Google, membaca putusan (aplikasi asli? perangkat asli? berlisensi?) dan memeriksa bahwa nonce adalah yang diterbitkannya. Pemeriksaan server itulah yang membuat jawabannya dapat dipercaya.
Mengembalikan: id permintaan seketika ('' jika build tidak mengaktifkan Integritas perangkat). Jawabannya datang sebagai event appmint:integrity (dan window.onAppMintIntegrity) dengan {requestId, token} atau {requestId, error, code} - kesalahan seperti nonce_too_short, nonce_is_not_base64, network_error, play_store_not_found, cloud_project_number_is_invalid, too_many_requests. Memerlukan: Pemeriksaan integritas perangkat aktif di langkah Integrasi (Langkah 3), Play Integrity API diaktifkan untuk aplikasi (Play Console → Integritas aplikasi), dan - untuk aplikasi yang TIDAK dipasang dari Google Play - nomor proyek Google Cloud Anda, di wizard atau sebagai argumen kedua ('' memakai milik 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());Langkah server - panggil POST https://playintegrity.googleapis.com/v1/<your.package>:decodeIntegrityToken dengan {"integrity_token": token} memakai akun layanan dari proyek Cloud yang terhubung, lalu periksa:
const p = decoded.tokenPayloadExternal;
const ok = p.requestDetails.nonce === issuedNonce &&
p.appIntegrity.appRecognitionVerdict === 'PLAY_RECOGNIZED' &&
p.deviceIntegrity.deviceRecognitionVerdict.includes('MEETS_DEVICE_INTEGRITY');Bentuk callback, untuk kode yang tidak dapat mendengarkan event:
window.onAppMintIntegrity = function (d) {
if (d.error) console.warn('integrity failed', d.error, d.code);
else sendToServer(d.token);
};
WebToApk.requestIntegrityToken(nonceFromServer, '123456789012');setCrashUserId jembatan#
window.WebToApk.setCrashUserId(id: String): Boolean
Laporan crash: id ANDA untuk pengguna yang masuk (huruf, angka dan _ @ . : + -, hingga 128), dilampirkan ke laporan mulai sekarang sehingga Anda dapat menghitung pengguna yang terpengaruh dan menemukan crash satu pengguna. "" menghapusnya. Jangan pernah alamat e-mail atau nomor telepon. Mengembalikan false untuk id dengan karakter lain, atau saat Laporan crash mati.
Contoh
Melampirkan id ANDA untuk pengguna yang masuk ke laporan crash mulai sekarang, sehingga layar Laporan crash dapat menghitung berapa banyak pengguna yang terpengaruh suatu masalah. Panggil setelah masuk; panggil dengan '' saat keluar.
Mengembalikan: true saat tersimpan; false untuk id dengan karakter selain huruf, angka, dan _ @ . : + - (atau lebih dari 128), atau saat build tidak mengaktifkan Laporan crash. Memerlukan: Laporan crash aktif di langkah Integrasi (Langkah 3). Gunakan id database Anda, jangan pernah alamat e-mail atau nomor telepon.
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
}Id yang ditolak, atau build tanpa Laporan crash, mengembalikan 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 addressBeri tahu pengguna tentang crash terakhir setelah restart:
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 + ').');
}
}Catatan: id tetap di ponsel sampai Anda mengubahnya dan hanya dikirim di dalam laporan crash.