Skip to the guide
TestForge · Mobile SDK guide

TestForge mobile SDK · version 0.2.4

Bug reports from inside your app

Add the TestForge SDK to a test build of your app. Your testers shake the phone, mark up a screenshot or record the screen, and send. The report arrives in your TestForge workspace with the steps that led up to it, the logs, and the app version, ready to become a bug.

Start the setup What you need Troubleshooting
Android full Capacitor / hybrid full on Android Web JavaScript iOS early (0.1)
On this page

What it is and what your testers get

The SDK is a small library you add to your own app. It turns every test build into a bug-reporting tool, with nothing for your testers to install or sign in to.

Shake to report

Two quick shakes open the report form. You can also add your own "Report a bug" button.

Screenshot with drawing

The current screen, with text fields blacked out. Testers circle the problem with Pen, Undo and Clear.

Screen recording

Up to 5 minutes, no audio. The phone asks the tester every time, and the video pauses while the TestForge form is on screen.

Crashes and freezes

Captured automatically: crashes with their stack trace, and freezes where the app stops responding for 5 seconds.

Steps and logs

The screens and buttons used before the report, failed requests, and console, network and app logs. Testers see a preview and choose what to attach.

A link to the report

After sending, the tester sees Report sent with Copy link and Share. The link opens the report for anyone in your workspace.

What you see in TestForge

  • Bugs → Inbox. Every report lands in the Inbox tab of Bugs, beside your bug list, in the project you registered the app in. The Bugs menu item shows how many reports are open. Repeats of the same crash are grouped, with a count (×3).
  • The From app badge and the build chip. Every report is marked as coming from your app, next to a chip naming the build it came from, for example YourApp 1.4-test (1042) · Debug: the app, its version, the build number in brackets, and the build channel.
  • The App & build card. Inside a report: the app and its bundle ID, version and build number, channel, the web bundle version (hybrid apps), the SDK versions, the platform and OS, the device, the language setting and the screen orientation.
  • Filters. Narrow the Inbox by status, Build channel and App version (the last two appear once reports come from more than one), by type (Bugs, Crashes, Freezes), or search by title, screen, device or version.
  • Create bug. One click turns a report into a normal TestForge bug, with the steps filled in and the screenshot, replay, recording and logs attached. The bug keeps the From app badge and build chip in your bug list. Its Found in build field (labelled Build) holds the build chip, and Environment says Tester build · Debug. A link leads back to the tester's report.

Reports are evidence for your team to look at. Nothing becomes a bug until someone on your team decides it should.

Before you start

The Mobile SDK feature must be on

The SDK is a TestForge feature called Mobile bug-report SDK (mobileSdk). It is off until it is switched on for your workspace. Turn on the bug tracker as well, so reports appear in Bugs → Inbox and can become bugs.

  • On the hosted TestForge service: ask TestForge support to switch the Mobile SDK on for your workspace.
  • On a TestForge server your company runs: a platform administrator turns on Mobile bug-report SDK under Admin → Settings → Feature flags, for everyone or for chosen organizations.

The SDK is not tied to a particular plan in the app: what matters is that the feature is on for your workspace. The people who read and triage reports use their normal TestForge seats. Testers who only send reports from the app don't need a TestForge account. See cost and limits.

Who does what

WhoWhat they do
A workspace adminRegisters the app in TestForge and copies its key (step 1). Only organization administrators see the setup panel.
An app developerAdds the SDK to a test build, asks for consent, and ships the build to testers (steps 2 and 3).
TestersInstall the test build and shake to report. They need no TestForge account.
Your QA teamReads reports in the Inbox, creates bugs, and shares links. Opening a report needs a TestForge sign-in to your workspace.

Supported platforms

Your appSupportWhat you get
Native Android
Views, AppCompat, Jetpack Compose
Full · 0.2.4Everything on this page: report form, screenshot with drawing, visual replay, screen recording, crashes, freezes, app log, report link. Android 6 (API 23) or later; WebView capture needs Android 8; OS crash and freeze records need Android 11.
Hybrid on Android
Capacitor or another WebView app
Full · 0.2.4Everything above, plus readable web steps (routes, button names, forms) and the web console and network logs. Uses the Android SDK and the JavaScript SDK together.
Web app or PWAJavaScript · 0.2.4JavaScript errors, freezes of the page, readable steps, console and network logs, and a simple web report form. No screenshots, screen recording or visual replay.
iOSEarly · 0.1Crashes and freezes (delivered by iOS, often a day later), shake to report with a simple title-and-message form, and screen names you send. No screenshots, recording, logs or report link yet. In a hybrid iOS app the JavaScript SDK runs on its own. See iOS status.

