DEV Community

Roronoa
Roronoa

Posted on

Audit What Your Mobile AI Feature Actually Sends: Point It at a Server You Control First

Test environment for this walkthrough: Pixel 7 on Android 14, iPhone 13 on iOS 17.5, a React Native 0.74 app with a fetch-based LLM client, tested on Wi-Fi with one forced switch to LTE mid-request. The lifecycle transition I care about here is simple: the app goes from foreground to background while a request is in flight, and I want to know exactly what bytes left the device before the OS suspended the socket.

Most mobile AI features fail their first privacy review not because of what the model returns, but because of what the client sends. Device IDs, full conversation history, timezone, locale, clipboard residue, carrier info — I've seen all of these ride along in request bodies that the developer thought contained "just the prompt." The fix is not a policy document. The fix is pointing your app at an endpoint you control and reading the traffic yourself.

Why your staging backend is the wrong place to audit

Your real backend logs what it chose to log. A third-party LLM API logs nothing you can see. What you want is a dumb endpoint that records the raw request — headers, body, timing — and optionally forwards it to a model so the round trip still completes and your client code behaves normally.

For this kind of throwaway inspection server, I've been using MonkeyCode's free server option, which is enough to host a small logging endpoint without standing up my own infra for a one-day audit, and its free model access lets me complete the round trip without touching a paid API key. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Any endpoint you control works the same way — a $5 VPS, a local machine behind ngrok, whatever. The method matters more than the host.

The logging endpoint

This is the entire server. Node 20, no framework:

// audit-server.mjs — logs every request, optionally proxies to a model
import { createServer } from 'node:http';
import { appendFileSync } from 'node:fs';

const LOG = './requests.ndjson';

createServer((req, res) => {
  const chunks = [];
  const started = Date.now();
  req.on('data', (c) => chunks.push(c));
  req.on('end', () => {
    const body = Buffer.concat(chunks).toString('utf8');
    appendFileSync(LOG, JSON.stringify({
      ts: new Date().toISOString(),
      method: req.method,
      url: req.url,
      headers: req.headers,
      bodyBytes: Buffer.byteLength(body),
      body,
      clientClosedEarly: false,
      ms: Date.now() - started,
    }) + '\n');

    // Minimal fake completion so the client parses a normal response
    res.writeHead(200, { 'content-type': 'application/json' });
    res.end(JSON.stringify({ choices: [{ message: { content: 'audit-ok' } }] }));
  });
  req.on('close', () => {
    if (!res.writableEnded) {
      appendFileSync(LOG, JSON.stringify({
        ts: new Date().toISOString(),
        url: req.url,
        clientClosedEarly: true, // backgrounding or network switch killed the socket
      }) + '\n');
    }
  });
}).listen(8080);
Enter fullscreen mode Exit fullscreen mode

Point your app's base URL at it (http://<host>:8080 — temporarily allow cleartext on Android via networkSecurityConfig, or use HTTPS if your host terminates TLS). Now every request your feature makes is a line of NDJSON you can jq.

The test plan

Run each scenario three times. Record device, OS, app state, and what you observe.

# Scenario What you're checking
1 Foreground, Wi-Fi, fresh launch Baseline payload: what does a "clean" request contain?
2 Foreground, after 10 prior turns Does history grow unbounded? Are system prompts re-sent every turn?
3 Background mid-request (press home at ~50% of expected latency) Did the socket close (clientClosedEarly)? Did the client retry on foreground — and did the retry re-send everything?
4 Wi-Fi → LTE switch mid-request Same as 3, plus: does the retry include new device metadata?
5 Permission revoked (e.g., contacts/photos) while feature is idle Does the next request quietly include less data, or fail open and send a stale cached copy?
6 Airplane mode → restore Does the offline queue flush duplicates when connectivity returns?

Scenario 3 is the one that surprises people. On Android 14, my test app's socket was closed by the OS within seconds of backgrounding, the client retried on foreground, and the retry contained the full conversation history again — meaning a single user message produced two complete transmissions of the entire context. On iOS 17.5 the behavior differed: the request completed in background via a URLSession background task, so no retry, but the payload sat in a system-managed cache I hadn't accounted for.

What to grep for in the logs

# Which headers are you leaking?
jq -r '.headers | keys[]' requests.ndjson | sort -u

# Anything that looks like a device ID or location?
jq -r '.body' requests.ndjson | grep -iE 'idfa|gaid|device|lat|lon|locale|timezone'

# Payload growth across turns
jq -r '[.ts, .bodyBytes] | @tsv' requests.ndjson
Enter fullscreen mode Exit fullscreen mode

Red flags I've actually caught this way: an analytics SDK injecting its own headers into LLM requests, a retry queue persisting request bodies to disk (which then flowed into Android Auto Backup — a separate problem I wrote about earlier), and a "privacy mode" toggle that changed the UI but not the payload.

Limitations and who shouldn't use this

  • A logging endpoint sees what your client sends. It cannot tell you what the upstream model provider logs or retains — that's a contractual question, not a technical one.
  • Cleartext HTTP to your own host is fine for a lab audit; never ship the cleartext exception. Re-run the audit over HTTPS before release, because TLS pinning and proxy behavior can change what actually goes out.
  • This is a single-device method. OEM-specific background killers (I see you, aggressive battery optimizers) change scenario 3's outcome per device class, so repeat on at least one low-end Android before drawing conclusions.
  • Free hosted tiers are for audits and prototypes, not load tests or production traffic. Don't point your beta cohort at one.

If your feature handles health, financial, or children's data, this audit is necessary but nowhere near sufficient — you need a real threat model and probably legal review.

Try it and compare notes

The whole setup takes under an hour, and a free server plus a free model endpoint is enough to complete the loop without spending anything. If you run this, I'd genuinely like to hear: your device and OS, which scenario surprised you, and whether the failed requests recovered, restarted, or silently vanished. Drop your NDJSON red flags in the comments — the interesting bugs are always in what nobody meant to send.

Top comments (0)