Skip to content

SDK Features

Configuration

The Logger needs to be started before it can be used. Note that the start method only needs to be called once and will cause the Logger to retain this configuration until the end of the process. Subsequent calls to the start method won't have any effect.

The minimum required configuration of the SDK is as follows:

Kotlin
import io.bitdrift.capture.Capture.Logger
import io.bitdrift.capture.providers.session.SessionStrategy

Logger.start(
  apiKey = "<your-api-key>",
  sessionStrategy = SessionStrategy.ActivityBased(),
)

Initialization

The Android SDK handles its initial configuration using Jetpack Startup1. It uses an Initializer called ContextHolder to get a reference to the application's Context.

Initialization Dependency

If you wish to start the Logger from inside your own Initializer make sure to add it as a dependency:

Kotlin
import io.bitdrift.capture.ContextHolder

class AppExampleInitializer : Initializer<AppExampleDependency> {
  override fun create(context: Context): AppExampleDependency {
    // You can call Capture.Logger.start() here safely
    return AppExampleDependency()
  }

  override fun dependencies(): List<Class<out Initializer<*>>> {
    // Defines a dependency on ContextHolder so it can be
    // initialized after bitdrift Capture is initialized.
    return listOf(ContextHolder::class.java)
  }
}

Manual Initialization

If you wish to avoid automatic initialization and want to manually call the bitdrift Capture initializer you can follow the steps below.

Add this to your AndroidManifest.xml configuration to disable automatic initialization:

XML
<application>
  <provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge"
  >
    <!-- Disable automatic initialization of Bitdrift ContextHolder -->
    <meta-data
      android:name="io.bitdrift.capture.ContextHolder"
      tools:node="remove"
    />
  </provider>
</application>

Manually call the bitdrift initializer:

Kotlin
import io.bitdrift.capture.ContextHolder
// This needs to run before calling Logger.start()
AppInitializer.getInstance(applicationContext)
  .initializeComponent(ContextHolder::class.java)

Java
import io.bitdrift.capture.Capture.Logger;
import io.bitdrift.capture.providers.session.SessionStrategy;

Logger.start(
  "<your-api-key>",
  new SessionStrategy.ActivityBased()
);

Initialization

The Android SDK handles its initial configuration using Jetpack Startup1. It uses an Initializer called ContextHolder to get a reference to the application's Context.

Initialization Dependency

If you wish to start the Logger from inside your own Initializer make sure to add it as a dependency:

Java
import io.bitdrift.capture.ContextHolder;

class AppExampleInitializer implements Initializer<AppExampleDependency> {
  @Override
  public AppExampleDependency create(context: Context) {
    // You can call Capture.Logger.start() here safely
    return new AppExampleDependency();
  }

  @Override
  public List<Class<Initializer<?>>> dependencies() {
    // Defines a dependency on ContextHolder so it can be
    // initialized after bitdrift Capture is initialized.
    return Arrays.asList(ContextHolder.class);
  }
}

Manual Initialization

If you wish to avoid automatic initialization and want to manually call the bitdrift Capture initializer you can follow the steps below.

Add this to your AndroidManifest.xml configuration to disable automatic initialization:

XML
<application>
  <provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge"
  >
    <!-- Disable automatic initialization of Bitdrift ContextHolder -->
    <meta-data
      android:name="io.bitdrift.capture.ContextHolder"
      tools:node="remove"
    />
  </provider>
</application>

Manually call the bitdrift initializer:

Java
import io.bitdrift.capture.ContextHolder;
// This needs to run before calling Logger.start()
AppInitializer.getInstance(context)
  .initializeComponent(ContextHolder.class);

Swift
import Capture

Logger.start(
  withAPIKey: "<your-api-key>",
  sessionStrategy: .activityBased()
)

Info

It's recommended that you initialize the SDK as part of your application's application(_:willFinishLaunchingWithOptions:) or application(_:didFinishLaunchingWithOptions:) methods. This allows the SDK to observe system events such as didFinishLaunchingNotification, which power some of the SDK's out-of-the-box events.

Objective-C
#import <Capture/Capture.h>

[CAPLogger
  startWithAPIKey:@"your-api-key"
  sessionStrategy: [CAPSessionStrategy activityBased]
];

Info

It's recommended that you initialize the SDK as part of your application's application(_:willFinishLaunchingWithOptions:) or application(_:didFinishLaunchingWithOptions:) methods. This allows the SDK to observe system events such as didFinishLaunchingNotification, which power some of the SDK's out-of-the-box events.

When using the JavaScript-based initialization (e.g. using Expo), the SDK can be initialized as follows:

JavaScript
import { init, SessionStrategy } from '@bitdrift/react-native';

init("<your-api-key>", SessionStrategy.Activity);

When using the native initialization (e.g. ejected Expo, non-Expo React Native), the SDK can be initialized using the per platform instructions above.

JavaScript
import { init, SessionStrategy } from '@bitdrift/react-native';

init("<your-api-key>", SessionStrategy.Activity);

The SDK should be initialized in the main process.

See the Quickstart Guide section for information on how to obtain an API Key.

Session Management

Events collected by the SDK are annotated with a session identifier (ID). This session identifier is utilized to group events emitted by the SDK and can be used to retrieve events from a specific session.

The SDK manages the used session identifier using one of the two following strategies:

  • ActivityBased (Kotlin) / activityBased (Swift) / SessionStrategy.Activity (JavaScript): A session strategy that generates a new session ID after a certain period of app inactivity. The inactivity duration is measured by the minutes elapsed since the last log. The session ID is persisted to disk and survives app restarts.
  • Fixed (Kotlin) / fixed (Swift) / SessionStrategy.Fixed: A session strategy that never expires the session ID but does not survive process restart.