Set up in five steps

Plan about an hour for a native Android app, a little longer for a hybrid app. Examples use com.example.app and a placeholder key; use your own.

  1. Register your app in TestForge and copy the key

    1. Sign in to TestForge as a workspace admin and choose the project the reports should go to.
    2. Open Bugs → Inbox and expand SDK apps and installation at the top of the Inbox. This setup panel is where you register apps and manage their keys.
    3. Enter an App name, the Bundle or package ID (for example com.example.app) and the SDK platform: android, ios, web, or All platforms.
    4. Click Register SDK app.
    5. Click Copy SDK key and keep the key somewhere safe for your developer. It is shown only once. Copy the SDK endpoint shown below it too; it looks like https://testforge.example.com/api/public/mobile-sdk/events.
    Tip

    Pick the platform your app reports as. A hybrid app whose web layer can also run in a browser, or one key shared by several builds, should be All platforms. A mismatch makes TestForge reject the reports (see Troubleshooting).

    The key is tfm_ followed by exactly 43 characters. Copy all of it: from version 0.2.4 the SDK checks the length when your app starts and refuses a shortened key with the message tfm_ followed by 43 characters. The key can only send reports for this one app; it can't read anything (more about the key). From the same panel you can Rotate key (the old key stops working at once) or Revoke it. A project can have up to 25 registered apps.

  2. Install the SDK

    The SDK comes as source packages: an Android library module and a JavaScript package. It is not on Maven Central or the public npm registry yet. Ask TestForge support for the 0.2.4 SDK files, copy them into your repository (for example under third_party/testforge-mobile/), and keep both parts at the same version.

    Requirements: minSdk 23, Java 11, compileSdk 36. The module brings OkHttp 4.12.0 with it.

    settings.gradle.kts
    include(":testforge-mobile")
    project(":testforge-mobile").projectDir = file("third_party/testforge-mobile/android")
    app/build.gradle.kts
    android {
        buildFeatures { buildConfig = true }
        defaultConfig {
            buildConfigField("String", "TESTFORGE_ENDPOINT", "\"https://testforge.example.com/api/public/mobile-sdk/events\"")
            buildConfigField("String", "TESTFORGE_KEY", "\"tfm_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"")
        }
        buildTypes {
            getByName("debug") { buildConfigField("boolean", "TESTFORGE_TEST_BUILD", "true") }
            getByName("release") { buildConfigField("boolean", "TESTFORGE_TEST_BUILD", "false") }
        }
    }
    dependencies {
        implementation(project(":testforge-mobile"))
    }

    One fix in the copied module: its build.gradle pins the Android Gradle Plugin so it can build on its own. Inside your project, remove the version so Gradle uses yours: plugins { id 'com.android.library' }.

    A Capacitor app uses both parts. Add the Android module exactly as on the Android tab, then install the JavaScript package in your web project:

    npm install ./third_party/testforge-mobile/js

    It installs as @testforge/mobile-sdk, with TypeScript types and no dependencies. The two parts are connected in step 3 and in Hybrid apps.

    npm install ./third_party/testforge-mobile/js

    It installs as @testforge/mobile-sdk, an ES module with TypeScript types and no dependencies. It needs fetch and the browser's crypto, which every current browser has.

    Because the SDK sends reports from the page, the TestForge server must accept your page's web address (its origin, for example https://app.example.com). Send it to TestForge support, or to whoever runs your TestForge server, before you test.

  3. Initialise the SDK and ask for consent

    Create one SDK client when the app starts. Nothing is collected until your app turns consent on, after the user has said yes. The SDK doesn't remember that choice, so store it yourself and turn consent on again at each launch.

    App.kt
    package com.example.app
    
    import android.app.Application
    import android.content.Context
    import com.testforge.mobile.TestForgeConfig
    import com.testforge.mobile.TestForgeMobile
    
    class App : Application() {
        lateinit var testForge: TestForgeMobile
            private set
    
        override fun onCreate() {
            super.onCreate()
            val test = BuildConfig.TESTFORGE_TEST_BUILD
            val config = TestForgeConfig.Builder()
                .visualReplay(test)      // masked replay of the last seconds
                .screenRecording(test)   // needs the manifest entries below
                .appLogs(test)           // your app's own log lines
                .build()
            testForge = TestForgeMobile(this, BuildConfig.TESTFORGE_ENDPOINT,
                BuildConfig.TESTFORGE_KEY, BuildConfig.VERSION_NAME, config)
            // Consent starts off at every launch: restore the user's choice.
            if (ConsentStore.isGranted(this)) testForge.setConsent(true)
        }
    }
    
    // Your own storage for the choice. Don't use "testforge_mobile_prefs":
    // the SDK clears that file when consent is turned off.
    object ConsentStore {
        private const val FILE = "diagnostics_consent"
        fun isGranted(c: Context) = c.getSharedPreferences(FILE, Context.MODE_PRIVATE).getBoolean("granted", false)
        fun set(c: Context, granted: Boolean) = c.getSharedPreferences(FILE, Context.MODE_PRIVATE).edit().putBoolean("granted", granted).apply()
    }
    When the user answers your consent prompt
    fun onConsentChosen(activity: Activity, granted: Boolean) {
        ConsentStore.set(activity, granted)
        val sdk = (activity.application as App).testForge
        sdk.setConsent(granted)
        if (granted) sdk.attach(activity)   // the screen that is already open
    }
    App.java
    package com.example.app;
    
    import android.app.Application;
    import android.content.Context;
    import com.testforge.mobile.TestForgeConfig;
    import com.testforge.mobile.TestForgeMobile;
    
    public class App extends Application {
        private TestForgeMobile testForge;
    
        @Override public void onCreate() {
            super.onCreate();
            boolean test = BuildConfig.TESTFORGE_TEST_BUILD;
            TestForgeConfig config = new TestForgeConfig.Builder()
                .visualReplay(test)
                .screenRecording(test)
                .appLogs(test)
                .build();
            testForge = new TestForgeMobile(this, BuildConfig.TESTFORGE_ENDPOINT,
                BuildConfig.TESTFORGE_KEY, BuildConfig.VERSION_NAME, config);
            if (ConsentStore.isGranted(this)) testForge.setConsent(true);
        }
    
        public TestForgeMobile testForge() { return testForge; }
    }
    
    final class ConsentStore {
        private static final String FILE = "diagnostics_consent";
        static boolean isGranted(Context c) { return c.getSharedPreferences(FILE, Context.MODE_PRIVATE).getBoolean("granted", false); }
        static void set(Context c, boolean granted) { c.getSharedPreferences(FILE, Context.MODE_PRIVATE).edit().putBoolean("granted", granted).apply(); }
    }
    When the user answers your consent prompt
    void onConsentChosen(Activity activity, boolean granted) {
        ConsentStore.set(activity, granted);
        TestForgeMobile sdk = ((App) activity.getApplication()).testForge();
        sdk.setConsent(granted);
        if (granted) sdk.attach(activity);
    }

    For a web app. In a Capacitor app, start the web layer as shown in Hybrid apps, in addition to the Android code.

    src/testforge.ts
    import { createTestForge } from '@testforge/mobile-sdk';
    
    export const testForge = createTestForge({
      endpoint: 'https://testforge.example.com/api/public/mobile-sdk/events',
      apiKey: 'tfm_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
      appVersion: '1.4.0',
      platform: 'web',
      persistQueue: true,   // keep unsent reports across reloads (only while consent is on)
    });
    
    const CONSENT = 'diagnostics-consent';
    if (localStorage.getItem(CONSENT) === 'yes') testForge.setConsent(true);
    
    export async function allowDiagnostics() {
      localStorage.setItem(CONSENT, 'yes');
      testForge.setConsent(true);
      await testForge.enableShake();   // optional; call it from the click handler
    }
    
    export function stopDiagnostics() {
      localStorage.removeItem(CONSENT);
      testForge.setConsent(false);     // stops capture and deletes unsent reports
    }

    Register the class in AndroidManifest.xml with <application android:name=".App" …>. Suggested consent wording: "Send diagnostic reports to the developers: crash and freeze details, the screens and buttons you used, and, when you report a bug, a screenshot with text fields blacked out."

    Screen recording (Android)

    Recording is off unless you turn on screenRecording(true) and add these permissions. Put them in src/debug/AndroidManifest.xml so they only exist in test builds:

    app/src/debug/AndroidManifest.xml
    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
        <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
        <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
        <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    </manifest>

    Without the first two, Record screen doesn't appear. The notification permission is optional; without it the notification has no Stop button, and testers stop with the floating Stop recording pill instead.

  4. Send a test report

    Install the test build on a real phone, accept your consent prompt, then shake the phone twice. Or give testers a button:

    reportButton.setOnClickListener { testForge.requestReport() }
    reportButton.setOnClickListener(v -> testForge.requestReport());
    reportButton.addEventListener('click', () => testForge.showReporter());

    On Android the report form opens with a screenshot. Draw on it, type a Title or What happened?, pick a Severity, optionally open Attach logs to see exactly what will be sent, and tap Send. After a few seconds the form shows Report sent with a link, Copy link and Share.

    On the web, showReporter() opens a small form with Summary, What happened?, Attach logs and Send report.

  5. Find it in the Inbox

    In TestForge, open Bugs → Inbox in the project you registered the app in. Your report is at the top with the From app badge and its build chip, for example YourApp 1.4-test (1042) · Debug. Open it to see the screenshot, the steps and the logs. Or open the link from Report sent on a computer where you're signed in.

    Nothing there after a minute? Go to Troubleshooting. The most common causes are consent not turned on, a key that was rotated, or the wrong platform.

Use it only in test builds

The SDK is built for testers using test builds of your app. Keep it, and especially screenshots, recording and logs, out of the builds you publish to the stores.

  • Switch the rich capture on by build type. The TESTFORGE_TEST_BUILD flag from step 2 is true in debug builds and false in release builds, and the config in step 3 uses it for visual replay, recording and app logs.
  • Or don't create the client at all in store builds. If no client exists, nothing is captured or sent and shake does nothing:
    var testForge: TestForgeMobile? = null
    
    if (BuildConfig.TESTFORGE_TEST_BUILD) {
        testForge = TestForgeMobile(this, BuildConfig.TESTFORGE_ENDPOINT,
            BuildConfig.TESTFORGE_KEY, BuildConfig.VERSION_NAME, config)
    }
  • Keep recording permissions in src/debug. Google Play asks every app that declares FOREGROUND_SERVICE_MEDIA_PROJECTION to explain it in the Play Console. If the permission is only in test builds, your store listing is unaffected.
  • Leave the JavaScript SDK out of production bundles. Load it with a dynamic import() behind a build-time variable (see Hybrid apps). After a production build, grep -ri testforge dist/ should find nothing but your own wrapper.
  • Give every test build a new version code. It's the build number in the brackets of the build chip, so reports are easy to tell apart in the Inbox. It also matters for apps with live web updates (see below).

Label your builds with a channel

The build channel says how a build reached your testers, for example Debug, Play internal, Firebase or TestFlight. It appears at the end of the build chip, on the App & build card and on bugs created from a report (Environment: Tester build · Play internal). You can also filter the Inbox by it. That makes it quick to tell this morning's internal build from last week's debug build, and to look at one channel's reports only.

  • Android sends the channel Debug on its own for debuggable builds, and the build number (your versionCode) on every report. Set a channel for other builds with buildChannel(…). An empty string sends none.
  • Web and hybrid pages send nothing by default. Pass buildChannel and, for a plain web app, buildNumber. In a hybrid app the Android layer already sends the build number.
  • Channels are short labels: letters, digits, spaces and . _ + -, up to 32 characters. Don't put personal data in them.
val config = TestForgeConfig.Builder()
    .buildChannel("Play internal")   // default: "Debug" for debuggable builds
    .visualReplay(test)
    .build()
TestForgeConfig config = new TestForgeConfig.Builder()
    .buildChannel("Play internal")   // default: "Debug" for debuggable builds
    .visualReplay(test)
    .build();
createTestForge({
  endpoint: 'https://testforge.example.com/api/public/mobile-sdk/events',
  apiKey: 'tfm_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
  appVersion: '1.4.0',
  buildChannel: 'Staging',   // shown on reports and filterable in the Inbox
  buildNumber: '1042',       // web apps; in a hybrid app Android sends it
});
If you ship it anyway

Every user must agree first: the SDK sends nothing without consent. Tell users what is collected, declare it in Google Play's Data safety form and Apple's privacy details, and keep visual replay, recording and logs off.

Hybrid apps (Capacitor and WebView)

A hybrid app has two layers, and the SDK has a part for each. The Android SDK owns the report form, screenshots, recording, crashes and uploads. The JavaScript SDK runs in the page and adds readable steps, web logs, and the positions of fields to black out.

Connect the web layer

First do steps 2 and 3 for Android, then turn on WebView capture in test builds:

val config = TestForgeConfig.Builder()
    .visualReplay(test)
    .allowWebViewCapture(test)   // show the page, black out what the page reports
    .screenRecording(test)
    .appLogs(test)
    .build()

Without allowWebViewCapture(true) the whole page is black in every screenshot, because form contents live in the page where Android can't see them.

Next, give your web code access to the native SDK with a small Capacitor plugin in your app. The SDK doesn't install one itself. The important line is attachWebView, called in load() before the page starts:

android/app/src/main/java/com/example/app/TestForgePlugin.java
@CapacitorPlugin(name = "TestForge")
public class TestForgePlugin extends Plugin {
    private TestForgeMobile sdk;

    @Override public void load() {
        sdk = ((App) getContext().getApplicationContext()).testForge();
        if (sdk == null) return;                    // no SDK in this build
        sdk.attachWebView(getBridge().getWebView()); // before the page loads
    }

    @PluginMethod public void getConsent(PluginCall call) {
        JSObject out = new JSObject();
        out.put("granted", sdk != null && sdk.hasConsent());
        call.resolve(out);
    }

    @PluginMethod public void setConsent(PluginCall call) {
        boolean granted = Boolean.TRUE.equals(call.getBoolean("granted", false));
        ConsentStore.set(getContext(), granted);
        if (sdk != null) {
            sdk.setConsent(granted);
            if (granted && getActivity() != null) sdk.attach(getActivity());
        }
        call.resolve();
    }

    @PluginMethod public void report(PluginCall call) {
        if (sdk != null) sdk.requestReport();
        call.resolve();
    }

    @PluginMethod public void diagnostics(PluginCall call) {
        JSObject out = new JSObject();
        out.put("text", sdk == null ? "TestForge off in this build" : sdk.diagnostics().toString());
        call.resolve(out);
    }
}

Register it in MainActivity with registerPlugin(TestForgePlugin.class) before super.onCreate(…).

src/testforge/native.ts
import { registerPlugin } from '@capacitor/core';

export interface TestForgeNativePlugin {
  getConsent(): Promise<{ granted: boolean }>;
  setConsent(options: { granted: boolean }): Promise<void>;
  report(): Promise<void>;
  diagnostics(): Promise<{ text: string }>;
}
export const TestForgeNative = registerPlugin<TestForgeNativePlugin>('TestForge');

Start the web layer (test builds only)

src/testforge/index.ts
import type { TestForgeMobile } from '@testforge/mobile-sdk';
import { TestForgeNative } from './native';

let web: TestForgeMobile | null = null;

export async function startTestForge() {
  if (import.meta.env.VITE_TESTFORGE !== 'on') return null;   // dropped from production builds
  const { createTestForge } = await import('@testforge/mobile-sdk');
  web = createTestForge({
    endpoint: 'https://testforge.example.com/api/public/mobile-sdk/events',
    apiKey: 'tfm_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
    appVersion: import.meta.env.VITE_APP_VERSION ?? 'dev',
    // platform: detected from Capacitor ('android' in the Android shell)
  });
  const { granted } = await TestForgeNative.getConsent();
  if (granted) web.setConsent(true);
  return web;
}

// Both layers have their own consent switch: always change them together.
export async function setDiagnosticsConsent(granted: boolean) {
  await TestForgeNative.setConsent({ granted });
  web?.setConsent(granted);
}

Call startTestForge() early in main.ts, before your router renders, and build test builds with VITE_TESTFORGE=on. You don't need to set platform: from version 0.2.4 the web SDK asks Capacitor, so in the Android shell it reports android and matches an app registered as android or All platforms. Set it yourself only to override that.

Capacitor pages on Android have the web address https://localhost. The TestForge server must accept it for the page's own reports and logs. If it doesn't, the web SDK keeps those reports on the phone and retries every few minutes, so nothing is lost: they arrive once the address is allowed. Ask support, or your TestForge server's operator, if web reports stay queued while native ones arrive.

Privacy tags for your pages

The page tells Android where its form fields are, and those areas are blacked out in screenshots and replay: input, textarea, select, editable elements, and the Ionic inputs (ion-input, ion-textarea, ion-searchbar, ion-select). Add tags for everything else:

<!-- Black out anything that shows personal data, not only inputs -->
<ion-list data-tf-private>…customer names…</ion-list>

<!-- Readable names for icon-only buttons, screens and forms -->
<ion-button data-tf-name="Add customer"><ion-icon name="add"></ion-icon></ion-button>
<h1 data-tf-screen="Customer list">Customers</h1>
<form data-tf-name="New customer">…</form>
  • data-tf-private blacks out the area, and taps and labels inside it are never recorded. Use it on lists of names, account pages, chat and payment details.
  • data-tf-name and data-tf-screen make the steps readable ("Tapped Add customer on Customer list"). Use fixed names, never customer data.

The live-update pitfall (Capgo and similar)

Turn live updates off in test builds

Over-the-air update plugins such as Capgo can replace your test build's web code with the production bundle from your update channel. That bundle doesn't contain the JavaScript SDK, so the page goes silent: every screenshot says Page hidden: TestForge web layer not connected and web logs are missing.

  1. Disable live updates in test builds, for example with a test-only Capacitor config that turns the updater's automatic updates off, or by not installing the updater in test builds. Or point test builds at a separate channel that serves test bundles.
  2. Or reset to the built-in bundle when a test build starts (Capgo: CapacitorUpdater.reset()).
  3. Use a new version code for every test build, so the updater doesn't keep an old downloaded bundle across installs.

Check your update plugin's documentation for the exact option names in your version.

Check that both layers are connected

Put a hidden row in your test build's settings that shows TestForgeNative.diagnostics() and refreshes when tapped. When everything is connected, the bridge line shows MASKS_FRESH and the web SDK version:

TestForge 0.2.4 · consent on
Web bridge: MASKS_FRESH · 1 WebView(s) · js/0.2.4 · hello 12.4 s ago
Capture: PixelCopy · ok · 3.1 s ago · black 2%
Queue: 0 events · 0 media

ATTACHED with no js/… version means the web layer never connected. See Troubleshooting.

Privacy and consent

The SDK starts switched off, collects only what's listed below, hides what people type, and keeps detailed data for 14 days.

  • Nothing is collected until your app calls setConsent(true) after the user agrees.
  • setConsent(false) stops everything, cancels uploads, and deletes unsent reports, media and history from the device.
  • In a hybrid app, the native and web layers each have a consent switch, and both must be on.
  • Turning consent off can't recall reports that TestForge already received. A workspace admin can delete them (Delete telemetry in the setup panel removes an app's reports and media).

