Ghost Spector

Self-hosted · Android · Kotlin & Java

See inside your Android app, from your desk.

Every network request, the screen, the databases and the preferences of any install — a tester's phone across the office or a customer's across the country — live in one dashboard on your own server.

OkHttp & Retrofit HttpURLConnection Android 7.0+ One line of Gradle

HTTP Monitor

Production A Acme Shop
GET /v1/catalog?page=2 200 184 ms
POST /v1/cart/items 201 242 ms
GET /v1/profile 200 96 ms
POST /v1/checkout 422 318 ms
GET /v1/orders/5812 200 131 ms
PUT /v1/addresses/3 500 1.2 s
POST api.acme.shop/v1/cart/items

Status

201 Created

Duration

242 ms

Size

1.4 kB

Response

{
  "id": 5812,
  "sku": "TEA-250",
  "qty": 2,
  "total": "RM 37.80"
}

Copy as cURL · headers · request body

While you build

See what the app sent and what came back without a cable, a proxy or a certificate on the phone. Copy any call as cURL and replay it.

While it is tested

Watch a tester's phone as they go: its traffic, its screen, its data. Reach in and tap, type, or tell every tester something at once.

Once it ships

Point production builds at their own key. Staging and production never mix, and the same dashboard switches between them.

Features

Everything the app does, where you can see it

One library in the build. Everything below arrives on its own — nothing to configure per screen or per request.

HTTP Monitor

Every request, exactly as it happened

URL, method, status and timing, with the full headers and bodies of both sides — JSON and XML pretty-printed. Captured on the device, batched, and searchable long after the app was closed.

  • Filter by method, status, host or any part of the URL
  • Copy any call as cURL and replay it in a terminal
  • Redact Authorization, cookies or any header before it leaves the phone
  • A viewer inside the app too, for when there is no dashboard at hand
checkout POST 4xx · 5xx api.acme.shop
POST /v1/checkout 422 Unprocessable 318 ms
POST /v1/checkout 500 Server Error 1.4 s
POST /v1/checkout/retry 409 Conflict 207 ms
$ curl -X POST 'https://api.acme.shop/v1/checkout' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: ‹redacted›' \
  --data '{"cart":5812}'

Mocking

See the error screen without breaking the server

Make a rule, and a matching call answers the way you say — a 503, an empty list, a malformed body — on the phones you choose, while the backend carries on untouched. Or hold the call a few seconds, or fail it as a dead network would.

  • Respond with any status, headers and body, or delay, or fail
  • Start one from any captured request — Mock this response
  • One device, one build, or every build — switched on and off in a click
  • Ends after an hour unless kept; production asks you to confirm
Checkout is down POST api.acme.shop/v1/checkout Respond 503 Ends in 42 min
Slow catalog GET api.acme.shop/v1/catalog* Delay 3 s Until turned off
Empty cart GET api.acme.shop/v1/cart Respond 200 Off
POST /v1/checkout Mocked 503

Device Control

Drive the app on someone else's phone

Ask any install for a screenshot of the app, then click it to tap the phone. Every tap comes back as a fresh screenshot of what it did — and shows on the phone too, so whoever holds it sees exactly where you pressed.

  • Tap and long-press, press Back, type into whatever field has focus
  • Enter, Tab, Escape and Backspace, one click each
  • Sees only the app's own window — no permission to grant
  • Quiet when idle, quick the moment you start driving

Database & Shared Preferences

Its data, live, without pulling a file

Run SQL against the app's own SQLite databases on the phone, and read or change its SharedPreferences in place — the app sees a change the moment it is made, and the page says whether each one took.

  • Reads are always safe: the database opens read-only for them
  • A change needs a fresh backup first — taken in one click
  • Back up the databases and preferences files you choose, kept 30 days
  • Open any backup in a read-only SQL console, or download it
shop.db Backed up 2 min ago
SELECT sku, qty, updated_at
FROM cart_items WHERE qty > 1;
skuqtyupdated_at
TEA-250210:42:07
CUP-SET310:44:51
HONEY-500210:46:13
settings.xml dark_mode true — applied

Notifications

Tell every tester at once

Type a message and it pops up inside the app on every install you choose — over whatever screen is up. Send a picture to look at, or a file to open: a new build to install, say.

  • Text, an image to enlarge, or a file to open
  • Delivery tracked per device: seen, waiting, or never collected
  • Held for an app in the background, shown when it comes forward