The minimal session strategy setup looks as follows:

Kotlin
// `ActivityBased` session strategy
Logger.start(
  // ...
  sessionStrategy = SessionStrategy.ActivityBased()
)

// `Fixed` session strategy
Logger.start(
  // ...
  sessionStrategy = SessionStrategy.Fixed()
)
Java
// `ActivityBased` session strategy
Logger.start(
  // ...
  new SessionStrategy.ActivityBased()
);

// `Fixed` session strategy
Logger.start(
  // ...
  new SessionStrategy.Fixed()
);
Swift
// `activityBased` session strategy
Logger.start(
  // ...
  sessionStrategy: .activityBased()
)

// `fixed` session strategy
Logger.start(
  // ...
  sessionStrategy: .fixed()
)
Objective-C
// `activityBased` session strategy
[CAPLogger
  // ...
  sessionStrategy: [CAPSessionStrategy activityBased]
];

// `fixed` session strategy
[CAPLogger
  // ...
  sessionStrategy: [CAPSessionStrategy fixed]
];
JavaScript
// `activityBased` session strategy
init("<your-api-key>", SessionStrategy.Activity);

// `fixed` session strategy
init("<your-api-key>", SessionStrategy.Fixed);
JavaScript
// `activityBased` session strategy
init("<your-api-key>", SessionStrategy.Activity);

// `fixed` session strategy
init("<your-api-key>", SessionStrategy.Fixed);

Retrieving Session ID

The current session identifier can be retrieved when needed using the Logger getter. Session ID is only available after the Logger has been started.

Kotlin
Logger.sessionId
Java
Logger.getSessionId();
Swift
Logger.sessionID
Objective-C
[CAPLogger sessionID];
JavaScript
import { getSessionId } from '@bitdrift/react-native';

getSessionId();

Retrieving Session URL

Similarly, the full session permalink URL can be retrieved using the Logger getter. This is particularly helpful when integrating with external systems. Session URL is only available after the Logger has been started.

Kotlin
Logger.sessionUrl
Java
Logger.getSessionUrl();
Swift
Logger.sessionURL
JavaScript
import { getSessionUrl } from '@bitdrift/react-native';

getSessionUrl();

Generating New Session ID

It may be desirable to generate a new session identifier in response to specific user action(s) (e.g., user logging out) or other events.

A new session identifier can be generated using a simple method call:

Kotlin
Logger.startNewSession()
Java
Logger.startNewSession();
Swift
Logger.startNewSession()