What is collected

DataWhenDefault
Crash type and stack trace (never the error message)The app crashesOn
Freezes of 5 seconds or moreThe app is on screenOn
Steps: screen names, button names, taps, failed requestsWhile consent is onOn
The tester's title, description, expected and actual result, severityThe tester sends a reportOn
Screenshot (Android), masked, with the tester's drawingThe report form opensOn; the tester can untick it
Visual replay (Android): about 1.5 masked images per second, the last 30 seconds attachedWhile the app is on screenOff: visualReplay(true)
Screen recording (Android), no audio, up to 5 minutesThe tester starts it and accepts Android's promptOff: screenRecording(true)
Console log (web) and network log: method, address without query, status, timingWhile consent is onOn in the web SDK
App log (Android): your own app's log lines, last 5 minutesReports, freezes, the launch after a crashOff: appLogs(true)
Request and response contentsNetwork log entriesOff: captureBodies

Never collected: what people type, the keyboard, the clipboard, cookies, passwords and Authorization headers, query strings in addresses, audio, the camera, location, advertising IDs, user IDs or device IDs. Android includes the device model, which doesn't identify a single device. Testers see a preview of every log before they send it.

Masking

Masking happens on the phone before an image is saved; masked pixels never leave the device.

  • Text and password fields are always blacked out.
  • Private views: tag a view testforge_private (Kotlin: view.tag = "testforge_private") to black it out with everything inside it.
  • Protected screens: screens your app marks as secure (FLAG_SECURE) are never captured.
  • Jetpack Compose screens are blacked out unless you tag the ComposeView testforge_capture, because the SDK can't find Compose text fields.
  • Web pages are blacked out unless you turn on WebView capture and connect the JavaScript SDK; then only fields and data-tf-private areas are hidden. If the page stops reporting its fields, the whole page is blacked out again.
