# Crash reports and device integrity - JavaScript bridge API

> Errors the page logs, the last native crash, and whether the phone looks rooted or modified, with an optional Play Integrity token for your server.

- **Applies to:** AppMint
- **Source:** extracted from the app runtime and its per-method example files; this page is generated from them.
- **HTML:** https://freewebtoapk.com/docs/api/integrity

### `AppMint.integrity`

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

**Example**

Promise versions of `getDeviceIntegrity` and `requestIntegrityToken`, present before your page's first script when the build turned on **Device integrity checks** (Integrate step, Step 3).

**Returns:** `check()` resolves with the heuristics object (`{rooted, emulator, debuggerAttached, signals, installer, installedFromPlay, signatureSha256, method: 'heuristic'}`); `token(nonce)` resolves with the Play Integrity token string, or rejects with an Error whose message is the error name (`not_enabled`, `nonce_too_short`, `network_error`, …) and `code` the Play error code. The heuristics are hints; only your server's decode of the token is a verdict.

```js
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);
  }
}
```

### `getDeviceIntegrity`

```js
window.WebToApk.getDeviceIntegrity(): String
```

Local root / emulator / debugger HEURISTICS (see DeviceIntegrity) as JSON: `{rooted, emulator, debuggerAttached, signals:[…], installer, installedFromPlay, signatureSha256, method:"heuristic", sdk}`. A clean result means "no sign found", not proof - for a verdict you can trust, send a Play Integrity token to your server (requestIntegrityToken). `{"error":"not_enabled"}` unless the build turned on Device integrity. Blocks for a few milliseconds (it reads files): call it once.

**Example**

Local root, emulator and debugger HEURISTICS as a JSON string. A clean result means "no sign found", not proof: a determined user with a root-hiding module passes. Use it to warn or to add a check; for a verdict you can trust, send a Play Integrity token to your server (`requestIntegrityToken`).

**Returns:** `'{"rooted":false,"emulator":false,"debuggerAttached":false,"signals":[],"installer":"com.android.vending","installedFromPlay":true,"signatureSha256":"AB:CD:…","method":"heuristic","sdk":34}'`, or `'{"error":"not_enabled"}'`. Signals name what was found: `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`. **Needs:** turn on **Device integrity checks** in the Integrate step (Step 3). It takes a few milliseconds (it reads files): call it once, not in a loop.

```js
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)');
```

**Check your own signature** - a repackaged copy of your app is signed with another key:

```js
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();
```

### `getLastCrash`

```js
window.WebToApk.getLastCrash(): String
```

The newest crash this install recorded (the app closing on an uncaught error, or the page renderer crashing), as JSON `{kind, message, stack, url, at, count, appVersion, user, fp}`, or "" when there has been none (or Crash reports are off). Kept after upload, so a page can say "sorry, the app closed last time".

**Example**

The newest crash this install recorded - the app closing on an uncaught error (`kind: "native"`) or the page renderer crashing (`kind: "renderer"`) - as a JSON string, or `''` when there has been none.

**Returns:** `'{"kind","message","stack","url","at","count","appVersion","user","fp"}'` or `''`. Also `''` when the build did not turn on Crash reports. **Needs:** **Crash reports** on in the Integrate step (Step 3).

**Say sorry once after a crash:**

```js
(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.');
})();
```

**Notes:** it stays after the report is uploaded, so compare `at` with what you stored to show the message once. Page errors and `logError` calls are not crashes and never appear here.

### `logError`

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

Crash reports: records an error your page CAUGHT (a failed payment call, a parse error) so it appears in AppMint's Crash reports screen next to the crashes, grouped and counted. [stack]: `err.stack` or "". Returns false when the build did not turn on Crash reports, when both are empty, or when the same error was already logged many times this session. Never put personal data (e-mail, phone, tokens) in the message.

**Example**

Records an error your page CAUGHT (a failed payment call, a JSON parse error) so it appears in AppMint's **Crash reports** screen (Update the App → your app → Crash reports), grouped with the crashes and counted. Uncaught errors and unhandled promise rejections are recorded for you; this is for the ones you handled.

**Returns:** `true` when recorded; `false` when the build did not turn on Crash reports, when both arguments are empty, or when the same error was already logged many times this session. Reports upload the next time the app opens. **Needs:** turn on **Crash reports** in the Integrate step (Step 3) when you build. Never put personal data (e-mail address, phone number, tokens) in the message.

**Log a handled error:**

```js
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');
  }
}
```

**Report failed API calls** without the address' query string (it may carry tokens):

```js
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;
}
```

**Notes:** reports with the same cause are grouped even when numbers in the message differ (`item 42` and `item 7`). The page address is stored without its query and fragment.

### `requestIntegrityToken`

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

Asks Google Play for a Play Integrity token (classic request). [nonce]: URL-safe Base64, 16-500 characters, made by YOUR server for this one request. [cloudProjectNumber]: your Google Cloud project number, or "" to use the one set in the build (an app installed from Play can leave both empty). Returns a request id at once ("" when the build did not turn on Device integrity); the answer arrives as `appmint:integrity` / `window.onAppMintIntegrity({requestId, token})` or `{requestId, error, code}`. Send the token to your server, which decodes it with Google (playintegrity.googleapis.com … :decodeIntegrityToken) and checks the nonce.

**Example**

Asks Google Play for a **Play Integrity** token (classic request). The token is opaque on the phone: your SERVER sends it to Google, reads the verdict (genuine app? genuine device? licensed?) and checks that the nonce is the one it issued. That server check is what makes the answer trustworthy.

**Returns:** a request id at once (`''` when the build did not turn on Device integrity). The answer arrives as the `appmint:integrity` event (and `window.onAppMintIntegrity`) with `{requestId, token}` or `{requestId, error, code}` - errors such as `nonce_too_short`, `nonce_is_not_base64`, `network_error`, `play_store_not_found`, `cloud_project_number_is_invalid`, `too_many_requests`. **Needs:** **Device integrity checks** on in the Integrate step (Step 3), the Play Integrity API enabled for the app (Play Console → App integrity), and - for an app NOT installed from Google Play - your Google Cloud project number, in the wizard or as the second argument (`''` uses the build's).

```js
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());
```

**The server step** - call `POST https://playintegrity.googleapis.com/v1/<your.package>:decodeIntegrityToken` with `{"integrity_token": token}` using a service account of the linked Cloud project, then check:

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

**The callback form**, for code that cannot listen to events:

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

### `setCrashUserId`

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

Crash reports: YOUR id for the signed-in user (letters, digits and _ @ . : + -, up to 128), attached to reports from now on so you can count affected users and find one user's crashes. "" clears it. Never an e-mail address or phone number. Returns false for an id with other characters, or when Crash reports are off.

**Example**

Attaches YOUR id for the signed-in user to crash reports from now on, so the Crash reports screen can count how many users a problem affects. Call it after sign-in; call it with `''` on sign-out.

**Returns:** `true` when saved; `false` for an id with characters other than letters, digits and `_ @ . : + -` (or longer than 128), or when the build did not turn on Crash reports. **Needs:** **Crash reports** on in the Integrate step (Step 3). Use your database id, never an e-mail address or phone number.

```js
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
}
```

**Notes:** the id stays on the phone until you change it and is only sent inside crash reports.