Depending on the session strategy used, the new session identifier is generated in the following way:

  • ActivityBased (Kotlin) / activityBased (Swift) / Activity: A random, unique session identifier is generated each time thestartNewSession` method is called.
  • Fixed (Kotlin) / fixed (Swift): A session identifier is set to the identifier retrieved using the sessionIdGenerator (Kotlin) lambda / sessionIDGenerator (Swift) closure. This closure is passed to the initializer of the Fixed (Kotlin) / fixed (Swift) session strategy.

For the Fixed (Kotlin) / fixed (Swift) session strategy, a session ID generator can be passed in the following way:

Kotlin
var counter = 0;
Logger.start(
  // ...
  sessionStrategy = SessionStrategy.Fixed(sessionIdGenerator = { (++counter).toString() })
)

println(Logger.sessionId) // prints "1"

Logger.startNewSession()

println(Logger.sessionId) // prints "2"
Java
AtomicInteger counter = new AtomicInteger(0);
Logger.start(
  // ...
  new SessionStrategy.Fixed(() -> Integer.toString(counter.incrementAndGet()))
);

println(Logger.getSessionId()); // prints "1"

Logger.startNewSession();

println(Logger.getSessionId()); // prints "2"
Swift
var counter = 0
Logger.start(
  // ...
  sessionStrategy: .fixed {
    counter += 1;
    return \"(counter)"
  }
)

print(Logger.sessionID) // prints "1"

Logger.startNewSession()

print(Logger.sessionID) // prints "2"

Observing Session ID Changes

In the case of the ActivityBased (Kotlin) / activityBased (Swift) session strategy, the session identifier is managed on behalf of SDK customers and can change without their explicit request. For this reason, the SDK provides a way for customers to register for session identifier changes specific to this session strategy type:

Kotlin
Logger.start(
  // ...
  sessionStrategy = SessionStrategy.ActivityBased(
    inactivityThresholdMins = 30,
    onSessionIdChanged = { newSessionId ->
      // Do something with the new session ID
    }
  )
)
Java
Logger.start(
  // ...
  new SessionStrategy.ActivityBased(
    30, //inactivityThresholdMins
    newSessionId -> {
      // Do something with the new session ID
      return Unit.INSTANCE;
    }
  )
);
Swift
Logger.start(
  // ...
  sessionStrategy: .activityBased(
    inactivityThresholdMins: 30,
    onSessionIDChanged: { newSessionID in
      // Do something with the new session ID
    }
  )
)

Automatic Instrumentation

Log Events

Upon start, the SDK automatically tracks a set of logs by observing certain app and device states.

Event Notes
App Close Triggered by ProcessLifecycleOwner Lifecycle.Event.ON_STOP
App Exit Emitted each time the SDK is started and Android returns a result from the getHistoricalProcessExitReasons method containing details about the ApplicationExitInfo.
App Launch Triggered by ProcessLifecycleOwner Lifecycle.Event.ON_CREATE
App Launch TTI Manually emitted by the SDK customer when the app becomes interactive or when a user interacts with the app, depending on the desired behavior.
App Moved To Background Triggered by ProcessLifecycleOwner Lifecycle.Event.ON_PAUSE
App Moved To Foreground Triggered by ProcessLifecycleOwner Lifecycle.Event.ON_RESUME
App Open Triggered by ProcessLifecycleOwner Lifecycle.Event.ON_START
App Update Emitted each time the SDK detects an versionName or longVersionCode change. The emitted log event contains information about the app installation size.
Fatal Issue - JVM Triggered by intercepting uncaught exceptions
Low Power Mode Triggered by isPowerSaveMode
Memory Pressure Emitted when app memory usage exceeds ActivityManager.MemoryInfo.threshold. Replaces noisy onTrimMemory-based logging.
Orientation Change Emmitted each time the SDK detects a device orientation change.
Resource Utilization Periodically reported event with a snapshot of application's resource consumption. The information captured, among others, includes memory usage and battery level.
SDK Started Emitted each time the SDK is started. It contains information about the SDK version and the duration of time the start took.
Screen View Manually emitted by the SDK customer when a user navigates to a different screen view representation.
Slow Rendering Emitted whenever an application frame takes too long to render, as detected by the JankStats library2. Android defines default latency thresholds3: frames taking longer than 16 ms are classified as Slow Frames, longer than 700 ms as Frozen Frames, and frames that exceed 5 seconds are labeled as ANR (Application Not Responding) Frames. These thresholds can be customized via runtime flags.
Thermal State Changed Emitted each time the callbacks of OnThermalStatusChangedListener are called.
Timezone Change Triggered by Intent.ACTION_TIMEZONE_CHANGED
Event Notes
App Close Triggered by UIApplicationDidEnterBackgroundNotification
App Did Finish Launching Triggered by didFinishLaunchingNotification
App Launch TTI Manually emitted by the SDK customer when the app becomes interactive or when a user interacts with the app, depending on the desired behavior.
App Not Responding (ANR) Emitted each time the main run loop becomes unresponsive for a specified amount of time. Default is 2 seconds but can be configured by runtime flag.
App Open Triggered by UIApplicationWillEnterForegroundNotification or didFinishLaunchingNotification
App Update Emitted each time the SDK detects a version or build number change. The emitted log event contains information about the app installation size.
App Will Terminate Triggered by UIApplicationWillTerminateNotification
Low Power Mode Triggered by isLowPowerModeEnabled
Memory Pressure Triggered by applicationDidReceiveMemoryWarning
Orientation Change Emmitted each time the SDK detects a device orientation change.
Resource Utilization Periodically reported event with a snapshot of application's resource consumption. The information captured, among others, includes memory usage and battery level.
Scene Did Enter Background Triggered by UIApplicationDidEnterBackgroundNotification
Scene Will Enter Foreground Triggered by UIApplicationWillEnterForegroundNotification
Screen View Manually emitted by the SDK customer when a user navigates to a different screen view representation.
SDK Started Emitted each time the SDK is started. It contains information about the duration of time the start took.
Session Replay Emitted each time the SDK captures session replay frame.
Thermal State Changed Emitted each time ProcessInfo.thermalStateDidChangeNotification notification is posted.
Timezone Change Triggered by NSSystemTimeZoneDidChange

React Native supports all the out of the box events provided by the platform the app is running on, see the iOS and Android sections for more details.

Event Notes
SDK Started Emitted each time the SDK is started. It contains information about the duration of time the start took.

Resource Reporting

The Capture SDK can take periodic snapshots of the resource consumption of the app on the device. The information captured, among others, includes memory usage and battery level.

Session Replay

The Capture SDK makes it possible to periodically capture optimized representations of the user screen that do not include Personal Identifiable Information (PII) like text or images.

The feature captures the application screen on a periodic basis, is enabled by default, and can be disabled at any time through a remote configuration update.

The feature allows you to specify additional categorizers, which can be used to provide custom rendering for your app-specific views. For example, categorizers can inform the SDK that your custom implementation of a switch-like UI should be rendered as a switch.

Kotlin
Logger.start(
  // ...
  configuration = Configuration(
    sessionReplayConfiguration = SessionReplayConfiguration(
      categorizers = mapOf(
        ReplayType.View to listOf("CoreUiPanel")
      )
    )
  )
)

Info

Session Replay supports both standard Android views as well as Compose views.

Java
HashMap categorizers = new HashMap();
List<String> names = new LinkedList<String>();
names.add("CoreUiPanel")
categorizers.put(ReplayType.View, names);

Logger.start(
  // ...
  new Configuration(
    new SessionReplayConfiguration(
      categorizers // categorizers
    )
  )
);

Info

Session Replay supports both standard Android views as well as Compose views.

Swift
Logger.start(
  // ...
  configuration: .init(
    sessionReplayConfiguration: .init(
      categorizers: [
          "CoreUiPanel": AnnotatedView(.view, recurse: false),
      ]
    )
  )
)

Info

Session Replay supports both UIKit views as well as SwiftUI views.

TTI

Currently the SDK requires the customer to supply Time To Interactive (TTI) information via an explicit API. This data will be used to populate the App Launch TTI log event as well as data in the TTI instant insights dashboard.

Kotlin
Logger.logAppLaunchTTI(...)
Java
Logger.logAppLaunchTTI(...);
Swift
Logger.logAppLaunchTTI(...)
JavaScript
import { logAppLaunchTTI } from '@bitdrift/react-native';

logAppLaunchTTI(ttiMs)

Screen Views

Currently the SDK requires the customer to supply which screens the users has visited via an explicit API. This data will be used to populate the User Journeys instant insights dashboard.

Kotlin
Logger.logScreenView("my_screen_name")
Java
Logger.logScreenView("my_screen_name");
Swift
Logger.logScreenView("my_screen_name")
JavaScript
import { logScreenView } from '@bitdrift/react-native';

logScreenView('my_screen_name')

Fatal Issues and Crash Reporting

Refer to Fatal Issues & Crashes section for more details about how to enable automatic detection and upload of issues.

Integrations

Refer to Integrations section for more details about various integrations including, but not limited to, capturing of network traffic information.

Custom Logs

You can add custom logs using the log message API. The SDK provides methods for the five supported severity levels: trace, debug, info, warning, and error.

The logging methods:

  • Take an optional fields dictionary where you can pass arbitrary attributes using key-value pairs. The fields parameter is of type Map<String, String> on Android, [String: Encodable & Sendable] in Swift (iOS), and [String: String] in Objective-C (iOS). Refer to Fields for more details.
  • Take an optional instance of Throwable (Kotlin/Java) or Error (Swift) as one of its arguments. See Capturing Errors and Exceptions for more details.
  • On iOS, capture file, line, and function arguments for the location in code where they appear. See Capturing File, Line and Function for more details.
Kotlin
Logger.log(LogLevel.INFO, mapOf("key" to "value")) { "Info log" }

Logger.logTrace(mapOf("key" to "value")) { "Trace log" }
Logger.logDebug(mapOf("key" to "value")) { "Debug log" }
Logger.logInfo(mapOf("key" to "value")) { "Info log" }
Logger.logWarning(mapOf("key" to "value")) { "Warning log" }
Logger.logError(mapOf("key" to "value")) { "Error log" }
Java
Logger.log(LogLevel.INFO, Collections.singletonMap("key", "value"), () -> "Info log");

Logger.logTrace(Collections.singletonMap("key", "value"), () -> "Trace log");
Logger.logDebug(Collections.singletonMap("key", "value"), () -> "Debug log");
Logger.logInfo(Collections.singletonMap("key", "value"), () -> "Info log");
Logger.logWarning(Collections.singletonMap("key", "value"), () -> "Warning log");
Logger.logError(Collections.singletonMap("key", "value"), () -> "Error log");
Swift
Logger.log(level: .info, "Info log", fields: ["key": "value"])

Logger.logTrace("Trace log", fields: ["key": "value"])
Logger.logDebug("Debug log", fields: ["key": "value"])
Logger.logInfo("Info log", fields: ["key": "value"])
Logger.logWarning("Warning log", fields: ["key": "value"])
Logger.logError("Error log", fields: ["key": "value"])
Objective-C
[CAPLogger logWithLevel:LogLevel.info message:@"Info log" fields: @{@"key": @"value"}];

[CAPLogger logTrace:@"Trace log" fields:@{@"key": @"value"}];
[CAPLogger logDebug:@"Debug log" fields:@{@"key": @"value"}];
[CAPLogger logInfo:@"Info log" fields:@{@"key": @"value"}];
[CAPLogger logWarning:@"Warning log" fields:@{@"key": @"value"}];
[CAPLogger logError:@"Error log" fields:@{@"key": @"value"}];
JavaScript
import { info } from '@bitdrift/react-native';

// Log line with LogLevel of Trace
trace('Hello world!', { key: 'value' });

debug('Hello world!', { key: 'value' });
info('Hello world!', { key: 'value' });
warning('Hello world!', { key: 'value' });
error('Hello world!', { key: 'value' });
JavaScript
import { info } from '@bitdrift/react-native';

// Log line with LogLevel of Trace
trace('Hello world!', { key: 'value' });

debug('Hello world!', { key: 'value' });
info('Hello world!', { key: 'value' });
warning('Hello world!', { key: 'value' });
error('Hello world!', { key: 'value' });

Capturing Errors and Exceptions

All logging methods support passing of an optional Throwable (Kotlin/Java) / Error (Swift) argument to help with the reporting of errors and exceptions.

For each logged Throwable instance, the SDK adds following fields to emitted log:

  • _error field with the Java exception name (it.javaClass.name) as its value, e.g., java.io.IOException.
  • _error_details field with exception message (it.message) as its value.
Kotlin
Logger.log(LogLevel.INFO, throwable) { "Info log" }

Logger.logTrace(throwable) { "Trace log" }
Logger.logDebug(throwable) { "Debug log" }
Logger.logInfo(throwable) { "Info log" }
Logger.logWarning(throwable) { "Warning log" }
Logger.logError(throwable) { "Error log" }

For each logged Throwable instance, the SDK adds following fields to emitted log:

  • _error field with the Java exception name (it.javaClass.name) as its value, e.g., java.io.IOException.
  • _error_details field with exception message (it.message) as its value.
Java
Logger.log(LogLevel.INFO, throwable, () -> "Info log");

Logger.logTrace(throwable, () -> "Trace log");
Logger.logDebug(throwable, () -> "Debug log");
Logger.logInfo(throwable, () -> "Info log");
Logger.logWarning(throwable, () -> "Warning log");
Logger.logError(throwable, () -> "Error log");

For each logged Error instance, the SDK adds following fields to emitted log:

  • _error field with a localized description of the error (error.localizedDescription).
  • _error_details field with the description of the error (String(describing: error)).
  • _error_info_X field(s) for each key-value pair present in userInfo dictionary found after casting the passed Error instance to NSError. For example, for a userInfo dictionary like ["foo": "bar"], the SDK emits a field with _error_info_foo key and bar value.
Swift
Logger.log(level: .info, "Info log", error: error)

Logger.logTrace("Trace log", error: error)
Logger.logDebug("Debug log", error: error)
Logger.logInfo("Info log", error: error)
Logger.logWarning("Warning log", error: error)
Logger.logError("Error log", error: error)

Capturing File, Line, and Function

Logs captured using Swift interfaces on iOS include line file (#fileID), line number (#line), and function (#function) information as their captured fields. That can be disabled by passing the nil values for file, line and function arguments explicitly.

Swift
// Logs emitted with file, line, and function information attached as fields.
Logger.log(level: .info, "Info log", fields: ["key": "value"])
Logger.logInfo(mapOf("key" to "value")) { "Info log" }

// Disable capturing of file, line, and function information.
Logger.log(level: .info, "Info log", file: nil, line: nil, function: nil fields: ["key": "value"])
Logger.logInfo(mapOf("key" to "value"), file: nil, line: nil, function: nil) { "Info log" }

Fields

For extra context, every log is accompanied with an optional fields dictionary.

It's encouraged to put changing parts of your log messages inside of fields argument as it makes it simpler to match on such attributes while creating workflows. For example, instead of logging message "Timezone changed from X to Y" log "Timezone changed" message with fields two fields: from and to.

Kotlin
Logger.log(LogLevel.INFO, mapOf("from" to "UTC", "to" to "PST")) { "Timezone changed" }
Java
HashMap fields = new HashMap();
fields.put("from", "UTC");
fields.put("to", "PST");

Logger.log(LogLevel.INFO, fields, () -> "Timezone changed");
Swift
Logger.log(level: .info, "Timezone changed", fields: ["from": "UTC", "to": "PST"])
Objective-C
[CAPLogger logWithLevel:LogLevel.info message:@"Timezone changed" fields: @{@"from": @"UTC", @"to": @"PST"}];
JavaScript
import { info } from '@bitdrift/react-native';

info('Timezone changed', { from: 'UTC', to: "PST" });
JavaScript
import { info } from '@bitdrift/react-native';

info('Timezone changed', { from: 'UTC', to: "PST" });

There are three different ways to attach fields to logs. Below is the list, in order of priority, indicating which methods have precedence over others (fields provided by methods higher in the list override those provided by methods lower in the list):

  • Using the fields argument in a specific log(...) method: This gives the greatest level of control on a per-log basis but has the highest performance impact. Refer to Custom Fields for more details.
  • Using the addField(...) method: This attaches fields to all logs emitted by the SDK after the method call. It is the most performant way to add fields to logs. Refer toField Addition and Removal Methods for more details.
  • Implementing FieldsProvider protocol/interface: This approach is more flexible but less direct. Refer to Field Providers for more details.

Default Fields

Every log emitted by the SDK is tagged with extra out-of-the-box attributes related to the device and app state. Below is the their list:

Field Name Field Key Notes
App Identifier app_id
App Version app_version The value of the versionName attribute of the PackageInfo
App Version Code _app_version_code The value obtained using getLongVersionCode or versionCode accessors.
Carrier carrier
Foreground foreground
Locale _locale The string value of the getLocales method call.
Log Level level
Network Type network_type
Network Quality _network_quality Expose if the app is Offline based on its ability to reach the bitdrift ingest API. The field is not shown if the app is online. Note that any successful network request will identify the app as online
OS os
OS Version os_version The value of RELEASE property.
Radio Type radio_type
Field Name Field Key
App Identifier app_id
App Version app_version The value stored under the CFBundleShortVersionString key of the info dictionary of the main Bundle.
Build Number _build_number The valued stored under the kCFBundleVersionKey key of the infoDictionary property of the main Bundle.
Foreground foreground
Locale _locale The value of the Locale.current.identifier property.
Log Level level
Network Type network_type
Network Quality _network_quality Expose if the app is Offline based on its ability to reach the bitdrift ingest API. The field is not shown if the app is online. Note that any successful network request will identify the app as online
OS os
OS Version os_version The value of systemVersion property.
Radio Type radio_type

React Native supports all the out of the box fields provided by the platform the app is running on, see the iOS and Android sections for more details.

Custom Fields

The SDK allows all but the following field names to be used by the customers of the SDK:

  • Fields with keys that conflict with keys of the default fields emitted by the SDK. This is enforced for all fields and offending fields are dropped (a warning message is printed in the console when that happens).
  • Fields whose names start with "_" (underscore character). This is enforced for global fields and offending fields are dropped (a warning message is printed in the console when that happens). The rule is going to be enforced for fields arguments of custom logs in the future.

Providing Fields with Logging Methods

You can attach arbitrary fields to logs emitted by and with the use of the SDK.

Kotlin
Logger.log(LogLevel.INFO, mapOf("key" to "value")) { "Info log" }

Logger.logTrace(mapOf("key" to "value")) { "Trace log" }
Logger.logDebug(mapOf("key" to "value")) { "Debug log" }
Logger.logInfo(mapOf("key" to "value")) { "Info log" }
Logger.logWarning(mapOf("key" to "value")) { "Warning log" }
Logger.logError(mapOf("key" to "value")) { "Error log" }
Java
Logger.log(LogLevel.INFO, Collections.singletonMap("key", "value"), () -> "Info log");

Logger.logTrace(Collections.singletonMap("key", "value"), () -> "Trace log");
Logger.logDebug(Collections.singletonMap("key", "value"), () -> "Debug log");
Logger.logInfo(Collections.singletonMap("key", "value"), () -> "Info log");
Logger.logWarning(Collections.singletonMap("key", "value"), () -> "Warning log");
Logger.logError(Collections.singletonMap("key", "value"), () -> "Error log");
Swift
Logger.log(level: .info, "Info log", fields: ["key": "value"])

Logger.logTrace("Trace log", fields: ["key": "value"])
Logger.logDebug("Debug log", fields: ["key": "value"])
Logger.logInfo("Info log", fields: ["key": "value"])
Logger.logWarning("Warning log", fields: ["key": "value"])
Logger.logError("Error log", fields: ["key": "value"])
Objective-C
[CAPLogger logWithLevel:LogLevel.info message:@"Info log" fields: @{@"key": @"value"}];

[CAPLogger logTrace:@"Trace log" fields:@{@"key": @"value"}];
[CAPLogger logDebug:@"Debug log" fields:@{@"key": @"value"}];
[CAPLogger logInfo:@"Info log" fields:@{@"key": @"value"}];
[CAPLogger logWarning:@"Warning log" fields:@{@"key": @"value"}];
[CAPLogger logError:@"Error log" fields:@{@"key": @"value"}];
JavaScript
import { info } from '@bitdrift/react-native';

// Log line with LogLevel of Trace
trace('Hello world!', { key: 'value' });

debug('Hello world!', { key: 'value' });
info('Hello world!', { key: 'value' });
warning('Hello world!', { key: 'value' });
error('Hello world!', { key: 'value' });
JavaScript
import { info } from '@bitdrift/react-native';

// Log line with LogLevel of Trace
trace('Hello world!', { key: 'value' });

debug('Hello world!', { key: 'value' });
info('Hello world!', { key: 'value' });
warning('Hello world!', { key: 'value' });
error('Hello world!', { key: 'value' });

Global Fields

The SDK supports two ways of adding log fields to attach to every emitted log. Each of these methods manages a separate pool of logs, ensuring that the method of managing fields does not impact those managed by the other method.

Note

Global fields are not persisted to disk and thus do not survive process restarts, so it is possible for these values to change within a session depending on your session management strategy.

Field Addition and Removal Methods

The logger exposes addField(...) and removeField(...) methods, which can be used to control the list of global fields added to emitted logs.

Kotlin
Logger.addField("key", "value")
Logger.removeField("key")
Java
Logger.addField("key", "value");
Logger.removeField("key");
Swift
Logger.addField(withKey: "key", value: "value")
Logger.removeField(withKey: "key")
JavaScript
import { addField, removeField } from '@bitdrift/react-native';

addField('key', 'value');
removeField('key');
Field Providers

The start method takes an optional list of fieldProviders where you can implement your own logic for retrieving the data you want to pass.

In cases of key conflicts (where two or more fields share the same key), the earlier a field provider appears in the list of registered providers, the higher priority the field it returns is given.

Kotlin
// Attach your own custom user_id field to each log
class CustomUserIdProvider : FieldProvider {
  override fun invoke(): Fields {
    return mapOf("user_id" to "<your_custom_user_id>")
  }
}

Logger.start(
  // ...
  fieldProviders = listOf(CustomUserIdProvider()),
)
Java
// Attach your own custom user_id field to each log
class CustomUserIdProvider implements FieldProvider {
  @Override
  public Map<String, ? extends String> invoke() {
    return Collections.singletonMap("user_id", "<your_custom_user_id>");
  }
}

Logger.start(
  // ...
  Collections.singletonList(new CustomUserIdProvider()) //fieldProviders
);
Swift
// Attach your own custom user_id field to each log
final class CustomUserIdProvider: FieldProvider {
  func getFields() -> Fields {
    ["user_id": "<your_custom_user_id>"]
  }
}

Logger.start(
  // ...
  fieldProviders: [CustomUserIdProvider()],
)

Tip

user_id is a one-of-a-kind special field. Providing it as shown above will cause it to appear in the Timeline header.

HTTP Traffic Logs

The SDK offers specialized logging APIs for capturing information about network requests. Exposed in the form of log method calls the APIs can be used to manually log information about each request and response.

Tip

The recommended way to add Capture logs for network traffic in an app is through Capture Networking Integrations integrations.

Kotlin
val requestInfo = HttpRequestInfo(
    host = "<endpoint_host>",
    path = HttpUrlPath("<endpoint_path>", "<endpoint_path_template>"),
    method = "<http_request_method>",
    headers = emptyMap(),
)
Logger.log(requestInfo)

val responseInfo = HttpResponseInfo(
    request = requestInfo,
    response = HttpResponse(
        result = HttpResponse.HttpResult.SUCCESS,
        statusCode = 200,
        headers = emptyMap(),
    ),
    durationMs = 500,
    metrics = HTTPRequestMetrics(
        requestBodyBytesSentCount = 1,
        responseBodyBytesReceivedCount = 2,
        requestHeadersBytesCount = 3,
        responseHeadersBytesCount = 3,
        dnsResolutionDurationMs = 1234,
    ),
)
Logger.log(responseInfo)
Java
HttpRequestInfo requestInfo = new HttpRequestInfo(
  "<endpoint_host>", //host
  HttpUrlPath("<endpoint_path>", "<endpoint_path_template>"), //path
  "<http_request_method>" //method
  Collections.emptyMap(), // HTTP headers
);
Logger.log(requestInfo);

HttpResponseInfo responseInfo = new HttpResponseInfo(
  requestInfo, //request
  new HttpResponse(
    HttpResponse.HttpResult.SUCCESS, //result
    200 //statusCode,
    Collections.emptyMap(), // HTTP headers
  ),
  500, //durationMs
  new HttpRequestMetrics(
    1, // requestBodyBytesSentCount
    2, // responseBodyBytesReceivedCount
    3, // requestHeadersBytesCount
    4, // responseHeadersBytesCount
    1234 // dnsResolutionDurationMs
  )
);
Logger.log(responseInfo);
Swift
let requestInfo = HTTPRequestInfo(
    host: "<endpoint_host>",
    path: HTTPURLPath(value: "<endpoint_path>", template: "endpoint_path_template"),
    method: "<http_request_method>",
    headers: [:],
)
Logger.log(requestInfo)

let responseInfo = HTTPResponseInfo(
    requestInfo: requestInfo,
    response: .init(
      result: .success,
      statusCode: 200,
      headers: [:],
      error: nil,
    ),
    duration: 0.5,
    metrics: .init(
      requestBodyBytesSentCount: 1,
      responseBodyBytesReceivedCount: 2,
      requestHeadersBytesCount: 3,
      responseHeadersBytesCount: 4,
      dnsResolutionDuration: 1.23
    )
)
Logger.log(responseInfo)

Fields

HTTP logs emitted with the use of HttpRequestInfo (Android) / HTTPRequestInfo (iOS) and HttpResponseInfo (Android) / HTTPResponseInfo (iOS) types contain multiple HTTP-specific fields outlined below.

The description of the specific fields discusses the intended values of these fields and how the Capture OkHttp (Android) and URLSession (iOS) integrations use these fields.

Request and response info objects can be manually initialized and logged by a customer of the Capture SDK. This flexibility means that depending on the values of the parameters passed to the initializers of these objects, the final values of the outlined fields may differ from what is described below.

When logging network traffic manually, reuse the same instance of HttpRequestInfo (Android) or HTTPRequestInfo (iOS) for logging both request and response information.

HTTP Request Fields

Field Name Field Key Example Values Notes
Method _method GET, POST
Host _host bitdrift.io
Path _path /v1/ping/123
Path Template _path_template /v1/ping/<id> A version of path with high-cardinality portions (if any) replaced with a a string placeholder (e.g., <id>). If a path template is not provided at the time of initialization, the SDK tries to find and replace high-cardinality portions of the path with the <id> placeholder on its own.
Protocol _protocol http/1.0, http/1.1, h2 The HTTP Protocol of the Request.
Query _query q=foo&source=bar
Request Body Size (Expected) _request_body_bytes_expected_to_send_count 123 The number of body bytes expected to be sent for a given request. On Android, it does not take into account any body payload modifications performed by the Interceptor chain (e.g. compression).
Span ID _span_id 8bcbbef6-7b3a-44c7-8c8e-47c5c15f2412 Each request-response pair shares the same span ID value.

HTTP Response Fields

Field Name Field Key Example Values Notes
Init Duration _fetch_init_duration_ms 55 Client time overhead before connecting.
TCP Duration _tcp_duration_ms 97 The cumulative duration of all TCP handshakes performed during the execution of a given HTTP request.
TLS Duration _tls_duration_ms 102 The cumulative duration of all TLS handshakes performed during the execution of a given HTTP request.
DNS Duration _dns_duration_ms 223 The duration of time the DNS query(ies) for a given request took. Present only on response logs for requests that triggered DNS lookups, as opposed to those that used previously cached DNS results (a common case).
Response Latency _response_latency_ms 865 The cumulative duration of all responses from the time the request is sent to the time we get the first byte from the server.
Duration _duration_ms 1342 The total roundtrip duration of a request / response pair.
Error Code _error_code -1009 Presents a code for client-side errors in cases where a request fails due to issues such as timeout or cancellation.
Error Message _error_message The Internet connection appears to be offline.
Host _host bitdrift.io
Method _method GET, POST
Path _path /v1/ping/123
Path Template _path_template /v1/ping/<id> A version of path with high-cardinality portions (if any) replaced with a a string placeholder (e.g., <id>). If a path template is not provided at the time of initialization, the SDK tries to find and replace high-cardinality portions of the path with the <id> placeholder on its own.
Protocol _protocol http/1.0, http/1.1, h2 The HTTP Protocol of the Response.
Query _query q=foo&source=bar
Request Body Size (Sent) _request_body_bytes_sent_count 1234 The number of request body bytes sent over-the-wire. It takes into account compression if one is used by the app.
Request Headers Size _request_headers_bytes_count 1234 The number of request headers bytes sent. This represents the over-the-wire bytes on iOS and an estimate of the over-the-wire bytes before they are compressed with HPACK on Android.
Response Body Size _response_body_bytes_received_count 1234 The number of response body bytes received over-the-wire.
Response Headers Size _response_headers_bytes_count 1234 The number of response headers bytes sent. This represents the over-the-wire bytes on iOS and an estimate of the over-the-wire bytes before they are compressed with HPACK on Android.
Result _result success, failure, canceled
Span ID _span_id 8bcbbef6-7b3a-44c7-8c8e-47c5c15f2412 Each request-response pair shares the same span ID value.
Status Code _status_code 200, 401 Present only if client receives a response from a server.

Spans

A span represents a unit of work with defined start and end logs. Spans are useful when instrumenting blocks of time, e.g., "How long was a loading spinner displayed?"

Kotlin
val span = Logger.startSpan(
  "loading_spinner",
  LogLevel.INFO
  mapOf(),
)

// ...

span?.end(SpanResult.SUCCESS, mapOf())

// or:

Logger.trackSpan("operation", LogLevel.INFO) {
  operation()
}
Java
Optional<Span> span = Logger.startSpan(
  "loading_spinner",
  LogLevel.INFO,
  new HashMap(),
)

// ...

span.ifPresent(s -> s.end(SpanResult.SUCCESS, new HashMap()))
Swift
let span = Logger.startSpan(
  name: "loading_spinner",
  level: .info,
  fields: [:]
)

// ...

span?.end(.success, fields: [:])

The SDK calculates and attaches information about the span duration each time a span ends.

Info

See the Timeline Waterfalls section for seeing how this information is displayed in the product.

Spans Hierarchy

Spans can be nested hierarchically by providing a parentID to the start call. The parent span ID is the ID of the span that started the child span.

graph TD
  A[Span A] --> B[Span B]
  A --> C[Span C]
  B --> D[Span D]
  C --> E[Span E]

In the example above, Span A is the parent of Spans B and C. Span B is the parent of Span D, and Span C is the parent of Span E.

This is specially useful when spans are rendered in the waterfall chart of the Timeline view.

Custom start & end time (advanced)

The SDK allows you to provide custom start and end times for spans. This is useful when you have a specific time source or timing heuristic that you want to use for your spans. These custom times are also used to display the logs in the correct position in the Timeline view. When providing custom times, you need to provide both the start and end times, failing to provide one of them will result in the span being tracked using system time.

Kotlin
val span = Logger.startSpan(
  "loading_spinner",
  LogLevel.INFO,
  mapOf(),
  startTimeMs = 1234567890000L,
)

// ...

span?.end(
  SpanResult.SUCCESS,
  mapOf(),
  endTimeMs = 1234567900000L,
)
Swift
let span = Logger.startSpan(
  name: "loading_spinner",
  level: .info,
  fields: [:],
  startTimeInterval: 1234567890.0
)
// ...
span?.end(.success, fields: [:], endTimeInterval: 1234567900.0)

Warning

Please note that this is an advanced feature and should be used with caution, as workflows match in the order logs are received, regardless of the provided time.

Fields

Span logs contain multiple out-of-the-box fields outlined below.

Span Start Fields

Field Name Field Key Example Values Notes
Span Parent _span_parent_id foobar-7b3a-44c7-8c8e-47c5c15f2412 Spans can be nested hirarchically. The parent span ID is the ID of the span that started the child span.
Span ID _span_id 8bcbbef6-7b3a-44c7-8c8e-47c5c15f2412 Each span start-end logs pair shares the same span ID value.
Span Name _span_name spinner_loading
Span Type _span_type start

Span Start End

Field Name Field Key Example Values Notes
Duration _duration_ms 123
Result _result success, failure, canceled, unknown The span result
Span Parent _span_parent_id foobar-7b3a-44c7-8c8e-47c5c15f2412 Spans can be nested hirarchically. The parent span ID is the ID of the span that started the child span.
Span ID _span_id 8bcbbef6-7b3a-44c7-8c8e-47c5c15f2412 Each span start-end logs pair shares the same span ID value.
Span Name _span_name spinner_loading
Span Type _span_type end

Date Provider

Capture SDK annotates each of the emitted logs with a timestamp.

By default, the Capture Logger queries the current time using system provided APIs - Date() in Kotlin/Swift.

A custom DateProvider instance can be provided at the Logger start time for cases when another time source is desirable (e.g., you have NTP time source in your app that you want to use for logs emitted by Capture too).

Kotlin
class CustomDateProvider : DateProvider {
  override fun invoke(): Date {
    return Date() // Replace with your own time source.
  }
}

Logger.start(
  // ...
  dateProvider = CustomDateProvider(),
)
Java
class CustomDateProvider implements DateProvider {
  @Override
  public Date invoke() {
    return new Date(); //Replace with your own time source.
  }
}

Logger.start(
  // ...
  new CustomDateProvider() //dateProvider
);
Swift
final class CustomDateProvider: DateProvider {
  func getDate() -> Date {
    return Date()
  }
}

Logger.start(
  // ...
  dateProvider: CustomDateProvider(),
)

SDK Version

Capture SDK exposes a way to query the current version of the SDK at runtime. This is provided as metadata that can be helpful to troubleshoot issues should they arise.

Kotlin
Capture.Logger.sdkVersion
Java
Capture.Logger.getSdkVersion()
Swift
Capture.Logger.sdkVersion
Objective-C
[CAPLogger sdkVersion]

Device

The Capture SDK can be connected to bd tail session through the use of a Device Identifier or Device Code.

Identifier

The device identifier can be obtained using the device identifier property on the Capture Logger. The identifier remains consistent as long as the host application is not reinstalled.

It can be used with the deviceid operator in the bd tail CLI command to stream logs directly from the device in real-time.

Kotlin
Logger.deviceId
Java
Logger.getDeviceId()
Swift
Logger.deviceID
Objective-C
[CAPLogger deviceID]
JavaScript
import { getDeviceId } from '@bitdrift/react-native';

getDeviceId().then((deviceId) => {
  // Use deviceId
}).catch((error) => {
  // Handle error
});

Code

Calling Logger.createTemporaryDeviceCode will asynchronously return a device code that's valid for a limited duration of time (around a day).

It can be used with the devicecode operator in the bd tail CLI command to stream logs directly from the device in real-time.

Kotlin
import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.onSuccess
import io.bitdrift.capture.Capture.Logger

// Result uses https://github.com/michaelbull/kotlin-result
Logger.createTemporaryDeviceCode(completion = { result ->
    result.onSuccess { deviceCode ->
        // Display code
    }
    result.onFailure { error ->
        // Handle error
    }
})
Java
import com.github.michaelbull.result.OnKt;
import io.bitdrift.capture.Capture.Logger;

// Result uses https://github.com/michaelbull/kotlin-result
Logger.createTemporaryDeviceCode(result -> {
    OnKt.onSuccess(result, deviceCode -> {
        // Display code
        return null;
    });
    OnKt.onFailure(result, error -> {
        // Handle error
        return null;
    });
    return null;
});
Swift
Logger.createTemporaryDeviceCode { result in
   switch result {
     case .success(let deviceCode):
        // Display code
     case .failure(let error):
        // Handle error
   }
}
JavaScript
import { generateDeviceCode } from '@bitdrift/react-native';

generateDeviceCode().then((deviceCode) => {
  // Display code
}).catch((error) => {
  // Handle error
});

  1. https://developer.android.com/topic/libraries/app-startup 

  2. https://developer.android.com/topic/performance/jankstats 

  3. https://developer.android.com/topic/performance/vitals/render#jank-relationship