Recording is different

A screen recording shows whatever is on screen, without masks: other apps, notifications and the keyboard. That's why it's off by default and Android asks every time. Ask testers to use test accounts while recording.

Text that is sent (titles, steps, logs) is cleaned on the phone and again on the server: email addresses, bearer tokens and values such as password= or token= are replaced. This is best effort, so never put personal data into screen names, button names or log messages.

How long data is kept

WhereWhatHow long
On the phoneUnsent reportsUntil sent; at most 20 reports and 7 days
On the phoneVisual replayIn memory only, the last 60 seconds. After a crash, a short copy is kept privately and sent on the next launch.
In TestForgeReport details: screenshots, replay, recordings, logs, steps14 days, then deleted automatically
In TestForgeReport group summaries and links to bugsUntil you delete them
In TestForgeBugs created from reportsLike any bug. Create bug copies the media onto the bug, so it stays after the 14 days.

The key is write-only

  • The tfm_… key can only send reports for the one app it was created for. It can't read reports, your Inbox, projects or anything else.
  • It isn't a secret: anyone who unpacks your app can read it. That's expected. Reports are treated as untrusted evidence, rate limits apply, and your team decides what becomes a bug.
  • TestForge stores only a fingerprint of the key, which is why it's shown once. Rotate key issues a new one and stops the old one at once; ship the new key in your next build.
  • Never put a TestForge password, session or personal API key into an app.