Build 3.5 is ready

Checkout is fixed. Please install and try a card payment again.

acme-shop-3.5-beta.apk 24.1 MB · tap to install
OK
Aina's Pixel 8 Seen
QA Galaxy S24 Seen
Badri's Redmi Waiting for the app

Overview

How the app is doing, at a glance

Request volume, failure rate and latency over the last hour, day, week or month — against the period before — with the slowest endpoints and busiest hosts named. For one device, one build, or all of them.

  • The average response time, and the time 95% of requests finish within — so a slow few can't hide
  • Every device's online status, kept up to date on its own
  • Refreshes live while you watch

Requests

48.2k

+12%

Failed

1.8%

−0.6 pt

95% within

412 ms

+38 ms

Requests Failed Last 24 hours

Crashes & Logs

"It crashed" — now with the stack trace

A crash is kept on the phone and sent the moment the app starts again, grouped with every other crash that failed the same way — and shown beside the requests and log lines that led up to it. Log lines stream to a live page per device.

  • The same crash across devices and versions, counted as one
  • The requests and logs from the two minutes before it
  • Resolve it — it reopens by itself if it happens again
  • A live log tail, filtered by level, tag or text
IllegalStateException Cart is empty Acme Shop · v3.4.0, v3.5.0 · first 2d ago 14crashes
java.lang.IllegalStateException: Cart is empty
    at shop.acme.Cart.pay(Cart.kt:42)
    at shop.acme.CheckoutActivity.onClick(CheckoutActivity.kt:88)
    at android.view.View.performClick(View.java:7659)

10:46:12.408 I Checkout: Paying 37.80

10:46:12.913 W Payment: Card needs 3-D Secure

10:46:13.120 E Checkout: Cart was emptied mid-payment

Staging & production

Two keys, one dashboard, nothing mixed up

Every project has a staging key and a production key. The key a build reports with decides where its traffic lands; the top bar switches between them. Apps name themselves from their own build — tag a build to tell a white-label or a tester's copy apart.

Staging ghost_Ck7g••••••••••••wLGc
Production ghost_HNtH••••••••••••wfBj

Apps name themselves

Package, label and icon are read off the build. Nobody types a package name into the dashboard.

Tags for every variant

A white-label build or a per-tester copy becomes its own app with one argument.

Online, or not

A heartbeat a minute keeps each device's status honest, at a few kilobytes an hour.

Your server, your data

Self-hosted. Traffic goes from the phone to you, and nowhere else.

Secrets stay on the phone

Headers you name are redacted before anything is stored or sent.

Kind to data plans

Batched every 15 seconds, bodies capped, and only the backups you pick.

How it works

Up and reporting in three steps

Kotlin or Java, OkHttp or HttpURLConnection. Kept out of release builds unless you want it there.

  1. 1

    Add the library

    From the Ghost Spector Maven repository, as a debug-only dependency of the app module — so none of it reaches a release build unless you ask.

    // settings.gradle.kts
    repositories {
        maven { url = uri("https://maven.ghostspector.com") }
    }
    
    // app/build.gradle.kts
    dependencies {
        debugImplementation("com.ghostspector:ghostspector:2.0.1")
    }
  2. 2

    Start it, and point it here

    Once, in Application.onCreate, with the key your project was given — the staging key for a staging build. Turn on every feature, or only the ones you want. Then add the interceptor to your OkHttp client; Retrofit is covered with it.

    GhostSpector.with(this, BuildConfig.GHOST_ENDPOINT, BuildConfig.GHOST_STAGING_KEY)
        .redactHeaders("Authorization", "Cookie")
        .enableAllMonitor()   // or .enableHttpMonitor(), .enableDeviceControl(), …
        .start()
    
    val client = OkHttpClient.Builder()
        .addInterceptor(HttpMonitorInterceptor())
        .build()
  3. 3

    Open the dashboard

    The app and its device appear on their first batch — named, with the app's own icon. Pick them in the top bar and every page follows.

    Environment Staging
    App Acme Shop
    Tag beta
    Device Aina's Pixel 8

Your app's traffic is already on its way

Sign in to see what your builds are doing right now — every request, every device, both environments.