TestBuddy Sign in

Developer docs

TestBuddy SDK: the Android SDK for closed testing

A small Kotlin library that links each tester's install to their place and measures how long your app stays in the foreground. One line in Application.onCreate, nothing on screen for your users.

A phone with a small module plugged into its side, sending a beam of light to a server

At a glance

What it sends, and what it does not

What it sends

What it does not collect

No name, email, advertising ID, device identifier, location, contacts, screen content or crash data. It does not see which screens are opened or what is typed. It reads the install referrer only to find a TestBuddy code in it.

On your real users' phones

A phone that is not linked to a tester sends the hello and nothing else. Sessions are still counted locally, because a tester's sessions before the link count too, but they stay in the app's private storage (at most 200 events) and are never sent unless that install gets linked.

Install

The SDK starts on its own when your app starts: it reads your key from the manifest, so no step is code. Whatever your app is built with, it comes down to the dependency and one manifest line. Use the key shown in TestBuddy at step 3 of the setup (later in Your app, under Setup details); it starts with pk_. The setup shows these steps with your key already in them.

Kotlin or Java (Android Studio)

In settings.gradle.kts, inside dependencyResolutionManagement { repositories { } }:

maven("https://jitpack.io")

In app/build.gradle.kts:

dependencies {
    implementation("com.github.jointestbuddy:testbuddy-android:0.2.0")
}

In app/src/main/AndroidManifest.xml, inside <application>:

<meta-data android:name="com.jointestbuddy.KEY" android:value="pk_..." />

Flutter

In android/build.gradle.kts, inside allprojects { repositories { } } (Groovy: maven { url "https://jitpack.io" } in android/build.gradle):

maven("https://jitpack.io")

At the end of android/app/build.gradle.kts:

dependencies {
    implementation("com.github.jointestbuddy:testbuddy-android:0.2.0")
}

In android/app/src/main/AndroidManifest.xml, inside <application>:

<meta-data android:name="com.jointestbuddy.KEY" android:value="pk_..." />

React Native

React Native already adds the JitPack repository. In android/app/build.gradle, inside dependencies { }:

implementation("com.github.jointestbuddy:testbuddy-android:0.2.0")

In android/app/src/main/AndroidManifest.xml, inside <application>:

<meta-data android:name="com.jointestbuddy.KEY" android:value="pk_..." />

Expo

Expo regenerates android/ at each prebuild, so a config plugin adds the SDK and your key every time. Save it as plugins/withTestBuddy.js at the root of your project:

// Adds the TestBuddy SDK to the Android build at each prebuild: the dependency and your key.
const { AndroidConfig, withAndroidManifest, withAppBuildGradle, withProjectBuildGradle } = require('expo/config-plugins')

module.exports = function withTestBuddy(config, { key }) {
  config = withProjectBuildGradle(config, (c) => {
    if (!c.modResults.contents.includes('jitpack.io'))
      c.modResults.contents = c.modResults.contents.replace(/allprojects\s*\{\s*repositories\s*\{/, (m) => `${m}\n    maven { url 'https://www.jitpack.io' }`)
    return c
  })
  config = withAppBuildGradle(config, (c) => {
    if (!c.modResults.contents.includes('testbuddy-android'))
      c.modResults.contents = c.modResults.contents.replace(/dependencies\s*\{/, (m) => `${m}\n    implementation("com.github.jointestbuddy:testbuddy-android:0.2.0")`)
    return c
  })
  return withAndroidManifest(config, (c) => {
    const app = AndroidConfig.Manifest.getMainApplicationOrThrow(c.modResults)
    AndroidConfig.Manifest.addMetaDataItemToMainApplication(app, 'com.jointestbuddy.KEY', key)
    return c
  })
}

Then, in app.json, add it to expo.plugins:

["./plugins/withTestBuddy", { "key": "pk_..." }]

Build with eas build --platform android, or run npx expo prebuild again with a local android/ folder. Expo Go cannot load native code; the build you upload to Play does.

Unity

In Project Settings, Player, Android, Publishing Settings, Build, turn on Custom Main Manifest, Custom Main Gradle Template and Custom Gradle Settings Template. Then, in Assets/Plugins/Android/settingsTemplate.gradle, inside dependencyResolutionManagement { repositories { } }:

maven { url "https://jitpack.io" }

In Assets/Plugins/Android/mainTemplate.gradle, inside dependencies { }:

implementation "com.github.jointestbuddy:testbuddy-android:0.2.0"

In Assets/Plugins/Android/AndroidManifest.xml, inside <application>:

<meta-data android:name="com.jointestbuddy.KEY" android:value="pk_..." />

Capacitor or Ionic

In android/build.gradle, inside allprojects { repositories { } }:

maven { url "https://jitpack.io" }

In android/app/build.gradle, inside dependencies { }:

implementation "com.github.jointestbuddy:testbuddy-android:0.2.0"

In android/app/src/main/AndroidManifest.xml, inside <application>:

<meta-data android:name="com.jointestbuddy.KEY" android:value="pk_..." />

Then: upload the build and open it once

Upload the build to your closed test track and open it once on any phone. The SDK says hello, the setup step turns verified, and you can join the next wave. The closed testing setup guide covers the Play Console side.

Prefer code?

Instead of the manifest line, call TestBuddy.init once in Application.onCreate. It is ignored if the SDK already started from the manifest. Its optional third parameter, apiUrl, is for local testing only.

import android.app.Application
import com.jointestbuddy.TestBuddy

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        TestBuddy.init(this, "pk_...")
    }
}

Java

Every public call is @JvmStatic, so Java calls it directly on TestBuddy:

import android.app.Application;
import com.jointestbuddy.TestBuddy;

public class App extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        TestBuddy.init(this, "pk_...");
    }
}
TestBuddy.objective("first_entry");
TestBuddy.link(code);
boolean linked = TestBuddy.isLinked();
long seconds = TestBuddy.getForegroundSeconds();