Reading a report

Open Bugs → Inbox and select a report. Everything the tester sent is on one page. On a phone, the list and the report are separate views with a back arrow.

The Inbox list

Reports are grouped by day. Each row shows the type (bug, crash or freeze), the title, the screen, device and version, the From app badge with the build chip, a status (New, Open, Resolved, or the linked bug such as B12), the time, and ×N for repeats. Use search, Report status, Build channel, App version and the type chips to narrow the list. Session replays without a report only appear under Replays.

Summary, app and build

The top of a report shows the badge, severity, status, type and date, then the title, the build chip, what happened, and the expected and actual results. The App & build card lists the app and bundle ID, version and build number, channel, web bundle, SDK versions, platform and OS, device, language and orientation. Other occurrences switches between repeats, so you can compare devices and builds. Crashes also show the Stack trace.

Video, replay and pauses

  • If the tester recorded the screen, the recording plays at the top. On its scrubber, dashed stretches mark where TestForge's own form was on screen. Those parts were paused and aren't in the video, and chips underneath list the pauses. A red mark shows the report moment.
  • Without a recording, the visual replay plays the masked images from the last seconds before the report or crash, with the report moment marked. Without either, you see the screenshot exactly as the tester drew on it. With a recording, the screenshot sits beside it as a thumbnail.
  • Beside the player: the device, OS, app version and screen, and a highlight of failed requests and errors in the 30 seconds before the report.

