Using Sauce Mobile Beta with Backtrace
Beta release
The Sauce Mobile Beta SDK is in beta. The current release candidates are 2.2.0-rc for iOS and Android and 3.0.0-rc for React Native.
- Final: the artifact names and the API.
- Can still change before general availability: the version numbers, the iOS package URL, and this documentation.
Share feedback with your Sauce Labs representative.
The Sauce Mobile Beta SDK and Backtrace (Sauce Labs Error Reporting) run side by side in one app. This guide describes the contract that makes that possible: Backtrace owns crashes, Sauce Mobile Beta owns beta sessions, and both carry the same correlation attribute so a crash report and its session recording can be matched across the two consoles.
Overview
A process can have only one owner of its signal and exception handlers. The Sauce Mobile Beta SDK never installs one, at any layer and on any platform, so Backtrace is always the sole crash owner. The two SDKs are designed to run in the same app.
| Product | Owns |
|---|---|
| Backtrace | Native crashes (iOS, Android JVM and NDK), JavaScript errors and unhandled promise rejections (React Native), out-of-memory detection, symbolication |
| Sauce Mobile Beta SDK | Beta sessions and video, tester feedback and screenshots, remote logs and events, session attributes, tester workflow |
Import the TestFairy module on iOS, com.testfairy.TestFairy on Android, and the TestFairy default export in React Native. On every platform, beginWithoutCrashHandler is the entry point for an app that runs Backtrace: it starts a session and never installs a crash handler.
Before You Begin
Install both SDKs.
- Install Backtrace for your platform: iOS, Android (plus Native Crash Integration if you want NDK crashes), or React Native. New to Backtrace? Start with Getting Started.
- Android: use Backtrace Android SDK 3.7.14 or later, which added
BacktraceClient.addAttribute(see Configuring Backtrace for Android). On 3.7.13 or earlier, use((BacktraceDatabase) client.database).addAttribute(key, value)instead.
- Android: use Backtrace Android SDK 3.7.14 or later, which added
- Install the Sauce Mobile Beta SDK: iOS, Android, or React Native.
- On Android, use Backtrace's
minSdk(21) for the combined app. Sauce Mobile Beta alone supportsminSdk16.
Initialization Order
Initialize in this order on every platform:
- Generate one lowercase UUID v4 for this launch and build the shared attribute map before either SDK starts.
- Initialize Backtrace with the shared attributes. Backtrace installs the crash handlers.
- Register a Sauce Mobile Beta session-state listener that mirrors the session URL back into Backtrace.
- Copy the shared attributes to Sauce Mobile Beta with
setAttribute. - Start Sauce Mobile Beta with
beginWithoutCrashHandler.
The listener is registered before beginWithoutCrashHandler because the session URL is assigned asynchronously. Reading it immediately after begin can return null.
- Android
- iOS Swift
- React Native
BacktraceClient constructor that also takes BacktraceDatabaseSettings. The attributes-only constructor creates a disabled database, and enableNativeIntegration() then silently does nothing. Use the /json submission URL and keep enableNativeIntegration() as the last Backtrace setup call: native annotations are snapshotted there, so values added later reach native reports only through client.addAttribute (backtrace-android 3.8 and later) or ((BacktraceDatabase) client.database).addAttribute (3.7.x).import android.app.Application
import backtraceio.library.BacktraceClient
import backtraceio.library.BacktraceCredentials
import backtraceio.library.models.BacktraceExceptionHandler
import backtraceio.library.models.database.BacktraceDatabaseSettings
import com.testfairy.SessionStateListener
import com.testfairy.TestFairy
import java.io.File
import java.util.UUID
class MainApplication : Application() {
private lateinit var backtrace: BacktraceClient
override fun onCreate() {
super.onCreate()
// 1. One lowercase UUID v4 per launch, before either SDK starts.
val shared = mapOf(
"sauce.correlation_id" to UUID.randomUUID().toString(),
"sauce.sdk.coexistence_mode" to "backtrace_crash_owner",
"sauce.environment" to "beta",
"sauce.release" to "${BuildConfig.APPLICATION_ID}@${BuildConfig.VERSION_NAME}",
"sauce.dist" to BuildConfig.VERSION_CODE.toString(),
)
// 2. Backtrace first: sole JVM and native crash owner.
backtrace = BacktraceClient(
applicationContext,
BacktraceCredentials("https://submit.backtrace.io/<universe>/<backtrace-token>/json"),
BacktraceDatabaseSettings(File(applicationContext.filesDir, "backtrace").absolutePath),
HashMap<String, Any>(shared)
)
BacktraceExceptionHandler.enable(backtrace)
backtrace.enableNativeIntegration() // keep last
// 3. Mirror every session URL into Backtrace (overwritten on each start).
TestFairy.addSessionStateListener(object : SessionStateListener() {
override fun onSessionStarted(sessionUrl: String?) {
// backtrace-android 3.7.14+; on 3.7.13 or earlier: (backtrace.database as BacktraceDatabase).addAttribute(key, value)
backtrace.addAttribute("sauce.mobile_beta.session_started", "true")
backtrace.addAttribute("sauce.mobile_beta.session_url", sessionUrl ?: "")
}
override fun onSessionFailed() {
backtrace.addAttribute("sauce.mobile_beta.session_started", "false")
}
})
// 4. Same attributes before begin; the SDK keeps them for every session.
shared.forEach { (key, value) -> TestFairy.setAttribute(key, value) }
// 5. Start the SDK. It never installs a crash handler.
TestFairy.beginWithoutCrashHandler(applicationContext, "<sauce-mobile-beta-token>")
}
}
try BacktraceClient(configuration:). PLCrashReporter's handlers are installed by then, and only that setter writes attributes into native crash reports. A crash before that line carries no correlation id. Assign the full map immediately after init, with String values. Later updates such as the session URL go through the same setter, which rewrites the attributes stored for crash reports. Use the /plcrash submission URL.import UIKit
import Backtrace
import TestFairy // the SwiftPM product is SauceMobileBeta
@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
private let sessionObserver = SessionObserver()
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 1. One lowercase UUID v4 per launch, before either SDK starts.
let info = Bundle.main.infoDictionary ?? [:]
let shared: [String: String] = [
"sauce.correlation_id": UUID().uuidString.lowercased(),
"sauce.sdk.coexistence_mode": "backtrace_crash_owner",
"sauce.environment": "beta",
"sauce.release": "\(Bundle.main.bundleIdentifier ?? "")@\(info["CFBundleShortVersionString"] ?? "")",
"sauce.dist": "\(info["CFBundleVersion"] ?? "")",
]
// 2. Backtrace first: sole crash owner.
let credentials = BacktraceCredentials(
submissionUrl: URL(string: "https://submit.backtrace.io/<universe>/<backtrace-token>/plcrash")!)
let configuration = BacktraceClientConfiguration(credentials: credentials)
BacktraceClient.shared = try? BacktraceClient(configuration: configuration)
BacktraceClient.shared?.attributes = shared // full map, next statement after init
// 3. Mirror every session URL into Backtrace (register once; the SDK retains it).
TestFairy.setSessionStateDelegate(sessionObserver)
// 4. Same attributes before begin; the SDK keeps them for every session.
for (key, value) in shared { TestFairy.setAttribute(key, withValue: value) }
// 5. Start the SDK. It never installs a crash handler.
TestFairy.beginWithoutCrashHandler("<sauce-mobile-beta-token>")
return true
}
}
private final class SessionObserver: NSObject, TestFairySessionStateDelegate {
func sessionStarted() {
guard let client = BacktraceClient.shared else { return }
var attributes = client.attributes
attributes["sauce.mobile_beta.session_started"] = "true"
attributes["sauce.mobile_beta.session_url"] = TestFairy.sessionUrl() ?? ""
client.attributes = attributes
}
func sessionFailed() {
guard let client = BacktraceClient.shared else { return }
var attributes = client.attributes
attributes["sauce.mobile_beta.session_started"] = "false"
client.attributes = attributes
}
}
userAttributes in BacktraceClient.initialize(...). addSessionStateListener returns a subscription. Call remove() on it if you tear the integration down.Install uuid and react-native-get-random-values (npm install uuid react-native-get-random-values). Neither is a dependency of the Sauce Mobile Beta package. Import the polyfill before uuid.import 'react-native-get-random-values'; // before uuid on React Native
import { v4 as uuidv4 } from 'uuid';
import { BacktraceClient } from '@backtrace/react-native';
import TestFairy from '@saucelabs/mobile-beta-react-native';
export function initializeObservability() {
// 1. One lowercase UUID v4 per launch, before either SDK starts.
const shared: Record<string, string> = {
'sauce.correlation_id': uuidv4(),
'sauce.sdk.coexistence_mode': 'backtrace_crash_owner',
'sauce.environment': 'beta',
'sauce.release': '<app-id>@<version>',
'sauce.dist': '<build-number>',
};
// 2. Backtrace first: sole crash owner (JS errors, native crashes).
const backtrace = BacktraceClient.initialize({
url: 'https://submit.backtrace.io/<universe>/<backtrace-token>/json',
userAttributes: shared,
database: {
enable: true,
captureNativeCrashes: true,
createDatabaseDirectory: true,
path: `${BacktraceClient.applicationDataPath}/backtrace`,
},
});
// 3. Mirror every session URL into Backtrace (overwritten on each start).
const subscription = TestFairy.addSessionStateListener({
onSessionStarted({ sessionUrl }) {
backtrace.addAttribute({
'sauce.mobile_beta.session_started': 'true',
'sauce.mobile_beta.session_url': sessionUrl ?? '',
});
},
onSessionFailed() {
backtrace.addAttribute({ 'sauce.mobile_beta.session_started': 'false' });
},
});
// 4. Same attributes before begin; the SDK keeps them for every session.
for (const [key, value] of Object.entries(shared)) {
TestFairy.setAttribute(key, value);
}
// 5. Start the SDK. It never installs a crash handler.
TestFairy.beginWithoutCrashHandler('<sauce-mobile-beta-token>');
return { backtrace, subscription };
}
beginWithoutCrashHandler forces the enableCrashReporter option (TFSDKEnableCrashReporterKey on iOS) to false even if you pass true. Plain begin(...) also starts a session without a crash handler, but the explicit call documents the contract in your code and is the recommended entry point.
Shared Attributes
Both SDKs receive the same keys and values. The values are copies, not references: nothing joins the two products server-side, so a report and a session match only when the app wrote the same value to both.
| Attribute | Value | Why |
|---|---|---|
sauce.correlation_id | Lowercase UUID v4, generated once per app launch | The join key. Search it in either console to find the matching report or session. |
sauce.sdk.coexistence_mode | backtrace_crash_owner | Documents which product owns crashes in this build. Use it to filter mixed fleets. |
sauce.environment | beta, production, or your own environment name | Separates beta traffic from other environments in both consoles. |
sauce.release | <appId>@<version> (for example com.example.app@1.2.3) | Ties reports and sessions to a release without relying on platform-specific version fields. |
sauce.dist | Build number (CFBundleVersion or versionCode) | Distinguishes builds of the same version. |
mad.distribution_id | Your Mobile App Distribution build id | Optional. Set it only when configured, so filters never see an empty string. |
The Backtrace side additionally receives two attributes the app writes on every Sauce Mobile Beta session start: sauce.mobile_beta.session_url (the recording's address) and sauce.mobile_beta.session_started (the strings "true" or "false"). A launch can produce several sessions (after stop() and a resume), so overwrite these values in the listener every time. Never set them once.
A crash belongs to the launch it happened in. Generating sauce.correlation_id once per launch, before either SDK starts, guarantees that the report Backtrace writes at crash time and the session Sauce Mobile Beta recorded up to that moment carry the same value. For identity that spans launches, use setUserId with your real user id or a persisted identifier. Do not reuse the correlation id for that.
Limits on the Sauce Mobile Beta side: 64 attributes per session, keys up to 64 characters, values up to 1000 characters on iOS and 1024 on Android. On iOS and Android, setAttribute returns false when a value is rejected. The React Native binding returns nothing. Set every shared attribute before beginWithoutCrashHandler. The SDK keeps them and attaches them to every session it starts, including sessions started after stop().
Do not use setCorrelationId or identify for the correlation id. Both are deprecated and write the user-identity field that setUserId writes (on Android only once per process), so they would collide with your real user id. Keep setUserId for the user and setAttribute for sauce.correlation_id. See Identifying Your Users and Session Attributes.
Finding a Crash's Session
- Index the attributes in Backtrace. Backtrace lets you filter and group on a custom attribute only after it has been indexed once per project under Project Settings > Attributes. Index
sauce.correlation_idwith the UUID format, andsauce.mobile_beta.session_urlas a string. See Indexing Attributes. - From a crash report, copy the
sauce.correlation_idvalue and search for it in the Sauce Labs Mobile App Distribution session list. The session recorded up to the crash carries the same value. - Or follow
sauce.mobile_beta.session_urlfrom the report directly to the recording. It points at the most recent session of that launch, which is the one running when the crash happened.
In the other direction, filter Backtrace on sauce.correlation_id with the value shown on a session's attributes to see whether that launch crashed.
Crash APIs
These methods exist in the API and do nothing: installCrashHandler, enableCrashHandler, and disableCrashHandler. didLastSessionCrash always returns false. In React Native, isCrashReportingAvailable() returns false and getIntegrationInfo().coexistenceMode is backtrace_crash_owner. Use Backtrace for crash history.
Production Builds
Keep the Sauce Mobile Beta SDK out of store builds and ship Backtrace in every build. Backtrace is designed for production crash reporting. The Sauce Mobile Beta SDK records sessions for testers. On Android, declare the SDK with debugImplementation or in a beta flavor so release variants never package it. On iOS, gate the calls behind a build setting or a no-op wrapper. Sauce Mobile Beta SDK in Production describes the options, including the no-op wrapper pattern. The shared attributes go to Backtrace in production as well, which keeps sauce.release and sauce.dist consistent between beta and store reports.
Reference Apps
This pattern is taken from the Sauce Labs demo apps and the React Native package's example app, all of which run the released SDKs:
- My Demo App for iOS repository, release 2.3.0. See
My Demo App/AppDelegate.swiftandMy Demo App/Utilities/TestFairyWrapper.swift. - My Demo App for Android repository, release 2.3.0. See
MyApplication.javaandapp/src/mobileBeta/.../SauceMobileBetaIntegration.java, which also asserts afterbeginWithoutCrashHandlerthat Backtrace's uncaught-exception handler is the one installed in the process. - React Native example app: testfairy/react-native-testfairy, release 3.0.0-rc. See
example/src/observability.ts.
To try it, run a demo app with your own Backtrace submission URL and Sauce Mobile Beta token, then trigger a crash: More > Crash the App on iOS, or Crash app (debug) in the menu on Android. The process dies as a genuine crash. The Sauce Mobile Beta SDK does not intercept it. Relaunch the app: Backtrace uploads the pending report while Sauce Mobile Beta starts a new session. Open the report, copy its sauce.correlation_id, and search for it in the session list to land on the recording of the launch that crashed.