The callback of link is a Kotlin function type, so from Java it returns Unit.INSTANCE (import kotlin.Unit;):

TestBuddy.link(code, linked -> {
    // Called on a background thread.
    return Unit.INSTANCE;
});

Objectives

An objective is a step you want testers to reach, like saving a first entry. An app has up to 3, each with a key of lowercase letters, digits and underscores (up to 40 characters), set on your app in TestBuddy. Record one where it happens in your code:

// After the first entry is saved.
TestBuddy.objective("first_entry")

Your dashboard shows how many testers reached each objective. Keys that are not on your app are ignored. Calling objective before the SDK starts does nothing.

Linking an install to a tester

Automatic: the install referrer

TestBuddy gives each tester a Play Store install link for your app with a referrer: https://play.google.com/store/apps/details?id=<package>&referrer=tb_<code>. On the first launch, the SDK reads the referrer once through the Play Install Referrer library, finds the tb_ code and links the install. If the network fails, it tries again at the next launch. An install from anywhere else carries Play's default referrer, with no code.

One tap: the testbuddy:// link

The SDK's manifest declares LinkActivity, which opens on:

testbuddy://<package>/link?code=XXXXXX

The TestBuddy web shows a "Link it" button on Android that opens this link. The activity is translucent and kept out of recents: it links the install, shows "Linked to TestBuddy" (or "This TestBuddy link did not work"), then opens your app's launch activity. Without a valid code, or in an app that never called init, it closes at once. You add nothing to your manifest: the activity is merged from the library, with your applicationId as the link's host.

A custom flow: TestBuddy.link(code)

If you want your own way to enter the code, call link with the 6 letters and digits the tester sees in TestBuddy:

TestBuddy.link(code) { linked ->
    // Background thread: switch to the main thread before touching views.
    runOnUiThread { showLinked(linked) }
}

linked is true once the install is linked; it is false for an unknown code or when init was not called. Keep such a screen out of sight, in a debug or settings menu for example: a code prompt in your app would also show to users who are not testers, and to Google's reviewers. That is why the SDK uses a link instead.

Checking the state

TestBuddy.isLinked is true once the install is linked. TestBuddy.foregroundSeconds is the time since the app last came to the foreground, 0 in the background. Both are handy on a debug screen.

Sessions, offline queue and retries

Threading and safety

Play Data safety declaration

Closed testing tracks need a Data safety form; only apps active exclusively on internal testing are exempt. Google also states that data sent off the device by SDKs in your app counts as your app's collection, and that you alone are responsible for complete and accurate declarations (Provide information for Google Play's Data safety section).

What the SDK transmits, to help you fill it in:

This is guidance, not legal advice. Check Play's Data safety form and its definitions for your own app, and review your answers when you remove the SDK.

Removing it before production

Keep the SDK in your closed test build for the length of your wave: the terms ask for it, and your testers' days depend on it. Once your wave is over, remove it before your production release:

  1. Delete the implementation("com.github.jointestbuddy:testbuddy-android:0.2.0") line, or the TestBuddy plugin entry in an Expo app.
  2. Delete the com.jointestbuddy.KEY manifest line (or the TestBuddy.init line) and any objective or link calls.
  3. Remove the JitPack repository if nothing else uses it.

The link activity and the INTERNET permission go away with the dependency, unless your app declares that permission itself.

Requirements