What led up to it

One timeline of the steps in plain words: screens, taps, form submits, dialogs, slow or failed requests and error logs. Select an entry to jump the video or replay to that moment. Raw breadcrumbs on the App & build card shows the underlying data.

Logs: sections and filters

Logs are split into sections, each with a count: Errors & warnings (open first), Network (with the number of failed requests), Console and App log. Each section has level chips and a search box, and Network has Failed requests only. Repeated lines are folded with a count. Download HAR and Download console (.txt) export the logs for your developers' tools.

Create a bug

Summarize steps with AI (in the ⋯ menu) drafts steps to reproduce, expected and actual results with your workspace's AI assistant. It uses the text only, never images, video or stack traces. Edit the draft, then click Create bug. The bug gets the steps, copies of the screenshot, replay, recording and logs, the From app badge and build chip, Found in build and Environment, and a link back to the report. Afterwards the button reads Open B12, and new reports of the same problem link to that bug.

You can also Resolve a report, or Ignore it from the ⋯ menu. A new occurrence reopens a resolved report; an ignored one stays ignored.

Share the link

Copy link and Share give you the same link the tester got on Report sent. It opens the report in the Inbox, or the bug once one exists, for anyone signed in to your workspace. It shows nothing to anyone else.

Troubleshooting

Messages testers can see on screenshots, in the report form, or in your diagnostics row, with what they mean and how to fix them.

What you seeWhat it meansFix
Page hidden: TestForge web layer not connectedThe web page's JavaScript SDK never started: it isn't in this build, its consent is off, or a live update replaced the web code.Load the JavaScript SDK in test builds and turn on its consent together with native consent. Turn off live updates in test builds.
Console log (not available: web layer not connected)The same cause, seen under Attach logs.As above.
Page hidden: privacy masks from the page are out of dateThe page stopped reporting where its fields are, usually because it was busy, reloading or paused. The reason follows in brackets.Keep both SDKs on the same version (0.2.3 or later) and try the screenshot again. Avoid long-running work on the page's main thread.
Page hidden: no privacy masks from the page yetThe web layer connected but hasn't sent field positions yet, often because native consent was off when the page started.Turn native consent on before or together with web consent. Make sure every WebView runs the JavaScript SDK.
Page hidden: WebView capture is off (allowWebViewCapture)The default: web pages are hidden.Turn on allowWebViewCapture(true) in test builds, with the JavaScript SDK in the page.
Page hidden: attachWebView() was not calledWebView capture is on, but the WebView isn't connected.Call attachWebView (Capacitor: in your plugin's load()).
Page hidden: WebView capture needs Android 8 or laterThe phone runs Android 7 or older.Test on Android 8 or later.
Compose content hidden (tag it testforge_capture to show it)A Jetpack Compose screen was hidden, because its text fields can't be found.If the screen is safe to show, tag the ComposeView testforge_capture; wrap sensitive parts in a view tagged testforge_private.
A black screenshot with no captionThe screen content wasn't in the app's window: video, maps, games or camera previews drawn on their own surface, or the screen was mid-transition.Expected for those views. Try again once the screen has settled.
No screenshot: this screen is protected or could not be captured.The screen is marked secure, or the capture failed.Expected on secure screens. The tester can still send the report.
A black screen recordingOn Android 14 the tester recorded "a single app" and picked a different one, or the screen was secure or showed protected video.Keep the default whole-screen recording. Secure screens are always black in recordings.
0 console or network linesNothing was logged since consent was turned on, logs are turned off, or the page calls the network through a native plugin instead of fetch.Check that consent is on early. Native network calls appear only through the SDK's OkHttp interceptor on your own client.
App log missing under Attach logsappLogs is off.Turn on appLogs(true) in test builds.
Record screen missing, or Screen recording needs FOREGROUND_SERVICE and FOREGROUND_SERVICE_MEDIA_PROJECTION…screenRecording is off, or the manifest permissions are missing.Add the permissions and turn on screenRecording(true).
Saved on this device. It will be sent when the connection is back.The phone is offline or TestForge is busy.Nothing to do: the report is sent automatically when the connection returns.
Reporting is turned off.Consent was off when the tester pressed Send.Check your consent flow turns consent on at launch.
Add a title or describe what happenedBoth fields are empty.Type a title or a description.
Reports never reach the InboxThe key was rotated or revoked, the endpoint is wrong, or the Mobile SDK feature is off for your workspace. When the key itself is rejected, the SDK discards its unsent reports, because retrying can't help.Check the key and endpoint against the setup panel, and that the feature is on.
Web reports stay queued, Android reports arriveThe server doesn't accept the page's web address (origin). The web SDK keeps those reports and retries every few minutes.Ask support to allow your page's origin (https://localhost for Capacitor on Android). The queued reports then arrive by themselves.
Web reports rejected as the wrong platformThe app is registered for a different platform than the page reports. Web SDKs before 0.2.4 always said web, even inside Capacitor.Update to 0.2.4, which detects Capacitor, or register the app as All platforms.
The app stops at start with tfm_ followed by 43 charactersThe key in your build is shortened or mistyped. Since 0.2.4 the SDK checks it when the app starts.Copy the whole key from SDK apps and installation. If it's lost, Rotate key and ship the new one.
A crash report is missingCrash reports are sent on the next launch.Open the app again after the crash.
A freeze isn't reportedFreeze detection doesn't run while a debugger is attached.Test without the debugger.

FAQ

What does it cost, and what are the limits?

The SDK isn't counted against your seats: testers who only send reports need no TestForge account, and the people who read reports use their normal seats. Whether the feature is included for your workspace is set by TestForge; ask support if you don't see the setup panel.

Limits per registered app or key:

  • 300 reports per hour per key, and 120 per minute from one network address;
  • 10,000 stored reports and 500 MB of media per app (media is deleted after 14 days, which frees space);
  • per report: screenshot 2 MB, visual replay 10 MB, recording 50 MB (and 5 minutes), logs 5 MB;
  • 25 registered apps per project.
What's the status of iOS?

The iOS package is an early version, 0.1. It captures crashes and freezes (crash reports are delivered by iOS itself, often a day later), supports shake to report with a simple title-and-message form, and records the screen names and steps you send. It doesn't have screenshots, drawing, visual replay, screen recording, logs, media upload or the report link yet. In a Capacitor app on iOS, the JavaScript SDK works on its own, without the native layer. Requires iOS 14 or later.

Where is the data stored?

Reports go straight from the phone to the TestForge service your workspace uses, at the endpoint shown in the setup panel.Screenshots, recordings and logs are stored as private files of your organization: only signed-in members of your workspace can open them, never the SDK key or an anonymous visitor. Report details aren't included in backups and are deleted after 14 days. Summarize steps sends only the report text and steps to your workspace's AI provider.

What happens when the phone is offline?

The report is saved on the phone and the form says Saved on this device. It will be sent when the connection is back. The SDK retries by itself and, on Android, shows a notification when the report is sent. Up to 20 unsent reports are kept for up to 7 days. Crashes are always saved and sent on the next launch. In a web app, unsent reports survive a reload only with persistQueue: true.

Do testers need a TestForge account?

No. Sending a report needs only the test build. Opening the report link needs a TestForge sign-in to your workspace, so testers without an account can share the link with someone who has one.

Can we leave it in our store builds?

It's designed for test builds, and we recommend keeping it there. If you do ship it, the SDK still sends nothing without each user's consent, and you must declare the data in the app stores' privacy forms.

Our Android release build is obfuscated. Will stack traces be readable?

Stack traces arrive as they were on the phone, so obfuscated builds give obfuscated traces. TestForge doesn't upload or apply mapping files; keep mapping.txt for each build. Test builds are usually not obfuscated.

Still stuck?

Send a message from How To → Contact support inside TestForge, or use the support page. Include your workspace, the app's package ID, the SDK version and your diagnostics row. Never include your key, passwords or customer data.